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.
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.