├── Kiwoom_datareader_v0.1.py ├── Kiwoom_datareader_v0.1.ui ├── README.md ├── decorators.py ├── kiwoomAPI.py ├── sample_img ├── daily.PNG ├── mainwindow.gif └── minute.PNG ├── stock_price.db └── tr_receive_handler.py /Kiwoom_datareader_v0.1.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import datetime 5 | import pandas as pd 6 | import sqlite3 7 | 8 | from PyQt5.QtWidgets import * 9 | from PyQt5.QtCore import * 10 | from PyQt5 import uic 11 | 12 | from kiwoomAPI import KiwoomAPI 13 | import decorators 14 | 15 | form_class = uic.loadUiType("Kiwoom_datareader_v0.1.ui")[0] 16 | 17 | 18 | class MainWindow(QMainWindow, form_class): 19 | def __init__(self): 20 | super().__init__() 21 | self.setupUi(self) 22 | 23 | self.kw = KiwoomAPI() 24 | 25 | # login 26 | self.kw.comm_connect() 27 | 28 | # status bar 에 출력할 메세지를 저장하는 변수 29 | # 어떤 모듈의 실행 완료를 나타낼 때 쓰인다. 30 | self.return_status_msg = '' 31 | 32 | # timer 등록. tick per 1s 33 | self.timer_1s = QTimer(self) 34 | self.timer_1s.start(1000) 35 | self.timer_1s.timeout.connect(self.timeout_1s) 36 | 37 | # label '종목코드' 오른쪽 lineEdit 값이 변경 될 시 실행될 함수 연결 38 | self.lineEdit.textChanged.connect(self.code_changed) 39 | 40 | # pushButton '실행'이 클릭될 시 실행될 함수 연결 41 | self.pushButton.clicked.connect(self.fetch_chart_data) 42 | 43 | def timeout_1s(self): 44 | current_time = QTime.currentTime() 45 | 46 | text_time = current_time.toString("hh:mm:ss") 47 | time_msg = "현재시간: " + text_time 48 | 49 | state = self.kw.get_connect_state() 50 | if state == 1: 51 | state_msg = "서버 연결 중" 52 | else: 53 | state_msg = "서버 미 연결 중" 54 | 55 | if self.return_status_msg == '': 56 | statusbar_msg = state_msg + " | " + time_msg 57 | else: 58 | statusbar_msg = state_msg + " | " + time_msg + " | " + self.return_status_msg 59 | 60 | self.statusbar.showMessage(statusbar_msg) 61 | 62 | # label '종목' 우측의 lineEdit의 이벤트 핸들러 63 | def code_changed(self): 64 | code = self.lineEdit.text() 65 | name = self.kw.get_master_code_name(code) 66 | self.lineEdit_2.setText(name) 67 | 68 | @decorators.return_status_msg_setter 69 | def fetch_chart_data(self): 70 | 71 | code = self.lineEdit.text() 72 | tick_unit = self.comboBox.currentText() 73 | # 일단 tick range = 1 인 경우만 구현함. 74 | # tick_range = self.comboBox_2.currentText() 75 | tick_range = 1 76 | 77 | input_dict = {} 78 | ohlcv = None 79 | 80 | if tick_unit == '일봉': 81 | # 일봉 조회의 경우 현재 날짜부터 과거의 데이터를 조회함 82 | base_date = datetime.datetime.today().strftime('%Y%m%d') 83 | input_dict['종목코드'] = code 84 | input_dict['기준일자'] = base_date 85 | input_dict['수정주가구분'] = 1 86 | 87 | self.kw.set_input_value(input_dict) 88 | self.kw.comm_rq_data("opt10081_req", "opt10081", 0, "0101") 89 | ohlcv = self.kw.latest_tr_data 90 | 91 | while self.kw.is_tr_data_remained == True: 92 | self.kw.set_input_value(input_dict) 93 | self.kw.comm_rq_data("opt10081_req", "opt10081", 2, "0101") 94 | for key, val in self.kw.latest_tr_data.items(): 95 | ohlcv[key][-1:] = val 96 | 97 | elif tick_unit == '분봉': 98 | # 일봉 조회의 경우 현재 날짜부터 과거의 데이터를 조회함 99 | # 현 시점부터 과거로 약 160일(약 60000개)의 데이터까지만 제공된다. (2018-02-20) 100 | base_date = datetime.datetime.today().strftime('%Y%m%d') 101 | input_dict['종목코드'] = code 102 | input_dict['틱범위'] = tick_range 103 | input_dict['수정주가구분'] = 1 104 | 105 | self.kw.set_input_value(input_dict) 106 | self.kw.comm_rq_data("opt10080_req", "opt10080", 0, "0101") 107 | ohlcv = self.kw.latest_tr_data 108 | 109 | while self.kw.is_tr_data_remained == True: 110 | self.kw.set_input_value(input_dict) 111 | self.kw.comm_rq_data("opt10080_req", "opt10080", 2, "0101") 112 | for key, val in self.kw.latest_tr_data.items(): 113 | ohlcv[key][-1:] = val 114 | 115 | df = pd.DataFrame(ohlcv, columns=['open', 'high', 'low', 'close', 'volume'], 116 | index=ohlcv['date']) 117 | 118 | con = sqlite3.connect("./stock_price.db") 119 | df.to_sql(code, con, if_exists='replace') 120 | 121 | 122 | if __name__ == "__main__": 123 | app = QApplication(sys.argv) 124 | mainWindow = MainWindow() 125 | mainWindow.show() 126 | app.exec_() 127 | -------------------------------------------------------------------------------- /Kiwoom_datareader_v0.1.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 526 10 | 502 11 | 12 | 13 | 14 | Kiwoom_datareader v0.1 15 | 16 | 17 | 18 | 19 | 20 | 110 21 | 60 22 | 301 23 | 291 24 | 25 | 26 | 27 | 키움증권 서버에서 주가 데이터 가져오기 28 | 29 | 30 | 31 | 32 | 110 33 | 240 34 | 93 35 | 28 36 | 37 | 38 | 39 | 실행 40 | 41 | 42 | 43 | 44 | 45 | 20 46 | 30 47 | 64 48 | 15 49 | 50 | 51 | 52 | 종목코드 53 | 54 | 55 | 56 | 57 | 58 | 20 59 | 90 60 | 64 61 | 15 62 | 63 | 64 | 65 | 틱 단위 66 | 67 | 68 | 69 | 70 | 71 | 20 72 | 120 73 | 64 74 | 15 75 | 76 | 77 | 78 | 틱 범위 79 | 80 | 81 | 82 | 83 | 84 | 100 85 | 30 86 | 113 87 | 21 88 | 89 | 90 | 91 | 92 | 93 | 94 | 100 95 | 90 96 | 111 97 | 22 98 | 99 | 100 | 101 | 102 | 일봉 103 | 104 | 105 | 106 | 107 | 분봉 108 | 109 | 110 | 111 | 112 | 113 | false 114 | 115 | 116 | 117 | 100 118 | 120 119 | 111 120 | 22 121 | 122 | 123 | 124 | 125 | 1 126 | 127 | 128 | 129 | 130 | 3 131 | 132 | 133 | 134 | 135 | 5 136 | 137 | 138 | 139 | 140 | 10 141 | 142 | 143 | 144 | 145 | 15 146 | 147 | 148 | 149 | 150 | 30 151 | 152 | 153 | 154 | 155 | 45 156 | 157 | 158 | 159 | 160 | 60 161 | 162 | 163 | 164 | 165 | 166 | 167 | 100 168 | 60 169 | 111 170 | 21 171 | 172 | 173 | 174 | background-color: rgb(170, 255, 255); 175 | 176 | 177 | true 178 | 179 | 180 | 181 | 182 | 183 | 20 184 | 60 185 | 64 186 | 15 187 | 188 | 189 | 190 | 종목이름 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 0 199 | 0 200 | 526 201 | 26 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kiwoom_datareader 2 | 키움증권 Open API를 사용하여 주가 데이터를 받아오는 PyQt 기반의 프로그램입니다. 3 | 4 | 데이터를 받아오는 기능만 구현하려한 것이 아니기때문에, 재사용성과 확장성을 염두에 두고 개발하였습니다. 5 | 프로그램 기능을 확장할 예정이므로 binary file로 배포하지 않습니다. 6 | 7 | [파이썬으로 배우는 알고리즘 트레이딩]를 참고하여 구현하였습니다. 8 | 9 | [파이썬으로 배우는 알고리즘 트레이딩]:https://wikidocs.net/book/110 10 | 11 | ## 실행 / 개발 환경 12 | 13 | 14 | [파이썬으로 배우는 알고리즘 트레이딩]을 참고하시기 바랍니다. 15 | 16 | 1. 키움증권 가입 & 키움증권 Open API 사용 허가 & Open API 모듈 설치 17 | 2. Anaconda 32-bit 설치 18 | 만약 Anaconda 64-bit을 사용하고 있는 경우 19 | - 32-bit도 설치 또는, 20 | - `set CONDA_FORCE_32BIT`을 이용하여 32-bit 가상환경을 만들어야 합니다. 21 | 3. 32-bit anaconda 가상환경에서 `python=3.5`, `pyqt5`, `sqlite3`, `pandas` 설치 22 | 23 | 24 | 25 | ## Preview 26 | ##### 실행화면 27 | ![mainwindow](./sample_img/mainwindow.gif) 28 | ##### 일봉 데이터 (SQLite3로 저장) 29 | ![daily](./sample_img/daily.PNG) 30 | ##### 분봉 데이터 (SQLite3로 저장) 31 | ![minute](./sample_img/minute.PNG) 32 | 33 | --- 34 | 35 | ##### *키움증권 OPEN API제한사항* 36 | (2018.02.20 기준) 37 | 현재 키움증권 OPEN API는 **분봉** 데이터를 조회 시점 기준으로 약 160일 전 까지의 데이터를 제공합니다. 38 | 데이터 개수로는 약 60000분 의 데이터입니다. 39 | 40 | 원했던 것 보다 데이터 양이 적기 때문에, 과거 분/틱 단위 데이터를 받아오는데에는 적합하지 않다고 생각합니다. 41 | 42 | 더 많은 데이터를 제공하는 증권사를 이용해볼 생각입니다. 43 | + (18.02.23 내용추가) 44 | 대신증권 Plus API에서는 45 | 46 | 1분봉 약 18.5만개(약 2년치 데이터) 조회 가능. 47 | 5분봉 약 9만개(약 5년치 데이터) 조회 가능. 48 | https://github.com/gyusu/Creon-Datareader 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /decorators.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import datetime 4 | 5 | 6 | def call_printer(original_func): 7 | """original 함수 call 시, 현재 시간과 함수 명을 출력하는 데코레이터""" 8 | 9 | def wrapper(*args, **kwargs): 10 | timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') 11 | print('[{:.22s}] func `{}` is called'.format(timestamp, original_func.__name__)) 12 | return original_func(*args, **kwargs) 13 | 14 | return wrapper 15 | 16 | 17 | def return_status_msg_setter(original_func): 18 | """ 19 | original 함수 exit 후, QMainWindow 인스턴스의 statusbar에 표시할 문자열을 수정하는 데코레이터 20 | 이 데코레이터는 QMainWindow 클래스의 메소드에만 사용하여야 함 21 | """ 22 | 23 | def wrapper(self): 24 | ret = original_func(self) 25 | 26 | timestamp = datetime.datetime.now().strftime('%H:%M:%S') 27 | 28 | # args[0]는 인스턴스 (즉, self)를 의미한다. 29 | self.return_status_msg = '`{}` 완료됨[{}]'.format(original_func.__name__, timestamp) 30 | return ret 31 | 32 | return wrapper 33 | -------------------------------------------------------------------------------- /kiwoomAPI.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import time 5 | 6 | from PyQt5.QtWidgets import * 7 | from PyQt5.QAxContainer import * 8 | from PyQt5.QtCore import * 9 | 10 | import decorators 11 | 12 | TR_REQ_TIME_INTERVAL = 0.2 13 | 14 | 15 | class KiwoomAPI(QAxWidget): 16 | def __init__(self): 17 | super().__init__() 18 | self._create_kiwoom_instance() 19 | self._set_signal_slots() 20 | 21 | def _create_kiwoom_instance(self): 22 | self.setControl("KHOPENAPI.KHOpenAPICtrl.1") 23 | 24 | def _set_signal_slots(self): 25 | # Login 요청 후 서버가 발생시키는 이벤트의 핸들러 등록 26 | self.OnEventConnect.connect(self._on_event_connect) 27 | 28 | # 조회 요청 후 서버가 발생시키는 이벤트의 핸들러 등록 29 | self.OnReceiveTrData.connect(self._on_receive_tr_data) 30 | 31 | def _on_event_connect(self, err_code): 32 | if err_code == 0: 33 | print("connected") 34 | else: 35 | print("disconnected") 36 | 37 | self.login_event_loop.exit() 38 | 39 | def _on_receive_tr_data(self, screen_no, rqname, trcode, record_name, next, 40 | unused1, unused2, unused3, unused4): 41 | import tr_receive_handler as tr 42 | 43 | self.latest_tr_data = None 44 | 45 | if next == '2': 46 | self.is_tr_data_remained = True 47 | else: 48 | self.is_tr_data_remained = False 49 | 50 | if rqname == "opt10081_req": 51 | self.latest_tr_data = tr.on_receive_opt10081(self, rqname, trcode) 52 | elif rqname == "opt10080_req": 53 | self.latest_tr_data = tr.on_receive_opt10080(self, rqname, trcode) 54 | 55 | try: 56 | self.tr_event_loop.exit() 57 | except AttributeError: 58 | pass 59 | 60 | def comm_connect(self): 61 | """Login 요청 후 서버가 이벤트 발생시킬 때까지 대기하는 메소드""" 62 | self.dynamicCall("CommConnect()") 63 | self.login_event_loop = QEventLoop() 64 | self.login_event_loop.exec_() 65 | 66 | @decorators.call_printer 67 | def comm_rq_data(self, rqname, trcode, next, screen_no): 68 | """ 69 | 서버에 조회 요청을 하는 메소드 70 | 이 메소드 호출 이전에 set_input_value 메소드를 수차례 호출하여 INPUT을 설정해야 함 71 | """ 72 | self.dynamicCall("CommRqData(QString, QString, int, QString)", rqname, trcode, next, screen_no) 73 | self.tr_event_loop = QEventLoop() 74 | self.tr_event_loop.exec_() 75 | 76 | # 키움 Open API는 시간당 request 제한이 있기 때문에 딜레이를 줌 77 | time.sleep(TR_REQ_TIME_INTERVAL) 78 | 79 | def comm_get_data(self, code, real_type, field_name, index, item_name): 80 | ret = self.dynamicCall("CommGetData(QString, QString, QString, int, QString)", code, 81 | real_type, field_name, index, item_name) 82 | return ret.strip() 83 | 84 | def get_code_list_by_market(self, market): 85 | """market의 모든 종목코드를 서버로부터 가져와 반환하는 메소드""" 86 | code_list = self.dynamicCall("GetCodeListByMarket(QString)", market) 87 | code_list = code_list.split(';') 88 | return code_list[:-1] 89 | 90 | def get_master_code_name(self, code): 91 | """종목코드를 받아 종목이름을 반환하는 메소드""" 92 | code_name = self.dynamicCall("GetMasterCodeName(QString)", code) 93 | return code_name 94 | 95 | def get_connect_state(self): 96 | """서버와의 연결 상태를 반환하는 메소드""" 97 | ret = self.dynamicCall("GetConnectState()") 98 | return ret 99 | 100 | def set_input_value(self, input_dict): 101 | """ 102 | CommRqData 함수를 통해 서버에 조회 요청 시, 103 | 요청 이전에 SetInputValue 함수를 수차례 호출하여 해당 요청에 필요한 104 | INPUT 을 넘겨줘야 한다. 105 | """ 106 | for key, val in input_dict.items(): 107 | self.dynamicCall("SetInputValue(QString, QString)", key, val) 108 | 109 | def get_repeat_cnt(self, trcode, rqname): 110 | ret = self.dynamicCall("GetRepeatCnt(QString, QString)", trcode, rqname) 111 | return ret 112 | 113 | def get_server_gubun(self): 114 | """ 115 | 실투자 환경인지 모의투자 환경인지 구분하는 메소드 116 | 실투자, 모의투자에 따라 데이터 형식이 달라지는 경우가 있다. 대표적으로 opw00018 데이터의 소수점 117 | """ 118 | ret = self.dynamicCall("KOA_Functions(QString, QString)", "GetServerGubun", "") 119 | return ret 120 | 121 | def get_login_info(self, tag): 122 | """ 123 | 계좌 정보 및 로그인 사용자 정보를 얻어오는 메소드 124 | """ 125 | ret = self.dynamicCall("GetLoginInfo(QString)", tag) 126 | return ret 127 | 128 | 129 | # C++과 python destructors 간의 충돌 방지를 위해 전역 설정 130 | # garbage collect 순서를 맨 마지막으로 강제함 131 | # 사실, 이 파일을 __main__으로 하지 않는경우에는 고려 안해도 무방 132 | app = None 133 | 134 | 135 | def main(): 136 | global app 137 | app = QApplication(sys.argv) 138 | kiwoom = KiwoomAPI() 139 | kiwoom.comm_connect() 140 | 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /sample_img/daily.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyusu/Kiwoom_datareader/a30a73971a650cd3d20a780d4cf9b4f59806e3f5/sample_img/daily.PNG -------------------------------------------------------------------------------- /sample_img/mainwindow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyusu/Kiwoom_datareader/a30a73971a650cd3d20a780d4cf9b4f59806e3f5/sample_img/mainwindow.gif -------------------------------------------------------------------------------- /sample_img/minute.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyusu/Kiwoom_datareader/a30a73971a650cd3d20a780d4cf9b4f59806e3f5/sample_img/minute.PNG -------------------------------------------------------------------------------- /stock_price.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyusu/Kiwoom_datareader/a30a73971a650cd3d20a780d4cf9b4f59806e3f5/stock_price.db -------------------------------------------------------------------------------- /tr_receive_handler.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ 4 | 이 모듈은 Kiwoom.py의 Kiwoom 클래스 내의 _on_receive_tr_data 메소드에서만 사용하도록 구현됨. 5 | TR마다 각각의 메소드를 일일이 작성해야하기 때문에 기존 클래스에서 분리하였음 6 | """ 7 | 8 | # cyclic import 를 피하며 type annotation 을 하기 위해 TYPE_CHECKING 을 이용함 9 | # (https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports) 10 | from typing import TYPE_CHECKING 11 | 12 | if TYPE_CHECKING: 13 | from kiwoomAPI import KiwoomAPI 14 | 15 | 16 | def on_receive_opt10080(kw: 'KiwoomAPI', rqname, trcode): 17 | """주식분봉차트조회요청 완료 후 서버에서 보내준 데이터를 받는 메소드""" 18 | 19 | data_cnt = kw.get_repeat_cnt(trcode, rqname) 20 | ohlcv = {'date': [], 'open': [], 'high': [], 'low': [], 'close': [], 'volume': []} 21 | 22 | for i in range(data_cnt): 23 | date = kw.comm_get_data(trcode, "", rqname, i, "체결시간") 24 | open = kw.comm_get_data(trcode, "", rqname, i, "시가") 25 | high = kw.comm_get_data(trcode, "", rqname, i, "고가") 26 | low = kw.comm_get_data(trcode, "", rqname, i, "저가") 27 | close = kw.comm_get_data(trcode, "", rqname, i, "현재가") 28 | volume = kw.comm_get_data(trcode, "", rqname, i, "거래량") 29 | 30 | ohlcv['date'].append(date) 31 | ohlcv['open'].append(abs(int(open))) 32 | ohlcv['high'].append(abs(int(high))) 33 | ohlcv['low'].append(abs(int(low))) 34 | ohlcv['close'].append(abs(int(close))) 35 | ohlcv['volume'].append(int(volume)) 36 | 37 | return ohlcv 38 | 39 | 40 | def on_receive_opt10081(kw: 'KiwoomAPI', rqname, trcode): 41 | """주식일봉차트조회요청 완료 후 서버에서 보내준 데이터를 받는 메소드""" 42 | 43 | data_cnt = kw.get_repeat_cnt(trcode, rqname) 44 | ohlcv = {'date': [], 'open': [], 'high': [], 'low': [], 'close': [], 'volume': []} 45 | 46 | for i in range(data_cnt): 47 | date = kw.comm_get_data(trcode, "", rqname, i, "일자") 48 | open = kw.comm_get_data(trcode, "", rqname, i, "시가") 49 | high = kw.comm_get_data(trcode, "", rqname, i, "고가") 50 | low = kw.comm_get_data(trcode, "", rqname, i, "저가") 51 | close = kw.comm_get_data(trcode, "", rqname, i, "현재가") 52 | volume = kw.comm_get_data(trcode, "", rqname, i, "거래량") 53 | 54 | ohlcv['date'].append(date) 55 | ohlcv['open'].append(int(open)) 56 | ohlcv['high'].append(int(high)) 57 | ohlcv['low'].append(int(low)) 58 | ohlcv['close'].append(int(close)) 59 | ohlcv['volume'].append(int(volume)) 60 | 61 | return ohlcv 62 | --------------------------------------------------------------------------------