2. 해외선물/2-3. 해외선물 설명

(키움증권 해외선물 OpenAPI-W) 1분봉 데이터로 3분봉, 60분봉 만들기 (4) 3분/60봉 데이터 만들기 (고급)

봄이오네 2023. 11. 2. 08:04
반응형

 

목 차
1. 들어가며
2. 사전설명 : 조회할 수 있는 분봉 종류
3. 코드설명
4. 전체코드
5. 마치며

 

1. 들어가며

지난 글에서는 리스트와 for문을 활용하여 1분봉을 3분봉으로 바꾸어 보았다. 리스트 및 for문의 혼용문은 잘 활용하면 코드 길이를 상당히 줄일 수 있을 것 같다. 당분간 조금더 집중해서 공부해보고 싶다.
 
이번 글에서는 1분봉 데이터를 활용하여 3분봉과 60분봉 데이터를 만들어볼 것이다. 이 글을 끝으로 "1분봉 활용하여 3분봉, 60분봉 만들기"에 대한 설명은 마무리 짓는다.
 


2. 사전설명

조회할 수 있는 분봉 종류는 아래와 같다.
< 그림1 >은 영웅문G에서 확인할 수 있는 분봉의 종류(①)이다. 물론 일봉, 월봉, 연봉도 조회할 수 있지만, 여기서는 분봉 1분, 3분, 5분, 10분, 15분, 30분, 45분, 60분봉에 포커스를 맞추자.
 
여기서 살짝 중/고등학교 때 배웠던 약수(約數)가 기억나는가? 1,3,5,...,60의 조합을 보았을 때 어떤게 생각이 나는지? 필자는 60의 약수를 생각하였다.

  • 60의 약수 = {1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60}

갑자기 왠 약수를 말하는지? 모수가 60의 약수이면 조회를 할 수 있다. 1회 조회시 키움측에서 제공하는 데이터의 개수는 600개(60 x 10)이다. 즉 우리가 활용할 데이터는 60의 공배수(公倍數)를 활용하면 된다. 약수나 공배수가 생각났다면, 역시 해선인들은 "매매" 빼고는 다 잘하는구나... 이런 생각이 든다. ^^
 
전체 모수는 많을수록 좋겠지만, commrqdata로 한번 조회시 600개를 제공해준다. 60분봉으로는 10개이다. 나중에 설명하겠지만, 600개(60 x 10개), 540개(60 x 9개)는 활용하지 못한다. ㅠㅠ
 
결론은 키움측에서 제공해주는 1분봉 600개 중 현재시간 기준으로 480개(60 x 8개)만 활용할 것이다. 60분봉은 8개, 10분봉은 48개, 3분봉은 160개 정도를 얻을 수 있다.
 
필자는 여기서 3분봉과 60분봉 2개를 설명할 것이다. 다른 분봉은 사용자가 숫자를 바꾸어서 출력해주면 된다.

그림1. 영웅문G에서 확인가능한 미니 나스닥(NQ23)의 3분봉 화면

 


3. 코드설명

라이브러리 및 로그인 관련 내용은 설명을 생략한다.

그림2-1. 라이브러리 및 로그인에 관한 설명은 생략한다.

 
1줄~25줄 : 활용할 라이브러리 및 로그인에 관한 설명은 생략한다.
28줄~33줄 : 90줄의 명령어가 데이터를 담아서 전달해주면, 실행되는 함수를 전달한다.
 

그림2-2. 1분봉 데이터를 활용하여 3분봉 데이터로 만든다

 
36줄~41줄 : 28줄~33줄에서 키움증권에 데이터 입력/요청하면, 36줄~41줄에서 데이터를 수신받는다.
42줄 : 45줄~84줄의 함수(1분봉 → 3분봉으로 변환하는 함수)를 실행한다.
45줄 : 현재 시간의 "체결시간"을 변수 current_time_0에 담는다.
51줄 : 현재시간의 분(minute)을 추출(10:12)해 낸다.
53줄~56줄 : 1분봉의 시가, 고가, 저가, 종가를 담을 리스트를 선언한다.
 
