Benchmark results, in full

Every number on this page comes from a single public run of the benchmark workflow in the engine repository. It installs each engine from PyPI on a GitHub-hosted runner, with no access to the engine source, then compares results before it compares speed. At 10,000,000 bars one backtest with the full metric suite costs 316.6 ms, 308x faster than vectorbt and 2.8x faster than raptorbt. On a grid of 10,000 combinations at 200,000 bars, the gap over raptorbt widens to 16.5x, because only one of these engines has a sweep API.

This page is the whole result, not the flattering half: the workloads where the engines diverge, the grids a challenger could not hold in memory, and the points too noisy to quote are all here. Nothing below has to be taken on trust: the run that produced it is public, log and job summary included, and so is the harness that measured it: workload definitions, parity checks, and the method it all rests on.

Where these numbers came from

Measured
2026-08-20
Commit
6cae68628325
backtests, cold start, memory
Machine
AMD EPYC 7763 64-Core Processor, 4 logical cores, 16.8 GB RAM
OS
Linux 6.17.0-1022-azure x86_64
Python
3.12.14
Engines
manifoldbt 0.17.3, vectorbt 0.28.4, raptorbt 0.9.0, numpy 2.4.3, numba 0.64.0, pandas 2.3.3
parameter sweeps
Machine
AMD EPYC 9V74 80-Core Processor, 4 logical cores, 16.8 GB RAM
OS
Linux 6.17.0-1022-azure x86_64
Python
3.12.14
Engines
manifoldbt 0.17.3, vectorbt 0.28.4, raptorbt 0.9.0, numpy 2.4.3, numba 0.64.0, pandas 2.3.3

A GitHub-hosted runner is a small shared machine, which is the point: it is the floor rather than the ceiling, and anyone can rent the identical one for free. Absolute times on a workstation are lower; the ratios are what travel.

One backtest, start to finish

Median wall time of a single call, the engines interleaved over 3 repetitions so a slow moment on the runner hits all of them equally. Data loading is excluded and the warmup call is discarded, so this is the simulation itself. Every row below produced identical results across the engines: a row where they disagree is never timed here, it is listed further down instead.

Single backtest median wall time by engine, workload and series length
WorkloadBarsmanifoldbtvectorbtraptorbtvs vectorbtvs raptorbt
sma_cross100,0003.2 ms183.1 ms9.8 ms54.9x ~2.7x ~
sma_cross_metrics100,0004.8 ms805.2 ms8.2 ms170x ~1.7x ~
sma_cross1,000,00034.4 ms2.15 s78.7 ms59.1x2.3x
sma_cross_metrics1,000,00033.9 ms8.14 s88.4 ms240x2.6x
sma_cross10,000,000317.2 ms24.75 s878.2 ms77.5x2.8x
sma_cross_metrics10,000,000316.6 ms97.46 s893.8 ms308x2.8x
ema_rsi_fees100,0004.9 ms185.8 msnot run38.2x-
ema_rsi_fees1,000,00052.5 ms2.21 snot run41.4x-
sma_cross_costs100,0003.7 ms180.0 msnot run48.5x-
sma_cross_costs1,000,00035.8 ms2.12 snot run59.1x-
sma_cross_costs10,000,000316.4 ms24.53 snot run77.5x-
multi_asset100,00013.4 ms221.1 msnot run16.5x ~-
multi_asset1,000,000139.9 ms2.34 snot run16.8x-

Ratios are the median of the per-repetition ratios against manifoldbt, not a ratio of medians. A ~ marks a point whose interquartile range exceeded 15% of its median: indicative only.

The parameter sweep that validates a strategy

One backtest tells you almost nothing; the grid around it is the actual job. Manifold-BT runs a grid as one native fan-out call, with the data loaded once and the combinations spread over every core. The other two engines have no sweep API, so a grid there is a Python loop paying a full backtest per cell. Wall time for the whole grid, and the peak RAM each engine adds running it, measured one engine per process:

Parameter sweep wall time and peak memory by engine, grid size and series length
BarsCombinationsmanifoldbtvectorbtraptorbtvs vectorbtvs raptorbtRAM manifoldbtRAM vectorbtRAM raptorbt
20,0005,000445.9 ms5.84 s7.08 s13.1x15.9x40 MB2.5 GB37 MB
20,00020,0001.77 snot run27.88 s-15.7x65 MB--
200,00010,0009.96 snot run164.50 s-16.5x79 MB--

Every grid is checked cell by cell before any of it is timed: the engines are joined on the parameter pair they actually ran, not on position, and the worst disagreement in the grid is what the gate sees. 3 repetitions per point. RAM is probed in a separate single-engine process, because interleaving is what makes a memory reading worthless: a dash there means the probe did not run for that engine at that point, not that it used nothing.

Where a challenger was not run

  • vectorbt, 20,000 combinations at 20,000 bars. vectorbt materialises the simulation per combination (measured: 1.57 MB per combination at 20k bars), so this grid would need tens of gigabytes. Running it would measure the swap file. An empty cell in a speed table reads as a defeat, so the reason is written out instead.
  • vectorbt, 10,000 combinations at 200,000 bars. vectorbt materialises the simulation per combination (measured: 1.57 MB per combination at 20k bars), so this grid would need tens of gigabytes. Running it would measure the swap file. An empty cell in a speed table reads as a defeat, so the reason is written out instead.

Not run, and why

  • raptorbt on ema_rsi_fees. Two blockers, either one sufficient. Sizing: raptorbt has no fixed-quantity mode. `position_sizes` is a fraction of equity (measured: 0.5 buys exactly half the equity of the bar before the entry), `lot_size` rounds a computed size down to a multiple, and `alloted_capital` fixes the notional, not the quantity. Reproducing `units=5` would mean feeding a fraction derived from an equity curve that does not exist until the run is over. Indicator: `raptorbt.ema` seeds on a simple mean of the first `period` bars and emits from bar `period-1`, while manifoldbt seeds on the first observation and emits from bar 0 (measured: 11 leading NaN for span 12, and ema[11] equal to sma(12)[11] to the last bit). The two are the same recursion with a different warmup, so the signal differs early and the round-trip count with it. Its `sma` and `rsi` do match, which is why the other three workloads run.
  • raptorbt on sma_cross_costs. Same blocker as the other fixed-quantity workload: raptorbt has no units sizing, and a cost model on top of a fraction of equity compares policy rather than arithmetic. See `ema_rsi_fees` for the measurements behind that.
  • raptorbt on multi_asset. No multi-instrument portfolio on the entry point this harness drives: `run_single_backtest` is one instrument, and `run_multi_backtest` broadcasts over strategies rather than over assets. 0.9.0 does add `run_portfolio_backtest`, but its allocation model is a different one and would need its own parity work before any timing from it could be published. The units blocker applies here too.

Cold start and memory

The two costs a timing loop hides. Cold start is what a fresh interpreter pays before the first result exists, which is what an agent, a CI job or a one-shot script actually feels. Memory is measured in a dedicated single-engine process, never in the interleaved timing run, where the peak sampled during one engine's call would include every other engine's allocations.

Cold start and memory footprint by engine
manifoldbtvectorbtraptorbt
Fresh process to first backtest0.41 s7.35 s0.40 s
Minus the 0.35 s bare interpreter0.07 s7.00 s0.05 s
RAM added by the run, per million bars (measured at 2,000,000)under 0.1 MB124 MB9.5 MB

What the engines agreed on

A speed number is worth nothing if the engines computed different things, so every workload is compared before it is timed: final equity against starting capital, round-trip count, fees. 13 of the 16 measured points came back exact. 3 carry a known, documented divergence and are kept out of the headline. None disagreed unexpectedly.

Workloads where the engines diverge for a documented reason
WorkloadBarsmanifoldbtvectorbtraptorbt
bracket_sl_tp100,0006.8 ms173.9 ms8.0 ms
bracket_sl_tp1,000,00068.3 ms2.00 s75.2 ms
bracket_sl_tp10,000,000658.9 ms24.64 s870.9 ms
  • vectorbt. Re-entry on the exit bar. When a bracket fires intrabar and the entry condition still holds at that bar's close, manifoldbt books two orders on that bar (the stop or target exit, then a fresh entry at the close); vectorbt processes one order per bar and re-enters on the next bar instead. Neither is wrong, and on controlled bars the bracket fills themselves match exactly (see the cross-engine parity suite shipped with the library). The harness counts the affected round-trips so the size of the divergence is measured, not asserted.
  • raptorbt. Same fork in the road, taken further: raptorbt does not re-arm at all. Once a bracket closes a position, the entry level being still true is not enough to open another one; it waits for the level to go false and true again. So on the same bars the three engines book a different number of round-trips from the same signal, manifoldbt re-entering on the exit bar, vectorbt on the next bar, raptorbt not until the next crossing. The count of affected round-trips is measured rather than asserted, and it is the same population for both challengers.
  • On bracket_sl_tp at 100,000 bars, 925 of 2,309 manifoldbt round-trips re-enter on the exit bar (40%), from 632 stop and 388 target exits. raptorbt books 1,384.

Run it yourself

A self-reported benchmark is worth what its reproduction costs. Fork the repository and press "Run workflow" on Benchmark vs vectorbt and raptorbt to get this table on your own runner, or run the harness locally:

reproduce.sh
# Every engine comes from PyPI, exactly as you would install it.
git clone https://github.com/manifoldbt/manifoldbt
cd manifoldbt/benchmarks/vs_vectorbt
pip install manifoldbt
pip install -r requirements-lock.txt

# Backtests, cold start and memory
python bench.py --bars 100000 1000000 10000000 --reps 3 \
  --cold-start-reps 3 --memory-bars 2000000

# The parameter grids, which need a licence (see below)
python bench.py --workloads sma_cross --bars 100000 \
  --sweep 20000:5000 20000:20000:oos 200000:10000:oos --sweep-reps 3

The sweep points need a licence: an unlicensed fan-out call waits out a fixed interval before any work starts, so the stopwatch would be timing the wait rather than the engine. Everything else runs on the free tier.

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