1. 국내주식/1-2. 키움 OpenAPI (사용)

(주식 자동 매매) 키움증권 OpenAPI - 1분봉 데이터 실시간 받기(opt10080)

봄이오네 2022. 10. 5. 08:02
반응형

1. 들어가며

지난 시간에는 키움증권 OpenAPI를 통해
이미 매수하여 계좌에 보유중인
4종목의 매수가격과 매수량을 출력해 보았다.

이번에는 키움증권 OpenAPI를 활용하여 4종목의 1분봉 데이터를 받아보자.
구현할 코드는 아래와 같다.

  • 1분마다 1분봉을 키움서버에서 받아온다. (이 글에서는 10초마다 반복하여 받아온다)
  • 받아온 1분봉을 데이터프레임에 담아 출력한다.
  • 데이터프레임에 들어간 1분봉을 엑셀에 보내서 계속 저장한다.


핵심은 1분봉을 엑셀에 저장하는 것이다.
1분봉의 패턴을 파악하여 진입/청산 시점을 알고 싶기 때문이다.


2. 사전준비

KOA StudioSA에 접속/로그인하면,
아래와 같이 TR요청번호와 입력해주어야 하는 정보를 확인할 수 있다.

KOA StudioSA의 TR목록 탭을 클릭한다.
② TR목록에서 10080을 입력
③ opt10080에서는 INPUT은 3가지(종목코드, 틱단위, 수정주가구분)를 입력한다.
OUTPUT는 시가,고가,저가,현재가,거래량,체결시간 등을 제공한다.
* 현재가는 "현재시각"에는 변하지만, 1분 전의 1분봉은 종가가 된다.
(1분 전의 봉을 받아올 때는 종가를 가져온다는 것이다.)
* 예를 들어, 현재 09:40분에 조회를 하면, 09:40분의 현재가는 변하지만,
09:40분에 조회할 경우, 09:39분의 현재가는 09:39분의 종가가 된다.
④ 사용할 함수 2개 : SetInputValue 함수(서버에 입력), CommRqData 함수(서버에 요청)
⑤ SetInputValue에 넣어주어야 하는 3가지 : 종목코드, 틱단위(분봉 종류),
수정주가구분(0으로 둔다.)
⑥ 종목코드, 틱단위, 수정주가구분 3가지를 입력 후 조회하면, ⑥과 같은 결과를 얻을 수 있다.

< 그림1. KOA StudioSA에서 opt10080을 요청하는 화면 >


3. 코드 구현

  • 키움증권의 OpenAPI에 접속/로그인하고,
  • for문을 활용하여 4종목의 1분봉을 요청/수신하여,
  • 데이터프레임 형태로 엑셀에 저장

 

< 그림2. 데이터프레임 초기화 및 종목코드 입력 화면 >

※ 로그인 관련 내용은 생략한다.
21줄 : 1분봉을 데이터프레임 형태로 받기 위해, minute_data의 날짜, 시간, 시가 등으로 초기화한다.
23줄 : 흥미종목(interesting_codes)을 전역변수 선언한다.
* 여기서 말하는 "흥미종목"은 임의로 명칭한 것이며, 영웅문4의 "관심종목"이 아니다.
(혼동 방지를 위해, 이글에서는 "흥미종목"이라고 하겠다)
24줄 : 4종목을 흥미종목에 넣어준다.
* 4종목 : 삼성전자(005930), 경동나비엔(009450), 아프리카TV(067160), 토니모리(214420)

< 그림3. 데이터 요청 및 수신받는 화면 >


