Skip to content

Repository files navigation

Tick-processing throughput: Rust vs Java vs Go vs Python

How fast can each language replay market ticks through a real backtest kernel?

Not a toy loop — a faithful simplified VolumeSpike backtest: 1-minute bar aggregation, a rolling-volume spike signal, and per-tick position management with a stop-loss and a trailing stop. The same algorithm, statement for statement, in four languages.

Benchmark results

Results

800,000,000 tick-iterations, single core, best wall time of repeated runs.

Language Best wall time Throughput vs Rust Checksum (trades / PnL)
Rust 0.804 s 995 M ticks/s 1.0× 5171 / 12.057009
Java 1.281 s 625 M ticks/s 1.59× slower 5171 / 12.057009
Go 1.516 s 528 M ticks/s 1.89× slower 5171 / 12.057009
Python 118.02 s 6.78 M ticks/s 146.8× slower 5171 / 12.057009

All four checksums are identical, so the programs provably performed the same computation rather than one of them quietly cutting corners.

Workload

  • Input: 5,000,000 real Binance aggTrade ticks (1000PEPEUSDT, 2023-06-21..23), read from one canonical binary file by all four languages.
  • Sweep: 160 parameter configs (8 volume multipliers × 5 stop-losses × 4 trailing stops) — i.e. "the optimization" — so 5,000,000 × 160 = 800,000,000 tick-iterations per run.

The full algorithm is specified in SPEC.md; the measurement write-up is in RESULTS.md.

Fairness controls

  • Zero third-party dependencies in every implementation. Rust is std only (there is no Cargo.toml — it compiles straight with rustc), Java is JDK only, Go's go.mod has no require entries, Python is standard library only: no NumPy, no Numba, no Cython.
  • Only scalar arithmetic in the hot loop: + - * / and comparisons on f64/i64 over flat primitive arrays, plus one integer division for the bar id. No transcendental functions, and nothing fusible into an FMA — which is why the four agree bit-for-bit.
  • Byte-identical input. Each program prints a fingerprint of what it loaded (count, first/last timestamp, price sum); all four matched.
  • The Rust kernel is #[inline(never)], so the compiler cannot inline it into the 160-config sweep and specialise on the parameters. This handicaps Rust rather than flattering it.
  • Single-threaded. Optimized/release build for each language. Data loading is excluded from the timed region. Minimum wall time over repeated runs, with JIT warm-up discarded for Java.
  • Each language measured alone on an unloaded machine. Run-to-run variance was tiny (e.g. Python 118.0161 s vs 118.0171 s).
  • Rust rebuilt without target-cpu=native produced the same 0.804 s. The kernel is branchy and path-dependent, so native tuning buys nothing here — the lead is not a compiler-flag artifact.

Toolchains

rustc 1.96.0 · OpenJDK 26 (Server VM) · go 1.26 · CPython 3.14, on Apple Silicon (macOS, darwin/arm64).

Running it

First produce ticks.bin. Without a Binance dump, generate synthetic ticks — standard library only, deterministic:

python3 gen_synthetic_ticks.py 5000000

With a real Binance parquet dump (columns ts_ns, price_scaled, qty_scaled, one file per day, requires pyarrow):

python3 gen_ticks.py day1.parquet day2.parquet day3.parquet

Then build and run. Every binary takes <ticks.bin> [repeats]:

rustc --edition 2021 -O -C target-cpu=native bench.rs -o bench_rust && ./bench_rust ticks.bin 5
javac Bench.java && java Bench ticks.bin 5
go build -o bench_go bench.go && ./bench_go ticks.bin 5
python3 bench.py ticks.bin 3

rustc invoked directly defaults to edition 2015, where TryInto is not in the prelude, so the --edition flag is required.

Compare the trades= and pnl= fields across the four. If they diverge, the runs are not comparable and the timings mean nothing.

Synthetic data will not reproduce the checksums above — those come from real ticks. It reproduces the property that actually matters: all four implementations must agree with each other on whatever bytes you feed them.

What this does and does not tell you

It measures the backtest and parameter-optimization axis, where you replay history across many configurations. There the compiled trio clusters within roughly 2× of each other and pure CPython sits about 147× behind.

It says nothing about live execution, where an order round-trip is milliseconds of exchange and network latency and the language is a rounding error next to it.

It is also a deliberately non-vectorized comparison, because this kernel cannot be vectorized: every tick depends on the previous position state, the trailing stop and a rolling window. Array-at-a-time approaches such as NumPy or vectorbt do not apply to a sequentially dependent backtest of this shape, which is why the pure-loop number is the relevant one.

Known asymmetry

Java's runBacktest reads the ts / price / qty arrays from static fields, while Rust, Go and Python receive them as parameters. It is the only structural difference among the four implementations and it did not move the result.

About

Rust vs Java vs Go vs Python on an identical tick-driven backtest kernel — 800M tick-iterations, checksum-verified

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages