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

(키움증권 해외선물 OpenAPI-W) 1분봉 데이터로 3분봉, 60분봉 만들기 (2) 3분봉 받아보기 (초급)

봄이오네 2023. 10. 31. 08:02
반응형
목 차
1. 들어가며
2. 사전설명
   1) WKOA Studio 확인하기
   2) 체결시간의 데이터 활용 순서
   3) 데이터 결과값 확인
3. 코드설명
4. 전체코드
5. 마치며

 

1. 들어가며

지난 글에서는 1분봉 데이터 모음으로 3분봉을 만드는 개념에 대해 알아보았다. 3의 배수에 해당하는 분(minute)이 첫번째 거래가격의 시가이며, 3분 ~ (3분+2)까지 최대값 및 최소값, (3분+2분)의 마지막 거래 가격이 3분봉의 종가였다. 3분봉 개념은 당연히 알거 같은데, 확인차 개념을 설명하였다.
 
이번 글에서는 1분봉 데이터 모음으로 3분봉 만드는 코드를 설명할 예정이다.
 


2. 사전설명

1) WKOA Studio 확인하기

< 그림1 >에서 분차트조회를 통해 TR목록의 결과값(output)으로 체결시간(④)을 얻을 수 있다. WKOA Studio의 TR 목록 중 opc10002(해외선물옵션 분차트조회)를 통해 NQZ23, 1분봉을 조회(②)하였다. 출력창(③)의 내용을 확인해 보자.
 
< 그림1 >에서는 체결시간의 데이터 활용 순서 및 형태 등 2가지를 확인할 수 있다. 첫번째, ⑤~ ⑧은 체결시간을 나타낸다. 키움측에서는 자료를 현재(⑤)→ 과거(⑧)의 형태로 데이터를 사용자에게 제공한다. 두번째, 데이터 결과값을 확인해보자. ⑤의 체결시간은 20231028055900이다. 연도-월-일-시간의 형태로 제공한다. 우리에게 필요한 것은 분(minute)이다. 문자형(str)에서 문자를 추출하는 방법은 아래에서 설명한다.
 
위의 2가지(데이터 제공받는 순서, 문자추출)은 바로 아래에서 설명한다.
 

그림1. WKOA Studio에서 확인가능한 opc10002의 체결시간

 

2) 체결시간의 데이터 활용 순서

지금껏 필자는 데이터를 받아올 때, 키움측에서 제공한 대로 "시간 역순(현재 → 과거)"으로 받아오고 나서, 리스트 형태를 시간 정배열(과거 → 현재)로 바꾸어서 보조지표(rsi, 스토캐스틱 등)의 값을 계산하였다.
 
여기서는 시간 정배열(과거 → 현재)로 데이터를 활용한다. 무슨 말인가?
예를들어 현재부터 11분(총 12분)까지의 데이터를 for 문으로 활용한다고 가정하면, 예전같으면 for i in range(0, 12)로 써주고, 리스트를 역변환(reverse)해주었다. 이제는 처음부터 for in range(11,-1, -1)로 활용하겠다는 것이다.

  • (기존 형태) for in range(0, 12)
  • (활용 형태) for in range(11, -1, -1)

range(0, 12)는 현재부터 11분까지로 직관적으로 다가오는데, range(11, -1, -1)의 형태가 조금 생소하다. range(0, 12)에서 12는 포함되지 않는다는 것은 알고 있을 것이다. range(0, 12)는 현재~11분까지를 가르킨다. 이에 비해, range(11, -1, -1)는 11분부터 현재까지를 나타낸다. -1은 포함되지 않는다(= 0까지 나타낸다). 또한 -1씩 감소시켜라는 뜻이다.
 

3) 데이터 결과값 확인

문자형(str)에서 사용자가 원하는 내용만 추출하려면, 대괄호([ ]) 안에 숫자를 써주면 된다.
< 그림2 >는 < 그림1 >의 ⑤의 체결시간(20231028055900)을 나타낸다. 2023년 10월 28일 05시 59분 00초를 나타낸다.
 

그림2. 체결시간을 파이썬에서 추출하려면 각 숫자의 자리수를 알아야 한다.

 
 우리에게 필요한건 분(minute)이다. 59를 추출하면 된다. ⑤의 체결시간을 변수화 시킨후 대괄호를 써서 추출하면 될 것이다. 아래 링크와 같이 59를 추출하는 방법은 링크의 1줄에서 데이터를 받아오고, 4줄처럼 [10, 12]를 넣어주면 된다. 끝수인 12는 포함되지 않는다. 즉 < 그림2 >의 10~11자리 숫자를 추출하라는 의미이다.
 
 

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

### (expected result) ###
### 20231028055900
### 0559
### 59

 


3. 코드설명

라이브러리, 로그인 등의 내용은 생략한다.
 

