안녕하세요 성민석입니다.
진행하는 튜토리얼의 모든 코드는 대신증권 사이보스플러스 자료실에서 제공하는 걸 기반으로 만들었습니다.
그리고 여기에 사용된 모든 코드는 저의 GitHub에서 확인하실 수 있습니다.
지난 시간에는 대신증권API를 통해서 예수금 가져오기에 대해서 알아보았습니다. 이 때 제가 대신증권API를 통해서 데이터를 받아오는 방법이 사뭇 다르다는 걸 깨닫고 대신증권에서 제공하는 튜토리얼을 한번 공부하고자 합니다.
대신증권API를 통해서 데이터를 가져올 수 있는 방식은 크게 2가지라고 합니다.
- BlockRequest 방식 - 가장 간단하게 데이터 요청해서 수신 가능
- Request 방식 - 함수 호출 후 Received 이벤트로 수신 받기
일반적인 데이터 요청에는 BlockRequest 방식이 가장 간단합니다. 다만, BlockRequest 함수 내에서도 동일 하게 메시지 펌핑을 하고 있어 해당 통신이 마치기 전에 실시간 시세를 수신 받거나 다른 이벤트에 의해 재귀 호출 되는 문제가 있을 경우 함수 호출이 실패할 수 있습니다. 복잡한 실시간 시세 수신 중에 통신을 해야 하는 경우에는 Request 방식을 이용해야 합니다.
출처: 대신증권 자료실
대신증권 자료실에 있는 말을 조금 더 쉽게 풀어보면 저번 시간에 예수금 가져왔을 때 확인할 수 있었듯이, BlockRequest()로 데이터를 가져올 수 있었습니다. 근데 이렇게 한꺼번에 (그래서 Block이란 표현을 사용하는듯 합니다.) 내가 필요한 모든 정보가 담겨있는 데이터와 달리 실시간 데이터는 계속해서 데이터를 받아야 합니다. 여전히 감이 안 오실 수 있으니 예제를 한번 살펴보죠.
일단 에러부터 해결해야합니다.
AttributeError: module 'win32event' has no attribute 'createEvent' 라는 에러가 뜨는데 생각보다 오랜 시간을 삽질하고 나서야 게시판의 댓글 도움으로 해결할 수 있었습니다. 저기 c가 C로 바뀌어야한다는 것입니다. 혹시 이걸 변경해도 동작이 안되시는 분들은 라이브러리 설치를 해보시길 바랍니다.
pip install pywin32
pip install pypiwin32
예제를 돌리면 아래와 같은 결과가 나옵니다. 한번 코드를 차근차근 뜯어보죠.
BlockRequest 방식에 대하여
여기서 보면 DsCbo1.StockMst API를 이용하여 객체를 생성합니다. 그런 후에는 SetInputValue 함수를 통해서 파라미터를 설정하고 BlockRequest() 함수를 통해서 데이터를 가져옵니다. 이렇게 가져온 데이터는 objStockMst의 GetHeaderValue를 통해서 가져올 수 있습니다.
# 1. BlockRequest
print('#####################################')
objStockMst = win32com.client.Dispatch("DsCbo1.StockMst")
code = 'A005930'
objStockMst.SetInputValue(0, code)
objStockMst.BlockRequest()
print('BlockRequest 로 수신 받은 데이터')
item = {}
item['종목명'] = g_objCodeMgr.CodeToName(code)
item['현재가'] = objStockMst.GetHeaderValue(11) # 종가
item['대비'] = objStockMst.GetHeaderValue(12) # 전일대비
print(item)
여기까진 예수금 데이터를 가져왔을 때처럼 동일하게 데이터를 가져올 수 있는 방식이었습니다. 근데 우리가 아직 생소한 Request 방식에 대해서 알아봅시다.
Request 방식에 대하여
일단 CpCurReply라는 클래스와 MessagePump라는 함수가 무엇인지 모르겠습니다. 하지만 코드로만 알 수 있는 것은 일단 CpCurReply 클래스를 통해서 객체를 생성한 후 Subscribe 함수를 호출합니다. 그러고 앞선 예제와 같이 objStockMst 객체를 통해서 이번엔 Request 함수를 호출하게 됩니다. (앞에서는 BlockRequest 함수를 호출했음을 기억해주세요.) 그런 뒤에 MessagePump 함수가 호출되고 이후는 앞의 예제와 동일하게 진행됩니다.
# 2. Request ==> 메시지 펌프 ==> OnReceived 이벤트 수신
print('#####################################')
objReply = CpCurReply(objStockMst)
objReply.Subscribe()
code = 'A005930'
objStockMst.SetInputValue(0, code)
objStockMst.Request()
MessagePump(10000)
item = {}
item['종목명'] = g_objCodeMgr.CodeToName(code)
item['현재가'] = objStockMst.GetHeaderValue(11) # 종가
item['대비'] = objStockMst.GetHeaderValue(12) # 전일대비
print(item)
일단 Request 방식을 이해하기 위해서 CpEvent 클래스와 CpCurReply 클래스와 마지막으로 MessagePump 함수를 하나씩 살펴봅시다. 저도 코딩을 잘하진 않아서 조금 헷갈리네요.
CpEvent 클래스
CpEvent 클래스는 실시간 통신을 하기 위한 객체입니다. 이후 CpCurReply 클래스의 Subscribe 메소드에서 사용될 예정입니다. 근데 OnReceived 메소드는 어디서 사용되는지 아직까지 확인할 수 없습니다.
class CpEvent:
def set_params(self, client, name, caller):
self.client = client # CP 실시간 통신 object
self.name = name # 서비스가 다른 이벤트를 구분하기 위한 이름
self.caller = caller # callback 을 위해 보관
def OnReceived(self):
# 실시간 처리 - 현재가 주문 체결
if self.name == 'stockmst':
print('recieved')
win32event.SetEvent(StopEvent)
return
CpCurReply 클래스
CpCurReply 클래스는 Subscribe 메소드를 통해서 이벤트가 발생하면 핸들링해주는 클래스입니다.
class CpCurReply:
def __init__(self, objEvent):
self.name = "stockmst"
self.obj = objEvent
def Subscribe(self):
handler = win32com.client.WithEvents(self.obj, CpEvent)
handler.set_params(self.obj, self.name, None)
MessagePump 함수
MessagePump 함수를 통해서 OnReceived 이벤트를 수신할 수 있게 됩니다.
def MessagePump(timeout):
waitables = [StopEvent]
while 1:
rc = win32event.MsgWaitForMultipleObjects(
waitables,
0, # Wait for all = false, so it waits for anyone
timeout, # (or win32event.INFINITE)
win32event.QS_ALLEVENTS) # Accepts all input
if rc == win32event.WAIT_OBJECT_0:
# Our first event listed, the StopEvent, was triggered, so we must exit
print('stop event')
break
elif rc == win32event.WAIT_OBJECT_0 + len(waitables):
# A windows message is waiting - take care of it. (Don't ask me
# why a WAIT_OBJECT_MSG isn't defined < WAIT_OBJECT_0...!).
# This message-serving MUST be done for COM, DDE, and other
# Windowsy things to work properly!
print('pump')
if pythoncom.PumpWaitingMessages():
break # we received a wm_quit message
elif rc == win32event.WAIT_TIMEOUT:
print('timeout')
return
pass
else:
print('exception')
raise RuntimeError("unexpected win32wait return value")
저는 해당 예제를 보면서 이해하려고 했는데, 여전히 감이 안 잡히네요. 그래서 여기저기 찾아보다가 대신증권 비공식 블로그에서 퍼온 내용은 아래와 같습니다. 이해하시는데 도움이 되시길 바랍니다.
CybosPlus Communication
대신증권의 통신은 Request/Reply ( RQ/RP ) 방식과 Subscribe/Publish (SB/PB) 방식으로 나눠집니다. CybosPlus의 각 통신 오브젝트는 이 두 가지 통신 모델 중 한가지만 지원합니다.
1. RQ/RP 와 SB/PB 비교
[ 비동기식 (asynchronous) ]
입력 데이터를 채워넣고 통신을 요청(Request or Subscribe) 하면 함수가 바로 반환됩니다. 그 후 서버로부터 데이터가 수신되면 Received 이벤트가 발생하게 됩니다
RQ/RP : 현시점의 데이타 1회 통신 요청
SB/PB : 실시간 데이타 수신 요청 변경 시에만 이벤트가 발생합니다. 요청시점의 데이타를 얻기위해서는 먼저 RQ/RP 오브젝트로 구현한 이후에 사용하세요. 복수종목을 실시간으로 수신받으려면 1,2 항목을 반복하면 됩니다.
2. RQ/RP의 동기식 통신 지원
[ 동기식 (synchronous) ]
입력 데이터를 채워넣고 BlockRequest 메소드를 호출하면, 서버로 부터 응답이 완료 될 때 까지 대기상태를 유지한다. 데이터를 정상적으로 수신한 후에야 함수가 리턴된다. 30초 동안 서버로 부터 요청한 데이타를 수신하지 못 하면 타임아웃으로 처리된다. BlockRequest 함수의 리턴값으로 통신결과 상태를 확인할 수 있다.
3. RQ/RP의 연속 데이타 통신
데이타 수신시에는 효율성을 고려하여 데이타의 적정 Size가 있습니다. 모든 데이타를 한번의 요청으로 얻는 것이 아니라, 여러번 요청으로 데이터를 얻을 수 있습니다. 예를 들면, CYBOS의 화면 7024,7026 처럼 시간대별, 일자별의 데이타의 양이 많습니다. 이런 경우 화면 우측 상단에 "다음" 버튼이 존재합니다. "다음" 버튼이 활성화 되어 있다는 것은 현재 수신된 데이타 이후로 데이타가 존재한다는 의미입니다. CybosPlus에서 CYBOS 화면의 "다음" 버튼이 활성화 된 상태와 같은 의미로는 각 오브젝트에 공통 속성인 Continue가 True인 상태입니다. 아래 그림과 같이 CybosPlus에서는 데이타를 수신 받고나서 Continue 속성을 체크합니다. Continue가 Ture인 것은 연속데이타가 있다는 의미이므로, 그 상태에서 통신을 요청하면(BlockRequest 또는 Request) 연속데이타를 얻을 수 있습니다.
( 다음 그림은 동기식(BlockRequest)으로 설명한 것입니다. 비동기(Request)로도 연속 데이타 통신 구현이 가능합니다)
생각보다 자세하고 쉽게 설명되어 있습니다. 간단하게 정리하자면 BlockRequest 방식은 요청할 때만 1번 데이터를 주는 것이고, Request 방식은 Subscribe 함수를 통해서 이벤트가 발생할 때마다 수시로 데이터를 수신할 수 있는 방식이라고 정리할 수 있습니다. 사실 동기식과 비동기식의 차이는 아직까지 정확하게 이해를 하지 못해서 내용을 잘 못 전달할 수 있으므로 생략하겠습니다. (추후 이해가 더 쌓이면 다시 오겠습니다...)
원문이 궁금하신 분들은 링크 걸어두었습니다. 직접 가서 확인해보시길 바랍니다.
전체코드는 아래를 확인해보시길 바랍니다.
import pythoncom
from PyQt5.QtWidgets import *
import win32com.client
import win32event
g_objCodeMgr = win32com.client.Dispatch('CpUtil.CpCodeMgr')
StopEvent = win32event.CreateEvent(None, 0, 0, None)
class CpEvent:
def set_params(self, client, name, caller):
self.client = client # CP 실시간 통신 object
self.name = name # 서비스가 다른 이벤트를 구분하기 위한 이름
self.caller = caller # callback 을 위해 보관
def OnReceived(self):
# 실시간 처리 - 현재가 주문 체결
if self.name == 'stockmst':
print('recieved')
win32event.SetEvent(StopEvent)
return
class CpCurReply:
def __init__(self, objEvent):
self.name = "stockmst"
self.obj = objEvent
def Subscribe(self):
handler = win32com.client.WithEvents(self.obj, CpEvent)
handler.set_params(self.obj, self.name, None)
def MessagePump(timeout):
waitables = [StopEvent]
while 1:
rc = win32event.MsgWaitForMultipleObjects(
waitables,
0, # Wait for all = false, so it waits for anyone
timeout, #(or win32event.INFINITE)
win32event.QS_ALLEVENTS) # Accepts all input
if rc == win32event.WAIT_OBJECT_0:
# Our first event listed, the StopEvent, was triggered, so we must exit
print('stop event')
break
elif rc == win32event.WAIT_OBJECT_0 + len(waitables):
# A windows message is waiting - take care of it. (Don't ask me
# why a WAIT_OBJECT_MSG isn't defined < WAIT_OBJECT_0...!).
# This message-serving MUST be done for COM, DDE, and other
# Windowsy things to work properly!
print('pump')
if pythoncom.PumpWaitingMessages():
break # we received a wm_quit message
elif rc == win32event.WAIT_TIMEOUT:
print('timeout')
return
pass
else:
print('exception')
raise RuntimeError("unexpected win32wait return value")
code = 'A005930'
##############################################################
#1. BlockRequest
print('#####################################')
objStockMst = win32com.client.Dispatch("DsCbo1.StockMst")
objStockMst.SetInputValue(0, code)
objStockMst.BlockRequest()
print('BlockRequest 로 수신 받은 데이터')
item = {}
item['종목명']= g_objCodeMgr.CodeToName(code)
item['현재가'] = objStockMst.GetHeaderValue(11) # 종가
item['대비'] = objStockMst.GetHeaderValue(12) # 전일대비
print(item)
print('')
##############################################################
# 2. Request ==> 메시지 펌프 ==> OnReceived 이벤트 수신
print('#####################################')
objReply = CpCurReply(objStockMst)
objReply.Subscribe()
code = 'A005930'
objStockMst.SetInputValue(0, code)
objStockMst.Request()
MessagePump(10000)
item = {}
item['종목명']= g_objCodeMgr.CodeToName(code)
item['현재가'] = objStockMst.GetHeaderValue(11) # 종가
item['대비'] = objStockMst.GetHeaderValue(12) # 전일대비
print(item)
참고자료
'금융(Finance) > 시스템 트레이딩(System Trading)' 카테고리의 다른 글
[개발] 지금까지 만든 매매 프로그램 (0) | 2020.12.01 |
---|---|
[개발] 대신증권API를 이용한 트레이딩 시스템 - 예수금 가져오기 (0) | 2020.09.22 |
[고민] 대신증권API과 키움증권 API 사이에서 (0) | 2020.09.21 |
[개발] 대신증권API를 이용한 트레이딩 시스템 - 주식 현재가 조회 (0) | 2020.09.18 |
[개발] 대신증권API를 이용한 트레이딩 시스템 - 종목정보 구하는 예제 (0) | 2020.09.18 |