Interactive backtesting notebook
A Jupyter notebook you can run from the browser: strategy definition, backtest, parameter sweep, walk-forward and Monte Carlo, one cell at a time. The kernel replays a recorded Manifold-BT session, so the outputs and timings are the real ones.
Shift+Enter run and move downCtrl+Enter run in place↑↓ select
Momentum crossover, from raw bars to a verdict
A complete research loop in ten cells: five years of 1-minute bars from Binance and CME, a strategy written in the expression DSL, a backtest with realistic execution, then the three checks that tell you whether the edge is real: a parameter sweep, walk-forward validation and a Monte Carlo bootstrap.
Select a cell and press Shift+Enter, or use Run all in the toolbar. The kernel replays a recorded session, so every output below is what the library actually printed.
import manifoldbt as btfrom manifoldbt.indicators import close, sma, rsi
1. Data
bt.ingest pulls the bars from the provider and stores them as Arrow IPC next to a SQLite catalogue. Binance, Bybit, Hyperliquid and Yahoo Finance are free connectors, Databento and Massive come with Pro. A second call on the same range is a cache hit, so re-running the notebook costs nothing.
# Five years of 1-minute bars, cached as Arrow IPC: BTC perps from Binance,# S&P 500 and gold futures (front month) from CME through Databentostore = bt.ingest(provider="binance",symbol="BTCUSDT",symbol_id=1,start="2021-01-01T00:00:00Z",end="2026-01-01T00:00:00Z",interval="1m",)bt.ingest(provider="databento",dataset="GLBX.MDP3",symbols=[("ES.c.0", 2), ("GC.c.0", 3)],start="2021-01-01T00:00:00Z",end="2026-01-01T00:00:00Z",interval="1m",)
2. Strategy
Indicators are expressions, not arrays. The graph compiles to Rust and runs bar by bar inside the simulator, so there is no way to read a bar that has not closed yet. bt.param marks the two periods as sweepable.
# Momentum crossover in the expression DSL. param() makes the periods# sweepable later without touching the strategy.fast = sma(close, bt.param("sma_fast", default=20))slow = sma(close, bt.param("sma_slow", default=50))momentum = (fast - slow) / slowsignal = bt.when((momentum > 0.01) & (rsi(close, 14) < 70), 1.0,bt.when(momentum < -0.01, -1.0, 0.0),)strategy = (bt.Strategy.create("momentum_crossover").signal("momentum", momentum).size(signal * 0.25).describe("SMA momentum with an RSI overbought filter"))strategy.to_json_dict()["parameters"] # the two knobs the sweep will turn
start, end = bt.time_range("2021-01-01", "2026-01-01")config = bt.BacktestConfig(universe=[1], # BTCUSDTinitial_capital=10_000,time_range_start=start,time_range_end=end,bar_interval=bt.Interval.minutes(1),fees=bt.FeeConfig.binance_perps(),slippage=bt.Slippage.volume_impact(0.1),execution=bt.ExecutionConfig(signal_delay=1, # fill on the next bar, never the same oneallow_short=True,max_position_pct=0.5,),warmup_bars=50,)config.to_json_dict() # exactly what the Rust engine receives
3. Backtest
One call. Five years of 1-minute bars, Binance perp fees, volume-impact slippage, orders filled on the bar after the signal.
result = bt.run(strategy, config, store)result
print(result.profile_summary())
4. Look at it
Every figure is a plotly object: interactive in Jupyter, exportable as PNG or HTML. bt.plot.tearsheet(result, show=True) bundles all of them into one report.
bt.plot.summary(result).show()bt.plot.monthly_returns(result).show()bt.plot.rolling_sharpe(result).show()
5. Is the edge real, or a lucky pair of periods?
The sweep re-runs the backtest on every point of a 332 × 1,664 grid. A robust strategy shows a plateau; a single bright pixel is noise you have just optimised into.
sweep = bt.run_sweep_2d(strategy,{"x_param": "sma_fast", "x_values": list(range(5, 1000, 3)),"y_param": "sma_slow", "y_values": list(range(10, 5000, 3)),"metric": "tstat_alpha",},config,store,)bt.plot.heatmap_2d(sweep)
bt.plot.surface_3d(sweep)
6. Walk-forward
Optimise on a training window, trade the next one blind, move on. Five anchored folds, each re-optimised on its own past. The stitched out-of-sample curve is the strategy as you would actually have traded it.
wf = bt.run_walk_forward(strategy,{"geometry": "anchored","n_splits": 5,"train_ratio": 0.7,"optimize_metric": "sharpe","param_grid": {"sma_fast": [10, 15, 20, 25, 30],"sma_slow": [40, 50, 60, 80, 100],},},config,store,)wf
bt.plot.walk_forward(wf, mode="stitched", full_result=result)
7. Monte Carlo
Bootstrap the trade sequence 10,000 times. If the median path and the 5th percentile both end above the starting capital, the result does not hinge on the order the trades happened to arrive in.
bt.plot.monte_carlo(result, n_simulations=10_000, method="bootstrap", seed=42)
That is the whole loop: data, strategy, backtest, sweep, walk-forward, Monte Carlo. Run any cell straight away: the kernel first runs the cells it depends on, in order, then yours. pip install manifoldbt to run it on your own data.
Same code, your data: pip install manifoldbt