Files
wickra/BENCHMARKS.md
T
kingchenc d59cd44043 docs: standardise language naming + binding security sections (#290)
* docs: standardise language naming and add binding security sections

Canonical binding list everywhere: Rust, Python, Node.js, WASM, C, C++, C#,
Go, Java, R. Use C# (not .NET) as the language label, WASM (not WebAssembly)
in prose, and frame the C ABI as a hub rather than a list item.

- Bump stale indicator counts (200+ -> 514) and family count (sixteen ->
  twenty-four) in the Node/Python/WASM and docs READMEs.
- Add a short Security section to all eight binding READMEs.
- Relabel benchmark rows (C -> C / C++, C# / .NET -> C#).
- Fix the 'language stecker' wording in the C#/Go/R API intros.
- Documentation only; no code or public API changes.

* release.yml: extend install snippets and expose version output

Add the missing registry installs to the release body (dotnet, go, Gradle/
Maven Central, r-universe) alongside cargo/pip/npm, and expose a v-stripped
'version' output from the tag step for the Gradle coordinate. Also fix the
C-ABI language order in the assets note (C# before Go).

* release.yml: correct the release body (10 languages, all registries)

Reframe the tagline to '10 languages' (native Rust/Python/Node.js/WASM + a C
ABI hub for C, C++, C#, Go, Java, R) instead of '4 language registries', note
that C#/Java/Go/R publish to NuGet/Maven/Go/r-universe via their own jobs, and
tidy the Node.js label and the C-ABI hub list.
2026-06-13 23:34:24 +02:00

9.0 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.089 µs ★ 0.96 µs (11×) 422 µs (4 700×)
EMA(20) 0.111 µs ★ 1.19 µs (11×) 430 µs (3 900×)
RSI(14) 0.061 µs ★ 0.95 µs (16×) 298 µs (4 900×)
MACD(12, 26, 9) 0.079 µs ★ 3.30 µs (42×) 327 µs (4 100×)
Bollinger(20, 2) 0.089 µs ★ 4.97 µs (56×) 296 µs (3 300×)

Against the only other incremental Python peer Wickra is 1156× faster; against the recompute-on-every-tick libraries it is 2 80019 000× faster (finta RSI hits 19 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 finta
SMA(20) 22.2 15.6 15.9 32.7 290.1
EMA(20) 30.5 30.4 30.9 46.7 198.5
RSI(14) 52.3 72.0 34.2 88.8 812.3
MACD(12, 26, 9) 129.8 111.1 38.4 286.8 716.7
Bollinger(20, 2) 87.2 74.6 37.9 474.3 1255.5
ATR(14) 74.7 87.3 35.5 3496.4

Wickra beats pandas-ta and finta on every row and TA-Lib on RSI and ATR; tulipy's SIMD C (and TA-Lib on SMA/EMA) lead the remaining rows.

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

3. Per-binding throughput — the cost of the boundary

The sections above compare Wickra against other libraries, which only exists for Python and Rust (there is no comparable streaming TA library for C, C++, C#, Go, Java, R or WASM to benchmark against). Every binding calls the same Rust core, so these per-binding benchmarks are not a speed claim and not a cross-library ratio — they document the raw cost of crossing each language's FFI boundary, in million updates per second (Mupd/s).

Each binding ships a small throughput benchmark that feeds a synthetic OHLCV series through three indicators chosen by call-signature archetype — SMA(20) (1-in → 1-out), ATR(14) (multi-in → 1-out) and MACD(12,26,9) (1-in → multi-out). Two things fall out of the numbers:

  • Batch converges. A batch call crosses the boundary once and the Rust core computes the whole series internally, so batch throughput is roughly the same in every binding — close to the core speed.
  • Streaming reveals the boundary. A per-tick update crosses the boundary once per value, so streaming throughput is where the bindings differ: the raw C ABI and P/Invoke-style calls are nearly free, while managed or interpreted per-call marshalling (cgo, FFM, the R/WASM boundary) costs more per tick.

The Rust core ships the same benchmark with no FFI boundary (examples/rust/.../throughput.rs) — it is the ceiling each binding is measured against and the value the batch paths converge towards.

SMA(20), 200 000 bars, median of 3 runs, on the reference machine (Windows 11, AMD Ryzen 9 9950X):

Target streaming (Mupd/s) batch (Mupd/s)
Rust core (no FFI) 391 500
C / C++ 383 330
C# 337 244
Python 33 488
Java 28 175
Go 24 400
WASM 19 167
Node.js 17 10
R 0.1 193

Streaming spans more than three orders of magnitude — the raw C ABI (383) is nearly the FFI-free Rust ceiling (391), while R's per-call interpreter overhead (0.1) makes streaming ~2000× slower than its own batch. Batch converges near the core speed for the zero-copy bindings (numpy, slices, typed arrays); the two outliers are Node — whose napi batch boxes every element into a JS Array — and R. These are machine-dependent and reflect FFI overhead, not algorithm speed.

These are throughput numbers, not competitive numbers — the "Wickra is fast" claim lives in sections 1 and 2 (Rust core + the Python/Rust cross-library runs).

Run any target's benchmark (build the C ABI library first where it links one):

cargo run -p wickra-examples --release --bin throughput           # Rust core baseline (no FFI)

node bindings/node/benchmarks/throughput.js                       # native napi-rs
( cd bindings/python && python -m benchmarks.throughput )         # native PyO3
( cd bindings/wasm && wasm-pack build --target nodejs --out-dir pkg-node --release ) \
  && node bindings/wasm/benchmarks/throughput.mjs                 # wasm boundary

cargo build -p wickra-c --release                                 # the C ABI hub
cmake -S bindings/c/benchmarks -B build/cbench && cmake --build build/cbench \
  && ./build/cbench/throughput                                    # raw C ABI
dotnet run -c Release --project bindings/csharp/benchmarks        # C# (P/Invoke)
( cd bindings/go/benchmarks && go run . )                         # Go (cgo)
mvn -q -f bindings/java install -DskipTests \
  && mvn -q -f bindings/java/benchmarks exec:exec -Dexec.mainClass=org.wickra.benchmarks.Throughput
Rscript bindings/r/benchmarks/throughput.R                        # R (.Call)