티스토리 뷰

오늘은 키움증권의 open api를 이용하여 자동매매하는 틀을 만들예정이다. 키움증권 HTS에 등록한 조건검색식에서 검출되는 종목을 실시간으로 모니터링하고, 해당 종목을 매수한다. 매수 후에 -2%는 손절가, +3%는 익절가로 설정하여 자동으로 매도하도록 한다. 트레이딩 관련한 모든 정보는 DB에 저장되어 장 종료 후 트레이딩 분석자료로 활용한다.



자동매매 구현을 위한 TopTrader 클래스를 다음과 같이 정의한다.

class TopTrader(QMainWindow, ui):
def __init__(self):
super().__init__()
self.tt_logger = TTlog()
self.mongo = MongoClient()
self.tt_db = self.mongo.TopTrader
self.slack = Slacker(config_manager.SLACK_TOKEN)
self.kw = Kiwoom()
self.login()
self.auto_trading()

def login(self):
# Login
err_code = self.kw.login()
if err_code != 0:
self.tt_logger.error("Login Fail")
return
self.tt_logger.info("Login success")

def auto_trading(self):
"""키움증권 HTS에 등록한 조건검색식에서 검출한 종목을 매수하고
-2%, +3%에 손절/익절 매도하는 기본적인 자동매매 함수

:return:
"""
pass

<main_app.py>


조건검색식 불러오기

키움증권 HTS에 등록한 사용자 조건검색식을 로딩하는 부분을 먼저 구현한다.

실시간 조건검색 등록 후, 조건검색식을 통해 실시간 종목이 검출(편입)되면, 키움 모듈의 OnReceiveRealCondition 이벤트가 발생하고,  main app(TopTrader)의 search_condi 함수가 callback되도록 구현하였다.


아래 샘플코드에서는 5개 조건검색식을 실시간 검색하도록 등록하였다.

def auto_trading(self):
"""키움증권 HTS에 등록한 조건검색식에서 검출한 종목을 매수하고
-2%, +3%에 손절/익절 매도하는 기본적인 자동매매 함수

:return:
"""
# callback fn 등록
self.kw.notify_fn["_on_receive_real_condition"] = self.search_condi

screen_no = "4000"
condi_info = self.kw.get_condition_load()
# {'추천조건식01': '002', '추천조건식02': '000', '급등/상승_추세조건': '001', 'Envelop횡단': '003', '스켈핑': '004'}
for condi_name, condi_id in condi_info.items():
# 화면번호, 조건식이름, 조건식ID, 실시간조건검색(1)
self.kw.send_condition(screen_no, condi_name, int(condi_id), 1)
time.sleep(0.2)

<main_app.py>


실시간 조건검색식 종목 검출하기

키움모듈이 조건검색식 실시간 검색을 수행하고 종목이 검출되면 OnReceiveRealCondition이벤트가 발생한다. 해당 이벤트를 처리하는 부분을 먼저 구현한다.


class Kiwoom(QAxWidget):
def __init__(self):
super().__init__()
self.logger = KWlog()
self.tr_mgr = TrManager(self)
self.evt_loop = QEventLoop() # lock/release event loop
self.ret_data = None
self.req_queue = deque(maxlen=10)
self._create_kiwoom_instance()
self._set_signal_slots()
self.tr_controller = TrController(self)
self.notify_fn = {}

def _set_signal_slots(self):
self.OnEventConnect.connect(self._on_event_connect) # 로긴 이벤트
self.OnReceiveTrData.connect(self.tr_mgr._on_receive_tr_data) # tr 수신 이벤트
self.OnReceiveRealData.connect(self._on_receive_real_data) # 실시간 시세 이벤트
self.OnReceiveRealCondition.connect(self._on_receive_real_condition) # 조건검색 실시간 편입, 이탈종목 이벤트
self.OnReceiveTrCondition.connect(self._on_receive_tr_condition) # 조건검색 조회응답 이벤트
self.OnReceiveConditionVer.connect(self._on_receive_condition_ver) # 로컬에 사용자조건식 저장 성공여부 응답 이벤트
self.OnReceiveChejanData.connect(self._on_receive_chejan_data) # 주문 접수/확인 수신시 이벤트
self.OnReceiveMsg.connect(self._on_receive_msg) # 수신 메시지 이벤트

<kw.py>



OnReceiveRealCondition 이벤트 처리하는 부분

