Test system
v1.0.01.9.78.123Reamer's execution core is single-threaded by design — these results reflect one CPU core, not a multi-core advantage.
Why Backtrader
There is no certified, industry-standard benchmark suite for backtesting engines. Backtrader is the long-standing standard for retail Python backtesting — the tool an entire generation of retail/indie quants learned on, and still one of the most widely-used. It's been in long-term maintenance mode since 2023 (no major new features, last major commit in 2020), and current community consensus doesn't recommend it for brand-new projects in 2026 — but that doesn't change what it does when it runs.
Maintenance status affects future feature velocity, not the runtime behavior of the version actually benchmarked here, and it remains — like reamer_py — an event-driven engine: both step through the timeline one bar at a time via a per-bar Python callback (next() vs. on_bar()), rather than processing the entire history as a single batch array operation the way a purely vectorized backtester (e.g. VectorBT) does.
on_bar call hands the strategy zero-copy numpy array views (per-ticker lookback windows, or 2D cross-sectional arrays across tickers for multi-asset strategies), a single call can return multiple orders at once via orders(*items), and numpy is a hard dependency because the core engine relies on vectorized execution internally, not just Python object loops.
What reamer_py and Backtrader share, and VectorBT doesn't, is the per-bar event loop — the thing that makes path-dependent state, realistic order management, and tick-level fills possible in the first place. Both engines ask the strategy the same question, once per bar, and the numbers below are what Backtrader actually does today, not a claim about its future.
What "minimal" and "realistic" mean
Two workloads were run against both engines, to separate two different questions.
minimal — pure per-bar callback cost
The strategy reads one price and does nothing else — never submits an order:
# reamer_py
def on_bar(self, data):
_ = data["TICKER"].close[-1]
return None
# Backtrader
def next(self):
_ = self.data.close[0]
This isolates the fixed overhead of crossing into the strategy once per bar, with nothing else in the way — the fastest either engine could possibly go for the given bar count, upper-bounded only by interpreter/FFI overhead.
realistic — moving-average crossover with real order flow
A 5-period fast moving average vs. a 20-period slow moving average on close price, submitting real market orders through each engine's own order management and fill-matching pipeline:
# reamer_py
def on_bar(self, data):
tv = data["TICKER"]
fast, slow = tv.close[-5:].mean(), tv.close.mean()
if fast > slow and tv.position.side <= 0:
return buy_market(1.0, ticker="TICKER")
if fast < slow and tv.position.side >= 0:
return sell_market(1.0, ticker="TICKER")
# Backtrader
def next(self):
pos = self.position.size
if self.fast_ma[0] > self.slow_ma[0] and pos <= 0:
self.buy(size=1.0 - pos)
elif self.fast_ma[0] < self.slow_ma[0] and pos >= 0:
self.sell(size=1.0 + pos)
A position reversal is submitted as a single order sized to flip the net position directly on both sides (not close-then-reopen as two separate orders), so trade counts between the two engines land within a few percent of each other rather than differing by a fixed 2x purely from a counting-convention mismatch. This workload exercises indicator computation, order submission, and fill matching — the actual cost profile of a strategy that trades, not just reads data.
Data & methodology
A synthetic 5-minute-bar OHLCV dataset built from a real historical crypto price series, extended to arbitrary length by tiling it end-to-end with continuous re-stamped timestamps — real price action, not fabricated from scratch. For the 50-ticker portfolio test, each ticker is an independently price-scaled, time-offset slice of that same base series, all sharing one timeline so the two engines' union alignment treats them as fully overlapping — approximating 50 different instruments traded over the same 1-year window.
- Each engine run happens in its own isolated process, so peak-memory measurements are clean per engine, not inflated by whatever the other engine already had loaded.
- Timing wraps only the actual backtest call, excluding one-time setup (data loading into the engine, strategy/indicator object construction).
- Peak memory measured via each process's own resident-set-size high-water mark.
Single ticker, 500,000 bars
~4.75 years of continuous 5-min bars.
| Strategy | reamer_py | Backtrader | reamer_py advantage |
|---|---|---|---|
| minimal | 0.81s (617,103 bars/s) | 71.01s (7,041 bars/s) | 87.6x |
| realistic | 5.69s (87,832 bars/s), 32,066 trades | 84.89s (5,890 bars/s), 33,505 trades | 14.9x |
Single ticker, 5,000,000 bars
~47.5 years of continuous 5-min bars.
| Strategy | reamer_py | Backtrader | reamer_py advantage |
|---|---|---|---|
| minimal | 7.62s (655,942 bars/s) | 744.31s (6,718 bars/s) | 97.6x |
| realistic | 81.14s (61,623 bars/s), 319,529 trades | 1014.32s (4,929 bars/s), 333,794 trades | 12.5x |
50-ticker portfolio
105,192 bars/ticker (1 year of continuous 5-min bars each, ~5.26M total ticker-bar-instances).
| Strategy | reamer_py | Backtrader | reamer_py advantage |
|---|---|---|---|
| minimal | 2.94s (35,818 steps/s) | 812.12s (130 steps/s) | 276.5x |
| realistic | 42.68s (2,464 steps/s), 266,318 trades | 1107.83s (95 steps/s), 351,982 trades | 26.0x |
"steps/s" = aligned timesteps/second, not ticker-bars/second — reamer_py's on_bar fires once per aligned step across all 50 tickers simultaneously, not once per ticker-bar, so this isn't directly comparable to the single-ticker bars/s figures above.
Reading these numbers honestly
- The gap is consistently far larger on
minimal(87-277x) thanrealistic(12-26x) — expected, sinceminimalisolates pure per-bar interpreter overhead (where a compiled C++ core vs. pure-Python architecture is starkest), whilerealisticadds order-matching and bookkeeping work both engines have to do regardless of implementation language. - The portfolio test shows the largest gap of all (276.5x on
minimal) — Backtrader's per-bar synchronization cost across 50 simultaneous data feeds appears to scale worse than linearly with ticker count, not just proportionally. - These are throughput numbers on zero-cost execution config, run on an ultrabook laptop CPU — not a benchmark of P&L realism, and not a comparison against a compiled-core competitor. Backtrader was chosen specifically because it's the long-standing pure-Python standard in this space — what a large fraction of retail/indie quants already know or learned on — not as a broader claim about every backtesting tool on the market.