KhueApps
Home/Python/Calculate MACD in Python for Algorithmic Trading (Pandas)

Calculate MACD in Python for Algorithmic Trading (Pandas)

Last updated: October 03, 2025

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.

ComponentDefinitionTypical span
MACD lineEMA(12) − EMA(26)12, 26
Signal lineEMA of MACD line9
HistogramMACD − Signal

Quickstart

  1. Install dependencies
pip install pandas numpy yfinance
  1. Load price data (Close), ideally adjusted for splits/dividends.

  2. Compute MACD with pandas ewm and adjust=False.

  3. Create a position rule (e.g., long when MACD > Signal; short otherwise).

  4. Backtest with next-bar execution to avoid lookahead bias.

  5. 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.

Next Article: Generating Real-Time Trading Signals with yfinance and Python

Series: Algorithmic Trading with Python

Python