개발 1일차
개발 2일차
개발 3일차
개발 4일차
요구 사항
Anaconda - Python 3.5 32 bit
Pycharm IDE
는 항상 관리자 권한
으로 실행.
키움증권 계좌 개설
키움증권 OpenAPI+
신청
OpenAPI+ 모듈
Microsoft Visual C++ 2010 SP1 재배포 가능 패키지(x86)
사전 작업
Anaconda 32bit 설치
Python 3.5 사용 환경 만들기 - conda create -n [envname] python=3.5 anaconda
번개 HTS 설치.
키움 Open API+
설치.
powershell
설정 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
Get-ExecutionPolicy -List # 전체 권한 확인
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine
Scope에서 CurrentUser가 RemoteSigned로 되어 있어야 정상적으로 작동
For more information, see Set-ExecutionPolicy.
개발 1일차
자동 버전처리 스크립트
OpenAPI+를 이용해 개발한 프로그램에서 로그인을 시도할 때 버전 처리가 필요하면 버전 처리 메시지 창이 아래와 같이 나타남.
버전 처리를 가장 쉽게 할 수 있는 방법은 번개 HTS를 사용하는 것.
번개 HTS를 실행하면 자동으로 버전 처리가 완료되기 때문에 OpenAPI+를 사용해 개발한 프로그램을 실행하기 전에 먼저 번개 HTS를 실행하면 버전 처리 문제가 해결됨.
고객 ID
와 비밀번호
를 입력하는 자동화는 pywinauto
패키지를 이용하면 해결.
pywinauto
패키지는 윈도우 대화상자에 자동으로 마우스나 키보드 이벤트를 보낼 수 있음.
pip install pywinauto
를 입력해 패키지를 설치
pywinquto
패키지를 이용해 코드를 작성하기 위해 PyCharm
과 같은 파이썬 IDE
를 관리자 권한으로 실행.
아래와 같이 코드를 작성 후 실행하면 키움 번개 로그인 창이 출력 됨.
로그인 창 출력 1 2 3 4 5 6 7 8 9 10 from pywinauto import applicationfrom pywinauto import timingsimport timeimport osapp = application.Application() app.start("C:/Kiwoom/KiwoomFlash3/bin/nkministarter.exe" ) title = "번개3 Login" dlg = timings.WaitUntilPasses(20 , 0.5 , lambda : app.window_(title=title))
마우스와 키보드 입력 자동화
마우스와 키보드 입력 자동화 1 2 3 4 5 6 7 8 9 10 pass_ctrl = dlg.Edit2 pass_ctrl.SetFocus() pass_ctrl.TypeKeys('xxxx' ) cert_ctrl = dlg.Edit3 cert_ctrl.SetFocus() cert_ctrl.TypeKyes('yyyy' ) btn_ctrl = dlg.Button0 btn_ctrl.Click()
위의 모든 코드를 실행하면 자동으로 로그인이 이뤄진 후 아래와 같이 키움 번개가 정상적으로 실행되는 것을 확인.
이 과정에서 업데이트가 있는 경우 업데이트 파일을 다운로드하기 때문에 자동으로 버전 처리가 완료 됨.
윈도우에서는 taskkill
명령을 이용해 특정 프로그램을 종료할 수 있음.
파이썬에서 윈도우 명령을 실행하려면 os
모듈의 system
함수를 사용하면 됨.
참고로 로그인 후에 업데이트를 수행하는 과정에 시간이 소용되기 때문에 time.sleep
함수를 호출해 약 50초 정도 대기.
50초 후 키움 종료 1 2 time.sleep(50 ) os.system("taskkill /im nkmini.exe" )
전체코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pywinauto import applicationfrom pywinauto import timingsimport timeimport osapp = application.Application() app.start("C:/Kiwoom/KiwoomFlash2/khministarter.exe" ) title = "번개 Login" dlg = timings.WaitUntilPasses(20 , 0.5 , lambda : app.window_(title=title)) pass_ctrl = dlg.Edit2 pass_ctrl.SetFocus() pass_ctrl.TypeKeys('xxxx' ) cert_ctrl = dlg.Edit3 cert_ctrl.SetFocus() cert_ctrl.TypeKeys('yyyy' ) btn_ctrl = dlg.Button0 btn_ctrl.Click() time.sleep(50 ) os.system("taskkill /im khmini.exe" )
만약 PyCharm
을 관리자 권한으로 실행하기 않을 경우 아래와 같은 에러가 발생.
윈도우 대화상자의 이름과 각 컨트롤의 이름을 알아내가 위해 SWAPY 라는 실행 프로그램을 사용.
윈도우 작업 스케쥴러
윈도우의 작업 스케쥴러를 이용해 정해진 시간에 파이썬 스크립트 자동 실행.
python.exe
대신 pythonw.exe
를 선택한 것은 스크립트 실행 시 콘솔
PyTrader 구현
Qt Designer
를 이용해 메인 윈도우를 만듬.
PyQt를 이용한 GUI 프로그래밍 참조.
프로그램에 사용할 무료 아이콘 MyIconFinder 나 Flaticon
PyTrader
에서 UI는 Qt Designer
를 통해 생성한 pytrader.ui
파일을 불러와서 사용.
키움 OpenAPI+
와 관련된 코드는 Kiwoom
클래스를 사용.
Kiwoom.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 import sysfrom PyQt5.QtWidgets import *from PyQt5.QAxContainer import *from PyQt5.QtCore import *import timeimport pandas as pdimport sqlite3TR_REQ_TIME_INTERVAL = 0.2 class Kiwoom (QAxWidget) : def __init__ (self) : super().__init__() self._create_kiwoom_instance() self._set_signal_slots() def _create_kiwoom_instance (self) : self.setControl("KHOPENAPI.KHOpenAPICtrl.1" ) def _set_signal_slots (self) : self.OnEventConnect.connect(self._event_connect) self.OnReceiveTrData.connect(self._receive_tr_data) def comm_connect (self) : self.dynamicCall("CommConnect()" ) self.login_event_loop = QEventLoop() self.login_event_loop.exec_() def _event_connect (self, err_code) : if err_code == 0 : print("connected" ) else : print("disconnected" ) self.login_event_loop.exit() def get_code_list_by_market (self, market) : code_list = self.dynamicCall("GetCodeListByMarket(QString)" , market) code_list = code_list.split(';' ) return code_list[:-1 ] def get_master_code_name (self, code) : code_name = self.dynamicCall("GetMasterCodeName(QString)" , code) return code_name def get_connect_state (self) : ret = self.dynamicCall("GetConnectState()" ) return ret def set_input_value (self, id, value) : self.dynamicCall("SetInputValue(QString, QString)" , id, value) def comm_rq_data (self, rqname, trcode, next, screen_no) : self.dynamicCall("CommRqData(QString, QString, int, QString)" , rqname, trcode, next, screen_no) self.tr_event_loop = QEventLoop() self.tr_event_loop.exec_() def _comm_get_data (self, code, real_type, field_name, index, item_name) : ret = self.dynamicCall("CommGetData(QString, QString, QString, int, QString)" , code, real_type, field_name, index, item_name) return ret.strip() def _get_repeat_cnt (self, trcode, rqname) : ret = self.dynamicCall("GetRepeatCnt(QString, QString)" , trcode, rqname) return ret def _receive_tr_data (self, screen_no, rqname, trcode, record_name, next, unused1, unused2, unused3, unused4) : if next == '2' : self.remained_data = True else : self.remained_data = False if rqname == "opt10081_req" : self._opt10081(rqname, trcode) try : self.tr_event_loop.exit() except AttributeError: pass def _opt10081 (self, rqname, trcode) : data_cnt = self._get_repeat_cnt(trcode, rqname) for i in range(data_cnt): date = self._comm_get_data(trcode, "" , rqname, i, "일자" ) open = self._comm_get_data(trcode, "" , rqname, i, "시가" ) high = self._comm_get_data(trcode, "" , rqname, i, "고가" ) low = self._comm_get_data(trcode, "" , rqname, i, "저가" ) close = self._comm_get_data(trcode, "" , rqname, i, "현재가" ) volume = self._comm_get_data(trcode, "" , rqname, i, "거래량" ) self.ohlcv['date' ].append(date) self.ohlcv['open' ].append(int(open)) self.ohlcv['high' ].append(int(high)) self.ohlcv['low' ].append(int(low)) self.ohlcv['close' ].append(int(close)) self.ohlcv['volume' ].append(int(volume))
pytrader.py
파일에 모듈을 임포트하고 ui 파일을 불러오는 코드를 구현.
pytrader.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import sysfrom PyQt5.QtWidgets import *from PyQt5.QtCore import *from PyQt5 import uicfrom Kiwoom import *form_class = uic.loadUiType("pytrader.ui" )[0 ] class MyWindow (QMainWindow, form_class) : def __init__ (self) : super().__init__() self.setupUi(self) if __name__ == "__main__" : app = QApplication(sys.argv) myWindow = MyWindow() myWindow.show() app.exec_()
PyTrader
프로그램이 실행될 때 키움 로그인이 진행되도록 해주기 위해 생성자에서 키움 객체를 생성한 후 Comm_Connect 메서드를 호출.
call comm_connect pytrader.py 1 2 3 4 5 6 7 class MyWindow (QMainWindow, form_class) : def __init__ (self) : super().__init__() self.setupUi(self) self.kiwoom = Kiwoom() self.kiwoom.comm_connect()
StatusBar
위젯에 서버 연결 상태 및 현재 시간을 출력하는 기능 구현.
서버 연결 상태는 Kiwoom class
에 추가한 get_connect_state
메서드를 사용.
현재 시간을 출력하는 기능은 일정한 단위로 현재 시간을 얻어온 후 이를 StatusBar
에 출력.
이를 위해서는 주기적으로 이벤트를 발생시키는 Timer
가 필요.
예를 들어, Timer
가 1초에 한 번 이벤트(시그널)를 발생시키면 이 이벤트를 처리하는 메서드(슬롯)에서 현재 시간을 얻어온 후 이를 StatusBar
에 출력
Qt의 QTimer
클래스를 사용하면 정해진 시간마다 이벤트를 발생시킬 수 있음.
1 2 3 self.timer = QTimer(self) self.timer.start(1000 ) self.timer.timeout.connect(self.timout)
QTimer
클래스는 start
메서드를 제공, 이 메서드에 인자로 1000
을 지정하면 1초
에 한 번씩 주기적으로 timeout
시그널이 발생.
따라서 timeout
시그널이 발생할 때 이를 처리할 슬롯으로 self.timeout
을 설정하면 됨.
timout class pytrader.py 1 2 3 4 5 6 7 8 9 10 11 12 def timeout (self) : current_time = QTime.currentTime() text_time = current_time.toString("hh:mm:ss" ) time_msg = "현재시간: " + text_time state = self.kiwoom.GetConnectState() if state == 1 : state_msg = "서버 연결 중" else : state_msg = "서버 미 연결 중" self.statusbar.showMessage(state_msg + " | " + time_msg)
전체 pytrader.py 코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import sysfrom PyQt5.QtWidgets import *from PyQt5.QtCore import *from PyQt5 import uicfrom Kiwoom import *form_class = uic.loadUiType("pytrader.ui" )[0 ] class MyWindow (QMainWindow, form_class) : def __init__ (self) : super().__init__() self.setupUi(self) self.kiwoom = Kiwoom() self.kiwoom.comm_connect() self.timer = QTimer(self) self.timer.start(1000 ) self.timer.timeout.connect(self.timeout) def timeout (self) : current_time = QTime.currentTime() text_time = current_time.toString("hh:mm:ss" ) time_msg = "현재시간: " + text_time state = self.kiwoom.get_connect_state() if state == 1 : state_msg = "서버 연결 중" else : state_msg = "서버 미 연결 중" self.statusbar.showMessage(state_msg + " | " + time_msg) if __name__ == "__main__" : app = QApplication(sys.argv) myWindow = MyWindow() myWindow.show() app.exec_()
키움 OpenAPI+ 자동 로그인
자동화를 위해 사용자 계정 컨트롤 기능 비활성화
키움 증권 로그인 과정 자동화.
키움증권은 사용자의 편의를 위해 자동 로그인 기능을 지원.
자동 로그인 설정을 위해 pytrader.py
파일을 실행시켜 로그인을 진행.
정상적으로 로그인되면 아래와 같이 윈도우 오른쪽 아래의 아이콘 중 키움증권 아이콘에서 마우스 오른쪽 버튼을 클릭한 후 계좌비밀번호 저장
을 선택.
자동 로그인 설정은 계좌 단위로 가능한데, 계좌를 선택한 후 등록 부분에 계좌 비밀번호를 입력하고 등록
버튼을 눌러 등록.
비밀번호가 잘 등록되면 AUTO
라는 체크박스를 체크.
설정을 모두 완료했다면 실행시킨 PyTrader 프로그램을 종료.
그리고 다시 pytrader.py
파일을 실행해 보면 로그인 창이 화면에 나타나지 않고 바로 로그인이 수행된 후 PyTrader
프로그램이 실행 됨.
정리
윈도우 작업 스케쥴러를 이용해 매일 08:00에 키움 번개 자동 로그인 스크립트(login.py
)를 실행.
해당 스크립트가 실행되면 키움 번개에 로그인되면 버전 처리 완료.
키움 번개 HTS 자동 종료.
08:15 윈도우 작업 스케쥴러를 이용해 pytrader.py
파일을 실행.
이때 위 그림 같이 자동 로그인 설정해 둠.
아래 그림과 같은 순서로 프로그램이 실행되게 해두면 버전 처리 및 로그인 자동화.
PyTrader
개발과 관련해서 키움 OpenAPI+
관련 코드는 모두 Kiwoom.py
에 구현.
UI는 Qt Designer
를 사용해 pytrader.ui
파일에 구현.
pytrader.py
에는 이벤트 처리하는 코드만 구현.
에러사항
KOA Studio
에서 모의투자 체크 박스가 정상적으로 안되는 경우 발생.
일단 모의 투자로 접속하고 계좌비밀번호 저장
에서 AUTO
체크박스 해제로 일반 투자로 접속 가능.
모의투자는 최대 3개월
만 사용가능하여 기간 종료 시에는 모의 투자로 접속이 안되어 다시 모의 투자 가입하여야 함.
개발 2일차
주문 기능 추가.
1) UI 구성
QGroupBox
위젯을 MainWindow
안으로 배치.
Label
을 선택. Label
은 PyQt의
QLabel` 클래스에 해당.
Combo Box
를 선택. Combo Box
는 QComboBox
클래스를 의미
세 개의 Combo Box
를 Main Window
에 배치.
Combo Box
위젯을 더블클릭하면 아래와 같이 세부 항목을 추가 할 수 있는 창이 나타남.
Line Edit
를 선택한 후 배치. Line Edit
는 QLineEdit
클래스에 해당.
Spin Box
를 선택한 후 수량과 가격 옆으로 배치.
Push Button
을 선택한 후 맨 아래쪽에 배치.
주문과 관련된 API를 처리하는 메서드를 구현.
SendOrder 메서드를 사용하면 주식 주문에 대한 정보를 서버로 전송.
주문이 체결되면 증권사 서버는 아래와 같이 OnReceiveChejanData
라는 이벤트를 발생.
앞서 OnReceiveTrData 메서드 내에서 CommGetData
메서드를 호출해 데이터를 얻어왔던 것과 동일하게 체결과 관련해서는 OnReceiveChejanData
라는 메서드 내에서 GetChejanData
라는 메서드를 호출해서 체결잔고 데이터를 얻어 옴.
Kiwoom 클래스에 send_order
메서드를 추가
1 2 3 def send_order (self, rqname, screen_no, acc_no, order_type, code, quantity, price, hoga, order_no) : self.dynamicCall("SendOrder(QString, QString, QString, int, QString, int, int, QString, QString)" , [rqname, screen_no, acc_no, order_type, code, quantity, price, hoga, order_no])
체결잔고 데이터를 가져오는 메서드인 GetChejanData 를 사용하는 get_chejan_data
메서드를 Kiwoom 클래스에 추가.
1 2 3 def get_chejan_data (self, fid) : ret = self.dynamicCall("GetChejanData(int)" , fid) return ret
주문체결 시점에서 키움증권 서버가 발생시키는 OnReceiveChejanData
이벤트를 처리하는 메서드를 구현.
먼저 _set_signal_slots
메서드에 시그널과 슬롯을 연결하는 코드 추가.
1 self.OnReceiveChejanData.connect(self._receive_chejan_data)
OnReceiveChejanData
이벤트가 발생할 때 호출되는 _receive_chejan_data
는 다음과 같이 구현.
1 2 3 4 5 6 def _receive_chejan_data (self, gubun, item_cnt, fid_list) : print(gubun) print(self.get_chejan_data(9203 )) print(self.get_chejan_data(302 )) print(self.get_chejan_data(900 )) print(self.get_chejan_data(901 ))
get_chejan_data
메서드는 함수 인자인 FID
값을 통해 서로 다른 데이터를 얻을 수 있음.
더 자세한 FID
정보는 개발 가이드 8.19절
을 참조.
FID
설명
9203
주문번호
302
종목명
900
주문수량
901
주문가격
902
미체결수량
904
원주문번호
905
주문구분
908
주문/체결시간
909
체결번호
911
체결량
10
현재가, 체결가, 실시간 종가
OpenAPI+
에서 계좌 정보 및 로그인 사용자 정보를 얻어오는 메서드는 GetLoginInfo
.
다음과 같이 dynamicCall
메서드로 GetLoginInfo
메서드를 호출하는 get_login_info
메서드를 Kiwoom 클래스에 추가.
1 2 3 def get_login_info (self, tag) : ret = self.dynamicCall("GetLoginInfo(QString)" , tag) return ret
3) pytrader.py
파일 업데이트
가장 먼저 추가할 기능은 QLinkEdit
위젯에 사용자가 코드명을 입력하면 해당 코드에 대한 종목명을 출력하는 기능.
pytrader.py
파일에 위젯에 대한 코드를 구현할 때 가장 먼저 할 일은 위젯의 이름을 파악하는 것.
사용자가 lineEdit
객체에 종목 코드를 입력하면 PyTrader
는 사용자가 입력한 종목 코드를 읽은 후 키움증권의 OpenAPI+
를 사용하여 종목명을 알아내야 함.
이를 위해 먼저 lineEdit
객체가 변경될 때 호출되는 슬롯을 지정.
1 self.lineEdit.textChanged.connect(self.code_changed)
위에서 시그널 슬롯을 설정했기 때문에 lineEdit
객체로부터 textChanged
라는 이벤트가 발생하면 MyWindow
클래스의 code_changed
메서드가 호출될 것임.
MyWindow
클래스에 code_changed
메서드를 다음과 같이 구현.
먼저 사용자가 입력한 종목 코드를 얻어 옴.
종목 코드에 대한 종목명은 Kiwoom 클래스에 구현된 get_master_code_name
메서드를 호출하여 알아냄.
1 2 3 4 def coe_changed (self) : code = self.lineEdit.text() name = self.kiwoom.get_master_code_name(code) self.lineEdit_2.ssetText(name)
계좌 정보를 QComboBox
위젯에 출력하는 코드 구현.
이를 위해 먼저 전체 계좌 개수와 계좌 번호를 키움증권으로부터 얻어 옴.
Kiwoom 클래스의 get_login_info
메서드를 사용하여 다음과 같이 필요한 데이터를 얻어 옴.
1 2 accouns_num = int(self.kiwoom.get_login_info("ACCOUNT_CNT" )) accounts = self.kiwoom.get_login_info("ACCNO" )
계좌 번호를 QComboBox
위젯에 출력하려면 아래와 같이 먼저 objectName
을 확인.
계좌가 여러 개인 경우 각 계좌는 ;
를 통해 구분됨. 따라서 먼저 얻어온 전체 계좌에 대해 split
메서드를 호출하여 리스트로 분리한 후 슬라이시을 통해 출력할 계좌번호를 선택.
계좌를 QComboBox
에 출력하기 위해 addItems
메서드를 호출.
1 2 accounts_list = accounts.split(';' )[0 :accouns_num] self.comboBox.addItems(accounts_list)
마지막으로 현금주문에 대한 코드를 구현.
현금주문은 UI 창에서 현금주문
버튼을 클릭할 때 수행됨.
따라서 먼저 버튼의 이름을 확인한 후 해당 버튼에 대한 시그널과 슬롯을 연결.
1 self.pushButton.clicked.connect(self.send_order)
send_order
메서드를 구현
주의할 점은 사용자는 QComboBox
를 통해 ‘신규매수’, ‘신규매도’, ‘매수취소’, '매도취소’와 같은 세부 항목을 선택하지만
실제 키움증권의 API에는 세부 항목에 대응되는 정숫값이 전달돼야 함.
마찬가지로 호가에서도 지정가일 때는 00
이라는 문자열을 전달해야 하고, 시장가일 때는 03
이라는 문자열을 전달해야 함.
1 2 3 4 5 6 7 8 9 10 11 12 def send_order (self) : order_type_lookup = {'신규매수' : 1 , '신규매도' : 2 , '매수취소' : 3 , '매도취소' : 4 } hoga_lookup = {'지정가' : "00" , '시장가' : "03" } account = self.comboBox.currentText() order_type = self.comboBox_2.currentText() code = self.lineEdit.text() hoga = self.comboBox_3.currentText() num = self.spinBox.value() price = self.spinBox_2.value() self.kiwoom.send_order("send_order_req" , "0101" , account, order_type_lookup[order_type], code, num, price, hoga_lookup[hoga], "" )
4) 매수 테스트
pytrader.py
파일을 관리자 권한으로 실행.
거래할 계좌 번호를 선택한 후 주문을 신규매수로 선택.
매수하고자 하는 종목코드를 입력
종류에 시장가를 선택하고 적당한 수량을 입력한 후 현금주문
버튼을 클릭
매수 API가 정상적으로 동작했는지를 확인하는 쉬운 방법은 HTS를 사용하는 것
다른 방법은 KOA Studio를 사용하는 것.
아래 그림처럼 TR
목록에서 opt10085를 선택한 후 오른쪽 계좌번오에 계좌번호를 입력한 후 조회
버튼을 클릭하면 화면 하단 출력부에 데이터가 출력.
개발 3일차
보유 주식 현황 출력과 잔고확인 기능 구현.
1) UI 구성
Table Widget
을 선택한 후 아래와 같이 Property Editor
에서 QTableWidget
항목에서 rowCount
값을 1
로 변경.
QTableWidget
객체는 rowCount
값을 제대로 설정하지 않으면 아이템을 추가할 수 없으므로 반드시 값을 설정.
Check Box
의 경우 Property Editor
에서 text 항목을 실시간 조회
로 변경하고 버튼의 text 항목을 조회
로 변경.
2) Kiwoom.py
파일 업데이트
KOA Studio
를 참고하면 잔고 및 보유종목 현황 출력 기능에 필요한 대부분의 데이터는 opw00018
이라는 TR
를 통해 얻을 수 있음.
예수금 정보는 opw00001 TR
을 사용.
먼저 OnReceiveTrData 이벤트가 발생할 때 수신 데이터를 가져오는 함수인 _opw0001
를 Kiwoom 클래스에 추가.
1 2 def _opw00001 (self, rqname, trcode) : self.d2_deposit = self._comm_get_data(trcode, "" , rqname, 0 , "d+2추정예수금" )
_receive_tr_data
메서드를 구현할 때 다음 세가지 고려 사항
여러 종류의 TR을 요청해도 모두 _receive_tr_data
메서드 내에서 처리해야 함.
따라서 rqname
이라는 인자를 통해서 요청한 TR을 구분한 후 TR에 따라서 적당한 데이터를 가져오도록 코딩.
연속조회에 대한 처리.
이벤트 루프에 대한 처리.
_receive_tr_data
메서드에서 _opw00001
메서드를 호출하도록 코드 수정.
1 2 3 4 5 if rqname == "opt10081_req" : self._opt10081(rqname, trcode) elif rqname == "opw00001_req" : self._opw00001(rqname, trcode)
d+2 추정예수금
을 잘 얻어오는지 확인하기 위해 Kiwoom.py
파을이 main
코드 부분을 아래와 같이 수정.
Kiwoom.py 1 2 3 4 5 6 7 8 9 10 if __name__ == "__main__": app = QApplication(sys.argv) kiwoom = Kiwoom() kiwoom.comm_connect() kiwoom.set_input_value("계좌번호", "8087711111") kiwoom.set_input_value("비밀번호", "0000") kiwoom.comm_rq_data("opw00001_req", "opw00001", 0, "2000") print(kiwoom.de_deposit)
얻어온 d+2추정예수금
을 확인하면 000000499225300
와 같이 문자열의 앞쪽에 0
이 존재하는 것을 확인.
보통 금액은 천의 자리마다 콤마를 사용하여 표시. 이를 위해 Kiwoom 클래스에 change_format
이라는 static method
를 추가.
change_format
메서드는 입력된 문자열에 대해 lstrip
메서드를 통해 문자열 왼쪽에 존재하는 -
또는 0
을 제거.
그리고 format
함수를 통해 천의 자리마다 콤마를 추가한 문자열로 변경.
staticmethod 1 2 3 4 5 6 7 8 9 10 11 @staticmethod def change_format (data) : strip_data = data.lstrip('-0' ) if strip_data == '' : strip_data = '0' format_data = format(int(strip_data), ',d' ) if data.startswith('-' ): format_data = '-' + format_data return format_data
정적 메서드를 호출하려면 정적 메서드 이름 앞에 클래스 이름을 붙여줌.
1 2 3 def _opw00001 (self, rqname, trcode) : d2_deposit = self._comm_get_data(trcode, "" , rqnam, 0 , "d+2추정예수금" ) self.d2_deposit = Kiwoom.change_format(d2_deposit)
총매입금액
, 총평가금액
, 총평가손익금액
, 총수익률
, 추정예탁자산
을 _comm_get_data
메서드를 통해 얻어 옴.
얻어온 데이터는 change_format
메서드를 통해 포맷을 문자열로 변경
1 2 3 4 5 6 7 8 9 10 11 12 def _opw00018 (self, rqname, trcode) : total_purchase_price = self._comm_get_data(trcode, "" , rqname, 0 , "총매입금액" ) total_eval_price = self._comm_get_data(trcode, "" , rqname, 0 , "총평가금액" ) total_eval_profit_loss_price = self._comm_get_data(trcode, "" , rqname, 0 , "총평가손익금액" ) total_earning_rate = self._comm_get_data(trcode, '' , rqname, 0 , "총수익률(%)" ) estimated_deposit = self._comm_get_data(trcode, '' , rqname, 0 , "추정예탁자산" ) print(Kiwoom.change_format(total_purchase_price)) print(Kiwoom.change_format(total_eval_price)) print(Kiwoom.change_format(total_eval_profit_loss_price)) print(Kiwoom.change_format(total_earning_rate)) print(Kiwoom.change_format(estimated_deposit))
_receive_tr_data
메서드에서 _opw00018
메서드를 호출하도록 코드 수정.
1 2 3 4 5 6 if rqname == "opt10081_req" : self._opt10081(rqname, trcode) elif rqname == "opw00001_req" : self._opw00001(rqname, trcode) elif rqname == "opw0001_req" : self._opw00018(rqname, trcode)
opw00018 TR
을 통해 싱글 데이터를 잘 얻어오는지 테스트하기 위해 Kiwoom.py
파일의 __main__
코드 부분을 수정.
1 2 3 4 5 6 7 8 9 10 if __name__ == "__main__" : app = QApplication(sys.argv) kiwoom = Kiwoom() kiwoom.comm_connect() account_number = kiwoom.get_login_info("ACCNO" ) account_number = account_number.split(';' )[0 ] kiwoom.set_input_value("계좌번호" , account_number) kiwoom.comm_rq_data("opw00018_req" , "opw00018" , 0 , "2000" )
만약 Kiwoom.py
파일을 실행했을 때 아래와 같은 에러가 발생한다면 계좌 비밀번호 등록한 후 자동 로그인 설정 해줌.
멀티 데이터를 통해 보유 종목별 평가 잔고 데이터를 가져오기.
다음 코드를 _opw00018
에 추가.
멀티 데이터는 _get_repeat_cnt
메서드를 호출하여 보유 종목의 개수를 얻어 옴.
그런 다음 해당 개수마큼 반복하면서 각 보유 종목에 대한 상세 데이터를 _comm_get_data
를 통해 얻어 옴.
참고로 opw00018 TR
을 사용하는 경우 한 번의 TR 요청으로 최대 20개의 보유 종목에 대한 데이터를 얻을 수 있음.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 rows = self._get_repeat_cnt(trcode, rqname) for i in range(rows): name = self._comm_get_data(trcode, "" , rqname, i, "종목명" ) quantity = self._comm_get_data(trcode, "" , rqname, i, "보유수량" ) purchase_price = self._comm_get_data(trcode, "" , rqname, i, "매입가" ) current_price = self._comm_get_data(trcode, "" , rqname, i, "현재가" ) eval_profit_loss_price = self._comm_get_data(trcode, "" , rqname, i, "평가손익" ) earning_rate = self._comm_get_data(trcode, "" , rqname, i, "수익률(%)" ) quantity = Kiwoom.change_format(quantity) purchase_price = Kiwoom.change_format(purchase_price) current_price = Kiwoom.change_format(current_price) eval_profit_loss_price = Kiwoom.change_format(eval_profit_loss_price) earning_rate = Kiwoom.change_format2(earning_rate) print(name, quantity, purchase_price, current_price, eval_profit_loss_price, earning_rate)
수익률에 대한 포맷 변경은 change_format2
라는 정적 메서드를 사용.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @staticmethod def change_format2 (data) : strip_data = data.lstrip('-0' ) if strip_data == '' : strip_data = '0' if strip_data.startswith('.' ): strip_data = '0' + strip_data if data.startswith('-' ): strip_data = '-' + strip_data return strip_data
opw00018 TR
을 통해 얻어온 데이터를 인스턴스 변수에 저장
Kiwoom 클래스에 다음 메서드 추가
1 2 def reset_opw00018_output (self) : self.opw00018_output = {'single' : [], 'multi' : []}
_opw00018
메서드는 아래와 같이 수정.
싱글 데이터는 1차원 리스트로 데이터를 저장,
멀티 데이터는 2차원 리스트로 데이터를 저장.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 def _opw00018 (self, rqname, trcode) : total_purchase_price = self._comm_get_data(trcode, "" , rqname, 0 , "총매입금액" ) total_eval_price = self._comm_get_data(trcode, "" , rqname, 0 , "총평가금액" ) total_eval_profit_loss_price = self._comm_get_data(trcode, "" , rqname, 0 , "총평가손익금액" ) total_earning_rate = self._comm_get_data(trcode, "" , rqname, 0 , "총수익률(%)" ) estimated_deposit = self._comm_get_data(trcode, "" , rqname, 0 , "추정예탁자산" ) self.opw00018_output['single' ].append(Kiwoom.change_format(total_purchase_price)) self.opw00018_output['single' ].append(Kiwoom.change_format(total_eval_price)) self.opw00018_output['single' ].append(Kiwoom.change_format(total_eval_profit_loss_price)) self.opw00018_output['single' ].append(Kiwoom.change_format(total_earning_rate)) self.opw00018_output['single' ].append(Kiwoom.change_format(estimated_deposit)) rows = self._get_repeat_cnt(trcode, rqname) for i in range(rows): name = self._comm_get_data(trcode, "" , rqname, i, "종목명" ) quantity = self._comm_get_data(trcode, "" , rqname, i, "보유수량" ) purchase_price = self._comm_get_data(trcode, "" , rqname, i, "매입가" ) current_price = self._comm_get_data(trcode, "" , rqname, i, "현재가" ) eval_profit_loss_price = self._comm_get_data(trcode, "" , rqname, i, "평가손익" ) earning_rate = self._comm_get_data(trcode, "" , rqname, i, "수익률(%)" ) quantity = Kiwoom.change_format(quantity) purchase_price = Kiwoom.change_format(purchase_price) current_price = Kiwoom.change_format(current_price) eval_profit_loss_price = Kiwoom.change_format(eval_profit_loss_price) earning_rate = Kiwoom.change_format2(earning_rate) self.opw00018_output['multi' ].append([name, quantity, purchase_price, current_price, eval_profit_loss_price, earning_rate])
opw00018 TR
을 사용할 때 한가지 주의 사항은 실 서버로 접속할 때와 모의투자 서버로 접속할 때에 제공되는 데이터 형식이 다름.
예를 들어 실 서버에서 수익률은 소수점 표시 없이 전달되지만, 모의투자에선 소수점을 포함해서 데이터가 전달됨.
따라서 접속 서버를 구분하여 데이터를 다르게 처리해야 할 필요가 있음.
Kiwoom 클래스에 다음 메서드를 추가
1 2 3 def get_server_gubun (self) : ret = self.dynamicCall("KOA_Functions(QString, QString)" , "GetServerGubun" , "" ) return ret
_opw00018
메서드에서 모의투자일 때는 총수익률(%)
의 값을 100으로 나눈 후 출력 되도록 다음과 같이 코드 수정.
1 2 3 4 5 6 7 total_earning_rate = Kiwoom.change_format(total_earning_rate) if self.get_server_gubun(): total_earning_rate = float(total_earning_rate) / 100 total_earning_rate = str(total_earning_rate) self.opw00018_output['single' ].append(total_earning_rate)
3) pytrader.py
파일 업데이트
Kiwoom 클래스를 사용해 잔고 및 보유종목 현황 데이터를 요청하고 데이터를 UI에 출력하는 코드 작성.
call check_balance 1 2 self.pushButton_2.clicked.connect(self.check_balance)
check_balance
메서드 구현.
check_balance method 1 2 3 4 5 6 7 8 9 10 11 12 def check_balance(self): self.kiwoom.reset_opw00018_output() account_number = self.kiwoom.get_login_info("ACCNO") account_number = account_number.split(';')[0] self.kiwoom.set_input_value("계좌번호", account_number) self.kiwoom.comm_rq_data("opw00018_req", "opw00018", 0, "2000") while self.kiwoom.remained_data: time.sleep(0.2) self.kiwoom.set_input_value("계좌번호", account_number) self.kiwoom.comm_rq_data("opw00018_req", "opw00018", 2, "2000")
예수금 데이터를 얻기 위해 opw00001 TR
을 요청하는 코드
opw00001 TR을 요청하는 코드 1 2 3 # opw00001 self.kiwoom.set_input_value("계좌번호", account_number) self.kiwoom.comm_rq_data("opw00001_req", "opw00001", 0, "2000")
데이터가 준비됐다면 데이터를 QTableWidget
객체에 출력하면 됨. self.tableWidget
을 통해 해당 객체에 접근 가능.
예수금 데이터를 QTableWidget
에 출력하기 위해 먼저 self.kiwoom.d2_deposit
에 저장된 예수금 데이터를 QTableWidgetItem
객체로 만들어 줌.
setItem
메서드를 호출해 QTableWidget
객체에 넣으면 됨.
1 2 3 4 item = QTableWidgetItem(self.kiwoom.d2_deposit) item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) self.tableWidget.setItem(0 , 0 , item)
총매입
, 총평가
, 총손익
, 총수익률(%)
, 추정자산
을 QTableWidget
의 칼럼에 추가하는 코드.
데이터는 self.kiwoom.opw00018_output['single']
을 통해 얻어올 수 있음.
1 2 3 4 for i in range(1 , 6 ): item = QTableWidgetItem(self.kiwoom.opw00018_output['single' ][i - 1 ]) item.setTextAlignment(QT.AlignVCenter | Qt.AlignRight) self.tableWidget.setItem(0 , i, item)
resizeRowsToContents
메서드를 호출해서 아이템의 크기에 맞춰 행의 높이를 조절
1 self.tableWidget.resizeRowsToContents()
보유 종목별 평가 잔고 데이터를 추가.
먼저 보유종목의 개수를 확인한 후 행의 개수를 설정
1 2 3 item_count = len(self.kiwoom.opw00018_output['multi]) self.tableWidget_2.setRowCount(item_count)
한 종목에 대한 종목명, 보유량, 매입가, 현재가, 평가손익, 수익률(%)은 출력
1 2 3 4 5 6 for j in range(item_count): row = self.kiwoom.opw00018_output['multi' ][j] for i in range(len(row)): item = QTableWidgetItem(row[i]) item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) self.tableWidget_2.setItem(j, i, item)
check_balance method 전체코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 def check_balance (self) : self.kiwoom.reset_opw00018_output() account_number = self.kiwoom.get_login_info("ACCNO" ) account_number = account_number.split(';' )[0 ] self.kiwoom.set_input_value("계좌번호" , account_number) self.kiwoom.comm_rq_data("opw00018_req" , "opw00018" , 0 , "2000" ) while self.kiwoom.remained_data: time.sleep(0.2 ) self.kiwoom.set_input_value("계좌번호" , account_number) self.kiwoom.comm_rq_data("opw00018_req" , "opw00018" , 2 , "2000" ) self.kiwoom.set_input_value("계좌번호" , account_number) self.kiwoom.comm_rq_data("opw00001_req" , "opw00001" , 0 , "2000" ) item = QTableWidgetItem(self.kiwoom.d2_deposit) item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) self.tableWidget.setItem(0 , 0 , item) for i in range(1 , 6 ): item = QTableWidgetItem(self.kiwoom.opw00018_output['single' ][i - 1 ]) item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) self.tableWidget.setItem(0 , i, item) self.tableWidget.resizeRowsToContents() item_count = len(self.kiwoom.opw00018_output['multi' ]) self.tableWidget_2.setRowCount(item_count) for j in range(item_count): row = self.kiwoom.opw00018_output['multi' ][j] for i in range(len(row)): item = QTableWidgetItem(row[i]) item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) self.tableWidget_2.setItem(j, i, item) self.tableWidget_2.resizeRowsToContents()
실시간 조회
체크 박스 구현.
새로운 타이머 객체를 생성
1 2 3 4 self.timer2 = QTimer(self) self.timer2.start(1000 *10 ) self.timer2.timeout.connect(self.timeout2)
timeout2
메서드 구현.
위의 메서드는 QCheckBox
가 체크 됐는지 확인한 후 데이터 갱신.
데이터 갱신 처리는 check_balance
메서드가 담당하고 있으므로 다음과 같이 구현
1 2 3 def timeout2 (self) : if self.checkBox.isChecked(): self.check_balance()
개발 4일차
선정된 매수/매도 종목을 자동으로 매매하고 해당 종목에 대한 정보를 출력하는 기능 구현.
1) UI 구성 및 매수/매도 목록 파일 생성
선정된 매수/매도 종목에 대한 정보가 이미 buy_list.txt
라는 파일과 sell_list.txt
라는 파일로 저장돼 있다고 가정하고 프로그램을 구현.
2) 선정 종목 정보 출력하기
위에서 생성했던 buy_list.txt
와 sell_list.txt
파일을 읽어서 이름 QTableWidget
객체로 출력하는 기능을 구현.
출력하는 기능은 load_buy_sell_list
라는 이름의 메서드로 구현할 것임.
프로그램 시작되자마자 출력돼야 하므로 MyWindow
클래스의 생성자에서 load_buy_sell_list
를 호출.
1 2 3 4 5 6 7 class MyWindow (QMainWindow, form_class) : sef __init__(self): super().__init__() self.setupUi(self) self.load_buy_sell_list()
load_buy_sell_list method 1 2 3 4 5 6 7 8 def load_buy_sell_list (self) : f = open("buy_list.txt" , 'rt' ) buy_list = f.readlines() f.close() f = open("sell_list.txt" , 'rt' ) sell_list = f.readlines() f.close()
파일로부터 매수/매도 리스트를 읽었다면 데이터의 총 개수를 확인하여 종목 각각에 대한 데이터 개수를 확인한 후 이 두 값을 더한 값을 QTableWidget
객체의 setRowCount
메서드로 설정.
1 2 row_count = len(buy_list) + len(sell_list) self.tablewidget_4.setRowCount(row_count)
1 2 3 4 5 6 7 8 9 10 for j in range(len(buy_list)): row_data = buy_list[j] split_row_data = row_data.split(';' ) split_row_data[1 ] = self.kiwoom.get_master_code_name(split_row_data[1 ].rsplit()) for i in range(len(split_row_data)): item = QTableWidgetItem(split_row_data[i].rstrip()) item.setTextAlignment(Qt.AlignVCenter | Qt.AlignCenter) self.tableWidget_4.setItem(j, i, item)
1 2 3 4 5 6 7 8 9 10 for j in range(len(sell_list)): row_data = sell_list[j] split_row_data = row_data.split(';' ) split_row_data[1 ] = self.kiwoom.get_master_code_name(split_row_data[1 ].rstrip()) for i in range(len(split_row_data)): item = QTableWidgetItem(split_row_data[i].rstrip()) item.setTextAlignment(Qt.AlignVCenter | Qt.AlignCenter) self.tableWidget_4.setItem(len(buy_list) + j, i, item)
load_buy_sell_list method 전체코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 def load_buy_sell_list(self): f = open("buy_list.txt", 'rt') buy_list = f.readlines() f.close() f = open("sell_list.txt", 'rt') sell_list = f.readlines() f.close() row_count = len(buy_list) + len(sell_list) self.tableWidget_4.setRowCount(row_count) # buy list for j in range(len(buy_list)): row_data = buy_list[j] split_row_data = row_data.split(';') split_row_data[1] = self.kiwoom.get_master_code_name(split_row_data[1].rsplit()) for i in range(len(split_row_data)): item = QTableWidgetItem(split_row_data[i].rstrip()) item.setTextAlignment(Qt.AlignVCenter | Qt.AlignCenter) self.tableWidget_4.setItem(j, i, item) # sell list for j in range(len(sell_list)): row_data = sell_list[j] split_row_data = row_data.split(';') split_row_data[1] = self.kiwoom.get_master_code_name(split_row_data[1].rstrip()) for i in range(len(split_row_data)): item = QTableWidgetItem(split_row_data[i].rstrip()) item.setTextAlignment(Qt.AlignVCenter | Qt.AlignCenter) self.tableWidget_4.setItem(len(buy_list) + j, i, item) self.tableWidget_4.resizeRowsToContents()
3) 자동 주문 구현하기
각 거래일의 장 시작에 맞춰 정해진 주문 방식에 따라 주문을 수행하는 간단한 방식을 사용.
자동 주문하는 기능은 MyWindow 클래스
의 trade_stocks
메서드에 구현.
1 2 3 4 5 6 7 8 9 10 def trade_stocks (self) : hoga_lookup = {'지정가' : "00" , '시장가' : "03" } f = open("buy_list.txt" , 'rt' ) buy_list = f.readlines() f.close() f = oepn("sell_list.txt" , 'rt' ) sell_list = f.readlines() f.close()
주문할 때 필요한 계좌 정보를 QComboBox
위젯으로부터 얻어 옴.
1 account = self.comboBox.currentText()
buy_list
로 부터 데이터를 하나씩 얻어온 후 문자열을 분리해서 주문에 필요한 정보(거래구분, 종목코드, 수량, 가격)를 준비.
읽어 온 데이터의 주문 수행 여부가 '매수전’인 경우에만 해당 주문 데이터를 토대로 send_order
메서드를 통해 매수 주문을 수행.
1 2 3 4 5 6 7 8 9 10 for row_data in buy_list: split_row_data = row_data.split(';' ) hoga = split_row_data[2 ] code = split_row_data[1 ] num = split_row_data[3 ] price = split_row_data[4 ] if split_row_data[-1 ].rstrip() == '매수전' : self.kiwoom.send_order("send_order_req" , "0101" , account, 1 , code, num, price, hoga_lookup[hoga], "" )
매도 주문 역시 동일한 방식으로 처리
1 2 3 4 5 6 7 8 9 10 for row_data in sell_list: split_row_data = row_data.split(';' ) hoga = split_row_data[2 ] code = split_row_data[1 ] num = split_row_data[3 ] price = split_row_data[4 ] if split_row_data[-1 ].rstrip() == '매도전' : self.kiwoom.send_order("send_order_req" , "0101" , account, 2 , code, num, price, hoga_lookup[hoga], "" )
매매 주문이 완료되면 buy_list.txt
에 저장된 주문 여부를 업데이트.
앞서 매매 주문을 실행했기 때문에 매수전
과 매도전
을 주문완료
로 변경.
1 2 3 4 5 6 7 8 9 for i, row_data in enumerate(buy_list): buy_list[i] = buy_list[i].replace("매수전" , "주문완료" ) f = open("buy_list.txt" , 'wt' ) for row_data in buy_list: f.write(row_data) f.close()
trade_stocks 전체코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 def trade_stocks (self) : hoga_lookup = {'지정가' : "00" , '시장가' : "03" } f = open("buy_list.txt" , 'rt' ) buy_list = f.readlines() f.close() f = open("sell_list.txt" , 'rt' ) sell_list = f.readlines() f.close() account = self.comboBox.currentText() for row_data in buy_list: split_row_data = row_data.split(';' ) hoga = split_row_data[2 ] code = split_row_data[1 ] num = split_row_data[3 ] price = split_row_data[4 ] if split_row_data[-1 ].rstrip() == '매수전' : self.kiwoom.send_order("send_order_req" , "0101" , account, 1 , code, num, price, hoga_lookup[hoga], "" ) for row_data in sell_list: split_row_data = row_data.split(';' ) hoga = split_row_data[2 ] code = split_row_data[1 ] num = split_row_data[3 ] price = split_row_data[4 ] if split_row_data[-1 ].rstrip() == '매도전' : self.kiwoom.send_order("send_order_req" , "0101" , account, 2 , code, num, price, hoga_lookup[hoga], "" ) for i, row_data in enumerate(buy_list): buy_list[i] = buy_list[i].replace("매수전" , "주문완료" ) f = open("buy_list.txt" , 'wt' ) for row_data in buy_list: f.write(row_data) f.close() for i, row_data in enumerate(sell_list): sell_list[i] = sell_list[i].replace("매도전" , "주문완료" ) f = open("sell_list.txt" , 'wt' ) for row_data in sell_list: f.write(row_data) f.close()
timeout method 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def timeout(self): market_start_time = QTime(9, 0, 0) current_time = QTime.currentTime() if current_time > market_start_time and self.trade_stocks_done is False: self.trade_stocks() self.trade_stocks_done = True text_time = current_time.toString("hh:mm:ss") time_msg = "현재시간: " + text_time state = self.kiwoom.get_connect_state() if state == 1: state_msg = "서버 연결 중" else: state_msg = "서버 미 연결 중" self.statusbar.showMessage(state_msg + " | " + time_msg)
MyWindow
클래스의 생성자에 trade_stocks_done
속성을 추가.
참고로 trade_stocks_done
은 생성자에게 Qtimer
객체를 생성하는 코드보다 먼저 위치해야 함.
1 2 3 4 5 6 class MyWindow (QMainWindow, form_class) : def __init__ (self) : super().__init__() self.setupUi(self) self.trade_stocks_done = False
Link
파이썬으로 배우는 알고리즘 트레이딩