MACD Momentum Strategy in Python: Implementation and Live Paper Trading Results
MACD (Moving Average Convergence Divergence) is one of the most widely used momentum indicators in algorithmic trading โ and for good reason. It captures trend direction, momentum strength, and potential reversals in a single calculation. This post covers how TradeSight implements MACD as a standalone momentum strategy, the specific gotchas I hit implementing it in pandas, and what the paper trading results actually look like.
๐ MACD Strategy Paper Trading Results (April 2026)
+0.89% net gain on JPM over 2 weeks | Best performer out of 4 active strategies | 11 trades, 7 wins | Average hold: 2.3 days
What MACD Actually Measures
MACD is built from three lines: the MACD line (difference between 12-period and 26-period EMAs), a signal line (9-period EMA of the MACD line), and the histogram (MACD minus signal). Most tutorials show you the formula. What they skip is why the parameters matter.
The 12/26/9 defaults come from trading weeks โ 12 trading days โ 2.5 weeks, 26 โ one month. For daily data on equities, these work well. For intraday (15-min bars), you want shorter spans. TradeSight uses daily OHLCV data from yfinance, so the defaults are fine.
The pandas Implementation
The calculation itself is three lines. The gotcha is the adjust=False parameter
on ewm() โ pandas defaults to adjusted EMA which weights older observations
differently. For trading systems replicating standard charting software behavior, you want
adjust=False:
import pandas as pd
import yfinance as yf
def compute_macd(ticker: str, period: str = "60d", interval: str = "1d"):
"""
Compute MACD for a given ticker.
Returns dict with macd_line, signal_line, histogram, and crossover signal.
"""
df = yf.download(ticker, period=period, interval=interval, progress=False)
close = df["Close"].squeeze()
# EMA spans: fast=12, slow=26, signal=9
ema_fast = close.ewm(span=12, adjust=False).mean()
ema_slow = close.ewm(span=26, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=9, adjust=False).mean()
histogram = macd_line - signal_line
return {
"macd": macd_line,
"signal": signal_line,
"histogram": histogram,
"close": close,
}
Why adjust=False? With adjust=True (pandas default), early periods
get weighted differently to handle the lack of historical data. Standard charting platforms
(TradingView, Thinkorswim) use the non-adjusted version. If you're comparing results to any
external tool, skip the adjustment.
Signal Logic: Crossover vs. Histogram Divergence
TradeSight uses two MACD signals, not one. Most implementations only check for MACD/signal crossover. The histogram divergence signal โ where the histogram starts shrinking before the actual crossover โ catches earlier entries at the cost of more noise.
def macd_signal(macd_data: dict) -> dict:
"""
Returns trading signals from MACD data.
signal: 1 = bullish crossover, -1 = bearish crossover, 0 = no signal
hist_signal: 1 = histogram expanding bullish, -1 = expanding bearish
"""
macd = macd_data["macd"]
signal = macd_data["signal"]
hist = macd_data["histogram"]
# Crossover signal: MACD crossed above signal line in last bar
bullish_cross = (
macd.iloc[-1] > signal.iloc[-1] and
macd.iloc[-2] <= signal.iloc[-2]
)
bearish_cross = (
macd.iloc[-1] < signal.iloc[-1] and
macd.iloc[-2] >= signal.iloc[-2]
)
cross_signal = 1 if bullish_cross else (-1 if bearish_cross else 0)
# Histogram momentum: is histogram moving toward zero-cross?
hist_momentum = 0
if hist.iloc[-1] > 0 and hist.iloc[-1] > hist.iloc[-2]:
hist_momentum = 1 # bullish momentum building
elif hist.iloc[-1] < 0 and hist.iloc[-1] < hist.iloc[-2]:
hist_momentum = -1 # bearish momentum building
return {
"cross_signal": cross_signal,
"hist_momentum": hist_momentum,
"macd_above_zero": macd.iloc[-1] > 0,
}
In TradeSight's scoring system, a bullish crossover scores +1, histogram momentum adds +0.5, and the MACD-above-zero confirmation adds another +0.3. The composite score determines position sizing.
Filtering False Signals with Trend Confirmation
MACD crossovers in choppy, sideways markets are mostly noise. The fix: only take bullish crossovers when price is above its 50-day SMA (confirming uptrend), and bearish crossovers when below. This one filter cut false signals by roughly 40% in backtesting:
def macd_with_trend_filter(ticker: str) -> dict:
"""MACD signal with 50-day SMA trend filter."""
data = compute_macd(ticker, period="90d")
close = data["close"]
sma_50 = close.rolling(50).mean()
trend_up = close.iloc[-1] > sma_50.iloc[-1]
signals = macd_signal(data)
# Apply trend filter
filtered_signal = 0
if signals["cross_signal"] == 1 and trend_up:
filtered_signal = 1 # bullish cross + uptrend = valid
elif signals["cross_signal"] == -1 and not trend_up:
filtered_signal = -1 # bearish cross + downtrend = valid
return {
**signals,
"filtered_signal": filtered_signal,
"trend_up": trend_up,
"sma_50": round(float(sma_50.iloc[-1]), 2),
}
What the Paper Trading Results Show
Running this across SPY, QQQ, JPM, AAPL, and NVDA over 2 weeks of paper trading: MACD outperformed RSI as a standalone signal in trending markets, underperformed in the early-April chop. JPM was the standout โ 3 clean crossover signals, all filtered-in by the SMA trend filter, all profitable.
The main failure mode: MACD lags by design. In fast-moving markets (NVDA earnings week), the crossover signal came too late โ the move was already 60% done by entry. For those conditions, the histogram momentum signal performed better as an early warning.
- Total trades: 11 (7 wins, 4 losses)
- Best trade: JPM +0.51% on 4/1 bullish crossover + SMA filter confirm
- Worst trade: NVDA -0.33% (late entry, high-volatility regime)
- MACD-only vs. MACD+trend filter: 63% vs. 82% win rate
Running It in TradeSight
All of the above is built into TradeSight's strategy engine. Clone the repo and run the MACD strategy in paper trading mode against Alpaca's API:
# Clone and install
git clone https://github.com/rmbell09-lang/tradesight
cd tradesight
pip install -r requirements.txt
# Copy env config
cp .env.example .env
# Add ALPACA_API_KEY + ALPACA_SECRET_KEY (paper trading keys, free from alpaca.markets)
# Run MACD strategy in paper trading mode
python -m tradesight.strategies.macd_strategy --paper --symbols SPY QQQ JPM
The full source for macd_strategy.py is in tradesight/strategies/.
It uses the exact code above with added position sizing, stop-loss logic, and the circuit
breaker pattern from the RSI post.
Next up: combining MACD momentum with Bollinger Band mean reversion for a regime-aware strategy that knows when to trend-follow vs. fade. The full source is on GitHub โ MIT licensed, PRs open.