PYTHON/AUTO TRADE SYSTEM

[자동 매매 시스템 구축하기] 차트 데이터 조회하기 (1)

  • -

지난 게시글에서 데이터를 요청하기 위해 필요한 SetInputValue와 CommRqData, 그리고 이벤트를 함수와 연결한 후에 해당 함수 내에서 GetCommData를 이용해 데이터를 얻어오는 것까지 코드를 구축했다. 하지만 자동 매매 시스템을 구축하기 위해서는 특정 종목의 매수 가격과 매도 가격을 계산해내기 위해 차트 데이터를 요청하는 절차가 필요하기 마련이다.

따라서 이번 게시글에서는 차트 데이터를 요청하는 방법에 대해 살펴보고, 지난 게시글에서는 다루지 않았던 CommRqData의 세 번째 인자인   nPreNext  에 대해서도 함께 살펴볼 예정이다. 

 

 

차트 데이터 조회 코드를 제작하기 전에 앞서

차트 데이터를 조회한 후에 DB에 저장을 할 것인지 아니면 조회하여 해당 데이터만 사용한 후에 버릴 것인지를 결정하는 것이 중요하다. 왜냐하면 이는 실거래를 위해 차트 데이터만 사용하고 버릴 것인지 아니면 백테스트를 위해 필요한 차트 데이터를 저장할 것인지 등과 밀접하게 관련돼있기 때문이다.

다시 말해, 본인이 구축하고자 하는 알고리즘의 유효성을 확인하고자 한다면 백테스트를 하기 위해 차트 데이터를 저장해주어야 하겠지만, 알고리즘이 이미 충분한 유효성을 갖고 있다고 판단된다면 매번 차트 데이터를 조회한 후에 저장해서 새롭게 불러올 필요가 없는 것이다.(데이터를 곧바로 사용하는 것과 저장한 후에 불러와서 사용하는 것은 자원 관리 측면에서나 시간 관리 측면에서나 아주 큰 차이가 있다.) 

다만 지금 당장으로서는 알고리즘이 없는 분들이 대다수일테니 백테스트를 위해 차트 데이터를 조회한 후에 데이터베이스에 저장하는 방식으로 글을 전개해 나갈 예정이다. 일단 이번 게시글에서는 분봉·일봉 차트 데이터를 조회하는 방법 그리고 해당 데이터를 데이터프레임(DataFrame)을 이용해서 간결하게 정리하는 방법에 대해 알아보고, 마지막으로는 데이터프레임 내의 데이터들에 접근하는 방법에 대해 알아보고자 한다.

 

 

차트 데이터를 조회해보자.

KOA Studio 상에서 차트 데이터를 조회하는 방법은 아래의 사진과 같이 분봉, 일봉, 주봉, 월봉의 네 종류로 나뉘어져 있다. 실제로 분봉 차트 데이터의 경우에는 1분봉, 3분봉, 5분봉, 15분봉 등등 다양한 데이터들이 있는데 세부적인 값들은 "틱범위"라는 값의 인자로 전달해줌으로써 특정 분봉에 해당하는 차트 데이터를 얻어올 수 있다.

 

차트 데이터를 조회하는 데에 있어 입력해줘야 하는 데이터는 모두 다르긴 하나, opt10080을 예로 들자면 "종목코드", "틱범위", "수정주가구분"이라는 세 가지 데이터이다. 그렇다면 삼성전자 종목의 3분봉 수정주가 차트 데이터를 조회해보도록 하자. 이 과정에서 수정주가구분에 입력할 값 역시 KOA Studio 내에서 확인하면 된다.

 

다만 수정주가에 있어서는 "1"을 인자로 전달하는 것을 권하고 싶다. 왜냐하면 실제로 여러 종목들은 거래 정지 등과 같은 이력이 포함되어 있는 경우가 있고, 그에 따라 거래 정지 시에 유지된 가격을 제대로 계산할 수 있어야 하기 때문이다. 예를 들어, 1,200원에서 거래가 정지된 종목이 있다고 가정했을 때, 해당 종목의 차트 데이터를 불러오면 거래 정지 기간 동안은 0원이라는 값이 계산되기 마련이다. 하지만 수정주가구분의 인자로 1을 전달해주게 되면 거래 정지 기간 동안에도 주가는 1,200원으로 출력되게 된다.
※ Line : 45~48

