2. 해외선물/2-4. 해외선물 API (사용)

(키움증권 해외선물 OpenAPI-W) RSI 값 구하기 (3) RSI 계산하기

봄이오네 2023. 2. 2. 08:01
반응형
목 차
1. 들어가며
2. 사전설명
1) RSI 모수는 700개 종가 활용
2) CommRqData 1초당 2회 사용
3) 사용 TR 목록은 opc10002
3. 코드 설명
< 1차 계산(산술평균) >
< 2차 계산(지수가중평균) >
4. 전체코드
5. 마치며

1. 들어가며

지난 글에서는 RSI 개념 및 계산하는 방법을 알아보았다. 상승분 평균(au)과 하락분 평균(ad)의 상대적 강도를 의미하며, 최초 기준값 찾기(1차 계산) 및 지수가중평균(2차 계산)을 계산하는 방법을 설명하였다.

이번 글에서는 RSI 계산하는 방법을 코드로 구현해보자.
RSI 계산을 위한 마지막 설명이다.

그림1. 2023년 1월 28일 06:59분의 RSI는 46.54이다.

2. 사전설명

1) RSI 모수는 700개 종가 활용

RSI 모수는 700개의 종가를 활용한다. 키움증권 영웅문G에서 RSI 계산을 위한 1분봉을 다운받아 보면 630~680개 정도의 종가를 받을 수 있다. 사용자에게 제공해주는 데이터가 일정하지 않다는 점이 마음에 걸렸다. 결론은 연속조회를 통해 700개의 종가를 가져올 것이다.

2) CommRqData 1초당 2회 사용

이번부터는 글을 쓸 때 1초당 CommRqData의 사용횟수를 체크를 할 것이다. 최초 기준값(1차 계산)을 위해 첫번째 사용할 것이고, 지수가중평균(2차 계산)을 위해 두번째 사용할 것이다.

3) 사용 TR 목록은 opc10002
사용할 TR 목록은 opc10002(해외선물옵션분차트조회)이다.
WKOA Studio에서 관련 내용을 확인하면 분 차트 조회를 위해 "종목코드"와 "시간단위"를 입력(input)한다.
수신받는 데이터(output)는 현재가 1개를 활용한다.

그림2. opc10002를 통해 현재가를 받는다.

3. 코드 설명

opc10002로 종가 데이터 700개를 받아오고, 최초 기준값(au / ad)을 설정하고, 지수가중평균을 통해 RSI 값을 구한다.
반복 활용되는 모듈과 로그인 설명은 생략한다.

그림3-1. 임포트한 모듈과 로그인 관련 함수


1줄~33줄 : 임포트한 모듈과 로그인 코드 내역이다.
20줄 : 56줄~86줄에서 받는 1분봉 종가데이터를 담는 리스트이며, 초기 값은 빈칸으로 설정한다.
21줄 : 81줄~84줄, RSI 최초값 설정을 위한 1차 계산을 위해 15개 종가를 담는 리스트이다.
22줄 : 85줄~94줄, 기준값 설정을 위해 종가 간의 차이를 담는 리스트이다.

24줄 : 68줄에서 연속조회로 받아온 100개 데이터를 받아오려면, for문에는 101값이 들어가야 한다.(101로 설정)
25줄 : 96줄에서 데이터의 지수가중평균을 위해 for문의 range의 끝값을 위해 700으로 설정한다.

그림3-2. 데이터 입력/요청/수신받는 화면


36줄~41줄 : RSI 기준값(산술평균)을 구하기 위해 키움증권 서버에 종목코드와 시간단위를 입력/요청한다.
43줄~48줄 : RSI의 지수가중평균을 구하기 위해 종목코드와 시간단위를 입력/요청한다.

51줄~58줄 : RSI 기준값(산술평균)을 받아오는 함수이다.
52줄 : WKOA Studio에 의하면 GetRepeatCnt 함수는 페이지에 있는 데이터 갯수를 반환한다. (여기서는 숫자형 600이다.)
55줄 : 36줄의 함수를 실행하면, 키움증권 내 이벤트가 발생하는데 13줄의 "연결"을 통해 종가 600개를 제공받는다.
56줄 : 받아온 600개 데이터를 20줄의 self.price_rsi_total 에 빈 리스트에 추가해준다.(append)

60줄 : 연속조회(prenext)에 빈칸이 아니면 (= 내용이 있으면)
61줄 : 50줄에서 전달하는 prenext를 self.prenext에 넣는다.
63줄 : 43줄의 rq_data_opc10002_1 함수를 실행한다. 46줄의 연속조회에는 "self.prenext"를 넣는다.

46줄 : self.kiwoom.dynamicCall("CommRqData(QString, QString, QString, QString)", "opc_10002_1", "opc10002", self.prenext, "1012")


66줄 : 수신받는 데이터(rqname)가 opc_10002_1이면,
68줄 : for문을 통해 100개 종가데이터를 받아온다. 이때 self.prenext_price_date는 24줄에서 101로 설정하였는데, for문의 range의 끝값을 101로 설정하면, for ~ range(101)이면, 0~100번까지 적용된다.
70줄 : 20줄의 self.price_rsi_total에 종가 데이터를 넣는다.