58줄~59줄 : 이 글의 핵심이다.
58줄 : 1분봉 600개 중 480개(60의 공배수)를 활용한다.
59줄 : 3분봉을 설정한다. 여기서 사용자는 숫자를 변경할 수 있다. 59줄에 3을 넣으면 3분봉이 만들어지고, 10을 넣으면 10분봉이 만들어진다.
 

그림2-3. 1분봉을 3분봉으로 만드는 함수를 정의하였다.

 
여기서부터는 자세히 설명하겠다.
62줄 : self.bong_what_minute_sort는 59줄에서 정의한 3이 들어간다. 62줄을 다시 작성해보면, for i in range(0,3)이 될 것이다. 

※ 여기서 잠깐! 62줄에서 range의 끝수를 3으로 설정한 이유는 무엇인가?
우리는 지금까지 몫(quotient)와 나머지(remainder)를 알아보았다. 나머지를 통해 적용될 코드를 만들어주는 것을 기억할 것이다.

1분봉이 3분봉으로 전환될 때 적용될 코드가 바뀌는 "경우의 수"는 총 3가지이다. 1분봉의 "체결시간"을 3(분봉)으로 나누었을 때, 나머지가 0, 1, 2인 경우이다.

지금은 3분봉이기 때문에, 경우가 수가 3가지이다. 만약 10분봉이면 어떨까? 경우의 수는 10가지가 발생한다. 10가지 달리 적용되는 코드를 일일히 작성하는 일은 없다. 차분하게 아래의 설명(아래의 62줄~64줄의 접은글)을 읽어보자.

 
63줄 : 51줄에서 현재의 "체결시간"의 분(minute)을 59줄(지금은 3)으로 나눈 숫자가 62줄에서 선언한 나머지(i)와 같다면,
64줄 : (480개 모수 + 나머지) ~ (나머지) 시간까지 64줄 이하의 작업을 하라. @.@ 아래 접은글에서 부연하겠다.
 

더보기

※ 내용이 상당히 난해하게 느껴질 수 있다. 천천히 설명을 해볼 생각이다.

현재시간이 05:59분이라고 하자. 62줄~64줄까지 코드를 간단하게 써보면,

62줄 : for i in range(0, 3):

63줄 :         if 59 % 3 == i

64줄 :                 for j in range(480+i, i, -3)

 

빨간색 3은 3분봉이기 때문에 들어가는 숫자이다.

63줄 : 59 % 3은 59를 3으로 나눈 나머지(remainder)를 구하라는 뜻이다. 59/3의 나머지는 2이다. 즉 i가 2일때 64줄 이하의 명령어들이 실행된다.

64줄 : 63줄에서 i가 2로 결정되었으므로 64줄은 for j in range(482, 2, -3)이 된다.

 

※ 정리를 해보면,

62줄은 어떤 나머지(remainder)가 적용되는지 for문을 돌리는 것이며

63줄은 나머지를 구하는 것이다.

64줄은 63줄에서 구한 나머지를 i에 넣어주어서, (480+2분)과 (2분) 사이의 3분봉을 구하는 것이다.

 

위에서 설명한 10분봉으로 조회한 경우, 나머지가 10가지가 나오므로, 10가지 경우의 수가 존재한다고 했지만, 62줄~64줄을 통해 10가지의 경우의 수를 처리할 수 있다.

 

 

 
65줄 : 이 글의 핵심이다. 여기에서 2시간 정도는 헤맸다. ㅠㅠ
64줄에서 조회할 범위(482분전 ~ 2분전)을 정했다. 65줄에서는 몫(equtient)을 구한다. 이유는?
3분봉일 때를 생각해보자. 사용자가 영웅문G에서 03:47분의 3분봉을 조회한 경우는 03:45~03:47분의 데이터를 조합할 것이다. 03:45분의 1분봉 시가가 3분봉의 시가가 되며, 03:47분의 1분봉 종가가 3분봉 종가가 된다.
03:45~03:47분의 각각의 고가/저가를 리스트에 넣고, 최고값/최저값을 찾으면, 각각 3분봉의 고가/종가가 된다.
 
말이 길어졌지만, 65줄은 (3의배수 시간) ~ (3의배수시간 + 2분) 시간의 각각의 분(minute)의 몫(quotient)이 64줄의 (480+i-i)/3과 같다면, 즉 주어진 체결시간의 분(minute)이 3의 배수이면
 