def _on_receive_real_condition(self, code, event_type, condi_name, condi_index):
"""
Kiwoom Receive Realtime Condition Result(stock list) Callback, 조건검색 실시간 편입, 이탈 종목을 받을 시점을 알려준다.
condi_name(조건식)으로 부터 검출된 종목이 실시간으로 들어옴.
update_type으로 편입된 종목인지, 이탈된 종목인지 구분한다.
* 조건식 검증할때, 어떤 종목이 검출된 시간을 본 함수내에서 구현해야 함
:param code str: 종목코드
:param event_type str: 편입("I"), 이탈("D")
:param condi_name str: 조건식명
:param condi_index str: 조건식명 인덱스
:return: 없음
"""
try:
self.logger.info("_on_receive_real_condition")
max_char_cnt = 60
self.logger.info("[실시간 조건 검색 결과]".center(max_char_cnt, '-'))
data = [
("code", code),
("event_type", event_type),
("condi_name", condi_name),
("condi_index", condi_index)
]
max_key_cnt = max(len(d[0]) for d in data) + 3
for d in data:
key = ("* " + d[0]).rjust(max_key_cnt)
self.logger.info("{0}: {1}".format(key, d[1]))
self.logger.info("-" * max_char_cnt)
data = dict(data)
data["kw_event"] = "OnReceiveRealCondition"
if '_on_receive_real_condition' in self.notify_fn:
self.notify_fn['_on_receive_real_condition'](data)

except Exception as e:
self.logger.error(e)
finally:
self.real_condition_search_result = []

<kw.py>





종목 매수하기

조건검색식 실시간 검색을 통해 검출된 종목을 매수하는 함수.

간단하게 종목당 10만원어치 시장가매수를 하도록 구현하였다.

def search_condi(self, event_data):
"""키움모듈의 OnReceiveRealCondition 이벤트 수신되면 호출되는 callback함수
이벤트 정보는 event_data 변수로 전달된다.

ex)
event_data = {
"code": code, # "066570"
"event_type": event_type, # "I"(종목편입), "D"(종목이탈)
"condi_name": condi_name, # "스켈핑"
"condi_index": condi_index # "004"
}
:param dict event_data:
:return:
"""
if event_data["event_type"] == "I":
if self.stock_account["계좌정보"]["예수금"] < 100000: # 잔고가 10만원 미만이면 매수 안함
return
curr_price = self.kw.get_curr_price(event_data["code"])
quantity = int(100000/curr_price)
self.kw.reg_callback("OnReceiveChejanData", ("조건식매수", "5000"), self.update_account)
self.kw.send_order("조건식매수", "5000", self.acc_no, 1, event_data["code"], quantity, 0, "03", "")

<main_app.py>




계좌 업데이트

키움증권 open api 중 OPW00004(계좌평가현황요청) TR을 이용하여 현재 계좌현황정보를 얻을 수 있다.
아래와 같이 자동매매 프로그램이 구동될 때, 초기화를 해주고, search_condi 함수내에서 매수주문을 넣기직전에 callback을 등록하여 매수주문이 체결되면 계좌정보도 업데이트 되도록 구현하였다.

def set_account(self):
self.acc_no = self.kw.get_login_info("ACCNO")
self.acc_no = self.acc_no.strip(";") # 계좌 1개를 가정함.
self.stock_account = self.kw.계좌평가현황요청("계좌평가현황요청", self.acc_no, "", "1", "6001")

def update_account(self):
self.stock_account = self.kw.계좌평가현황요청("계좌평가현황요청", self.acc_no, "", "1", "6001")

종목 매도하기

PyQT의 Timer기능을 이용하여 주기적으로 계좌현황을 조회하고, 익절/손절 목표가에 도달하였을 때 매도를 하는 코드를 작성하였다.
30초 주기로 계좌현황을 조회하도록 작성하였다.
def start_timer(self):
if self.timer:
self.timer.stop()
self.timer.deleteLater()
self.timer = QTimer()
self.timer.timeout.connect(self.sell)
# self.timer.setSingleShot(True)
self.timer.start(30000) # 30 sec interval

def sell(self):
self.update_account()
print("=" * 50)
print("현재 계좌 현황입니다...")
for data in self.stock_account["종목정보"]:
stock_name, code, quantity = data["종목코드"], data["종목명"], data["보유수량"]
print("* 종목: {}, 손익율: {}%, 보유수량: {}, 평가금액: {}원".format(
data["종목명"], ("%.2f" % data["손익율"]), int(data["보유수량"]), format(int(data["평가금액"]), ',')
))
if data["손익율"] > 3.0:
print("시장가로 물량 전부 익절합니다. [{}, {}주]".format(stock_name, quantity))
self.kw.send_order("익절매도", "5001", self.acc_no, 2, code, quantity, 0, "03", "")
elif data["손익율"] < -2.0:
print("시장가로 물량 전부 손절합니다. [{}, {}주]".format(stock_name, quantity))
self.kw.send_order("손절매도", "5002", self.acc_no, 2, code, quantity, 0, "03", "")
아래와 같이 출력된다.

