Files
wickra/BENCHMARKS.md
T
kingchenc 05fe7ffa90 perf: bit-exact batch fast paths + streaming-first benchmark docs (#202)
## Summary
- Dedicated batch fast paths for **EMA, RSI, Bollinger, MACD and ATR** (used by the Python bindings): one allocation filled in a single pass, warmup encoded as `NaN`, no per-element `Option` or input re-validation. Each is **bit-for-bit equal** to replaying `update` — SMA/Bollinger keep the drift-reseed cadence, the EMA-family keep the seed division and `mul_add` recurrences. Adds the `BatchNanExt` extension trait.
- **Cross-library benchmark refresh**: `compare_libraries.py` reports the median across timing rounds (`--rounds` / `--streaming-rounds`), gains `--skip-batch` / `--skip-streaming`, and runs every peer through the streaming arena (recompute for batch-only libraries). `wickra-bench` drives the batch fast paths against `kand`.
- **README** benchmark section reordered streaming-first (the order-of-magnitude result), with measured TA-Lib/tulipy/pandas-ta numbers in place of the CI-only placeholders.

## Impact
- Python batch ~2× faster on EMA/RSI/MACD/ATR; streaming path unchanged.
- The `batch == streaming` equivalence stays bit-exact.

## Verification
- `cargo fmt` · `cargo clippy --workspace --all-targets --all-features -- -D warnings` (clean)
- `cargo test --workspace --all-features` — 3782 unit + 420 doc tests pass
- Python `pytest` — streaming-vs-batch, known-values, input-validation, smoke pass

## Notes
- Node/WASM bindings keep their existing batch; the fast paths are Python-only for now.
2026-06-08 00:17:58 +02:00

4.8 KiB
Raw Blame History

Benchmarks

Read these as relative speedups on identical input — absolute µs depend on CPU, memory clock and OS scheduler, not a universal contract. Streaming is the headline: it is where Wickra's design pays off and where the gap is measured in orders of magnitude, not percent. The batch numbers come second and are shown honestly — the leanest crates edge Wickra out on the simple recurrences, and that is a deliberate trade for warmup/NaN semantics, not a ceiling.

  • Reproduced on: Windows 11 Pro 26200, AMD Ryzen 9 9950X, 64 GB DDR5, Rust 1.92 (release: lto = "fat", codegen-units = 1), Python 3.12.
  • Reproduce yourself:
    • Rust core vs Rust crates: cargo bench -p wickra-bench
    • Python vs Python libs: pip install -e bindings/python[bench] then python -m benchmarks.compare_libraries (auto-detects installed peers).

1. Streaming — the structural win

Live trading feeds one tick at a time. Wickra updates every indicator in O(1); batch-only libraries (TA-Lib, tulipy, finta, pandas-ta) have no incremental API and must recompute the whole history on every tick. Only talipp (Python) and ta-rs / yata (Rust) carry real per-tick state. This is the gap the library was built to expose.

Python — per-tick latency (seed 5 000 bars, then feed ticks one at a time):

Indicator ★ Wickra talipp TA-Lib (recompute)
SMA(20) 0.063 µs ★ 0.59 µs (9×) 204 µs (3 300×)
EMA(20) 0.060 µs ★ 0.72 µs (12×) 212 µs (3 500×)
RSI(14) 0.065 µs ★ 1.06 µs (16×) 230 µs (3 600×)
MACD(12, 26, 9) 0.078 µs ★ 4.22 µs (54×) 245 µs (3 100×)
Bollinger(20, 2) 0.088 µs ★ 5.15 µs (58×) 229 µs (2 600×)

Against the only other incremental Python peer Wickra is 958× faster; against the recompute-on-every-tick libraries it is 2 60014 000× faster (finta RSI hits 14 000×). tulipy / pandas-ta land in the same recompute band as TA-Lib.

Rust — per-tick latency (whole 50 000-bar series, lower = faster):

Indicator ★ Wickra kand ta-rs yata
SMA(20) 50 38 47 38
EMA(20) 154 69 56 69
RSI(14) 164 216 74
MACD(12, 26, 9) 275 143 66
Bollinger(20, 2) 128 ★ 248 168
ATR(14) 152 166 61

ta-rs hands back a bare f64 from the first tick with no warmup and no validation; it leads several rows by giving those guarantees up. Against kand, Wickra wins streaming RSI, Bollinger and ATR. yata exposes only SMA/EMA as raw-value methods, so its other rows are omitted rather than faked.

2. Batch — competitive, not the headline

Whole series in one call. Here hand-tuned C (tulipy, TA-Lib) and the leanest Rust crate (kand) win the simple recurrences — Wickra trades a few µs per pass for the None-warmup, NaN-safety and bit-exact batch == streaming guarantees none of them keep. It still wins several rows outright and beats the rest of the field everywhere.

Python (20 000-bar pass, µs/op, lower = faster):

Indicator Wickra TA-Lib tulipy pandas-ta
SMA(20) 22.7 15.4 15.9 33.7
EMA(20) 30.8 30.3 31.1 48.8
RSI(14) 58.9 72.5 38.5 94.8
MACD(12, 26, 9) 71.7 99.1 33.5 207.6
Bollinger(20, 2) 84.9 65.7 32.3 336.4
ATR(14) 52.0 79.4 31.9

Wickra beats TA-Lib on RSI, MACD and ATR and the whole Python field on every row; tulipy's SIMD C stays ahead on the heavier indicators.

Rust (50 000-bar pass, µs, lower = faster). Only Wickra and kand expose a batch API; ta-rs and yata are streaming-only:

Indicator ★ Wickra kand
SMA(20) 53 41
EMA(20) 111 71
RSI(14) 221 ★ 259
MACD(12, 26, 9) 533 327
Bollinger(20, 2) 404 ★ 460
ATR(14) 122 ★ 169

Run the suite yourself:

cargo bench -p wickra-bench            # Rust core vs kand / ta-rs / yata
pip install -e bindings/python[bench]  # Python peers
python -m benchmarks.compare_libraries