이제 서버 연결, 매수, 매도에 대해 부분적으로 확인되었으니 전체를 하나의 통합 코드로 만들어보겠습니다.

1) 서버 연결, 매수, 매도 통합 생성

ChatGPT 로 전달한 프롬프트입니다.

좀 복잡한 내용인데 이를 이용하여 코드를 생성해준다니 대단한 것 같습니다.

만들고 나니 좀 민망한 느낌도 들었어요.

뭔가~ 거창하게 코딩할 줄 알았는데 이 것은 처음부터 끝까지  ChatGPT 에 어떻게 프롬프트로 명령을 넣는지만 궁리하면 되었거든요.

그런데 지금 코드를 키움 API, PyQt  메뉴얼을 보고 만든다면 훨씬더 시간이 많이 걸릴 것 같습니다.

아래 코드가 버그가 없다고는 할 수 없습니다.

어디까지나 초기 동작 확인 용 코드라고 할 수 있습니다.

하지만 현제 계좌 정보라든지 다른 기능을 추가할 때 프롬프트에 어떤 추가 기능에 대해서 명시를 하면 되기때문에 기능 추가 시에도 활용도가 높다고 생각됩니다.

장인은 도구를 탓하지 않는다고 했습니다.

현재 가장 효율성이 있는 도구가 있다면 안사용할 이유가 없습니다.

뽀대가 나지 않는다~ 폼이 안 산다는 말은 허망한 외침일 뿐입니다.

무엇이든 현재 사용할 수 있는 도구로 최대한 눈에 보이는 것을 만든 후에 이를 발전 시키는 것도 나쁘지는 않다고 봅니다.

요즘 들어 더더욱 느끼지만 생각이 중요한 것이 아니고 실천이 중요하며

코드 지식이 중요한 것이 아니고 어떤 제품을 만들지 아이디어가 더 중요하고

똑똑한 사람이 성공하는 게 아니라, 꾸준한 사람이 끝내 성공하는 것 같습니다.

 

갑자기 영화 짝패에서 나온 명대사가 생각이 나네요.

"강한놈이 오래가는게 아니라 오래가는 놈이 강한거더라"

내가 지금 얼마나 똑똑한 가 보다 얼마나 더 오래~~ 꾸준히 하는가가  제일 중요한 것 같아요.

 

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 : QLineEdit
name : buyAmount, type : QLineEdit
name : sellPrice, type : QLineEdit
name : sellAmount, type : QLineEdit
name : buyButton, type : QPushButton

다음과 같은 기능을 제공합니다.
1. 키움증권 API 를 연결하고 연결 후 API를 초기화하고 실 서버 연결 시 "실 서버 연결 성공" 이라고 메시지를 logTextEdit 에 출력하고 
모의 투자 서버 연결 시 "모의 투자 서버 연결 성공"이라고 메시지를 logTextEdit 에 출력
2. Kiwoom 계좌 정보를 accountComboBox 에 출력
3. orderType라는 이름을 가진 ComboBox 로 시장가, 지정가 선택이 있습니다.
4. orderType을 시장가 선택하고 buyButton 을 클릭하면  키움증권 api 라이브러리인 pykiwoom 을 사용하여 stockCode 에 입력된 주식코드로 시장가로 buyAmount 만큼 매수 주문을 합니다.
매수 주문이 실행되면 logTextEdit 에 stockCode 의 종목명을 확인하여 어떤 종목이 시장가로 얼마의 양이 매수 주문되었습니다 라고 표시합니다.
5. orderType을 지정가 선택하고 buyButton 을 클릭하면 키움증권 api 라이브러리인 pykiwoom 을 사용하여 stockCode 에 입력된 주식코드로  buyPrice 에 입력된 금액으로 buyAmount 개수만큼 매수 주문을 합니다.
매수 주문이 실행되면 logTextEdit 에 stockCode 의 종목명을 확인하여 어떤 종목이 buyPrice 가격으로 얼마의 양이 매수 주문되었습니다 라고 표시합니다.
6. 매수가 체결되면 어떤 종목명이 얼마의 가격으로 얼마의 양만큼 매수되었다고 표시합니다.
7. 매수가 체결되었다는 이벤트를 받으면 sellPrice 가격으로 sellAmount 개수만큼 매도 주문을 보냅니다.
매도 주문이 실행되면 logTextEdit 에 stockCode 의 종목명을 확인하여 어떤 종목이 sellPrice 가격으로 얼마의 양이 매도 주문되었습니다 라고 표시합니다.
8. 매도 주문이 체결되면 어떤 종목이 얼마의 가격으로 얼마의 양만큼 매도 체결이 되었다 라고 표시합니다.

