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.
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.
| Universe | {"binance": ["BTCUSDT"]} |
| Bar interval | 4h |
| Fees | FeeConfig.binance_perps() |
| Slippage | fixed 2 bps |
| Warmup | 35 bars |
| Total return | +44.2% |
| Sharpe | 0.92 |
| Max drawdown | -23.5% |
| Win rate | 43% |
| Trades | 210 |
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.