import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
from PyQt5.QAxContainer import *

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.kiwoom.OnReceiveTrData.connect(self.receive_trdata)  ## 데이터 조회 요청 처리 함수

    """데이터 수신 구간"""
    def receive_trdata(self, ScrNo, RqName, TrCode, RecordName, PreNext):
        if RqName == "신고저가요청":
            self.OPT10016(TrCode, RecordName)
        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}")

    """opt10080 : 분봉차트조회요청"""
    def request_opt10080(self):
        self.__setinputvalue("종목코드", "005930")
        self.__setinputvalue("틱범위", "3")
        self.__setinputvalue("수정주가구분", "1")
        self.__commrqdata("분봉차트조회요청", "opt10080", 0, "0600")


    """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])




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

 

 


728x90

 

 

이제 요청하는 코드를 모두 구축했으니, 요청한 데이터의 결과값을 받아와보도록 하자. 데이터를 반환받는 것은 def receive_trdata() 함수 내에서 구현해주면 된다. 반환되는 데이터 역시 현재가, 거래량, 체결시간, 시가, 고가, 저가, 수정주가구분, 수정비율, 대업종구분, 소업종구분, 종목정보, 수정주가이벤트, 전일종가의 총 13개 데이터인데, 이 모든 걸 다 가져올 필요는 없기 때문에 체결시간, 현재가, 시가, 고가 ,저가, 거래량, 전일종가 다섯 개의 데이터만 조회해보도록 하자. 
※ Line : 29~30, 49~59

import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
from PyQt5.QAxContainer import *

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.kiwoom.OnReceiveTrData.connect(self.receive_trdata)  ## 데이터 조회 요청 처리 함수


    """데이터 수신 구간"""
    def receive_trdata(self, ScrNo, RqName, TrCode, RecordName, PreNext):
        if RqName == "신고저가요청":
            self.OPT10016(TrCode, RecordName)
        if RqName == "분봉차트조회요청":
            self.opt10080(TrCode, RecordName)
        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}")


    """opt10080 : 분봉차트조회요청"""
    def request_opt10080(self):
        self.__setinputvalue("종목코드", "005930")
        self.__setinputvalue("틱범위", "3")
        self.__setinputvalue("수정주가구분", "1")
        self.__commrqdata("분봉차트조회요청", "opt10080", 0, "0600")



    """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])




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

 

 

이제 데이터를 요청하는 함수인 request_opt10080 함수를 실행하기 위한 버튼을 GUI 파일 상에서 구현해준 후에, 다시 def __init__(self) 아래에서 해당 버튼과 def reuqest_opt10080을 연결해주도록 하자.GUI 파일 내에서 "분봉차트조회"라는 이름의 버튼을 생성해주었고, 해당 버튼의 객체 이름은 "pushButton_2"이다. 
※ Line : 20

import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
from PyQt5.QAxContainer import *

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)  ## 데이터 조회 요청 처리 함수

    """데이터 수신 구간"""
    def receive_trdata(self, ScrNo, RqName, TrCode, RecordName, PreNext):
        if RqName == "신고저가요청":
            self.OPT10016(TrCode, RecordName)
        if RqName == "분봉차트조회요청":
            self.opt10080(TrCode, RecordName)
        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}")


    """opt10080 : 분봉차트조회요청"""
    def request_opt10080(self):
        self.__setinputvalue("종목코드", "005930")
        self.__setinputvalue("틱범위", "3")
        self.__setinputvalue("수정주가구분", "1")
        self.__commrqdata("분봉차트조회요청", "opt10080", 0, "0600")



    """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])




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

 

 

이제 해당 코드를 실행한 후에 pushButton_2 버튼을 클릭하게 되면 아래와 같은 결과값을 얻어올 수 있다. 분봉 차트 데이터는 회당 900건의 데이터만 조회가 가능한데, 3분봉 차트를 기준으로 한다면 하루 130개 데이터와 더불어 종가 데이터 한 개(15:33분)가 추가되기 때문에 하루가 131개의 데이터를 갖게 되고, 900개의 데이터를 조회할 경우 6.87일 간의 3분봉 데이터를 조회하게 되는 것이다. 

[20220704144800] NOW:+57100, OPEN:+57000, HIGH:+57100, LOW:+57000, YCLOSE:
[20220704144500] NOW:+57000, OPEN:+57000, HIGH:+57100, LOW:+56900, YCLOSE:
[20220704144200] NOW:+57000, OPEN:+56900, HIGH:+57000, LOW:+56900, YCLOSE:
[20220704143900] NOW:+57000, OPEN:+57000, HIGH:+57000, LOW:+56900, YCLOSE:
                                     :
                                   (중략)
                                     :
[20220623150900] NOW:-57500, OPEN:-57400, HIGH:-57500, LOW:-57300, YCLOSE:
[20220623150600] NOW:-57400, OPEN:-57400, HIGH:-57500, LOW:-57300, YCLOSE:
[20220623150300] NOW:-57400, OPEN:-57400, HIGH:-57500, LOW:-57300, YCLOSE:
[20220623150000] NOW:-57300, OPEN:-57500, HIGH:-57500, LOW:-57300, YCLOSE:

 

 

하지만 암만 3분봉 데이터래도 거래일 기준 7일도 되지 않는 데이터를 조회해서 백테스트 등의 절차에 사용한다는 것은 터무니없이 부족한 데이터 양이다. 다음 게시글에서는 연속조회를 구축하여 전체 데이터를 조회하는 방법과 self 변수를 통해 함수 간 이동 시에도 데이터를 계속해서 축적시킨 후 결과값을 확인하는 방법에 대해서도 알아볼 예정이다.

 

 


728x90
반응형
Contents

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

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