Files
QuanTAlib/docs/benchmarks.md
T
Miha Kralj f0d91478e1 docs: document Python wheel matrix and wickra benchmark results
Published the pre-built wheel matrix (win/linux/macOS including arm64) and added wickra columns with fresh numbers to the Python benchmark docs.
2026-09-27 18:00:16 -07:00

11 KiB
Raw Blame History

Benchmarks

Performance claims without measurement: just marketing. QuanTAlib benchmarks against established libraries: TA-Lib (the industry-standard C library accessed via P/Invoke), Wickra (a Rust-core streaming library accessed via FFI), Skender.Stock.Indicators and Ooples.FinancialIndicators (popular .NET implementations).

Test Environment

Component Specification
Data Size 500,000 bars
Period 220 (sufficient to expose algorithmic inefficiencies)
Framework .NET 10.0.12 (10.0.1226.42308), X64 RyuJIT
SIMD AVX-512F+CD+BW+DQ+VL+VBMI
Benchmarking BenchmarkDotNet v0.15.3

These results represent what current-generation CPUs achieve in production. Your mileage varies with older hardware, but relative performance ratios hold.

SIMD and FMA: The Speed Multipliers

The library auto-detects and exploits the highest available instruction set. Processing multiple data points per clock cycle changes everything:

Instruction Set Vector Width Doubles/Cycle Hardware
AVX-512 512-bit 8 Modern server/desktop CPUs
AVX2 256-bit 4 Most x86-64 since 2013
NEON 128-bit 2 ARM processors (Apple Silicon, Raspberry Pi)
Scalar fallback 64-bit 1 Everything else

Fused Multiply-Add (FMA) performs a * b + c in a single cycle with one rounding step. Two advantages compound:

  1. Throughput: Double the floating-point operations per cycle versus separate multiply/add
  2. Precision: Cumulative rounding errors shrink in iterative calculations

In convolution-heavy algorithms (WMA, LinReg, Correlation), SIMD + FMA explains most of the observed speedup. The CPU stops being the bottleneck; memory bandwidth takes over.

Benchmark Results

Simple Moving Average (SMA)

QuanTAlib Span mode: 500,000 SMA values in 288 microseconds. Zero allocations. That works out to 0.58 nanoseconds per value. For perspective: a single L1 cache access takes approximately 1 nanosecond. Moving averages calculating faster than cache fetch.

Library Mean Time Allocations Relative Speed
QuanTAlib (Span) 287.9 μs 0 B baseline
TA-Lib 364.8 μs 32 B 1.27× slower
Wickra 1,561.5 μs 4.0 MB 5.42× slower
Skender 68,377.5 μs 42.0 MB 238× slower
Ooples 346,187.1 μs 151.3 MB 1,202× slower

Skender and Ooples allocate 42-151 MB for what should be a stateless calculation. Garbage collector wakes up, stretches, and ruins everyone's day.

Exponential Moving Average (EMA)

QuanTAlib at 424 microseconds outperforms both the C library TA-Lib (721 μs) and the Rust-core Wickra (1,628 μs). Beating heavily optimized native code with managed code sounds improbable. The secret: FMA instructions on the hot path. Those libraries predate AVX-512 optimizations by a decade.

Library Mean Time Allocations Relative Speed
QuanTAlib (Span) 423.8 μs 0 B baseline
TA-Lib 720.8 μs 32 B 1.70× slower
Wickra 1,627.5 μs 4.0 MB 3.84× slower
Ooples 14,783.5 μs 79.3 MB 34.9× slower
Skender 25,939.1 μs 42.0 MB 61.2× slower

The 1.7× speedup over the C library represents what happens when old code meets new silicon. Modern instruction sets exist; using them helps.

Weighted Moving Average (WMA)

QuanTAlib WMA: 295 microseconds. TA-Lib: 367 μs. Wickra: 1,716 μs. Not a measurement error. Pure C# with proper SIMD vectorization beating C code that predates AVX-512 optimizations.

Library Mean Time Allocations Relative Speed
QuanTAlib (Span) 294.7 μs 0 B baseline
TA-Lib 366.5 μs 32 B 1.24× slower
Wickra 1,715.6 μs 4.0 MB 5.82× slower
Ooples 74,938.8 μs 70.9 MB 254× slower
Skender 101,151.5 μs 42.0 MB 343× slower

WMA involves weighted summation: dot product territory. SIMD shines here. Eight multiplications per cycle instead of one.

Hull Moving Average (HMA)

HMA requires multiple moving average calculations: traditionally expensive. QuanTAlib processes 500,000 bars in 961 microseconds. Wickra: 3,849 μs. Skender: 255,203 μs. TA-Lib lacks HMA implementation entirely.

Library Mean Time Allocations Relative Speed
QuanTAlib (Span) 961.4 μs 0 B baseline
Wickra 3,849.1 μs 4.0 MB 4.00× slower
Ooples 125,720.8 μs 108.7 MB 131× slower
Skender 255,202.5 μs 200.8 MB 265× slower
TA-Lib — — not implemented

A 265× improvement over standard .NET implementations. Compound calculations expose implementation quality: inefficiencies multiply with each nested operation.

Chaikin Oscillator (ADOSC)

Multi-input indicator using high, low, close, and volume. Tests OHLCV data handling efficiency.