66줄 : 3분봉 시가는 3의 배수에 해당하는 시가이며
67줄~68줄 : (3의배수 분) ~ (3의배수 분 - 3분 + 1분) 사이의 최고값/최저값이 3분봉의 고가/시가이다.
69줄 : 3분봉의 종가는 (3의 배수의 분 - 3분 +1분) 분의 종가이다.

※ 여기서 잠깐! "3의 배수의 분 - 3 + 1분"을 왜 하는가?
현재시간이 05:59분이라고 하자. 3분봉의 완성된 마지막 봉은 몇 분일까? 05:54분의 3분봉(05:54~05:56)이다. 필자의 코드로 05:59분에 조회하면 05:54분, 05:51분 등이 조회된다.

05:59분 기준, 05:54를 표현하면 어떻게 될까?
66줄의 "j - self.bong_what_minute_sort + 1"을 보자. j는 64줄에서 range(482, 2)로 결정되었다.
self.bong_what_minute_sort는 59줄에서 3으로 결정되었다.

66줄을 다시 써보면, 482 - 3 + 1이다. 결과값은 480이며, 480은 3의 배수이다. 나머지가 어떻게 되었든 조회시간의 분을 3의 배수로 만들어주는 것이다. 즉, 언제 조회되어도 체결시간의 분(minute)을 3의 배수로 만든다. 66줄의 후단을 보자. 64줄에서 j는 482로 결정되었다. j+1은 483이다.

결론 : 66줄의 범위를 다시 써보면, range(480, 483)이다. for문의 range의 끝수(483)는 사용되지 않으므로, 66줄은 480분~482분을 뜻한다.

 
71줄~74줄 : 66줄~69줄에서 계산한 3분봉 시가, 고가, 저가, 종가를 리스트에 각각 넣어준다.
90줄 : 28줄에서 정의한 함수를 종목코드(NQZ23)와 분봉 종료(1분봉)을 담아서 실행한다.
 


4. 전체코드

아래 < 접은글 >은 키움증권에 월 시세 이용료($185)를 지불하여야 한다.
 

더보기
import sys
from PyQt5.QAxContainer import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

