본문 바로가기
카테고리 없음

키움 open api 이용해서 자동매매 구현(Basic)

by S트레이더 2018. 7. 18.
반응형

오늘은 키움증권의 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
})


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



반응형