그림3-1. 라이브러리 및 로그인에 대한 내용은 생략한다.

 
1줄~25줄 : 1분봉 데이터를 받기 위해 필요한 라이브러리 및 로그인에 대한 내용이며, 여기에서는 설명을 생략한다.
28줄~33줄 : 1분봉 데이터를 받기 위해 116줄(btl.rq_data_opc10002)에 의해 실행되는 함수를 정의한다.
 

그림3-2. 데이터 수신 및 체결시간을 출력하는 화면

 
37줄~41줄 : 28줄~33줄에 의해 키움증권에 데이터를 입력/요청하면, 데이터를 수신받는 내용이다.
44줄 : 46줄~126줄에서 정의한 함수를 실행한다.
 
자, 여기서부터 구체적으로 설명하겠다.
46줄 : 1분봉 데이터를 3분봉 시가, 고가, 저가, 종가 등으로 변경하기 위한 함수를 선언한다.
47줄 :  < 그림1 >에서 확인하였듯이, "체결시간"을 가져온다. < 그림3-2 >에서는 짤렸지만, 현재시간(0)을 가져온다. 현재시간이 필요한 이유는? 조회시간은 사용자마다 각각 다르다. 체결시간에 따라 62줄~126줄까지 적용될 코드는 다르다.

time_0 = self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", 0, "체결시간")

 
47줄~53줄 : 현재의 "체결시간" 등을 조회해본다. 53줄에서 현재 체결시간의 분(minute)를 인수화(int) 시켜준다.
55줄~58줄 : 1분봉 데이터의 시가, 고가, 저가, 종가를 담아줄 리스트를 각각 선언한다.
 

그림3-3. 3분봉을 구하기 위해 각 분(minute)의 나머지(remainder)를 구한다.

 
지난 글에서 설명했던 나머지(remainder)에 대한 내용이다. 59분을 3으로 나누면 몫은 19이고, 나머지는 2이다. 이렇듯 나머지(remainder)에 의해 각각의 코드를 작성한다.
 
58줄 : 나머지가 0이라면,
59줄 : 12분 ~ 1분전까지, 3씩 감소(3분봉 이므로) 시키면서 for문을 돌려라.
60줄 : i가 i//3 및 i/3 동일하다면, 61줄~69줄을 실행하라.
여기서 1시간 이상 엄청 많이 헤맸다. 이 글의 핵심이다.
i는 59줄에 의해 for문의 첫번째 숫자인 12가 들어간다. 12 // 3은 12를 3으로 나누었을 때의 몫(quotient)을 의미하며 여기에서는 4를 의미한다. 또한 i/3 = 4이다.

※ 여기서 잠깐! i // 3 == i /3의 코드를 사용한 이유
현재시간을 03:47분이라고 가정해보자. 현재의 3분봉(03:45~03:47)은 완성되지 않았고, 가장 최근의 확정된 3분봉은 03:42~03:44분이다.

우리가 03:45분, 03:46분, 03:47분에 조회하든, 확정된 3분봉(03:42~03:44)의 데이터는 항상 동일하여야 한다.
i // 3 == i /3을 다시 확인해보자. (03시는 생략한다)

ㅇ 45분 // 3 = 15
ㅇ 46분 // 3 = 15.33
ㅇ 47분 // 3 = 15.66

45분~47분까지 몫(quotient)은 15로 동일하다. 이것은 3분봉이기 때문에 45분~47분간의 1분봉 시가/고가/저가/종가를 3분봉으로 변환하기 위함이다.

 
65줄~72줄 : 이 곳이 조금 어렵긴 하지만, 코드를 천천히 확인해 보자.

open_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "시가").strip()))
high_price = max(abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "고가").strip())),
                 abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "고가").strip())),
                 abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-2, "고가").strip())))
low_price = min(abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "저가").strip())),
                 abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "저가").strip())),
                 abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "저가").strip())))
close_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-2, "현재가").strip()))

자! 3분봉의 시가는 3의 배수에 해당하는 분(minute)의 시가이다. 63줄에서 i는 12이므로 12분에서의 1분봉 시가가 3분봉의 시가가 되는 것(65줄)이다. 종가는 어떨까? 12분전~10분전 사이에서 10분의 1분봉 종가가 3분봉의 종가가 되는 것(72줄)이다!
 
그렇다면, 1분봉 데이터 모음에서 3분봉의 최고가, 최저가는 어떻게 구할까? max 함수와 min 함수를 활용한다.
64줄의 내용은 ii/3 == i/3이었다. 이것은 03:45~03:47 사이에서의 몫(quotioent)은 15로 각각 동일하다는 것을 이용한다.
i가 15일때, 14(15-1)분, 13(15-2)를 뜻한다. 즉, max(15분전 최고가, 14분전 최고가, 13분전 최고가)의 코드를 의미한다.(65줄~68줄)
 
이와 동일하게, i=15일때, min(15분전 최저가, 14분전 최저가, 14분전 최저가)를 통해 3분봉의 최저가를 구한다.(69줄~71줄)
 