class btl_system():
    def __init__(self):
        self.kiwoom = QAxWidget("KFOPENAPI.KFOpenAPICtrl.1")
        print("로그인 시작!")

        self.kiwoom.dynamicCall("CommConnect(1)")  # CommConnect() : 괄호 안에 자동(1)을 넣는다.
        self.kiwoom.OnEventConnect.connect(self.login_Connect)
        self.kiwoom.OnReceiveTrData.connect(self.trdata_get)

        self.login_event_loop = QEventLoop()
        self.login_event_loop.exec_()

        self.kiwoom.dynamicCall("GetCommonFunc(QString, QString)", "ShowAccountWindow", "")  # 계좌번호 입력창을 띄우는 내부함수

    def login_Connect(self, err_code):
        if err_code == 0:
            print('로그인 성공했습니다!')
        else:
            print('로그인 실패했습니다!')
        self.login_event_loop.exit()

    ########## 키움서버에 TR 요청하는 함수 모음 ##########
    def rq_data_opc10002(self, stock_code_num, time_unit):  # 1분봉 받기
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "종목코드", stock_code_num)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "시간단위", time_unit)
        self.kiwoom.dynamicCall("CommRqData(QString, QString, QString, QString)", "opc_10002", "opc10002", "", "1002")
        self.tr_event_loop = QEventLoop()
        self.tr_event_loop.exec_()

    ########## OnReceiveTrData을 통해 수신받은 데이터 함수  ##########
    def trdata_get(self, scrno, rqname, trcode, recordname, prenext):
        if rqname == "opc_10002":
            getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString,QString)", trcode, recordname)
            self.getrepeatcnt = getrepeatcnt

            self.tr_event_loop.exit()
            btl.minute_1_whant_minute_bong()

    def minute_1_whant_minute_bong(self):
        current_time_0 = self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", 0, "체결시간")
        print(current_time_0)
        print(current_time_0[8:12])
        print(current_time_0[10:12])
        print(self.getrepeatcnt)

        current_time_1 = int(current_time_0[10:12])

        self.bong_1_what_minute_open_list = []
        self.bong_1_what_minute_high_list = []
        self.bong_1_what_minute_low_list = []
        self.bong_1_what_minute_close_list = []

        self.getrepeatcnt_480 = 480     # 60분 설정을 대비하여 600설정은 안됨, 이유? 리스트 내에 0~599개(600개)임. 즉 리스트 오른쪽 마지막 숫자를 가리키는 "자리수"는 599이다.
        self.bong_what_minute_sort = 3     # 1,3,5,10,30, 60의 공배수를 찾는다.
                                            # 60의 약수 = {1,2,3,4,5,6,10,12,15,20,30,60}

        for i in range(0, self.bong_what_minute_sort):
            if current_time_1 % self.bong_what_minute_sort == i:
                for j in range(self.getrepeatcnt_480 + i, i, -self.bong_what_minute_sort):        ### 16, 4
                    if j//self.bong_what_minute_sort == (j-i)/self.bong_what_minute_sort: # -1, 0, 1,2,3
                        open_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", j, "시가").strip()))
                        high_price = max([abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", j, "고가").strip())) for j in range(j - self.bong_what_minute_sort + 1, j + 1)])
                        low_price = min([abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", j, "저가").strip())) for j in range(j - self.bong_what_minute_sort + 1, j + 1)])
                        close_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", j - self.bong_what_minute_sort + 1, "현재가").strip()))

                        self.bong_1_what_minute_open_list.append(open_price)
                        self.bong_1_what_minute_high_list.append(high_price)
                        self.bong_1_what_minute_low_list.append(low_price)
                        self.bong_1_what_minute_close_list.append(close_price)

        print(self.bong_1_what_minute_open_list)
        print(self.bong_1_what_minute_high_list)
        print(self.bong_1_what_minute_low_list)
        print(self.bong_1_what_minute_close_list)

        self.bong_1_what_minute_open_list = []
        self.bong_1_what_minute_high_list = []
        self.bong_1_what_minute_low_list = []
        self.bong_1_what_minute_close_list = []

if __name__ == "__main__":
    app = QApplication(sys.argv)
    btl = btl_system()

    btl.rq_data_opc10002("NQZ23", "1")

    app.exec_()

 


5. 마치며

1분봉을 3분봉, 60분봉 등의 데이터로 바꾸는 방법을 알아보았다. 관건은 58줄(조회할 모수)와 59줄(분봉의 종류)이다. 사용자 취향에 따라 59줄에 10분, 20분 등을 각각 넣을 수 있다. 59줄(분봉 종류)는 58줄(모수)에 대해 약수에 해당하면 조회된다. 
 
필자의 데이터 조회에 대해 한계도 존재한다.
만약 59줄에 77을 넣어서 77분봉을 조회하고 싶다면, 58줄에는 77의 공배수를 넣어주면 된다. 77, 154, 231, 308, 385, 462 중 어떤 숫자든 넣으면 된다. 77분봉 조회시 58줄에 539을 넣어주면 좋을 거 같은데, 58줄에 539(77의 공배수)을 넣으면, 64줄에서 에러가 발생한다. 정확히는 빈 리스트만 출력될 뿐, 아무것도 출력되지 않는다. 이유는? 가져오는 데이터가 600개이다. 64줄에서 (58줄의 모수 + 59줄의 분봉)이 600을 넘으면 안된다.
 
필자의 경우도 600개 데이터를 이용해 60분봉 10개를 출력하고  싶었으나, 64줄을 range(540+60, 60,-60)으로 설정하니 빈 리스트만 출력되고 아무것도 출력되지 않았다. 이유를 생각해보면, 600개의 데이터를 자리수로 표현하면, 0~599으로 표기된다. 599~0으로 표기되는데, 600(540+60)을 요청해서, 없는값 조회가 되어서 64줄의 for가 돌아가지 않았다.
 
2가지만 기억하자. 59줄의 분봉종류는 58줄의 약수일 것과 58줄의 전체모수는 64줄에 의해 "모수 + 분봉"이 600을 초과하면 안된다는 것을 기억하자.
 
65줄에서 2시간 정도 헤매긴 했지만, 즐겁게 코드를 작성한 것 같다. 1분봉만 보면서 거래를 하다보면 시야기 좁아지는 것 같다. 여러 분봉을 활용하여 시장을 크게 보는 시야를 기르자.
 
 

반응형