Skip to content

Kairos Thetadata — cross-SDK performance benchmarks

Measured numbers for the three Kairos SDKs (Rust core, Python facade, TypeScript / Node addon) on identical hardware. Every number in this document is reproducible from a script checked into the repo (bench/cross-sdk/) and traces to a raw results/*.json artefact. No metrics are hand-computed.

Methodology

All three SDKs ride the same Rust analytics core. The Python facade is a PyO3 binding; the TypeScript SDK is a napi-rs 3.x binding. The benchmarks below isolate the per-call boundary cost each binding pays and the steady-state analytic cost the Rust core pays directly. Wall-clock comparisons mean the same iteration shape was run under each runtime; CPU-time comparisons are not attempted (criterion is wall-clock; pytest-benchmark is wall-clock; process.hrtime.bigint() is wall-clock).

Run host

FieldValue
Hostthetagamma-systems
OSLinux 6.8.0-117-generic
Kernel#117-Ubuntu SMP PREEMPT_DYNAMIC Tue May 5 19:26:24 UTC 2026
Archx86_64
CPUIntel(R) Core(TM) i7-10700KF CPU @ 3.80GHz
CPU count16
CPU governorpowersave
Rustrustc 1.88.0 (6b00bc388 2025-06-23)
Cargocargo 1.88.0 (873a06493 2025-05-10)
Python3.14.5
Nodev26.0.0
Run started (UTC)2026-05-21T22:17:42+00:00

What is measured

  • Rust core (criterion): per-tick + per-batch analytic latency on the analytic hot paths, plus the cell-pair dispatch overhead. Criterion runs 50 sample batches with a 1 s warm-up + 5 s measurement window per benchmark function.
  • Python facade (pytest-benchmark): per-call FFI boundary cost — five getter calls on GreekTickFlags, three setter calls on a spec, one Spec.default(filter) round-trip, and one empty-batch replay query end-to-end through ticks_to_record_batch + PyArrow lift.
  • TypeScript SDK (Node): same shape as the Python facade — spec construct, three setter calls, and three getter reads. Replay is not part of the v0.1.0 TypeScript surface and lands in a follow-up release.
  • Live live feed: a single 10-second QQQ Greeks subscribe per SDK. The harness reports ticks observed and first-tick wall-clock latency. The number varies with market activity; the row records the trading session.

What is NOT measured

  • Wire-protocol decode throughput (covered in REPORT.pdf against the official thetadata lib for the thetadatadx core).
  • Cross-host network latency to ThetaData's live feed / historical query backend servers.
  • Steady-state Greeks IV-solver microbenchmarks on the TS / Python sides — both bindings call into the same Rust hot path, so the Rust criterion numbers are the source of truth.
  • The Rust criterion path-cost numbers are NOT directly comparable to the Python / TS per-call FFI numbers below — different work per iteration.

Reproducibility

The cross-SDK harness lives under bench/cross-sdk/ in the source tree. Wall-clock budget: roughly 10 minutes for the Rust benches (cold rebuild dominates), under a minute for Python, under a minute for TS, 10 seconds per live feed subscribe. Re-runs reproduce every number within ±15% on the same host.

Results — Rust core (criterion)

BenchmarkMeanMedianStd devOps / secThroughput
cycle_alternating_q5_t1/disabled88.8 ns87.1 ns5.7 ns11.26 M/s
cycle_alternating_q5_t1/exact_buffered129.0 ns125.3 ns13.6 ns7.75 M/s
cycle_alternating_q5_t1/provisional105.7 ns104.7 ns4.1 ns9.46 M/s
cycle_quote_only/disabled14.7 ns14.1 ns1.9 ns67.84 M/s
cycle_quote_only/exact_buffered23.3 ns19.9 ns5.2 ns42.86 M/s
cycle_quote_only/provisional20.7 ns19.8 ns2.7 ns48.36 M/s
greeks/cached_quiet_10k_option_trades5.03 ms4.92 ms406.17 μs198.7/s10000 elements/iter
greeks/steady_state_10k_option_trades7.34 ms7.34 ms9.77 μs136.3/s10000 elements/iter
greeks/uncached_sweep_10k_option_trades12.94 ms12.77 ms673.26 μs77.3/s10000 elements/iter
ohlcvc/steady_state_30k_trades697.55 μs690.46 μs28.01 μs1.43 K/s30000 elements/iter
ohlcvc_per_tick/sweep_10k_no_emit201.14 μs196.37 μs17.60 μs4.97 K/s10000 elements/iter
ohlcvc_per_tick/trade_only_emit_on_bucket_close61.7 ns59.7 ns6.2 ns16.22 M/s1 elements/iter
ohlcvc_per_tick/trade_only_no_emit25.5 ns22.6 ns7.5 ns39.14 M/s1 elements/iter
volatility_per_tick/warm_grid_alternating_quote_watermark16.39 μs15.92 μs1.95 μs61.02 K/s2 elements/iter
volatility_per_tick/warm_grid_emit_on_watermark16.17 μs15.93 μs842.3 ns61.85 K/s1 elements/iter
volatility_per_tick/warm_grid_quote_no_recompute127.7 ns125.7 ns7.4 ns7.83 M/s1 elements/iter

Results — Python facade (pytest-benchmark)

BenchmarkMeanMinMedianStd devOps / sec
benches/bench_dispatch.py::test_bench_greek_tick_flags_round_trip624.8 ns419.0 ns617.0 ns397.5 ns1.60 M/s
benches/bench_dispatch.py::test_bench_spec_default_construct1.30 μs967.1 ns1.28 μs526.7 ns771.55 K/s
benches/bench_dispatch.py::test_bench_spec_field_mutation437.5 ns287.2 ns417.7 ns1.86 μs2.29 M/s
benches/bench_historical.py::test_bench_record_batch_to_pandas765.34 μs642.05 μs676.25 μs441.56 μs1.31 K/s
benches/bench_historical.py::test_bench_replay_empty_query218.98 μs208.66 μs218.54 μs6.69 μs4.57 K/s

Results — TypeScript SDK (Node, napi-rs)

BenchmarkMeanMinMedianStd devOps / sec
spec_default_construct2.36 μs1.70 μs2.07 μs1.03 μs423.33 K/s
spec_field_mutation334.2 ns299.8 ns335.8 ns15.1 ns2.99 M/s
spec_field_read307.9 ns283.7 ns289.1 ns24.1 ns3.25 M/s

Results — cross-SDK headline

Same operation across three SDKs. Numbers are mean wall-clock per iteration. The Python / TS rows are point-for-point comparable; the Rust column references the closest core benchmark when one exists (otherwise marked n/a — the work the binding pays is the FFI cost, which the core does not have).

OperationRust corePython facadeTypeScript / Node
Spec construct from Filter.symbols(["..."])n/a — no Rust FFI cost1.30 μs2.36 μs
Spec field write — three settersn/a — no Rust FFI cost437.5 ns334.2 ns
FFI getter read loopn/a — no Rust FFI cost624.8 ns307.9 ns
Replay empty-batch query + read columns()n/a — no Rust FFI cost218.98 μsn/a — not in v0.1.0 TS

Per-getter normalisation

BindingGetter countPer-loop costPer-getter cost
Python (PyO3 GreekTickFlags)5624.8 ns125.0 ns
TypeScript (napi-rs VpinRequest)3307.9 ns102.6 ns

The per-getter cost is the comparable number: it normalises out the difference in how many fields each loop reads. PyO3 #[getter] on a #[pyclass] reads a Rust struct field through the GIL-held trampoline; napi-rs #[napi(getter)] issues a JS-side function call into the native addon that returns a primitive. Numbers in this range are dominated by the trampoline itself, not the field-read work.

Headline-table caveats

  • Spec construct from Filter.symbols(["..."]) — Python: VpinParams.default(Filter.symbols(["SPY"])) — one PyO3 trampoline per call. TypeScript: same shape via the napi-rs binding. Directly comparable.
  • Spec field write — three setters — Three setter calls per iteration; spec is constructed out of band. Python: PyO3 #[setter] trampoline. TypeScript: napi-rs #[napi(setter)] trampoline. Directly comparable.
  • FFI getter read loop — Python: five GreekTickFlags getters per iteration (PyO3 #[getter]). TypeScript: three VpinRequest getters per iteration (napi-rs #[napi(getter)]). Not 1:1 — see the per-getter normalisation below the table.
  • Replay empty-batch query + read columns() — Python runs the parquet → RecordBatch → pyarrow lift path end-to-end. Replay is not part of the v0.1.0 TypeScript surface, so the row has no TS counterpart.

Results — live feed (QQQ Greeks, 10 s)

SDKStarted (UTC)Trading sessionConnect wall-clockTicksTicks/secFirst-tick wall-clock
python2026-05-21T20:52:34+00:00after-hours (16:52 EDT)3.34 s00.0/s
ts2026-05-21T20:52:55.206Zafter-hours (Thu 16:52 ET)2.34 s00.0/s

Notes + caveats

  • Warm-up: criterion uses a 1 s warm-up; the Python harness ignores the first round (pytest-benchmark default); the TS harness runs a fixed pre-iteration warm-up before sampling.
  • Iteration shape: criterion measures a iter_batched closure; pytest-benchmark times one function call; the TS harness times a tight for-loop over a closure. Per-iteration overhead differs slightly across the three (criterion's iter_batched adds a few ns the others don't).
  • No-GIL discipline: the Python live path uses streaming_async() so the GIL never blocks the live feed reader thread. The TS live path keeps the napi next() Promise off the libuv event-loop critical path. Different mechanisms, same goal — neither binding stalls the engine.
  • CPU governor: powersave was active for this run. Setting cpufreq-set -r -g performance before a measurement pass can drop criterion variance by 30-50%.
  • Single-run vs averaged: all reported numbers are means across the criterion / pytest-benchmark sample distribution. Raw distributions live in the JSON files under results/.
  • Live live feed: subject to market hours, live feed load, and per-Nexus session limits. The user account is on Pro tier with all asset classes and one Nexus session, so only one SDK runs at a time.
  • Connect cost: the live-feed rows record connect_ns — the wall-clock from Credentials(...) to Client.connect(...) returning. This is dominated by the entitlement round-trip + the live feed TLS handshake; it does not move with binding shape.

Raw JSON locations

All numbers above derive from these files (paths relative to repo root):

  • bench/cross-sdk/results/live-python.json
  • bench/cross-sdk/results/live-ts.json
  • bench/cross-sdk/results/python.json
  • bench/cross-sdk/results/rust-cycle_alternating_q5_t1__disabled.json
  • bench/cross-sdk/results/rust-cycle_alternating_q5_t1__exact_buffered.json
  • bench/cross-sdk/results/rust-cycle_alternating_q5_t1__provisional.json
  • bench/cross-sdk/results/rust-cycle_quote_only__disabled.json
  • bench/cross-sdk/results/rust-cycle_quote_only__exact_buffered.json
  • bench/cross-sdk/results/rust-cycle_quote_only__provisional.json
  • bench/cross-sdk/results/rust-greeks__cached_quiet_10k_option_trades.json
  • bench/cross-sdk/results/rust-greeks__steady_state_10k_option_trades.json
  • bench/cross-sdk/results/rust-greeks__uncached_sweep_10k_option_trades.json
  • bench/cross-sdk/results/rust-ohlcvc__steady_state_30k_trades.json
  • bench/cross-sdk/results/rust-ohlcvc_per_tick__sweep_10k_no_emit.json
  • bench/cross-sdk/results/rust-ohlcvc_per_tick__trade_only_emit_on_bucket_close.json
  • bench/cross-sdk/results/rust-ohlcvc_per_tick__trade_only_no_emit.json
  • bench/cross-sdk/results/rust-volatility_per_tick__warm_grid_alternating_quote_watermark.json
  • bench/cross-sdk/results/rust-volatility_per_tick__warm_grid_emit_on_watermark.json
  • bench/cross-sdk/results/rust-volatility_per_tick__warm_grid_quote_no_recompute.json
  • bench/cross-sdk/results/ts.json

Run python3 bench/cross-sdk/aggregate.py to re-render this file from the on-disk JSON. The script never fabricates numbers — missing JSON shows up as in the tables, not a guess.

Proprietary. All rights reserved.