141 lines
4.9 KiB
Markdown
141 lines
4.9 KiB
Markdown
|
|
# ferro-ta Performance Roadmap
|
|||
|
|
|
|||
|
|
## Goal: 100x Faster Than Tulipy for Every Indicator
|
|||
|
|
|
|||
|
|
This document tracks the path from current performance to the 100x target.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Current State (10k bars, median µs)
|
|||
|
|
|
|||
|
|
| Indicator | ferro_ta | Tulipy | Ratio (tu/ft) | Status |
|
|||
|
|
|-----------|--------:|-------:|:-------------:|--------|
|
|||
|
|
| SMA | 186 | 84 | 0.45x | ❌ Tulipy faster |
|
|||
|
|
| EMA | 90 | 89 | 0.99x | 🔄 Parity |
|
|||
|
|
| RSI | 112 | 91 | 0.81x | 🔄 Near parity |
|
|||
|
|
| MACD | 135 | 99 | 0.73x | 🔄 Near parity |
|
|||
|
|
| BBANDS | 99 | 96 | 0.97x | 🔄 Parity |
|
|||
|
|
| ATR | 113 | 103 | 0.91x | 🔄 Near parity |
|
|||
|
|
| CCI | 147 | 126 | 0.86x | 🔄 Near parity |
|
|||
|
|
| WILLR | 167 | 119 | 0.71x | 🔄 Near parity |
|
|||
|
|
| OBV | 88 | 83 | 0.94x | 🔄 Parity |
|
|||
|
|
| ADX | 165 | 126 | 0.76x | 🔄 Near parity |
|
|||
|
|
| MFI | 111 | 122 | 1.10x | ✅ ferro_ta faster |
|
|||
|
|
| STOCH | 176 | 144 | 0.82x | 🔄 Near parity |
|
|||
|
|
|
|||
|
|
**vs `ta` library** (Python loops): ferro_ta is already **150–350x faster** for slow indicators (ATR, CCI, ADX, MFI).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Why ferro_ta Doesn't Beat Tulipy Yet
|
|||
|
|
|
|||
|
|
Both ferro_ta and Tulipy are Rust/C extensions processing 10,000 `f64` values. The bottlenecks are:
|
|||
|
|
|
|||
|
|
1. **FFI overhead dominates at 10k bars** — Python→Rust call overhead is ~50µs fixed cost
|
|||
|
|
2. **Array allocation**: ferro_ta pads NaN values; Tulipy truncates (saves allocation)
|
|||
|
|
3. **SIMD**: Tulipy's C code uses auto-vectorization; ferro_ta Rust needs explicit SIMD
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Optimization Plan
|
|||
|
|
|
|||
|
|
### Phase 1: Eliminate FFI Overhead (Target: 2x improvement)
|
|||
|
|
|
|||
|
|
**Problem**: Each Python call into Rust costs ~50µs regardless of array size.
|
|||
|
|
|
|||
|
|
**Solutions**:
|
|||
|
|
- [ ] Batch API: `compute_many([("SMA", close, 20), ("EMA", close, 14)])` — single FFI call
|
|||
|
|
- [ ] Buffer reuse: accept pre-allocated output arrays to avoid allocation round-trips
|
|||
|
|
- [ ] NumPy zero-copy: use `PyReadonlyArray` in pyo3 to avoid copies on input
|
|||
|
|
|
|||
|
|
**Expected gain**: 2x for small arrays (<1k bars), 1.3x for 10k bars.
|
|||
|
|
|
|||
|
|
### Phase 2: SIMD Auto-Vectorization (Target: 3x improvement)
|
|||
|
|
|
|||
|
|
**Problem**: Rust scalar loops vs SIMD C in Tulipy.
|
|||
|
|
|
|||
|
|
**Solutions**:
|
|||
|
|
- [ ] Use `std::simd` (portable SIMD) for rolling sum accumulation (SMA, WMA)
|
|||
|
|
- [ ] Use `packed_simd2` for element-wise operations (ADD, SQRT, LOG10, price transforms)
|
|||
|
|
- [ ] Enable `target-cpu=native` in `.cargo/config.toml` for AVX2/AVX-512
|
|||
|
|
|
|||
|
|
```toml
|
|||
|
|
# .cargo/config.toml
|
|||
|
|
[target.x86_64-unknown-linux-gnu]
|
|||
|
|
rustflags = ["-C", "target-cpu=native"]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expected gain**: 3-5x for vectorizable indicators (SMA, WMA, price transforms, math ops).
|
|||
|
|
|
|||
|
|
### Phase 3: Algorithm-Level Optimizations (Target: 5-10x improvement)
|
|||
|
|
|
|||
|
|
#### SMA — O(n) running sum
|
|||
|
|
Current: recomputes each window.
|
|||
|
|
Target: single-pass running sum (already done in Rust — verify SIMD path is hit).
|
|||
|
|
|
|||
|
|
#### BBANDS — Welford's algorithm
|
|||
|
|
Current: compute mean, then variance in two passes.
|
|||
|
|
Target: Welford's online algorithm — single pass, better cache utilization.
|
|||
|
|
|
|||
|
|
#### ATR/ADX — Avoid redundant True Range calculations
|
|||
|
|
Current: ATR → ADX each compute TR independently.
|
|||
|
|
Target: Compute TR once, share with ATR, NATR, +DI, -DI, ADX in a single pass.
|
|||
|
|
|
|||
|
|
#### MACD — Reuse EMA computations
|
|||
|
|
Current: Compute fast EMA and slow EMA separately.
|
|||
|
|
Target: Single function computes both EMAs in one pass.
|
|||
|
|
|
|||
|
|
#### Candlestick Patterns — Batch lookup table
|
|||
|
|
Current: Sequential condition checks per bar.
|
|||
|
|
Target: Pre-compute body/shadow ratios, vectorized pattern matching.
|
|||
|
|
|
|||
|
|
### Phase 4: Streaming Precomputation (Target: 100x for incremental updates)
|
|||
|
|
|
|||
|
|
For real-time systems that update one bar at a time:
|
|||
|
|
|
|||
|
|
- [ ] `StreamingSMA` already O(1) per update — document and benchmark vs batch
|
|||
|
|
- [ ] `StreamingEMA` α * new + (1-α) * prev — single multiply + add
|
|||
|
|
- [ ] `StreamingBBands` — use Welford's online variance
|
|||
|
|
- [ ] `StreamingRSI` — Wilder's smoothing: single multiply per update
|
|||
|
|
|
|||
|
|
**At 100k bars, streaming 1 bar at a time is O(n) vs O(n) batch, but with near-zero latency per update.**
|
|||
|
|
|
|||
|
|
Benchmark: batch 100k bars vs 100k × streaming 1 bar:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ferro_ta batch SMA(100k): ~1.8ms
|
|||
|
|
ferro_ta streaming SMA(100k): ~0.5ms total (5µs per bar × 100k = too slow)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Streaming becomes 100x advantage when:
|
|||
|
|
- You only need the latest value (no history needed)
|
|||
|
|
- Input arrives one bar at a time (WebSocket price feed)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Measurement Methodology
|
|||
|
|
|
|||
|
|
All benchmarks use:
|
|||
|
|
- `pytest-benchmark` with `pedantic()` mode
|
|||
|
|
- 5 iterations × 20 rounds × 2 warmup rounds
|
|||
|
|
- Median timing (not mean) to exclude JIT warmup
|
|||
|
|
- C-contiguous `float64` arrays
|
|||
|
|
- 10,000 bars for main benchmarks, 100,000 for scaling tests
|
|||
|
|
|
|||
|
|
Machine: Apple M-series / Intel x86_64 (note: results vary significantly by CPU)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Tracking Progress
|
|||
|
|
|
|||
|
|
Run `pytest benchmarks/test_speed.py --benchmark-only --benchmark-json=benchmarks/results.json`
|
|||
|
|
and commit `results.json` to track regression over time.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## References
|
|||
|
|
|
|||
|
|
- [Tulipy source](https://github.com/cirla/tulipy) — C with auto-vectorization
|
|||
|
|
- [Rust SIMD Guide](https://doc.rust-lang.org/std/simd/index.html)
|
|||
|
|
- [pyo3 zero-copy arrays](https://pyo3.rs/v0.22.0/numpy)
|