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

(키움증권 해외선물 OpenAPI-W) MACD 값 구하기 (3) 파이썬에서 작성하기

봄이오네 2023. 8. 8. 08:10
반응형
1. 들어가며
2. 사전설명
   1) 종가데이터 
   2) MACD 모수는 100개 종가를 활용
   3) 지수이동평균
3. 코드 설명
4. 전체 코드
5. 마치며

 

1. 들어가며

지난 글에서는 MACD, 시그널, 오실레이터 등을 엑셀에서 구하는 방법을 알아보았다. 지수이동평균의 개념을 잘 이해하면 수월하게 엑셀함수를 작성할 수 있을 것이다.
 
이번 글에서는 MACD, 시그널, 오실레이터  등을 파이참을 통해 파이썬에서 작성하는 방법을 설명할 예정이다. 로그인 등 중복되는 내용은 생략하도록 한다.


2. 사전설명

1) 종가데이터 

MACD를 구하기 위해서는 종가데이터가 필요하다. 키움증권의 OpenAPI-W에서 제공하는 종가데이터를 활용하여 MACD 등을 구한다. 여기서 이용할 TR은 opc10002이며, 여기에서는 현재가(종가)를 활용한다.
 

그림1. opc10002를 통해 현재가(종가)를 받는다.

 

2) MACD 모수는 100개 종가를 활용

MACD를 구하기 위해 필요한 종가 100개로 받아온다. 지난 글에서 확인하였듯이 48~50개 정도로 충분하긴 하나, 안정적으로 100개의 종가를 활용한다. (현재 ~ 현재-100분)
 

3) 지수이동평균

지수이동평균은 아래와 같이 가중치를 미리 설정해둔다.

  • 12일 지수이동평균 : 2 / (12+1)
  • 26일 지수이동평균 : 2 / (26+1)
  • 9일 지수이동평균   : 2 / (9+1)

3. 코드 설명

opc10002를 통해 종가 데이터 100개를 받아오고, 12일 지수이동평균(ema_12)와 26일 지수이동평균(ema_26)을 구한다. 그 이후 macd 및 시그널, 오실레이터를 구해주면 된다.
 
전체 코드는 4번(전체 코드)에서 확인 가능하자.
반복 활용되는 모듈 및 로그인 설명(1줄~22줄, 29줄~34줄)의 설명은 생략한다.
 

그림2-1. 로그인 및 macd 변수 초기화 선언

 
1줄~22줄 : 로그인 관련 설명이며, 여기에서는 생략한다.
20줄~26줄 : macd를 구하기 위한 변수 선언 및 초기화 한다.
20줄 : 52줄 및 59줄의 for문의 range에서 활용하기 위해  self.macd_minute_bong_counting를 100으로 선언한다.
21줄 : 53줄에서 받아온 종가데이터를 담을 self.price_macd_total 리스트를 선언한다.
22줄 : 82줄에서 구한 macd(12일 지수이동평균 - 26일 지수이동평균)을 83줄에서 담을 수 있는 리스트를 선언한다.
24줄~26일 : 12일/26일/9일 지수이동평균의 가중치를 미리 선언해준다.
 

그림2-2. 키움서버에 데이터 입력/요청/수신받는 화면

 
37줄 : 168줄에서 실행하기 위해 37줄에서 함수를 정의한다.
38줄~39줄 : 종목코드 및 시간(분봉 종류)를 입력하기 위해 SetInputValue를 작성하여 데이터를 입력할 수 있도록 해준다.
40줄 : CommRqData 함수를 통해 38~39줄에서 입력받은 내용을 키움서버에 요청한다.
 
44줄~49줄 : 37줄~42줄에서 데이터 입력/요청받은 데이터를 수신받기 위해 작성하는 44줄~49줄이다.
45줄 : 40줄에서 키움서버에 요청한 데이터가 45줄의 opc10002이면,
47줄 : 51줄의 macd_searching 함수를 실행하라.
48줄 : 51줄~98줄의 함수 실행이 완료되면, 다음 코드를 실행하라.
 
