├── Kiwoom.py ├── README.md ├── buy_list.txt ├── kiwoomauto.py ├── logging.conf ├── pytrader.png ├── pytrader.py ├── pytrader.ui └── sell_list.txt /Kiwoom.py: -------------------------------------------------------------------------------- 1 | """ 2 | Kiwoom 클래스는 OCX를 통해 API 함수를 호출할 수 있도록 구현되어 있습니다. 3 | OCX 사용을 위해 QAxWidget 클래스를 상속받아서 구현하였으며, 4 | 주식(현물) 거래에 필요한 메서드들만 구현하였습니다. 5 | 6 | author: 서경동 7 | last edit: 2017. 02. 05 8 | """ 9 | 10 | 11 | import sys 12 | import logging 13 | import logging.config 14 | from PyQt5.QAxContainer import QAxWidget 15 | from PyQt5.QtCore import QEventLoop 16 | from PyQt5.QtWidgets import QApplication 17 | from pandas import DataFrame 18 | 19 | 20 | class Kiwoom(QAxWidget): 21 | 22 | def __init__(self): 23 | super().__init__() 24 | 25 | self.setControl("KHOPENAPI.KHOpenAPICtrl.1") 26 | 27 | # Loop 변수 28 | # 비동기 방식으로 동작되는 이벤트를 동기화(순서대로 동작) 시킬 때 29 | self.loginLoop = None 30 | self.requestLoop = None 31 | self.orderLoop = None 32 | self.conditionLoop = None 33 | 34 | # 서버구분 35 | self.server = None 36 | 37 | # 조건식 38 | self.condition = None 39 | 40 | # 에러 41 | self.error = None 42 | 43 | # 주문번호 44 | self.orderNo = "" 45 | 46 | # 조회 47 | self.inquiry = 0 48 | 49 | # 서버에서 받은 메시지 50 | self.msg = "" 51 | 52 | # 예수금 d+2 53 | self.opw00001Data = 0 54 | 55 | # 보유종목 정보 56 | self.opw00018Data = {'accountEvaluation': [], 'stocks': []} 57 | 58 | # signal & slot 59 | self.OnEventConnect.connect(self.eventConnect) 60 | self.OnReceiveTrData.connect(self.receiveTrData) 61 | self.OnReceiveChejanData.connect(self.receiveChejanData) 62 | self.OnReceiveRealData.connect(self.receiveRealData) 63 | self.OnReceiveMsg.connect(self.receiveMsg) 64 | self.OnReceiveConditionVer.connect(self.receiveConditionVer) 65 | self.OnReceiveTrCondition.connect(self.receiveTrCondition) 66 | self.OnReceiveRealCondition.connect(self.receiveRealCondition) 67 | 68 | # 로깅용 설정파일 69 | logging.config.fileConfig('logging.conf') 70 | self.log = logging.getLogger('Kiwoom') 71 | 72 | ############################################################### 73 | # 로깅용 메서드 정의 # 74 | ############################################################### 75 | 76 | def logger(origin): 77 | def wrapper(*args, **kwargs): 78 | args[0].log.debug('{} args - {}, kwargs - {}'.format(origin.__name__, args, kwargs)) 79 | return origin(*args, **kwargs) 80 | 81 | return wrapper 82 | 83 | ############################################################### 84 | # 이벤트 정의 # 85 | ############################################################### 86 | 87 | def eventConnect(self, returnCode): 88 | """ 89 | 통신 연결 상태 변경시 이벤트 90 | 91 | returnCode가 0이면 로그인 성공 92 | 그 외에는 ReturnCode 클래스 참조. 93 | 94 | :param returnCode: int 95 | """ 96 | 97 | try: 98 | if returnCode == ReturnCode.OP_ERR_NONE: 99 | 100 | self.server = self.getLoginInfo("GetServerGubun", True) 101 | 102 | if len(self.server) == 0 or self.server != "1": 103 | self.msg += "실서버 연결 성공" + "\r\n\r\n" 104 | 105 | else: 106 | self.msg += "모의투자서버 연결 성공" + "\r\n\r\n" 107 | 108 | else: 109 | self.msg += "연결 끊김: 원인 - " + ReturnCode.CAUSE[returnCode] + "\r\n\r\n" 110 | 111 | except Exception as error: 112 | self.log.error('eventConnect {}'.format(error)) 113 | 114 | finally: 115 | # commConnect() 메서드에 의해 생성된 루프를 종료시킨다. 116 | # 로그인 후, 통신이 끊길 경우를 대비해서 예외처리함. 117 | try: 118 | self.loginLoop.exit() 119 | except AttributeError: 120 | pass 121 | 122 | def receiveMsg(self, screenNo, requestName, trCode, msg): 123 | """ 124 | 수신 메시지 이벤트 125 | 126 | 서버로 어떤 요청을 했을 때(로그인, 주문, 조회 등), 그 요청에 대한 처리내용을 전달해준다. 127 | 128 | :param screenNo: string - 화면번호(4자리, 사용자 정의, 서버에 조회나 주문을 요청할 때 이 요청을 구별하기 위한 키값) 129 | :param requestName: string - TR 요청명(사용자 정의) 130 | :param trCode: string 131 | :param msg: string - 서버로 부터의 메시지 132 | """ 133 | 134 | self.msg += requestName + ": " + msg + "\r\n\r\n" 135 | 136 | def receiveTrData(self, screenNo, requestName, trCode, recordName, inquiry, 137 | deprecated1, deprecated2, deprecated3, deprecated4): 138 | """ 139 | TR 수신 이벤트 140 | 141 | 조회요청 응답을 받거나 조회데이터를 수신했을 때 호출됩니다. 142 | requestName과 trCode는 commRqData()메소드의 매개변수와 매핑되는 값 입니다. 143 | 조회데이터는 이 이벤트 메서드 내부에서 getCommData() 메서드를 이용해서 얻을 수 있습니다. 144 | 145 | :param screenNo: string - 화면번호(4자리) 146 | :param requestName: string - TR 요청명(commRqData() 메소드 호출시 사용된 requestName) 147 | :param trCode: string 148 | :param recordName: string 149 | :param inquiry: string - 조회('0': 남은 데이터 없음, '2': 남은 데이터 있음) 150 | """ 151 | 152 | print("receiveTrData 실행: ", screenNo, requestName, trCode, recordName, inquiry) 153 | 154 | # 주문번호와 주문루프 155 | self.orderNo = self.commGetData(trCode, "", requestName, 0, "주문번호") 156 | 157 | try: 158 | self.orderLoop.exit() 159 | except AttributeError: 160 | pass 161 | 162 | self.inquiry = inquiry 163 | 164 | if requestName == "관심종목정보요청": 165 | data = self.getCommDataEx(trCode, "관심종목정보") 166 | print(type(data)) 167 | print(data) 168 | 169 | """ commGetData 170 | cnt = self.getRepeatCnt(trCode, requestName) 171 | 172 | for i in range(cnt): 173 | data = self.commGetData(trCode, "", requestName, i, "종목명") 174 | print(data) 175 | """ 176 | 177 | elif requestName == "주식일봉차트조회요청": 178 | data = self.getCommDataEx(trCode, "주식일봉차트조회") 179 | 180 | colName = ['종목코드', '현재가', '거래량', '거래대금', '일자', '시가', '고가', '저가', 181 | '수정주가구분', '수정비율', '대업종구분', '소업종구분', '종목정보', '수정주가이벤트', '전일종가'] 182 | 183 | data = DataFrame(data, columns=colName) 184 | 185 | print(type(data)) 186 | print(data.head(5)) 187 | 188 | """ commGetData 189 | cnt = self.getRepeatCnt(trCode, requestName) 190 | 191 | for i in range(cnt): 192 | date = self.commGetData(trCode, "", requestName, i, "일자") 193 | open = self.commGetData(trCode, "", requestName, i, "시가") 194 | high = self.commGetData(trCode, "", requestName, i, "고가") 195 | low = self.commGetData(trCode, "", requestName, i, "저가") 196 | close = self.commGetData(trCode, "", requestName, i, "현재가") 197 | print(date, ": ", open, ' ', high, ' ', low, ' ', close) 198 | """ 199 | 200 | elif requestName == "예수금상세현황요청": 201 | deposit = self.commGetData(trCode, "", requestName, 0, "d+2추정예수금") 202 | deposit = self.changeFormat(deposit) 203 | self.opw00001Data = deposit 204 | 205 | elif requestName == "계좌평가잔고내역요청": 206 | # 계좌 평가 정보 207 | accountEvaluation = [] 208 | keyList = ["총매입금액", "총평가금액", "총평가손익금액", "총수익률(%)", "추정예탁자산"] 209 | 210 | for key in keyList: 211 | value = self.commGetData(trCode, "", requestName, 0, key) 212 | 213 | if key.startswith("총수익률"): 214 | value = self.changeFormat(value, 1) 215 | else: 216 | value = self.changeFormat(value) 217 | 218 | accountEvaluation.append(value) 219 | 220 | self.opw00018Data['accountEvaluation'] = accountEvaluation 221 | 222 | # 보유 종목 정보 223 | cnt = self.getRepeatCnt(trCode, requestName) 224 | keyList = ["종목명", "보유수량", "매입가", "현재가", "평가손익", "수익률(%)"] 225 | 226 | for i in range(cnt): 227 | stock = [] 228 | 229 | for key in keyList: 230 | value = self.commGetData(trCode, "", requestName, i, key) 231 | 232 | if key.startswith("수익률"): 233 | value = self.changeFormat(value, 2) 234 | elif key != "종목명": 235 | value = self.changeFormat(value) 236 | 237 | stock.append(value) 238 | 239 | self.opw00018Data['stocks'].append(stock) 240 | 241 | try: 242 | self.requestLoop.exit() 243 | except AttributeError: 244 | pass 245 | 246 | def receiveRealData(self, code, realType, realData): 247 | """ 248 | 실시간 데이터 수신 이벤트 249 | 250 | 실시간 데이터를 수신할 때 마다 호출되며, 251 | setRealReg() 메서드로 등록한 실시간 데이터도 이 이벤트 메서드에 전달됩니다. 252 | getCommRealData() 메서드를 이용해서 실시간 데이터를 얻을 수 있습니다. 253 | 254 | :param code: string - 종목코드 255 | :param realType: string - 실시간 타입(KOA의 실시간 목록 참조) 256 | :param realData: string - 실시간 데이터 전문 257 | """ 258 | 259 | try: 260 | self.log.debug("[receiveRealData]") 261 | self.log.debug("({})".format(realType)) 262 | 263 | if realType not in RealType.REALTYPE: 264 | return 265 | 266 | data = [] 267 | 268 | if code != "": 269 | data.append(code) 270 | codeOrNot = code 271 | else: 272 | codeOrNot = realType 273 | 274 | for fid in sorted(RealType.REALTYPE[realType].keys()): 275 | value = self.getCommRealData(codeOrNot, fid) 276 | data.append(value) 277 | 278 | # TODO: DB에 저장 279 | self.log.debug(data) 280 | 281 | except Exception as e: 282 | self.log.error('{}'.format(e)) 283 | 284 | def receiveChejanData(self, gubun, itemCnt, fidList): 285 | """ 286 | 주문 접수/확인 수신시 이벤트 287 | 288 | 주문요청후 주문접수, 체결통보, 잔고통보를 수신할 때 마다 호출됩니다. 289 | 290 | :param gubun: string - 체결구분('0': 주문접수/주문체결, '1': 잔고통보, '3': 특이신호) 291 | :param itemCnt: int - fid의 갯수 292 | :param fidList: string - fidList 구분은 ;(세미콜론) 이다. 293 | """ 294 | 295 | fids = fidList.split(';') 296 | print("[receiveChejanData]") 297 | print("gubun: ", gubun, "itemCnt: ", itemCnt, "fidList: ", fidList) 298 | print("========================================") 299 | print("[ 구분: ", self.getChejanData(913) if '913' in fids else '잔고통보', "]") 300 | for fid in fids: 301 | print(FidList.CHEJAN[int(fid)] if int(fid) in FidList.CHEJAN else fid, ": ", self.getChejanData(int(fid))) 302 | print("========================================") 303 | 304 | ############################################################### 305 | # 메서드 정의: 로그인 관련 메서드 # 306 | ############################################################### 307 | 308 | def commConnect(self): 309 | """ 310 | 로그인을 시도합니다. 311 | 312 | 수동 로그인일 경우, 로그인창을 출력해서 로그인을 시도. 313 | 자동 로그인일 경우, 로그인창 출력없이 로그인 시도. 314 | """ 315 | 316 | self.dynamicCall("CommConnect()") 317 | self.loginLoop = QEventLoop() 318 | self.loginLoop.exec_() 319 | 320 | def getConnectState(self): 321 | """ 322 | 현재 접속상태를 반환합니다. 323 | 324 | 반환되는 접속상태는 아래와 같습니다. 325 | 0: 미연결, 1: 연결 326 | 327 | :return: int 328 | """ 329 | 330 | state = self.dynamicCall("GetConnectState()") 331 | return state 332 | 333 | def getLoginInfo(self, tag, isConnectState=False): 334 | """ 335 | 사용자의 tag에 해당하는 정보를 반환한다. 336 | 337 | tag에 올 수 있는 값은 아래와 같다. 338 | ACCOUNT_CNT: 전체 계좌의 개수를 반환한다. 339 | ACCNO: 전체 계좌 목록을 반환한다. 계좌별 구분은 ;(세미콜론) 이다. 340 | USER_ID: 사용자 ID를 반환한다. 341 | USER_NAME: 사용자명을 반환한다. 342 | GetServerGubun: 접속서버 구분을 반환합니다.("1": 모의투자, 그외(빈 문자열포함): 실서버) 343 | 344 | :param tag: string 345 | :param isConnectState: bool - 접속상태을 확인할 필요가 없는 경우 True로 설정. 346 | :return: string 347 | """ 348 | 349 | if not isConnectState: 350 | if not self.getConnectState(): 351 | raise KiwoomConnectError() 352 | 353 | if not isinstance(tag, str): 354 | raise ParameterTypeError() 355 | 356 | if tag not in ['ACCOUNT_CNT', 'ACCNO', 'USER_ID', 'USER_NAME', 'GetServerGubun']: 357 | raise ParameterValueError() 358 | 359 | if tag == "GetServerGubun": 360 | info = self.getServerGubun() 361 | else: 362 | cmd = 'GetLoginInfo("%s")' % tag 363 | info = self.dynamicCall(cmd) 364 | 365 | return info 366 | 367 | def getServerGubun(self): 368 | """ 369 | 서버구분 정보를 반환한다. 370 | 리턴값이 "1"이면 모의투자 서버이고, 그 외에는 실서버(빈 문자열포함). 371 | 372 | :return: string 373 | """ 374 | 375 | ret = self.dynamicCall("KOA_Functions(QString, QString)", "GetServerGubun", "") 376 | return ret 377 | 378 | ################################################################# 379 | # 메서드 정의: 조회 관련 메서드 # 380 | # 시세조회, 관심종목 조회, 조건검색 등 이들의 합산 조회 횟수가 1초에 5회까지 허용 # 381 | ################################################################# 382 | 383 | def setInputValue(self, key, value): 384 | """ 385 | TR 전송에 필요한 값을 설정한다. 386 | 387 | :param key: string - TR에 명시된 input 이름 388 | :param value: string - key에 해당하는 값 389 | """ 390 | 391 | if not (isinstance(key, str) and isinstance(value, str)): 392 | raise ParameterTypeError() 393 | 394 | self.dynamicCall("SetInputValue(QString, QString)", key, value) 395 | 396 | def commRqData(self, requestName, trCode, inquiry, screenNo): 397 | """ 398 | 키움서버에 TR 요청을 한다. 399 | 400 | 조회요청메서드이며 빈번하게 조회요청시, 시세과부하 에러값 -200이 리턴된다. 401 | 402 | :param requestName: string - TR 요청명(사용자 정의) 403 | :param trCode: string 404 | :param inquiry: int - 조회(0: 조회, 2: 남은 데이터 이어서 요청) 405 | :param screenNo: string - 화면번호(4자리) 406 | """ 407 | 408 | if not self.getConnectState(): 409 | raise KiwoomConnectError() 410 | 411 | if not (isinstance(requestName, str) 412 | and isinstance(trCode, str) 413 | and isinstance(inquiry, int) 414 | and isinstance(screenNo, str)): 415 | 416 | raise ParameterTypeError() 417 | 418 | returnCode = self.dynamicCall("CommRqData(QString, QString, int, QString)", requestName, trCode, inquiry, screenNo) 419 | 420 | if returnCode != ReturnCode.OP_ERR_NONE: 421 | raise KiwoomProcessingError("commRqData(): " + ReturnCode.CAUSE[returnCode]) 422 | 423 | # 루프 생성: receiveTrData() 메서드에서 루프를 종료시킨다. 424 | self.requestLoop = QEventLoop() 425 | self.requestLoop.exec_() 426 | 427 | def commGetData(self, trCode, realType, requestName, index, key): 428 | """ 429 | 데이터 획득 메서드 430 | 431 | receiveTrData() 이벤트 메서드가 호출될 때, 그 안에서 조회데이터를 얻어오는 메서드입니다. 432 | getCommData() 메서드로 위임. 433 | 434 | :param trCode: string 435 | :param realType: string - TR 요청시 ""(빈문자)로 처리 436 | :param requestName: string - TR 요청명(commRqData() 메소드 호출시 사용된 requestName) 437 | :param index: int 438 | :param key: string 439 | :return: string 440 | """ 441 | 442 | return self.getCommData(trCode, requestName, index, key) 443 | 444 | def getCommData(self, trCode, requestName, index, key): 445 | """ 446 | 데이터 획득 메서드 447 | 448 | receiveTrData() 이벤트 메서드가 호출될 때, 그 안에서 조회데이터를 얻어오는 메서드입니다. 449 | 450 | :param trCode: string 451 | :param requestName: string - TR 요청명(commRqData() 메소드 호출시 사용된 requestName) 452 | :param index: int 453 | :param key: string - 수신 데이터에서 얻고자 하는 값의 키(출력항목이름) 454 | :return: string 455 | """ 456 | 457 | if not (isinstance(trCode, str) 458 | and isinstance(requestName, str) 459 | and isinstance(index, int) 460 | and isinstance(key, str)): 461 | raise ParameterTypeError() 462 | 463 | data = self.dynamicCall("GetCommData(QString, QString, int, QString)", 464 | trCode, requestName, index, key) 465 | return data.strip() 466 | 467 | def getRepeatCnt(self, trCode, requestName): 468 | """ 469 | 서버로 부터 전달받은 데이터의 갯수를 리턴합니다.(멀티데이터의 갯수) 470 | 471 | receiveTrData() 이벤트 메서드가 호출될 때, 그 안에서 사용해야 합니다. 472 | 473 | 키움 OpenApi+에서는 데이터를 싱글데이터와 멀티데이터로 구분합니다. 474 | 싱글데이터란, 서버로 부터 전달받은 데이터 내에서, 중복되는 키(항목이름)가 하나도 없을 경우. 475 | 예를들면, 데이터가 '종목코드', '종목명', '상장일', '상장주식수' 처럼 키(항목이름)가 중복되지 않는 경우를 말합니다. 476 | 반면 멀티데이터란, 서버로 부터 전달받은 데이터 내에서, 일정 간격으로 키(항목이름)가 반복될 경우를 말합니다. 477 | 예를들면, 10일간의 일봉데이터를 요청할 경우 '종목코드', '일자', '시가', '고가', '저가' 이러한 항목이 10번 반복되는 경우입니다. 478 | 이러한 멀티데이터의 경우 반복 횟수(=데이터의 갯수)만큼, 루프를 돌면서 처리하기 위해 이 메서드를 이용하여 멀티데이터의 갯수를 얻을 수 있습니다. 479 | 480 | :param trCode: string 481 | :param requestName: string - TR 요청명(commRqData() 메소드 호출시 사용된 requestName) 482 | :return: int 483 | """ 484 | 485 | if not (isinstance(trCode, str) 486 | and isinstance(requestName, str)): 487 | raise ParameterTypeError() 488 | 489 | count = self.dynamicCall("GetRepeatCnt(QString, QString)", trCode, requestName) 490 | return count 491 | 492 | def getCommDataEx(self, trCode, multiDataName): 493 | """ 494 | 멀티데이터 획득 메서드 495 | 496 | receiveTrData() 이벤트 메서드가 호출될 때, 그 안에서 사용해야 합니다. 497 | 498 | :param trCode: string 499 | :param multiDataName: string - KOA에 명시된 멀티데이터명 500 | :return: list - 중첩리스트 501 | """ 502 | 503 | if not (isinstance(trCode, str) 504 | and isinstance(multiDataName, str)): 505 | raise ParameterTypeError() 506 | 507 | data = self.dynamicCall("GetCommDataEx(QString, QString)", trCode, multiDataName) 508 | return data 509 | 510 | def commKwRqData(self, codes, inquiry, codeCount, requestName, screenNo, typeFlag=0): 511 | """ 512 | 복수종목조회 메서드(관심종목조회 메서드라고도 함). 513 | 514 | 이 메서드는 setInputValue() 메서드를 이용하여, 사전에 필요한 값을 지정하지 않는다. 515 | 단지, 메서드의 매개변수에서 직접 종목코드를 지정하여 호출하며, 516 | 데이터 수신은 receiveTrData() 이벤트에서 아래 명시한 항목들을 1회 수신하며, 517 | 이후 receiveRealData() 이벤트를 통해 실시간 데이터를 얻을 수 있다. 518 | 519 | 복수종목조회 TR 코드는 OPTKWFID 이며, 요청 성공시 아래 항목들의 정보를 얻을 수 있다. 520 | 521 | 종목코드, 종목명, 현재가, 기준가, 전일대비, 전일대비기호, 등락율, 거래량, 거래대금, 522 | 체결량, 체결강도, 전일거래량대비, 매도호가, 매수호가, 매도1~5차호가, 매수1~5차호가, 523 | 상한가, 하한가, 시가, 고가, 저가, 종가, 체결시간, 예상체결가, 예상체결량, 자본금, 524 | 액면가, 시가총액, 주식수, 호가시간, 일자, 우선매도잔량, 우선매수잔량,우선매도건수, 525 | 우선매수건수, 총매도잔량, 총매수잔량, 총매도건수, 총매수건수, 패리티, 기어링, 손익분기, 526 | 잔본지지, ELW행사가, 전환비율, ELW만기일, 미결제약정, 미결제전일대비, 이론가, 527 | 내재변동성, 델타, 감마, 쎄타, 베가, 로 528 | 529 | :param codes: string - 한번에 100종목까지 조회가능하며 종목코드사이에 세미콜론(;)으로 구분. 530 | :param inquiry: int - api 문서는 bool 타입이지만, int로 처리(0: 조회, 1: 남은 데이터 이어서 조회) 531 | :param codeCount: int - codes에 지정한 종목의 갯수. 532 | :param requestName: string 533 | :param screenNo: string 534 | :param typeFlag: int - 주식과 선물옵션 구분(0: 주식, 3: 선물옵션), 주의: 매개변수의 위치를 맨 뒤로 이동함. 535 | :return: list - 중첩 리스트 [[종목코드, 종목명 ... 종목 정보], [종목코드, 종목명 ... 종목 정보]] 536 | """ 537 | 538 | if not self.getConnectState(): 539 | raise KiwoomConnectError() 540 | 541 | if not (isinstance(codes, str) 542 | and isinstance(inquiry, int) 543 | and isinstance(codeCount, int) 544 | and isinstance(requestName, str) 545 | and isinstance(screenNo, str) 546 | and isinstance(typeFlag, int)): 547 | 548 | raise ParameterTypeError() 549 | 550 | returnCode = self.dynamicCall("CommKwRqData(QString, QBoolean, int, int, QString, QString)", 551 | codes, inquiry, codeCount, typeFlag, requestName, screenNo) 552 | 553 | if returnCode != ReturnCode.OP_ERR_NONE: 554 | raise KiwoomProcessingError("commKwRqData(): " + ReturnCode.CAUSE[returnCode]) 555 | 556 | # 루프 생성: receiveTrData() 메서드에서 루프를 종료시킨다. 557 | self.requestLoop = QEventLoop() 558 | self.requestLoop.exec_() 559 | 560 | ############################################################### 561 | # 메서드 정의: 실시간 데이터 처리 관련 메서드 # 562 | ############################################################### 563 | 564 | def disconnectRealData(self, screenNo): 565 | """ 566 | 해당 화면번호로 설정한 모든 실시간 데이터 요청을 제거합니다. 567 | 568 | 화면을 종료할 때 반드시 이 메서드를 호출해야 합니다. 569 | 570 | :param screenNo: string 571 | """ 572 | 573 | if not self.getConnectState(): 574 | raise KiwoomConnectError() 575 | 576 | if not isinstance(screenNo, str): 577 | raise ParameterTypeError() 578 | 579 | self.dynamicCall("DisconnectRealData(QString)", screenNo) 580 | 581 | def getCommRealData(self, code, fid): 582 | """ 583 | 실시간 데이터 획득 메서드 584 | 585 | 이 메서드는 반드시 receiveRealData() 이벤트 메서드가 호출될 때, 그 안에서 사용해야 합니다. 586 | 587 | :param code: string - 종목코드 588 | :param fid: - 실시간 타입에 포함된 fid 589 | :return: string - fid에 해당하는 데이터 590 | """ 591 | 592 | if not (isinstance(code, str) 593 | and isinstance(fid, int)): 594 | raise ParameterTypeError() 595 | 596 | value = self.dynamicCall("GetCommRealData(QString, int)", code, fid) 597 | 598 | return value 599 | 600 | def setRealReg(self, screenNo, codes, fids, realRegType): 601 | """ 602 | 실시간 데이터 요청 메서드 603 | 604 | 종목코드와 fid 리스트를 이용해서 실시간 데이터를 요청하는 메서드입니다. 605 | 한번에 등록 가능한 종목과 fid 갯수는 100종목, 100개의 fid 입니다. 606 | 실시간등록타입을 0으로 설정하면, 첫 실시간 데이터 요청을 의미하며 607 | 실시간등록타입을 1로 설정하면, 추가등록을 의미합니다. 608 | 609 | 실시간 데이터는 실시간 타입 단위로 receiveRealData() 이벤트로 전달되기 때문에, 610 | 이 메서드에서 지정하지 않은 fid 일지라도, 실시간 타입에 포함되어 있다면, 데이터 수신이 가능하다. 611 | 612 | :param screenNo: string 613 | :param codes: string - 종목코드 리스트(종목코드;종목코드;...) 614 | :param fids: string - fid 리스트(fid;fid;...) 615 | :param realRegType: string - 실시간등록타입(0: 첫 등록, 1: 추가 등록) 616 | """ 617 | 618 | if not self.getConnectState(): 619 | raise KiwoomConnectError() 620 | 621 | if not (isinstance(screenNo, str) 622 | and isinstance(codes, str) 623 | and isinstance(fids, str) 624 | and isinstance(realRegType, str)): 625 | raise ParameterTypeError() 626 | 627 | self.dynamicCall("SetRealReg(QString, QString, QString, QString)", 628 | screenNo, codes, fids, realRegType) 629 | 630 | def setRealRemove(self, screenNo, code): 631 | """ 632 | 실시간 데이터 중지 메서드 633 | 634 | setRealReg() 메서드로 등록한 종목만, 이 메서드를 통해 실시간 데이터 받기를 중지 시킬 수 있습니다. 635 | 636 | :param screenNo: string - 화면번호 또는 ALL 키워드 사용가능 637 | :param code: string - 종목코드 또는 ALL 키워드 사용가능 638 | """ 639 | 640 | if not self.getConnectState(): 641 | raise KiwoomConnectError() 642 | 643 | if not (isinstance(screenNo, str) 644 | and isinstance(code, str)): 645 | raise ParameterTypeError() 646 | 647 | self.dynamicCall("SetRealRemove(QString, QString)", screenNo, code) 648 | 649 | ############################################################### 650 | # 메서드 정의: 조건검색 관련 메서드와 이벤트 # 651 | ############################################################### 652 | 653 | def receiveConditionVer(self, receive, msg): 654 | """ 655 | getConditionLoad() 메서드의 조건식 목록 요청에 대한 응답 이벤트 656 | 657 | :param receive: int - 응답결과(1: 성공, 나머지 실패) 658 | :param msg: string - 메세지 659 | """ 660 | 661 | try: 662 | if not receive: 663 | return 664 | 665 | self.condition = self.getConditionNameList() 666 | print("조건식 개수: ", len(self.condition)) 667 | 668 | for key in self.condition.keys(): 669 | print("조건식: ", key, ": ", self.condition[key]) 670 | print("key type: ", type(key)) 671 | 672 | except Exception as e: 673 | print(e) 674 | 675 | finally: 676 | self.conditionLoop.exit() 677 | 678 | def receiveTrCondition(self, screenNo, codes, conditionName, conditionIndex, inquiry): 679 | """ 680 | (1회성, 실시간) 종목 조건검색 요청시 발생되는 이벤트 681 | 682 | :param screenNo: string 683 | :param codes: string - 종목코드 목록(각 종목은 세미콜론으로 구분됨) 684 | :param conditionName: string - 조건식 이름 685 | :param conditionIndex: int - 조건식 인덱스 686 | :param inquiry: int - 조회구분(0: 남은데이터 없음, 2: 남은데이터 있음) 687 | """ 688 | 689 | print("[receiveTrCondition]") 690 | 691 | try: 692 | if codes == "": 693 | return 694 | 695 | codeList = codes.split(';') 696 | del codeList[-1] 697 | 698 | print(codeList) 699 | print("종목개수: ", len(codeList)) 700 | 701 | finally: 702 | self.conditionLoop.exit() 703 | 704 | def receiveRealCondition(self, code, event, conditionName, conditionIndex): 705 | """ 706 | 실시간 종목 조건검색 요청시 발생되는 이벤트 707 | 708 | :param code: string - 종목코드 709 | :param event: string - 이벤트종류("I": 종목편입, "D": 종목이탈) 710 | :param conditionName: string - 조건식 이름 711 | :param conditionIndex: string - 조건식 인덱스(여기서만 인덱스가 string 타입으로 전달됨) 712 | """ 713 | 714 | print("[receiveRealCondition]") 715 | 716 | print("종목코드: ", code) 717 | print("이벤트: ", "종목편입" if event == "I" else "종목이탈") 718 | 719 | def getConditionLoad(self): 720 | """ 조건식 목록 요청 메서드 """ 721 | 722 | if not self.getConnectState(): 723 | raise KiwoomConnectError() 724 | 725 | isLoad = self.dynamicCall("GetConditionLoad()") 726 | 727 | # 요청 실패시 728 | if not isLoad: 729 | raise KiwoomProcessingError("getConditionLoad(): 조건식 요청 실패") 730 | 731 | # receiveConditionVer() 이벤트 메서드에서 루프 종료 732 | self.conditionLoop = QEventLoop() 733 | self.conditionLoop.exec_() 734 | 735 | def getConditionNameList(self): 736 | """ 737 | 조건식 획득 메서드 738 | 739 | 조건식을 딕셔너리 형태로 반환합니다. 740 | 이 메서드는 반드시 receiveConditionVer() 이벤트 메서드안에서 사용해야 합니다. 741 | 742 | :return: dict - {인덱스:조건명, 인덱스:조건명, ...} 743 | """ 744 | 745 | data = self.dynamicCall("GetConditionNameList()") 746 | 747 | if data == "": 748 | raise KiwoomProcessingError("getConditionNameList(): 사용자 조건식이 없습니다.") 749 | 750 | conditionList = data.split(';') 751 | del conditionList[-1] 752 | 753 | conditionDictionary = {} 754 | 755 | for condition in conditionList: 756 | key, value = condition.split('^') 757 | conditionDictionary[int(key)] = value 758 | 759 | return conditionDictionary 760 | 761 | def sendCondition(self, screenNo, conditionName, conditionIndex, isRealTime): 762 | """ 763 | 종목 조건검색 요청 메서드 764 | 765 | 이 메서드로 얻고자 하는 것은 해당 조건에 맞는 종목코드이다. 766 | 해당 종목에 대한 상세정보는 setRealReg() 메서드로 요청할 수 있다. 767 | 요청이 실패하는 경우는, 해당 조건식이 없거나, 조건명과 인덱스가 맞지 않거나, 조회 횟수를 초과하는 경우 발생한다. 768 | 769 | 조건검색에 대한 결과는 770 | 1회성 조회의 경우, receiveTrCondition() 이벤트로 결과값이 전달되며 771 | 실시간 조회의 경우, receiveTrCondition()과 receiveRealCondition() 이벤트로 결과값이 전달된다. 772 | 773 | :param screenNo: string 774 | :param conditionName: string - 조건식 이름 775 | :param conditionIndex: int - 조건식 인덱스 776 | :param isRealTime: int - 조건검색 조회구분(0: 1회성 조회, 1: 실시간 조회) 777 | """ 778 | 779 | if not self.getConnectState(): 780 | raise KiwoomConnectError() 781 | 782 | if not (isinstance(screenNo, str) 783 | and isinstance(conditionName, str) 784 | and isinstance(conditionIndex, int) 785 | and isinstance(isRealTime, int)): 786 | raise ParameterTypeError() 787 | 788 | isRequest = self.dynamicCall("SendCondition(QString, QString, int, int", 789 | screenNo, conditionName, conditionIndex, isRealTime) 790 | 791 | if not isRequest: 792 | raise KiwoomProcessingError("sendCondition(): 조건검색 요청 실패") 793 | 794 | # receiveTrCondition() 이벤트 메서드에서 루프 종료 795 | self.conditionLoop = QEventLoop() 796 | self.conditionLoop.exec_() 797 | 798 | def sendConditionStop(self, screenNo, conditionName, conditionIndex): 799 | """ 종목 조건검색 중지 메서드 """ 800 | 801 | if not self.getConnectState(): 802 | raise KiwoomConnectError() 803 | 804 | if not (isinstance(screenNo, str) 805 | and isinstance(conditionName, str) 806 | and isinstance(conditionIndex, int)): 807 | raise ParameterTypeError() 808 | 809 | self.dynamicCall("SendConditionStop(QString, QString, int)", screenNo, conditionName, conditionIndex) 810 | 811 | ############################################################### 812 | # 메서드 정의: 주문과 잔고처리 관련 메서드 # 813 | # 1초에 5회까지 주문 허용 # 814 | ############################################################### 815 | 816 | def sendOrder(self, requestName, screenNo, accountNo, orderType, code, qty, price, hogaType, originOrderNo): 817 | 818 | """ 819 | 주식 주문 메서드 820 | 821 | sendOrder() 메소드 실행시, 822 | OnReceiveMsg, OnReceiveTrData, OnReceiveChejanData 이벤트가 발생한다. 823 | 이 중, 주문에 대한 결과 데이터를 얻기 위해서는 OnReceiveChejanData 이벤트를 통해서 처리한다. 824 | OnReceiveTrData 이벤트를 통해서는 주문번호를 얻을 수 있는데, 주문후 이 이벤트에서 주문번호가 ''공백으로 전달되면, 825 | 주문접수 실패를 의미한다. 826 | 827 | :param requestName: string - 주문 요청명(사용자 정의) 828 | :param screenNo: string - 화면번호(4자리) 829 | :param accountNo: string - 계좌번호(10자리) 830 | :param orderType: int - 주문유형(1: 신규매수, 2: 신규매도, 3: 매수취소, 4: 매도취소, 5: 매수정정, 6: 매도정정) 831 | :param code: string - 종목코드 832 | :param qty: int - 주문수량 833 | :param price: int - 주문단가 834 | :param hogaType: string - 거래구분(00: 지정가, 03: 시장가, 05: 조건부지정가, 06: 최유리지정가, 그외에는 api 문서참조) 835 | :param originOrderNo: string - 원주문번호(신규주문에는 공백, 정정및 취소주문시 원주문번호르 입력합니다.) 836 | """ 837 | 838 | if not self.getConnectState(): 839 | raise KiwoomConnectError() 840 | 841 | if not (isinstance(requestName, str) 842 | and isinstance(screenNo, str) 843 | and isinstance(accountNo, str) 844 | and isinstance(orderType, int) 845 | and isinstance(code, str) 846 | and isinstance(qty, int) 847 | and isinstance(price, int) 848 | and isinstance(hogaType, str) 849 | and isinstance(originOrderNo, str)): 850 | 851 | raise ParameterTypeError() 852 | 853 | returnCode = self.dynamicCall("SendOrder(QString, QString, QString, int, QString, int, int, QString, QString)", 854 | [requestName, screenNo, accountNo, orderType, code, qty, price, hogaType, originOrderNo]) 855 | 856 | if returnCode != ReturnCode.OP_ERR_NONE: 857 | raise KiwoomProcessingError("sendOrder(): " + ReturnCode.CAUSE[returnCode]) 858 | 859 | # receiveTrData() 에서 루프종료 860 | self.orderLoop = QEventLoop() 861 | self.orderLoop.exec_() 862 | 863 | def getChejanData(self, fid): 864 | """ 865 | 주문접수, 주문체결, 잔고정보를 얻어오는 메서드 866 | 867 | 이 메서드는 receiveChejanData() 이벤트 메서드가 호출될 때 그 안에서 사용해야 합니다. 868 | 869 | :param fid: int 870 | :return: string 871 | """ 872 | 873 | if not isinstance(fid, int): 874 | raise ParameterTypeError() 875 | 876 | cmd = 'GetChejanData("%s")' % fid 877 | data = self.dynamicCall(cmd) 878 | return data 879 | 880 | ############################################################### 881 | # 기타 메서드 정의 # 882 | ############################################################### 883 | 884 | def getCodeListByMarket(self, market): 885 | """ 886 | 시장 구분에 따른 종목코드의 목록을 List로 반환한다. 887 | 888 | market에 올 수 있는 값은 아래와 같다. 889 | '0': 장내, '3': ELW, '4': 뮤추얼펀드, '5': 신주인수권, '6': 리츠, '8': ETF, '9': 하이일드펀드, '10': 코스닥, '30': 제3시장 890 | 891 | :param market: string 892 | :return: List 893 | """ 894 | 895 | if not self.getConnectState(): 896 | raise KiwoomConnectError() 897 | 898 | if not isinstance(market, str): 899 | raise ParameterTypeError() 900 | 901 | if market not in ['0', '3', '4', '5', '6', '8', '9', '10', '30']: 902 | raise ParameterValueError() 903 | 904 | cmd = 'GetCodeListByMarket("%s")' % market 905 | codeList = self.dynamicCall(cmd) 906 | return codeList.split(';') 907 | 908 | def getCodeList(self, *market): 909 | """ 910 | 여러 시장의 종목코드를 List 형태로 반환하는 헬퍼 메서드. 911 | 912 | :param market: Tuple - 여러 개의 문자열을 매개변수로 받아 Tuple로 처리한다. 913 | :return: List 914 | """ 915 | 916 | codeList = [] 917 | 918 | for m in market: 919 | tmpList = self.getCodeListByMarket(m) 920 | codeList += tmpList 921 | 922 | return codeList 923 | 924 | def getMasterCodeName(self, code): 925 | """ 926 | 종목코드의 한글명을 반환한다. 927 | 928 | :param code: string - 종목코드 929 | :return: string - 종목코드의 한글명 930 | """ 931 | 932 | if not self.getConnectState(): 933 | raise KiwoomConnectError() 934 | 935 | if not isinstance(code, str): 936 | raise ParameterTypeError() 937 | 938 | cmd = 'GetMasterCodeName("%s")' % code 939 | name = self.dynamicCall(cmd) 940 | return name 941 | 942 | def changeFormat(self, data, percent=0): 943 | 944 | if percent == 0: 945 | d = int(data) 946 | formatData = '{:-,d}'.format(d) 947 | 948 | elif percent == 1: 949 | f = int(data) / 100 950 | formatData = '{:-,.2f}'.format(f) 951 | 952 | elif percent == 2: 953 | f = float(data) 954 | formatData = '{:-,.2f}'.format(f) 955 | 956 | return formatData 957 | 958 | def opwDataReset(self): 959 | """ 잔고 및 보유종목 데이터 초기화 """ 960 | self.opw00001Data = 0 961 | self.opw00018Data = {'accountEvaluation': [], 'stocks': []} 962 | 963 | 964 | class ParameterTypeError(Exception): 965 | """ 파라미터 타입이 일치하지 않을 경우 발생하는 예외 """ 966 | 967 | def __init__(self, msg="파라미터 타입이 일치하지 않습니다."): 968 | self.msg = msg 969 | 970 | def __str__(self): 971 | return self.msg 972 | 973 | 974 | class ParameterValueError(Exception): 975 | """ 파라미터로 사용할 수 없는 값을 사용할 경우 발생하는 예외 """ 976 | 977 | def __init__(self, msg="파라미터로 사용할 수 없는 값 입니다."): 978 | self.msg = msg 979 | 980 | def __str__(self): 981 | return self.msg 982 | 983 | 984 | class KiwoomProcessingError(Exception): 985 | """ 키움에서 처리실패에 관련된 리턴코드를 받았을 경우 발생하는 예외 """ 986 | 987 | def __init__(self, msg="처리 실패"): 988 | self.msg = msg 989 | 990 | def __str__(self): 991 | return self.msg 992 | 993 | def __repr__(self): 994 | return self.msg 995 | 996 | 997 | class KiwoomConnectError(Exception): 998 | """ 키움서버에 로그인 상태가 아닐 경우 발생하는 예외 """ 999 | 1000 | def __init__(self, msg="로그인 여부를 확인하십시오"): 1001 | self.msg = msg 1002 | 1003 | def __str__(self): 1004 | return self.msg 1005 | 1006 | 1007 | class ReturnCode(object): 1008 | """ 키움 OpenApi+ 함수들이 반환하는 값 """ 1009 | 1010 | OP_ERR_NONE = 0 # 정상처리 1011 | OP_ERR_FAIL = -10 # 실패 1012 | OP_ERR_LOGIN = -100 # 사용자정보교환실패 1013 | OP_ERR_CONNECT = -101 # 서버접속실패 1014 | OP_ERR_VERSION = -102 # 버전처리실패 1015 | OP_ERR_FIREWALL = -103 # 개인방화벽실패 1016 | OP_ERR_MEMORY = -104 # 메모리보호실패 1017 | OP_ERR_INPUT = -105 # 함수입력값오류 1018 | OP_ERR_SOCKET_CLOSED = -106 # 통신연결종료 1019 | OP_ERR_SISE_OVERFLOW = -200 # 시세조회과부하 1020 | OP_ERR_RQ_STRUCT_FAIL = -201 # 전문작성초기화실패 1021 | OP_ERR_RQ_STRING_FAIL = -202 # 전문작성입력값오류 1022 | OP_ERR_NO_DATA = -203 # 데이터없음 1023 | OP_ERR_OVER_MAX_DATA = -204 # 조회가능한종목수초과 1024 | OP_ERR_DATA_RCV_FAIL = -205 # 데이터수신실패 1025 | OP_ERR_OVER_MAX_FID = -206 # 조회가능한FID수초과 1026 | OP_ERR_REAL_CANCEL = -207 # 실시간해제오류 1027 | OP_ERR_ORD_WRONG_INPUT = -300 # 입력값오류 1028 | OP_ERR_ORD_WRONG_ACCTNO = -301 # 계좌비밀번호없음 1029 | OP_ERR_OTHER_ACC_USE = -302 # 타인계좌사용오류 1030 | OP_ERR_MIS_2BILL_EXC = -303 # 주문가격이20억원을초과 1031 | OP_ERR_MIS_5BILL_EXC = -304 # 주문가격이50억원을초과 1032 | OP_ERR_MIS_1PER_EXC = -305 # 주문수량이총발행주수의1%초과오류 1033 | OP_ERR_MIS_3PER_EXC = -306 # 주문수량이총발행주수의3%초과오류 1034 | OP_ERR_SEND_FAIL = -307 # 주문전송실패 1035 | OP_ERR_ORD_OVERFLOW = -308 # 주문전송과부하 1036 | OP_ERR_MIS_300CNT_EXC = -309 # 주문수량300계약초과 1037 | OP_ERR_MIS_500CNT_EXC = -310 # 주문수량500계약초과 1038 | OP_ERR_ORD_WRONG_ACCTINFO = -340 # 계좌정보없음 1039 | OP_ERR_ORD_SYMCODE_EMPTY = -500 # 종목코드없음 1040 | 1041 | CAUSE = { 1042 | 0: '정상처리', 1043 | -10: '실패', 1044 | -100: '사용자정보교환실패', 1045 | -102: '버전처리실패', 1046 | -103: '개인방화벽실패', 1047 | -104: '메모리보호실패', 1048 | -105: '함수입력값오류', 1049 | -106: '통신연결종료', 1050 | -200: '시세조회과부하', 1051 | -201: '전문작성초기화실패', 1052 | -202: '전문작성입력값오류', 1053 | -203: '데이터없음', 1054 | -204: '조회가능한종목수초과', 1055 | -205: '데이터수신실패', 1056 | -206: '조회가능한FID수초과', 1057 | -207: '실시간해제오류', 1058 | -300: '입력값오류', 1059 | -301: '계좌비밀번호없음', 1060 | -302: '타인계좌사용오류', 1061 | -303: '주문가격이20억원을초과', 1062 | -304: '주문가격이50억원을초과', 1063 | -305: '주문수량이총발행주수의1%초과오류', 1064 | -306: '주문수량이총발행주수의3%초과오류', 1065 | -307: '주문전송실패', 1066 | -308: '주문전송과부하', 1067 | -309: '주문수량300계약초과', 1068 | -310: '주문수량500계약초과', 1069 | -340: '계좌정보없음', 1070 | -500: '종목코드없음' 1071 | } 1072 | 1073 | 1074 | class FidList(object): 1075 | """ receiveChejanData() 이벤트 메서드로 전달되는 FID 목록 """ 1076 | 1077 | CHEJAN = { 1078 | 9201: '계좌번호', 1079 | 9203: '주문번호', 1080 | 9205: '관리자사번', 1081 | 9001: '종목코드', 1082 | 912: '주문업무분류', 1083 | 913: '주문상태', 1084 | 302: '종목명', 1085 | 900: '주문수량', 1086 | 901: '주문가격', 1087 | 902: '미체결수량', 1088 | 903: '체결누계금액', 1089 | 904: '원주문번호', 1090 | 905: '주문구분', 1091 | 906: '매매구분', 1092 | 907: '매도수구분', 1093 | 908: '주문/체결시간', 1094 | 909: '체결번호', 1095 | 910: '체결가', 1096 | 911: '체결량', 1097 | 10: '현재가', 1098 | 27: '(최우선)매도호가', 1099 | 28: '(최우선)매수호가', 1100 | 914: '단위체결가', 1101 | 915: '단위체결량', 1102 | 938: '당일매매수수료', 1103 | 939: '당일매매세금', 1104 | 919: '거부사유', 1105 | 920: '화면번호', 1106 | 921: '921', 1107 | 922: '922', 1108 | 923: '923', 1109 | 949: '949', 1110 | 10010: '10010', 1111 | 917: '신용구분', 1112 | 916: '대출일', 1113 | 930: '보유수량', 1114 | 931: '매입단가', 1115 | 932: '총매입가', 1116 | 933: '주문가능수량', 1117 | 945: '당일순매수수량', 1118 | 946: '매도/매수구분', 1119 | 950: '당일총매도손일', 1120 | 951: '예수금', 1121 | 307: '기준가', 1122 | 8019: '손익율', 1123 | 957: '신용금액', 1124 | 958: '신용이자', 1125 | 959: '담보대출수량', 1126 | 924: '924', 1127 | 918: '만기일', 1128 | 990: '당일실현손익(유가)', 1129 | 991: '당일신현손익률(유가)', 1130 | 992: '당일실현손익(신용)', 1131 | 993: '당일실현손익률(신용)', 1132 | 397: '파생상품거래단위', 1133 | 305: '상한가', 1134 | 306: '하한가' 1135 | } 1136 | 1137 | 1138 | class RealType(object): 1139 | 1140 | REALTYPE = { 1141 | '주식시세': { 1142 | 10: '현재가', 1143 | 11: '전일대비', 1144 | 12: '등락율', 1145 | 27: '최우선매도호가', 1146 | 28: '최우선매수호가', 1147 | 13: '누적거래량', 1148 | 14: '누적거래대금', 1149 | 16: '시가', 1150 | 17: '고가', 1151 | 18: '저가', 1152 | 25: '전일대비기호', 1153 | 26: '전일거래량대비', 1154 | 29: '거래대금증감', 1155 | 30: '거일거래량대비', 1156 | 31: '거래회전율', 1157 | 32: '거래비용', 1158 | 311: '시가총액(억)' 1159 | }, 1160 | 1161 | '주식체결': { 1162 | 20: '체결시간(HHMMSS)', 1163 | 10: '체결가', 1164 | 11: '전일대비', 1165 | 12: '등락율', 1166 | 27: '최우선매도호가', 1167 | 28: '최우선매수호가', 1168 | 15: '체결량', 1169 | 13: '누적체결량', 1170 | 14: '누적거래대금', 1171 | 16: '시가', 1172 | 17: '고가', 1173 | 18: '저가', 1174 | 25: '전일대비기호', 1175 | 26: '전일거래량대비', 1176 | 29: '거래대금증감', 1177 | 30: '전일거래량대비', 1178 | 31: '거래회전율', 1179 | 32: '거래비용', 1180 | 228: '체결강도', 1181 | 311: '시가총액(억)', 1182 | 290: '장구분', 1183 | 691: 'KO접근도' 1184 | }, 1185 | 1186 | '주식호가잔량': { 1187 | 21: '호가시간', 1188 | 41: '매도호가1', 1189 | 61: '매도호가수량1', 1190 | 81: '매도호가직전대비1', 1191 | 51: '매수호가1', 1192 | 71: '매수호가수량1', 1193 | 91: '매수호가직전대비1', 1194 | 42: '매도호가2', 1195 | 62: '매도호가수량2', 1196 | 82: '매도호가직전대비2', 1197 | 52: '매수호가2', 1198 | 72: '매수호가수량2', 1199 | 92: '매수호가직전대비2', 1200 | 43: '매도호가3', 1201 | 63: '매도호가수량3', 1202 | 83: '매도호가직전대비3', 1203 | 53: '매수호가3', 1204 | 73: '매수호가수량3', 1205 | 93: '매수호가직전대비3', 1206 | 44: '매도호가4', 1207 | 64: '매도호가수량4', 1208 | 84: '매도호가직전대비4', 1209 | 54: '매수호가4', 1210 | 74: '매수호가수량4', 1211 | 94: '매수호가직전대비4', 1212 | 45: '매도호가5', 1213 | 65: '매도호가수량5', 1214 | 85: '매도호가직전대비5', 1215 | 55: '매수호가5', 1216 | 75: '매수호가수량5', 1217 | 95: '매수호가직전대비5', 1218 | 46: '매도호가6', 1219 | 66: '매도호가수량6', 1220 | 86: '매도호가직전대비6', 1221 | 56: '매수호가6', 1222 | 76: '매수호가수량6', 1223 | 96: '매수호가직전대비6', 1224 | 47: '매도호가7', 1225 | 67: '매도호가수량7', 1226 | 87: '매도호가직전대비7', 1227 | 57: '매수호가7', 1228 | 77: '매수호가수량7', 1229 | 97: '매수호가직전대비7', 1230 | 48: '매도호가8', 1231 | 68: '매도호가수량8', 1232 | 88: '매도호가직전대비8', 1233 | 58: '매수호가8', 1234 | 78: '매수호가수량8', 1235 | 98: '매수호가직전대비8', 1236 | 49: '매도호가9', 1237 | 69: '매도호가수량9', 1238 | 89: '매도호가직전대비9', 1239 | 59: '매수호가9', 1240 | 79: '매수호가수량9', 1241 | 99: '매수호가직전대비9', 1242 | 50: '매도호가10', 1243 | 70: '매도호가수량10', 1244 | 90: '매도호가직전대비10', 1245 | 60: '매수호가10', 1246 | 80: '매수호가수량10', 1247 | 100: '매수호가직전대비10', 1248 | 121: '매도호가총잔량', 1249 | 122: '매도호가총잔량직전대비', 1250 | 125: '매수호가총잔량', 1251 | 126: '매수호가총잔량직전대비', 1252 | 23: '예상체결가', 1253 | 24: '예상체결수량', 1254 | 128: '순매수잔량(총매수잔량-총매도잔량)', 1255 | 129: '매수비율', 1256 | 138: '순매도잔량(총매도잔량-총매수잔량)', 1257 | 139: '매도비율', 1258 | 200: '예상체결가전일종가대비', 1259 | 201: '예상체결가전일종가대비등락율', 1260 | 238: '예상체결가전일종가대비기호', 1261 | 291: '예상체결가', 1262 | 292: '예상체결량', 1263 | 293: '예상체결가전일대비기호', 1264 | 294: '예상체결가전일대비', 1265 | 295: '예상체결가전일대비등락율', 1266 | 13: '누적거래량', 1267 | 299: '전일거래량대비예상체결률', 1268 | 215: '장운영구분' 1269 | }, 1270 | 1271 | '장시작시간': { 1272 | 215: '장운영구분(0:장시작전, 2:장종료전, 3:장시작, 4,8:장종료, 9:장마감)', 1273 | 20: '시간(HHMMSS)', 1274 | 214: '장시작예상잔여시간' 1275 | }, 1276 | 1277 | '업종지수': { 1278 | 20: '체결시간', 1279 | 10: '현재가', 1280 | 11: '전일대비', 1281 | 12: '등락율', 1282 | 15: '거래량', 1283 | 13: '누적거래량', 1284 | 14: '누적거래대금', 1285 | 16: '시가', 1286 | 17: '고가', 1287 | 18: '저가', 1288 | 25: '전일대비기호', 1289 | 26: '전일거래량대비(계약,주)' 1290 | }, 1291 | 1292 | '업종등락': { 1293 | 20: '체결시간', 1294 | 252: '상승종목수', 1295 | 251: '상한종목수', 1296 | 253: '보합종목수', 1297 | 255: '하락종목수', 1298 | 254: '하한종목수', 1299 | 13: '누적거래량', 1300 | 14: '누적거래대금', 1301 | 10: '현재가', 1302 | 11: '전일대비', 1303 | 12: '등락율', 1304 | 256: '거래형성종목수', 1305 | 257: '거래형성비율', 1306 | 25: '전일대비기호' 1307 | }, 1308 | 1309 | '주문체결': { 1310 | 9201: '계좌번호', 1311 | 9203: '주문번호', 1312 | 9205: '관리자사번', 1313 | 9001: '종목코드', 1314 | 912: '주문분류(jj:주식주문)', 1315 | 913: '주문상태(10:원주문, 11:정정주문, 12:취소주문, 20:주문확인, 21:정정확인, 22:취소확인, 90,92:주문거부)', 1316 | 302: '종목명', 1317 | 900: '주문수량', 1318 | 901: '주문가격', 1319 | 902: '미체결수량', 1320 | 903: '체결누계금액', 1321 | 904: '원주문번호', 1322 | 905: '주문구분(+:현금매수, -:현금매도)', 1323 | 906: '매매구분(보통, 시장가등)', 1324 | 907: '매도수구분(1:매도, 2:매수)', 1325 | 908: '체결시간(HHMMSS)', 1326 | 909: '체결번호', 1327 | 910: '체결가', 1328 | 911: '체결량', 1329 | 10: '체결가', 1330 | 27: '최우선매도호가', 1331 | 28: '최우선매수호가', 1332 | 914: '단위체결가', 1333 | 915: '단위체결량', 1334 | 938: '당일매매수수료', 1335 | 939: '당일매매세금' 1336 | }, 1337 | 1338 | '잔고': { 1339 | 9201: '계좌번호', 1340 | 9001: '종목코드', 1341 | 302: '종목명', 1342 | 10: '현재가', 1343 | 930: '보유수량', 1344 | 931: '매입단가', 1345 | 932: '총매입가', 1346 | 933: '주문가능수량', 1347 | 945: '당일순매수량', 1348 | 946: '매도매수구분', 1349 | 950: '당일총매도손익', 1350 | 951: '예수금', 1351 | 27: '최우선매도호가', 1352 | 28: '최우선매수호가', 1353 | 307: '기준가', 1354 | 8019: '손익율' 1355 | }, 1356 | 1357 | '주식시간외호가': { 1358 | 21: '호가시간(HHMMSS)', 1359 | 131: '시간외매도호가총잔량', 1360 | 132: '시간외매도호가총잔량직전대비', 1361 | 135: '시간외매수호가총잔량', 1362 | 136: '시간외매수호가총잔량직전대비' 1363 | } 1364 | } 1365 | 1366 | 1367 | if __name__ == "__main__": 1368 | """ 조건검색 테스트 코드 """ 1369 | 1370 | app = QApplication(sys.argv) 1371 | 1372 | try: 1373 | kiwoom = Kiwoom() 1374 | kiwoom.commConnect() 1375 | 1376 | server = kiwoom.getServerGubun() 1377 | print("server: ", server) 1378 | print("type: ", type(server)) 1379 | print("len: ", len(server)) 1380 | 1381 | if len(server) == 0 or server != "1": 1382 | print("실서버 입니다.") 1383 | 1384 | else: 1385 | print("모의투자 서버입니다.") 1386 | 1387 | except Exception as e: 1388 | print(e) 1389 | 1390 | sys.exit(app.exec_()) 1391 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyTrader는 시스템 트레이딩을 학습하는 과정에서 만들어진 프로그램입니다. 2 | 이 프로그램은 파이썬3, PyQt5을 기반으로 제작되었으며, 3 | 키움증권의 OpenApi+ 를 사용하고 있습니다. -------------------------------------------------------------------------------- /buy_list.txt: -------------------------------------------------------------------------------- 1 | 매수;035720;시장가;10;0;매수전 2 | 매수;039490;시장가;10;0;매수전 -------------------------------------------------------------------------------- /kiwoomauto.py: -------------------------------------------------------------------------------- 1 | """ 2 | 키움 OpenApi+ 모듈을 업데이트하기 위해서 번개2를 실행했다가 종료시킨다. 3 | 스케줄러에 등록해서 사용한다. 4 | 5 | author: 서경동 6 | last edit: 2017. 01. 10. 7 | """ 8 | 9 | 10 | from pywinauto import application 11 | from pywinauto import timings 12 | import time 13 | import os 14 | 15 | 16 | # Account 17 | account = [] 18 | with open("C:\\Users\\seoga\\PycharmProjects\\PyTrader\\account.txt", 'r') as f: 19 | account = f.readlines() 20 | 21 | # 번개2 실행 및 로그인 22 | app = application.Application() 23 | app.start("C:\Kiwoom\KiwoomFlash2\khministarter.exe") 24 | 25 | title = "번개 Login" 26 | dlg = timings.WaitUntilPasses(20, 0.5, lambda: app.window_(title=title)) 27 | 28 | idForm = dlg.Edit0 29 | idForm.SetFocus() 30 | idForm.TypeKeys(account[0]) 31 | 32 | passForm = dlg.Edit2 33 | passForm.SetFocus() 34 | passForm.TypeKeys(account[1]) 35 | 36 | certForm = dlg.Edit3 37 | certForm.SetFocus() 38 | certForm.TypeKeys(account[2]) 39 | 40 | loginBtn = dlg.Button0 41 | loginBtn.Click() 42 | 43 | # 업데이트가 완료될 때 까지 대기 44 | while True: 45 | time.sleep(5) 46 | with os.popen('tasklist /FI "IMAGENAME eq khmini.exe"') as f: 47 | lines = f.readlines() 48 | if len(lines) >= 3: 49 | break 50 | 51 | # 번개2 종료 52 | time.sleep(30) 53 | os.system("taskkill /im khmini.exe") 54 | -------------------------------------------------------------------------------- /logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,infoLogger 3 | 4 | [handlers] 5 | keys=simpleHandler,fileHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=NOTSET 12 | handlers= 13 | 14 | [logger_infoLogger] 15 | level=DEBUG 16 | handlers=simpleHandler,fileHandler 17 | qualname=Kiwoom 18 | propagate=1 19 | 20 | [handler_simpleHandler] 21 | class=StreamHandler 22 | formatter=simpleFormatter 23 | args=(sys.stdout,) 24 | 25 | [handler_fileHandler] 26 | class=FileHandler 27 | formatter=simpleFormatter 28 | args=('kiwoom.log', 'w') 29 | 30 | [formatter_simpleFormatter] 31 | format=%(asctime)s - %(name)s - %(levelname)-8s - %(message)s 32 | datefmt= -------------------------------------------------------------------------------- /pytrader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdseo/PyTrader/00adb14a242348455075c782ed78737f82c87c4f/pytrader.png -------------------------------------------------------------------------------- /pytrader.py: -------------------------------------------------------------------------------- 1 | """ 2 | QtDesigner로 만든 UI와 해당 UI의 위젯에서 발생하는 이벤트를 컨트롤하는 클래스 3 | 4 | author: 서경동 5 | last edit: 2017. 01. 18 6 | """ 7 | 8 | 9 | import sys, time 10 | from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox, QTableWidget, QTableWidgetItem 11 | from PyQt5.QtCore import Qt, QTimer, QTime 12 | from PyQt5 import uic 13 | from Kiwoom import Kiwoom, ParameterTypeError, ParameterValueError, KiwoomProcessingError, KiwoomConnectError 14 | 15 | 16 | ui = uic.loadUiType("pytrader.ui")[0] 17 | 18 | class MyWindow(QMainWindow, ui): 19 | 20 | def __init__(self): 21 | super().__init__() 22 | self.setupUi(self) 23 | self.show() 24 | 25 | self.kiwoom = Kiwoom() 26 | self.kiwoom.commConnect() 27 | 28 | self.server = self.kiwoom.getLoginInfo("GetServerGubun") 29 | 30 | if len(self.server) == 0 or self.server != "1": 31 | self.serverGubun = "실제운영" 32 | else: 33 | self.serverGubun = "모의투자" 34 | 35 | self.codeList = self.kiwoom.getCodeList("0") 36 | 37 | # 메인 타이머 38 | self.timer = QTimer(self) 39 | self.timer.start(1000) 40 | self.timer.timeout.connect(self.timeout) 41 | 42 | # 잔고 및 보유종목 조회 타이머 43 | self.inquiryTimer = QTimer(self) 44 | self.inquiryTimer.start(1000*10) 45 | self.inquiryTimer.timeout.connect(self.timeout) 46 | 47 | self.setAccountComboBox() 48 | self.codeLineEdit.textChanged.connect(self.setCodeName) 49 | self.orderBtn.clicked.connect(self.sendOrder) 50 | self.inquiryBtn.clicked.connect(self.inquiryBalance) 51 | 52 | # 자동 주문 53 | # 자동 주문을 활성화 하려면 True로 설정 54 | self.isAutomaticOrder = False 55 | 56 | # 자동 선정 종목 리스트 테이블 설정 57 | self.setAutomatedStocks() 58 | 59 | def timeout(self): 60 | """ 타임아웃 이벤트가 발생하면 호출되는 메서드 """ 61 | 62 | # 어떤 타이머에 의해서 호출되었는지 확인 63 | sender = self.sender() 64 | 65 | # 메인 타이머 66 | if id(sender) == id(self.timer): 67 | currentTime = QTime.currentTime().toString("hh:mm:ss") 68 | automaticOrderTime = QTime.currentTime().toString("hhmm") 69 | 70 | # 상태바 설정 71 | state = "" 72 | 73 | if self.kiwoom.getConnectState() == 1: 74 | 75 | state = self.serverGubun + " 서버 연결중" 76 | else: 77 | state = "서버 미연결" 78 | 79 | self.statusbar.showMessage("현재시간: " + currentTime + " | " + state) 80 | 81 | # 자동 주문 실행 82 | # 1100은 11시 00분을 의미합니다. 83 | if self.isAutomaticOrder and int(automaticOrderTime) >= 1100: 84 | self.isAutomaticOrder = False 85 | self.automaticOrder() 86 | self.setAutomatedStocks() 87 | 88 | # log 89 | if self.kiwoom.msg: 90 | self.logTextEdit.append(self.kiwoom.msg) 91 | self.kiwoom.msg = "" 92 | 93 | # 실시간 조회 타이머 94 | else: 95 | if self.realtimeCheckBox.isChecked(): 96 | self.inquiryBalance() 97 | 98 | def setCodeName(self): 99 | """ 종목코드에 해당하는 한글명을 codeNameLineEdit에 설정한다. """ 100 | 101 | code = self.codeLineEdit.text() 102 | 103 | if code in self.codeList: 104 | codeName = self.kiwoom.getMasterCodeName(code) 105 | self.codeNameLineEdit.setText(codeName) 106 | 107 | def setAccountComboBox(self): 108 | """ accountComboBox에 계좌번호를 설정한다. """ 109 | 110 | try: 111 | cnt = int(self.kiwoom.getLoginInfo("ACCOUNT_CNT")) 112 | accountList = self.kiwoom.getLoginInfo("ACCNO").split(';') 113 | self.accountComboBox.addItems(accountList[0:cnt]) 114 | except (KiwoomConnectError, ParameterTypeError, ParameterValueError) as e: 115 | self.showDialog('Critical', e) 116 | 117 | def sendOrder(self): 118 | """ 키움서버로 주문정보를 전송한다. """ 119 | 120 | orderTypeTable = {'신규매수': 1, '신규매도': 2, '매수취소': 3, '매도취소': 4} 121 | hogaTypeTable = {'지정가': "00", '시장가': "03"} 122 | 123 | account = self.accountComboBox.currentText() 124 | orderType = orderTypeTable[self.orderTypeComboBox.currentText()] 125 | code = self.codeLineEdit.text() 126 | hogaType = hogaTypeTable[self.hogaTypeComboBox.currentText()] 127 | qty = self.qtySpinBox.value() 128 | price = self.priceSpinBox.value() 129 | 130 | try: 131 | self.kiwoom.sendOrder("수동주문", "0101", account, orderType, code, qty, price, hogaType, "") 132 | 133 | except (ParameterTypeError, KiwoomProcessingError) as e: 134 | self.showDialog('Critical', e) 135 | 136 | def inquiryBalance(self): 137 | """ 예수금상세현황과 계좌평가잔고내역을 요청후 테이블에 출력한다. """ 138 | 139 | self.inquiryTimer.stop() 140 | 141 | try: 142 | # 예수금상세현황요청 143 | self.kiwoom.setInputValue("계좌번호", self.accountComboBox.currentText()) 144 | self.kiwoom.setInputValue("비밀번호", "0000") 145 | self.kiwoom.commRqData("예수금상세현황요청", "opw00001", 0, "2000") 146 | 147 | # 계좌평가잔고내역요청 - opw00018 은 한번에 20개의 종목정보를 반환 148 | self.kiwoom.setInputValue("계좌번호", self.accountComboBox.currentText()) 149 | self.kiwoom.setInputValue("비밀번호", "0000") 150 | self.kiwoom.commRqData("계좌평가잔고내역요청", "opw00018", 0, "2000") 151 | 152 | while self.kiwoom.inquiry == '2': 153 | time.sleep(0.2) 154 | 155 | self.kiwoom.setInputValue("계좌번호", self.accountComboBox.currentText()) 156 | self.kiwoom.setInputValue("비밀번호", "0000") 157 | self.kiwoom.commRqData("계좌평가잔고내역요청", "opw00018", 2, "2") 158 | 159 | except (ParameterTypeError, ParameterValueError, KiwoomProcessingError) as e: 160 | self.showDialog('Critical', e) 161 | 162 | # accountEvaluationTable 테이블에 정보 출력 163 | item = QTableWidgetItem(self.kiwoom.opw00001Data) # d+2추정예수금 164 | item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) 165 | self.accountEvaluationTable.setItem(0, 0, item) 166 | 167 | for i in range(1, 6): 168 | item = QTableWidgetItem(self.kiwoom.opw00018Data['accountEvaluation'][i-1]) 169 | item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) 170 | self.accountEvaluationTable.setItem(0, i, item) 171 | 172 | self.accountEvaluationTable.resizeRowsToContents() 173 | 174 | # stocksTable 테이블에 정보 출력 175 | cnt = len(self.kiwoom.opw00018Data['stocks']) 176 | self.stocksTable.setRowCount(cnt) 177 | 178 | for i in range(cnt): 179 | row = self.kiwoom.opw00018Data['stocks'][i] 180 | 181 | for j in range(len(row)): 182 | item = QTableWidgetItem(row[j]) 183 | item.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) 184 | self.stocksTable.setItem(i, j, item) 185 | 186 | self.stocksTable.resizeRowsToContents() 187 | 188 | # 데이터 초기화 189 | self.kiwoom.opwDataReset() 190 | 191 | # inquiryTimer 재시작 192 | self.inquiryTimer.start(1000*10) 193 | 194 | # 경고창 195 | def showDialog(self, grade, error): 196 | gradeTable = {'Information': 1, 'Warning': 2, 'Critical': 3, 'Question': 4} 197 | 198 | dialog = QMessageBox() 199 | dialog.setIcon(gradeTable[grade]) 200 | dialog.setText(error.msg) 201 | dialog.setWindowTitle(grade) 202 | dialog.setStandardButtons(QMessageBox.Ok) 203 | dialog.exec_() 204 | 205 | def setAutomatedStocks(self): 206 | fileList = ["buy_list.txt", "sell_list.txt"] 207 | automatedStocks = [] 208 | 209 | try: 210 | for file in fileList: 211 | # utf-8로 작성된 파일을 212 | # cp949 환경에서 읽기위해서 encoding 지정 213 | with open(file, 'rt', encoding='utf-8') as f: 214 | stocksList = f.readlines() 215 | automatedStocks += stocksList 216 | except Exception as e: 217 | e.msg = "setAutomatedStocks() 에러" 218 | self.showDialog('Critical', e) 219 | return 220 | 221 | # 테이블 행수 설정 222 | cnt = len(automatedStocks) 223 | self.automatedStocksTable.setRowCount(cnt) 224 | 225 | # 테이블에 출력 226 | for i in range(cnt): 227 | stocks = automatedStocks[i].split(';') 228 | 229 | for j in range(len(stocks)): 230 | if j == 1: 231 | name = self.kiwoom.getMasterCodeName(stocks[j].rstrip()) 232 | item = QTableWidgetItem(name) 233 | else: 234 | item = QTableWidgetItem(stocks[j].rstrip()) 235 | 236 | item.setTextAlignment(Qt.AlignVCenter | Qt.AlignCenter) 237 | self.automatedStocksTable.setItem(i, j, item) 238 | 239 | self.automatedStocksTable.resizeRowsToContents() 240 | 241 | def automaticOrder(self): 242 | fileList = ["buy_list.txt", "sell_list.txt"] 243 | hogaTypeTable = {'지정가': "00", '시장가': "03"} 244 | account = self.accountComboBox.currentText() 245 | automatedStocks = [] 246 | 247 | # 파일읽기 248 | try: 249 | for file in fileList: 250 | # utf-8로 작성된 파일을 251 | # cp949 환경에서 읽기위해서 encoding 지정 252 | with open(file, 'rt', encoding='utf-8') as f: 253 | stocksList = f.readlines() 254 | automatedStocks += stocksList 255 | except Exception as e: 256 | e.msg = "automaticOrder() 에러" 257 | self.showDialog('Critical', e) 258 | return 259 | 260 | cnt = len(automatedStocks) 261 | 262 | # 주문하기 263 | buyResult = [] 264 | sellResult = [] 265 | 266 | for i in range(cnt): 267 | stocks = automatedStocks[i].split(';') 268 | 269 | code = stocks[1] 270 | hoga = stocks[2] 271 | qty = stocks[3] 272 | price = stocks[4] 273 | 274 | try: 275 | if stocks[5].rstrip() == '매수전': 276 | self.kiwoom.sendOrder("자동매수주문", "0101", account, 1, code, int(qty), int(price), hogaTypeTable[hoga], "") 277 | 278 | # 주문 접수시 279 | if self.kiwoom.orderNo: 280 | buyResult += automatedStocks[i].replace("매수전", "매수주문완료") 281 | self.kiwoom.orderNo = "" 282 | # 주문 미접수시 283 | else: 284 | buyResult += automatedStocks[i] 285 | 286 | # 참고: 해당 종목을 현재도 보유하고 있다고 가정함. 287 | elif stocks[5].rstrip() == '매도전': 288 | self.kiwoom.sendOrder("자동매도주문", "0101", account, 2, code, int(qty), int(price), hogaTypeTable[hoga], "") 289 | 290 | # 주문 접수시 291 | if self.kiwoom.orderNo: 292 | sellResult += automatedStocks[i].replace("매도전", "매도주문완료") 293 | self.kiwoom.orderNo = "" 294 | # 주문 미접수시 295 | else: 296 | sellResult += automatedStocks[i] 297 | 298 | except (ParameterTypeError, KiwoomProcessingError) as e: 299 | self.showDialog('Critical', e) 300 | 301 | # 잔고및 보유종목 디스플레이 갱신 302 | self.inquiryBalance() 303 | 304 | # 결과저장하기 305 | for file, result in zip(fileList, [buyResult, sellResult]): 306 | with open(file, 'wt', encoding='utf-8') as f: 307 | for data in result: 308 | f.write(data) 309 | 310 | 311 | if __name__ == "__main__": 312 | app = QApplication(sys.argv) 313 | myWindow = MyWindow() 314 | sys.exit(app.exec_()) 315 | -------------------------------------------------------------------------------- /pytrader.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1699 10 | 1468 11 | 12 | 13 | 14 | PyTrader v0.4 15 | 16 | 17 | 18 | pytrader.pngpytrader.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 356 31 | 426 32 | 33 | 34 | 35 | 36 | 356 37 | 426 38 | 39 | 40 | 41 | 수동주문 42 | 43 | 44 | 45 | 18 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 계좌 55 | 56 | 57 | accountComboBox 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 주문 72 | 73 | 74 | orderTypeComboBox 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 신규매수 83 | 84 | 85 | 86 | 87 | 신규매도 88 | 89 | 90 | 91 | 92 | 매수취소 93 | 94 | 95 | 96 | 97 | 매도취소 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 종목 110 | 111 | 112 | codeLineEdit 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | background-color: rgb(170, 255, 255) 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 종류 138 | 139 | 140 | hogaTypeComboBox 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 지정가 149 | 150 | 151 | 152 | 153 | 시장가 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 수량 166 | 167 | 168 | qtySpinBox 169 | 170 | 171 | 172 | 173 | 174 | 175 | 1000000 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 가격 187 | 188 | 189 | priceSpinBox 190 | 191 | 192 | 193 | 194 | 195 | 196 | 1000000 197 | 198 | 199 | 50 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 현금주문 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 356 222 | 16777215 223 | 224 | 225 | 226 | 로그 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 1289 244 | 876 245 | 246 | 247 | 248 | 249 | 1289 250 | 876 251 | 252 | 253 | 254 | 잔고 및 보유종목 현황 255 | 256 | 257 | 258 | 259 | 260 | 261 | 1251 262 | 111 263 | 264 | 265 | 266 | 267 | 1251 268 | 111 269 | 270 | 271 | 272 | 1 273 | 274 | 275 | 276 | 277 | 예수금 (d+2) 278 | 279 | 280 | 281 | 282 | 총매입 283 | 284 | 285 | 286 | 287 | 총평가 288 | 289 | 290 | 291 | 292 | 총손익 293 | 294 | 295 | 296 | 297 | 총수익률 (%) 298 | 299 | 300 | 301 | 302 | 추정자산 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 1251 312 | 631 313 | 314 | 315 | 316 | 317 | 1251 318 | 16777215 319 | 320 | 321 | 322 | 323 | 종목명 324 | 325 | 326 | 327 | 328 | 보유량 329 | 330 | 331 | 332 | 333 | 매입가 334 | 335 | 336 | 337 | 338 | 현재가 339 | 340 | 341 | 342 | 343 | 평가손익 344 | 345 | 346 | 347 | 348 | 수익률 (%) 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 920 357 | 358 | 359 | 360 | 361 | true 362 | 363 | 364 | 365 | 170 366 | 28 367 | 368 | 369 | 370 | 실시간 조회 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 150 379 | 46 380 | 381 | 382 | 383 | 조회 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 자동 선정 종목 리스트 396 | 397 | 398 | 399 | 400 | 401 | 402 | 주문유형 403 | 404 | 405 | 406 | 407 | 종목명 408 | 409 | 410 | 411 | 412 | 호가구분 413 | 414 | 415 | 416 | 417 | 수량 418 | 419 | 420 | 421 | 422 | 가격 423 | 424 | 425 | 426 | 427 | 상태 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 0 445 | 0 446 | 1699 447 | 38 448 | 449 | 450 | 451 | 452 | 453 | 454 | accountComboBox 455 | orderTypeComboBox 456 | codeLineEdit 457 | hogaTypeComboBox 458 | qtySpinBox 459 | priceSpinBox 460 | orderBtn 461 | codeNameLineEdit 462 | 463 | 464 | 465 | 466 | -------------------------------------------------------------------------------- /sell_list.txt: -------------------------------------------------------------------------------- 1 | 매도;035720;시장가;10;0;매도전 2 | 매도;039490;시장가;10;0;매도전 --------------------------------------------------------------------------------