Overview
The Moving Average Convergence Divergence (MACD) is a momentum indicator widely used in systematic strategies. It is defined as:
- MACD line = EMA(fast) − EMA(slow)
- Signal line = EMA(signal) of MACD line
- Histogram = MACD line − Signal line
Typical parameters are (fast, slow, signal) = (12, 26, 9) on closing prices.
| Component | Definition | Typical span |
|---|---|---|
| MACD line | EMA(12) − EMA(26) | 12, 26 |
| Signal line | EMA of MACD line | 9 |
| Histogram | MACD − Signal | — |
Quickstart
- Install dependencies
pip install pandas numpy yfinance
Load price data (Close), ideally adjusted for splits/dividends.
Compute MACD with pandas ewm and adjust=False.
Create a position rule (e.g., long when MACD > Signal; short otherwise).
Backtest with next-bar execution to avoid lookahead bias.
Review metrics and iterate on parameters.
Minimal working example (single asset, daily)
import numpy as np
import pandas as pd
import yfinance as yf
# 1) Download daily data
symbol = "SPY"
df = yf.download(symbol, period="2y", interval="1d", auto_adjust=True, progress=False)
close = df["Close"].rename(symbol)
# 2) MACD computation
def compute_macd(price: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> pd.DataFrame:
ema_fast = price.ewm(span=fast, adjust=False).mean()
ema_slow = price.ewm(span=slow, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=signal, adjust=False).mean()
hist = macd_line - signal_line
out = pd.DataFrame({
"macd": macd_line,
"signal": signal_line,
"hist": hist,
})
return out
macd = compute_macd(close)
# 3) Simple long-short strategy using histogram sign
ret = close.pct_change()
position = np.where(macd["hist"] > 0, 1, -1)
position = pd.Series(position, index=close.index)
# 4) Next-bar execution (shift signal by 1 day)
strategy_ret = ret * position.shift(1)
# 5) Clean warm-up region (drop early NaNs from EMAs)
strategy_ret = strategy_ret.dropna()
ret = ret.loc[strategy_ret.index]
# 6) Metrics
cum_strategy = (1 + strategy_ret).cumprod()
cum_buyhold = (1 + ret).cumprod()
def sharpe(r, freq=252):
return np.sqrt(freq) * r.mean() / r.std(ddof=0)
print("Final strategy equity:", float(cum_strategy.iloc[-1]))
print("Final buy&hold equity:", float(cum_buyhold.iloc[-1]))
print("Sharpe (strategy):", float(sharpe(strategy_ret)))
print("Sharpe (buy&hold):", float(sharpe(ret)))
# Inspect latest MACD values
print(macd.tail())
Notes:
- adjust=False aligns with most trading tools and avoids bias from historical reweighting.
- We shifted the position by one bar to simulate trading on the next close/open.
Implementation details
- Exponentially weighted mean: pandas.Series.ewm(span=s, adjust=False).mean() uses alpha = 2 / (s + 1).
- Warm-up: EMAs need multiple bars to stabilize. Expect NaNs early; drop or ignore the initial period.
- Parameter intuition:
- Larger slow span smooths more, reduces false signals, reacts later.
- Smaller fast span increases responsiveness, may increase noise.
Common signal rules
- Crossover: go long when MACD crosses above Signal; exit/short when below.
- Histogram sign: long if hist > 0; short if hist < 0.
- Zero-line filter: trade only when MACD is above/below 0 to align with broader trend.
Example crossover detection (without full backtest):
cross_up = (macd["macd"] > macd["signal"]) & (macd["macd"].shift(1) <= macd["signal"].shift(1))
cross_dn = (macd["macd"] < macd["signal"]) & (macd["macd"].shift(1) >= macd["signal"].shift(1))
entry_dates = macd.index[cross_up]
exit_dates = macd.index[cross_dn]
Multi-asset example (vectorized)
# Suppose we have multiple symbols
symbols = ["SPY", "QQQ", "IWM"]
px = yf.download(symbols, period="2y", interval="1d", auto_adjust=True, progress=False)["Close"]
# Stack to long format and groupby compute MACD per symbol
long_px = px.stack().rename("close").to_frame()
long_px.index.names = ["date", "symbol"]
def macd_grouped(g):
out = compute_macd(g["close"]).dropna()
return out
macd_long = long_px.groupby(level="symbol", group_keys=False).apply(macd_grouped)
Pitfalls to avoid
- Lookahead bias: never use today’s close to decide today’s trade at that same close; shift signals by one bar.
- EMA parameters mismatch: pandas ewm(span=s, adjust=False) matches common charting defaults. Using adjust=True or halflife will differ.
- Data gaps and corporate actions: use adjusted prices; forward-fill only when appropriate.
- Intraday to daily mixing: resample carefully; confirm timezones and market sessions.
- Warm-up contamination: exclude early bars where EMAs are not stabilized.
- Slippage/fees: include costs; MACD flip-flopping can be costly in choppy markets.
Performance notes
- Vectorize: use pandas/numpy operations; avoid Python loops.
- Batch symbols: compute by groupby over a long DataFrame rather than iterating.
- Memory: drop unused columns and downcast to float32 when many symbols are used.
- Rolling windows: EMA is O(n); computing on large intraday histories is cheap; IO may dominate.
- Parallelism: batch downloads and use multiprocessing for backtests if CPU-bound.
- JIT option: for ultra-low latency, a Numba-compiled EMA can reduce Python overhead, though pandas ewm is typically sufficient.
Validation checklist
- Does MACD(12,26,9) match your charting platform? If not, confirm adjust=False.
- Are signals shifted to the next bar in backtests?
- Are metrics robust to transaction costs and parameter changes?
- Have you tested across regimes (trending vs. mean-reverting)?
Tiny FAQ
Which price should I use?
- Close is standard. For intraday, typical is last traded price per bar; ensure consistency.
How do I match TradingView/TA-Lib values?
- Use adjust=False with spans (12,26,9). Small differences can still occur due to rounding or data vendor differences.
Why is my first MACD value NaN?
- EMAs need warm-up. Drop the initial period or ignore until stabilized.
Can I use MACD intraday?
- Yes. Compute on 1m/5m bars. Ensure latency assumptions and costs reflect reality.
How do I avoid whipsaws?
- Add a threshold on the histogram, confirm with a higher-timeframe trend, or require persistence over N bars.
Is MACD better long-only or long-short?
- Depends on asset and regime. Test both, include costs, and validate out of sample.