PYTHON/AUTO TRADE SYSTEM

[자동 매매 시스템 구축하기] 특정 시간에 특정 작업 수행하기 (1)

  • -

지난 게시글에서 차트 데이터를 요청하고 결과 데이터를 받아온 후에 해당 데이터를 데이터프레임화를 시키는 부분까지 코드를 구축하였다. 사실 이번 콘텐츠에서 궁극적으로 제작하고자 하는 자동 매매 시스템을 사용하기 위해서는 반드시 특정 시점에 특정 작업을 수행하도록 하는 코드가 구축되어 있어야 한다. 이름부터 자동 매매 시스템인데 뭐 하나하나 눌러줘야 한다면 자동 매매 시스템이라는 이름이 얼마나 구색하겠는가?

예를 들어, 아침 8시 50분이 되면 계좌 잔고를 조회한 후에 당일 거래할 종목을 실시간 등록을 한다거나 하는 등의 작업을 사전에 시간 데이터를 기반으로 해서 동작하게끔 구현해두어야 프로그램이 실행되는 동안에 특정 시점이 됐을 때 해당 작업을 자동적으로 실시하게 된다.

 

 

로그인이 완료된 시점에서 자동 매매를 시작하도록 해보자.

일단 현재 시점까지는 차트 데이터를 조회하는 부분만 구현되어 있기 때문에 프로그램 실행 시 로그인이 완료되는 동시에 특정 작업을 수행하도록 하는 코드를 구축할 예정이다. 그러기 위해서는 로그인이 완료되는 시점을 확인할 수 있어야 하는데, 키움증권 Open API에서는 로그인 처리와 관련하여 처리 결과 정보를 전달해주는 기능을 제공하고 있다. 키움증권 개발가이드 14p를 확인해보자.

키움증권 Open API 서비스에서는 총 8개의 이벤트를 발생시키고 있으며, 그 중 로그인과 관련해서는   OnEventConnect  라는 이벤트를 발생시키고 있음을 확인할 수 있다. 1번 줄에는   OnReceiveTrData  이벤트를 소개하고 있는데, 해당 이벤트는 우리가 차트 데이터를 요청하고 결과 데이터를 조회할 때 사용했던 이벤트이다. 

 

 

OnEventConnect 이벤트는 언제 발생할까? 

위 사진의 설명 란을 보면 알 수 있듯이, "통신 연결 상태 변경시"에 발생하는 이벤트인 만큼 로그인이 진행 중일 때 발생된다. 다시 말해, 로그인을 요청해서 통신을 시도하는 과정과 로그인이 된 상태에서 갑자기 연결이 끊기면서 통신 상태가 변경될 때 발생한다는 것이다.

따라서 우리는   OnEventConnect  이벤트가 발생했을 때 로그인이 정상적으로 이루어졌는지 등과 같은 여러 데이터들을 함께 처리해주고, 해당 코드 내에서 모든 통신이 정상적으로 처리된 경우에만 자동 매매 시스템을 시작하도록 할 수 있다. 

 

 

로그인과 관련하여 사용할 수 있는 함수는 무엇이 있을까?

앞서 로그인을 실행할 때에는   CommConnect   함수를 이용해서 로그인을 시도했는데, 로그인을 시도했다면 그 결과와 관련된 데이터를 전달해주는 함수도 있을 것이다. 바로   GetLoginInfo  이다. 

위의 사진 사엥서 확인할 수 있듯이,   GetLoginInfo   함수는 태그(Tag)값에 따라 각기 다른 반환값들을 전달해준다는 점을 확인할 수 있다. 즉, 전체 계좌 개수를 반환받고 싶다면   GetLoginInfo("ACCOUNT_CNT")  를 사용하면 되고, 전체 계좌 목록을 반환받고 싶다면   GetLoginInfo("ACCNO")  를 사용하면 된다. 이 때 해당 데이터 요청으로 인한 결과값을 곧바로 반환받기 때문에, 우리는 이   GetLoginInfo  함수를 여타   SetInputValue   함수나   CommRqData   함수와 같이 캡슐 형태로 제작해준 후에,   return  을 사용해서 결과값을 곧바로 사용할 수 있다.

    """로그인 정보 표시"""
    def __getlogininfo(self, tag):
        """
        ① "ACCOUNT_CNT" : 전체 계좌 개수
        ② "ACCNO" : 전체 계좌 리스트 반환(구분자는 ;)
        ③ "USER_ID" : 사용자의 ID 반환
        ④ "USER_NAME" : 사용자명 반환
        ⑤ "KEY_BSECGB" : 키보드 보안 해지 여부 (0:정상, 1:해지)
        ⑥ "FIREW_SECGB" : 방화벽 설정 여부 (0:미설정, 1:설정, 2:해지)
        """
        return self.kiwoom.dynamicCall("GetLoginInfo(QString)", [tag])

 

 

OnEventConnect가 발생했을 때 실행할 함수 제작하기 : def process_login

