Update version numbers across Rust, Python, and documentation files to 1.1.0. Enhance the .gitignore to include macOS dSYM files and plans directory. Introduce new dependencies in the Rust core library and update the README to reflect recent performance benchmarks and backtesting engine capabilities. Add new artifacts to the benchmarks manifest and improve documentation for the backtesting engine API.
ferro-ta Benchmark Suite
Reproducible speed and accuracy comparisons across 62 indicators and the libraries available in your environment.
Overview
The benchmark suite compares ferro-ta against other Python technical-analysis libraries on a common dataset and shared wrappers so the results are easier to reproduce and critique.
It is not designed to prove that ferro-ta wins everywhere. It is designed to show where ferro-ta is faster, where it only ties, and where another library still wins.
| Library | Notes |
|---|---|
| TA-Lib | C extension; widely used comparison baseline |
| pandas-ta | Pure Python; broad indicator set |
| ta | Simple API; some indicators use O(n²) loops and are very slow |
| Tulipy | C extension; truncated output (no leading NaN padding) |
| finta | Expects DatetimeIndex DataFrame; some indicators very slow |
Dataset (LARGE = 100k bars)
All speed benchmarks use the LARGE dataset: 100,000 bars of OHLCV data.
- Source:
benchmarks/data_generator.py— geometric Brownian motion for realistic prices; C-contiguousfloat64arrays for all libraries. - Why 100k: Reflects backtesting and batch workloads; stresses memory and CPU so differences between libraries are clear.
- Scales available:
SMALL(1k),MEDIUM(10k),LARGE(100k). Speed suite uses LARGE by default.
from benchmarks.data_generator import SMALL, MEDIUM, LARGE
# SMALL = 1,000 bars
# MEDIUM = 10,000 bars (e.g. accuracy tests)
# LARGE = 100,000 bars (speed benchmarks)
Methodology
- Harness: pytest-benchmark with
benchmark.pedantic(..., iterations=5, rounds=20, warmup_rounds=2). - Reported metric: Median time per call in microseconds (µs) — lower is better.
- TA-Lib head-to-head JSON:
benchmarks/bench_vs_talib.pyrecords per-run samples, variance stats, machine/runtime/build metadata, and Python-tracked peak allocation snapshots. - Machine info: Stored in the generated JSON artifacts for reproducibility.
- Libraries: Only libraries present in the environment are benchmarked; missing ones are skipped.
Current checked-in TA-Lib artifact
The checked-in benchmarks/artifacts/latest/benchmark_vs_talib.json artifact
uses contiguous float64 arrays at 10k and 100k bars on an Apple M3 Max,
CPython 3.13.5, and Rust 1.91.1 with the default release profile
(lto = true, codegen-units = 1).
- ferro-ta is ahead outside the tie band on 6 of 12 rows at 10k bars and 6 of 12 rows at 100k bars.
- TA-Lib still wins in the current artifact on
STOCHandADX, and remains close onEMA,RSI,ATR, andOBVdepending on size. - The public claim should therefore be read as "often faster on selected indicators," not "faster everywhere."
- When publishing performance statements, point readers to the raw JSON artifact, not just the summary table.
- The artifact now includes per-run samples, variance stats, and Python-tracked allocation snapshots for each compared indicator.
Reproducible Perf Artifacts
Use the perf-contract runner when you want a compact set of machine-readable artifacts for single-series latency, batch throughput, streaming throughput, and hotspot attribution in one directory:
uv run python benchmarks/run_perf_contract.py --output-dir benchmarks/artifacts/latest --skip-talib
That command writes:
indicator_latency.json— canonical-fixture timings for the benchmark suite indicatorsbatch.json— 2-D batch throughput plus grouped multi-indicator timingsstreaming.json— streaming update throughput vs batch baselinesruntime_hotspots.json— ranked hotspot report with reference speedupsmanifest.json— runtime/git metadata plus hashes for the generated artifacts
For CI or local guardrails, validate the hotspot report with:
uv run python benchmarks/check_hotspot_regression.py --input benchmarks/artifacts/latest/runtime_hotspots.json
Speed comparison (100k bars, median µs — lower is better)
The speed table includes all 62 indicators. Number = median µs; N/A = library does not support that indicator. To regenerate: run the full suite, then uv run python benchmarks/benchmark_table.py.
| Indicator | ferro_ta | talib | pandas_ta | ta | tulipy | finta |
|---|---|---|---|---|---|---|
| SMA | 256 | 327 | 425 | 798 | 338 | 856 |
| EMA | 369 | 365 | 427 | 641 | 358 | 722 |
| WMA | 257 | 356 | 433 | N/A | 356 | 112422 |
| DEMA | 444 | 588 | 670 | N/A | 335 | 1830 |
| TEMA | 437 | 768 | 866 | N/A | 358 | 3481 |
| T3 | 462 | 407 | 478 | N/A | N/A | 496 |
| TRIMA | 598 | 400 | 474 | N/A | 386 | 1722 |
| KAMA | 992 | 369 | 140751 | N/A | 367 | 2501 |
| HULL_MA | 547 | N/A | 957 | N/A | 372 | 329392 |
| VWMA | 376 | N/A | 669 | N/A | 391 | N/A |
| MIDPOINT | 1345 | 4685 | N/A | N/A | N/A | N/A |
| MIDPRICE | 1273 | 831 | N/A | N/A | N/A | N/A |
| RSI | 653 | 647 | 728 | 1762 | 404 | 2429 |
| MACD | 833 | 793 | 1058 | 1657 | 423 | 1726 |
| STOCH | 2445 | 941 | 1253 | 3233 | 901 | 3321 |
| CCI | 918 | 1029 | 1122 | 367074 | 676 | 321471 |
| WILLR | 1303 | 750 | 859 | 3409 | 775 | 3575 |
| AROON | 1418 | 587 | 1322 | 130842 | 737 | N/A |
| AROONOSC | 1464 | 586 | N/A | N/A | 773 | N/A |
| ADX | 855 | 746 | 27637 | 321625 | 614 | N/A |
| MOM | 189 | 180 | 254 | N/A | 186 | 352 |
| ROC | 578 | 204 | 272 | 361 | 202 | 463 |
| CMO | 876 | 634 | 707 | N/A | 312 | 2301 |
| PPO | 391 | 538 | 1045 | N/A | 380 | 2395 |
| TRIX | 488 | 831 | 1831 | 1891 | 426 | 1773 |
| TSF | 1519 | 678 | N/A | N/A | 363 | N/A |
| ULTOSC | 2069 | 619 | N/A | 14142 | 588 | N/A |
| BOP | 249 | 228 | 361 | N/A | 226 | N/A |
| PLUS_DI | 794 | 629 | 26792 | N/A | 690 | N/A |
| MINUS_DI | 796 | 600 | N/A | N/A | 642 | N/A |
| BBANDS | 345 | 581 | 1079 | 2163 | 406 | 2432 |
| ATR | 640 | 660 | 800 | 157763 | 370 | 6835 |
| NATR | 722 | 662 | 782 | N/A | 396 | N/A |
| TRANGE | 217 | 205 | 374 | N/A | 199 | 6606 |
| STDDEV | 611 | 408 | 461 | N/A | 400 | 1552 |
| VAR | 1281 | 357 | 398 | N/A | 417 | N/A |
| SAR | 520 | 459 | N/A | N/A | 454 | N/A |
| KELTNER_CHANNELS | 926 | N/A | 1062 | 2369 | N/A | N/A |
| DONCHIAN | 2399 | N/A | 3334 | 3145 | N/A | N/A |
| SUPERTREND | 1242 | N/A | 638613 | N/A | N/A | N/A |
| CHOPPINESS_INDEX | 2442 | N/A | 4892 | N/A | N/A | N/A |
| OBV | 482 | 475 | 592 | 496 | 515 | 4646 |
| AD | 271 | 282 | 424 | 615 | 291 | N/A |
| ADOSC | 482 | 409 | 544 | N/A | 376 | N/A |
| MFI | 350 | 779 | 925 | 433698 | 692 | 401076 |
| VWAP | 288 | N/A | 11460 | N/A | N/A | 880 |
| AVGPRICE | 215 | 211 | N/A | N/A | 229 | N/A |
| MEDPRICE | 203 | 188 | N/A | N/A | 197 | 445 |
| TYPPRICE | 195 | 205 | N/A | N/A | 204 | 435 |
| WCLPRICE | 199 | 197 | N/A | N/A | 210 | 292 |
| SQRT | 204 | 208 | N/A | N/A | 199 | N/A |
| LOG10 | 434 | 408 | N/A | N/A | 411 | N/A |
| ADD | 188 | 186 | N/A | N/A | 189 | N/A |
| LINEARREG | 1555 | 704 | N/A | N/A | 368 | N/A |
| LINEARREG_SLOPE | 1548 | 665 | N/A | N/A | 370 | N/A |
| CORREL | 4277 | 413 | N/A | N/A | N/A | N/A |
| BETA | 5226 | 483 | N/A | N/A | N/A | N/A |
| HT_DCPERIOD | 10864 | 4187 | N/A | N/A | N/A | N/A |
| HT_TRENDMODE | 10984 | 23020 | N/A | N/A | N/A | N/A |
| CDLENGULFING | 308 | 617 | N/A | N/A | N/A | N/A |
| CDLDOJI | 273 | 312 | N/A | N/A | N/A | N/A |
| CDLHAMMER | 304 | 1418 | N/A | N/A | N/A | N/A |
Apple M3 Max, Python 3.13; 273 passed, 121 skipped (unsupported = N/A). Regenerate with Running benchmarks.
Takeaways:
tais 20–350× slower on ATR, CCI, ADX, MFI (O(n²) Python loops).- ferro-ta is often materially faster than pandas-ta on the checked-in 100k-bar table.
- TA-Lib and Tulipy (C extensions) are strong; ferro-ta is competitive and avoids native dependencies.
Running benchmarks
# Full speed suite (100k bars, all indicator × library pairs) — writes results.json
uv run pytest benchmarks/test_speed.py --benchmark-only --benchmark-json=benchmarks/results.json -v
# Head-to-head only (12 indicators × ferro_ta) — quick check
uv run pytest benchmarks/test_speed.py --benchmark-only -k "test_head_to_head" -v
# Large-dataset scaling only (ferro_ta at 100k)
uv run pytest benchmarks/test_speed.py --benchmark-only -k "test_large_dataset" -v
# Regenerate the Speed Comparison markdown table from results.json
uv run python benchmarks/benchmark_table.py
# TA-Lib head-to-head with machine/runtime/build metadata, per-run samples,
# variance stats, and Python-tracked allocation snapshots
uv run python benchmarks/bench_vs_talib.py --sizes 10000 100000 --json benchmark_vs_talib.json
# Selected derivatives analytics comparison (BSM price, IV, Greeks, Black-76)
# against built-in analytical references plus optional installed libraries
uv run python benchmarks/bench_derivatives_compare.py --sizes 1000 10000 --json benchmark_derivatives_compare.json
# Optional regression check used in CI
uv run python benchmarks/check_vs_talib_regression.py --input benchmark_vs_talib.json
# Batch throughput + grouped multi-indicator calls
uv run python benchmarks/bench_batch.py --samples 100000 --series 100 --json batch_benchmark.json
# Streaming update throughput vs batch baselines
uv run python benchmarks/bench_streaming.py --bars 100000 --json streaming_benchmark.json
# Ranked hotspot attribution against bundled reference implementations
uv run python benchmarks/profile_runtime_hotspots.py --json runtime_hotspots.json
# Portable vs SIMD-enabled build comparison
uv run python benchmarks/bench_simd.py --json simd_benchmark.json
# One-shot perf artifact bundle
uv run python benchmarks/run_perf_contract.py --output-dir benchmarks/artifacts/latest
Without uv: use pytest and python from the same environment where ferro_ta and optional libs (e.g. talib, pandas_ta, ta, tulipy, finta) are installed.
Derivatives analytics
benchmarks/bench_derivatives_compare.py focuses on selected options-analytics
paths rather than the full surface area:
BSMcall pricing- call implied-volatility recovery
- call Greeks
Black-76call pricing
The script always includes two analytical baselines:
reference_numpy— pure NumPy formulas with vectorized IV bisectionreference_python_loop— scalarmath-based reference for sanity checking
If py_vollib is installed, it is added automatically as an extra baseline.
The output JSON includes runtime/build metadata, per-run timing samples,
variance stats, and Python-tracked peak allocation snapshots.
WASM
From the wasm/ directory:
wasm-pack build --target nodejs --out-dir pkg
node bench.js --json ../wasm_benchmark.json
Indicator coverage
Overlap (12)
SMA EMA WMA DEMA TEMA T3 TRIMA KAMA HULL_MA VWMA MIDPOINT MIDPRICE
Momentum (18)
RSI MACD STOCH CCI WILLR AROON AROONOSC ADX MOM ROC CMO PPO TRIX TSF ULTOSC BOP PLUS_DI MINUS_DI
Volatility (11)
BBANDS ATR NATR TRANGE STDDEV VAR SAR KELTNER_CHANNELS DONCHIAN SUPERTREND CHOPPINESS_INDEX
Volume (5)
OBV AD ADOSC MFI VWAP
Price Transform (4)
AVGPRICE MEDPRICE TYPPRICE WCLPRICE
Math (3)
SQRT LOG10 ADD
Statistics (4)
LINEARREG LINEARREG_SLOPE CORREL BETA
Cycle (2)
HT_DCPERIOD HT_TRENDMODE
Candlestick patterns (3)
CDLENGULFING CDLDOJI CDLHAMMER
Accuracy results
Accuracy is tested separately; ferro_ta is the reference.
- 243 pairs pass (allclose or correlation).
- 138 pairs skipped (known formula/anchoring/scaling differences).
- 0 failures.
Known structural differences
| Pair | Reason |
|---|---|
| CMO vs talib/pandas_ta/finta | ferro-ta CMO uses different smoothing variant |
| BBANDS vs finta | finta normalizes bands differently |
| ATR vs finta | finta uses simple TR instead of Wilder smoothing |
| VWAP vs pandas_ta | pandas_ta anchors to session start |
| HT_TRENDMODE vs talib | Hilbert Transform seed divergence |
| RSI vs ta/finta | ta/finta use SMA warmup vs Wilder EMA |
| Tulipy ROC | Fraction (0.01 = 1%) vs ferro-ta (1.0 = 1%) |
| Tulipy BBANDS | (lower, mid, upper) order differs from ferro-ta |
# Accuracy tests (62 indicators × 6 libraries)
uv run pytest benchmarks/test_accuracy.py -v
Data generator
benchmarks/data_generator.py:
generate_ohlcv(size)— dict of C-contiguousfloat64arrays:open,high,low,close,volume. High ≥ close ≥ low > 0; volume > 0.get_pandas_ohlcv(data)— DataFrame with DatetimeIndex for pandas-ta and finta.
Pre-built: SMALL, MEDIUM, LARGE (and *_DF variants).
Library compatibility
Detailed notes per library: