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
을 써야 한다.
<출처 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를 잘 이해를 하고 적절한 곳에 써야
한다는 것을 알 수 있을 것이다.