실행된 JbTrader Main Window 를 보면 폰트에 따라 글씨가 잘리기도 합니다.
뭔가~~ 완성도가 떨어지는 것으로 보이네요.
폰트는 시스템마다 약간씩 다르므로 제대로 설정하지 않으면 글씨가 잘리거나 너무 작게 표시될 수 있습니다.
1) 폰트 다운로드
폰트 중에 무료 글꼴인 나눔 글꼴을 다운로드 받아 보겠습니다.
나눔글꼴 저작권을 검색하니 다음과 같다고 합니다.
참고하세요.
네이버 나눔글꼴의 지적 재산권은 네이버와 네이버문화재단에 있습니다. 네이버 나눔글꼴은 개인 및 기업 사용자를 포함한 모든 사용자에게 무료로 제공되며 자유롭게 수정하고 재배포하실 수 있습니다. 단, 글꼴 자체를 유료로 판매하는 것은 금지하며 네이버 나눔글꼴은 본 저작권 안내와 라이선스 전문을 포함해서 다른 소프트웨어와 번들하거나 재배포 또는 판매가 가능합니다.
naver.com 에서 나눔 고딕 검색합니다.
한글한글 아름답게를 선택합니다.
글꼴모음을 클릭합니다.
나눔글꼴 전체 내려받기를 클릭합니다.
2) 폰트 설치
나눔고딕은 압축 파일을 풀고 nanum-all\나눔 글꼴\나눔고딕\NanumFontSetup_TTF_GOTHIC\ 폴더의 ttf 파일을 아래 경로로 복사하면 됩니다.
C:\Windows\Fonts\
3) Qt Designer 에 폰트 적용
Qt Designer 실행 후 폰트를 모두 적용합니다.
아래 코드는 다음 위치에서 다운 받는 것이 가능합니다.
git clone https://github.com/jbpark/JbTraderExample.git
cd JbTraderExample/jbtrader/ch5.8
QLabel 글씨가 잘리지 않도록 글씨 선택 후 마우스로 잘린 영역 없도록 드래그하여 늘려주는 것이 중요합니다.
4) 폰트 적용 확인
폰트 적용 후 프로그램을 다시 실행하면 아래와 같이 짤리는 글씨 없이 정상 출력됨을 확인 할 수 있습니다.
이제 서버 연결, 매수, 매도에 대해 부분적으로 확인되었으니 전체를 하나의 통합 코드로 만들어보겠습니다.
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(자동 로그인) 을 선택합니다.
만약 계좌비밀번호가 저장되지 않은 상태에서 주문이 실행되면 아래와 같은 오류가 발생합니다.
키움증권 api 라이브러리인 pykiwoom 을 사용하여 매도하는 코드를 만들어주세요.
pyqt 와도 같이 연결해주세요.
OrderType이라는 이름을 가진 ComboBox 로 시장가, 매도가 선택이 있고 매도를 선택시 OrderPrice라는 LineEdit 에서 매도가를 불러옵니다.
그리고 수량을 입력한 OrderAmount라는 LineEdit 도 있습니다.
그리고 종목코드를 입력한 StockCode라는 LineEdit 도 있습니다.
매도 주문이 나가면 Log라는 TextEdit에 종목코드에 해당하는 종목명을 가져와서 종목명이 매도가 얼마에 몇개가 주문되었다고 표시합니다.
그리고 매도가 체결이 되면 매도가 체결되었다라는 메시지를 표시합니다.
파이썬 코드를 만들고 주석도 추가해주세요
생성된 파이썬 코드입니다.
아래 코드는 다음 위치에서 다운 받는 것이 가능합니다.
git clone https://github.com/jbpark/JbTraderExample.git
cd JbTraderExample/jbtrader/ch5.6
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QComboBox, QLineEdit, QTextEdit, QLabel
from PyQt5.QtCore import Qt
from pykiwoom.kiwoom import Kiwoom
import sys
def get_stock_name(kiwoom, code):
stock_info = kiwoom.GetMasterCodeName(code)
return stock_info if stock_info else "알 수 없는 종목"
class KiwoomSellApp(QWidget):
def __init__(self):
super().__init__()
self.kiwoom = Kiwoom()
self.kiwoom.CommConnect(block=True) # 로그인
self.initUI()
def initUI(self):
layout = QVBoxLayout()
# 종목 코드 입력
self.stock_code_label = QLabel("종목 코드:")
self.stock_code = QLineEdit()
layout.addWidget(self.stock_code_label)
layout.addWidget(self.stock_code)
# 매도 유형 선택
self.order_type_label = QLabel("매도 유형:")
self.order_type = QComboBox()
self.order_type.addItems(["시장가", "매도가"])
self.order_type.currentIndexChanged.connect(self.order_type_changed)
layout.addWidget(self.order_type_label)
layout.addWidget(self.order_type)
# 매도가 입력 (매도가 선택 시 활성화)
self.order_price_label = QLabel("매도가:")
self.order_price = QLineEdit()
self.order_price.setDisabled(True)
layout.addWidget(self.order_price_label)
layout.addWidget(self.order_price)
# 수량 입력
self.order_amount_label = QLabel("수량:")
self.order_amount = QLineEdit()
layout.addWidget(self.order_amount_label)
layout.addWidget(self.order_amount)
# 매도 버튼
self.sell_button = QPushButton("매도 주문")
self.sell_button.clicked.connect(self.sell_stock)
layout.addWidget(self.sell_button)
# 로그 창
self.log = QTextEdit()
self.log.setReadOnly(True)
layout.addWidget(self.log)
self.setLayout(layout)
self.setWindowTitle("Kiwoom 매도 주문 시스템")
def order_type_changed(self):
""" 매도 유형 변경 시 매도가 입력 활성화 여부 설정 """
if self.order_type.currentText() == "매도가":
self.order_price.setDisabled(False)
else:
self.order_price.setDisabled(True)
def sell_stock(self):
""" 매도 주문 실행 """
stock_code = self.stock_code.text().strip()
order_type = 1 if self.order_type.currentText() == "시장가" else 0 # 1: 시장가, 0: 지정가
order_price = self.order_price.text().strip() if order_type == 0 else ""
order_amount = self.order_amount.text().strip()
if not stock_code or not order_amount:
self.log.append("[오류] 종목 코드와 수량을 입력하세요.")
return
# 종목명 조회
stock_name = get_stock_name(self.kiwoom, stock_code)
# 매도 주문 전송
order_id = self.kiwoom.SendOrder(
"매도주문", "0101", self.kiwoom.GetLoginInfo("ACCNO").split(';')[0], 2,
stock_code, int(order_amount), int(order_price) if order_price else 0,
order_type, ""
)
self.log.append(f"[주문] {stock_name} ({stock_code}) 매도 주문 - 가격: {order_price if order_price else '시장가'}, 수량: {order_amount}")
# 체결 이벤트 감지하여 로그 업데이트
self.kiwoom.OnReceiveChejanData = self.order_filled
def order_filled(self, gubun, item_cnt, fid_list):
""" 체결 완료 시 로그 업데이트 """
if gubun == "0": # 체결 정보
stock_code = self.kiwoom.GetChejanData(9001).strip()[1:] # 종목코드 앞에 'A'가 붙어있음
stock_name = get_stock_name(self.kiwoom, stock_code)
self.log.append(f"[체결] {stock_name} ({stock_code}) 매도 주문 체결 완료")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = KiwoomSellApp()
window.show()
sys.exit(app.exec_())
코드 내용도 아래와 같이 간단합니다.
OrderType (QComboBox): "시장가" 또는 "매도가" 선택
OrderPrice (QLineEdit): 매도 주문가 입력 (매도가 선택 시 활성화)
OrderAmount (QLineEdit): 매도 수량 입력
StockCode (QLineEdit): 종목코드 입력
Log (QTextEdit): 주문 및 체결 내역 표시
아래 코드는 실제 매도 요청이 나가는 것으로 매수와 매도가 거의 유사하며 order type 이 아래 명령에는 2로 나가고 있습니다.