How to backtest a trading strategy in Python
Backtesting replays a trading rule over historical data to estimate how it would have performed. This guide walks through every step in Python, from raw bars to a tearsheet, using a realistic execution model so the numbers mean something. The full script is at the bottom; run it as-is.
The six steps
- 01
Install Manifold-BT
Install the library with pip install manifoldbt. The Rust core ships in the wheel, so there is nothing else to compile.
- 02
Get market data
Download bars from a supported provider with mbt.ingest(), which returns a DataStore ready to backtest.
- 03
Define the strategy
Express indicators and the entry/exit logic as DSL signals, then set a position-sizing expression.
- 04
Model realistic execution
Configure fees, slippage, the bar interval, and warmup in a BacktestConfig so results are not optimistic.
- 05
Run the backtest
Call mbt.run(strategy, config, store) to execute the whole history on the Rust engine in well under a second.
- 06
Read the tearsheet
Inspect result.summary() and render mbt.plot.tearsheet(result) to judge Sharpe, drawdown, and trade quality.
Step 1, install and load data
Install with pip install manifoldbt. Data comes from mbt.ingest(), which downloads bars from a provider such as Binance or Hyperliquid and returns a DataStore. Nothing touches the network until you call it, and the store is cached locally for the next run.
Steps 2 to 4, define the strategy and model execution
A strategy is built from indicator expressions and a sizing rule. The most common beginner mistake is ignoring costs: a rule that looks profitable at zero fees often dies once you add exchange fees, a few basis points of slippage, and a stop. Manifold-BT makes those explicit in the BacktestConfig, so you cannot accidentally backtest a frictionless market.
Steps 5 and 6, run it and read the tearsheet
mbt.run() executes the full history on the Rust core, typically in well under a second, so you can iterate quickly. result.summary() prints the headline metrics, and mbt.plot.tearsheet() renders an equity curve, drawdown, and trade statistics. Here is the complete, runnable script:
import manifoldbt as mbt
from manifoldbt.indicators import close, ema
from manifoldbt.helpers import time_range, Slippage, Interval
# 1. Indicators are expression objects, evaluated later on the Rust core
fast = ema(close, 20)
slow = ema(close, 50)
# 2. Go long when the fast EMA is above the slow EMA, flat otherwise
signal = mbt.when(fast > slow, 1.0, 0.0)
strategy = (
mbt.Strategy.create("ema_crossover")
.signal("fast", fast)
.signal("slow", slow)
.size(signal)
.stop_loss(pct=3.0)
)
# 3. Model realistic execution: fees, slippage, bar interval, warmup
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(1),
initial_capital=10_000,
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=50,
)
# 4. Download data, then run the whole history on Rust
store = mbt.ingest(provider="binance", symbol="BTCUSDT", symbol_id=1,
interval="1h", start="2021-01-01T00:00:00Z",
end="2026-01-01T00:00:00Z")
result = mbt.run(strategy, config, store)
# 5. Read the results and render a tearsheet
print(result.summary())
print("Sharpe:", result.metrics["sharpe"])
mbt.plot.tearsheet(result, show=True)Avoid the classic traps
A backtest is only as honest as its assumptions. Watch for look-ahead bias (using data a bar before it exists), overfitting to one lucky period, and survivorship bias in your symbol list. Set warmup_bars to at least your longest indicator window so early NaN-dominated bars do not generate phantom signals, and validate a promising rule out-of-sample with a parameter sweep before trusting it.
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.