시스템 트레이딩/JbTrader 1호

5.10편-매수/매 가격에 현재 가격 및 상하 호가 선택 기능 추가

제이브레인 2025. 3. 19. 20:37

시장가로 매수하거나 지정가로 매수하려고 할 때 현재 가격이 표시되어 있지 않고 입력만 하도록 되어 있어서 잘못 입력할 수도 있고 가격을 다른 곳에서 확인 후에 입력해야 해서 많이 불편하네요.

그래서 매수/매도 가격에 현재가와 상하호가를 입력하여 직관적으로 선택하도록 기능추가하려고 합니다.

1) 코드 생성

ChatGPT 에 사용한 프롬프트입니다.

매수/매도 가격에 현재가를 기준으로 상하호가를 계산하여 입력하도록 했습니다.

그리고 5초에 한번 자동 업데이트하게 했고 만약 사용자 매수/매도 가격을 선택하면 자동 업데이트를 멈추게했습니다.

python pyqt5 designer 로 guruma_one.ui 를 만들었습니다.

guruma_one.ui 에는 다음과 같은 콤포넌트들이 존재합니다.
name : accountComboBox, type : QComboBox
name : logTextEdit, type : QTextEdit
name : stockCode , type : QLineEdit
name : orderType, type : QComboBox
name : buyPrice, type : QComboBox
name : buyAmount, type : QLineEdit
name : sellPrice, type : QComboBox
name : sellAmount, type : QLineEdit
name : buyButton, type : QPushButton

다음과 같은 기능을 제공합니다.
1. 키움증권 API 를 연결하고 연결 후 API를 초기화하고 실 서버 연결 시 "실 서버 연결 성공" 이라고 메시지를 logTextEdit 에 출력하고 
모의 투자 서버 연결 시 "모의 투자 서버 연결 성공"이라고 메시지를 logTextEdit 에 출력
2. Kiwoom 계좌 정보를 accountComboBox 에 출력
3. stockCode 에 종목 코드를 입력하면 호가 단위를 현재 종목코드의 현재가를 기준으로 계산
4. buyPrice 에 -20호가 부터 +20호가까지 추가합니다. 
QSpinBox 의 각 item 은 -+호가 : 가격을 표시하고 현재가격은 현재가 : 가격을 표시합니다.
기본값은 현재가를 선택합니다.
5. sellPrice 에도 buyPrice와 동일한 값을 선택합니다.
기본값은 +1호가를 선택합니다.
6. 5초에 한번씩 가격을 업데이트하고 만약 buyPrice 또는 sellPrice 의 가격을 선택하면 업데이트를 멈춥니다.

파이썬 코드로 작성해주세요. 주석도 자세히 달아주세요. 코드는 오류 테스트 후에 오류가 있으면 수정해주세요.

 

Qt Designer 에서 buyPrice, sellPrice 를 QLineEdit -> QComboBox 로 변경합니다.

2) 생성된 코드

생성된 코드이며 일부 버그는 수정하였습니다.

아래 코드는 다음 위치에서 다운 받는 것이 가능합니다.

git clone https://github.com/jbpark/JbTraderExample.git
cd JbTraderExample/jbtrader/ch5.10

price_update.py

import sys

from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.uic import loadUi
from pykiwoom.kiwoom import Kiwoom


def get_cell_value_or_error(df, row_idx, col_name):
    try:
        return df.at[row_idx, col_name]  # 특정 위치의 값 반환
    except KeyError:
        raise ValueError(f"Invalid index '{row_idx}' or column '{col_name}'")