51줄~68줄 : 12일/26일/9일의 지수이동평균을 이용하여 macd, 시그널, 오실레이터를 구한다.
52줄 : for문의 range 내에는 20줄에서 선언한 self.macd_minute_bong_counting = 100을 설정하여, 키움서버에서 받아온 데이터 중 종가 100개를 요청한다. (현재 ~ 현재-100분)
53줄 : GetCommData를 활용하여 38~40줄에서 키움서버에 데이터 입력/요청한 데이터를 수신받는다.
54줄 : 53줄에서 받아온 종가데이터(100개)를 21줄에서 선언한 self.price_macd_total 리스트에 담는다.
56줄 : 54줄의 리스트를 역순으로 재배정한다. 이유는? 54줄에서 받아온 데이터는 현재 → 과거로 받아오는데, 이것을 과거 → 현재의 순서로 바꾸어주기 위해서이다. 즉, 54줄의 리스트 내에는 [최근종가, 최근종가-1분, 최근종가-2분....]이다. 54줄에서 [::-1]을 통해 [..., 최근종가-2, 최근종가-1, 최근종가]로 바꾸어준다.
 
64줄 : 이 글의 첫번째 핵심이다. 100개의 종가 중 최초값은 초기값 그대로 적는다. 2번째 값부터 가중치(24줄 12일 지수평균, 2/13)를 적용한다. 최초값 및 두번째값이 있다면, 64줄에서 두번째값(최근값)에는 가중치를 곱하고, 최초값에는 (1-가중치)를 곱한다.
64줄 : 12일 지수이동평균으로 나온 값을 self.ema_12_second를 넣어주어서, 71줄에서 활용한다.
 
67줄 : 26일 지수이동평균을 구한다. 가중치(25줄 26일 지수평균, 2/27)를 적용한다. 최초값 및 두번째값이 있다면, 67줄에서 두번째값(최근값)에는 가중치를 곱하고, 최초값에는 (1-가중치)를 곱한다.
68줄 : 26일 지수이동평균으로 나온 값을 self.ema_26_second를 넣어주어서, 76줄에서 활용한다.
 

그림2-3. 12일 및 26일 지수이동평균을 이용하여 macd를 구한다.

 
71줄~74줄 : 12일 지수이동평균을 구하는 과정이다.
71줄 : 65줄에서 선언한 self.ema_12_second을 self.ema_12_third에 넣어준다.
72줄 : self.ema_12_forth에 56줄에서 선언한 self.price_macd_total_final의 2번째에 있는 숫자를 너는다.
73줄 : 지수가중평균을 이용하여 2번째 수의 12일 지수이동평균을 구한다.
74줄 : 2번째 수에서 계산된 지수이동평균(self.ema_12_final)을 self.ema_12_second에 넣어 71줄에서 다시활용한다.
 
76줄~79줄 : 26일 지수이동평균을 구하는 과정이며, 71줄~74줄과 설명(12일)이 유사하여 26일의 지수이동평균을 구하는 설명은 생략하도록 한다.
 
82줄 : 이 글의 두번째 핵심이다. macd는 12일에서 26일 지수이동평균을 차감한 값이다.
83줄 : 109줄과 114줄에서 시그널을 구하기 위해, 82줄에서 구한 macd를 22줄의 self.macd_12_26_total에 넣는다.
 
85줄~98줄 : 20줄에서 선언한 self.macd_minute_bong_counting는 100(int 형이며)이다.
85줄~94줄 : self.macd_minute_bong_counting(100) - 7은 93이다. 예를 들어 100에 해당하는 시간이 10:17분이면, 93이면 10:11분(현재시간 대비 6분전을 가르킨다)에 해당한다. 필자의 개인취향이다. 필자는 -6분 ~ -2분까지의 macd 등을 활용할 예정이라 85줄~94줄이며, 개인 취향에 따라 85줄~94줄은 삭제해 주어도 된다.
 
95줄 : 1분 전의 macd를 나타낸다.
97줄 : 현재 시간의 macd를 나타낸다.
 

