python MACD momentum

MACD Momentum Strategy in Python: Implementation and Live Paper Trading Results

๐Ÿ“… April 3, 2026 โฑ 7 min read ๐Ÿ‘ค TradeSight Project

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.

Key takeaway: MACD crossover alone is marginal. MACD + trend filter + position sizing by histogram strength is where the edge is.

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.