Moving average crossover in Python

The moving average crossover is the 'hello world' of trading systems, here it is in Python, backtested properly.

What is moving average crossover?

A moving average crossover uses two moving averages of different lengths. When the fast one crosses above the slow one, the trend is deemed up; when it crosses below, down. The 50/200 version is the famous 'golden cross' and 'death cross'.

It is simple, transparent, and a fair baseline every systematic trader should know. It lags by design, so it gives back some profit at turns, but it reliably keeps you on the right side of large trends.

The logic

We compute a 50-bar and a 200-bar EMA of the close. EMAs react faster than simple averages while still smoothing noise.

The position is long while the fast EMA sits above the slow EMA, flat otherwise. No stop is needed, the slow average is itself the exit.

moving average crossover 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.

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

# Golden-cross: 50-period EMA over 200-period EMA
fast = ema(close, 50)
slow = ema(close, 200)

strategy = (
    mbt.Strategy.create("ma_crossover")
    .signal("fast", fast)
    .signal("slow", slow)
    .size(mbt.when(fast > slow, 1.0, 0.0))
)

start, end = time_range("2020-01-01", "2026-01-01")
config = mbt.BacktestConfig(
    universe={"binance": ["BTCUSDT"]},
    time_range_start=start,
    time_range_end=end,
    bar_interval=Interval.days(1),
    initial_capital=10_000,
    fees=mbt.FeeConfig.binance_perps(),
    slippage=Slippage.fixed_bps(2),
    warmup_bars=200,
)
store = mbt.ingest(provider="binance", symbol="BTCUSDT", symbol_id=1,
                   interval="1d", start="2020-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

On daily bars the 50/200 cross trades only a handful of times across years, so costs are negligible and the result is dominated by how well it catches major trends.

The two lengths are the natural sweep. Because crossover systems are so few-trade, judge them on robustness across a wide grid rather than the single best pair, which tells you little.

Backtest configuration
Universe{"binance": ["BTCUSDT"]}
Bar interval1d
FeesFeeConfig.binance_perps()
Slippagefixed 2 bps
Warmup200 bars
Sample tearsheet (illustrative, not a forecast)
Total return+128.0%
Sharpe1.14
Max drawdown-26.4%
Win rate47%
Trades29

Frequently asked questions

What is the golden cross?

The golden cross is when a shorter moving average (often 50-period) crosses above a longer one (often 200-period), a widely watched bullish signal. The opposite crossing is the death cross.

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