74줄 : 이 글의 핵심이다. 76줄의 rsi_searching 함수를 실행한다. 20줄의 self.price.price_rsi_total에 700개 종가 데이터를 넣었고, for문을 통해 기준값이 산출되면 첫번째 au / ad를 그 다음 au / ad에 적용하면 된다.

그림3-3. RSI 최초값 및 가중평균을 통해 RSI 현재값 받는 화면


76줄 : 여기서부터는 설명이 조금 길어진다. 천천히 코드를 확인해보자.
56줄에서 현재가 ~ 600분의 종가 600개, 70줄에서 601분~700분의 종가 100개가 20줄의 self.price_rsi_total에 들어있다. 여기서 주목할 점은 현재가 ~ 701분전의 종가시간 반대순으로 들어있다. 이유는? 키움에서는 현재시간을 기준으로 과거로 데이터를 반환해준다. 즉, 최근의 데이터부터 제공해준다는 것이다.

77줄 : 20줄의 self.price_rsi_total에 시간 역순으로 제공된 데이터를 시간 순으로 정렬할 필요가 있다. 이유는? 과거부터 가중평균된 au / ad가 현재의 au / ad에 영향을 미치게 때문이다. 즉, 시간 역순에서 시간순으로 데이터를 정렬한다.
리스트 내의 요소들을 역순으로 정렬해 주는 명령어는 [::1]이다. a = [1, 2, 3]인데, a[::1]을 붙이면, [3, 2, 1]로 반환된다.
역순으로 정렬된 리스트를 77줄의 self.price_rsi_total_final로 정렬해준다. (700개 종가 데이터가 시간 순으로 정렬되었다)

1차 계산(산술평균)은 79줄~94줄에서 계산하며, 2차 계산(지수가중평균)은 96줄~119줄에서 계산한다.

< 1차 계산(산술평균) >

79줄~81줄 : 1차 계산(산술평균)을 위해 700개 데이터가 들어있는 self.price_rsi_total_final의 요소를 for문을 통해 self.close_price_rsi_change의 리스트에 넣는다.

83줄~84줄 : 종가 간 변화율(차이)를 계산하기 위해 self.close_price_rsi_change에서 종가를 2개씩 추출하여 차이를 계산한다.
85줄 : 각각 종가의 차이는 self.close_price_third의 리스트에 추가한다.(append)

87줄~88줄 : self.close_price_third 리스트 내 종가의 차이가 14개 들어있다.
87줄 : self.close_price_third 리스트에서 항목을 하나씩 추출하되, 0보다 작으면 0으로 표시하여 self.early_au1에 넣고, 0보다 크면 0보다 큰 값으로 self.earlt_au1 리스트에 넣는다.
88줄 : self.close_price_third 리스트 내 요소를 하나씩 추출하되, 0보다 작으면 0보다 작은값으로 표시하여 self.early_au1에 넣고, 0보다 크면 0으로 표기하여 self.earlt_au1 리스트에 넣는다.

90줄~92줄 : 상승분의 평균과 하락분의 평균을 구한다. 하락분의 평균은 abs(절대값)을 붙여 양수로 만든다.
이 글의 핵심이다. 여기서 구한 self.au7 및 self.ad7은 108줄과 109줄에서 활용한다.
94줄 : 메모리 절약을 위해 self.close_price_third의 리스트 내의 내역을 삭제한다.

< 2차 계산(지수가중평균) >

96줄 : 24줄의 self.prenext_price_date는 101로 설정한 이유가 여기에 있다. for ~ range(1, 700)의 내용이므로 1~699까지 699번 반복한다. 즉 연속조회를 통해 현재까지의 rsi를 구한다.

97줄~99줄 : "종가"와 "종가+1분"의 차이를 구해서, current_price_3에 넣는다.
101줄~103줄 : current_price_3(차이)가 양수이면 positive_price에 양의 값으로 넣고, negative_price에 0을 넣는다.
104줄~106줄 : current_price_3(차이)가 음수이면 positive_price에 0을 넣고, negative_price에 음의 값을 넣는다.

108줄~109줄 : 가중평균을 통해 au / ad를 구한다.
108줄 : 1차 계산에서 구한 au에 13을 곱하고 positive_price를 더한 후 14로 나눈다.
109줄 : 1차 계산에서 구한 ad에 13을 곱하고 negative_price를 더한 후 14로 나눈다.

111줄 : rs 값은 au8을 ad8로 나눈다.
113줄 : rsi 값을 구한다.
114줄 : rsi 값의 가독성 향상을 위해 소수 3자리에서 반올림한다.

116줄~117줄 : 2차 계산에서 산출된 au8와 ad8을 au7와 ad7에 넣는다. 향후 96줄의 for문에 의하여 au / ad 값이 과거 종가부터 계산되어 현재까지 빠르게 계산된다.
119줄 : 현재가의 current_rsi가 출력된다.

그림3-4. 125줄에서 함수를 실행한다.


125줄 : 36줄의 함수(rq_data_opc10002)를 실행한다.