그림2-4. signal을 구한다.

 
101줄 : 83줄에서 리스트에 추가한 macd의 갯수(len)를 구한다. 여기에서 주의할 것은 20줄 self.macd_minute_bong_counting는 100인데, 101줄의 len(self.macd_12_26_total)은 98이다. 이유는? 83줄에서는 i >=2인 경우에만 추가(2~99)하였다. 즉 60줄 i==0 과 i==1인 경우는 83줄의 리스트에 추가(append)하지 않았다.
추가하지 않은 이유? 98개의 데이터로도 시그널(signal)을 구할 수 있기 때문이다.
 
104줄~133줄 : macd의 시그널을 구하기 위한 코드이다.
104줄 : 101줄의 len(self.macd_12_26_total)이 98이며, for i in range(98)을 의미한다.
105줄~110줄 : 82줄 self.macd_12_26_total에 들어있는 첫번째, 두번째 macd의 9일간 지수이동평균을 구한다.
112줄~133줄 : 6분전, 5분전, 4분전, 3분전, 2분전, 1분전, 현재의 7개 시그널을 구한다. (6분전~2분전까지는 필자의 알고리즘 설정을 위해 정한 숫자이다.)
 ※ 개인 취향에 따라 1분전, 현재의 시그널만 필요한 경우, 120줄~129줄은 삭제하여도 시스템 작동에 영향이 없다.
 

그림2-5. 오실레이터를 구하는 화면

 
135줄~137줄 : 계속 사용할 것이기 때문에 리스트를 초기화한다.
135줄 : 54줄에서 키움증권에서 받아온 종가 리스트를 초기화한다.
136줄 : 55줄에서 54줄의 내림차순(현재→과거)인 종가데이터를 오름차순(과거→현재) 데이터로 변환한 리스트를 초기화한다.
137줄 : 83줄에서 추가한 macd의 리스트를 초기화한다.

 

140줄~146줄 : 오실레이터(=macd-시그널)을 구한다.

148줄~150줄 : 3가지(2분전, 1분전, 현재) 오실레이터를 round함수를 활용하여 소수 셋째자리에서 반올림한다.

152줄~162줄 : 3가지(2분전, 1분전, 현재) macd, 시그널, 오실레이터를 출력한다.

168줄 : 37줄(def rq_data_opc10002) 함수를 실행한다.
 
※ 초기화 이유? 초기화하지 않으면 기존데이터가 지워지지 않고, 계속 누적되어 macd나 시그널 등에 왜곡된 값이 반환된다.


4. 전체 코드

전체코드는 아래와 같다. 지수이동평균이 산출되는 과정을 이해한다면, 순조롭게 코드 작성을 할 수 있을 것이다.
아래 내용은 2023년 8월4일(금) 20:00 ~ 8월5일(토) 05:59분의 데이터를 기반으로 하였다. 출력되는 데이터는 8월5일(토) 05:57분, 05:58분, 05:59분의 macd, 시그널, 오실레이터를 각각 나타낸다.
 

그림3. 2023년 8월5일(토) 05:57~05:59분의 macd, 시그널, 오실레이터 현황

 

해외선물(나스닥) macd 구하기.xlsx
0.18MB

 파일1 : 해외선물(나스닥) 시가~종가 데이터(2023.8.4~8.5)
 