Library Mean Time Allocations Relative Speed
QuanTAlib (Span) 596.8 μs 0 B baseline
TA-Lib 665.7 μs 40 B 1.12× slower
Wickra 2,259.6 μs 4.0 MB 3.79× slower
Ooples 108,952.4 μs 569.9 MB 183× slower
Skender 134,249.4 μs 194.0 MB 225× slower

Pearson Correlation

A dual-input statistic computed over two series. QuanTAlib's SIMD batch path (1,338 μs) beats TA-Lib (2,157 μs) — the dual-input case where a C library is left behind, and Skender by 190×.

Library Mean Time Allocations Relative Speed
QuanTAlib (Span) 1,337.6 μs 0 B baseline
TA-Lib 2,157.2 μs 32 B 1.61× slower
Skender 253,912.5 μs 1.88 GB 190× slower

Multi-Mode Comparison

Span mode represents maximum speed. Production code often needs different trade-offs. Here's how all four modes compare using EMA:

QuanTAlib Mode Mean Time Allocations Trade-off
Span 423.8 μs 0 B Maximum throughput, batch processing
Streaming 1,536.8 μs 176 B Real-time updates, minimal overhead
Batch (TSeries) 2,388.1 μs 16.0 MB Time-aligned series with metadata
Eventing 3,381.1 μs 16.8 MB Reactive architectures with event infrastructure

Even QuanTAlib's slowest mode (Eventing with complete event infrastructure, 16.8 MB allocations) processes 500,000 EMA values in 3.4 milliseconds. Still faster than Ooples at 14.8 ms and Skender at 25.9 ms for identical calculation. The "slow" path here beats other libraries' only path.

Python Benchmark: quantalib vs pandas-ta vs wickra

The same indicators benchmarked in C# are also available through Python via NativeAOT shared library + ctypes FFI. This comparison measures the real-world cost of calling QuanTAlib from Python versus pandas-ta (the most popular pure-Python technical analysis library) and wickra (a Rust-core streaming library with a PyO3 binding).

Test Environment

Component Specification
Data Size 500,000 bars
Period 220
Python 3.12.10
NumPy 2.2.6
pandas 3.0.1
pandas-ta 0.4.71b0
wickra 1.0.6 (Rust via PyO3)
quantalib 0.8.12 (NativeAOT via ctypes)

quantalib vs pandas-ta and wickra

Indicator quantalib pandas-ta wickra
SMA 1,298 μs 63,721 μs 12,548 μs
EMA 1,067 μs 4,280 μs 12,179 μs
WMA 1,139 μs 83,903 μs 12,547 μs
HMA 2,647 μs 152,733 μs 14,768 μs
ADOSC 1,621 μs 15,761 μs 40,779 μs
CORRELATION 5,995 μs — 25,062 μs
SKEW 4,187 μs 7,846 μs 19,298 μs

quantalib beats pandas-ta by 1.9-74× and wickra by 4.2-25×. The pandas-ta gap is widest on SMA and WMA: pandas-ta implements SMA as a pure-Python rolling window, and WMA falls back to an element-at-a-time dot product. wickra's Rust core compiles to native code, so its margin is narrower — but its stdlib array.array return and PyO3 FFI boundary still leave it behind on the SIMD hot paths. pandas-ta ships no rolling correlation.

quantalib vs pandas builtins

pandas itself provides optimized C implementations for common rolling operations. Fair comparison:

Indicator quantalib pandas Speedup
SMA 1,298 μs 7,265 μs (rolling.mean) 5.6×
EMA 1,067 μs 3,692 μs (ewm.mean) 3.5×
WMA 1,139 μs 686,466 μs (rolling+apply) 603×
CORRELATION 5,995 μs 35,337 μs (rolling.corr) 5.9×
SKEW 4,187 μs 8,137 μs (rolling.skew) 1.9×

Even against pandas' C-optimized rolling operations, quantalib NativeAOT wins 2-6× on simple indicators. WMA is the extreme case: pandas lacks a native WMA implementation, falling back to rolling().apply() with a Python lambda — 603× slower.

FFI Overhead

The ctypes foreign function interface adds approximately 5-15 microseconds per invocation. At 500,000 bars, this overhead disappears into noise. Below ~100 bars, FFI marshaling dominates and pandas-ta's pure-Python approach wins on latency. Above ~1,000 bars, NativeAOT SIMD takes over decisively.

Methodology

BenchmarkDotNet handles all performance testing. The framework provides:

Feature Why It Matters
Warmup iterations Stabilizes JIT compilation before measurement
Statistical analysis Mean, standard deviation, confidence intervals
Memory tracking Allocation profiling per iteration
Environment isolation Process affinity, GC control, noise reduction

Raw timing loops lie. BenchmarkDotNet tells the truth, even when the truth hurts.

Running Benchmarks Locally

Verify these results on your own hardware. Skepticism is healthy.

Clone the repository:

git clone https://github.com/mihakralj/QuanTAlib.git
cd QuanTAlib

C# Benchmarks

cd perf
dotnet run -c Release

Debug builds include instrumentation that destroys performance measurements. Release configuration or the numbers mean nothing.

Python Benchmarks

cd python
python tests/benchmark.py --bars 500000 --period 220 --iterations 10

Adjust --bars and --period to match your use case. The script degrades gracefully — if pandas-ta or quantalib is unavailable, it benchmarks whatever is installed.

Results vary by CPU generation, but relative ratios (QuanTAlib vs competitors) remain consistent across hardware. The architectures that make something fast stay fast; the ones that allocate memory keep allocating.