2. 해외선물/2-2. 해외선물 알고리즘 연구

(해외선물 자동매매 알고리즘) (2) rsi 70이상, 30이하 넓이 구하기

봄이오네 2023. 12. 24. 08:05
반응형

 

목 차
1. 들어가며
2. 사전설명
   1) rsi 넓이를 구하기 위한 경우의 수
   2) rsi 넓이 구하는 영역
3. 코드설명
4. 전체코드
5. 마치며

 

1. 들어가며

지난글에서는 rsi가 70이상, 30이하일 때 차트에 표기된 노란색(①) 혹은 파란색(②) 넓이를 구하는 방법을 알아보았다. 과매수 상태일 때는 노란색, 과매도 상태일 때는 파란색으로 표기된다.

 

그림1. 과매수 및 과매도 상태의 rsi 차트 현황

 

< 그림1 >은 2023년 12월 21일(목)의 22:30~00:30분까지의 1분봉이다. 22:30분 지표발표가 있었고, 저점 16,858.50을 기준으로 22:43분 최고가 16971.25까지 112.75p를 올리는 나스닥을 보니... 역시 나스닥이구나.ㅎㅎㅎ

 

필자는 < 그림1 >의 과매수(①) 및 과매도(②) 구간의 넓이를 구하고자 한다.

 

< 그림2 >에서 확인가능하지만, GDP QoQ는 예측치(5.2%)에 미달하였지만, 실업수당청구건수가 예측치 대비 감소(△9K)하여 상승한 것으로 사료된다. (출처 : 인베스팅 닷컴)

 ※ 자료 경로 : 인베스팅 닷컴(investing.com) > 뉴스 > 경제캘린더

 

그림2. 주요지표 발표 현황 (231221.목)

 


2. 사전설명

1) rsi 넓이를 구하기 위한 경우의 수

< 그림1 >에서 확인하였듯이, rsi는 실수(float)으로 산출된다. 이런 rsi 값을 직선으로 연결하고 rsi 70이상 및 30이하인 곳에서 넓이를 구하면 얼마나 과매수 및 과매도가 되는지 알 수 있다.

 

우리가 구하는 넓이의 경우의 수는 총 8가지이다. 이중 6가지만 코드로 구성할 것이다.

 

먼저 과매수 구간(rsi 70선)의 넓이는 구하는 경우를 알아보자. (그림1의 ①)

  • 1) 2분전rsi < 70 and 1분전rsi < 70인 경우는 과매수 구간이 아니므로, 코드를 구성하지 않는다.
  • 2) 2분전rsi < 70 and 1분전rsi > 70인 경우 : 2분전rsi가 rsi 70선을 상방으로 돌파하였으므로 삼각형 넓이를 구한다.
  • 3) 2분전rsi > 70 and 1분전rsi > 70인 경우 : 2분전 및 1분전 rsi 가 둘다 rsi 70선 위에 있으므로 사다리꼴 넓이를 구한다.
  • 4) 2분전rsi > 70 and 1분전rsi < 70인 경우 : 1분전rsi가 rsi 70선을 하방으로 돌파하였으므로 삼각형 넓이를 구한다.

 

두번째, 과매도 구간(rsi 30선)의 넓이를 구하는 경우를 알아보자. (그림1의 ②)

  • 1) 2분전rsi > 30 and 1분전rsi > 30인 경우는 과매도 구간이 아니므로, 코드를 구성하지 않는다.
  • 2) 2분전rsi > 30 and 1분전rsi < 30인 경우 : 2분전rsi가 rsi 30선을 하방으로 돌파하였으므로 삼각형 넓이를 구한다.
  • 3) 2분전rsi < 30 and 1분전rsi < 30인 경우 : 2분전 및 1분전 rsi 가 둘다 rsi 30선 아래에 있으므로 사다리꼴 넓이를 구한다.
  • 4) 2분전rsi < 30 and 1분전rsi > 30인 경우 : 1분전rsi가 rsi 30선을 상방으로 돌파하였으므로 삼각형 넓이를 구한다.

 

