MACD trading strategy in Python

MACD is one of the most popular momentum indicators, here is a MACD trading strategy in Python, backtested with real costs.

What is MACD?

MACD (moving average convergence divergence) measures the gap between a fast and a slow exponential moving average. A signal line, an EMA of that gap, smooths it. When the MACD line crosses above the signal line, momentum is turning up; below, down.

It is a trend-following oscillator: good at catching sustained moves, prone to whipsaw in flat markets. Its popularity also makes it a useful, well-understood baseline to benchmark more complex ideas against.

The logic

Manifold-BT's macd() returns the MACD line, the signal line, and the histogram from a single call with the classic 12/26/9 periods.

The rule is the textbook crossover: long while the MACD line is above the signal line, flat when it drops below. A stop-loss limits damage during the false signals that flat markets produce.

MACD in Python

Here is the full strategy in the Manifold-BT expression DSL. It imports manifoldbt, builds the signal, configures realistic execution, and runs the backtest against Rust.

macd.py
import manifoldbt as mbt
from manifoldbt.indicators import close, macd
from manifoldbt.helpers import time_range, Slippage, Interval

# MACD with the classic 12 / 26 / 9 periods
macd_line, signal_line, hist = macd(close)

# Long while the MACD line is above its signal line
strategy = (
    mbt.Strategy.create("macd_cross")
    .signal("macd", macd_line)
    .signal("signal", signal_line)
    .size(mbt.when(macd_line > signal_line, 1.0, 0.0))
    .stop_loss(pct=5.0)
)

start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
    universe={"binance": ["BTCUSDT"]},
    time_range_start=start,
    time_range_end=end,
    bar_interval=Interval.hours(4),
    initial_capital=10_000,
    fees=mbt.FeeConfig.binance_perps(),
    slippage=Slippage.fixed_bps(2),
    warmup_bars=35,
)
store = mbt.ingest(provider="binance", symbol="BTCUSDT", symbol_id=1,
                   interval="4h", start="2021-01-01T00:00:00Z",
                   end="2026-01-01T00:00:00Z")
result = mbt.run(strategy, config, store)
print(result.summary())
mbt.plot.tearsheet(result, show=True)

Backtest it with Manifold-BT

We warm up 35 bars so the 26-period EMA and 9-period signal are fully formed before the first trade, skipping this is a classic way to generate phantom early signals.

MACD's three periods are an obvious sweep target. Map Sharpe across fast, slow, and signal lengths and prefer a broad stable region over a single sharp peak, which is almost always overfit.

Backtest configuration
Universe{"binance": ["BTCUSDT"]}
Bar interval4h
FeesFeeConfig.binance_perps()
Slippagefixed 2 bps
Warmup35 bars
Sample tearsheet (illustrative, not a forecast)
Total return+44.2%
Sharpe0.92
Max drawdown-23.5%
Win rate43%
Trades210

Keep reading

Run your first backtest

Install Manifold-BT and reproduce the backtest above in seconds. The Rust core runs years of bars sub-second so you can sweep parameters instead of waiting.

$pip install manifoldbt