PYTHON/AUTO TRADE SYSTEM

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

  • -

지난 게시글에서 로그인이 정상적으로 처리되었다고 판단할 수 있는 기준을 제작했고, 이번 게시글에서는 해당 기준이 충족된 경우 자동 매매 코드를 실행하도록 하는 코드를 구축할 것이다.

 

 

쓰레드를 사용해야 한다.

쓰레드란 구체적으로 어떤 개념인지 이해하지는 못해도 되지만, 깊게 파고들면 들수록 복잡한 개념이 바로 쓰레드이다. 다시 말해, 제대로 알지 못한 채로 사용하게 되면 프로그램 상 오류가 발생할 수도 있다는 것이다.

일례로, 앞서 살펴봤던 이벤트루프의 경우 이벤트루프가 시작됐으나 종료시키지 못하는 바람에 무한 루프에 걸리게 되는 문제점이 발생할 수 있다고 했다. 쓰레드 역시 특정 쓰레드가 모두 완료될 때까지 다른 쓰레드의 실행 및 개입이 이루어지지 않도록 하는 쓰레드락(Thread-Lock)이라는 개념이 있으며, 무한루프라는 개념이 쓰레드 하에서는 데드 쓰레드(Dead-Thread)라고 불린다.

여기서 쓰레드를 사용하는 가장 궁극적인 이유를 살펴보자면 첫째, GUI 파일과 코드를 연결해서 몇 가지 기능을 구현해봤다면 알 수 있겠지만 하나의 버튼을 눌렀을 때 다른 버튼이 눌리지 않게 되고, 설상가상으로 눌렀다 하면 프로그램이 하얘지면서 GUI에서는 "응답 없음" 신호를 보내주기 때문이다. 둘째, 자동 매매 시스템은 근본적으로 While문을 통해 시간 데이터를 지속적으로 갱신해야 하며, 지속적으로 갱신하는(계속해서 작동 중인) 함수가 있기 때문에, 이 부분으로 인해 첫 번째 이유로 귀결된다.

 

 

사용하는 건 어렵지 않다.

threading 모듈에서 제공하는 Thread를 사용하면 되며, 쓰레드로 동작시킬 함수 역시 그냥 target=이라는 파라미터의 인자로 전달해주면 되기 때문이다. 사용 예시는 아래와 같다. 

from threading import Thread

target_thread = Thread(target=self.auto_trade)
target_thread.start()

def auto_trade():
    while:
        ~~~~

 