2) rsi 넓이 구하는 영역

이글에서는 rsi 70이상, 30이하인 곳의 넓이를 구하고자 한다. 중복되는 설명 부분은 생략한다. rsi를 구하는 방법은 링크(https://springcoming.tistory.com/217)에서 확인하자.

 

또한, 필자는 현재시간을 기준으로 40분전~6분전의 rsi의 70 및 30의 돌파 여부를 확인할 것이다. 5분~1분은 훼이크 패턴인지 필자 나름대로의 검증을 거칠 것이다. 즉, 5분전~1분전의 알고리즘은 사용자가 직접 구현해야 한다. 물론 40분~6분전을 조금 뒤로 당길수 있다. 물론, 30분~1분 등 시간을 설정하는 부분은 필자가 이 코드의 165줄에서 설명한다.

 

그림3-1. 나스닥 1분봉 차트(231223, 토)

 

< 그림3-1 >에서 2번은 현재시간(06:59분)을 말한다. 사각형으로 된 부분이 40분전~6분전을 구한것이다.  < 그림3-1 >에서 rsi 30을 돌파한 곳은 06:48분 rsi 29.03을 확인할 수 있다.

 

 

그림3-2. 231223(토)의 06:48분 rsi는 29.03이다.

 

< 그림3-2 >는 < 그림3-1 >을 확대한 그림이다. < 그림3-1 >에서 rsi 30을 돌파한 부분은 06:48분의 rsi 29.03만 있으므로, 우리는 < 그림3-2 >의 파란색 부분만 구하면 된다. (과매도된 부분을 시각적으로 확인할 수 있다)

 


3. 코드설명

로그인, 활용모듈, rsi 구하는 방법에 대한 설명은 생략한다.

 

그림4-1. 로그인 및 모듈에 관한 설명은 생략한다.

 

 

1줄~23줄 : 로그인 및 모듈에 대한 내용이며, 설명을 생략한다.

26줄~32줄 : 74줄~148줄에서 rsi를 구하기 위한 리스트를 미리 선언한다.

 

그림4-2. 데이터 입력, 요청 및 수신에 관한 내용이다.

 

51줄~67줄 : 1분봉 데이터의 입력, 요청 및 수신 등에 관한 내용이다. 설명을 생략한다.

 

그림4-3. rsi를 구하는 방법이다.

 

70줄 : 74줄의 rsi 구하는 함수를 실행한다.

74줄~102줄 : rsi 구하는 방법이며 맨 앞의 링크에서 설명하였으므로, 여기서는 설명을 생략한다.

 

그림4-4. rsi를 구하기 위해 au(상승분) 및 ad(하락분)을 각각 구한다.

 

103줄~137줄 : rsi 를 구하는 방법이며, 1번 링크에서 확인가능한 내용으로, 여기서는 설명을 생략한다.

 

그림4-5. 현재가의 rsi 등을 구한다.

 

139줄~148줄 : 5분전~현재가의 rsi를 구한다.

 

161줄~163줄 : 여기서부터 본격적으로 설명한다. 리스트는 총 3가지를 선언한다.

161줄 : rsi를 담아줄 리스트를 선언한다. 여기서 rsi 값을 두개씩 추출해서 계산할 것이다.

162줄 : rsi 70 초과한 데이터를 담는다.

163줄 : rsi 30 미만인 데이터를 담는다.

 

165줄 : 141줄에서 현재시간 → 과거시간으로 되어 있는 리스트의 6번째(5분전)~40번째(40분전)의 리스트를 추출한다.

166줄 : 165줄에서 추출한 데이터를 역순으로 배열한다. 이유는? 2개씩 추출해서 계산하기 위해서이다.

 

169줄~170줄 : 사용자는 본인의 성향에 따라 데이터를 정할 수 있다. 일반적인 과매수의 기준은 70, 과매도는 30을 설정하였다. 사용자 취향에 따라 결정한다.

 

 

그림4-6. rsi 70이 넘을 때, 과매수 구간의 넓이를 구한다.

 

필자는 2번에서 경우의 수 6가지를 설명하였다. 174줄~234줄까지는 경우의 수 6가지에 대한 내용이며, 각각의 경우에 따라 삼각형 및 사다리꼴을 구하는 방법이다.

 

174줄~189줄 : 2분전 rsi는 70미만이며, 1분전 rsi는 70을 돌파한 경우이다. 화면에서 짤린 부분은 4번(전체코드)에서 확인가능하다.

 

191줄~194줄 : 2분전 rsi 및 1분전 rsi가 각각 70이상인 경우이다. 사다리꼴 넓이를 구한다.

196줄~209줄 : 2분전 rsi는 70이상이고, 1분전 rsi가 70미만인 경우이며, 삼각형의 넓이를 구한다.

 

그림4-7. rsi 30 미만인 범위의 넓이를 구한다.

 

211줄~244줄 : 2분전rsi 및 1분전rsi가 30미만이거나 30이상인 경우의 삼각형 및 사다리꼴을 구하는 내용이다.

211줄~224줄 : 2분전 rsi는 30이상이고, 1분전 rsi는 30이하인 경우, rsi 30선 돌파이므로 삼각형의 넓이를 구한다.

226줄~229줄 : 2분전 rsi 및 1분전 rsi가 각각 30미만인 구간으로 사다리꼴의 넓이를 구한다.

231줄~244줄 : 2분전 rsi는 30미만이고, 1분전 rsi가 30이상인 경우로, rsi 30선을 상방 돌파한 구간으로, 삼각형의 넓이를 구한다.

 

그림4-8. rsi 70이상(과매수) 및 30이하(과매도) 구간의 넓이를 각각 합산한다.

 

251줄~257줄 : 40분전~6분전의 구간에서 rsi 넓이가 70이상 혹은 30이하인 갯수 및 넓이를 각각 합산한다.

266줄~268줄 : 사용완료된 리스트를 초기화한다.

 


4. 전체코드

아래 코드를 실행하려면 키움증권에 $185달러의 시세조회 이용료를 지불하여야 한다.

 

더보기
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.password_login()  # 영구적으로 관리자 권한으로 실행
        self.login_event_loop = QEventLoop()  # from PyQt5.QtCore import * : qtcore가 임포트되어야 함
        self.login_event_loop.exec_()

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

        ##### 계좌 번호, 종목코드, 익절/손절 타점 설정 #####
        self.interesting_codes = "MNQH24"  # 실투 (3월물 변경)

        ##### 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.early_au1 = []
        self.early_ad1 = []
        self.rsi_total_list = []

    ########## 로그인 관련 함수 ##########
    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", "", "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":

            self.tr_event_loop.exit()

            ########### rsi 구하기 ###########
            getrepeatcnt = self.kiwoom.dynamicCall("GetRepeatCnt(QString,QString)", trcode, recordname)
            self.getrepeatcnt = getrepeatcnt

            ########### 일목균형목 기준선 구하기 ###########
            btl.rsi_searching()
            time.sleep(0.1)

    ########## rsi 구하기 ###########
    def rsi_searching(self):
        for i in range(self.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.price_rsi_total_final = self.price_rsi_total[::-1]
        self.price_rsi_total = []

        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.close_price_rsi_change = []

        for i in self.close_price_third:  # 음수를 0으로 표시
            if i <= 0:
                i = 0
                self.early_au1.append(i)
            else:
                self.early_au1.append(i)

        for j in self.close_price_third:  # 양수를 0으로 표시
            if j >= 0:
                j = 0
                self.early_ad1.append(j)
            else:
                self.early_ad1.append(j)

        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)

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

        for k in range(1, self.getrepeatcnt - 1):
            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.rsi_total_list.append(self.current_rsi)

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

        self.price_rsi_total_final = []

        self.rsi_total_list = self.rsi_total_list[::-1]

        self.current_rsi_0 = self.rsi_total_list[0]  # 현재시간
        self.current_rsi_1 = self.rsi_total_list[1]  # 1분전
        self.current_rsi_2 = self.rsi_total_list[2]  # 2분전
        self.current_rsi_3 = self.rsi_total_list[3]  # 3분전
        self.current_rsi_4 = self.rsi_total_list[4]  # 4분전
        self.current_rsi_5 = self.rsi_total_list[5]  # 5분전

        # print(self.current_rsi_1)
        # print(self.current_rsi_0)
        #
        # print(len(self.rsi_total_list))
        # print(self.rsi_total_list)
        #
        # print(len(self.rsi_total_list[6:41]))       # 6분~40분
        # print(self.rsi_total_list[6:41])

        ###############################################################

        self.rsi_math_area_list = []
        self.rsi_math_area_list_70 = []
        self.rsi_math_area_list_30 = []

        self.rsi_math_area_list = self.rsi_total_list[6:41]
        self.rsi_math_area_list = self.rsi_math_area_list[::-1]
        # print(self.rsi_math_area_list)

        self.rsi_70_max_num = 70
        self.rsi_30_min_num = 30

        print("172줄")

        for i in range(len(self.rsi_math_area_list)-1):
            if self.rsi_math_area_list[i] < self.rsi_70_max_num and self.rsi_math_area_list[i+1] > self.rsi_70_max_num:
                rsi_math_area_var = (self.rsi_math_area_list[i+1]) * (self.rsi_math_area_list[i+1] - 70)/(70 - self.rsi_math_area_list[i] + self.rsi_math_area_list[i+1] - 70) / 2
                rsi_math_area_var = round(rsi_math_area_var, 2)
                self.rsi_math_area_list_70.append(rsi_math_area_var)

                """
                c = 70 - self.rsi_math_area_list[i]
                d = self.rsi_math_area_list[i+1] - 70
                
                c:d = (1-b):b
                cb = d-db
                cb + db = d
                b(c+d) = d
                b = d/(c+d)
                """

            elif self.rsi_math_area_list[i] > self.rsi_70_max_num and self.rsi_math_area_list[i+1] > self.rsi_70_max_num:
                rsi_math_area_var = (self.rsi_math_area_list[i] + self.rsi_math_area_list[i+1] - 140) * 1 / 2
                rsi_math_area_var = round(rsi_math_area_var, 2)
                self.rsi_math_area_list_70.append(rsi_math_area_var)

            elif self.rsi_math_area_list[i] > self.rsi_70_max_num and self.rsi_math_area_list[i+1] < self.rsi_70_max_num:
                rsi_math_area_var = (self.rsi_math_area_list[i] - 70) * (self.rsi_math_area_list[i] - 70) /(self.rsi_math_area_list[i] - 70 + 70 - self.rsi_math_area_list[i+1]) / 2
                rsi_math_area_var = round(rsi_math_area_var, 2)
                self.rsi_math_area_list_70.append(rsi_math_area_var)

                """
                c = self.rsi_math_area_list[i] - 70
                d = 70 - self.rsi_math_area_list[i+1]

                c:d = a:(1-a)
                da = c - ca
                (c+d)a = c
                a = c/(c+d)                
                """

            elif self.rsi_math_area_list[i] > self.rsi_30_min_num and self.rsi_math_area_list[i+1] < self.rsi_30_min_num:
                rsi_math_area_var = (30 - self.rsi_math_area_list[i+1]) * (30 - self.rsi_math_area_list[i+1]) /(self.rsi_math_area_list[i] - 30 + 30 - self.rsi_math_area_list[i+1]) / 2
                rsi_math_area_var = round(rsi_math_area_var, 2)
                self.rsi_math_area_list_30.append(rsi_math_area_var)

                """
                c = self.rsi_math_area_list[i] - 30
                d = 30 - self.rsi_math_area_list[i+1]

                c:d = (1-b):b
                bc = d - bd
                (c+d)/b = d
                b = d/(c+d)                
                """

            elif self.rsi_math_area_list[i] < self.rsi_30_min_num and self.rsi_math_area_list[i+1] < self.rsi_30_min_num:
                rsi_math_area_var = (60 - self.rsi_math_area_list[i] - self.rsi_math_area_list[i+1]) * 1 / 2
                rsi_math_area_var = round(rsi_math_area_var, 2)
                self.rsi_math_area_list_30.append(rsi_math_area_var)

            elif self.rsi_math_area_list[i] < self.rsi_30_min_num and self.rsi_math_area_list[i+1] > self.rsi_30_min_num:
                rsi_math_area_var = (30 - self.rsi_math_area_list[i]) * (30 - self.rsi_math_area_list[i])/(30 - self.rsi_math_area_list[i] + self.rsi_math_area_list[i+1] - 30) / 2
                rsi_math_area_var = round(rsi_math_area_var, 2)
                self.rsi_math_area_list_30.append(rsi_math_area_var)

                """
                c = 30 - self.rsi_math_area_list[i]
                d = self.rsi_math_area_list[i+1] - 30

                c:d = a:(1-a)
                da = c - ca
                (c+d)a = c
                a = c/(c+d)                
                """

        print("246줄")
        print(self.rsi_math_area_list_70)
        print(self.rsi_math_area_list_30)
        print("249줄")

        self.rsi_math_area_list_70_counting = len(self.rsi_math_area_list_70)
        self.rsi_math_area_list_70_result = sum(self.rsi_math_area_list_70)
        self.rsi_math_area_list_70_result = round(self.rsi_math_area_list_70_result, 2)

        self.rsi_math_area_list_30_counting = len(self.rsi_math_area_list_30)
        self.rsi_math_area_list_30_result = sum(self.rsi_math_area_list_30)
        self.rsi_math_area_list_30_result = round(self.rsi_math_area_list_30_result, 2)

        print("259줄")
        print(self.rsi_math_area_list_70_counting)
        print(self.rsi_math_area_list_70_result)

        print(self.rsi_math_area_list_30_counting)
        print(self.rsi_math_area_list_30_result)

        self.rsi_total_list = []
        self.rsi_math_area_list = []
        self.rsi_math_area_list_70 = []
        self.rsi_math_area_list_30 = []

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

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

    app.exec_()

 


5. 마치며

코드가 상당히 길어 이해하는 것이 약간은 어려워보일 수도 있다. 천천히 이해하고 직접 작성해보면, 금방 감을 잡을 수 있을 것으로 생각된다.

 

rsi 과매수 및 과매도 구간을 계산한다고 해서, 100% 수익을 보장하지는 않는다. 분명 과매수 구간인데도 불구하고, 하방으로 떨어지지도 않고 계속 횡보하다가 추가 상승을 하는 경우가 생각보다 많다. 즉, 위의 rsi 넓이를 구할수 있다고 해서, 과매수에서 short으로, 과매도 구간에서 long으로 들어가서 수익을 안정적으로 낼 수 없다.

 

사용자마다 나름대로의 훼이크패턴에 당하지 않도록 꾸준히 차트를 관찰할 필요가 있다. "rsi 넓이가 00~00의 사이에 있고, 횡보 상태에서 1분전 rsi가 00이라면 long으로 진입"하겠다는 본인만의 알고리즘 숫자 설정이 분명히 필요하다.

 

필자와 같이 rsi 과매수/과매도가 나오면 손이 근질근질하는 사람들이 많을 것이다. 보조지표만 믿고 진입하기에 요즘 그 변동성이 너무 심하다. 본인만의 매매스킬을 통해 안정적 수익을 달성할 수 있었으면 좋겠다.

 

 

반응형