Typical Python backtester
- Future data leaks are hard to detect.
- Float rounding can drift into PnL logic.
- Latency often becomes a single loose assumption.
No look-ahead. No float rounding. No guesswork.
Most research backtesters make it too easy to see the future, blur exchange time with local time, or leak float rounding into PnL. crypto-rs-backtester keeps those failure modes explicit with a deterministic Rust core and a Python research interface.
Problem
Queue position, local observation time, order acknowledgement, and integer money accounting should be first-class parts of the simulation model.
ts_exchange, ts_local, and ts_sim stay separated.i64 keeps the money path integer-only.feed_latency_ns injects nanosecond-scale observation delay.Current capabilities
Use Python for strategy iteration while Rust handles deterministic event ordering, fixed-point accounting, and latency-aware execution.
Strategies only observe market state after feed latency. Order arrivals and acknowledgements are sequenced on the simulator timeline.
Model multi-exchange crypto data, latency, L2/L3 queue logic, and realistic race conditions such as pending cancels.
Feed Polars LazyFrames into the Python API, or use the Arrow C Stream path when large datasets demand minimal copying.
Seeded RNGs, stable tie-breakers for identical timestamps, and lexicographic symbol assignment keep research runs reproducible.
Architecture
The repository is organized as a Rust workspace with a PyO3 wrapper and a Python package named rust_backtester. The core uses event-driven simulation and fixed-point i64 values for monetary logic.
on_tick strategy callbacks.on_ticks and on_order_updates.ts_exchange, ts_local, and ts_sim.Quickstart
pip install crypto-rs-backtester
import polars as pl
from rust_backtester import Backtester
lf = pl.DataFrame({
"ts_exchange": [1000, 2000, 3000, 4000],
"price": [100_00000000, 101_00000000, 99_00000000, 100_00000000],
"qty": [1_00000000, 1_00000000, 1_00000000, 1_00000000],
"side": [1, -1, 1, -1],
"seq": list(range(4)),
}).lazy()
class MyStrategy:
def on_tick(self, tick, ctx):
ctx.submit_order(
symbol_id=int(tick["symbol_id"]),
side=1,
price=int(tick["price"]),
qty=1_00000000,
)
bt = Backtester(
data={"binance:BTC/USDT": lf},
seed=42,
python_mode="tick",
feed_latency_ns=1_000,
)
result = bt.run(MyStrategy())
print(result.stats())
class MyBatch:
def on_ticks(self, ticks, ctx):
for tick in ticks:
ctx.submit_order(
symbol_id=tick["symbol_id"],
side=1,
price=tick["price"],
qty=1_00000000,
)
bt = Backtester(
data={"binance:BTC/USDT": lf},
seed=42,
python_mode="batch",
batch_ms=50,
)
result = bt.run(MyBatch())
# For large datasets, pass a PyArrow RecordBatchReader
# implementing __arrow_c_stream__.
bt = Backtester(
data={},
seed=42,
python_mode="batch",
batch_ms=50,
)
result = bt.run_arrow(
stream=record_batch_reader,
strategy=MyBatch(),
)
Performance
Representative local benchmark output from benchmark/auto_tuning_review_fix_20260130.txt. Throughput estimates use the median time for 1,000,000 synthetic ticks.
Median for bench_event_loop_1m_ticks, about 7.45M ticks/sec.
Median for 4 symbols x 250k ticks, about 5.33M ticks/sec.
Median for the same 1M-tick scenario, about 5.49M ticks/sec.
Status
Technical design lives in docs/SPEC.md with implementation planning in docs/PLAN.md.
Criterion benches can scale symbols, ticks, latency, order cadence, and batch windows through environment variables.
The repository includes a profile-guided optimization pipeline for maximum Rust core throughput on Linux and macOS.
Start now