지난번에는 키움증권에서 제공하는 OCX API (Open API+)를 가지고 Python Pyqt를 이용해, QAxWidget 인스턴스를 생성하고, Open API+에서 제공하는 메서드중에 가장 첫번째 메서드인 CommConnect(로그인 윈도우를 실행)을 구현해 보았습니다.
이번에는 첫번째 메서드 정의서를 잘 보셨으면, 발견하셨을 수도 있는... 이벤트에 대해서 구현해 보겠습니다. 앞으로의 모든 TR(Transaction)은 이벤트로 응답이 오기 때문에 필수적인 관문이라고 할 수 있습니다.
자세히 보시면 비고에 "로그인이 성공하거나 실패하는 경우 OnEventConnect이벤트가 발생" 이라고 써있습니다. 반환값의 0성공 / 음수실패가 dynamicCall 그 자체에 대한 결과라면... 이벤트는 메서드 실행에 대한 결과라고 생각하면 됩니다.
따라서 위의 메서드를 실행해서 로그인 윈도우가 뜬다면, 그 이후 로그인 단계를 진행해야 그 이벤트가 발생하게 됩니다. 그림으로 보자면...
위와같이 단계적인 상관관계가 생기게 됩니다. 그림에서 처럼 위의 빨간색 로직이 실행되고, 회색 로직이 실행되는 방식입니다. 그럼 현재 구현되지 않은 이벤트처리하는 방법과 가장 첫 TR의 전체로직을 구현해 봄으로써 완벽히 이해해 보겠습니다.
아래의 순서로 진행하겠습니다.
- OnEventConnect 구현
- TR에 대한 dynamicCall 및 OnReceiveTrData 구현
1. OnEventConnect
사실 해당 이벤트는 언제 발생할 지 프로그램은 알 수가 없습니다. 때문에 계속 발생하는지 확인하는 작업이 필요한데 일반적으로 이를 listener를 구현한다고 합니다. QAxWidget에서 제공하는 방법은 너무나 간단합니다.
QAxWidget 인스턴스에 해당 이벤트를 연결하고, 그 뒤에 발생했을때 동작할 메서드를 연결하면 됩니다. 실질적인 코드는 아래와 같습니다.
# Event Listener
self.OCXconn.OnEventConnect.connect(self.connEvent)
이어서 개발 가이드의 OnEventConnect를 찾아보면 아래와 같습니다. 입력값이 AT Project로 전달되어 오는 값이라고 생각하면되고, 이를 메서드에서 받아서 처리하면 됩니다.
그렇다면 관련된 부분을 구현하고 그 결과를 확인해 보겠습니다.
[KiwoomTest.py]
import sys
from PyQt5.QAxContainer import QAxWidget
from PyQt5.QtWidgets import QApplication
class KiwoomAPI:
def __init__(self):
# QAxWidget Instance
self.OCXconn = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1")
# Event Listener
self.OCXconn.OnEventConnect.connect(self.connEvent)
def login(self):
ret = self.OCXconn.dynamicCall("CommConnect()")
print(ret)
def connEvent(self, nErrCode):
if nErrCode == 0:
print('로그인 성공')
else:
print('로그인 실패')
if __name__ == "__main__":
app = QApplication(sys.argv)
test = KiwoomAPI()
test.login()
app.exec_()
def connEvent라는 메서드를 추가해서 이벤트 발생 시, 연결되는 부분을 처리해줬습니다.
정상적으로 로그인창이 뜨고, 로그인을 하게되면 결과를 Event로 리턴을 잘 해주네요...!!!
2. 첫번째 TR구현 (opt10001)
이제 본격적으로 Open API+가 제공하는 TR을 구현해 보겠습니다. 처음이니 일단 가장 첫 TR을 보자면... 우선 생김새를 개발자 가이드와 KOA Studio를 통해서 확인해 봅니다. 해당 TR은 주식기본정보요청 이라는 Transaction이며, SetInputValue를 하고 CommRqData로 요청하는 방식입니다.
이 정보들을 토대로 정리를 해 본다면, 아래의 단계로 구현을 진행하면 될 것 같습니다.
- SetInputValue 로 필요한 파라메터 값 세팅 메서드 호출
- CommRqData로 Transaction 메서드 호출
- OnReceiveTrData 이벤트 리스너에서 이벤트 감지
- connect된 메서드에서 CommGetData 메서드로 값 가져옴
그럼 이제 한땀 한땀 자세히 알아보겠습니다.
[SetInputValue]
KOA를 보면 아래와 같이 opt10001를 호출하기 전에 종목코드를 설정하는 단계가 필요합니다. 따라서, dynamicCall을 이용해서 item과 value를 기재하여 setting합니다.
self.OCXconn.dynamicCall("SetInputValue(QString, QString)", "종목코드", "005930")
[CommRqData]
위의 KOA가이드의 다음단계로 SetInputValue가 완료되면 TR을 서버로 요청하면 됩니다. 총 4개의 파라미터의 입력이 필요하며, 첫번째는 사용자가 여러개의 TR을 요청할테니 TR에 대한 구분자 입니다. 두번째는 TR이름인 opt10001을 입력하고, 세번째도 default 값입니다. 마지막은 화면번호인데... 위의 개발자 가이드의 sample을 인용해서 넣습니다.
self.OCXconn.dynamicCall("CommRqData(QString, QString, QString, QString)", "AT_opt10001", "opt10001", "0", "0101")
[OnReceiveTrData]
서버에 요청했으니... 이제 응답이 옵니다. TR에 대한 이벤트는 아래의 개발자 가이드의 이벤트 목록을 통해서 보면...
OnReceiveTrData이 발생한다는 것을 알 수 있습니다. 그럼 로그인과 동일하게 이벤트 listener를 등록합니다.
self.OCXconn.OnReceiveTrData.connect(self.trEvent)
TR에 대해서는 동일하게 하나의 이벤트가 발생하기 때문에, 처리할 메서드는 한개로 만들고 안에서 분기로 처리하면 될 것 같은 느낌이 듭니다. 이벤트 리스너를 등록해서 제작된 메서드에 연결하는 건 간단한 작업입니다. 하지만 연결할 메서드를 만들기 위해서는 어떤 값들이 이벤트로 발생해서 넘어오는지 알아야 합니다. 그래야 만들 수 있으니까요... 개발자 가이드를 보겠습니다.
해당 이벤트는 CommRqData를 하고 서버로부터 데이터를 전달받는 시점에 발생합니다. 총 9개의 파라메터가 넘어옵니다. 그중에 4개는 사용하지 않으니... 5개만 잘 보면 되겠습니다. 그런데 잘 보아하니... 실제 원하는 데이터는 없고 사용자구분... Tran명 등 뭔가 구분하는 내용만 존재하네요!!! 그렇습니다. 바로 데이터를 제공하지 않고... 데이터 수신 이벤트가 발생하면 마지막 메서드로 데이터를 가져와야 합니다.
따라서 한개의 이벤트와 연결된 메서드를 만들어 놓는다면, 어떤 TR도 구분자로 if를 처리해 준다면 별도 메서드 없이 데이터를 막 가져올 수 있는 것 입니다.
def trEvent(self, sScrNo, sRQName, sTrCode, sRecordName, sPreNext, nDataLength, sErrorCode, sMessage, sSplmMsg):
# sScrNo(화면번호), sRQName(사용자구분), sTrCode(Tran명), sRecordName(레코드명), sPreNext(연속조회 유무)
if sRQName == 'AT_opt10001':
print('응답옴')
다음과 같이, CommRqData에 사용자 구분으로 넣은 코드(ex. AT_opt10001)로 구분하면 됩니다.
[CommGetData]
개발자 가이드를 보면... 느낌상
이 메서드를 통해서 값을 가져오는것 같은데... 이 함수대신 다른걸 사용하라고 합니다... 우린 그냥 조회기 때문에 GetCommData()를 사용하면 됩니다.
if sRQName == 'AT_opt10001':
print('레코드이름', sRecordName)
name = self.OCXconn.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, 0, "종목명")
print(name)
##################################################
레코드이름
삼성전자
데이터를 가져와보면... 실제로 sRQName에는 무엇이 들어가도 상관이 없는 것 같습니다. 혹시나 해서 이벤트를 통해서 가져온 9개의 parameter중에서 sRecordName을 출력해봐도 아무런 값이 없으며, 아무 String이나 넣어도 결과는 오는것을 확인할 수 있습니다.
또한, 받은데이터를 출력해보면... string에 많은 공백이 있습니다. 따라서 공백제거를 strip( )을 통해서 해주시면 됩니다. 그럼 현재까지 전체코드를 확인해보면 아래와 같습니다.
[전체코드]
import sys
from PyQt5.QAxContainer import QAxWidget
from PyQt5.QtWidgets import QApplication
class KiwoomAPI:
def __init__(self):
# QAxWidget Instance
self.OCXconn = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1")
# Event Listener
self.OCXconn.OnEventConnect.connect(self.connEvent)
self.OCXconn.OnReceiveTrData.connect(self.trEvent)
def reqTR(self):
#opt10001
self.OCXconn.dynamicCall("SetInputValue(QString, QString)", "종목코드", "005930")
self.OCXconn.dynamicCall("CommRqData(QString, QString, QString, QString)", "AT_opt10001", "opt10001", "0", "0101")
def login(self):
ret = self.OCXconn.dynamicCall("CommConnect()")
print(ret)
def connEvent(self, nErrCode):
if nErrCode == 0:
print('로그인 성공')
self.reqTR()
else:
print('로그인 실패')
def trEvent(self, sScrNo, sRQName, sTrCode, sRecordName, sPreNext, nDataLength, sErrorCode, sMessage, sSplmMsg):
# sScrNo(화면번호), sRQName(사용자구분), sTrCode(Tran명), sRecordName(레코드명), sPreNext(연속조회 유무)
if sRQName == 'AT_opt10001':
print('레코드이름', sRecordName)
name = self.OCXconn.dynamicCall("GetCommData(QString, QString, int, QString)", sTrCode, sRQName, 0, "종목명")
print(name.strip())
if __name__ == "__main__":
app = QApplication(sys.argv)
test = KiwoomAPI()
test.login()
app.exec_()
고생하셨습니다.
-Ayotera Lab-
댓글