class GurumaApp(QMainWindow):
    def __init__(self):
        super().__init__()
        loadUi("guruma_one.ui", self)  # UI 파일 로드

        self.kiwoom = Kiwoom()  # Kiwoom 인스턴스 생성
        self.kiwoom.CommConnect()  # API 연결

        self.kiwoom.OnEventConnect = self.event_connect  # 연결 이벤트 핸들러

        # 버튼 클릭 이벤트 연결
        self.buyButton.clicked.connect(self.update_stock_price)

        # 주기적으로 가격 업데이트를 위한 타이머 설정
        self.price_update_timer = QTimer(self)
        self.price_update_timer.timeout.connect(self.update_stock_price)
        self.price_update_timer.start(5000)  # 5초마다 실행

        self.buyPrice.installEventFilter(self)
        self.sellPrice.installEventFilter(self)

    def eventFilter(self, obj, event):
        if (obj == self.buyPrice or obj == self.sellPrice) and event.type() == event.MouseButtonPress:
            self.on_combobox_clicked()
        return super().eventFilter(obj, event)

    def on_combobox_clicked(self):
        print("QComboBox가 클릭되었습니다!")  # 특정 함수 실행
        self.stop_price_update()

    def event_connect(self, err_code):
        """
        키움증권 API 연결 이벤트 처리
        """
        if err_code == 0:
            server_type = self.kiwoom.GetLoginInfo("GetServerGubun")
            if server_type == "1":
                self.logTextEdit.append("모의 투자 서버 연결 성공")
            else:
                self.logTextEdit.append("실 서버 연결 성공")
            self.load_accounts()
        else:
            self.logTextEdit.append("API 연결 실패")

    def load_accounts(self):
        """
        계좌 정보를 가져와서 accountComboBox에 출력
        """
        accounts = self.kiwoom.GetLoginInfo("ACCNO").split(';')
        self.accountComboBox.addItems([acc for acc in accounts if acc])

    def update_stock_price(self):
        """
        stockCode 입력 시 현재 가격 및 상하 몇 호가 가격을 buyPrice, sellPrice에 반영
        """
        stock_code = self.stockCode.text().strip()
        if not stock_code:
            self.logTextEdit.append("종목 코드를 입력하세요.")
            return

        self.kiwoom.SetInputValue("종목코드", stock_code)
        self.kiwoom.CommRqData("주식기본정보", "opt10001", 0, "0101")
        QTimer.singleShot(5000, self.process_stock_price)

    def get_price_tick(self, price):
        """
        현재가를 기반으로 호가 단위를 계산하는 함수
        """
        if price < 1000:
            return 1
        elif price < 5000:
            return 5
        elif price < 10000:
            return 10
        elif price < 50000:
            return 50
        elif price < 100000:
            return 100
        elif price < 500000:
            return 500
        else:
            return 1000

    def process_stock_price(self):
        """
        조회한 주식 정보를 buyPrice와 sellPrice에 반영
        """
        """
                조회한 주식 정보를 buyPrice와 sellPrice에 반영
                """

        stock_code = self.stockCode.text().strip()
        if not stock_code:
            self.logTextEdit.append("종목 코드를 입력하세요.")
            return

        df = self.kiwoom.block_request("opt10001",
                                       종목코드=stock_code,
                                       output="주식기본정보",
                                       next=0)

        current_price_str = get_cell_value_or_error(df, 0, '현재가')
        if not current_price_str:
            self.logTextEdit.append("현재가 정보를 가져올 수 없습니다.")
            return

        current_price_str = str(int(current_price_str))

        try:
            current_price = abs(int(current_price_str))
        except ValueError:
            self.logTextEdit.append("현재가 변환 오류: {current_price_str}")
            return

        price_tick = self.get_price_tick(current_price)

        self.buyPrice.clear()
        self.sellPrice.clear()

        for i in range(-20, 21):  # -20호가부터 +20호가까지 추가
            price = current_price + i * price_tick
            label = f"{i}호가: {price}" if i != 0 else f"현재가: {price}"
            self.buyPrice.addItem(label, price)
            self.sellPrice.addItem(label, price)

        # 기본값 설정: buyPrice는 현재가, sellPrice는 +1호가
        self.buyPrice.setCurrentIndex(20)
        self.sellPrice.setCurrentIndex(21)

        self.logTextEdit.append(f"현재가 {current_price}원부터 ±20호가까지 설정 완료")

    def stop_price_update(self):
        """
        사용자가 가격을 선택하면 타이머 정지
        """
        print("stop_price_update")
        self.price_update_timer.stop()
        self.logTextEdit.append("가격 선택 완료. 자동 업데이트 중지")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = GurumaApp()
    window.show()
    sys.exit(app.exec_())

3) 코드 실행

종목 코드에 삼성전자를 입력하고 실행 시 아래와 같이 매수 금액이 표시되는 것을 확인 할 수 있습니다.

매도 금액은 현재가 보다 1호가 위를 기본 선택하도록 했습니다.

 

4) JbTrader 에 가격 정보 업데이트 통합

위 가격 정보를 표시하는 기능을 JbTrader 에 통합시켰습니다.

통합된 코드는 다음 위치에 있습니다.

git clone https://github.com/jbpark/JbTraderExample.git
cat JbTraderExample/jbtrader/ch5.10/guruma_one.py

통합 후에 실행한 모습니다.

뭔가~ 그럴 듯한 모습을 갖춰가는 것 같지 않나요?

모의 계좌이긴한데 삼성전자를 사두었었는데 수익률이 1.32%네요.

실계좌도 빨리 수익이 났으면 좋겠어요.