파이썬 코드로 작성해주세요. 주석도 자세히 달아주세요.

 

생성된 코드로 실제 생성 코드를 돌려보니 실행 시 에러가 있어서 버그 수정을 한 코드입니다.

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

git clone https://github.com/jbpark/JbTraderExample.git
cd JbTraderExample/jbtrader/ch5.7
import sys
from PyQt5 import uic, QtWidgets
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import QTimer
from pykiwoom.kiwoom import Kiwoom
import traceback
import time


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

        self.kiwoom = Kiwoom()  # Kiwoom API 객체 생성
        self.kiwoom.CommConnect(block=True)  # API 로그인 (블록킹 방식)

        # 기본 Text
        # 테스트 종목 코드 기본 설정
        # 종목 코드 : 삼성전자
        if hasattr(self, "stockCode"):
            self.stockCode.setText("005930")

        # 매수 수량 : 1
        if hasattr(self, "buyAmount"):
            self.buyAmount.setText("1")

        # orderType 초기 설정
        self.set_order_type()

        # 서버 연결 상태 확인 후 로그 출력
        self.check_server_connection()

        # 계좌 정보 가져오기
        self.get_account_info()

        # TR 수신 이벤트
        self.kiwoom.OnReceiveTrData = self.receive_tr_data

        # 체결 이벤트 처리
        self.kiwoom.OnReceiveChejanData = self.receive_chejan_data

        # 실시간 데이터 수신 이벤트
        self.kiwoom.OnReceiveRealData = self.receive_real_data

        # 수신 메시지 이벤트
        self.kiwoom.OnReceiveMsg = self.receive_msg

        # 조건식 목록 요청에 대한 응답 이벤트
        self.kiwoom.OnReceiveConditionVer = self.receive_condition_ver

        # (1회성, 실시간) 종목 조건검색 요청시 발생되는 이벤트
        self.kiwoom.OnReceiveTrCondition = self.receive_tr_condition

        # 실시간 종목 조건검색 요청시 발생되는 이벤트
        self.kiwoom.OnReceiveRealCondition = self.receive_real_condition
        self.kiwoom._set_signals_slots()

        # 주문 버튼 이벤트 설정
        self.buyButton.clicked.connect(self.process_order)

    def addLog(self, *args):
        print(args)
        output = " ".join(map(str, args))
        self.logTextEdit.append(output)

    def set_order_type(self):
        # orderType 초기 설정
        self.orderType.addItems(["시장가", "지정가"])
        self.orderType.setCurrentText("시장가")

    def receive_real_condition(self, code, event, conditionName, conditionIndex):
        """
        실시간 종목 조건검색 요청시 발생되는 이벤트

        :param code: string - 종목코드
        :param event: string - 이벤트종류("I": 종목편입, "D": 종목이탈)
        :param conditionName: string - 조건식 이름
        :param conditionIndex: string - 조건식 인덱스(여기서만 인덱스가 string 타입으로 전달됨)
        """

        self.addLog("[receiveRealCondition]")

        self.addLog("종목코드: ", code)
        self.addLog("이벤트: ", "종목편입" if event == "I" else "종목이탈")

    def receive_tr_condition(self, screenNo, codes, conditionName, conditionIndex, inquiry):
        """
        (1회성, 실시간) 종목 조건검색 요청시 발생되는 이벤트

        :param screenNo: string
        :param codes: string - 종목코드 목록(각 종목은 세미콜론으로 구분됨)
        :param conditionName: string - 조건식 이름
        :param conditionIndex: int - 조건식 인덱스
        :param inquiry: int - 조회구분(0: 남은데이터 없음, 2: 남은데이터 있음)
        """

        self.addLog("[receiveTrCondition]")

    def receive_condition_ver(self, receive, msg):
        """
        getConditionLoad() 메서드의 조건식 목록 요청에 대한 응답 이벤트

        :param receive: int - 응답결과(1: 성공, 나머지 실패)
        :param msg: string - 메세지
        """

        try:
            if not receive:
                return

            self.condition = self.getConditionNameList()
            self.addLog("조건식 개수: ", len(self.condition))

            for key in self.condition.keys():
                self.addLog("조건식: ", key, ": ", self.condition[key])
                self.addLog("key type: ", type(key))

        except Exception as e:
            self.addLog(e)

        finally:
            self.conditionLoop.exit()

    def receive_msg(self, screenNo, requestName, trCode, msg):
        """
        수신 메시지 이벤트

        서버로 어떤 요청을 했을 때(로그인, 주문, 조회 등), 그 요청에 대한 처리내용을 전달해준다.

        :param screenNo: string - 화면번호(4자리, 사용자 정의, 서버에 조회나 주문을 요청할 때 이 요청을 구별하기 위한 키값)
        :param requestName: string - TR 요청명(사용자 정의)
        :param trCode: string
        :param msg: string - 서버로 부터의 메시지
        """

        self.addLog(f"receive_msg :: msg={msg}")

    def receive_real_data(self, code, realType, realData):
        """
        실시간 데이터 수신 이벤트

        실시간 데이터를 수신할 때 마다 호출되며,
        setRealReg() 메서드로 등록한 실시간 데이터도 이 이벤트 메서드에 전달됩니다.
        getCommRealData() 메서드를 이용해서 실시간 데이터를 얻을 수 있습니다.

        :param code: string - 종목코드
        :param realType: string - 실시간 타입(KOA의 실시간 목록 참조)
        :param realData: string - 실시간 데이터 전문
        """

        try:
            self.addLog("[receiveRealData]")
            self.addLog("({})".format(realType))

        except Exception as e:
            self.addLog('{}'.format(e))

    def receive_tr_data(self, screenNo, requestName, trCode, recordName, inquiry,
                      deprecated1, deprecated2, deprecated3, deprecated4):
        """
        TR 수신 이벤트

        조회요청 응답을 받거나 조회데이터를 수신했을 때 호출됩니다.
        requestName과 trCode는 commRqData()메소드의 매개변수와 매핑되는 값 입니다.
        조회데이터는 이 이벤트 메서드 내부에서 getCommData() 메서드를 이용해서 얻을 수 있습니다.

        :param screenNo: string - 화면번호(4자리)
        :param requestName: string - TR 요청명(commRqData() 메소드 호출시 사용된 requestName)
        :param trCode: string
        :param recordName: string
        :param inquiry: string - 조회('0': 남은 데이터 없음, '2': 남은 데이터 있음)
        """

        self.addLog("receiveTrData 실행: ", screenNo, requestName, trCode, recordName, inquiry)

    def check_server_connection(self):
        """키움증권 서버 연결 상태 확인 후 로그 출력"""
        server = self.kiwoom.GetLoginInfo("GetServerGubun")
        if server == "1":
            self.addLog("모의 투자 서버 연결 성공")
        else:
            self.addLog("실 서버 연결 성공")

    def get_account_info(self):
        """로그인한 계좌 정보를 가져와 ComboBox에 추가"""
        accounts = self.kiwoom.GetLoginInfo("ACCNO")
        self.accountComboBox.addItems(accounts)

    def process_order(self):
        """orderType 선택에 따라 매수 주문을 실행"""
        order_type = self.orderType.currentText()
        stock_code = self.stockCode.text()
        buy_price = self.buyPrice.text()
        buy_amount = self.buyAmount.text()
        sell_price = self.sellPrice.text()
        sell_amount = self.sellAmount.text()
        account = self.accountComboBox.currentText()

        if not stock_code:
            QMessageBox.warning(self, "입력 오류", "종목 코드를 입력하세요.")
            self.addLog("종목 코드를 입력하세요.")
            return

        if not order_type:
            QMessageBox.warning(self, "입력 오류", "주문 종류를 선택하세요.")
            self.addLog("주문 종류를 선택하세요.")
            return

        if order_type == "지정가" and not buy_price:
            QMessageBox.warning(self, "입력 오류", "매수 금액을 입력하세요.")
            self.addLog("매수 금액을 입력하세요.")
            return

        if not buy_amount:
            QMessageBox.warning(self, "입력 오류", "매수 수량을 입력하세요.")
            self.addLog("매수 수량을 입력하세요.")
            return

        if order_type == "시장가":
            order_type_code = 1  # 시장가 매수 주문 코드
            price = 0  # 시장가는 가격 입력 없이 0으로 설정
            msg = f"{stock_code} 시장가로 {buy_amount}주 매수 주문 실행"
        elif order_type == "지정가":
            if not buy_price:
                self.addLog("매수 가격을 입력하세요.")
                return
            order_type_code = 0  # 지정가 매수 주문 코드
            price = int(buy_price)
            msg = f"{stock_code} {buy_price}원으로 {buy_amount}주 매수 주문 실행"
        else:
            return

        self.addLog(msg)

        try :
            result = self.kiwoom.SendOrder("매수주문", "0101", account, order_type_code, stock_code, int(buy_amount), price, "00", "")
            if result != 0:
                raise Exception(f"SendOrder 실패! 리턴값: {result}")

            self.addLog("✅ SendOrder 성공!")
        except Exception as e:
            self.addLog("❌ 예외 발생!")
            self.addLog(f"에러 메시지: {str(e)}")
            self.addLog("🔹 상세 오류 정보:")
            traceback.print_exc()  # 전체 스택 트레이스 출력


    def receive_chejan_data(self, gubun, item_cnt, fid_list):
        print(f"receive_chejan_data :: gubun:{gubun}")

        """체결 이벤트가 발생했을 때 실행되는 함수"""
        if gubun == "0":  # 매수 체결
            stock_code = self.kiwoom.GetChejanData(9001).strip()
            stock_name = self.kiwoom.GetMasterCodeName(stock_code)
            price = self.kiwoom.GetChejanData(910).strip()
            amount = self.kiwoom.GetChejanData(900).strip()
            self.addLog(f"{stock_name} {price}원으로 {amount}주 매수 체결 완료")

            # 매수 체결 후 매도 주문 실행
            sell_price = self.sellPrice.text()
            sell_amount = self.sellAmount.text()
            account = self.accountComboBox.currentText()
            self.sell_order(stock_code, sell_price, sell_amount, account)
        elif gubun == "1":  # 매도 체결
            stock_code = self.kiwoom.GetChejanData(9001).strip()
            stock_name = self.kiwoom.GetMasterCodeName(stock_code)
            price = self.kiwoom.GetChejanData(910).strip()
            amount = self.kiwoom.GetChejanData(900).strip()
            self.addLog(f"{stock_name} {price}원으로 {amount}주 매도 체결 완료")

    def sell_order(self, stock_code, sell_price, sell_amount, account):
        """매수 체결 후 매도 주문 실행"""
        if not sell_price or not sell_amount:
            self.addLog("매도가격과 매도 수량을 입력하세요.")
            return

        order_type_code = 1  # 시장가 매도
        msg = f"{stock_code} {sell_price}원으로 {sell_amount}주 매도 주문 실행"

        self.kiwoom.SendOrder("매도주문", "0101", account, order_type_code, stock_code, int(sell_amount), int(sell_price),
                              "00", "")
        self.addLog(msg)


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

 

guruma_one.ui 파일 변경

아래 형식에 맞게 변경을 해야 합니다.

name : accountComboBox, type : QComboBox
name : logTextEdit, type : QTextEdit
name : stockCode , type : QLineEdit
name : orderType, type : QComboBox
name : buyPrice, type : QLineEdit
name : buyAmount, type : QLineEdit
name : sellPrice, type : QLineEdit
name : sellAmount, type : QLineEdit

Object 를 object name, object type 에 맞추어 변경해주어야 합니다.

2) 통합 코드 실행

실제 매매 보다는 모의투자접속으로 테스트 해주세요

모의투자 접속을 체크하세요.

Open API 접속 후 Windows 우측 하단에서 아래 Open API 서버 연결 아이콘에서 마우스 우측 클릭 후 계좌 비밀번호 저장을 선택합니다.

계좌비밀번호 저장 선택

계좌 비밀번호를 입력하고 등록을 클릭하고 AUTO(자동 로그인) 을 선택합니다.

 

만약 계좌비밀번호가 저장되지 않은 상태에서 주문이 실행되면 아래와 같은 오류가 발생합니다.

실행하면 로그인 화면이 나오고 해당 로그인 화면 후에 표시되는 화면입니다.

Posted by 제이브레인
,