이 개념을 바탕으로 자동 매매를 실행할 함수와 로그인이 정상적으로 처리됐을 때 자동 매매 함수를 실행할 코드를 제작해보면 아래와 같이 제작할 수 있을 것이다.
※ Line : 9, 33~35, 46~47

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


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 _auto_trade(self):
        pass


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

        if account_cnt != "0" and key_b == "0":
            auto_trade = Thread(target=self._auto_trade)
            auto_trade.start()



    """데이터 수신 구간"""
    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)

        try:
            self.event_loop.exit()
        except AttributeError:
            pass


    """데이터 처리 구간"""
    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("틱범위", "30")
        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("틱범위", "30")
            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)
        chart_data.to_sql(name="s005930", con=manage_db.engine_3min, index=False, if_exists='replace')
        print("차트데이터 저장이 완료되었습니다.")


    """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_()

 

 

여기까지 코드를 구축했다면, 사실 자동 매매 프로그램이 정상적으로 동작하는지 안 하는지 확인할 게 필요하지 않겠는가? _auto_trade 함수의 내용을 조금 수정해주도록 하자.

3번째 줄에 있는 while 1:은 단순하게 말하자면 계속해서 반복하라는 의미이다. 다시 말해, break와 같은 별도의 신호가 있기 전까지는 while문 안에 있는 코드를 계~~~~속해서 평~~생 동안 반복해주라는 의미이다.  그 후 4번째 줄에서는 코드가 정상 동작 중이라는 문구를 출력하도록 하고, 해당 문구를 출력한 후 5번째 줄과 같이 1초 간 동작을 멈추라는 것이다. 따라서, 이 코드를 실행하게 되면 별다른 동작이 없더라도 1초마다 "정상 동작 중입니다."라는 문구가 출력되어야 한다.
※ Line : 3~5

    """자동 매매 구간"""
    def _auto_trade(self):
        while 1:
            print("정상 동작 중입니다.")
            time.sleep(1)

 

직접 실행해보면 아래와 같이 1초마다 "정상 동작 중입니다."라는 문구가 출력된다는 것을 확인할 수 있으며, GUI 파일이 이전과는 다르게 어떤 코드가 수행 중일 때 화면이 멈추는 현상이 발생하지 않고 있다는 것 역시 확인할 수 있다. 

pydev 디버거에 연결되었습니다(빌드 221.5080.212)
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.
정상 동작 중입니다.

종료 코드 -1(으)로 완료된 프로세스

 

 

 


728x90

 

 

자동 매매 시스템을 어떻게 구현해?

이는 시간 개념을 통해 구현할 것이다. 다시 말해, 특정 시간에 특정 동작을 수행하도록 하는 코드를 구현하는 것이다. 예를 들어 장이 9시에 개장되니 9시 이전에는 계좌 잔고를 조회한다거나, DB 상 보유 종목 데이터가 실제 보유 종목 데이터와 일치하는지를 확인한다거나, 매수가 매도가를 재계산한다거나 하는 모든 거래 준비가 완료되어야 한다는 것이다. 그렇다면 시간 데이터는 어떻게 얻어올 수 있을까? 이전에 import했던 PyQt5의 QtCore 안에 있는 QTime() 메서드를 사용하면 된다.

  QtCore.QTime.currentTime()  을 통해 현재 시간 데이터를 얻어올 수 있으며,   toString()  을 통해 시간 데이터를 우리가 원하는 형식으로 변경해줄 수 있다. 만약 현재 시간이 오후 3시인 경우,   toString('hh:mm:ss'를 사용하게 되면 현재 시간 데이터는 "15:00:00"으로 출력되며, 오전 9시인 경우   toString('hh:mm:ss'를 사용하게 되면 "09:00:00"으로 출력된다. 만약   toString('hh:mm') 을 사용하게 되면 오전 9시는 어떻게 표시될까? "09:00"으로 표시된다.

now_time = QTime.currentTime().toString('hh:mm:ss')
## 또는
now_time = QTime.currentTime().toString('hh:mm')

 

그렇다면 이 개념을 기반으로 해서 def _auto_trade 함수를 다음과 같이 수정해보도록 하자. 
※ Line : 4

    """자동 매매 구간"""
    def _auto_trade(self):
        while 1:
            now_time = QtCore.QTime.currentTime().toString('hh:mm:ss')
            print(f"[{now_time}] 정상 동작 중입니다.")
            time.sleep(1)

 

그 후 코드를 실행시키면 아래와 같은 결과물이 출력될 것이다.

pydev 디버거에 연결되었습니다(빌드 221.5080.212)
[14:34:21] 정상 동작 중입니다.
[14:34:22] 정상 동작 중입니다.
[14:34:23] 정상 동작 중입니다.
[14:34:24] 정상 동작 중입니다.
[14:34:25] 정상 동작 중입니다.
[14:34:26] 정상 동작 중입니다.
[14:34:27] 정상 동작 중입니다.
[14:34:28] 정상 동작 중입니다.
[14:34:29] 정상 동작 중입니다.
[14:34:30] 정상 동작 중입니다.
              :
         (이하 생략)

 

 

사실 여기서, time.sleep() 안에 동작을 멈출 시간을 1초로 설정해서 그렇지, 실제로는 1초까지 짧은 시간이 필요하지는 않다. 대략적으로 60초, 다시 말해 1분 간격으로 실행하는 것을 권장한다. 예를 들어, 코드를 아래와 같은 코드를 구축했을 경우 오류가 발생할 수 있기 때문이다.

    """자동 매매 구간"""
    def _auto_trade(self):
        while 1:
            now_time = QtCore.QTime.currentTime().toString('hh:mm:ss')
            print(f"[{now_time}] 정상 동작 중입니다.")
            
            if now_time == "08:55":
                self.계좌잔고조회 함수
            
            time.sleep(1)

 

이제 8시 55분이 됐을 때면 if now_time == "08:55":가 몇 번 충족될지 생각해보자. 오전 8시 55분 0초, 8시 55분 1초, 8시 55분 2초..... 8시 55분 59초까지 총 60번을 충족시키게 된다. 따라서 매 초마다 self.계좌잔고조회 함수를 실행하게 되며, 1초마다 해당 함수를 실행하기 때문에 프로그램은 멈추게 될 것이다.(구체적으로 말하자면, 아직 쓰레드 간 데이터 간섭이 이루어지는 현상이 발생하게 된다.)

따라서 조금은 답답하더라도 time.sleep()을 60초로 설정해서 프로그램을 몇 초에 작동시켰든 간에 매 분마다 한 번씩만 now_time 변수가 업데이트되도록 하는 방법이 있고, 아니면 아예 if now_time == "":의 시간을 "08:55"가 아닌 "08:55:30" 등과 같이 초 단위까지 하나하나 세밀하게 설정해주는 방법이 있을 것이다.

전자의 방법은 now_time 데이터가 아래와 같이 갱신될 것이다. 1초에 실행시켰다면 1초라는 기준만 동일하게 유지된 채로 분 개념만 변경될 것이다.

## 매 분 갱신
pydev 디버거에 연결되었습니다(빌드 221.5080.212)
[14:30:01] 정상 동작 중입니다.
[14:31:01] 정상 동작 중입니다.
[14:32:01] 정상 동작 중입니다.
[14:33:01] 정상 동작 중입니다.
[14:34:01] 정상 동작 중입니다.
              :
         (이하 생략)

 

여기서 toString("hh:mm")은 24시간 개념을 기준으로 하기 때문에 오후 3시를 사용한다면   if now_time == "15:00"을, 오전 10시에 동작하길 원한다면   if now_time == "10:00"을, 오전 9시 37분에 동작하길 원한다면   if now_time == "09:37"을 사용하면 된다.

만약 장이 마감된 직후인 오후 3시 31분에 삼성전자의 3분봉 데이터를 조회하고 싶다면, 아래와 같이 구축하면 된다.
※ Line : 7~10

    """자동 매매 구간"""
    def _auto_trade(self):
        while 1:
            now_time = QtCore.QTime.currentTime().toString('hh:mm:ss')
            print(f"[{now_time}] 정상 동작 중입니다.")
            
            if now_time == "08:55":
                self.request_opt10080()
            
            time.sleep(60)

 

 


728x90
반응형
Contents

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

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