Building a MACD indicator using Python and Pandas
Published on Feb 9, 2024 8:09 AM
17
MACD Indicator with Bitcoin¶
In this playground, I'm using Python and Pandas to create the MACD indicator for Daily bitcoin prices.
The MACD indicator is used to spot changes in trends in an asset. But, as it is a lagging indicator, it's hardly useful to spot any real trend changes and its usage as an investment technique is discouraged.
To calculate it, we just need to calculate "Exponential moving average" (EMA) of a given asset price.
Let's take it step by step!
Do you want to try this by yourself? Copy this Playground to get started! You'll get the same dataset and my original notebook.
In [5]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
First, let's start by reading the Data:
In [6]:
df = pd.read_csv('/data/bitstamp-btcusd-daily-historic/btcusd_daily.csv', index_col='timestamp', parse_dates=True)
Explore it:
In [7]:
df.head()
Out[7]:
open | high | low | close | volume | |
---|---|---|---|---|---|
timestamp | |||||
2015-01-01 00:00:00+00:00 | 321.00 | 321.00 | 312.60 | 313.81 | 3087.436554 |
2015-01-02 00:00:00+00:00 | 313.81 | 317.01 | 311.96 | 315.42 | 3468.281375 |
2015-01-03 00:00:00+00:00 | 315.42 | 316.58 | 280.00 | 282.00 | 21752.719146 |
2015-01-04 00:00:00+00:00 | 280.00 | 289.39 | 255.00 | 264.00 | 41441.278553 |
2015-01-05 00:00:00+00:00 | 264.55 | 280.00 | 264.07 | 276.80 | 9528.271002 |
For our MACD Indicator, I'll use the Close price, so I create a new empty dataframe:
In [8]:
close_df = df[['close']].copy()
In [9]:
close_df.head()
Out[9]:
close | |
---|---|
timestamp | |
2015-01-01 00:00:00+00:00 | 313.81 |
2015-01-02 00:00:00+00:00 | 315.42 |
2015-01-03 00:00:00+00:00 | 282.00 |
2015-01-04 00:00:00+00:00 | 264.00 |
2015-01-05 00:00:00+00:00 | 276.80 |
Calculating MACD¶
MACD has 3 main Time Series:
- The MACD Series
- The Signal Line
- The difference between 1 and 2, often called the "divergence" series.
1. The MACD Series¶
The MACD Series is the difference between a "fast" (short period) exponential moving average (EMA), and a "slow" (longer period) EMA of the price series. Usually, the values chosen are 12
for the fast one, and 26
for the fast one. So let's start calculating these EMAs:
In [10]:
close_df['EMA_12'] = close_df['close'].ewm(span=12, adjust=False).mean()
close_df['EMA_26'] = close_df['close'].ewm(span=26, adjust=False).mean()
close_df.head()
Out[10]:
close | EMA_12 | EMA_26 | |
---|---|---|---|
timestamp | |||
2015-01-01 00:00:00+00:00 | 313.81 | 313.810000 | 313.810000 |
2015-01-02 00:00:00+00:00 | 315.42 | 314.057692 | 313.929259 |
2015-01-03 00:00:00+00:00 | 282.00 | 309.125740 | 311.564129 |
2015-01-04 00:00:00+00:00 | 264.00 | 302.183318 | 308.040860 |
2015-01-05 00:00:00+00:00 | 276.80 | 298.278192 | 305.726722 |
Now, we can calculate teh MACD
series, which is just the difference between EMA_12
and EMA_26
:
In [11]:
close_df['MACD'] = close_df['EMA_12'] - close_df['EMA_26']
close_df.head()
Out[11]:
close | EMA_12 | EMA_26 | MACD | |
---|---|---|---|---|
timestamp | ||||
2015-01-01 00:00:00+00:00 | 313.81 | 313.810000 | 313.810000 | 0.000000 |
2015-01-02 00:00:00+00:00 | 315.42 | 314.057692 | 313.929259 | 0.128433 |
2015-01-03 00:00:00+00:00 | 282.00 | 309.125740 | 311.564129 | -2.438389 |
2015-01-04 00:00:00+00:00 | 264.00 | 302.183318 | 308.040860 | -5.857542 |
2015-01-05 00:00:00+00:00 | 276.80 | 298.278192 | 305.726722 | -7.448530 |
2. The Signal Line¶
The signal line is just an EMA of the MACD series itself. Usually, 9 periods are used:
In [12]:
close_df['Signal_Line'] = close_df['MACD'].ewm(span=9, adjust=False).mean()
close_df.head()
Out[12]:
close | EMA_12 | EMA_26 | MACD | Signal_Line | |
---|---|---|---|---|---|
timestamp | |||||
2015-01-01 00:00:00+00:00 | 313.81 | 313.810000 | 313.810000 | 0.000000 | 0.000000 |
2015-01-02 00:00:00+00:00 | 315.42 | 314.057692 | 313.929259 | 0.128433 | 0.025687 |
2015-01-03 00:00:00+00:00 | 282.00 | 309.125740 | 311.564129 | -2.438389 | -0.467129 |
2015-01-04 00:00:00+00:00 | 264.00 | 302.183318 | 308.040860 | -5.857542 | -1.545211 |
2015-01-05 00:00:00+00:00 | 276.80 | 298.278192 | 305.726722 | -7.448530 | -2.725875 |
3. The divergence series¶
And finally, we can calculate the divergence series, which is just the difference between the two:
In [13]:
close_df['Divergence'] = close_df['MACD'] - close_df['Signal_Line']
close_df.head()
Out[13]:
close | EMA_12 | EMA_26 | MACD | Signal_Line | Divergence | |
---|---|---|---|---|---|---|
timestamp | ||||||
2015-01-01 00:00:00+00:00 | 313.81 | 313.810000 | 313.810000 | 0.000000 | 0.000000 | 0.000000 |
2015-01-02 00:00:00+00:00 | 315.42 | 314.057692 | 313.929259 | 0.128433 | 0.025687 | 0.102746 |
2015-01-03 00:00:00+00:00 | 282.00 | 309.125740 | 311.564129 | -2.438389 | -0.467129 | -1.971261 |
2015-01-04 00:00:00+00:00 | 264.00 | 302.183318 | 308.040860 | -5.857542 | -1.545211 | -4.312331 |
2015-01-05 00:00:00+00:00 | 276.80 | 298.278192 | 305.726722 | -7.448530 | -2.725875 | -4.722655 |
Plotting MACD¶
It's sometimes useful to plot the MACD indicator along with the original price to understand the changes of trends. To do so, I'll start first by only getting the prices of 2023 onwards, and then creating two axes:
The first one contains the price itself with the EMA12 and EMA26. The second one, contains the Signal Line and the MACD along with a barchar of the Divergence
In [14]:
df_2023 = close_df.loc['2023':].copy()
In [12]:
fig = plt.figure(figsize=(14, 12), constrained_layout=True)
gs = fig.add_gridspec(2, 1, height_ratios=[2, 1])
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[1, 0])
df_2023['close'].plot(ax=ax1, color='blue', label='Close')
df_2023['EMA_12'].plot(ax=ax1, color='black', label='EMA_12')
df_2023['EMA_26'].plot(ax=ax1, color='red', label='EMA_26')
ax2.bar(df_2023.index, df_2023['Divergence'], label='MACD Histogram', color='grey')
ax2.plot(df_2023.index, df_2023['MACD'], label='MACD', color='blue', alpha=.5)
ax2.plot(df_2023.index, df_2023['Signal_Line'], label='Signal Line', color='red', alpha=.5)
for ax in (ax1, ax2):
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
ax.tick_params(axis='x', which='major', labelrotation=45)
ax1.set_title('Close Price including EMA 12 and 26')
ax2.set_title('MACD')
ax1.legend()
ax2.legend(loc='upper left')
plt.tight_layout()
/tmp/ipykernel_14/3985725785.py:30: UserWarning: The figure layout has changed to tight plt.tight_layout()
What does MACD tells us?¶
EMA 12 vs EMA 26¶
We can start by just analyzing the EMAs. In theory, when EMA_12 crosses EMA_26 could be considered a bullish signal, while the opposite (EMA_26 crossing over EMA_12) could be considered a bearish market. Let's take a look!
First I'll extract only the EMAs in a single dataframe:
In [15]:
analysis_df = df_2023[['EMA_12', 'EMA_26']].copy()
analysis_df.head()
Out[15]:
EMA_12 | EMA_26 | |
---|---|---|
timestamp | ||
2023-01-01 00:00:00+00:00 | 16701.523922 | 16838.578239 |
2023-01-02 00:00:00+00:00 | 16697.135626 | 16826.313184 |
2023-01-03 00:00:00+00:00 | 16692.960915 | 16814.734430 |
2023-01-04 00:00:00+00:00 | 16716.966928 | 16817.272620 |
2023-01-05 00:00:00+00:00 | 16733.741247 | 16817.919093 |
We can then shift both EMAs to compare them with the day before:
In [16]:
analysis_df['EMA_12_prev'] = analysis_df['EMA_12'].shift(1)
analysis_df['EMA_26_prev'] = analysis_df['EMA_26'].shift(1)
And then compare the times where EMA_12 crossed over EMA_26 and the other way around:
In [17]:
positive_flips = (analysis_df['EMA_12_prev'] < analysis_df['EMA_26_prev']) & (analysis_df['EMA_12'] > analysis_df['EMA_26'])
negative_flips = (analysis_df['EMA_26_prev'] < analysis_df['EMA_12_prev']) & (analysis_df['EMA_26'] > analysis_df['EMA_12'])
In [18]:
analysis_df.loc[positive_flips.loc[positive_flips].index - pd.Timedelta(days=1)]
Out[18]:
EMA_12 | EMA_26 | EMA_12_prev | EMA_26_prev | |
---|---|---|---|---|
timestamp | ||||
2023-01-08 00:00:00+00:00 | 16844.333909 | 16857.325500 | 16794.394620 | 16836.391540 |
2023-03-14 00:00:00+00:00 | 22603.703225 | 22660.892063 | 22213.467447 | 22493.763428 |
2023-06-20 00:00:00+00:00 | 26610.391812 | 26669.514586 | 26300.463050 | 26537.875753 |
2023-09-19 00:00:00+00:00 | 26463.337800 | 26518.750666 | 26326.853763 | 26463.130719 |
2023-09-28 00:00:00+00:00 | 26522.229408 | 26530.428801 | 26430.634755 | 26490.783105 |
In [19]:
analysis_df.loc[negative_flips.loc[negative_flips].index - pd.Timedelta(days=1)]
Out[19]:
EMA_12 | EMA_26 | EMA_12_prev | EMA_26_prev | |
---|---|---|---|---|
timestamp | ||||
2023-03-05 00:00:00+00:00 | 23087.328435 | 23086.354448 | 23207.206332 | 23139.022804 |
2023-05-08 00:00:00+00:00 | 28663.309955 | 28623.164825 | 28840.275402 | 28697.818011 |
2023-07-24 00:00:00+00:00 | 29941.999726 | 29888.027428 | 30081.090585 | 29944.909622 |
2023-09-23 00:00:00+00:00 | 26569.991342 | 26566.414817 | 26567.626132 | 26565.088002 |
Now, putting altogether:
In [20]:
fig, ax = plt.subplots(figsize=(14, 7))
df_2023['close'].plot(ax=ax, color='blue', label='Close')
df_2023['EMA_12'].plot(ax=ax, color='black', label='EMA_12')
df_2023['EMA_26'].plot(ax=ax, color='red', label='EMA_26')
for ts in positive_flips.loc[positive_flips].index - pd.Timedelta(days=1):
ax.axvline(pd.Timestamp(ts), color='red')
for ts in negative_flips.loc[negative_flips].index - pd.Timedelta(days=1):
ax.axvline(pd.Timestamp(ts), color='blue')
In [21]:
df_2023.head()
Out[21]:
close | EMA_12 | EMA_26 | MACD | Signal_Line | Divergence | |
---|---|---|---|---|---|---|
timestamp | ||||||
2023-01-01 00:00:00+00:00 | 16615.0 | 16701.523922 | 16838.578239 | -137.054317 | -122.929765 | -14.124552 |
2023-01-02 00:00:00+00:00 | 16673.0 | 16697.135626 | 16826.313184 | -129.177558 | -124.179323 | -4.998235 |
2023-01-03 00:00:00+00:00 | 16670.0 | 16692.960915 | 16814.734430 | -121.773515 | -123.698162 | 1.924646 |
2023-01-04 00:00:00+00:00 | 16849.0 | 16716.966928 | 16817.272620 | -100.305692 | -119.019668 | 18.713975 |
2023-01-05 00:00:00+00:00 | 16826.0 | 16733.741247 | 16817.919093 | -84.177846 | -112.051303 | 27.873457 |
Signal-line crossover¶
In this case, what we can analyze is the Divergence. In theory, when the Divergence series crosses above 0, we're in a bull market, and when it drops below zero, we're in a bearish market.
In [22]:
div_df = df_2023[['Divergence']].copy()
div_df.head()
Out[22]:
Divergence | |
---|---|
timestamp | |
2023-01-01 00:00:00+00:00 | -14.124552 |
2023-01-02 00:00:00+00:00 | -4.998235 |
2023-01-03 00:00:00+00:00 | 1.924646 |
2023-01-04 00:00:00+00:00 | 18.713975 |
2023-01-05 00:00:00+00:00 | 27.873457 |
To calculate it, we'll follow a similar process as the previous one. We'll first calculate the shifted value:
In [23]:
div_df['Shifted'] = div_df['Divergence'].shift(1)
div_df.head()
Out[23]:
Divergence | Shifted | |
---|---|---|
timestamp | ||
2023-01-01 00:00:00+00:00 | -14.124552 | NaN |
2023-01-02 00:00:00+00:00 | -4.998235 | -14.124552 |
2023-01-03 00:00:00+00:00 | 1.924646 | -4.998235 |
2023-01-04 00:00:00+00:00 | 18.713975 | 1.924646 |
2023-01-05 00:00:00+00:00 | 27.873457 | 18.713975 |
In [30]:
!jupyter nbextension enable spellchecker/main
Enabling notebook extension spellchecker/main...
- Validating: OK
And now, for positive crossover, we can just identify the points in which the previous value was negative, and the current one is positive. For negative crossover, is just the oposite. If the value was positive before and now it's negative, we identify it as a negative crossover.
In [25]:
positive_crossover = (
(div_df['Shifted'] < 0) & (div_df['Divergence'] > 0)
)
negative_crossover = (
(div_df['Shifted'] > 0) & (div_df['Divergence'] < 0)
)
In [26]:
positive_crossover.loc[positive_crossover]
Out[26]:
timestamp 2023-01-03 00:00:00+00:00 True 2023-02-17 00:00:00+00:00 True 2023-03-14 00:00:00+00:00 True 2023-04-11 00:00:00+00:00 True 2023-05-27 00:00:00+00:00 True 2023-06-06 00:00:00+00:00 True 2023-06-17 00:00:00+00:00 True 2023-08-08 00:00:00+00:00 True 2023-08-29 00:00:00+00:00 True 2023-10-16 00:00:00+00:00 True 2023-11-10 00:00:00+00:00 True 2023-12-02 00:00:00+00:00 True dtype: bool
And we can plot it to verify it:
In [29]:
ax = div_df['Divergence'].plot(figsize=(14, 7))
ax.axhline(0, color='green')
for ts in positive_crossover.loc[positive_crossover].index:
ax.axvline(ts, color='red')
for ts in negative_crossover.loc[negative_crossover].index:
ax.axvline(ts, color='blue')
Finally, we can combine it with the price and the EMAs:
In [32]:
fig, ax = plt.subplots(figsize=(14, 7))
df_2023['close'].plot(ax=ax, color='black', label='Close')
df_2023['EMA_12'].plot(ax=ax, color='green', label='EMA_12', alpha=.5)
df_2023['EMA_26'].plot(ax=ax, color='orange', label='EMA_26', alpha=.5)
for ts in positive_crossover.loc[positive_crossover].index:
ax.axvline(ts, color='red')
for ts in negative_crossover.loc[negative_crossover].index:
ax.axvline(ts, color='blue')
Conclusion¶
As we can clearly see from the previous chart, the MACD indicator doesn't look as a great indicator to use as an investment tool. But hey! At least we can use it to practice some Pandas!