A Backtesting.py alternative for when one instrument is not enough

Backtesting.py is the library most people write their first Python backtest in, and for good reason: a Strategy class, an init() and a next(), an interactive chart and a built-in optimizer, all on a DataFrame you already have. Manifold-BT is what you reach for when that first backtest raises the questions it cannot answer: what happens across a universe, what the strategy earns after funding and partial fills, and whether the parameters that won survive out of sample. Here is where each one stops, honestly.

Two sizes of the same problem

Backtesting.py makes a deliberate trade. It backtests one instrument at a time, from a plain OHLCV DataFrame, with a strategy that is a class whose next() runs once per bar in Python. Costs are a commission (fixed, relative, or a callable), a spread, and a margin ratio; orders take a stop and a target, and a stop is checked before a target on a bar that hits both. The optimizer is genuinely good: a grid or the SAMBO model-based search, in parallel across processes, returning a heatmap. It is open source under AGPL-3.0, maintained, at version 0.6.6 as of July 2026, and its documentation is a model of clarity. Within its scope there is little to criticise.

Manifold-BT is built for the questions that scope leaves out. A strategy is an expression graph, evaluated vectorized in Rust over a whole universe in one shared account, then run through a sequential fills pass where costs are explicit settings: maker and taker fees, market-impact slippage, funding on perpetuals, partial fills, and a signal delay so bar t decides and bar t+1 fills. A parameter grid is one native call across every core, and walk-forward, Monte Carlo, stability maps and look-ahead detection sit next to it rather than in a notebook you write yourself. The cost of that is a different mental model: no next() to step through in a debugger, and a DSL to learn instead of plain Python control flow.

Side by side

 Manifold-BTBacktesting.py
ScopeA universe in one shared account, cross-asset references, portfolios of strategiesOne instrument per backtest; MultiBacktest reruns a strategy over several datasets
Core engineRust: vectorized signals, then a sequential fills passPure Python: init() precomputes, next() runs once per bar
Strategy modelAn expression graph: indicators, a signal, a sizing ruleA Strategy subclass with init() and next(), buy() and sell()
Costs modelledMaker/taker fees, market-impact slippage, funding on perpetuals, partial fills, signal delayCommission (fixed, relative or callable), spread, margin, trade-on-close
Stops and targetsBracket exits with gap semantics: a gap through the stop fills at the opensl/tp per order, stop checked before target on the same bar
Parameter searchOne native call across every core, 2-D heatmaps, GPU on Prooptimize(): grid or SAMBO, in parallel across processes, with a heatmap
ValidationWalk-forward, Monte Carlo, stability maps, look-ahead detection, built inYours to build on top of optimize()
DataFree connectors (Binance, Dukascopy, Yahoo, dYdX...), CSV, DataFrameBring your own OHLCV DataFrame
Indicators104 built in, 38 candlestick patterns, higher-timeframe and cross-assetBring your own (pandas, TA-Lib, anything callable)
ChartsHTML tearsheet, Plotly candlesticks with trades, heatmaps, fan chartsInteractive Bokeh chart of equity, trades and indicators
Licence and priceCommunity free; Pro $19/month or $290 lifetimeFree, open source, AGPL-3.0

The same crossover, both ways

The SMA crossover from the Backtesting.py quick start, as written in its own documentation:

sma_cross_bpy.py
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import GOOG, SMA

# Backtesting.py: a class, a bar-by-bar next(), one instrument
class SmaCross(Strategy):
    n1 = 10
    n2 = 20

    def init(self):
        self.sma1 = self.I(SMA, self.data.Close, self.n1)
        self.sma2 = self.I(SMA, self.data.Close, self.n2)

    def next(self):
        if crossover(self.sma1, self.sma2):
            self.position.close()
            self.buy()
        elif crossover(self.sma2, self.sma1):
            self.position.close()
            self.sell()

bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002)
stats = bt.run()
stats = bt.optimize(n1=range(5, 30, 5), n2=range(10, 70, 5),
                    maximize='Equity Final [$]',
                    constraint=lambda p: p.n1 < p.n2)

The same idea in Manifold-BT. The class becomes an expression, the commission becomes fees plus slippage plus a one-bar delay, and the same grid runs as one native call:

sma_cross_mbt.py
import manifoldbt as mbt
from manifoldbt.indicators import close, sma
from manifoldbt.helpers import time_range, Slippage, Interval