==================================================
현재 계좌 현황입니다...
* 종목: LG전자, 손익율: 0.21%, 보유수량: 40, 평가금액: 3,152,000원

트레이딩 이력 분석하기

마지막으로 조건검색식과 자동매매 application의 매매성능을 분석하기 위해 모든 트레이딩 이력을 DB에 저장하겠습니다.
먼저 조건검색식으로 검출(편입만..)된 종목과 검출시간을 저장합니다. 그리고 매수/매도시점에 해당 이력또한 DB에 저장합니다.


조건검색식 검출시점과 매수이력을 DB에 저장
def search_condi(self, event_data):
"""키움모듈의 OnReceiveRealCondition 이벤트 수신되면 호출되는 callback함수
이벤트 정보는 event_data 변수로 전달된다.

ex)
event_data = {
"code": code, # "066570"
"event_type": event_type, # "I"(종목편입), "D"(종목이탈)
"condi_name": condi_name, # "스켈핑"
"condi_index": condi_index # "004"
}
:param dict event_data:
:return:
"""

curr_time = datetime.today()
# 실시간 조건검색 이력정보
self.tt_db.real_condi_search.insert({
'date': curr_time,
'code': event_data["code"],
'stock_name': self.stock_dict[event_data["code"]]["stock_name"],
'market': self.stock_dict[event_data["code"]]["market"],
'event': event_data["event_type"],
'condi_name': event_data["condi_name"]
})

if event_data["event_type"] == "I":
if self.stock_account["계좌정보"]["예수금"] < 100000: # 잔고가 10만원 미만이면 매수 안함
return
curr_price = self.kw.get_curr_price(event_data["code"])
quantity = int(100000/curr_price)
self.kw.reg_callback("OnReceiveChejanData", ("조건식매수", "5000"), self.update_account)
self.tt_db.trading_history.insert({
'date': curr_time,
'code': event_data["code"],
'stock_name': self.stock_dict[event_data["code"]]["stock_name"],
'market': self.stock_dict[event_data["code"]]["market"],
'event': event_data["event_type"],
'condi_name': event_data["condi_name"],
'trade': 'buy',
'quantity': quantity,
'hoga_gubun': '시장가',
'account_no': self.acc_no
})
self.kw.send_order("조건식매수", "5000", self.acc_no, 1, event_data["code"], quantity, 0, "03", "")

매도시점에 해당 이력을 DB에 저장
def sell(self):
self.update_account()
curr_time = datetime.today()
print("=" * 50)
print("현재 계좌 현황입니다...")
for data in self.stock_account["종목정보"]:
stock_name, code, quantity = data["종목코드"], data["종목명"], data["보유수량"]
print("* 종목: {}, 손익율: {}%, 보유수량: {}, 평가금액: {}원".format(
data["종목명"], ("%.2f" % data["손익율"]), int(data["보유수량"]), format(int(data["평가금액"]), ',')
))

if data["손익율"] > 3.0 or data["손익율"] < -2.0:
if data["손익율"] > 0:
print("시장가로 물량 전부 익절합니다. ^^ [{}, {}주]".format(stock_name, quantity))
else:
print("시장가로 물량 전부 손절합니다. ㅜㅜ. [{}, {}주]".format(stock_name, quantity))

self.kw.reg_callback("OnReceiveChejanData", ("시장가매도", "5001"), self.update_account)
self.kw.send_order("시장가매도", "5001", self.acc_no, 2, code, quantity, 0, "03", "")
self.tt_db.trading_history.insert({
'date': curr_time,
'code': code,
'stock_name': self.stock_dict[code]["stock_name"],
'market': self.stock_dict[code]["market"],
'event': '',
'condi_name': '',
'trade': 'sell',
'profit': data["손익율"],
'quantity': quantity,
'hoga_gubun': '시장가',
'account_no': self.acc_no
})


이렇게 만들어진 기본적인 자동매매 프로그램을 내일부터 돌려보고 결과를 차트와 함께 분석해보는 시간을 갖도록 하겠습니다...



댓글