Benchmark Results¶
tulip_rs is benchmarked against three established technical-analysis libraries across four benchmark suites.
Competitors¶
| Library | Language | Description |
|---|---|---|
| Tulip Indicators (C) | C | The C library that inspired tulip_rs; primary scalar baseline |
| TA-Lib | C | Industry-standard C technical-analysis library |
| kand | Rust | Pure-Rust TA-Lib–inspired library; 36 indicators benchmarked |
| RustTa | Rust | Rust implementation with streaming support; 20 indicators benchmarked |
| technicalindicators | JavaScript/TypeScript | Popular JS/TS TA library; primary Node.js binding benchmark reference |
| indicatorts | TypeScript | Pure-TS TA library; second Node.js binding benchmark reference |
All timings are nanoseconds (ns) — lower is better. Ratios > 1.00 mean Rust is faster than the competitor.
Benchmark Suites¶
| Suite | Description |
|---|---|
| Standard Performance | Single-asset throughput averaged across 4 option sets, vs C Tulip / RustTa / TA-Lib |
| SIMD | 4-asset parallel (by_assets) and 4-option-set parallel (by_options) vectorised performance |
| Optional Outputs | Single-pass computation advantage for indicators with sub-indicator outputs |
| Streaming | Per-bar stateful update performance vs full batch recomputation |
| Python Binding | tulip_rs_python (PyO3) vs ta (pandas-based) across 35 indicators |
| Node Binding | tulip_rs_node (napi-rs) vs technicalindicators and indicatorts across 41 indicators |
Methodology¶
| Item | Detail |
|---|---|
| Data source | indicator_benchmark PostgreSQL database — latest run per indicator |
| Input data | Real OHLCV market data — 6,705 bars per asset (single ticker) |
| Options | Each indicator is run across 4 option sets (see reference below); reported time is the average across all sets |
| Build flags | -C opt-level=3 -C lto=fat -C target-cpu=native |
| Benchmark harness | Criterion.rs |
| SIMD lane count | N = 4 (256-bit AVX2 f64x4) |
| Comparison method | C libraries called through native FFI; no wrappers inside the timed region |
| Warm-up | Criterion warm-up phase runs before measurement to stabilise CPU frequency and cache state |
N/A entries
"N/A" in a competitor column means that library does not implement the indicator. "—" in a ratio column means the ratio is not applicable (library absent or Rust is the only implementation).
RustTa streaming
RustTa processes data one bar at a time (streaming) so its timings reflect full 6,705-bar throughput across sequential single-bar calls. See Streaming for tulip_rs streaming performance.
Benchmark options reference — all indicators
Times are averaged across the following 4 option sets per indicator. Indicators with no options run a single configuration.
| Indicator | Option sets used (averaged) |
|---|---|
ad |
(no options) |
adosc |
(short=2, long=5) · (short=6, long=20) · (short=5, long=15) · (short=10, long=30) |
adx |
period: 5 · 14 · 24 · 30 |
adxr |
period: 5 · 14 · 24 · 30 |
ao |
(no options) |
apo |
(short=2, long=5) · (short=11, long=21) · (short=5, long=11) · (short=14, long=30) |
aroon |
period: 25 · 35 · 50 · 100 |
aroonosc |
period: 25 · 35 · 50 · 100 |
atr |
period: 5 · 14 · 25 · 30 |
avgprice |
(no options) |
bbands |
(period=5, mult=2) · (period=14, mult=2) · (period=20, mult=2) · (period=50, mult=2) |
bop |
(no options) |
cci |
period: 20 · 25 · 30 · 50 |
chaikinmf |
period: 5 · 14 · 20 · 30 |
chandelierexit |
(period=5, step=2) · (period=14, step=2) · (period=20, step=2) · (period=30, step=2) |
cmo |
period: 5 · 14 · 20 · 30 |
cvi |
period: 5 · 14 · 20 · 30 |
dema |
period: 5 · 14 · 20 · 50 |
di |
period: 5 · 14 · 20 · 30 |
dm |
period: 24 · 14 · 5 · 30 |
donchianchannel |
period: 25 · 35 · 50 · 100 |
dpo |
period: 5 · 14 · 20 · 30 |
dx |
period: 24 · 14 · 5 · 30 |
ef |
period: 5 · 14 · 20 · 30 |
elderray |
period: 5 · 14 · 20 · 30 |
ema |
period: 14 · 20 · 26 · 50 |
emv |
(no options) |
fisher |
period: 25 · 35 · 50 · 100 |
fosc |
period: 5 · 14 · 20 · 25 |
hma |
period: 5 · 14 · 20 · 50 |
kama |
period: 5 · 10 · 14 · 20 |
keltnerchannel |
(period=5, step=2) · (period=14, step=2) · (period=20, step=2) · (period=30, step=2) |
kvo |
(short=2, long=5) · (short=9, long=26) · (short=14, long=30) · (short=20, long=50) |
linreg |
period: 5 · 14 · 20 · 25 |
macd |
(fast=5, slow=13, signal=8) · (fast=19, slow=39, signal=9) · (fast=10, slow=30, signal=10) · (fast=6, slow=20, signal=9) |
marketfi |
(no options) |
mass |
period: 25 · 30 · 50 · 100 |
max |
period: 25 · 35 · 50 · 100 |
md |
period: 5 · 10 · 14 · 25 |
medprice |
(no options) |
mfi |
period: 14 · 20 · 25 · 30 |
min |
period: 25 · 35 · 50 · 100 |
mom |
period: 25 · 30 · 50 · 100 |
msw |
period: 20 · 25 · 30 · 50 |
natr |
period: 14 · 20 · 25 · 30 |
nvi |
(no options) |
obv |
(no options) |
ppo |
(short=2, long=5) · (short=12, long=26) · (short=9, long=20) · (short=8, long=18) |
psar |
(step=0.02, max=0.2) · (step=0.2, max=2.0) · (step=0.04, max=0.4) · (step=0.4, max=4.0) |
pvi |
(no options) |
qstick |
period: 5 · 2 · 8 · 14 |
roc |
period: 25 · 30 · 50 · 100 |
rocr |
period: 25 · 30 · 50 · 100 |
rsi |
period: 14 · 20 · 25 · 30 |
sma |
period: 50 · 100 · 200 · 300 |
smaenvelope |
(period=50, pct=2) · (period=100, pct=2) · (period=200, pct=2) · (period=300, pct=2) |
stddev |
period: 20 · 50 · 100 · 200 |
stoch |
(k=28, ks=16, d=12) · (k=35, ks=21, d=14) · (k=50, ks=30, d=21) · (k=100, ks=50, d=30) |
stochrsi |
period: 14 · 20 · 25 · 35 |
tema |
period: 5 · 14 · 20 · 50 |
tr |
(no options) |
trima |
period: 14 · 20 · 25 · 30 |
trvi |
period: 5 · 14 · 20 · 30 |
trix |
period: 14 · 15 · 20 · 30 |
tsf |
period: 5 · 14 · 20 · 25 |
typprice |
(no options) |
ultosc |
(s=2, m=3, l=5) · (s=10, m=14, l=20) · (s=14, m=20, l=50) · (s=20, m=50, l=100) |
vhf |
period: 25 · 35 · 50 · 100 |
vidya |
(short=9, long=12, α=0.2) · (short=12, long=26, α=0.2) · (short=14, long=30, α=0.2) · (short=14, long=30, α=0.4) |
volatility |
period: 14 · 20 · 25 · 30 |
vortex |
period: 5 · 14 · 20 · 30 |
vosc |
(short=2, long=5) · (short=5, long=20) · (short=10, long=25) · (short=14, long=28) |
vwma |
period: 14 · 20 · 25 · 30 |
wad |
(no options) |
wcprice |
(no options) |
wilders |
period: 20 · 25 · 30 · 50 |
willr |
period: 25 · 35 · 50 · 100 |
wma |
period: 14 · 20 · 25 · 30 |
zlema |
period: 5 · 10 · 14 · 20 |
Summary and Key Findings¶
Standard (single asset)¶
- Rust beats C Tulip for all but 4 indicators:
di(0.73×),dm(0.73×),dx(0.72×),obv(0.97×) mswnow 4.53× faster than C Tulip (was 0.85× — Rust was slower before the SDFT implementation)- Rust beats RustTa for all 20 compared indicators
- Rust beats TA-Lib for all but 5 indicators:
obv(0.94×),psar(0.76×),wma(0.79×),homodynediscriminator(0.92×),mama(0.96×) - Rust beats kand on all 36 compared indicators; gap ranges from 1.07× (ad, obv) to 43× (mfi) and 34× (stoch)
- Median C / Rust ratio: ~1.47× (Rust is ~46% faster on average)
- Largest wins vs C Tulip:
msw(4.53×),cmo(3.64×),min(3.54×),stddev(2.89×),max(2.75×) - Largest wins vs TA-Lib:
hilberttransform(11.93×),macd(5.84×),atr(5.96×),natr(5.81×),rsi(5.30×)
SIMD by_assets¶
- 73% of indicators (68 / 93) show a SIMD speedup over 4× sequential Rust
- Top performers:
roofingfilter(3.46×),mama(3.46×),trendmode(3.42×),supersmoother(3.31×),ccfisher(3.25×),tema(3.13×),dema(2.96×) mswSIMD is now 1.35× faster than 4× sequential (SDFT optimisation)- Indicators that don't benefit are mostly trivial or have inherently sequential dependencies
SIMD by_options¶
- 75% of indicators (56 / 75) show a SIMD speedup over 4× sequential Rust
- Top performers:
trendmode(3.85×),roofingfilter(3.66×),mama(3.50×),supersmoother(3.48×),tema(3.44×),ccfisher(3.31×),di(3.14×),dema(3.05×) mswby_options: 2.13× speedup (SDFT optimisation)- Same non-benefiting indicators as by_assets — the bottleneck is algorithmic, not data layout
Optional Outputs Single-Pass¶
- 35 indicators offer optional outputs computed in a single pass
- C Tulip would require 2–6× more compute time to obtain the same information via separate calls
- TA-Lib multipliers are often higher still due to its slower base times
- Standouts:
natr(10.68× vs TA-Lib),tema(8.35× vs TA-Lib),di(7.81× vs TA-Lib),ppo(7.16× vs TA-Lib)
Combined Advantage: a Worked Example¶
Consider computing tema with all its sub-indicators (dema, ema) across 4 assets simultaneously with SIMD:
| Implementation | Time |
|---|---|
tulip_rs SIMD 4-asset (all outputs, one pass) |
8,099 ns |
| C Tulip — 4 × (tema + dema + ema) sequential | 94,408 ns |
| TA-Lib — 4 × (tema + dema + ema) sequential | 258,992 ns |
Combined speedup
~11.7× faster than C Tulip and ~32.0× faster than TA-Lib for the same result.
Streaming¶
- All 79 indicators implement the stateful streaming path
- Median streaming time: ~27 ns per bar update
- Fastest —
ao,ema,typprice,wildersat 14 ns/bar - Slowest —
mswat 114 ns/bar - The streaming path is 100–5,489× faster than full batch recompute depending on the indicator
Node.js Binding¶
- 41 indicators benchmarked:
tulip_rs_nodevstechnicalindicatorsandindicatorts - vs technicalindicators:
wma(403×),ao(125×),bbands(117×),roc/mfi(~92×),sma(83×),cci(74×) - vs indicatorts:
max(57×),tr(55×),willr(39×),donchianchannel/vortex(~38×),min/atr(~32×) - Median speedup: ~46× vs technicalindicators, ~4.5× vs indicatorts
- Candlestick scanner (81 patterns, single pass): 2.9 ms via Node vs 107 ms via technicalindicators — 36.83× faster
- The napi-rs binding adds ~4–25 µs fixed per-call overhead on top of native Rust computation
Python Binding¶
- 35 indicators benchmarked:
tulip_rs_pythonvsta(pandas-based) tafalls back to pure-Python loops for many indicators —tulip_rs_pythonis up to 20,802× faster (nvi), 6,848× (psar), 1,639× (mfi), 1,030× (atr)tauses pandas/numpy C paths for a few indicators (EMA, BBands, MACD) —tulip_rs_pythonis 3–5× faster there, with PyO3 call overhead narrowing the gap- Median speedup: ~22× across all 35 indicators
- The Python binding adds ~5–25 µs of fixed per-call overhead (GIL + PyO3 marshalling) on top of the native Rust computation