# Manifold-BT: the same crossover as an expression, long and short
fast = sma(close, mbt.param("n1", default=10))
slow = sma(close, mbt.param("n2", default=20))
strategy = (
    mbt.Strategy.create("sma_cross")
    .signal("fast", fast)
    .signal("slow", slow)
    .size(mbt.when(fast > slow, 1.0, -1.0))
)

start, end = time_range("2020-01-01", "2026-01-01")
config = mbt.BacktestConfig(
    universe={"yahoo": ["GOOG"]},
    time_range_start=start, time_range_end=end,
    bar_interval=Interval.days(1),
    initial_capital=10_000,
    trading_days_per_year=252,
    execution=mbt.ExecutionConfig(signal_delay=1, allow_short=True),
    fees=mbt.FeeConfig(taker_fee_bps=20.0, maker_fee_bps=20.0),
    slippage=Slippage.fixed_bps(2),
    warmup_bars=20,
)
store = mbt.ingest(provider="yahoo", symbol="GOOG", symbol_id=1,
                   interval="1d", asset_class="equity",
                   start="2019-01-01T00:00:00Z", end="2026-01-01T00:00:00Z")
result = mbt.run(strategy, config, store)

# The grid runs as one native call across every core
sweep = mbt.run_sweep(strategy,
                      param_grid={"n1": range(5, 30, 5), "n2": range(10, 70, 5)},
                      config=config, store=store)
print(sweep.best("sharpe"))

Two things changed that are not cosmetic. The Manifold-BT run fills the bar after the signal, because a close you can see is a close you cannot trade at, and it charges fees on both sides of a reversal, which is what a reversal costs. Turn those off and the two engines are asking the same question; leave them on and the second one is asking the one the market will ask.

Where the first backtest stops

The moment usually arrives in one of three shapes. You want the strategy on twenty markets with one account, and one instrument per run means twenty runs and a spreadsheet to combine them; MultiBacktest reruns the strategy across datasets, but it does not share the capital. You trade perpetuals and the funding you paid every eight hours is not in the result, or you trade thin markets and the fill you assumed at the close is not the fill you would have got. Or the grid search found a winner and you want to know whether it survives the next year, which is a walk-forward, and how bad the drawdown gets on a resampled path, which is a Monte Carlo, and both are yours to write.

Those are not flaws in Backtesting.py; they are outside what it set out to do, and it says so. They are what Manifold-BT set out to do. The one thing to know before moving is that no benchmark exists between the two, so this page makes no claim about how much faster a single run is. The design is built for large grids and full universes, and the published benchmarks against vectorbt and RaptorBT are where the numbers live.

Which should you pick?

Choose Backtesting.py to learn, to test one idea on one instrument, and whenever a class with a next() you can step through is worth more to you than realism: it is free, clear, and quick to reach a first number. Choose Manifold-BT when the first number raises the next question: a universe in one account, costs the market will actually charge, and sweeps, walk-forward and Monte Carlo that run on every core instead of in a notebook you maintain.

Frequently asked questions

Is Backtesting.py good enough?

For a first pass on one instrument, yes, and it is one of the best-documented ways to get there. Its limits are the ones its design accepts on purpose: one instrument per backtest, a commission rate and a spread as the cost model, a Python loop per bar, and validation beyond a grid search left to you. Those are the exact points where a strategy that looked good starts to look different, so the question is less whether the library is good and more when you outgrow it.

Backtesting.py vs vectorbt vs Manifold-BT: which should I use?

Backtesting.py is the simplest: one instrument, a class with next(), an interactive chart, a built-in optimizer. vectorbt is the fastest way to test signal-heavy ideas vectorized in NumPy, at the cost of path-dependent logic being awkward. Manifold-BT sits at the research end: a Rust core, vectorized signals with a sequential fills pass so stops and partial fills are modelled, multi-asset universes, and sweeps, walk-forward and Monte Carlo built in. Start with Backtesting.py to learn, move when you need a universe, realistic costs, or validation you do not want to write yourself.

Can I reuse my Backtesting.py strategies in Manifold-BT?

Not as-is: Backtesting.py strategies are classes with a per-bar next(), and Manifold-BT strategies are expression graphs the engine evaluates vectorized. The rewrite is usually short, because most next() methods reduce to a condition and a size, which is exactly what the DSL expresses. What you keep is your data, since Manifold-BT ingests the same OHLCV DataFrames, and your intuition about the strategy.

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