더보기
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_()

        ##### macd의 현재 1분 데이터의 au/ad 구하기 #####
        self.macd_minute_bong_couning = 100  # macd를 구하기 위해 받아올 일봉 갯수
        self.price_macd_total = []  # 56~86줄 : 1분봉들의 종가데이터를 담는 리스트 모음
        self.macd_12_26_total = []  # macd 시그널을 구하기 위한 리스트 선언

        self.ema_var_12 = 2 / (12 + 1)
        self.ema_var_26 = 2 / (26 + 1)
        self.signal_var_9 = 2 / (9 + 1)

    ########## 로그인 관련 함수 ##########
    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 trdata_get(self, scrno, rqname, trcode, recordname, prenext):
        if rqname == "opc_10002":

            btl.macd_searching()

            self.tr_event_loop.exit()

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

        self.price_macd_total_final = self.price_macd_total[::-1]

        ### ema_12, ema_26 지수이동평균 구하기 ###
        for i in range(self.macd_minute_bong_couning):
            if i == 0:
                pass

            elif i == 1:
                self.ema_12_first = (self.price_macd_total_final[1] * self.ema_var_12) + (self.price_macd_total_final[0] * (1 - self.ema_var_12))
                self.ema_12_second = self.ema_12_first

                self.ema_26_first = (self.price_macd_total_final[1] * self.ema_var_26) + (self.price_macd_total_final[0] * (1 - self.ema_var_26))
                self.ema_26_second = self.ema_26_first

            elif i >= 2:
                self.ema_12_third = self.ema_12_second
                self.ema_12_forth = self.price_macd_total_final[i]
                self.ema_12_final = (self.ema_12_forth * self.ema_var_12) + (self.ema_12_third * (1 - self.ema_var_12))
                self.ema_12_second = self.ema_12_final

                self.ema_26_third = self.ema_26_second
                self.ema_26_forth = self.price_macd_total_final[i]
                self.ema_26_final = (self.ema_26_forth * self.ema_var_26) + (self.ema_26_third * (1 - self.ema_var_26))
                self.ema_26_second = self.ema_26_final

                # self.macd_12_26 = round(self.ema_12_final - self.ema_26_final, 2)   # 소수 셋째 자리에서 반올림 (소수 둘째 자리까지 출력)
                self.macd_12_26 = self.ema_12_final - self.ema_26_final           # 반올림없이 값 출력
                self.macd_12_26_total.append(self.macd_12_26)

                if i == self.macd_minute_bong_couning - 7:
                    self.macd_12_26_6_mintue_ago = self.macd_12_26
                elif i == self.macd_minute_bong_couning - 6:
                    self.macd_12_26_5_mintue_ago = self.macd_12_26
                elif i == self.macd_minute_bong_couning - 5:
                    self.macd_12_26_4_mintue_ago = self.macd_12_26
                elif i == self.macd_minute_bong_couning - 4:
                    self.macd_12_26_3_mintue_ago = self.macd_12_26
                elif i == self.macd_minute_bong_couning - 3:
                    self.macd_12_26_2_mintue_ago = self.macd_12_26
                elif i == self.macd_minute_bong_couning - 2:
                    self.macd_12_26_1_mintue_ago = self.macd_12_26
                elif i == self.macd_minute_bong_couning - 1:
                    self.macd_12_26_current = self.macd_12_26

        ### macd_signal 구하기 ###
        self.macd_12_26_counting = len(self.macd_12_26_total)
        print(self.macd_12_26_counting)

        for i in range(self.macd_12_26_counting):
            if i == 0:
                pass

            if i == 1:
                self.macd_signal_first = (self.macd_12_26_total[1] * self.signal_var_9) + (self.macd_12_26_total[0] * (1 - self.signal_var_9))
                self.macd_signal_second = self.macd_signal_first

            elif i >= 2:
                self.macd_signal_third = self.macd_signal_second
                self.macd_signal_forth = self.macd_12_26_total[i]
                self.macd_signal_final = (self.macd_signal_forth * self.signal_var_9) + (self.macd_signal_third * (1 - self.signal_var_9))
                # self.macd_signal_final = round(self.macd_signal_final, 2)    # 소수 셋째 자리에서 반올림 (소수 둘째 자리까지 출력)

                self.macd_signal_second = self.macd_signal_final

                if i == self.macd_12_26_counting - 7:
                    self.macd_signal_final_6_mintue_ago = self.macd_signal_final
                elif i == self.macd_12_26_counting - 6:
                    self.macd_signal_final_5_mintue_ago = self.macd_signal_final
                elif i == self.macd_12_26_counting - 5:
                    self.macd_signal_final_4_mintue_ago = self.macd_signal_final
                elif i == self.macd_12_26_counting - 4:
                    self.macd_signal_final_3_mintue_ago = self.macd_signal_final
                elif i == self.macd_12_26_counting - 3:
                    self.macd_signal_final_2_mintue_ago = self.macd_signal_final
                elif i == self.macd_12_26_counting - 2:
                    self.macd_signal_final_1_mintue_ago = self.macd_signal_final
                elif i == self.macd_12_26_counting - 1:
                    self.macd_signal_final_current = self.macd_signal_final

        self.price_macd_total = []
        self.price_macd_total_final = []
        self.macd_12_26_total = []

        ### macd_oscillators 구하기 ###
        self.macd_Oscillator_6_mintue_ago = self.macd_12_26_6_mintue_ago - self.macd_signal_final_6_mintue_ago
        self.macd_Oscillator_5_mintue_ago = self.macd_12_26_5_mintue_ago - self.macd_signal_final_5_mintue_ago
        self.macd_Oscillator_4_mintue_ago = self.macd_12_26_4_mintue_ago - self.macd_signal_final_4_mintue_ago
        self.macd_Oscillator_3_mintue_ago = self.macd_12_26_3_mintue_ago - self.macd_signal_final_3_mintue_ago
        self.macd_Oscillator_2_mintue_ago = self.macd_12_26_2_mintue_ago - self.macd_signal_final_2_mintue_ago
        self.macd_Oscillator_1_mintue_ago = self.macd_12_26_1_mintue_ago - self.macd_signal_final_1_mintue_ago
        self.macd_Oscillator_final_current = self.macd_12_26_current - self.macd_signal_final

        self.macd_Oscillator_2_mintue_ago = round(self.macd_12_26_2_mintue_ago - self.macd_signal_final_2_mintue_ago, 2)
        self.macd_Oscillator_1_mintue_ago = round(self.macd_12_26_1_mintue_ago - self.macd_signal_final_1_mintue_ago, 2)
        self.macd_Oscillator_final_current = round(self.macd_12_26_current - self.macd_signal_final, 2)

        print(self.macd_12_26_2_mintue_ago)
        print(self.macd_signal_final_2_mintue_ago)
        print(self.macd_Oscillator_2_mintue_ago)
        print("\n")
        print(self.macd_12_26_1_mintue_ago)
        print(self.macd_signal_final_1_mintue_ago)
        print(self.macd_Oscillator_1_mintue_ago)
        print("\n")
        print(self.macd_12_26_current)
        print(self.macd_signal_final_current)
        print(self.macd_Oscillator_final_current)

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

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

    app.exec_()
    