33줄 : 72줄(btl.rq_data_opt10080(interesting_code, "1", 0)을 실행하기 위한 함수를 만든다.
* interesting_code는 24줄에서 이미 정의하였으며,
72줄의 함수를 실행하면, 70줄의 for문에 따라 interesting_code
32줄의 rq_data_opt10080(stock_code, tik, jugagubun)의 코드(stock_code)에 순차적으로 들어간다.
34줄~36줄 : SetInputValue를 통해 종목코드, 틱범위, 수정주가구분을 키움서버에 입력한다.
37줄 : CommRqData를 통해 키움서버에 "opt10080"을 요청한다.
38줄~39줄 : tr_event_loop를 선언하여, 데이터 입력/요청이 완료될 때까지
이벤트 루프를 대기한다.

41줄 : 34~37줄까지 입력(SetInputValue)하고 요청(CommRqData)한 내용을
사용자에게 수신해 준다.
42줄 : 요청한 내용이 37줄의 opt_10080이면, 아래 실행
43줄 : 43줄~60줄이 이 글의 핵심이다.
그림1의 KOA StudioSA의 ⑥에서 확인하였듯이,
키움서버로부터 정보를 받아올 때는 최신정보부터 받아온다.
→ 가장 최근의 데이터는 현재의 봉이며, 그 이전의 1분봉의 정보를 얻는 것이다.
(위에서 확인하였듯이, 현재봉의 현재가는 거래가 계속 되기 때문에 변하지만,
1분 전의 OHLCV 중 Close는 종가이다.)

for i in range(1,2)는 i는 첫번째[0]를 뜻하는 이 아니라,
(= range는 1이상 2미만을 의미한다.)
1분 전의 데이터[1]을 을 불러오라는 것이다.
(예시) 09:40분의 현재가는 계속 바뀌지만,
09:40분에 조회한 09:39분 "현재가"로 표기된 데이터는 종가이다.

range(1,3)이면, "1~2"까지만 실행해 달라는 이야기이다. (3은 들어가지 않음)

44줄~51줄 : GetCommData를 통해 필요한 데이터를 수신한다.
51줄 : (고가-종가) + (저가-종가) + (종가-시가)를 통해, 패턴을 기억한다.
매수할 때 기준으로 쓰일 예정 (향후 구현 예정)

53줄~60줄 : 받아온 데이터를 21줄의 "minute_data"에 넣어준다. (append)
61줄~64줄 : 38줄~39줄에서 선언/실행한 이벤트 루프를 종료한다. (exit)

< 그림4. threading.Timer을 이용하여 10초마다 1분봉 받기 >


70줄 : 92줄의 threading.Timer을 이용하여 10초마다 반복하기 위해 rq_data_minute()를 만들어준다.
71줄 : 전역변수인 24줄의 흥미코드(interesting_codes)를 for문을 활용하여,
72줄 : 33줄의 rq_data_opt10080에 순차적으로 넣어준다.
73줄 : 44줄~60줄을 통해 받아온 데이터(OHLCV 등)를 데이터프레임에 넣어준다.
74줄 : for문이 10초마다 반복되기 때문에, 데이터가 누적되어 출력된다.
이러한 누적 방지를 위해 tail(n=1)을 넣어준다.
* tail(n=1)은 받아온 데이터의 마지막에서 두번째를 나타낸다.
(마지막 혹은 최근 봉은 tail(n=0)으로 나타낸다)

77줄 : 데이터프레임으로 받아온 데이터를 df2에 넣어준다. (코드가 길다)
78줄 : 이 부분이 중요하다. 3일 정도를 헤맸다. ㅠㅠ
head(n=0)은 맨 위쪽의 내용만 받아오겠다는 내용이다.

아래 그림5에서 0으로 된 부분이 아니라, date, time 등 맨 윗줄은 head(n=0)로 나타낸다.
(자세한 설명은 83줄에서 실시한다)

< 그림5. head(n=0)은 빨간색으로 표기한 부분 >

80줄 : 엑셀 파일을 만들어줄 경로를 설정한다. (6줄의 import os가 여기서 쓰인다)
엑셀 이름은 본인이 임의로 지으면 된다. 필자는 minute_data로 지었다.
주의할 것은 확장자를 xlsx로 만든다.
(pandas모듈의 ExcelWriter을 활용하기 위함)

82줄~91줄 : 이 부분도 중요하다. 데이터프레임 데이터를 엑셀로 보내는 내용이다.
80줄에서 이야기했듯이 pandas모듈의 ExcelWriter로 활용하여,
엑셀에 저장하기 위해서이다.
* (주의) 82줄~91줄을 실행하려면 pandas는 1.4.2이상이어야 함
(1.4.2 버전 이하이면, 83줄의 overlay 명령어가 실행되지 않는다)
82줄 : dir의 경로에 있는 "minute_data.xlsx"가 경로에 존재하면,
83줄 : 위 경로에 있는 내용을 시트명(sheet_name)이
24줄에서 정의한 흥미코드(interesting_code)에
각 종목을 이름으로하는 시트에 1분봉을 각각 저장한다.
* 83줄에서 제일 많이 헤맸다. 이유는 df2.to_excel로 저장하니,
첫번째 종목인 삼성전자(005930)가 각 종목명의 시트에 처음으로 저장
* 결론은, 파일이 없을 때는 78줄처럼 "date, time, open 등"의 정보만 기록하여,
1분봉이 기록되도록 하면 된다.
* 궁금하다면,
88줄의 첫번째인 df3.to_excel(~~~)을 d2.to_excel(~~~)로 바꾸면,
필자가 왜 헤맸는지 알게 될 것이다.

85줄 : dir로 정의한 경로에 "minute_data.xlsx"이 없으면,
86줄 : "minute_data.xlsx"파일을 만들어서,
87줄~90줄 : 각 종목코드명 시트에 각 종목의 1분봉을 저장한다.

92줄 : threading.Timer 함수를 이용하여 10초마다, 70줄(rq_data_minute) 함수를 실행된다.
70줄 함수가 실행되면, for문이 돌면서 흥미코드(24줄)72줄의 함수에 들어가고,
33줄의 rq_data_opt10080의 함수가 실행된다.

94줄 : 92줄의 threading.Timer 함수를 실행하기 위해서는
threading.Timer가 들어있는 함수를 1번은 실행해 주어야 한다.

< 그림6. 1분봉을 데이터프레임 형태로 받은 형태 >

 

< 그림7. 바탕화면에 생성된 삼성전자(005930) 1분봉 모음 >

 

< 그림8. 바탕화면에 생성된 경동나비엔(009450) 1분봉 모음 >


4. 전체 코드

import sys
from PyQt5.QAxContainer import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import pandas as pd
import os
import threading

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

        self.kiwoom.OnEventConnect.connect(self.login_Connect)
        self.kiwoom.OnReceiveTrData.connect(self.trdata_get)

        self.kiwoom.dynamicCall("CommConnect()")
        self.login_event_loop = QEventLoop()
        self.login_event_loop.exec_()

        self.minute_data = {'date': [],'time': [], 'open': [], 'high': [], 'low': [], 'close': [], 'volume': [], 'pattern': []}

        global interesting_codes
        interesting_codes = ["005930", "009450", "067160", "214420"]

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

    def rq_data_opt10080(self, stock_code, tik, jugagubun):
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "종목코드", stock_code)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "틱범위", tik)
        self.kiwoom.dynamicCall("SetInputValue(QString,QString)", "수정주가구분", jugagubun)
        self.kiwoom.dynamicCall("CommRqData(QString,QString,int,QString)", "opt_10080", "opt10080", 0, "0102")
        self.tr_event_loop = QEventLoop()
        self.tr_event_loop.exec_()

    def trdata_get(self, sScrNo, rqname, strcode, sRecordName, sPreNext, nDataLength, sErrorCode, sMessage, sSplmMsg):
        if rqname == 'opt_10080':
            for i in range(1, 2):
                date = int(self.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", "opt10080", "주식분봉차트조회요청", i, "체결시간").strip()[0:8])
                time = int(self.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", "opt10080", "주식분봉차트조회요청", i, "체결시간").strip()[8:])
                open = abs(int(self.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", "opt10080", "주식분봉차트조회요청", i, "시가").strip()))
                high = abs(int(self.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", "opt10080", "주식분봉차트조회요청", i, "고가").strip()))
                low = abs(int(self.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", "opt10080", "주식분봉차트조회요청", i, "저가").strip()))
                close = abs(int(self.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", "opt10080", "주식분봉차트조회요청", i, "현재가").strip()))
                volume = abs(int(self.kiwoom.dynamicCall("GetCommData(QString, QString, int, QString)", "opt10080", "주식분봉차트조회요청", i, "거래량").strip()))
                pattern = str(high - open) + str(low - open) + str(close - open)

                self.minute_data['date'].append(date)
                self.minute_data['time'].append(time)
                self.minute_data['open'].append(open)
                self.minute_data['high'].append(high)
                self.minute_data['low'].append(low)
                self.minute_data['close'].append(close)
                self.minute_data['volume'].append(volume)
                self.minute_data['pattern'].append(pattern)
        try:
            self.tr_event_loop.exit()
        except AttributeError:
            pass

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

    def rq_data_minute():
        for interesting_code in interesting_codes:
            btl.rq_data_opt10080(interesting_code, "1", 0)
            df_minute_data = pd.DataFrame(btl.minute_data, columns=['date', 'time', 'open', 'high', 'low', 'close', 'volume', 'pattern'])
            df_minute_data = df_minute_data.tail(n=1)
            print(df_minute_data)

            df2 = df_minute_data
            df3 = df_minute_data.head(n=0)

            dir = r'C:\Users\User\Desktop\minute_data.xlsx'  # 경로 설정

            if os.path.exists(dir):
                with pd.ExcelWriter(dir, mode='a', engine='openpyxl', if_sheet_exists='overlay') as writer:
                    df2.to_excel(writer, header=False, index=False, sheet_name=interesting_code, startrow=writer.sheets[interesting_code].max_row)
            else:
                with pd.ExcelWriter(dir, mode='w', engine='openpyxl') as writer:
                    df2.to_excel(writer, index=False, sheet_name=interesting_codes[0])
                    df3.to_excel(writer, index=False, sheet_name=interesting_codes[1])
                    df3.to_excel(writer, index=False, sheet_name=interesting_codes[2])
                    df3.to_excel(writer, index=False, sheet_name=interesting_codes[3])

        threading.Timer(10, rq_data_minute).start()

    rq_data_minute()

    app.exec_()

5. 마치며

한달 내내 1분봉 받는 데 시간을 많이 할애했다.
실행을 위해 shift + f10을 100번은 누른 거 같다.

에러가 나면, 처음에는 속상한 마음이 들었지만,
나중에는 해결하려고 애썼던거 같다.

글을 다 쓰고 보니,
본문의 내용 중 더 설명을 했어야 하지 않았나 싶다.
82줄~90줄의 pandas모듈의 ExcelWriter의 설명이 부족한거 같아 조금 아쉽다.

개인적으로 엑셀을 좋아한다.
익숙하지 않은 MySQL에 저장하면 어떨까 생각했으나,
그냥 나에게 맞는 엑셀로 저장하는 게 좋을 것 같다.

위의 코드는 10초마다 실행하는데,
92줄을 60초로 바꾸어주면

1분 전의 "분봉"을 받아오는 것(OHLCV)이 되는 것이다.

반응형