🧱 1단계 – 콘솔에서 혼자 거래를 시작합니다
처음에는 파이썬으로 주식 매매 프로그램을 간단히 만들었습니다.
엑셀 파일에 종목, 매수가, 매도가를 기록하고, 매매 조건에 따라 콘솔에 매수/매도 메시지를 출력합니다.
🔹 사용한 코드:
current_price = get_current_price(kiwoom, code)
print(f"[{name}] 현재가: {current_price} | 매수: {buy_price} | 매도: {sell_price}")
qty = 10 # 10주 매매 예시
if current_price <= buy_price and name not in bought:
print(f"[매수] {name} @ {current_price}")
kiwoom.SendOrder("매수", "1001", account, 1, code, qty, 0, "03", "")
bought.add(name)
sold.discard(name)
elif current_price >= sell_price and name not in sold:
print(f"[매도] {name} @ {current_price}")
kiwoom.SendOrder("매도", "1002", account, 2, code, qty, 0, "03", "")
sold.add(name)
bought.discard(name)
하지만 문제는 매매 내역만 볼 수 있고, 현재 어떤 종목을 가지고 있는지 확인하기가 어렵다는 점입니다.
보유 종목 현황이 없어 불편함을 느낍니다.
💡 2단계 – GUI로 보유 주식을 눈으로 확인하고자 합니다
콘솔 출력만으로는 부족하다는 것을 깨닫고, GUI를 통해 보유 주식을 시각적으로 보여주자는 결심을 합니다.
PyQt를 이용하여 간단한 창과 표를 만드는 것부터 시작합니다.
🪟 3단계 – PyQt5로 테이블 GUI를 구성합니다
PyQt5를 사용하여 종목명, 수량, 평균 매수가를 보여주는 테이블을 생성합니다.
🔹 PyQt 기본 구조:
import sys
import time
import pandas as pd
from datetime import datetime
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QTimer
from pykiwoom.kiwoom import Kiwoom
EXCEL_PATH = "stocks.xlsx"
LOG_FILE = "trade_log.txt"
INTERVAL_MS = 30000 # 30초 주기
class TradingBot(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("\U0001F4C8 Excel 기반 자동 매매 시스템")
self.setGeometry(300, 100, 900, 700)
self.kiwoom = Kiwoom()
self.kiwoom.CommConnect(block=True)
self.account = self.kiwoom.GetLoginInfo("ACCNO")[0]
self.account_pw = ""
self.bought_set = set()
self.sold_set = set()
self.init_ui()
self.get_password()
self.update_holdings()
self.log("시스템 트레이딩 시작됨 ✅")
self.timer = QTimer()
self.timer.timeout.connect(self.run_trading)
self.timer.start(INTERVAL_MS)
def init_ui(self):
self.text_log = QTextEdit()
self.text_log.setReadOnly(True)
self.btn_manual = QPushButton("\U0001F501 수동 실행")
self.btn_manual.clicked.connect(self.run_trading)
self.status_label = QLabel("⏱️ 대기 중...")
self.table = QTableWidget()
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(["종목명", "수량", "매입가", "현재가"])
self.table.horizontalHeader().setStretchLastSection(True)
layout = QVBoxLayout()
layout.addWidget(self.text_log)
layout.addWidget(self.status_label)
layout.addWidget(self.btn_manual)
layout.addWidget(QLabel("\n📦 현재 보유 종목"))
layout.addWidget(self.table)
central = QWidget()
central.setLayout(layout)
self.setCentralWidget(central)
def get_password(self):
pw, ok = QInputDialog.getText(self, "계좌 비밀번호 입력", "계좌 비밀번호를 입력하세요:", QLineEdit.Password)
if ok and pw:
self.account_pw = pw
self.log("🔐 계좌 비밀번호가 등록되었습니다.")
else:
QMessageBox.critical(self, "오류", "비밀번호가 입력되지 않았습니다. 프로그램을 종료합니다.")
sys.exit(1)
def log(self, message):
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_msg = f"[{now}] {message}"
self.text_log.append(log_msg)
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(log_msg + "\n")
def read_excel(self):
return pd.read_excel(EXCEL_PATH)
def name_to_code(self, name):
codes = self.kiwoom.GetCodeListByMarket('0') + self.kiwoom.GetCodeListByMarket('10')
for code in codes:
if self.kiwoom.GetMasterCodeName(code) == name:
return code
return None
def get_current_price(self, code):
price = self.kiwoom.GetMasterLastPrice(code)
if isinstance(price, str):
return int(price.replace(",", ""))
elif isinstance(price, int):
return price
else:
return 0
def run_trading(self):
self.status_label.setText("🔍 주가 확인 중...")
df = self.read_excel()
for _, row in df.iterrows():
name = row['종목명']
buy_price = int(row['매수가'])
sell_price = int(row['매도가'])
code = self.name_to_code(name)
if not code:
self.log(f"[오류] 종목명 {name} → 코드 변환 실패")
continue
current = self.get_current_price(code)
self.log(f"{name} 현재가: {current} / 매수: {buy_price} / 매도: {sell_price}")
qty = 10
if current <= buy_price and name not in self.bought_set:
self.log(f"📥 매수 주문: {name} @ {current}")
self.kiwoom.SendOrder("매수", "1001", self.account, 1, code, qty, 0, "03", "")
self.bought_set.add(name)
self.sold_set.discard(name)
elif current >= sell_price and name not in self.sold_set:
self.log(f"📤 매도 주문: {name} @ {current}")
self.kiwoom.SendOrder("매도", "1002", self.account, 2, code, qty, 0, "03", "")
self.sold_set.add(name)
self.bought_set.discard(name)
self.update_holdings()
self.status_label.setText("⏱️ 대기 중...")
def update_holdings(self):
try:
data = self.kiwoom.block_request("opw00018",
계좌번호=self.account,
비밀번호=self.account_pw,
비밀번호입력매체구분="00",
조회구분=2,
output="계좌평가잔고개별합산",
next=0)
self.log(f"[디버그] 계좌번호: {self.account}")
self.log(f"[디버그] 비밀번호: {'입력됨' if self.account_pw else '미입력'}")
self.log(f"[디버그] opw00018 응답 타입: {type(data)}")
self.log(f"[디버그] opw00018 응답: {data}")
if not isinstance(data, dict) or '종목번호' not in data:
self.log("[경고] 보유 종목 정보가 없습니다.")
self.table.setRowCount(0)
return
codes = data['종목번호']
names = data['종목명']
qtys = data['보유수량']
prices = data['매입가']
self.table.setRowCount(len(codes))
for i in range(len(codes)):
name = names[i].strip()
qty = int(qtys[i]) if str(qtys[i]).strip().isdigit() else 0
buy_price = int(prices[i]) if str(prices[i]).strip().isdigit() else 0
cur_price = self.get_current_price(codes[i].strip())
self.table.setItem(i, 0, QTableWidgetItem(name))
self.table.setItem(i, 1, QTableWidgetItem(str(qty)))
self.table.setItem(i, 2, QTableWidgetItem(str(buy_price)))
self.table.setItem(i, 3, QTableWidgetItem(str(cur_price)))
except Exception as e:
self.log(f"[보유 종목 조회 오류] {str(e)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
bot = TradingBot()
bot.show()
sys.exit(app.exec_())
이제 콘솔이 아닌 GUI 창을 통해 보유 종목을 확인할 수 있게 됩니다.
한눈에 보기 편하고, 시각적으로도 깔끔해서 훨씬 관리가 쉬워집니다.
🏁 4단계 – GUI를 통해 실전 트레이딩을 관리합니다
PyQt로 보유 현황을 시각화한 후, 프로그램의 실용성이 크게 향상되었습니다.
더 이상 콘솔 메시지를 뒤져볼 필요 없이, 어떤 종목을 몇 주나 가지고 있는지, 평단가는 얼마인지 한눈에 확인할 수 있습니다.
트레이딩 과정이 체계적으로 정리되어 개발자이자 트레이더로서의 만족도가 매우 높아졌습니다.
✅ 정리
구분 | 개선 전 | 개선 후 |
데이터 확인 | 콘솔 로그 | GUI 테이블 |
시각화 | 없음 | 실시간 보유 종목 표시 |
사용성 | 불편 | 직관적이고 명확함 |
GUI를 도입한 이 작은 변화는 프로그램의 품질을 한 단계 끌어올렸습니다.
이제 이 주식 트레이더는 콘솔 속에서 혼잣말을 하던 시절을 뒤로하고,
PyQt라는 창을 통해 매매를 직관적이고 명확하게 관리하고 있습니다.
'시스템 트레이딩 > 엑셀 주식 트레이더 1호' 카테고리의 다른 글
엑셀 + 파이썬 미국 주식 트레이더 1.3편 (Qt GUI 보유 주식 표시) (2) | 2025.06.27 |
---|---|
엑셀 + 파이썬 주식 트레이더 1.1편 (Console 로그) (3) | 2025.06.27 |