이벤트는 기본적으로, 특정 이벤트가 발생했을 때 해당 이벤트를 기준으로 특정 동작을 수행하고 싶다면 이벤트와 특정 함수를 연결해주면 되고 쓰고 싶은 데이터가 없는 이벤트라면 굳이 해당 이벤트와 특정 함수를 연결해주지 않아도 된다. (전혀 필요가 없는 이벤트라면, 어떠한 코드도 구축해주지 않아도 문제는 발생하지 않는다. 우리가 여태 OnReceiveTrData 이벤트만 처리하고 나머지 7개의 이벤트를 처리하지 않았음에도 불구하고 아무런 문제도 발생하지 않았다.)
※ Line : 28, 31~32, 140~150

import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
from PyQt5.QAxContainer import *
from PyQt5.QtCore import *
import pandas as pd
import time


form_class = uic.loadUiType("main.ui")[0]


class tradesystem(QMainWindow, form_class):
    def __init__(self):
        super().__init__()
        self.setupUi(self)  ## GUI 켜기
        self.setWindowTitle("주식 프로그램")  ## 프로그램 화면 이름 설정

        self.kiwoom = QAxWidget("KHOPENAPI.KHOpenAPICtrl.1")  ## OpenAPI 시작
        self.kiwoom.dynamicCall("CommConnect()")  ## 로그인 요청

        """데이터 요청 구간"""
        self.pushButton.clicked.connect(self.request_opt10016)
        self.pushButton_2.clicked.connect(self.request_opt10080)

        """이벤트 처리 구간"""
        self.kiwoom.OnReceiveTrData.connect(self.receive_trdata)  ## 데이터 조회 요청 처리 함수
        self.kiwoom.OnEventConnect.connect(self.process_login)    ## 로그인 반환값 처리 함수

    """로그인 처리 구간"""
    def process_login(self):
        pass


    """데이터 수신 구간"""
    def receive_trdata(self, ScrNo, RqName, TrCode, RecordName, PreNext):
        print(f"RQNAME:{RqName}, TRCODE:{TrCode}, RECORDNAME:{RecordName}, PRENEXT:{PreNext}")

        if PreNext == "2":
            self.remained_data = True
        elif PreNext != "2":
            self.remained_data = False

        if RqName == "신고저가요청":
            self.OPT10016(TrCode, RecordName)
        if RqName == "분봉차트조회요청":
            self.opt10080(TrCode, RecordName)

        self.event_loop.exit()



    """데이터 처리 구간"""
    def OPT10016(self, TrCode, RecordName):
        """
        data_len : 데이터의 개수를 구함
        for문 : 데이터의 개수를 하나씩 돌면서 index의 인자로 전달
        """
        data_len = self.__getrepeatcnt(TrCode, RecordName)

        for index in range(data_len):
            item_code = self.__getcommdata(TrCode, RecordName, index, "종목코드").strip(" ")
            item_name = self.__getcommdata(TrCode, RecordName, index, "종목명").strip(" ")
            now = self.__getcommdata(TrCode, RecordName, index, "현재가").strip(" ")

            print(f"[{item_code}]:{item_name}. 현재가:{now}")

    """데이터 처리 구간"""
    def opt10080(self, TrCode, RecordName):
        data_len = self.__getrepeatcnt(TrCode, RecordName)

        for index in range(data_len):
            time = self.__getcommdata(TrCode, RecordName, index, "체결시간").strip(" ")
            now = self.__getcommdata(TrCode, RecordName, index, "현재가").strip(" ")
            open = self.__getcommdata(TrCode, RecordName, index, "시가").strip(" ")
            high = self.__getcommdata(TrCode, RecordName, index, "고가").strip(" ")
            low = self.__getcommdata(TrCode, RecordName, index, "저가").strip(" ")
            yclose = self.__getcommdata(TrCode, RecordName, index, "전일종가").strip(" ")
            # print(f"[{time}] NOW:{now}, OPEN:{open}, HIGH:{high}, LOW:{low}, YCLOSE:{yclose}")

            self.chart_data['time'].append(time)
            self.chart_data['now'].append(now)
            self.chart_data['open'].append(open)
            self.chart_data['high'].append(high)
            self.chart_data['low'].append(low)


    """opt10080 : 분봉차트조회요청"""
    def request_opt10080(self):
        self.chart_data = {'time':[], 'now':[], 'open':[], 'high':[], 'low':[]}
        self.__setinputvalue("종목코드", "005930")
        self.__setinputvalue("틱범위", "60")
        self.__setinputvalue("수정주가구분", "1")
        self.__commrqdata("분봉차트조회요청", "opt10080", 0, "0600")
        self.event_loop = QEventLoop()
        self.event_loop.exec_()
        time.sleep(0.5)

        while self.remained_data == True:
            self.__setinputvalue("종목코드", "005930")
            self.__setinputvalue("틱범위", "60")
            self.__setinputvalue("수정주가구분", "1")
            self.__commrqdata("분봉차트조회요청", "opt10080", 2, "0600")
            self.event_loop = QEventLoop()
            self.event_loop.exec_()
            time.sleep(0.5)

        chart_data = pd.DataFrame(self.chart_data, columns=['time', 'now', 'open', 'high', 'low'])
        print(chart_data)


    """OPT10016 : 신고저가요청"""
    def request_opt10016(self):
        self.__setinputvalue("시장구분", "000")
        self.__setinputvalue("신고저구분", "1")
        self.__setinputvalue("고저종구분", "1")
        self.__setinputvalue("종목조건", "0")
        self.__setinputvalue("거래량구분", "00000")
        self.__setinputvalue("신용조건", "0")
        self.__setinputvalue("상하한포함", "1")
        self.__setinputvalue("기간", "60")
        self.__commrqdata("신고저가요청", "OPT10016", 0, "0161")

    """데이터 입력 구간"""
    def __setinputvalue(self, item, value):
        self.kiwoom.dynamicCall("SetInputValue(QString, QString)", [item, value])

    """데이터 요청 구간"""
    def __commrqdata(self, rqname, trcode, pre, scrno):
        self.kiwoom.dynamicCall("CommRqData(QString, QString, int, QString)", [rqname, trcode, pre, scrno])

    """데이터 수신 구간"""
    def __getcommdata(self, trcode, recordname, index, itemname):
        return self.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", [trcode, recordname, index, itemname])

    """데이터 개수 반환"""
    def __getrepeatcnt(self, trcode, recordname):
        return self.kiwoom.dynamicCall("GetRepeatCnt(QString, QString)", [trcode, recordname])

    """로그인 정보 표시"""
    def __getlogininfo(self, tag):
        """
        ① "ACCOUNT_CNT" : 전체 계좌 개수
        ② "ACCNO" : 전체 계좌 리스트 반환(구분자는 ;)
        ③ "USER_ID" : 사용자의 ID 반환
        ④ "USER_NAME" : 사용자명 반환
        ⑤ "KEY_BSECGB" : 키보드 보안 해지 여부 (0:정상, 1:해지)
        ⑥ "FIREW_SECGB" : 방화벽 설정 여부 (0:미설정, 1:설정, 2:해지)
        """
        return self.kiwoom.dynamicCall("GetLoginInfo(QString)", [tag])



if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = tradesystem()
    myWindow.show()
    app.exec_()

 

 

이제   OnEventConnect   이벤트가 발생했을 때   def process_login   함수를 실행하도록 했으니,   def process_login   함수 내에서는 사전에 140번째 Line에 제작해둔   def __getlogininfo()  함수를 사용해서 우리가 사용하고자 하는 데이터들을 얻어오면 된다. 일단은   ACCOUNT_CNT와 ACCNO, KEY_BSECGB, FIREW_SECGB   네 개에 대한 정보만 얻어오도록 하자. 
※ 개별 함수만 수정(Line : 3~6)

    """로그인 처리 구간"""
    def process_login(self):
        account_cnt = self.__getlogininfo("ACCOUNT_CNT")
        accno = self.__getlogininfo("ACCNO")
        key_b = self.__getlogininfo("KEY_BSECGB")
        firew = self.__getlogininfo("FIRWE_SECGB")

 

이제 각각의 데이터가 각각의 변수에 입력되었다. 그렇다면 우리는 이 값들이 어떤 값일 때 로그인이 정상적으로 이루어졌다고 판단할 수 있을까? 기본적으로 account_cnt는 0이 아니어야 할 것이다. 계좌의 개수가 한 개는 있어야 매매를 진행할 수 있기 때문이다. 다음으로 accno는 개수를 의미하는 것이 아니기 때문에 중요하지 않게 느껴질 수 있지만 자동 매매 시스템 상 주문을 넣을 때에는 계좌번호가 필요하기 때문에 사용했다. 다음으로 key_b는 키보드보안을 의미하므로 정상을 의미하는 0이어야 하고, 방화벽 여부를 나타내는 firew는 개별적으로 설정할 수 있기 때문에 크게 중요하지 않다. 

이제 필요한 값들을 단순하게 정리해보자면,   account_cnt != "0", key_b == "0"인 경우  에 로그인이 정상적으로 이루어졌다고 볼 수 있는 것이다.

[주의] 키움증권 함수를 통해 반환되는 값들은 모두 str, 문자열 형태로 이루어져 있다. 다시 말해, account_cnt != 0을 사용하게 되면 제대로 동작하지 않고, account_cnt != "0"을 사용해야 정상적으로 동작한다. account_cnt 뿐만 아니라 모든 반환값들이 문자열 형태로 반환된다. 숫자형으로 사용하고 싶다면 int()형으로 개별적으로 형을 변환시켜주어야 한다.
※ 개별 함수만 수정(Line : 8, 9)

    """로그인 처리 구간"""
    def process_login(self):
        account_cnt = self.__getlogininfo("ACCOUNT_CNT")
        accno = self.__getlogininfo("ACCNO")
        key_b = self.__getlogininfo("KEY_BSECGB")
        firew = self.__getlogininfo("FIRWE_SECGB")

        if account_cnt != "0" and key_b == "0":
            print("로그인 성공")

 

이제 다음 게시글에서 해당 요건이 충족됐을 때 자동적으로 자동 매매 코드를 동작하도록 하는 방법에 대해 살펴보도록 하자.

 

 


728x90
반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.