Home MACD 지표의 python 코드 작성 (Pandas library and Dictionary)
Post
Cancel

MACD 지표의 python 코드 작성 (by Pandas library)

Pandas 설치

pandas library를 사용하려면 당연하게도 pandas library를 설치 해야한다.
아래와 같이 나의 환경관리 툴에 맞게 설치를 해준다.

1
2
3
4
5
# PyPi (python package index 환경)
> pip install pandas

# Conda (anaconda or miniconda 환경)
> conda install pandas

Data 준비

OHLCV

MACD를 계산할 OHLCV (Open, High, Low, Close, Volume)의 Data가 필요하다.
요즘은 각종 증권사이트나 암호화폐 사이트에서 API를 이용하거나 크롤링을 하는 방식으로
내가 원하는 종목의 데이터를 가져올 수 있다.
하지만, 여기선 가상의 데이터를 만들어서 MACD 지표를 계산하려고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import datetime
import random

random.seed(0)  # 코드를 실행할 때마다 생성되는 Random 숫자가 동일하게 해주기

def get_random_price_dict(tot_num, start_datetime=None):
    """tot_num개의 OHLCV data list 생성"""
    if start_datetime is None:
        start_datetime = datetime.datetime.now()
    ohlcv_dict_list = []  # OHLCV Data의 Dictionary를 List에 append.
    for num in range(tot_num):
        dict_temp = {}
        dict_temp['Date'] = (datetime.datetime(
                            year=start_datetime.year, 
                            month=start_datetime.month, 
                            day=start_datetime.day,
                            hour=start_datetime.hour+(num // 24),
                            minute=0+(num % 24), second=0, microsecond=0
                            ).strftime('%Y-%m-%d %H:%M:%S'))
        dict_temp['Open'] = random.randint(110, 115)
        dict_temp['High'] = random.randint(110, 115)
        dict_temp['Low'] = random.randint(110, 115)
        dict_temp['Close'] = random.randint(110, 115)
        dict_temp['Volume'] = random.randint(2100, 4000)
        ohlcv_dict_list.append(dict_temp)
    return ohlcv_dict_list

ohlcv_dict_list = get_random_price_dict(100)
print(ohlcv_dict_list)
1
2
[{'Date': '2022-08-24 00:00:00', 'Open': 113, 'High': 113, 'Low': 110, 'Close': 112, 'Volume': 3147}, {'Date': '2022-08-24 00:01:00', 'Open': 113, 'High': 113, 'Low':112, 'Close': 113, 'Volume': 2833}, {'Date': '2022-08-24 00:02:00', 'Open': 114, 'High': 111, 'Low': 114, 'Close': 111, 'Volume': 2677}, {'Date': '2022-08-24 00:03:00','Open': 111, 'High': 110, 'Low': 114, 'Close': 112, 'Volume': 3963}, {'Date': '2022-08-24 00:04:00', 'Open': 114, 'High': 115, 'Low': 114, 'Close': 111, 'Volume':
2735}, {'Date': '2022-08-24 00:05:00', 'Open': 110, 'High': 115, 'Low': 110, 'Close':115, 'Volume': 2776}, {'Date': '2022-08-24 00:06:00', 'Open': 113, 'High': 114, 'Low':110, 'Close': 112, 'Volume': 2989}, {'Date': '2022-08-24 00:07:00', 'Open': 112, 'High': 114,

Random으로 만들어진 OHLCV Ditionary List가 생성 되었다.
아니 근데 이게 뭐가 뭔지 지저분해서 보기가 너무 힘들다.
pprint 사용해서 보기 좋게 출력해보자.

1
2
3
4
from pprint import pprint
...
...
pprint(ohlcv_dict_list)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[{'Close': 112,
  'Date': '2022-08-24 00:00:00',
  'High': 113,
  'Low': 110,
  'Open': 113,
  'Volume': 3147},
 {'Close': 113,
  'Date': '2022-08-24 00:01:00',
  'High': 113,
  'Low': 112,
  'Open': 113,
  'Volume': 2833},
  ...
  ...
  ]

이제 보기 좋게 출력이 되었고 OHLCV Data가 구조에 맞게 제대로 생성되었는지 확인이 가능하고,
random.seed(임의의 수)를 사용하여 여러번 반복 실행을 해도 동일한 Data가 생성되는 것을 확인 할 수 있다.

OHLCV Pandas

OHLCV를 Dictionary 형태로 만들었으니 이제 pandas의 Dataframe 객체로 변환을 해 보자.

1
2
3
4
import pandas as pd

df = pd.DataFrame.from_dict(ohlcv_dict_list)
print(df)
1
2
3
4
5
6
7
8
9
10
11
12
13
                Date       Open   High    Low   Close  Volume
0    2022-08-28 12:00:00    113    113    110    112    3147
1    2022-08-28 12:01:00    113    113    112    113    2833
2    2022-08-28 12:02:00    114    111    114    111    2677
3    2022-08-28 12:03:00    111    110    114    112    3963
4    2022-08-28 12:04:00    114    115    114    111    2735
...    ...    ...    ...    ...    ...    ...
95    2022-08-28 15:23:00    115    111    110    113    2116
96    2022-08-28 16:00:00    110    113    114    114    2693
97    2022-08-28 16:01:00    113    113    114    115    3491
98    2022-08-28 16:02:00    111    113    110    112    2550
99    2022-08-28 16:03:00    112    114    111    113    2493
100 rows × 6 columns

Pandas의 Dataframe 객체로 Data의 변환이 잘 된 것을 볼 수 있다.
그런데 Index가 0~99로 숫자이다.
내가 원하는 것은 Index가 Date 인 객체이므로 set_index함수를 통해 Index를 바꿔보도록 하자.

1
2
df.set_index('Date', inplace=True)
print(df)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
                      Open    High   Low   Close  Volume
Date                    
2022-08-28 12:00:00    113    113    110    112    3147
2022-08-28 12:01:00    113    113    112    113    2833
2022-08-28 12:02:00    114    111    114    111    2677
2022-08-28 12:03:00    111    110    114    112    3963
2022-08-28 12:04:00    114    115    114    111    2735
...    ...    ...    ...    ...    ...
2022-08-28 15:23:00    115    111    110    113    2116
2022-08-28 16:00:00    110    113    114    114    2693
2022-08-28 16:01:00    113    113    114    115    3491
2022-08-28 16:02:00    111    113    110    112    2550
2022-08-28 16:03:00    112    114    111    113    2493
100 rows × 5 columns

Index가 Date로 잘 변환 된 것을 확인 할 수 있다.

※여기서 중요한점은 Pandas Dataframe 객체를 한번에 생성해야 한다는 것이다.
Pandas는 C언어로 작성되어 사용하여 Data Science에서 필요한 대량의 계산을 빠르게 처리해 주는데
객체 생성을 할 때에는 어쩔수 없이 상대적으로 큰 시간이 소요된다.
따라서 만일, 위에서 Dictionary List를 만들때 한개의 Data마다 Dataframe 객체를 생성하게 되면,
큰 Data를 다룰 때 매우 느린 것을 경험 할 수 있을 것이다.
Pandas의 장점을 살릴 수가 없게 되는 것이다.

MACD 함수 by pandas

OHLCV Data의 준비가 되었으니 이번에는 MACD 함수를 만들어 보도록 하자.

1
2
3
4
5
6
7
8
def get_macd(df, sets):
    df["EMA_short"] = df["Close"].ewm(span=sets["short"], adjust=True).mean()
    df["EMA_long"] = df["Close"].ewm(span=sets["long"], adjust=True).mean()
    df["MACD"] = df["EMA_short"] - df["EMA_long"]
    df["MACD_sig"] = df["MACD"].ewm(span=sets["signal"], adjust=True).mean()
    df["MACD_bool"] = df["MACD"] > df["MACD_sig"]
    df.drop(columns = ["EMA_short", "EMA_long"], inplace=True)
    return df

get_macd 함수를 만들었으니 아까 만들어진 Data로 MACD계산을 해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import datetime
import random
import pandas as pd

random.seed(0)  # 코드를 실행할 때마다 생성되는 Random 숫자가 동일하게 해주기

def get_random_price_dict(tot_num, start_datetime=None):
    """tot_num개의 OHLCV data list 생성"""
    if start_datetime is None:
        start_datetime = datetime.datetime.now()
    ohlcv_dict_list = []  # OHLCV Data의 Dictionary를 List에 append.
    for num in range(tot_num):
        dict_temp = {}
        dict_temp['Date'] = (datetime.datetime(
                            year=start_datetime.year, 
                            month=start_datetime.month, 
                            day=start_datetime.day + (num // 24),
                            minute=0+(num % 24), second=0, microsecond=0
                            ).strftime('%Y-%m-%d %H:%M:%S'))
        dict_temp['Open'] = random.randint(110, 115)
        dict_temp['High'] = random.randint(110, 115)
        dict_temp['Low'] = random.randint(110, 115)
        dict_temp['Close'] = random.randint(110, 115)
        dict_temp['Volume'] = random.randint(2100, 4000)
        ohlcv_dict_list.append(dict_temp)
    return ohlcv_dict_list

def get_macd(df, sets):
    df["EMA_short"] = df["Close"].ewm(span=sets["short"], adjust=True).mean()
    df["EMA_long"] = df["Close"].ewm(span=sets["long"], adjust=True).mean()
    df["MACD"] = df["EMA_short"] - df["EMA_long"]
    df["MACD_sig"] = df["MACD"].ewm(span=sets["signal"], adjust=True).mean()
    df["MACD_bool"] = df["MACD"] > df["MACD_sig"]
    df.drop(columns = ["Open", "High", "Low", "Volume", 
                       "EMA_short", "EMA_long"], inplace=True)
                       # 결과의 가독성을 위해 불필요한 부분 삭제
    return df

ohlcv_dict_list = get_random_price_dict(100)
df = pd.DataFrame.from_dict(ohlcv_dict_list)
df.set_index('Date', inplace=True)
macd_set = {"short": 12, "long": 26, "signal": 9}
df = get_macd(df_test, macd_set)
print(df)
1
2
3
4
5
6
7
8
9
10
11
12
13
                      Close    MACD    MACD_sig    MACD_bool
Date                
2022-08-28 12:00:00    112    0.000000    0.000000    False
2022-08-28 12:01:00    113    0.022436    0.012464    True
2022-08-28 12:02:00    111    -0.033432    -0.006346    False
2022-08-28 12:03:00    112    -0.021918    -0.011621    False
2022-08-28 12:04:00    111    -0.054992    -0.024523    False
...    ...    ...    ...    ...
2022-08-28 15:23:00    113    -0.024678    0.008761    False
2022-08-28 16:00:00    114    0.141292    0.035267    True
2022-08-28 16:01:00    115    0.349447    0.098103    True
2022-08-28 16:02:00    112    0.269341    0.132351    True
2022-08-28 16:03:00    113    0.283249    0.162530    True

MACD 계산 결과가 잘 출력되는 것을 볼 수 있다.
(여기서 True는 매수, False는 매도이다.)
여기까지는 Pandas Library를 활용하여 대량의 OHLCV Data의 MACD를 일괄계산하는 방법에 대해 알아보았다.
다음으로는 MACD계산을 python standard 객체인 Dictionary로 계산하는 방법에 대해서 알아보겠다.


MACD 지표의 python 코드 작성 (by Dictionary)

실시간으로 생성된 OHLCV 차트에 Dictionary로 지표계산을 하는 이유

앞에서 pandas DataFrame으로 계산한 MACD지표는 과거의 수 많은 Data table에서 한꺼번에 MACD지표를 계산하는 방법이다.
과거의 방대한 Data로 내 거래 기준을 테스트하는 Back Testing과 같은 대량의 계산에는 C언어 기반으로 작성된 pandas DataFrame을 사용하는 것이 가장 빠르고 효율적이다.
다만, 실제 거래를 위해서 실시간으로 Data를 받아서 봉을 만들고 지표계산을 한 개씩 하는 경우엔 이야기가 달라지게 된다. pandas DataFrame의 경우 Data를 DataFrame 객체로 변환을 해줘야 하는데 이 과정이 상대적으로 매우 느린 과정이다.

실시간으로 생성되는 OHLCV Data로 지표 계산Dictionary로 계산하는 것pandas 계산보다(객체생성 과정 때문에) 월등히 (약 100배 가량) 빠른데자세한 내용 및 속도비교는 아래의 링크를 확인하면 확실히 알 수 있을 것이다.

실시간으로 생성되는 차트의 MACD 지표계산 속도 비교: https://pioneergu.github.io/posts/macd-pandas-vs-dict/

실시간 MACD 지표 모의 계산

앞서 계산과 마찬가지로 random library를 사용하여 실시간 MACD 지표 계산을 모의로 계산해보겠다.
앞에서 작성한 Random Price Dict를 생성하는 Loop 계산에 OHLCV Data dictionary가 생성되는 대로 MACD 지표를 계산하는 방식으로 진행 할 것이다.
우선 Dictionary에서 MACD 지표를 한줄 한줄 계산하는 함수를 짜 볼 것인데, Exponential Weighted Moving Avarage 계산 특성상 과거 값이 계속 보정이 되기때문에 (아래의 수식 참조) Coroutine을 써야 한다.

macd-code1

<출처 Pandas Docs: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html)

근데 이 Coroutine을 함수 내부에서 불러오려 하는데 함수를 빠져나갈때 Coroutine 객체가 garbage collection 되지 않도록 하기 위해 편의를 위해서 코드를 Class로 작성 하도록 하겠다. (Closure를 사용해도 된다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import random
import datetime

random.seed(0)  # 코드를 실행할 때마다 생성되는 Random 숫자가 동일하게 해주기

class Indicators:
    def __init__(self, tot_num):
        """
        tot_num개의 OHLCV and MACD dict list 생성
        """
        self.tot_num = tot_num
        self.coro_adj_ewm = {}  # Coroutine 객체를 저장할 Dictionary

    def get_ohlcv_macd_dict(self, sets, adjust=False, start_datetime=None):
        if start_datetime is None:
            start_datetime = datetime.datetime.now()
        ohlcv_dict_list = []  # OHLCV Data의 Dictionary를 List에 append.
        for num in range(self.tot_num):
            dict_temp = {}
            dict_temp['Date'] = (datetime.datetime(
                                year=start_datetime.year, 
                                month=start_datetime.month, 
                                day=start_datetime.day,
                                hour=(num // 24),
                                minute=(num % 24), second=0, microsecond=0
                                ).strftime('%Y-%m-%d %H:%M:%S'))
            dict_temp['Open'] = random.randint(110, 115)
            dict_temp['High'] = random.randint(110, 115)
            dict_temp['Low'] = random.randint(110, 115)
            dict_temp['Close'] = random.randint(110, 115)
            dict_temp['Volume'] = random.randint(2100, 4000)
            ohlcv_dict_list.append(dict_temp)
            ohlcv_dict_list = self.append_macd_dict(ohlcv_dict_list,
                                                    sets, adjust=adjust)
        df = pd.DataFrame.from_dict(ohlcv_dict_list)
        # 보기 편하게 마지막에 df로 변환
        df.set_index('Date', inplace=True)
        df.drop(columns = ["Open", "High", "Low", "Volume", 
                           "EMA_short", "EMA_long"], inplace=True)
        return df

    def append_macd_dict(self, dict_list, sets, adjust):
        dict_list[-1]["EMA_short"] = self.get_ewm(dict_list, 
                                                  close_key="Close", 
                                                  span_key="short", 
                                                  span=sets["short"], 
                                                  adjust=adjust)
        dict_list[-1]["EMA_long"] = self.get_ewm(dict_list, 
                                                 close_key="Close", 
                                                 span_key="long", 
                                                 span=sets["long"], 
                                                 adjust=adjust)
        dict_list[-1]["MACD"] = (dict_list[-1]["EMA_short"] 
                                - dict_list[-1]["EMA_long"])
        dict_list[-1]["MACD_sig"] = self.get_ewm(dict_list, 
                                                 close_key="MACD", 
                                                 span_key="signal", 
                                                 span=sets["signal"], 
                                                 adjust=adjust)
        dict_list[-1]["MACD_bool"] = (dict_list[-1]["MACD"] 
                                    > dict_list[-1]["MACD_sig"])
        return dict_list

    def get_ewm(self, 
                dict_list: list, 
                close_key: str="Close", 
                com: float=None, 
                span_key: str=None,
                span: float=None, 
                adjust: bool=False) -> float:
        if not any([com, span]):
            raise KeyError("One of com and span must be provided.")
        if com:
            if com < 0:
                raise ValueError("com >= 0")
            else:
                alpha = 1/(1+com)
        elif span:
            if span < 1:
                raise ValueError("span >= 1")
            else:
                alpha = 2/(span+1)
        if adjust:
            if len(dict_list) == 1:
                self.coro_adj_ewm[span_key] = self.adjust_ewm(alpha)
                next(self.coro_adj_ewm[span_key])
                return (self.coro_adj_ewm[span_key]
                        .send(dict_list[-1][close_key]))
            else:
                return (self.coro_adj_ewm[span_key]
                        .send(dict_list[-1][close_key]))

        else:
            if len(dict_list) == 1:
                return dict_list[-1][close_key]
            else:
                return ((1-alpha) * dict_list[-2]
                        + alpha * dict_list[-1][close_key])

    def adjust_ewm(self, alpha):
        numerator = 0
        denominator = 1
        count = 0
        while True:
            close = yield numerator/denominator
            if count == 0:
                denominator = 0
            numerator = numerator * (1-alpha) + close
            denominator += (1-alpha) ** count
            count += 1

indicator = Indicators(100)
macd_set = {"short": 12, "long": 26, "signal": 9}
df = indicator.get_ohlcv_macd_dict(sets=macd_set, adjust=True)
print(df)

결과의 가독성을 좋게 하기 위해서 Dictionary로 계산을 다 한 후에 마지막에 일괄로 df로 변환하는 과정만 추가해 주었다.
아래의 결과를 보면 pandas library로 계산한 결과와 동일함을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                      Close   MACD        MACD_sig    MACD_bool
Date                
2022-08-29 00:00:00    112    0.000000    0.000000    False
2022-08-29 00:01:00    113    0.022436    0.012464    True
2022-08-29 00:02:00    111    -0.033432    -0.006346    False
2022-08-29 00:03:00    112    -0.021918    -0.011621    False
2022-08-29 00:04:00    111    -0.054992    -0.024523    False
...    ...    ...    ...    ...
2022-08-29 03:23:00    113    -0.024678    0.008761    False
2022-08-29 04:00:00    114    0.141292    0.035267    True
2022-08-29 04:01:00    115    0.349447    0.098103    True
2022-08-29 04:02:00    112    0.269341    0.132351    True
2022-08-29 04:03:00    113    0.283249    0.162530    True
100 rows × 4 columns

pandas library와 동일한 결과를 얻기 위한 코드가 매우 길어진다. python의 library들이 우리가 코드를 작성하는데 엄청난 편의를 제공하고 있다는 것을 잘 느낄 수 있는 부분이기도 하다.
그런데 다음 글에서 소개될 속도 비교를 보면 이 편리한 library도 사용함에 있어서 library를 잘 이해를 하고 적절한 곳에 써야 한다는 것을 알 수 있을 것이다.

This post is licensed under CC BY 4.0 by the author.

KT GIGA Wifi 공유기 관리자 모드 접속이 안되는 경우

깃허브(GitHub, jekyll) 블로그 이미지 호스팅 하기 (원드라이브, 구글드라이브)