The Moving Average Convergence-Divergence (MACD, sometimes pronounced “Mac-Dee”) is a very popular indicator that is frequently used in momentum and trend following systems. In short, the MACD is a trailing indicator that gives an indication of the general price trend of a security.
TL;DR
We walk through the reasoning and math behind the MACD along with Python code so you can learn to apply it yourself.
How the MACD Works
Despite the intimidating name, the MACD is relatively straightforward to understand and calculation. It takes some ideas from other indicators, namely EMA and moving average cross-over strategies, and combines them into a single, easy to use value.
In its most basic form, we have two EMA signals, a fast one and a slow one (12 and 26-days are popularly chosen). These are calculated every day, then we subtract the fast one from the slow one to get the difference. In psuedocode we have:
- Calculate fast EMA:
- EMA_fast[t] = (Price[t] - EMA_fast[t-1]) * 2 / (N_fast + 1) + EMA_fast[t-1]
2. Calculate the Slow EMA:
- EMA_slow[t] = (Price[t] - EMA_slow[t-1]) * 2 / (N_slow + 1) + EMA_slow[t-1]
3. Subtract the two to get the MACD:
- MACD[t] = EMA_fast[t] - EMA_slow[t]
Or, if you prefer to see it written mathematically:
It’s really that easy.
The MACD is called the Moving Average Convergence-Divergence. What are converging and diverging, are the two EMAs. As the short-term EMA and long-term EMA converge, they get closer to the same value and the indicator moves to 0. Divergence is driven by the short-term EMA moving up or down causing the distance between the two EMAs to move farther apart. There are other ways the terms convergence and divergence are used with this indicator which we’ll get to below.
The most basic way to trade it is buy when MACD > 0, and sell/short when MACD < 0. When it’s positive, we have the faster EMA above the longer EMA, and vice versa when it goes negative. This set up is a basic, exponential moving average crossover strategy.
Calculating the MACD in Python
To code it, we just need a few basic packages.
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
The details for the EMA calculation are given here, so we’ll just show the final code which we’ll leverage in our model.
def _calcEMA(P, last_ema, N):
return (P - last_ema) * (2 / (N + 1)) + last_ema
def calcEMA(data: pd.DataFrame, N: int, key: str = 'Close'):
# Initialize series
data['SMA_' + str(N)] = data[key].rolling(N).mean()
ema = np.zeros(len(data)) + np.nan
for i, _row in enumerate(data.iterrows()):
row = _row[1]
if np.isnan(ema[i-1]):
ema[i] = row['SMA_' + str(N)]
else:
ema[i] = _calcEMA(row[key], ema[i-1], N)
data['EMA_' + str(N)] = ema.copy()
return data
With the calcEMA function above, we can easily write our MACD function. We just need to call calcEMA twice with the fast and slow parameters, and subtract the values.
def calcMACD(data: pd.DataFrame, N_fast: int, N_slow: int):
assert N_fast < N_slow, ("Fast EMA must be less than slow EMA
parameter.")
# Add short term EMA
data = calcEMA(data, N_fast)
# Add long term EMA
data = calcEMA(data, N_slow)
# Subtract values to get MACD
data['MACD'] = data[f'EMA_{N_fast}'] - data[f'EMA_{N_slow}']
return data
We’re ready to test it. I just grabbed a one year time period from a random stock in the S&P 500 to illustrate how the indicator works. We’ll just run it through our function and take a look at the output.
ticker = 'DTE'
start = '2013-01-01'
end = '2014-01-01'
yfObj = yf.Ticker(ticker)
df = yfObj.history(start=start, end=end)
N_fast = 12
N_slow = 26
data = calcMACD(df, N_fast, N_slow)
# Drop extra columns
data.drop(['Open', 'High', 'Low', 'Volume',
'Dividends', 'Stock Splits'], axis=1, inplace=True)
data.iloc[N_slow-5:N_slow+5]
As you can see in the table above, our function provides the different EMA values (and the SMAs they’re initialized on) in addition to the MACD. We can plot these values below to see how they track with one another.
fig, ax = plt.subplots(2, figsize=(12, 8), sharex=True)
ax[0].plot(data['Close'], label=f'{ticker}', linestyle=':')
ax[0].plot(data[f'EMA_{N_fast}'], label=f'EMA-{N_fast}')
ax[0].plot(data[f'EMA_{N_slow}'], label=f'EMA-{N_slow}')
ax[0].set_ylabel('Price ($)')
ax[0].set_title(f'Price and EMA Values for {ticker}')
ax[0].legend()
ax[1].plot(data['MACD'], label='MACD')
ax[1].set_ylabel('MACD')
ax[1].set_xlabel('Date')
plt.show()
We have a few good oscillations around 0, which, in our simplest strategy, would indicate buy/sell signals as the EMAs cross over. You can also see that the initial price rise from January through May has a high MACD (>0.5), and the MACD peak roughly corresponds with the peak in the price of our security. The MACD begins to fall and turn negative, which follows a healthy retracement in the price before it rebounds again.
Also, note that as the MACD becomes more positive, this means that the short-term EMA is increasing more rapidly than the longer, slower EMA we’re comparing it to. This is a sign of stronger upward momentum in the price action. The opposite holds true when the MACD becomes more negative.
There’s another way to generate signals from the MACD, however. This method is based on using a signal line in addition to the MACD as we calculated it.
The MACD with a Signal Line
The signal line is a EMA of the MACD signal we calculated. Frequently this will be a 9-day EMA to go along with a 12 and 26-day EMA like we calculated above.
Writing the psuedocode, we just need to add one more step:
4. Calculate the Signal Line (SL) as the EMA of the MACD:
- SL[t] = (MACD[t] - EMA_MACD[t-1]) * 2 / (N_SL + 1) + EMA_MACD[t-1]
Again, we can write this mathematically as:
Let’s modify our calcMACD function to allow for signal line calculation too.
def calcMACD(data: pd.DataFrame, N_fast: int, N_slow: int,
signal_line: bool = True, N_sl: int = 9):
assert N_fast < N_slow, ("Fast EMA must be less than slow EMA
parameter.")
# Add short term EMA
data = calcEMA(data, N_fast)
# Add long term EMA
data = calcEMA(data, N_slow)
# Subtract values to get MACD
data['MACD'] = data[f'EMA_{N_fast}'] - data[f'EMA_{N_slow}']
if signal_line:
data = calcEMA(data, N_sl, key='MACD')
# Rename columns
data.rename(
columns={f'SMA_{N_sl}': f'SMA_MACD_{N_sl}',
f'EMA_{N_sl}': f'SignalLine_{N_sl}'},
inplace=True)
return data
Now we can run this new function and plot it to show how the signal line looks versus the MACD.
N_fast = 12
N_slow = 26
N_sl = 9
signal_line = True
data = calcMACD(df, N_fast, N_slow, signal_line, N_sl)
# Plot MACD and Signal Line
fig, ax = plt.subplots(figsize=(15, 8))
ax.plot(data['MACD'], label='MACD')
ax.plot(data[f'SignalLine_{N_sl}'], label='Signal Line',
c=colors[2])
ax.set_ylabel('MACD')
ax.set_xlabel('Date')
ax.set_title(f'MACD and Signal Line for {ticker}')
ax.legend()
plt.show()
In the plot above, we can see that the signal line intersects with the MACD on a number of occasions. As you have probably guessed, these intersections provide buy and sell signals as well. You can buy when the MACD crosses above the signal line and sell/short when it goes below.
MACD Momentum
Often times, you’ll see MACD charts with bars as well as lines on the graph like this:
data['MACD_bars'] = data['MACD'] - data[f'SignalLine_{N_sl}']
colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
fig, ax = plt.subplots(1, figsize=(15, 8), sharex=True)
# Re-index dates to avoid gaps over weekends
_x_axis = np.arange(data.shape[0])
month = pd.Series(data.index.map(lambda x: x.month))
x = month - month.shift(1)
_x_idx = np.where((x.values!=0) &
(~np.isnan(x.values)))[0]
_x_dates = data.apply(lambda x: any(np.isnan(x)), axis=1)
ax.plot(_x_axis, data['MACD'], label='MACD')
ax.plot(_x_axis, data[f'SignalLine_{N_sl}'], label='Signal Line',
c=colors[2])
ax.bar(_x_axis, data['MACD_bars'], label='MACD Bars',
color=colors[4], width=1, edgecolor='w')
ax.set_xticks(_x_axis[np.where(~np.isnan(_x_dates))])
ax.set_xticks(_x_idx)
ax.set_xticklabels(data.index[_x_idx].map(
lambda x: datetime.strftime(x, '%Y-%m-%d')),
fontsize=10)
ax.legend()
plt.show()
The MACD bars, also called MACD histogram, plots the difference between the MACD and the signal line. These bars can be used by chartists (those who read charts to make trades) to determine the strength of the momentum and make buy sell decisions. If the bars are growing in size, then momentum is increasing as the MACD is pulling away from the signal line. Additionally, if the bars begin shrinking, we could be seeing a reversal coming.
We’re focused on automatic and systematic trading, so we don’t want to spend time looking at bars to see if they’re growing or shrinking. Rather, we want the computer to do it for us!
Thankfully, this is only a few small steps away.
An easy way is to choose a number of consecutive days that the size of the bars are increasing or decreasing. If this condition is met, then we buy or go sell/short.
First, we need to determine how many consecutive days we want before we determine whether or not we want to enter a position. For our illustration here, we’ll use three days. From there, we can get the daily changes by using the diff() method and then use the np.sign() function to determine if it's positive or negative (note that this function returns a positive sign for 0s). We'll call this column Growth.
After that, we use Pandas’ handy rolling() method to sum our Growth column. This will give us 3 if we have three consecutive up days, -3 if we have three straight down days, or some other value between -2 and 2, making it easy for us to pick out our signals. For lack of a better name, let's call this column Consecutive_Bars.
Finally, we’ll get our position by looking for all of those +/-3 values. We want to go long on increasing upward momentum and short on decreasing downward momentum, so we just multiply the sign of our Consecutive_Bars column by 1 if the absolute value of Consecutive_Bars equals the number of consecutive days.
The code for all of this is given below.
N_consecutive = 3
data['Growth'] = np.sign(data['MACD_bars'].diff(1))
data['Consecutive_Bars'] = data['Growth'].rolling(N_consecutive).sum()
data['Position'] = data['Consecutive_Bars'].map(
lambda x: np.sign(x) * 1 if np.abs(x) == N_consecutive else 0)
long_idx = data['Position']==1
short_idx = data['Position']==-1
fig, ax = plt.subplots(2, figsize=(15, 8), sharex=True)
ax[0].plot(_x_axis, data['Close'], label=f'{ticker}', linestyle=':')
ax[0].scatter(_x_axis[long_idx], data.loc[long_idx]['Close'],
label='Longs', c=colors[3])
ax[0].scatter(_x_axis[short_idx], data.loc[short_idx]['Close'],
label='Shorts', c=colors[1])
ax[0].set_ylabel('Price ($)')
ax[0].set_title(f'{ticker} and Long/Short Positions based on MACD Bars')
ax[0].legend()
ax[1].bar(_x_axis, data['MACD_bars'], label='MACD Bars',
color=colors[4], width=1, edgecolor='w')
ax[1].scatter(_x_axis[long_idx], data.loc[long_idx]['MACD_bars'],
label='Longs', c=colors[3])
ax[1].scatter(_x_axis[short_idx], data.loc[short_idx]['MACD_bars'],
label='Shorts', c=colors[1])
ax[1].plot(_x_axis, data['MACD'], label='MACD')
ax[1].plot(_x_axis, data['SignalLine_9'], label='Signal Line',
color=colors[2])
ax[1].set_ylabel('MACD')
ax[1].set_title('MACD, Signal Line, MACD Bars, and Long/Short Positions')
ax[1].set_xticks(_x_axis[np.where(~np.isnan(_x_dates))])
ax[1].set_xticks(_x_idx)
ax[1].set_xticklabels(data.index[_x_idx].map(
lambda x: datetime.strftime(x, '%Y-%m-%d')),
fontsize=10)
ax[1].legend()
plt.show()
There’s a lot going on in the plots above!
In the first panel, we simply have the price action for our stock and the long/short positions overlaid so you can get a feel for what the stock may be doing when this indicator chooses to go long or short. It starts off catching the end of an uptrend and getting out near the peak, then waits a bit before entering to go short while the stock flat-lines.
As you can see working through the rest of the chart — like any indicator — it has a few mistakes. This indicator also lags the price action by quite a bit. Not only are you working off of three different EMAs, but then you wait for three consecutive days before putting a position on. This allows it to catch some strong trends, but because we were too quick to exit positions (e.g. wait for a single down day in the MACD bars to get out of a long position) we miss out on a good chunk of the moves, even though we were positionally correct. So, it looks like this could be useful, but we may need a better exit strategy to make the most of it.
In the second panel you see the MACD, signal line, MACD bars, and our longs/shorts. The MACD is derived from the price action and mimics it to some extent. The signal line smooths out some of the MACD moves and looks like a wave that is slightly out of phase. You can see our signal on the bars themselves too.
Build your own Trading Bot with the MACD
The MACD is a very popular indicator because of its flexibility and diversity. Here, we discussed just a handful of ways we can interpret the values to develop trading systems, but there are others out there we’ll address in the future.
While developing systems with the MACD, keep in mind that the indicator itself is unbounded, meaning it has no ceiling or floor. This is unlike other oscillating signals like RSI, which have a maximum and a minimum value. Moreover, the value is dependent on price. While our example ranged from a little over 1 to -1, you could have more expensive securities that produce MACD values in the teens or hundreds. This is important because it becomes very difficult — if not impossible — to compare MACD values directly across different assets.
The 12–26–9 MACD with a signal line is a very popular set up, but it isn’t the only one. You’re free to experiment, but note that the farther the short term and long term EMAs move from one another, the larger MACD values you’re going to get. Not that this is necessarily a problem, but it is something to keep an eye on as you may need to adjust some other parameters in your system to compensate.
There’s a lot to take in with this indicator and a lot of moving parts to keep track of. At Raposa, we make this easy for you. You can pick your stocks, set your parameters, and let us handle all of the details for you. We’ll provide you the stats on your backtests and give live signals when you’re ready to trade it in the real world.
Try our free demo to learn more!