### (expected result) ###
# 98
# -0.29036454962079006
# -0.02768247516922922
# -0.26

# -0.39690105689260236
# -0.10152619151390385
# -0.3

# -0.4559037214858108
# -0.17240169750828527
# -0.28

 


5. 마치며

3번의 글을 통해, macd에 대해 알아보았다. 한 동안 코딩에서 손을 놓아서인지, 글 작성을 위해 상당한 시간이 소요되었다. 7월 한달은 코딩보다는 마음을 다잡는데 시간을 대부분 보낸것 같다. 8월에는 macd 작성을 통해 자동매매를 돌려볼 예정이다.
 
7월의 나스닥 매매는 너무 어려웠다. 미국기업의 실적발표와 맞물려 연일 신고가를 찍었고, 언제 하방으로 떨어져도 이상하지 않을 정도의 분위기였다. 미국 신용등급이 하락한 8.3(목)을 기점으로 하루 200~300p가 떨어지니, 겁이 나서 진입을 유보하게 된다.
 
macd 지표를 활용하여 3번 진입할 것을 1번 정도로 진입자제했던 것 같다. macd와 시그널의 크로스 지점이 예상되었다고 판단하여 무모하게 진입하지 않았다. 수익이 적게 나더라도, 크로스 지점을 돌파한 것을 확인하고 진입하니 약간 마음은 편하긴 했다.
 
꾸준히 수익을 낼 수는 없다. 다만, 투자할 때 마음편한 매매가 될 수 있도록 하자!
 

반응형