63줄에서 i=12일 때 한번 for문이 진행되었고, -3을 차감한 i=9, i=6, i=3일때 3분봉의 시가/고가/저가/종가를 구한다.
74줄~77줄 : 63줄~68줄에서 구한 3분봉의 데이터를 각각의 리스트에 담는다.
79줄~82줄 : 3분봉의 시가/고가/저가/종가가 들어있는 리스트를 출력하라.
 

그림3-4. 3분봉을 구하기 위한 화면이다.

위와 중복이므로 설명을 생략한다.
84줄 : 53줄의 체결시간의 분(minute)를 3으로 나눈 후의 나머지 값이 1일때, 즉 1분,  4분, 7분,...58분이면
85줄 : 13분~2분전까지의 데이터를 활용하여 for문을 돌려라. (나머지가 1이므로 1을 더했다)
87줄~104줄 : 1분봉 데이터 모음을 통해 3분봉의 데이터 추출한 후 리스트에 넣어서 출력하라.
 

그림3-5. 3분봉을 구하기 위한 화면 및 1분봉 데이터 요청화면

106줄 : 53줄의 체결시간의 분(minute)를 3으로 나눈 후의 나머지 값이 1일때, 즉 2분,  5분, 8분,...59분이면
107줄 : 14분~3분전까지의 데이터를 활용하여 for문을 돌려라. (나머지가 1이므로 2을 더했다)
108줄~126줄 : 1분봉 데이터 모음을 통해 3분봉의 데이터 추출한 후 리스트에 넣어서 출력하라.
132줄 : 30줄의 1분봉 데이터 받는 함수를 실행한다.
 


4. 전체코드

아래 < 접은글 >을 실행시키려면 키움증권에 월 시세 이용료($185) 지불하여야 한다.  기존 1분봉 데이터 조회에서 44줄(함수실행) 및 46줄~126줄(1분봉 데이터를 3분봉 데이터로 만드는 함수)를 추가하였다.
 

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

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_3_bong()

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

        aaa = time_0[10:12]
        aaa = int(time_0[10:12])

        self.bbb = []
        self.ccc = []
        self.ddd = []
        self.eee = []

        # self.getrepeatcnt

        if aaa % 3 == 0:      ### aaa는 현재시간       ### 0으로 떨어지면 시가, 2로 떨어지면 종가
            for i in range(12, 0, -3):      # 1분전 봉까지 출력 (과거 → 현재)
                if i//3 == i/3:
                    open_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "시가").strip()))
                    high_price = max(abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "고가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "고가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-2, "고가").strip())))
                    low_price = min(abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "저가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "저가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "저가").strip())))
                    close_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-2, "현재가").strip()))

                    self.bbb.append(open_price)
                    self.ccc.append(high_price)
                    self.ddd.append(low_price)
                    self.eee.append(close_price)

            print(self.bbb)
            print(self.ccc)
            print(self.ddd)
            print(self.eee)

        elif aaa % 3 == 1:      ### aaa는 현재시간       ### 0으로 떨어지면 시가, 2로 떨어지면 종가
            for i in range(12+1, 1, -3):      # 1분전 봉까지 출력 (과거 → 현재)
                if i//3 == (i-1)/3:
                    open_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "시가").strip()))
                    high_price = max(abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "고가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "고가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-2, "고가").strip())))
                    low_price = min(abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "저가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "저가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "저가").strip())))
                    close_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-2, "현재가").strip()))

                    self.bbb.append(open_price)
                    self.ccc.append(high_price)
                    self.ddd.append(low_price)
                    self.eee.append(close_price)

            print(self.bbb)
            print(self.ccc)
            print(self.ddd)
            print(self.eee)

        elif aaa % 3 == 2:      ### aaa는 현재시간       ### 0으로 떨어지면 시가, 2로 떨어지면 종가
            for i in range(12+2, 2, -3):      # 1분전 봉까지 출력 (과거 → 현재)
                if i//3 == (i-2)/3:
                    open_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "시가").strip()))
                    high_price = max(abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "고가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "고가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-2, "고가").strip())))
                    low_price = min(abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "저가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "저가").strip())),
                                     abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-1, "저가").strip())))
                    close_price = abs(float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i-2, "현재가").strip()))

                    self.bbb.append(open_price)
                    self.ccc.append(high_price)
                    self.ddd.append(low_price)
                    self.eee.append(close_price)

            print(self.bbb)
            print(self.ccc)
            print(self.ddd)
            print(self.eee)

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

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

    app.exec_()

 
 


5. 마치며

1분봉 데이터에서 3분봉 데이터를 추출하는 방법을 알아보았다. 단순한 내용인데도 코드가 상당히 길다는 느낌을 받았다. 체결시간의 분(minute)의 몫(quotient)과 나머지(remainder)을 통해 접근하는 방법은 괜찮은거 같은데, 여전히 코드가 길어진다. 일단 반복되는 패턴은 조금씩 보인다. 반복이 되는 내용은 조금씩 줄여나가 보자.
 
다음글에서는 for문 안에 리스트를 활용하여 코드의 길이를 줄어보자.
 
 

반응형