4. 전체코드

opc10002를 통해 RSI (상대강도지수)를 구하는 방법은 아래와 같다.

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

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

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

        self.kiwoom.dynamicCall("CommConnect(1)")  # CommConnect() : 괄호 안에 자동(1)을 넣는다.
        self.login_event_loop = QEventLoop()  # from PyQt5.QtCore import * : qtcore가 임포트되어야 함
        self.login_event_loop.exec_()

        ##### rsi의 현재 1분 데이터의 au/ad 구하기 #####
        self.price_rsi_total = []           # 56~86줄 : 1분봉들의 종가데이터를 담는 리스트 모음
        self.close_price_rsi_change = []    # 81~84줄 : 기준값(au/ad)를 세팅하기 위해, 최근 15개 값을 담는 리스트
        self.close_price_third = []         # 85~94줄 : 기준값 15개의 각 종가들의 차이 모음 리스트

        self.prenext_price_date = 101       # 68줄 : 추가 조회할 자료  / # for문에서의 끝자리에 해당하므로 100개를 추가하려면, 101을 넣어주면 됨
        self.prenext_enging_price = 700     # 96줄 : 700개를 추출하기 위함

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

    ########## 키움서버에 TR 요청하는 함수 모음 ##########
    def rq_data_opc10002(self, stock_code_num, time_unit):
        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", "", "1012")
        self.tr_event_loop = QEventLoop()
        self.tr_event_loop.exec_()

    def rq_data_opc10002_1(self, stock_code_num, time_unit):
        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_1", "opc10002", self.prenext, "1012")
        self.tr_event_loop = QEventLoop()
        self.tr_event_loop.exec_()

    def trdata_get(self, scrno, rqname, trcode, recordname, prenext):
        if rqname == "opc_10002":
            getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString,QString)", trcode, recordname)

            for i in range(getrepeatcnt):   # 현재가 → 오래된 값으로 출력된다.
                self.current_price_rsi = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002", "opc10002", i, "현재가").strip())
                self.price_rsi_total.append(self.current_price_rsi)

            self.tr_event_loop.exit()

            if prenext != "":
                self.prenext = prenext
                print("62줄")
                btl.rq_data_opc10002_1("NQH23", "1")
                time.sleep(1)

        elif rqname == "opc_10002_1":
            time.sleep(1)
            for i in range(0, self.prenext_price_date):
                self.current_price_rsi_1 = float(self.kiwoom.dynamicCall("GetCommData(QString,QString,int,QString)", "opc_10002_1", "opc10002", i, "현재가").strip())
                self.price_rsi_total.append(self.current_price_rsi_1)

            self.tr_event_loop.exit()

            btl.rsi_searching()

    def rsi_searching(self):
        self.price_rsi_total_final = self.price_rsi_total[::-1]

        for i in range(0, 15):
            current_price_recent = self.price_rsi_total_final[i]
            self.close_price_rsi_change.append(current_price_recent)

        for j in range(len(self.close_price_rsi_change) - 1):
            self.close_price_second = self.close_price_rsi_change[j + 1] - self.close_price_rsi_change[j]
            self.close_price_third.append(self.close_price_second)

        self.early_au1 = [0 if i < 0 else i for i in self.close_price_third]  # 음수를 0으로 표시
        self.early_ad1 = [0 if i > 0 else i for i in self.close_price_third]  # 양수를 0으로 표시

        self.au7 = sum(self.early_au1) / len(self.early_au1)
        self.ad7 = sum(self.early_ad1) / len(self.early_ad1)
        self.ad7 = abs(self.ad7)

        del self.close_price_third[:]  # 향후 사용하지 않을 분봉 리스트는 초기화(삭제)하여 메모리 효율화 추진

        for k in range(1, self.prenext_enging_price):
            curren_price_rsi_1 = self.price_rsi_total_final[k]
            curren_price_rsi_2 = self.price_rsi_total_final[k+1]
            current_price_rsi_3 = curren_price_rsi_2 - curren_price_rsi_1

            if current_price_rsi_3 >= 0:
                positive_price = current_price_rsi_3
                negative_price = 0
            else:
                positive_price = 0
                negative_price = abs(current_price_rsi_3)

            self.au8 = (self.au7 * 13 + positive_price) / 14
            self.ad8 = (self.ad7 * 13 + negative_price) / 14

            rs2 = self.au8 / self.ad8

            self.rsi_second = rs2 / (1 + rs2) * 100
            self.current_rsi = round(self.rsi_second, 2)

            self.au7 = self.au8
            self.ad7 = self.ad8

        print(self.current_rsi)

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

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

    app.exec_()

5. 마치며

산술평균과 지수가중평균을 통해 현재시간의 RSI 값을 구하는 방법을 알아보았다. 여기서 RSI는 기준값 받기, 가중평균 계산을 위해 1초에 CommRqData를 2회 사용할 예정이다.

RSI가 30이하일 때 long, 70이상일 때 short으로 진입을 생각하되, 시뮬레이션을 돌릴 때 30 / 70의 적절성에 대해 좀더 고민해 보도록 하자.

반응형