Test system
v3.0.0reamer_py's execution core is single-threaded by design — these results reflect one CPU core.
What "minimal" and "realistic" mean
Two workloads were run, to separate two different questions.
minimal — pure per-bar callback cost
The strategy reads one price and does nothing else — never submits an order:
def on_bar(self, data):
_ = data["TICKER"].close[-1]
return None
This isolates the fixed overhead of crossing into the strategy once per bar, with nothing else in the way — the fastest reamer_py could possibly go for the given bar count, upper-bounded only by interpreter/FFI overhead.
realistic — 20-bar Donchian channel breakout with real order flow
Enter long on a new 20-bar high, enter short on a new 20-bar low, submitting real market orders through reamer_py's own order management and fill-matching pipeline — the same entry rule used in eval-kit/benchmark_your_machine.py:
def on_bar(self, data):
tv = data["TICKER"]
channel_high, channel_low = tv.high[-21:-1].max(), tv.low[-21:-1].min()
last_close, side = tv.close[-1], tv.position.side
if last_close > channel_high and side <= 0:
return buy_market(1000.0 + tv.position.qty, ticker="TICKER")
if last_close < channel_low and side >= 0:
return sell_market(1000.0 + tv.position.qty, ticker="TICKER")
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. reamer_py's reversal quantity — 1000.0 + tv.position.qty — isn't cosmetic: reamer's netting rule requires qty to exceed the current position size for the excess to open the opposite side (position.qty is 0 when flat, so the formula is a no-op there and only adds the existing opposite position's size when actually reversing) — a flat 1000.0 would just close an opposite position to flat, never reverse it. This workload exercises indicator computation, order submission, and fill matching — the actual cost profile of a strategy that trades, not just reads data. Trade size is 1,000 units, not 1 — a realistic minimum lot size for the FX data used here, not a single share/coin.
Multi-asset: cross-sectional, not per-ticker
on_bar's per-ticker access (data[ticker]) returns a zero-copy view — it's a primitive meant to be gathered into cross-sectional numpy arrays and processed with vectorized operations, not a cue to loop through tickers doing per-ticker computation. Both portfolio results below use the documented pattern:
def on_bar(self, data):
views = [data[t] for t in self.tickers] # N cheap reference reads
highs = np.stack([v.high for v in views]) # (N, lookback)
lows = np.stack([v.low for v in views])
closes = np.stack([v.close for v in views])
channel_high = highs[:, -21:-1].max(axis=1) # one vectorized call across all N tickers
channel_low = lows[:, -21:-1].min(axis=1)
last_close = closes[:, -1]
# ...threshold comparisons and order construction follow the same shape
The gather step is cheap because data[t].high is already a live view into the engine's own buffer, not a copy — the only real cost is the one stacking copy and the vectorized numpy calls that follow, each touching every ticker in a single call into compiled code instead of N separate Python-level calls.
Data & methodology
A synthetic 15-minute-bar OHLCV dataset built from a real historical GBP/USD 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 portfolio tests, each ticker is an independently price-scaled, time-offset slice of that same base series, all sharing one timeline so union alignment treats them as fully overlapping — approximating N different instruments traded over the same 1-year window. reamer_py reads the data as a memory-mapped .bin format directly, no intermediate parsing step in the timed region.
- Each tier runs in its own isolated process, so peak-memory measurements are clean per run, not inflated by whatever a previous tier already had loaded.
- Timing wraps only the actual backtest call, excluding one-time setup (data ingestion, strategy object construction).
- Peak memory measured via the process's own resident-set-size high-water mark.
- Every tier ran sequentially, never in parallel — concurrent runs would contend for the same CPU core and contaminate the single-threaded measurements.
Single ticker, 500,000 bars
~14.25 years of continuous 15-min bars.
| Strategy | Elapsed | Throughput | Trades | Peak RSS |
|---|---|---|---|---|
| minimal | 0.97s | 513,824 bars/s | 0 | 216 MB |
| realistic | 3.77s | 132,593 bars/s | 11,800 | 222 MB |
Single ticker, 5,000,000 bars
~142.5 years of continuous 15-min bars.
| Strategy | Elapsed | Throughput | Trades | Peak RSS |
|---|---|---|---|---|
| minimal | 9.60s | 520,618 bars/s | 0 | 1,749 MB |
| realistic | 39.15s | 127,718 bars/s | 118,069 | 1,952 MB |
Single ticker, 50,000,000 bars
~1,425 years of continuous 15-min bars — well past any realistic single backtest, run specifically to see where reamer_py's own numbers move at real scale.
| Strategy | Elapsed | Throughput | Trades | Peak RSS |
|---|---|---|---|---|
| minimal | 115.30s | 433,648 bars/s | 0 | 12,849 MB |
| realistic | 472.91s | 105,729 bars/s | 1,180,814 | 12,703 MB |
50-ticker portfolio
35,064 bars/ticker (1 year of continuous 15-min bars each, ~1.75M total ticker-bar-instances).
| Strategy | Elapsed | Throughput | Trades | Peak RSS |
|---|---|---|---|---|
| minimal | 1.27s | 27,690 steps/s | 0 | 269 MB |
| realistic | 8.74s | 4,012 steps/s | 41,405 | 388 MB |
"steps/s" = aligned timesteps/second, not ticker-bars/second — on_bar fires once per aligned step across all tickers simultaneously, not once per ticker-bar, so this isn't directly comparable to the single-ticker bars/s figures above.
1,000-ticker portfolio
35,064 bars/ticker (1 year of continuous 15-min bars each, ~35.1M total ticker-bar-instances) — 20x the ticker count of the tier above, same per-ticker depth.
| Strategy | Elapsed | Throughput | Trades | Peak RSS |
|---|---|---|---|---|
| minimal | 21.19s | 1,654 steps/s | 0 | 4,594 MB |
| realistic | 128.54s | 273 steps/s | 827,829 | 6,988 MB |
Raw read throughput (mmap)
A separate measurement from everything above: no strategy, no order matching, no synthetic tick generation — just how fast reamer_py's memory-mapped .bin reader can stream OhlcvBar records off disk once they're paged in. Every field of every record is touched (open, high, low, close, volume, notional, tick count, timestamp) so the compiler can't optimize the read away. This isolates the data layer from the backtest engine — the two numbers below aren't comparable to the bars/s figures elsewhere on this page, which include real computation per bar.
| Configuration | Throughput |
|---|---|
| Single instance | ~185 M records/s |
| 4 independent instances, 4 separate files | ~88 M records/s each, ~352 M records/s aggregate |
The 4-instance figure is genuinely separate processes reading genuinely separate files, each with its own independent memory mapping — not four threads sharing one file. Per-instance throughput drops to roughly half the solo rate under concurrent load (a single instance already asks for close to this machine's full memory bandwidth — around 11-12 GB/s at ~185M records/s × 64 bytes/record — so four of them competing for the same physical memory bus was always going to show some contention), but aggregate throughput still grows with more instances rather than collapsing. Measured on reamer_py v3.2.0, same test machine as above.
Multiple instances, real backtests
reamer_py's execution core is single-threaded by design — the way to run more than one backtest at once is more than one process, each handling its own ticker data independently, not internal multithreading contending over shared state. This measures exactly that: four separate processes, four separate .bin files, each running the full realistic workload (20-bar Donchian breakout, real order flow) from the single-ticker benchmarks above — 884,130 bars each, from the same GBP/USD-derived dataset used elsewhere on this page.
| Configuration | Elapsed (each) | Trades (each) | Peak RSS (each) |
|---|---|---|---|
| Single instance | 6.68s – 6.99s | 20,880 | ~374 MB |
| 4 concurrent instances | 6.59s – 6.93s | 20,880 | ~374 MB |
Per-instance elapsed time barely moves between running alone and running alongside three others — all four finished within a 6.59s–6.93s band, the same range as the solo run. Total wall-clock time for all four to complete was 7.14s: essentially the cost of running one, not four. Unlike the raw read throughput above, real backtest work has enough computation per bar (strategy logic, synthetic tick generation, fill matching) that four independent instances don't meaningfully compete for memory bandwidth the way four bare data reads do — this is the throughput picture that actually reflects how reamer_py is meant to scale. Measured on reamer_py v3.2.0, same test machine as above.
Reading these numbers honestly
- Single-ticker throughput barely moves across the first two orders of magnitude of scale —
minimalactually ticks up slightly from 500K to 5M bars (513,824 → 520,618 bars/s) andrealisticdips only ~4% (132,593 → 127,718 bars/s). The per-bar cost is close to genuinely flat in that range. - The 50M-bar tier is where a real cost shows up: peak RSS crosses 12GB (the whole dataset held resident) and both strategies' throughput drops a further ~15-20% versus the smaller tiers.
realistic's drop tracks trade count more than bar count — 1,180,814 trades at 50M bars is the same ~10x step as 118,069 at 5M, consistent with per-trade bookkeeping cost, not a fixed penalty from data size alone. - Trade counts scale almost exactly proportionally to bar count at every single-ticker tier tested (11,800 → 118,069 → 1,180,814, each a clean ~10x step) — the strategy's signal logic behaves identically regardless of scale, not degrading or drifting as data grows.
- Portfolio throughput scales better than proportionally with ticker count: going from 50 to 1,000 tickers (20x) drops
realisticthroughput only 14.7x, not 20x. Writing the strategy cross-sectionally (see above) means a larger portfolio gets relatively cheaper per ticker, not more expensive — the fixed cost of each vectorized numpy call amortizes across more tickers rather than compounding with them. - These are throughput numbers on zero-cost execution config, run on an ultrabook laptop CPU, single-threaded — not a benchmark of P&L realism, and not a claim about what a production research machine would see.
- Running multiple independent instances scales very differently depending on what those instances are doing. Four concurrent real backtests (real strategy logic, real order flow) finished in essentially the same wall-clock time as one — close to linear scaling. Four concurrent bare data reads (near-zero computation per record) show real per-instance slowdown under concurrent load, because that specific workload is close to saturating this machine's memory bandwidth even running alone. The first number is the one that reflects how
reamer_pyis actually meant to run at scale.
tests/stress/single_ticker_bench.py, tests/stress/portfolio_bench.py) — no hand-tuned scenarios, no cherry-picked runs.