Realistic backtest execution in Python

The fastest way to fool yourself is a backtest that fills every order instantly, at the close, with no fees. It produces beautiful equity curves and loses money live. Realistic backtesting is about closing the gap between the simulation and the market, here is what that takes, in Python.

The four costs naive backtests ignore

Signal delay

Acting on the same bar that produced a signal silently uses information you would not have had in time. Delaying execution to the next bar removes that free lunch.

Market-impact slippage

Real orders move the market. A flat '2 bps' ignores size; modeling slippage as a function of participation rate punishes large orders in thin bars, as reality does.

Funding

Perpetual futures pay or charge funding every few hours. Over a long hold that is a material cost or income stream that close-only backtests ignore entirely.

Partial fills

You cannot always fill the whole order at one price. Capping fills at a fraction of bar volume models the reality that big positions take time to build.

All four, modeled in one config

Manifold-BT makes each cost an explicit field, so you cannot accidentally backtest a frictionless market. The same run also exposes diagnostics that prove the result is honest: a look-ahead detector and a systematic risk check.

realistic.py
import manifoldbt as mbt
from manifoldbt.indicators import close, ema
from manifoldbt.diagnostics import detect_lookahead, risk_check
from manifoldbt.helpers import time_range, Slippage, Interval

fast, slow = ema(close, 12), ema(close, 26)
strategy = (
    mbt.Strategy.create("realistic")
    .signal("fast", fast)
    .signal("slow", slow)
    .size(mbt.when(fast > slow, 1.0, 0.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(1),
    initial_capital=10_000,
    # Perp funding is applied automatically via the funding_rate column
    fees=mbt.FeeConfig.binance_perps(),
    # Slippage scales with participation rate, not a flat constant
    slippage=Slippage.volume_impact(0.1, exponent=0.5),
    # Act on the NEXT bar after a signal (no acting on the close you just saw)
    execution=mbt.ExecutionConfig(signal_delay=1),
    # Never fill more than 10% of a bar's volume; large orders fill partially
    fill_model=mbt.FillModel.participation(0.1),
    warmup_bars=26,
)
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)

# Prove the result is honest before you trust it
print(detect_lookahead(strategy, config, store))  # look-ahead bias check
print(risk_check(result))                          # leverage, concentration
print(result.summary())

Why this is the whole game

For low-turnover strategies the gap between naive and realistic results is a rounding error. For anything that trades often, scalping, market making, grid, high-frequency mean reversion, it is the difference between a winner and a guaranteed loser. The honest test is simple: run your strategy with realistic costs, then set slippage and fees to zero. If the edge only exists in the second run, it was never real.

Because the engine runs on Rust, all of this, market-impact slippage, funding accrual, partial fills, next-bar execution, costs you nothing in speed: years of bars still backtest in well under a second.

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