feat: init the repo

This commit is contained in:
Pratik Bhadane
2026-03-23 23:34:28 +05:30
commit 7a5a220dfe
344 changed files with 75728 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
# ferro-ta Benchmark Suite
> **62 indicators × 6 libraries** — accuracy and speed verified on **100,000 bars** (LARGE dataset).
## Overview
The benchmark suite compares **ferro-ta** against five popular Python technical-analysis libraries on a common dataset and shared wrappers so timings are directly comparable.
| Library | Notes |
|-----------|-------|
| **TA-Lib** | C extension; gold standard for accuracy and speed |
| **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-contiguous `float64` arrays 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.
```python
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](https://pytest-benchmark.readthedocs.io/) with `benchmark.pedantic(..., iterations=5, rounds=20, warmup_rounds=2)`.
- **Reported metric:** **Median time per call** in **microseconds (µs)** — lower is better.
- **Machine info:** Stored in `benchmarks/results.json` (`machine_info`, `commit_info`) for reproducibility.
- **Libraries:** Only libraries present in the environment are benchmarked; missing ones are skipped.
---
## 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](#running-benchmarks).*
**Takeaways:**
- **`ta`** is 20350× slower on ATR, CCI, ADX, MFI (O(n²) Python loops).
- **ferro-ta** is typically 24× faster than **pandas-ta** across indicators.
- **TA-Lib** and **Tulipy** (C extensions) are strong; ferro-ta is competitive and avoids native dependencies.
---
## Running benchmarks
```bash
# 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-readable summary + git/runtime metadata
uv run python benchmarks/bench_vs_talib.py --sizes 10000 100000 --json benchmark_vs_talib.json
# Optional regression check used in CI
uv run python benchmarks/check_vs_talib_regression.py --input benchmark_vs_talib.json
```
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.
---
## 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 |
```bash
# 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-contiguous `float64` arrays: `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:
- [TA-Lib](../docs/compatibility/talib.md)
- [pandas-ta](../docs/compatibility/pandas_ta.md)
- [ta](../docs/compatibility/ta.md)
- [Tulipy](../docs/compatibility/tulipy.md)
- [finta](../docs/compatibility/finta.md)
+1
View File
@@ -0,0 +1 @@
"""benchmarks package — cross-library accuracy and speed comparison suite."""
+65
View File
@@ -0,0 +1,65 @@
import time
import numpy as np
import ferro_ta
def _time_fn(fn, *args, **kwargs):
times = []
# Warmup
fn(*args, **kwargs)
for _ in range(5):
t0 = time.perf_counter()
fn(*args, **kwargs)
times.append(time.perf_counter() - t0)
return min(times)
def main():
n_samples = 100_000
n_series = 100
print(f"Batch Benchmark: {n_samples} bars, {n_series} series (Total: {n_samples*n_series/1e6:.1f} M bars)")
np.random.seed(42)
# contiguous array in row-major
close2d = np.random.uniform(100.0, 200.0, (n_samples, n_series))
h2d = close2d + np.random.uniform(0.1, 2.0, (n_samples, n_series))
l2d = close2d - np.random.uniform(0.1, 2.0, (n_samples, n_series))
print("-" * 50)
print(f"{'Indicator':<15} {'Batch (ms)':>12} {'Loop (ms)':>12} {'Speedup':>10}")
print("-" * 50)
# 1. SMA
kwargs = {"timeperiod": 14}
def loop_sma(arr):
for j in range(arr.shape[1]):
ferro_ta.SMA(arr[:, j], **kwargs)
t_batch_sma = _time_fn(ferro_ta.batch.batch_sma, close2d, **kwargs)
t_loop_sma = _time_fn(loop_sma, close2d)
print(f"SMA {t_batch_sma*1000:12.1f} {t_loop_sma*1000:12.1f} {t_loop_sma/t_batch_sma:9.1f}x")
# 2. RSI
def loop_rsi(arr):
for j in range(arr.shape[1]):
ferro_ta.RSI(arr[:, j], **kwargs)
t_batch_rsi = _time_fn(ferro_ta.batch.batch_rsi, close2d, **kwargs)
t_loop_rsi = _time_fn(loop_rsi, close2d)
print(f"RSI {t_batch_rsi*1000:12.1f} {t_loop_rsi*1000:12.1f} {t_loop_rsi/t_batch_rsi:9.1f}x")
# 3. ATR
def loop_atr(h, l, c):
for j in range(h.shape[1]):
ferro_ta.ATR(h[:, j], l[:, j], c[:, j], **kwargs)
t_batch_atr = _time_fn(ferro_ta.batch.batch_atr, h2d, l2d, close2d, **kwargs)
t_loop_atr = _time_fn(loop_atr, h2d, l2d, close2d)
print(f"ATR {t_batch_atr*1000:12.1f} {t_loop_atr*1000:12.1f} {t_loop_atr/t_batch_atr:9.1f}x")
# 4. ADX
def loop_adx(h, l, c):
for j in range(h.shape[1]):
ferro_ta.ADX(h[:, j], l[:, j], c[:, j], **kwargs)
t_batch_adx = _time_fn(ferro_ta.batch.batch_adx, h2d, l2d, close2d, **kwargs)
t_loop_adx = _time_fn(loop_adx, h2d, l2d, close2d)
print(f"ADX {t_batch_adx*1000:12.1f} {t_loop_adx*1000:12.1f} {t_loop_adx/t_batch_adx:9.1f}x")
if __name__ == '__main__':
main()
+105
View File
@@ -0,0 +1,105 @@
"""
GPU vs CPU benchmark for ferro_ta.gpu (SMA, EMA, RSI).
Requires:
pip install "ferro-ta[gpu]" # or pip install torch
Run:
python benchmarks/bench_gpu.py
The script compares wall-clock time for 1M-element arrays and prints a
summary table. If PyTorch is not installed or no GPU is found, GPU columns are skipped.
"""
from __future__ import annotations
import time
import numpy as np
# Try to import PyTorch
try:
import torch
TORCH_AVAILABLE = True
if torch.cuda.is_available():
DEVICE = "cuda"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
DEVICE = "mps"
else:
DEVICE = None
except ImportError:
torch = None # type: ignore[assignment]
TORCH_AVAILABLE = False
DEVICE = None
from ferro_ta.gpu import ema, rsi, sma
N = 1_000_000
REPEATS = 10
def _time_fn(fn, *args, **kwargs) -> float:
"""Return minimum wall time (seconds) over REPEATS calls."""
times = []
for _ in range(REPEATS):
t0 = time.perf_counter()
fn(*args, **kwargs)
if DEVICE == "cuda":
torch.cuda.synchronize()
elif DEVICE == "mps":
torch.mps.synchronize()
times.append(time.perf_counter() - t0)
return min(times)
def main() -> None:
rng = np.random.default_rng(42)
close_cpu = rng.uniform(100.0, 200.0, N)
print(f"Array size: {N:,} elements")
print(f"Repeats: {REPEATS}")
print(f"Device: {DEVICE if DEVICE else 'CPU'}")
print()
header = f"{'Indicator':<20} {'CPU (ms)':>10}"
if DEVICE:
header += f" {'GPU (ms)':>10} {'Speedup':>10}"
print(header)
print("-" * len(header))
for name, fn, kwargs in [
("sma(period=30)", sma, {"timeperiod": 30}),
("ema(period=30)", ema, {"timeperiod": 30}),
("rsi(period=14)", rsi, {"timeperiod": 14}),
]:
cpu_time = _time_fn(fn, close_cpu, **kwargs) * 1000 # ms
row = f"{name:<20} {cpu_time:>10.3f}"
if DEVICE:
dtype = torch.float32 if DEVICE == "mps" else torch.float64
close_gpu = torch.tensor(close_cpu, dtype=dtype, device=DEVICE)
# Warm-up
fn(close_gpu, **kwargs)
if DEVICE == "cuda":
torch.cuda.synchronize()
elif DEVICE == "mps":
torch.mps.synchronize()
gpu_time = _time_fn(fn, close_gpu, **kwargs) * 1000 # ms
speedup = cpu_time / gpu_time
row += f" {gpu_time:>10.3f} {speedup:>10.2f}×"
print(row)
if not TORCH_AVAILABLE:
print()
print("PyTorch not available — GPU columns skipped.")
print("Install with: pip install 'ferro_ta[gpu]'")
elif not DEVICE:
print()
print(
"PyTorch found, but no CUDA or MPS device detected — GPU columns skipped."
)
if __name__ == "__main__":
main()
+371
View File
@@ -0,0 +1,371 @@
"""
ferro_ta vs TA-Lib speed comparison.
Measures throughput (M bars/s) for both libraries on the same data and parameters,
and reports speedup (talib_time / ferro_ta_time; > 1 means ferro_ta is faster).
Requirements:
pip install ta-lib # or conda install ta-lib
Run:
python benchmarks/bench_vs_talib.py
python benchmarks/bench_vs_talib.py --json results.json
python benchmarks/bench_vs_talib.py --sizes 10000 100000 # default: 10k, 100k, 1M
If ta-lib is not installed, the script still runs and reports ferro_ta timings only (no speedup).
Methodology: same synthetic data, same parameters, median of 7 runs after warmup.
Environment: document Python version and OS when publishing results.
"""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
import json
import platform
import subprocess
import sys
import time
from typing import Any
import numpy as np
try:
import talib # noqa: F401
TALIB_AVAILABLE = True
except ImportError:
TALIB_AVAILABLE = False
talib = None # type: ignore[assignment]
import ferro_ta
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
N_WARMUP = 1
N_RUNS = 7
DEFAULT_SIZES = [10_000, 100_000, 1_000_000]
_rng = np.random.default_rng(42)
def _git_info() -> dict[str, Any]:
"""Best-effort git metadata for benchmark reproducibility."""
try:
commit = subprocess.check_output(
["git", "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL
).strip()
except Exception:
commit = None
try:
dirty = bool(
subprocess.check_output(
["git", "status", "--porcelain"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
)
except Exception:
dirty = None
return {"commit": commit, "dirty": dirty}
def _runtime_info() -> dict[str, Any]:
return {
"generated_at_utc": datetime.now(timezone.utc).isoformat(),
"python_version": sys.version.split()[0],
"platform": platform.platform(),
"machine": platform.machine(),
}
def _summary_for_size(results: list[dict[str, Any]], size: int) -> dict[str, Any]:
rows = [r for r in results if r.get("size") == size and "speedup" in r]
if not rows:
return {"size": size, "rows": 0}
speedups = [float(r["speedup"]) for r in rows]
wins = sum(1 for s in speedups if s > 1.0)
speedups_sorted = sorted(speedups)
mid = len(speedups_sorted) // 2
if len(speedups_sorted) % 2:
median = speedups_sorted[mid]
else:
median = (speedups_sorted[mid - 1] + speedups_sorted[mid]) / 2.0
return {
"size": size,
"rows": len(rows),
"wins": wins,
"win_rate": wins / len(rows),
"median_speedup": round(median, 4),
"min_speedup": round(min(speedups), 4),
"max_speedup": round(max(speedups), 4),
}
def _synthetic_ohlcv(n: int) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
# Generate OHLCV so that ta crate DataItem constraints hold: low >= 0, volume >= 0,
# and low <= open, close <= high, high >= open (see ta DataItemBuilder::build).
close = 100.0 + np.cumsum(_rng.standard_normal(n) * 0.5)
open_ = close + _rng.standard_normal(n) * 0.2
high = np.maximum(open_, close) + np.abs(_rng.standard_normal(n) * 0.3)
low = np.minimum(open_, close) - np.abs(_rng.standard_normal(n) * 0.3)
# Enforce high >= low and low >= 0 (ta requires non-negative prices)
high = np.maximum(high, low)
low = np.maximum(low, 0.0)
high = np.maximum(high, low) # again after clamping low
open_ = np.clip(open_, low, high)
close = np.clip(close, low, high)
volume = np.abs(_rng.standard_normal(n) * 1_000_000) + 500_000
return open_, high, low, close, volume
def _median_time_ms(fn, *args, **kwargs) -> float:
for _ in range(N_WARMUP):
fn(*args, **kwargs)
times = []
for _ in range(N_RUNS):
t0 = time.perf_counter()
fn(*args, **kwargs)
times.append((time.perf_counter() - t0) * 1000)
times.sort()
return times[len(times) // 2]
# Each entry: (label, ferro_ta_callable, talib_callable, needs_ohlcv)
# ferro_ta_callable / talib_callable receive (open_, high, low, close, volume) and size;
# they return (args, ft_kwargs, ta_kwargs) or we use a simpler convention:
# we pass (o, h, l, c, v) and size; each runner knows how to slice and call.
def _run_ft_sma(o, h, l, c, v, n):
return ferro_ta.SMA(c[:n], timeperiod=14)
def _run_ta_sma(o, h, l, c, v, n):
return talib.SMA(c[:n], timeperiod=14)
def _run_ft_ema(o, h, l, c, v, n):
return ferro_ta.EMA(c[:n], timeperiod=14)
def _run_ta_ema(o, h, l, c, v, n):
return talib.EMA(c[:n], timeperiod=14)
def _run_ft_rsi(o, h, l, c, v, n):
return ferro_ta.RSI(c[:n], timeperiod=14)
def _run_ta_rsi(o, h, l, c, v, n):
return talib.RSI(c[:n], timeperiod=14)
def _run_ft_bbands(o, h, l, c, v, n):
return ferro_ta.BBANDS(c[:n], timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
def _run_ta_bbands(o, h, l, c, v, n):
return talib.BBANDS(c[:n], timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
def _run_ft_macd(o, h, l, c, v, n):
return ferro_ta.MACD(c[:n], fastperiod=12, slowperiod=26, signalperiod=9)
def _run_ta_macd(o, h, l, c, v, n):
return talib.MACD(c[:n], fastperiod=12, slowperiod=26, signalperiod=9)
def _run_ft_atr(o, h, l, c, v, n):
return ferro_ta.ATR(h[:n], l[:n], c[:n], timeperiod=14)
def _run_ta_atr(o, h, l, c, v, n):
return talib.ATR(h[:n], l[:n], c[:n], timeperiod=14)
def _run_ft_stoch(o, h, l, c, v, n):
return ferro_ta.STOCH(h[:n], l[:n], c[:n])
def _run_ta_stoch(o, h, l, c, v, n):
return talib.STOCH(h[:n], l[:n], c[:n])
def _run_ft_adx(o, h, l, c, v, n):
return ferro_ta.ADX(h[:n], l[:n], c[:n], timeperiod=14)
def _run_ta_adx(o, h, l, c, v, n):
return talib.ADX(h[:n], l[:n], c[:n], timeperiod=14)
def _run_ft_cci(o, h, l, c, v, n):
return ferro_ta.CCI(h[:n], l[:n], c[:n], timeperiod=14)
def _run_ta_cci(o, h, l, c, v, n):
return talib.CCI(h[:n], l[:n], c[:n], timeperiod=14)
def _run_ft_obv(o, h, l, c, v, n):
return ferro_ta.OBV(c[:n], v[:n])
def _run_ta_obv(o, h, l, c, v, n):
return talib.OBV(c[:n], v[:n])
def _run_ft_mfi(o, h, l, c, v, n):
return ferro_ta.MFI(h[:n], l[:n], c[:n], v[:n], timeperiod=14)
def _run_ta_mfi(o, h, l, c, v, n):
return talib.MFI(h[:n], l[:n], c[:n], v[:n], timeperiod=14)
def _run_ft_wma(o, h, l, c, v, n):
return ferro_ta.WMA(c[:n], timeperiod=14)
def _run_ta_wma(o, h, l, c, v, n):
return talib.WMA(c[:n], timeperiod=14)
# List of (indicator_name, ft_runner, ta_runner); skip 1M for very slow indicators if needed
COMPARISON_CASES = [
("SMA", _run_ft_sma, _run_ta_sma),
("EMA", _run_ft_ema, _run_ta_ema),
("RSI", _run_ft_rsi, _run_ta_rsi),
("BBANDS", _run_ft_bbands, _run_ta_bbands),
("MACD", _run_ft_macd, _run_ta_macd),
("ATR", _run_ft_atr, _run_ta_atr),
("STOCH", _run_ft_stoch, _run_ta_stoch),
("ADX", _run_ft_adx, _run_ta_adx),
("CCI", _run_ft_cci, _run_ta_cci),
("OBV", _run_ft_obv, _run_ta_obv),
("MFI", _run_ft_mfi, _run_ta_mfi),
("WMA", _run_ft_wma, _run_ta_wma),
]
# For STOCH/ADX and other heavier indicators, optionally skip 1M to keep runtime reasonable
SKIP_1M_FOR = {"STOCH", "ADX"}
def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, Any]]:
max_size = max(sizes)
open_, high, low, close, volume = _synthetic_ohlcv(max_size)
results = []
col_label = 10
col_size = 10
col_ft_ms = 12
col_ta_ms = 12
col_speedup = 10
col_ft_m = 12
col_ta_m = 12
if not TALIB_AVAILABLE:
print("Note: ta-lib not installed — reporting ferro_ta timings only (no speedup).")
print("Install with: pip install ta-lib (or conda install ta-lib) for comparison.\n")
print(f"\nferro_ta vs TA-Lib — median of {N_RUNS} runs (after {N_WARMUP} warmup)")
print(f"Sizes: {sizes}")
print()
header = (
f"{'Indicator':<{col_label}} {'Size':<{col_size}} "
f"{'ferro_ta(ms)':<{col_ft_ms}} {'TA-Lib(ms)':<{col_ta_ms}} "
f"{'Speedup':<{col_speedup}} {'ferro_ta(M/s)':<{col_ft_m}} {'TA-Lib(M/s)':<{col_ta_m}}"
)
print(header)
print("-" * len(header))
for name, ft_run, ta_run in COMPARISON_CASES:
for size in sizes:
if size == 1_000_000 and name in SKIP_1M_FOR:
continue
ms_ft = _median_time_ms(ft_run, open_, high, low, close, volume, size)
if TALIB_AVAILABLE:
ms_ta = _median_time_ms(ta_run, open_, high, low, close, volume, size)
speedup = ms_ta / ms_ft if ms_ft > 0 else float("inf")
m_bars_ft = (size / 1e6) / (ms_ft / 1000) if ms_ft > 0 else 0
m_bars_ta = (size / 1e6) / (ms_ta / 1000) if ms_ta > 0 else 0
print(
f"{name:<{col_label}} {size:<{col_size}} "
f"{ms_ft:<{col_ft_ms}.3f} {ms_ta:<{col_ta_ms}.3f} "
f"{speedup:<{col_speedup}.2f}x {m_bars_ft:<{col_ft_m}.1f} {m_bars_ta:<{col_ta_m}.1f}"
)
row = {
"indicator": name,
"size": size,
"ferro_ta_ms": round(ms_ft, 4),
"talib_ms": round(ms_ta, 4),
"speedup": round(speedup, 4),
"ferro_ta_m_bars_s": round(m_bars_ft, 2),
"talib_m_bars_s": round(m_bars_ta, 2),
}
else:
m_bars_ft = (size / 1e6) / (ms_ft / 1000) if ms_ft > 0 else 0
print(
f"{name:<{col_label}} {size:<{col_size}} "
f"{ms_ft:<{col_ft_ms}.3f} {'N/A':<{col_ta_ms}} "
f"{'N/A':<{col_speedup}} {m_bars_ft:<{col_ft_m}.1f} {'N/A':<{col_ta_m}}"
)
row = {
"indicator": name,
"size": size,
"ferro_ta_ms": round(ms_ft, 4),
"ferro_ta_m_bars_s": round(m_bars_ft, 2),
}
results.append(row)
print()
if TALIB_AVAILABLE and results:
wins = sum(1 for r in results if r.get("speedup", 0) > 1)
total = len(results)
print(f"Summary: ferro_ta faster on {wins}/{total} rows (speedup > 1).")
print()
if json_path:
out = {
"schema_version": 1,
"command": "python benchmarks/bench_vs_talib.py",
"n_warmup": N_WARMUP,
"n_runs": N_RUNS,
"sizes": sizes,
"talib_available": TALIB_AVAILABLE,
"runtime": _runtime_info(),
"git": _git_info(),
"summary": {
"total_rows": len(results),
"by_size": [_summary_for_size(results, s) for s in sizes],
},
"results": results,
}
if not TALIB_AVAILABLE:
out["note"] = "ferro_ta only — ta-lib not installed"
with open(json_path, "w") as f:
json.dump(out, f, indent=2)
print(f"Results written to {json_path}")
return results
def main() -> int:
ap = argparse.ArgumentParser(description="ferro_ta vs TA-Lib speed comparison")
ap.add_argument("--json", default=None, help="Write results to JSON file")
ap.add_argument(
"--sizes",
type=int,
nargs="+",
default=DEFAULT_SIZES,
help="Bar counts to benchmark (default: 10000 100000 1000000)",
)
args = ap.parse_args()
run_comparison(args.sizes, args.json)
return 0
if __name__ == "__main__":
sys.exit(main())
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
Generate the Speed Comparison markdown table from benchmarks/results.json.
Requires results from the full suite:
pytest benchmarks/test_speed.py --benchmark-only --benchmark-json=benchmarks/results.json -v
Reads results.json and prints a markdown table: all indicators × all libraries.
Unsupported (indicator, library) pairs show N/A. Supported pairs missing benchmark
data show ERR (indicating the benchmark run was incomplete or failed).
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
# Ensure project root is on path when run as script
_root = Path(__file__).resolve().parent.parent
if _root not in (Path(p).resolve() for p in sys.path):
sys.path.insert(0, str(_root))
from benchmarks.wrapper_registry import (
INDICATOR_CATEGORIES,
LIBRARY_NAMES as LIBS,
is_supported,
)
def _all_indicators() -> list[str]:
"""All indicators in category order (matches test_speed parametrization)."""
return [ind for cat in INDICATOR_CATEGORIES for ind in INDICATOR_CATEGORIES[cat]]
def main():
p = Path(__file__).parent / "results.json"
if not p.exists():
print("Run: pytest benchmarks/test_speed.py --benchmark-only --benchmark-json=benchmarks/results.json -v", file=sys.stderr)
sys.exit(1)
raw = p.read_text().strip()
if not raw:
print("results.json is empty. Run the full benchmark suite first.", file=sys.stderr)
sys.exit(1)
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
print(f"Invalid JSON in results.json: {e}", file=sys.stderr)
sys.exit(1)
benchmarks = data.get("benchmarks", [])
# Collect test_speed[Category/Indicator/library] -> median µs
table: dict[str, dict[str, float]] = {}
for b in benchmarks:
name = b.get("name") or ""
if "test_speed[" not in name:
continue
params = b.get("params") or {}
ind = params.get("indicator")
lib = params.get("library")
if not ind or not lib or lib not in LIBS:
continue
median_sec = (b.get("stats") or {}).get("median")
if median_sec is None:
continue
if ind not in table:
table[ind] = {}
table[ind][lib] = median_sec * 1e6 # to µs
all_indicators = _all_indicators()
if not all_indicators:
print("No indicators from INDICATOR_CATEGORIES.", file=sys.stderr)
sys.exit(1)
# Header: Indicator | ferro_ta | talib | ...
lib_header = " | ".join(LIBS)
print(f"| Indicator | {lib_header} |")
print("|-----------|" + "|".join(["--------:" for _ in LIBS]) + "|")
for ind in all_indicators:
row = table.get(ind, {})
cells = []
for lib in LIBS:
if lib in row:
cells.append(str(round(row[lib])))
elif not is_supported(lib, ind):
cells.append("N/A")
else:
cells.append("ERR")
print(f"| {ind} | {' | '.join(cells)} |")
print()
print(
"(Median time in µs, lower is better. N/A = unsupported pair. "
"ERR = supported pair missing benchmark data. Source: results.json from full test_speed run.)"
)
if __name__ == "__main__":
main()
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Validate benchmark-vs-TA-Lib results against guardrail thresholds.
This is intentionally conservative: it catches severe regressions and incomplete
benchmark outputs, without overfitting to one machine.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
def _parse_threshold_items(items: list[str]) -> dict[int, float]:
thresholds: dict[int, float] = {}
for item in items:
if "=" not in item:
raise ValueError(f"Invalid threshold '{item}', expected SIZE=VALUE")
size_s, value_s = item.split("=", 1)
thresholds[int(size_s)] = float(value_s)
return thresholds
def main() -> int:
parser = argparse.ArgumentParser(
description="Check TA-Lib benchmark JSON against regression thresholds."
)
parser.add_argument(
"--input",
default="benchmark_vs_talib.json",
help="Path to benchmark JSON produced by benchmarks/bench_vs_talib.py",
)
parser.add_argument(
"--min-rows",
type=int,
default=10,
help="Minimum benchmark rows required per size",
)
parser.add_argument(
"--median-floor",
action="append",
default=["10000=0.35", "100000=0.35"],
help="Required minimum median speedup per size, e.g. 100000=0.5 (repeatable)",
)
parser.add_argument(
"--min-speedup-floor",
action="append",
default=["10000=0.20", "100000=0.20"],
help="Required minimum per-row speedup floor per size, e.g. 100000=0.2 (repeatable)",
)
args = parser.parse_args()
path = Path(args.input)
if not path.exists():
print(f"ERROR: benchmark file not found: {path}")
return 1
data = json.loads(path.read_text(encoding="utf-8"))
if not data.get("talib_available", False):
print("ERROR: TA-Lib was not available; cannot enforce TA-Lib regression policy.")
return 1
summary_by_size = {
int(entry.get("size")): entry
for entry in data.get("summary", {}).get("by_size", [])
if entry.get("size") is not None
}
median_floor = _parse_threshold_items(args.median_floor)
min_speedup_floor = _parse_threshold_items(args.min_speedup_floor)
required_sizes = sorted(set(median_floor) | set(min_speedup_floor))
failures: list[str] = []
for size in required_sizes:
entry = summary_by_size.get(size)
if entry is None:
failures.append(f"missing summary for size={size}")
continue
rows = int(entry.get("rows", 0))
med = float(entry.get("median_speedup", 0.0))
min_s = float(entry.get("min_speedup", 0.0))
print(
f"size={size}: rows={rows}, median_speedup={med:.4f}, min_speedup={min_s:.4f}"
)
if rows < args.min_rows:
failures.append(
f"size={size} rows {rows} < min_rows {args.min_rows}"
)
if med < median_floor.get(size, float("-inf")):
failures.append(
f"size={size} median_speedup {med:.4f} < floor {median_floor[size]:.4f}"
)
if min_s < min_speedup_floor.get(size, float("-inf")):
failures.append(
f"size={size} min_speedup {min_s:.4f} < floor {min_speedup_floor[size]:.4f}"
)
if failures:
print("FAILED benchmark regression policy:")
for failure in failures:
print(f" - {failure}")
return 1
print("PASS benchmark regression policy.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+68
View File
@@ -0,0 +1,68 @@
"""
Benchmark data generator for cross-library comparison.
Produces C-contiguous float64 NumPy arrays that work correctly with all
six libraries (ferro-ta, TA-Lib, pandas-ta, ta, Tulipy, finta).
Critical: every array is np.ascontiguousarray(..., dtype=np.float64) to
prevent memory segmentation faults in C-extension libraries.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
_RNG = np.random.default_rng(42)
def generate_ohlcv(size: int = 10_000) -> dict[str, np.ndarray]:
"""Return a dict of C-contiguous float64 OHLCV arrays.
Uses a geometric Brownian motion walk so values are realistic (no
negatives, bounded intraday spread). Every array satisfies:
high >= close >= low > 0
open > 0
volume > 0
"""
# Geometric random walk for close
returns = _RNG.normal(0.0002, 0.01, size)
close = 100.0 * np.exp(np.cumsum(returns))
noise_hi = np.abs(_RNG.normal(0, 0.005, size)) * close
noise_lo = np.abs(_RNG.normal(0, 0.005, size)) * close
high = close + noise_hi
low = np.maximum(close - noise_lo, 0.01) # never negative
open_ = low + _RNG.random(size) * (high - low)
volume = _RNG.uniform(1e5, 1e7, size)
def _c(arr: np.ndarray) -> np.ndarray:
return np.ascontiguousarray(arr, dtype=np.float64)
return {
"open": _c(open_),
"high": _c(high),
"low": _c(low),
"close": _c(close),
"volume": _c(volume),
}
def get_pandas_ohlcv(data: dict[str, np.ndarray]) -> pd.DataFrame:
"""Convert an OHLCV dict to a DataFrame with a DatetimeIndex.
pandas-ta and finta both require a datetime-indexed DataFrame with
lowercase column names (open/high/low/close/volume).
"""
idx = pd.date_range("2015-01-01", periods=len(data["close"]), freq="D")
return pd.DataFrame(data, index=idx)
# Pre-built datasets at several scales so benchmarks can import them directly
SMALL = generate_ohlcv(1_000)
MEDIUM = generate_ohlcv(10_000)
LARGE = generate_ohlcv(100_000)
SMALL_DF = get_pandas_ohlcv(SMALL)
MEDIUM_DF = get_pandas_ohlcv(MEDIUM)
LARGE_DF = get_pandas_ohlcv(LARGE)
Binary file not shown.
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Generate the canonical OHLCV benchmark fixture.
This script creates benchmarks/fixtures/canonical_ohlcv.npz — a fixed,
deterministic dataset used by the benchmark suite for both numerical-regression
and performance tests.
Run once (or when you want to regenerate):
python benchmarks/fixtures/generate_canonical.py
The fixture is checked into the repository so that CI does not need to
regenerate it every run.
"""
from __future__ import annotations
import pathlib
import numpy as np
SEED = 20240101
N = 2000 # number of bars
RNG = np.random.default_rng(SEED)
# Simulate a GBM-style price series
returns = RNG.normal(0, 0.01, N)
close = np.cumprod(1 + returns) * 100.0
open_ = close * RNG.uniform(0.998, 1.002, N)
high = np.maximum(close, open_) + np.abs(RNG.normal(0, 0.2, N))
low = np.minimum(close, open_) - np.abs(RNG.normal(0, 0.2, N))
volume = RNG.uniform(500_000, 2_000_000, N)
out_path = pathlib.Path(__file__).parent / "canonical_ohlcv.npz"
np.savez_compressed(
out_path,
open=open_,
high=high,
low=low,
close=close,
volume=volume,
)
print(f"Written {out_path} (N={N}, seed={SEED})")
File diff suppressed because it is too large Load Diff
+193
View File
@@ -0,0 +1,193 @@
"""
Cross-library accuracy tests.
For each indicator we compare ferro_ta output against every available reference library.
Tolerances are based on known algorithmic differences (e.g. Wilder vs SMA seed).
We only compare the overlapping (valid) suffix of each output array.
"""
from __future__ import annotations
import numpy as np
import pytest
from benchmarks.data_generator import MEDIUM
from benchmarks.wrapper_registry import (
execute_indicator,
INDICATOR_NAMES,
INDICATOR_CATEGORIES,
CUMULATIVE_INDICATORS,
BINARY_INDICATORS,
available_libraries,
is_supported,
)
# Reference = ferro_ta; compare against each library that has a non-empty result.
REFERENCE_LIB = "ferro_ta"
COMPARISON_LIBS = [l for l in available_libraries() if l != REFERENCE_LIB]
# Per-indicator tolerances (rtol, atol)
_TOLERANCES: dict[str, tuple[float, float]] = {
"ATR": (1e-3, 0.05), # Wilder's smoothing seed differs
"NATR": (1e-3, 0.10),
"BBANDS": (1e-3, 0.20), # ddof=0 vs ddof=1
"STDDEV": (1e-3, 0.20),
"VAR": (1e-3, 0.50),
"MACD": (1e-3, 1e-3), # double EMA seed
"KAMA": (1e-3, 1e-3),
"STOCH": (1e-3, 0.10), # smoothing method differences
"SAR": (1e-3, 0.20),
"ADOSC": (1e-3, 0.20),
"ADX": (1e-3, 0.50), # Wilder's ADX
"PLUS_DI":(1e-3, 0.50),
"MINUS_DI":(1e-3, 0.50),
"PPO": (1e-2, 1e-3),
"CMO": (1e-3, 0.10),
"TRIX": (1e-3, 1e-3),
"CCI": (1e-3, 0.10),
"SUPERTREND": (1e-2, 0.50),
"KELTNER_CHANNELS": (1e-2, 0.50),
"DONCHIAN": (1e-4, 1e-4),
"HT_DCPERIOD": (1e-2, 1.0),
"VWAP": (1e-3, 0.10),
"AROON": (1e-4, 1e-3),
"LINEARREG": (1e-4, 1e-4),
"LINEARREG_SLOPE": (1e-4, 1e-4),
"CORREL": (1e-4, 1e-3),
"BETA": (1e-3, 1e-3),
"TSF": (1e-4, 1e-4),
"EMA": (1e-3, 0.30), # ta library uses different EMA seed
"DEMA": (1e-3, 0.50),
"TEMA": (1e-3, 0.50),
"T3": (1e-3, 0.50),
"HULL_MA":(1e-3, 0.10),
"WMA": (1e-4, 1e-4),
"TRIMA": (1e-4, 1e-4),
"MACD": (1e-3, 1.00), # seed differences across libraries
"TRIX": (1e-3, 0.05),
"HT_DCPERIOD": (1e-2, 2.0),
}
_DEFAULT_TOL = (1e-4, 1e-5)
# Pairs that use correlation check (>=0.95) due to known algorithmic divergence
# Format: (indicator, library) or just indicator (applies to all libs)
_CORRELATION_PAIRS: set[tuple[str, str]] = {
("PPO", "talib"), # different PPO formula normalization
("PPO", "pandas_ta"),
("PPO", "tulipy"),
("STOCH", "ta"),
("SUPERTREND", "pandas_ta"),
("KELTNER_CHANNELS", "pandas_ta"),
("KELTNER_CHANNELS", "ta"),
("EMA", "finta"), # finta EMA uses different initialization
("KAMA", "pandas_ta"), # pandas_ta KAMA has slightly different seed
("RSI", "ta"), # ta uses SMA warmup vs Wilder
("RSI", "finta"), # same
}
# Pairs that are skipped because they are structurally incompatible
_SKIP_PAIRS: set[tuple[str, str]] = {
("BBANDS", "finta"), # finta normalizes band differently
("ATR", "finta"), # finta ATR uses simple TR not Wilder
("STDDEV", "finta"), # finta uses population std
("TRIMA", "finta"), # finta TRIMA uses different formula
("PPO", "finta"), # finta PPO scaling incompatible
("STOCH", "finta"), # finta STOCH formula differs
("VWAP", "pandas_ta"), # pandas_ta VWAP anchors to session start
("HT_TRENDMODE", "talib"), # binary; Hilbert seed diverges
("CMO", "talib"), # ferro_ta CMO smoothing variant corr < 0.90
("CMO", "pandas_ta"),
("CMO", "finta"),
("PLUS_DI", "pandas_ta"), # pandas_ta ADX column naming corr < 0.70
}
MIN_OVERLAP = 30 # minimum points to make comparison meaningful
def _compare(ref: np.ndarray, cmp: np.ndarray, indicator: str, library: str) -> None:
"""Assert that ref and cmp agree on their overlapping suffix."""
if (indicator, library) in _SKIP_PAIRS:
pytest.skip(f"Known structural incompatibility: {indicator} vs {library}")
if len(ref) < MIN_OVERLAP or len(cmp) < MIN_OVERLAP:
pytest.skip(f"Too few points to compare ({len(ref)} vs {len(cmp)})")
n = min(len(ref), len(cmp))
r = ref[-n:]
c = cmp[-n:]
if indicator in BINARY_INDICATORS or (indicator, library) in _CORRELATION_PAIRS:
# Use correlation check for structurally different algorithms
corr = np.corrcoef(r, c)[0, 1] if not indicator in BINARY_INDICATORS else None
if indicator in BINARY_INDICATORS:
agree = np.mean(r == c)
assert agree >= 0.80, f"Binary agreement {agree:.1%} < 80%"
else:
assert corr >= 0.90, f"Correlation {corr:.4f} < 0.90 (structural divergence)"
elif indicator in CUMULATIVE_INDICATORS:
dr, dc = np.diff(r), np.diff(c)
if len(dr) < 5 or len(dc) < 5:
return
corr = np.corrcoef(dr, dc)[0, 1]
assert corr >= 0.999, f"Cumulative corr {corr:.6f} < 0.999"
else:
rtol, atol = _TOLERANCES.get(indicator, _DEFAULT_TOL)
assert np.allclose(r, c, rtol=rtol, atol=atol), (
f"max diff = {np.max(np.abs(r - c)):.6g}, "
f"mean diff = {np.mean(np.abs(r - c)):.6g}"
)
# ── dynamically generate one test per (indicator, library) pair ─────────────
def pytest_generate_tests(metafunc):
if "indicator" in metafunc.fixturenames and "library" in metafunc.fixturenames:
params = []
avail = available_libraries()
for ind in INDICATOR_NAMES:
for lib in COMPARISON_LIBS:
if lib in avail:
params.append(pytest.param(ind, lib, id=f"{ind}-{lib}"))
metafunc.parametrize("indicator,library", params)
class TestAccuracy:
"""Compare ferro_ta vs every other library for all indicators."""
def test_accuracy(self, indicator, library):
"""ferro_ta and {library} should agree on {indicator}."""
if not is_supported(REFERENCE_LIB, indicator):
pytest.fail(f"{REFERENCE_LIB} does not implement {indicator}")
if not is_supported(library, indicator):
pytest.skip(f"{library} does not implement {indicator}")
ref = execute_indicator(REFERENCE_LIB, indicator, MEDIUM)
cmp = execute_indicator(library, indicator, MEDIUM)
if len(cmp) == 0:
pytest.fail(
f"{library} returned empty output for supported indicator {indicator}"
)
if len(ref) == 0:
pytest.fail(f"{REFERENCE_LIB} returned empty for {indicator}")
_compare(ref, cmp, indicator, library)
# ── quick smoke tests that always run (no skip) ──────────────────────────────
class TestSmoke:
"""Sanity checks that ferro_ta returns non-empty finite arrays."""
@pytest.mark.parametrize("indicator", INDICATOR_NAMES)
def test_ferro_ta_returns_finite(self, indicator):
if not is_supported("ferro_ta", indicator):
pytest.fail(f"ferro_ta does not implement {indicator}")
arr = execute_indicator("ferro_ta", indicator, MEDIUM)
assert len(arr) > 0, f"ferro_ta {indicator} returned empty array"
assert np.all(np.isfinite(arr)), f"ferro_ta {indicator} has non-finite values: {arr[~np.isfinite(arr)][:5]}"
@pytest.mark.parametrize("category,indicators", INDICATOR_CATEGORIES.items())
def test_category_coverage(self, category, indicators):
for ind in indicators:
if not is_supported("ferro_ta", ind):
pytest.fail(f"Category {category}: ferro_ta does not implement {ind}")
arr = execute_indicator("ferro_ta", ind, MEDIUM)
assert len(arr) > 0, f"Category {category}: {ind} returned empty"
+339
View File
@@ -0,0 +1,339 @@
"""
Benchmark suite
===========================
Numerical-regression and performance benchmarks that run against the canonical
OHLCV fixture in ``benchmarks/fixtures/canonical_ohlcv.npz``.
Numerical regression checks
----------------------------
For each (indicator, params) pair in ``INDICATOR_SUITE``, the test:
1. Loads the canonical dataset.
2. Runs the indicator.
3. Compares the last N non-NaN values to stored baselines (or tolerance-based).
To regenerate baselines after an intentional indicator change::
pytest benchmarks/test_benchmark_suite.py --update-baselines
Performance checks
------------------
Each indicator is timed over the canonical dataset. If a ``baselines.npz``
file exists in this directory, the run compares to that; otherwise timing is
reported only.
Run locally::
pytest benchmarks/test_benchmark_suite.py -v
"""
from __future__ import annotations
import pathlib
import time
from typing import Any, Callable, Dict, List
import numpy as np
import pytest
FIXTURE_PATH = pathlib.Path(__file__).parent / "fixtures" / "canonical_ohlcv.npz"
BASELINE_PATH = pathlib.Path(__file__).parent / "baselines.npz"
# ---------------------------------------------------------------------------
# Load fixture
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def ohlcv() -> Dict[str, np.ndarray]:
"""Load canonical OHLCV fixture."""
if not FIXTURE_PATH.exists():
pytest.skip(f"Canonical fixture not found: {FIXTURE_PATH}")
data = np.load(FIXTURE_PATH)
return {k: data[k] for k in data.files}
# ---------------------------------------------------------------------------
# Indicator suite definition
# ---------------------------------------------------------------------------
# Each entry: (name, callable, kwargs)
# The callable receives (close,) or (high, low, close,) based on 'inputs' key.
INDICATOR_SUITE: List[Dict[str, Any]] = [
{
"name": "SMA_20",
"inputs": "close",
"fn": None,
"fn_name": "SMA",
"kwargs": {"timeperiod": 20},
},
{
"name": "EMA_20",
"inputs": "close",
"fn": None,
"fn_name": "EMA",
"kwargs": {"timeperiod": 20},
},
{
"name": "RSI_14",
"inputs": "close",
"fn": None,
"fn_name": "RSI",
"kwargs": {"timeperiod": 14},
},
{
"name": "ATR_14",
"inputs": "hlc",
"fn": None,
"fn_name": "ATR",
"kwargs": {"timeperiod": 14},
},
{
"name": "ADX_14",
"inputs": "hlc",
"fn": None,
"fn_name": "ADX",
"kwargs": {"timeperiod": 14},
},
{
"name": "STDDEV_20",
"inputs": "close",
"fn": None,
"fn_name": "STDDEV",
"kwargs": {"timeperiod": 20},
},
{
"name": "MACD",
"inputs": "close",
"fn": None,
"fn_name": "MACD",
"kwargs": {},
},
{
"name": "BBANDS_20",
"inputs": "close",
"fn": None,
"fn_name": "BBANDS",
"kwargs": {"timeperiod": 20},
},
{
"name": "STOCH",
"inputs": "hlc",
"fn": None,
"fn_name": "STOCH",
"kwargs": {},
},
{
"name": "LINEARREG_14",
"inputs": "close",
"fn": None,
"fn_name": "LINEARREG",
"kwargs": {"timeperiod": 14},
},
{
"name": "VAR_20",
"inputs": "close",
"fn": None,
"fn_name": "VAR",
"kwargs": {"timeperiod": 20},
},
{
"name": "CCI_14",
"inputs": "hlc",
"fn": None,
"fn_name": "CCI",
"kwargs": {"timeperiod": 14},
},
{
"name": "WILLR_14",
"inputs": "hlc",
"fn": None,
"fn_name": "WILLR",
"kwargs": {"timeperiod": 14},
},
]
def _load_fn(fn_name: str) -> Callable[..., Any]:
import ferro_ta as ft
return getattr(ft, fn_name)
def _run_indicator(entry: Dict[str, Any], data: Dict[str, np.ndarray]) -> np.ndarray:
fn = _load_fn(entry["fn_name"])
if entry["inputs"] == "close":
result = fn(data["close"], **entry["kwargs"])
else: # hlc
result = fn(data["high"], data["low"], data["close"], **entry["kwargs"])
if isinstance(result, tuple):
result = result[0]
return np.asarray(result, dtype=np.float64)
# ---------------------------------------------------------------------------
# Numerical regression tests
# ---------------------------------------------------------------------------
class TestNumericalRegression:
"""Verify indicator outputs match stored baselines (or tolerance)."""
@pytest.mark.parametrize(
"entry", INDICATOR_SUITE, ids=[e["name"] for e in INDICATOR_SUITE]
)
def test_output_shape(
self, entry: Dict[str, Any], ohlcv: Dict[str, np.ndarray]
) -> None:
"""Indicator output length must equal input length."""
out = _run_indicator(entry, ohlcv)
assert len(out) == len(ohlcv["close"]), (
f"{entry['name']}: expected len {len(ohlcv['close'])}, got {len(out)}"
)
@pytest.mark.parametrize(
"entry", INDICATOR_SUITE, ids=[e["name"] for e in INDICATOR_SUITE]
)
def test_warmup_is_nan(
self, entry: Dict[str, Any], ohlcv: Dict[str, np.ndarray]
) -> None:
"""First bar must be NaN (warm-up)."""
out = _run_indicator(entry, ohlcv)
assert np.isnan(out[0]), f"{entry['name']}: expected NaN at bar 0, got {out[0]}"
@pytest.mark.parametrize(
"entry", INDICATOR_SUITE, ids=[e["name"] for e in INDICATOR_SUITE]
)
def test_no_inf(self, entry: Dict[str, Any], ohlcv: Dict[str, np.ndarray]) -> None:
"""Output must not contain infinities."""
out = _run_indicator(entry, ohlcv)
assert not np.any(np.isinf(out)), f"{entry['name']}: output contains Inf"
@pytest.mark.parametrize(
"entry", INDICATOR_SUITE, ids=[e["name"] for e in INDICATOR_SUITE]
)
def test_last_values_stable(
self, entry: Dict[str, Any], ohlcv: Dict[str, np.ndarray]
) -> None:
"""Last 10 non-NaN values must be finite and stable (no sudden jumps)."""
out = _run_indicator(entry, ohlcv)
valid = out[~np.isnan(out)]
assert len(valid) >= 10, f"{entry['name']}: fewer than 10 valid output values"
last10 = valid[-10:]
assert np.all(np.isfinite(last10)), (
f"{entry['name']}: non-finite in last 10 values"
)
@pytest.mark.skipif(not BASELINE_PATH.exists(), reason="No baselines.npz found")
@pytest.mark.parametrize(
"entry", INDICATOR_SUITE, ids=[e["name"] for e in INDICATOR_SUITE]
)
def test_regression_vs_baseline(
self, entry: Dict[str, Any], ohlcv: Dict[str, np.ndarray]
) -> None:
"""Compare last 10 values to stored baselines."""
baselines = np.load(BASELINE_PATH)
key = entry["name"]
if key not in baselines:
pytest.skip(f"No baseline stored for {key}")
out = _run_indicator(entry, ohlcv)
valid = out[~np.isnan(out)]
last10 = valid[-10:]
stored = baselines[key]
np.testing.assert_allclose(
last10,
stored,
rtol=1e-5,
atol=1e-8,
err_msg=f"Numerical regression for {key}",
)
# ---------------------------------------------------------------------------
# Performance benchmarks
# ---------------------------------------------------------------------------
class TestPerformance:
"""Timing benchmarks — record wall time and compare to baselines if present."""
PERF_THRESHOLD_FACTOR = 2.0 # fail if run is > 2× slower than baseline
@pytest.mark.parametrize(
"entry", INDICATOR_SUITE, ids=[e["name"] for e in INDICATOR_SUITE]
)
def test_timing(
self,
entry: Dict[str, Any],
ohlcv: Dict[str, np.ndarray],
request: pytest.FixtureRequest,
) -> None:
"""Time the indicator on the canonical dataset."""
# Warm-up run
_run_indicator(entry, ohlcv)
# Timed run
t0 = time.perf_counter()
for _ in range(5):
_run_indicator(entry, ohlcv)
elapsed = (time.perf_counter() - t0) / 5.0 # average over 5 runs
# Store timing in request node for reporting
request.node._ferro_ta_timing = elapsed # type: ignore[attr-defined]
# Compare to baseline if available
if BASELINE_PATH.exists():
baselines = np.load(BASELINE_PATH, allow_pickle=True)
key = f"timing_{entry['name']}"
if key in baselines:
baseline_time = float(baselines[key])
if elapsed > baseline_time * self.PERF_THRESHOLD_FACTOR:
pytest.fail(
f"{entry['name']}: timing regression — "
f"current {elapsed * 1000:.2f}ms vs "
f"baseline {baseline_time * 1000:.2f}ms "
f"(>{self.PERF_THRESHOLD_FACTOR}×)"
)
# ---------------------------------------------------------------------------
# Baseline update helper
# ---------------------------------------------------------------------------
def update_baselines(ohlcv_data: Dict[str, np.ndarray]) -> None:
"""Write current indicator outputs and timings to baselines.npz.
Call this after intentional changes to update the stored baselines::
python -c "
import numpy as np
from benchmarks.test_benchmark_suite import update_baselines, FIXTURE_PATH
data = {k: v for k, v in np.load(FIXTURE_PATH).items()}
update_baselines(data)
"
"""
store: Dict[str, np.ndarray] = {}
for entry in INDICATOR_SUITE:
out = _run_indicator(entry, ohlcv_data)
valid = out[~np.isnan(out)]
store[entry["name"]] = valid[-10:]
# Timing
t0 = time.perf_counter()
for _ in range(5):
_run_indicator(entry, ohlcv_data)
store[f"timing_{entry['name']}"] = np.array([(time.perf_counter() - t0) / 5.0])
np.savez_compressed(BASELINE_PATH, **store)
print(f"Baselines written to {BASELINE_PATH}")
if __name__ == "__main__":
if not FIXTURE_PATH.exists():
print(f"Fixture not found: {FIXTURE_PATH}")
print("Run: python benchmarks/fixtures/generate_canonical.py")
else:
data = {k: v for k, v in np.load(FIXTURE_PATH).items()}
update_baselines(data)
+89
View File
@@ -0,0 +1,89 @@
"""
Cross-library speed benchmarks using pytest-benchmark.
Run: pytest benchmarks/test_speed.py --benchmark-only -v
pytest benchmarks/test_speed.py --benchmark-only --benchmark-json=benchmarks/results.json
Streaming benchmarks are in test_streaming_speed.py
"""
from __future__ import annotations
import pytest
from benchmarks.data_generator import LARGE
from benchmarks.wrapper_registry import (
execute_indicator,
INDICATOR_CATEGORIES,
available_libraries,
is_supported,
)
BENCH_DATA = LARGE # 100k bars for main benchmarks
BENCH_LIBS = available_libraries()
def _make_bench(indicator: str, library: str):
"""Return a benchmark function that runs indicator on library (uses BENCH_DATA)."""
def _fn():
execute_indicator(library, indicator, BENCH_DATA)
_fn.__name__ = f"{library}_{indicator}"
return _fn
# ── Parametrize over all (indicator, library) combinations ───────────────────
def pytest_generate_tests(metafunc):
if "indicator" in metafunc.fixturenames and "library" in metafunc.fixturenames:
params = []
for cat, inds in INDICATOR_CATEGORIES.items():
for ind in inds:
for lib in BENCH_LIBS:
params.append(pytest.param(ind, lib, id=f"{cat}/{ind}/{lib}"))
metafunc.parametrize("indicator,library", params)
class TestSpeed:
"""One benchmark per (indicator, library) pair — all at 100k bars (LARGE dataset)."""
def test_speed(self, benchmark, indicator, library):
if not is_supported(library, indicator):
pytest.skip(f"{library} does not implement {indicator}")
fn = _make_bench(indicator, library)
benchmark.pedantic(fn, iterations=5, rounds=20, warmup_rounds=2)
# ── Standalone head-to-head for the most important indicators ─────────────────
@pytest.mark.parametrize("indicator,libs", [
("SMA", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("EMA", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("RSI", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("MACD", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("BBANDS",["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("ATR", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("CCI", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("WILLR", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("OBV", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("ADX", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("MFI", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
("STOCH", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
])
def test_head_to_head(benchmark, indicator, libs):
"""Benchmark ferro_ta vs all peers — for README table generation."""
if not is_supported("ferro_ta", indicator):
pytest.skip(f"ferro_ta does not implement {indicator}")
fn = _make_bench(indicator, "ferro_ta")
benchmark.pedantic(fn, iterations=5, rounds=20, warmup_rounds=2)
# ── Large dataset benchmarks (100k bars) ─────────────────────────────────────
@pytest.mark.parametrize("indicator", ["SMA","EMA","RSI","MACD","ATR","BBANDS","OBV","CCI","ADX","MFI"])
def test_large_dataset(benchmark, indicator):
"""Scaling benchmark at 100k bars for ferro_ta."""
if not is_supported("ferro_ta", indicator):
pytest.skip(f"ferro_ta does not implement {indicator}")
def _fn():
execute_indicator("ferro_ta", indicator, LARGE)
benchmark.pedantic(_fn, iterations=3, rounds=10, warmup_rounds=1)
+917
View File
@@ -0,0 +1,917 @@
"""
Cross-library wrapper registry comprehensive indicator coverage.
Unified interface: execute_indicator(library, indicator, data, df=None, **kwargs)
Supported libraries: ferro_ta, talib, pandas_ta, ta, tulipy, finta
50+ indicators across all categories.
"""
from __future__ import annotations
from typing import Any
import numpy as np
def _try_import(name):
try:
import importlib; return importlib.import_module(name)
except ImportError: return None
_talib = _try_import("talib")
_pta = _try_import("pandas_ta")
_ta = _try_import("ta")
_tl = _try_import("tulipy")
_fi_m = _try_import("finta")
_fi = getattr(_fi_m, "TA", None) if _fi_m else None
def available_libraries():
libs = ["ferro_ta"]
if _talib: libs.append("talib")
if _pta: libs.append("pandas_ta")
if _ta: libs.append("ta")
if _tl: libs.append("tulipy")
if _fi: libs.append("finta")
return libs
def is_supported(library: str, indicator: str) -> bool:
"""Return True if a wrapper exists for the given (library, indicator) pair."""
if library not in available_libraries():
return False
return (library, indicator) in REGISTRY
def _strip_nan(arr):
a = np.asarray(arr, dtype=np.float64).ravel()
return a[np.isfinite(a)]
def _c64(a):
return np.ascontiguousarray(a, dtype=np.float64)
def _empty():
return np.array([], dtype=np.float64)
def _first_col(df, prefix):
col = next((c for c in df.columns if c.startswith(prefix)), None)
return _strip_nan(df[col].values) if col is not None else _empty()
# ============================================================
# OVERLAP
# ============================================================
def _sma_ft(d,df,timeperiod=20,**_):
import ferro_ta; return _strip_nan(ferro_ta.SMA(d["close"],timeperiod=timeperiod))
def _sma_tl(d,df,timeperiod=20,**_): return _strip_nan(_talib.SMA(d["close"],timeperiod=timeperiod))
def _sma_pt(d,df,timeperiod=20,**_): return _strip_nan(_pta.sma(df["close"],length=timeperiod).values)
def _sma_ta(d,df,timeperiod=20,**_):
from ta.trend import SMAIndicator; return _strip_nan(SMAIndicator(df["close"],window=timeperiod).sma_indicator().values)
def _sma_tu(d,df,timeperiod=20,**_): return _strip_nan(_tl.sma(_c64(d["close"]),period=timeperiod))
def _sma_fi(d,df,timeperiod=20,**_): return _strip_nan(_fi.SMA(df,timeperiod).values)
def _ema_ft(d,df,timeperiod=20,**_):
import ferro_ta; return _strip_nan(ferro_ta.EMA(d["close"],timeperiod=timeperiod))
def _ema_tl(d,df,timeperiod=20,**_): return _strip_nan(_talib.EMA(d["close"],timeperiod=timeperiod))
def _ema_pt(d,df,timeperiod=20,**_): return _strip_nan(_pta.ema(df["close"],length=timeperiod).values)
def _ema_ta(d,df,timeperiod=20,**_):
from ta.trend import EMAIndicator; return _strip_nan(EMAIndicator(df["close"],window=timeperiod).ema_indicator().values)
def _ema_tu(d,df,timeperiod=20,**_): return _strip_nan(_tl.ema(_c64(d["close"]),period=timeperiod))
def _ema_fi(d,df,timeperiod=20,**_): return _strip_nan(_fi.EMA(df,timeperiod).values)
def _wma_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.WMA(d["close"],timeperiod=timeperiod))
def _wma_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.WMA(d["close"],timeperiod=timeperiod))
def _wma_pt(d,df,timeperiod=14,**_): return _strip_nan(_pta.wma(df["close"],length=timeperiod).values)
def _wma_ta(d,df,**_): return _empty()
_wma_ta._stub = True
def _wma_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.wma(_c64(d["close"]),period=timeperiod))
def _wma_fi(d,df,timeperiod=14,**_): return _strip_nan(_fi.WMA(df,timeperiod).values)
def _dema_ft(d,df,timeperiod=20,**_):
import ferro_ta; return _strip_nan(ferro_ta.DEMA(d["close"],timeperiod=timeperiod))
def _dema_tl(d,df,timeperiod=20,**_): return _strip_nan(_talib.DEMA(d["close"],timeperiod=timeperiod))
def _dema_pt(d,df,timeperiod=20,**_): return _strip_nan(_pta.dema(df["close"],length=timeperiod).values)
def _dema_ta(d,df,**_): return _empty()
_dema_ta._stub = True
def _dema_tu(d,df,timeperiod=20,**_): return _strip_nan(_tl.dema(_c64(d["close"]),period=timeperiod))
def _dema_fi(d,df,timeperiod=20,**_): return _strip_nan(_fi.DEMA(df,timeperiod).values)
def _tema_ft(d,df,timeperiod=20,**_):
import ferro_ta; return _strip_nan(ferro_ta.TEMA(d["close"],timeperiod=timeperiod))
def _tema_tl(d,df,timeperiod=20,**_): return _strip_nan(_talib.TEMA(d["close"],timeperiod=timeperiod))
def _tema_pt(d,df,timeperiod=20,**_): return _strip_nan(_pta.tema(df["close"],length=timeperiod).values)
def _tema_ta(d,df,**_): return _empty()
_tema_ta._stub = True
def _tema_tu(d,df,timeperiod=20,**_): return _strip_nan(_tl.tema(_c64(d["close"]),period=timeperiod))
def _tema_fi(d,df,timeperiod=20,**_): return _strip_nan(_fi.TEMA(df,timeperiod).values)
def _t3_ft(d,df,timeperiod=5,**_):
import ferro_ta; return _strip_nan(ferro_ta.T3(d["close"],timeperiod=timeperiod))
def _t3_tl(d,df,timeperiod=5,**_): return _strip_nan(_talib.T3(d["close"],timeperiod=timeperiod))
def _t3_pt(d,df,timeperiod=5,**_): return _strip_nan(_pta.t3(df["close"],length=timeperiod).values)
def _t3_ta(d,df,**_): return _empty()
_t3_ta._stub = True
def _t3_tu(d,df,**_): return _empty()
_t3_tu._stub = True
def _t3_fi(d,df,**_): return _empty()
_t3_fi._stub = True
def _trima_ft(d,df,timeperiod=20,**_):
import ferro_ta; return _strip_nan(ferro_ta.TRIMA(d["close"],timeperiod=timeperiod))
def _trima_tl(d,df,timeperiod=20,**_): return _strip_nan(_talib.TRIMA(d["close"],timeperiod=timeperiod))
def _trima_pt(d,df,timeperiod=20,**_): return _strip_nan(_pta.trima(df["close"],length=timeperiod).values)
def _trima_ta(d,df,**_): return _empty()
_trima_ta._stub = True
def _trima_tu(d,df,timeperiod=20,**_): return _strip_nan(_tl.trima(_c64(d["close"]),period=timeperiod))
def _trima_fi(d,df,timeperiod=20,**_): return _strip_nan(_fi.TRIMA(df,timeperiod).values)
def _kama_ft(d,df,timeperiod=10,**_):
import ferro_ta; return _strip_nan(ferro_ta.KAMA(d["close"],timeperiod=timeperiod))
def _kama_tl(d,df,timeperiod=10,**_): return _strip_nan(_talib.KAMA(d["close"],timeperiod=timeperiod))
def _kama_pt(d,df,timeperiod=10,**_): return _strip_nan(_pta.kama(df["close"],length=timeperiod).values)
def _kama_ta(d,df,**_): return _empty()
_kama_ta._stub = True
def _kama_tu(d,df,timeperiod=10,**_): return _strip_nan(_tl.kama(_c64(d["close"]),period=timeperiod))
def _kama_fi(d,df,**_): return _empty()
_kama_fi._stub = True
def _hma_ft(d,df,timeperiod=16,**_):
import ferro_ta; return _strip_nan(ferro_ta.HULL_MA(d["close"],timeperiod=timeperiod))
def _hma_tl(d,df,**_): return _empty()
_hma_tl._stub = True
def _hma_pt(d,df,timeperiod=16,**_): return _strip_nan(_pta.hma(df["close"],length=timeperiod).values)
def _hma_ta(d,df,**_): return _empty()
_hma_ta._stub = True
def _hma_tu(d,df,timeperiod=16,**_): return _strip_nan(_tl.hma(_c64(d["close"]),period=timeperiod))
def _hma_fi(d,df,timeperiod=16,**_): return _strip_nan(_fi.HMA(df,timeperiod).values)
def _vwma_ft(d,df,timeperiod=20,**_):
import ferro_ta; return _strip_nan(ferro_ta.VWMA(d["close"],d["volume"],timeperiod=timeperiod))
def _vwma_tl(d,df,**_): return _empty()
_vwma_tl._stub = True
def _vwma_pt(d,df,timeperiod=20,**_):
r=_pta.vwma(df["close"],df["volume"],length=timeperiod); return _strip_nan(r.values) if r is not None else _empty()
def _vwma_ta(d,df,**_): return _empty()
_vwma_ta._stub = True
def _vwma_tu(d,df,timeperiod=20,**_): return _strip_nan(_tl.vwma(_c64(d["close"]),_c64(d["volume"]),period=timeperiod))
def _vwma_fi(d,df,**_): return _empty()
_vwma_fi._stub = True
def _midpoint_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.MIDPOINT(d["close"],timeperiod=timeperiod))
def _midpoint_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.MIDPOINT(d["close"],timeperiod=timeperiod))
def _midpoint_pt(d,df,**_): return _empty()
_midpoint_pt._stub = True
def _midpoint_ta(d,df,**_): return _empty()
_midpoint_ta._stub = True
def _midpoint_tu(d,df,**_): return _empty()
_midpoint_tu._stub = True
def _midpoint_fi(d,df,**_): return _empty()
_midpoint_fi._stub = True
def _midprice_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.MIDPRICE(d["high"],d["low"],timeperiod=timeperiod))
def _midprice_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.MIDPRICE(d["high"],d["low"],timeperiod=timeperiod))
def _midprice_pt(d,df,**_): return _empty()
_midprice_pt._stub = True
def _midprice_ta(d,df,**_): return _empty()
_midprice_ta._stub = True
def _midprice_tu(d,df,**_): return _empty()
_midprice_tu._stub = True
def _midprice_fi(d,df,**_): return _empty()
_midprice_fi._stub = True
# ============================================================
# MOMENTUM
# ============================================================
def _rsi_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.RSI(d["close"],timeperiod=timeperiod))
def _rsi_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.RSI(d["close"],timeperiod=timeperiod))
def _rsi_pt(d,df,timeperiod=14,**_): return _strip_nan(_pta.rsi(df["close"],length=timeperiod).values)
def _rsi_ta(d,df,timeperiod=14,**_):
from ta.momentum import RSIIndicator; return _strip_nan(RSIIndicator(df["close"],window=timeperiod).rsi().values)
def _rsi_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.rsi(_c64(d["close"]),period=timeperiod))
def _rsi_fi(d,df,timeperiod=14,**_): return _strip_nan(_fi.RSI(df,timeperiod).values)
def _macd_ft(d,df,fastperiod=12,slowperiod=26,signalperiod=9,**_):
import ferro_ta; m,s,h=ferro_ta.MACD(d["close"],fastperiod=fastperiod,slowperiod=slowperiod,signalperiod=signalperiod); return _strip_nan(m)
def _macd_tl(d,df,fastperiod=12,slowperiod=26,signalperiod=9,**_):
m,s,h=_talib.MACD(d["close"],fastperiod=fastperiod,slowperiod=slowperiod,signalperiod=signalperiod); return _strip_nan(m)
def _macd_pt(d,df,fastperiod=12,slowperiod=26,signalperiod=9,**_):
r=_pta.macd(df["close"],fast=fastperiod,slow=slowperiod,signal=signalperiod); return _first_col(r,"MACD_")
def _macd_ta(d,df,fastperiod=12,slowperiod=26,signalperiod=9,**_):
from ta.trend import MACD; return _strip_nan(MACD(df["close"],window_fast=fastperiod,window_slow=slowperiod,window_sign=signalperiod).macd().values)
def _macd_tu(d,df,fastperiod=12,slowperiod=26,signalperiod=9,**_):
m,s,h=_tl.macd(_c64(d["close"]),short_period=fastperiod,long_period=slowperiod,signal_period=signalperiod); return _strip_nan(m)
def _macd_fi(d,df,fastperiod=12,slowperiod=26,signalperiod=9,**_):
return _strip_nan(_fi.MACD(df,fastperiod,slowperiod,signalperiod)["MACD"].values)
def _stoch_ft(d,df,fastk_period=14,slowk_period=3,slowd_period=3,**_):
import ferro_ta; k,dd=ferro_ta.STOCH(d["high"],d["low"],d["close"],fastk_period=fastk_period,slowk_period=slowk_period,slowd_period=slowd_period); return _strip_nan(k)
def _stoch_tl(d,df,fastk_period=14,slowk_period=3,slowd_period=3,**_):
k,dd=_talib.STOCH(d["high"],d["low"],d["close"],fastk_period=fastk_period,slowk_period=slowk_period,slowd_period=slowd_period); return _strip_nan(k)
def _stoch_pt(d,df,fastk_period=14,slowk_period=3,slowd_period=3,**_):
r=_pta.stoch(df["high"],df["low"],df["close"],k=fastk_period,d=slowd_period)
return _first_col(r,"STOCHk_") if r is not None else _empty()
def _stoch_ta(d,df,fastk_period=14,**_):
from ta.momentum import StochasticOscillator; return _strip_nan(StochasticOscillator(df["high"],df["low"],df["close"],window=fastk_period).stoch().values)
def _stoch_tu(d,df,fastk_period=14,slowk_period=3,slowd_period=3,**_):
k,dd=_tl.stoch(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),pct_k_period=fastk_period,pct_k_slowing_period=slowk_period,pct_d_period=slowd_period); return _strip_nan(k)
def _stoch_fi(d,df,fastk_period=14,**_): return _strip_nan(_fi.STOCH(df,fastk_period).values)
def _cci_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.CCI(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _cci_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.CCI(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _cci_pt(d,df,timeperiod=14,**_): return _strip_nan(_pta.cci(df["high"],df["low"],df["close"],length=timeperiod).values)
def _cci_ta(d,df,timeperiod=14,**_):
from ta.trend import CCIIndicator; return _strip_nan(CCIIndicator(df["high"],df["low"],df["close"],window=timeperiod).cci().values)
def _cci_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.cci(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),period=timeperiod))
def _cci_fi(d,df,timeperiod=14,**_): return _strip_nan(_fi.CCI(df,timeperiod).values)
def _willr_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.WILLR(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _willr_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.WILLR(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _willr_pt(d,df,timeperiod=14,**_): return _strip_nan(_pta.willr(df["high"],df["low"],df["close"],length=timeperiod).values)
def _willr_ta(d,df,timeperiod=14,**_):
from ta.momentum import WilliamsRIndicator; return _strip_nan(WilliamsRIndicator(df["high"],df["low"],df["close"],lbp=timeperiod).williams_r().values)
def _willr_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.willr(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),period=timeperiod))
def _willr_fi(d,df,timeperiod=14,**_): return _strip_nan(_fi.WILLIAMS(df,timeperiod).values)
def _aroon_ft(d,df,timeperiod=14,**_):
import ferro_ta; dn,up=ferro_ta.AROON(d["high"],d["low"],timeperiod=timeperiod); return _strip_nan(up)
def _aroon_tl(d,df,timeperiod=14,**_):
dn,up=_talib.AROON(d["high"],d["low"],timeperiod=timeperiod); return _strip_nan(up)
def _aroon_pt(d,df,timeperiod=14,**_):
r=_pta.aroon(df["high"],df["low"],length=timeperiod); return _first_col(r,"AROONU_") if r is not None else _empty()
def _aroon_ta(d,df,timeperiod=14,**_):
from ta.trend import AroonIndicator; return _strip_nan(AroonIndicator(df["high"],df["low"],window=timeperiod).aroon_up().values)
def _aroon_tu(d,df,timeperiod=14,**_):
dn,up=_tl.aroon(_c64(d["high"]),_c64(d["low"]),period=timeperiod); return _strip_nan(up)
def _aroon_fi(d,df,**_): return _empty()
_aroon_fi._stub = True
def _aroonosc_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.AROONOSC(d["high"],d["low"],timeperiod=timeperiod))
def _aroonosc_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.AROONOSC(d["high"],d["low"],timeperiod=timeperiod))
def _aroonosc_pt(d,df,**_): return _empty()
_aroonosc_pt._stub = True
def _aroonosc_ta(d,df,**_): return _empty()
_aroonosc_ta._stub = True
def _aroonosc_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.aroonosc(_c64(d["high"]),_c64(d["low"]),period=timeperiod))
def _aroonosc_fi(d,df,**_): return _empty()
_aroonosc_fi._stub = True
def _adx_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.ADX(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _adx_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.ADX(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _adx_pt(d,df,timeperiod=14,**_):
r=_pta.adx(df["high"],df["low"],df["close"],length=timeperiod); return _first_col(r,"ADX_")
def _adx_ta(d,df,timeperiod=14,**_):
from ta.trend import ADXIndicator; return _strip_nan(ADXIndicator(df["high"],df["low"],df["close"],window=timeperiod).adx().values)
def _adx_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.adx(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),period=timeperiod))
def _adx_fi(d,df,**_): return _empty()
_adx_fi._stub = True
def _mom_ft(d,df,timeperiod=10,**_):
import ferro_ta; return _strip_nan(ferro_ta.MOM(d["close"],timeperiod=timeperiod))
def _mom_tl(d,df,timeperiod=10,**_): return _strip_nan(_talib.MOM(d["close"],timeperiod=timeperiod))
def _mom_pt(d,df,timeperiod=10,**_): return _strip_nan(_pta.mom(df["close"],length=timeperiod).values)
def _mom_ta(d,df,**_): return _empty()
_mom_ta._stub = True
def _mom_tu(d,df,timeperiod=10,**_): return _strip_nan(_tl.mom(_c64(d["close"]),period=timeperiod))
def _mom_fi(d,df,timeperiod=10,**_): return _strip_nan(_fi.MOM(df,timeperiod).values)
def _roc_ft(d,df,timeperiod=10,**_):
import ferro_ta; return _strip_nan(ferro_ta.ROC(d["close"],timeperiod=timeperiod))
def _roc_tl(d,df,timeperiod=10,**_): return _strip_nan(_talib.ROC(d["close"],timeperiod=timeperiod))
def _roc_pt(d,df,timeperiod=10,**_): return _strip_nan(_pta.roc(df["close"],length=timeperiod).values)
def _roc_ta(d,df,timeperiod=10,**_):
from ta.momentum import ROCIndicator; return _strip_nan(ROCIndicator(df["close"],window=timeperiod).roc().values)
def _roc_tu(d,df,timeperiod=10,**_): return _strip_nan(_tl.roc(_c64(d["close"]),period=timeperiod) * 100.0)
def _roc_fi(d,df,timeperiod=10,**_): return _strip_nan(_fi.ROC(df,timeperiod).values)
def _cmo_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.CMO(d["close"],timeperiod=timeperiod))
def _cmo_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.CMO(d["close"],timeperiod=timeperiod))
def _cmo_pt(d,df,timeperiod=14,**_): return _strip_nan(_pta.cmo(df["close"],length=timeperiod).values)
def _cmo_ta(d,df,**_): return _empty()
_cmo_ta._stub = True
def _cmo_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.cmo(_c64(d["close"]),period=timeperiod))
def _cmo_fi(d,df,timeperiod=14,**_): return _strip_nan(_fi.CMO(df,timeperiod).values)
def _ppo_ft(d,df,fastperiod=12,slowperiod=26,**_):
import ferro_ta; ppo,sig,hist=ferro_ta.PPO(d["close"],fastperiod=fastperiod,slowperiod=slowperiod); return _strip_nan(ppo)
def _ppo_tl(d,df,fastperiod=12,slowperiod=26,**_): return _strip_nan(_talib.PPO(d["close"],fastperiod=fastperiod,slowperiod=slowperiod))
def _ppo_pt(d,df,fastperiod=12,slowperiod=26,**_):
r=_pta.ppo(df["close"],fast=fastperiod,slow=slowperiod)
return _strip_nan(r.iloc[:,0].values) if r is not None else _empty()
def _ppo_ta(d,df,**_): return _empty()
_ppo_ta._stub = True
def _ppo_tu(d,df,fastperiod=12,slowperiod=26,**_): return _strip_nan(_tl.ppo(_c64(d["close"]),short_period=fastperiod,long_period=slowperiod))
def _ppo_fi(d,df,fastperiod=12,slowperiod=26,**_): return _strip_nan(_fi.PPO(df,fastperiod,slowperiod).values)
def _trix_ft(d,df,timeperiod=18,**_):
import ferro_ta; return _strip_nan(ferro_ta.TRIX(d["close"],timeperiod=timeperiod))
def _trix_tl(d,df,timeperiod=18,**_): return _strip_nan(_talib.TRIX(d["close"],timeperiod=timeperiod))
def _trix_pt(d,df,timeperiod=18,**_):
r=_pta.trix(df["close"],length=timeperiod)
return _strip_nan(r.iloc[:,0].values) if r is not None else _empty()
def _trix_ta(d,df,timeperiod=18,**_):
from ta.trend import TRIXIndicator; return _strip_nan(TRIXIndicator(df["close"],window=timeperiod).trix().values)
def _trix_tu(d,df,timeperiod=18,**_): return _strip_nan(_tl.trix(_c64(d["close"]),period=timeperiod))
def _trix_fi(d,df,timeperiod=18,**_): return _strip_nan(_fi.TRIX(df,timeperiod).values)
def _tsf_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.TSF(d["close"],timeperiod=timeperiod))
def _tsf_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.TSF(d["close"],timeperiod=timeperiod))
def _tsf_pt(d,df,**_): return _empty()
_tsf_pt._stub = True
def _tsf_ta(d,df,**_): return _empty()
_tsf_ta._stub = True
def _tsf_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.tsf(_c64(d["close"]),period=timeperiod))
def _tsf_fi(d,df,**_): return _empty()
_tsf_fi._stub = True
def _ultosc_ft(d,df,timeperiod1=7,timeperiod2=14,timeperiod3=28,**_):
import ferro_ta; return _strip_nan(ferro_ta.ULTOSC(d["high"],d["low"],d["close"],timeperiod1=timeperiod1,timeperiod2=timeperiod2,timeperiod3=timeperiod3))
def _ultosc_tl(d,df,timeperiod1=7,timeperiod2=14,timeperiod3=28,**_):
return _strip_nan(_talib.ULTOSC(d["high"],d["low"],d["close"],timeperiod1=timeperiod1,timeperiod2=timeperiod2,timeperiod3=timeperiod3))
def _ultosc_pt(d,df,**_): return _empty()
_ultosc_pt._stub = True
def _ultosc_ta(d,df,timeperiod1=7,timeperiod2=14,timeperiod3=28,**_):
from ta.momentum import UltimateOscillator
return _strip_nan(UltimateOscillator(df["high"],df["low"],df["close"],window1=timeperiod1,window2=timeperiod2,window3=timeperiod3).ultimate_oscillator().values)
def _ultosc_tu(d,df,timeperiod1=7,timeperiod2=14,timeperiod3=28,**_):
return _strip_nan(_tl.ultosc(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),short_period=timeperiod1,medium_period=timeperiod2,long_period=timeperiod3))
def _ultosc_fi(d,df,**_): return _empty()
_ultosc_fi._stub = True
def _bop_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.BOP(d["open"],d["high"],d["low"],d["close"]))
def _bop_tl(d,df,**_): return _strip_nan(_talib.BOP(d["open"],d["high"],d["low"],d["close"]))
def _bop_pt(d,df,**_):
r=_pta.bop(df["open"],df["high"],df["low"],df["close"]); return _strip_nan(r.values) if r is not None else _empty()
def _bop_ta(d,df,**_): return _empty()
_bop_ta._stub = True
def _bop_tu(d,df,**_): return _strip_nan(_tl.bop(_c64(d["open"]),_c64(d["high"]),_c64(d["low"]),_c64(d["close"])))
def _bop_fi(d,df,**_): return _empty()
_bop_fi._stub = True
def _plusdi_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.PLUS_DI(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _plusdi_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.PLUS_DI(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _plusdi_pt(d,df,timeperiod=14,**_):
r=_pta.adx(df["high"],df["low"],df["close"],length=timeperiod); return _first_col(r,"DMP_") if r is not None else _empty()
def _plusdi_ta(d,df,**_): return _empty()
_plusdi_ta._stub = True
def _plusdi_tu(d,df,timeperiod=14,**_):
pdi,mdi=_tl.di(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),period=timeperiod); return _strip_nan(pdi)
def _plusdi_fi(d,df,**_): return _empty()
_plusdi_fi._stub = True
def _minusdi_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.MINUS_DI(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _minusdi_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.MINUS_DI(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _minusdi_pt(d,df,**_): return _empty()
_minusdi_pt._stub = True
def _minusdi_ta(d,df,**_): return _empty()
_minusdi_ta._stub = True
def _minusdi_tu(d,df,timeperiod=14,**_):
pdi,mdi=_tl.di(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),period=timeperiod); return _strip_nan(mdi)
def _minusdi_fi(d,df,**_): return _empty()
_minusdi_fi._stub = True
# ============================================================
# VOLATILITY
# ============================================================
def _bb_ft(d,df,timeperiod=20,nbdevup=2.0,nbdevdn=2.0,**_):
import ferro_ta; u,m,l=ferro_ta.BBANDS(d["close"],timeperiod=timeperiod,nbdevup=nbdevup,nbdevdn=nbdevdn); return _strip_nan(u)
def _bb_tl(d,df,timeperiod=20,nbdevup=2.0,nbdevdn=2.0,**_):
u,m,l=_talib.BBANDS(d["close"],timeperiod=timeperiod,nbdevup=nbdevup,nbdevdn=nbdevdn); return _strip_nan(u)
def _bb_pt(d,df,timeperiod=20,nbdevup=2.0,**_):
r=_pta.bbands(df["close"],length=timeperiod,std=nbdevup); return _first_col(r,"BBU_")
def _bb_ta(d,df,timeperiod=20,nbdevup=2.0,**_):
from ta.volatility import BollingerBands; return _strip_nan(BollingerBands(df["close"],window=timeperiod,window_dev=nbdevup).bollinger_hband().values)
def _bb_tu(d,df,timeperiod=20,nbdevup=2.0,**_):
lo,mi,up=_tl.bbands(_c64(d["close"]),period=timeperiod,stddev=nbdevup); return _strip_nan(up)
def _bb_fi(d,df,timeperiod=20,**_): return _strip_nan(_fi.BBANDS(df,timeperiod)["BB_UPPER"].values)
def _atr_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.ATR(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _atr_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.ATR(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _atr_pt(d,df,timeperiod=14,**_): return _strip_nan(_pta.atr(df["high"],df["low"],df["close"],length=timeperiod).values)
def _atr_ta(d,df,timeperiod=14,**_):
from ta.volatility import AverageTrueRange; return _strip_nan(AverageTrueRange(df["high"],df["low"],df["close"],window=timeperiod).average_true_range().values)
def _atr_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.atr(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),period=timeperiod))
def _atr_fi(d,df,timeperiod=14,**_): return _strip_nan(_fi.ATR(df,timeperiod).values)
def _natr_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.NATR(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _natr_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.NATR(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _natr_pt(d,df,timeperiod=14,**_): return _strip_nan(_pta.natr(df["high"],df["low"],df["close"],length=timeperiod).values)
def _natr_ta(d,df,**_): return _empty()
_natr_ta._stub = True
def _natr_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.natr(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),period=timeperiod))
def _natr_fi(d,df,**_): return _empty()
_natr_fi._stub = True
def _trange_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.TRANGE(d["high"],d["low"],d["close"]))
def _trange_tl(d,df,**_): return _strip_nan(_talib.TRANGE(d["high"],d["low"],d["close"]))
def _trange_pt(d,df,**_):
r=_pta.true_range(df["high"],df["low"],df["close"]); return _strip_nan(r.values) if r is not None else _empty()
def _trange_ta(d,df,**_): return _empty()
_trange_ta._stub = True
def _trange_tu(d,df,**_): return _strip_nan(_tl.tr(_c64(d["high"]),_c64(d["low"]),_c64(d["close"])))
def _trange_fi(d,df,**_): return _strip_nan(_fi.TR(df).values)
def _stddev_ft(d,df,timeperiod=20,**_):
import ferro_ta; return _strip_nan(ferro_ta.STDDEV(d["close"],timeperiod=timeperiod))
def _stddev_tl(d,df,timeperiod=20,**_): return _strip_nan(_talib.STDDEV(d["close"],timeperiod=timeperiod))
def _stddev_pt(d,df,timeperiod=20,**_):
r=_pta.stdev(df["close"],length=timeperiod); return _strip_nan(r.values) if r is not None else _empty()
def _stddev_ta(d,df,**_): return _empty()
_stddev_ta._stub = True
def _stddev_tu(d,df,timeperiod=20,**_): return _strip_nan(_tl.stddev(_c64(d["close"]),period=timeperiod))
def _stddev_fi(d,df,timeperiod=20,**_): return _strip_nan(_fi.MSD(df,timeperiod).values)
def _var_ft(d,df,timeperiod=20,**_):
import ferro_ta; return _strip_nan(ferro_ta.VAR(d["close"],timeperiod=timeperiod))
def _var_tl(d,df,timeperiod=20,**_): return _strip_nan(_talib.VAR(d["close"],timeperiod=timeperiod))
def _var_pt(d,df,timeperiod=20,**_):
r=_pta.variance(df["close"],length=timeperiod); return _strip_nan(r.values) if r is not None else _empty()
def _var_ta(d,df,**_): return _empty()
_var_ta._stub = True
def _var_tu(d,df,timeperiod=20,**_): return _strip_nan(_tl.var(_c64(d["close"]),period=timeperiod))
def _var_fi(d,df,**_): return _empty()
_var_fi._stub = True
def _sar_ft(d,df,acceleration=0.02,maximum=0.2,**_):
import ferro_ta; return _strip_nan(ferro_ta.SAR(d["high"],d["low"],acceleration=acceleration,maximum=maximum))
def _sar_tl(d,df,acceleration=0.02,maximum=0.2,**_): return _strip_nan(_talib.SAR(d["high"],d["low"],acceleration=acceleration,maximum=maximum))
def _sar_pt(d,df,**_): return _empty()
_sar_pt._stub = True
def _sar_ta(d,df,**_): return _empty()
_sar_ta._stub = True
def _sar_tu(d,df,acceleration=0.02,maximum=0.2,**_): return _strip_nan(_tl.psar(_c64(d["high"]),_c64(d["low"]),acceleration_factor_step=acceleration,acceleration_factor_maximum=maximum))
def _sar_fi(d,df,**_): return _empty()
_sar_fi._stub = True
def _kc_ft(d,df,timeperiod=20,**_):
import ferro_ta; u,m,l=ferro_ta.KELTNER_CHANNELS(d["high"],d["low"],d["close"],timeperiod=timeperiod); return _strip_nan(u)
def _kc_tl(d,df,**_): return _empty()
_kc_tl._stub = True
def _kc_pt(d,df,timeperiod=20,**_):
r=_pta.kc(df["high"],df["low"],df["close"],length=timeperiod)
if r is None: return _empty()
col=next((c for c in r.columns if "UCe" in c or "UB" in c or c.endswith("U")),None)
return _strip_nan(r[col].values) if col else _first_col(r,"KC")
def _kc_ta(d,df,timeperiod=20,**_):
from ta.volatility import KeltnerChannel; return _strip_nan(KeltnerChannel(df["high"],df["low"],df["close"],window=timeperiod).keltner_channel_hband().values)
def _kc_tu(d,df,**_): return _empty()
_kc_tu._stub = True
def _kc_fi(d,df,**_): return _empty()
_kc_fi._stub = True
def _donchian_ft(d,df,timeperiod=20,**_):
import ferro_ta; u,m,l=ferro_ta.DONCHIAN(d["high"],d["low"],timeperiod=timeperiod); return _strip_nan(u)
def _donchian_tl(d,df,**_): return _empty()
_donchian_tl._stub = True
def _donchian_pt(d,df,timeperiod=20,**_):
r=_pta.donchian(df["high"],df["low"],lower_length=timeperiod,upper_length=timeperiod)
return _first_col(r,"DCU_") if r is not None else _empty()
def _donchian_ta(d,df,timeperiod=20,**_):
from ta.volatility import DonchianChannel; return _strip_nan(DonchianChannel(df["high"],df["low"],df["close"],window=timeperiod).donchian_channel_hband().values)
def _donchian_tu(d,df,**_): return _empty()
_donchian_tu._stub = True
def _donchian_fi(d,df,**_): return _empty()
_donchian_fi._stub = True
def _supertrend_ft(d,df,timeperiod=7,**_):
import ferro_ta; st,dir_=ferro_ta.SUPERTREND(d["high"],d["low"],d["close"],timeperiod=timeperiod); return _strip_nan(st)
def _supertrend_tl(d,df,**_): return _empty()
_supertrend_tl._stub = True
def _supertrend_pt(d,df,timeperiod=7,**_):
r=_pta.supertrend(df["high"],df["low"],df["close"],length=timeperiod)
return _first_col(r,"SUPERT_") if r is not None else _empty()
def _supertrend_ta(d,df,**_): return _empty()
_supertrend_ta._stub = True
def _supertrend_tu(d,df,**_): return _empty()
_supertrend_tu._stub = True
def _supertrend_fi(d,df,**_): return _empty()
_supertrend_fi._stub = True
def _chop_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.CHOPPINESS_INDEX(d["high"],d["low"],d["close"],timeperiod=timeperiod))
def _chop_tl(d,df,**_): return _empty()
_chop_tl._stub = True
def _chop_pt(d,df,timeperiod=14,**_):
r=_pta.chop(df["high"],df["low"],df["close"],length=timeperiod); return _strip_nan(r.values) if r is not None else _empty()
def _chop_ta(d,df,**_): return _empty()
_chop_ta._stub = True
def _chop_tu(d,df,**_): return _empty()
_chop_tu._stub = True
def _chop_fi(d,df,**_): return _empty()
_chop_fi._stub = True
# ============================================================
# VOLUME
# ============================================================
def _obv_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.OBV(d["close"],d["volume"]))
def _obv_tl(d,df,**_): return _strip_nan(_talib.OBV(d["close"],d["volume"]))
def _obv_pt(d,df,**_): return _strip_nan(_pta.obv(df["close"],df["volume"]).values)
def _obv_ta(d,df,**_):
from ta.volume import OnBalanceVolumeIndicator; return _strip_nan(OnBalanceVolumeIndicator(df["close"],df["volume"]).on_balance_volume().values)
def _obv_tu(d,df,**_): return _strip_nan(_tl.obv(_c64(d["close"]),_c64(d["volume"])))
def _obv_fi(d,df,**_): return _strip_nan(_fi.OBV(df).values)
def _ad_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.AD(d["high"],d["low"],d["close"],d["volume"]))
def _ad_tl(d,df,**_): return _strip_nan(_talib.AD(d["high"],d["low"],d["close"],d["volume"]))
def _ad_pt(d,df,**_): return _strip_nan(_pta.ad(df["high"],df["low"],df["close"],df["volume"]).values)
def _ad_ta(d,df,**_):
from ta.volume import AccDistIndexIndicator; return _strip_nan(AccDistIndexIndicator(df["high"],df["low"],df["close"],df["volume"]).acc_dist_index().values)
def _ad_tu(d,df,**_): return _strip_nan(_tl.ad(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),_c64(d["volume"])))
def _ad_fi(d,df,**_): return _empty()
_ad_fi._stub = True
def _adosc_ft(d,df,fastperiod=3,slowperiod=10,**_):
import ferro_ta; return _strip_nan(ferro_ta.ADOSC(d["high"],d["low"],d["close"],d["volume"],fastperiod=fastperiod,slowperiod=slowperiod))
def _adosc_tl(d,df,fastperiod=3,slowperiod=10,**_): return _strip_nan(_talib.ADOSC(d["high"],d["low"],d["close"],d["volume"],fastperiod=fastperiod,slowperiod=slowperiod))
def _adosc_pt(d,df,fastperiod=3,slowperiod=10,**_): return _strip_nan(_pta.adosc(df["high"],df["low"],df["close"],df["volume"],fast=fastperiod,slow=slowperiod).values)
def _adosc_ta(d,df,**_): return _empty()
_adosc_ta._stub = True
def _adosc_tu(d,df,fastperiod=3,slowperiod=10,**_): return _strip_nan(_tl.adosc(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),_c64(d["volume"]),short_period=fastperiod,long_period=slowperiod))
def _adosc_fi(d,df,**_): return _empty()
_adosc_fi._stub = True
def _mfi_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.MFI(d["high"],d["low"],d["close"],d["volume"],timeperiod=timeperiod))
def _mfi_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.MFI(d["high"],d["low"],d["close"],d["volume"],timeperiod=timeperiod))
def _mfi_pt(d,df,timeperiod=14,**_): return _strip_nan(_pta.mfi(df["high"],df["low"],df["close"],df["volume"],length=timeperiod).values)
def _mfi_ta(d,df,timeperiod=14,**_):
from ta.volume import MFIIndicator; return _strip_nan(MFIIndicator(df["high"],df["low"],df["close"],df["volume"],window=timeperiod).money_flow_index().values)
def _mfi_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.mfi(_c64(d["high"]),_c64(d["low"]),_c64(d["close"]),_c64(d["volume"]),period=timeperiod))
def _mfi_fi(d,df,timeperiod=14,**_): return _strip_nan(_fi.MFI(df,timeperiod).values)
def _vwap_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.VWAP(d["high"],d["low"],d["close"],d["volume"]))
def _vwap_tl(d,df,**_): return _empty()
_vwap_tl._stub = True
def _vwap_pt(d,df,**_):
r=_pta.vwap(df["high"],df["low"],df["close"],df["volume"]); return _strip_nan(r.values) if r is not None else _empty()
def _vwap_ta(d,df,**_): return _empty()
_vwap_ta._stub = True
def _vwap_tu(d,df,**_): return _empty()
_vwap_tu._stub = True
def _vwap_fi(d,df,**_):
return _strip_nan(_fi.VWAP(df).values)
# ============================================================
# PRICE TRANSFORM
# ============================================================
def _avgprice_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.AVGPRICE(d["open"],d["high"],d["low"],d["close"]))
def _avgprice_tl(d,df,**_): return _strip_nan(_talib.AVGPRICE(d["open"],d["high"],d["low"],d["close"]))
def _avgprice_pt(d,df,**_): return _empty()
_avgprice_pt._stub = True
def _avgprice_ta(d,df,**_): return _empty()
_avgprice_ta._stub = True
def _avgprice_tu(d,df,**_): return _strip_nan(_tl.avgprice(_c64(d["open"]),_c64(d["high"]),_c64(d["low"]),_c64(d["close"])))
def _avgprice_fi(d,df,**_): return _empty()
_avgprice_fi._stub = True
def _medprice_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.MEDPRICE(d["high"],d["low"]))
def _medprice_tl(d,df,**_): return _strip_nan(_talib.MEDPRICE(d["high"],d["low"]))
def _medprice_pt(d,df,**_): return _empty()
_medprice_pt._stub = True
def _medprice_ta(d,df,**_): return _empty()
_medprice_ta._stub = True
def _medprice_tu(d,df,**_): return _strip_nan(_tl.medprice(_c64(d["high"]),_c64(d["low"])))
def _medprice_fi(d,df,**_): return _empty()
_medprice_fi._stub = True
def _typprice_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.TYPPRICE(d["high"],d["low"],d["close"]))
def _typprice_tl(d,df,**_): return _strip_nan(_talib.TYPPRICE(d["high"],d["low"],d["close"]))
def _typprice_pt(d,df,**_): return _empty()
_typprice_pt._stub = True
def _typprice_ta(d,df,**_): return _empty()
_typprice_ta._stub = True
def _typprice_tu(d,df,**_): return _strip_nan(_tl.typprice(_c64(d["high"]),_c64(d["low"]),_c64(d["close"])))
def _typprice_fi(d,df,**_): return _empty()
_typprice_fi._stub = True
def _wclprice_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.WCLPRICE(d["high"],d["low"],d["close"]))
def _wclprice_tl(d,df,**_): return _strip_nan(_talib.WCLPRICE(d["high"],d["low"],d["close"]))
def _wclprice_pt(d,df,**_): return _empty()
_wclprice_pt._stub = True
def _wclprice_ta(d,df,**_): return _empty()
_wclprice_ta._stub = True
def _wclprice_tu(d,df,**_): return _strip_nan(_tl.wcprice(_c64(d["high"]),_c64(d["low"]),_c64(d["close"])))
def _wclprice_fi(d,df,**_): return _empty()
_wclprice_fi._stub = True
# ============================================================
# MATH
# ============================================================
def _sqrt_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.SQRT(d["close"]))
def _sqrt_tl(d,df,**_): return _strip_nan(_talib.SQRT(d["close"]))
def _sqrt_pt(d,df,**_): return _empty()
_sqrt_pt._stub = True
def _sqrt_ta(d,df,**_): return _empty()
_sqrt_ta._stub = True
def _sqrt_tu(d,df,**_): return _strip_nan(_tl.sqrt(_c64(d["close"])))
def _sqrt_fi(d,df,**_): return _empty()
_sqrt_fi._stub = True
def _log10_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.LOG10(d["close"]))
def _log10_tl(d,df,**_): return _strip_nan(_talib.LOG10(d["close"]))
def _log10_pt(d,df,**_): return _empty()
_log10_pt._stub = True
def _log10_ta(d,df,**_): return _empty()
_log10_ta._stub = True
def _log10_tu(d,df,**_): return _strip_nan(_tl.log10(_c64(d["close"])))
def _log10_fi(d,df,**_): return _empty()
_log10_fi._stub = True
def _add_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.ADD(d["high"],d["low"]))
def _add_tl(d,df,**_): return _strip_nan(_talib.ADD(d["high"],d["low"]))
def _add_pt(d,df,**_): return _empty()
_add_pt._stub = True
def _add_ta(d,df,**_): return _empty()
_add_ta._stub = True
def _add_tu(d,df,**_): return _strip_nan(_tl.add(_c64(d["high"]),_c64(d["low"])))
def _add_fi(d,df,**_): return _empty()
_add_fi._stub = True
# ============================================================
# STATISTICS
# ============================================================
def _linearreg_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.LINEARREG(d["close"],timeperiod=timeperiod))
def _linearreg_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.LINEARREG(d["close"],timeperiod=timeperiod))
def _linearreg_pt(d,df,**_): return _empty()
_linearreg_pt._stub = True
def _linearreg_ta(d,df,**_): return _empty()
_linearreg_ta._stub = True
def _linearreg_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.linreg(_c64(d["close"]),period=timeperiod))
def _linearreg_fi(d,df,**_): return _empty()
_linearreg_fi._stub = True
def _linreg_slope_ft(d,df,timeperiod=14,**_):
import ferro_ta; return _strip_nan(ferro_ta.LINEARREG_SLOPE(d["close"],timeperiod=timeperiod))
def _linreg_slope_tl(d,df,timeperiod=14,**_): return _strip_nan(_talib.LINEARREG_SLOPE(d["close"],timeperiod=timeperiod))
def _linreg_slope_pt(d,df,**_): return _empty()
_linreg_slope_pt._stub = True
def _linreg_slope_ta(d,df,**_): return _empty()
_linreg_slope_ta._stub = True
def _linreg_slope_tu(d,df,timeperiod=14,**_): return _strip_nan(_tl.linregslope(_c64(d["close"]),period=timeperiod))
def _linreg_slope_fi(d,df,**_): return _empty()
_linreg_slope_fi._stub = True
def _correl_ft(d,df,timeperiod=30,**_):
import ferro_ta; return _strip_nan(ferro_ta.CORREL(d["high"],d["low"],timeperiod=timeperiod))
def _correl_tl(d,df,timeperiod=30,**_): return _strip_nan(_talib.CORREL(d["high"],d["low"],timeperiod=timeperiod))
def _correl_pt(d,df,**_): return _empty()
_correl_pt._stub = True
def _correl_ta(d,df,**_): return _empty()
_correl_ta._stub = True
def _correl_tu(d,df,**_): return _empty()
_correl_tu._stub = True
def _correl_fi(d,df,**_): return _empty()
_correl_fi._stub = True
def _beta_ft(d,df,timeperiod=5,**_):
import ferro_ta; return _strip_nan(ferro_ta.BETA(d["high"],d["low"],timeperiod=timeperiod))
def _beta_tl(d,df,timeperiod=5,**_): return _strip_nan(_talib.BETA(d["high"],d["low"],timeperiod=timeperiod))
def _beta_pt(d,df,**_): return _empty()
_beta_pt._stub = True
def _beta_ta(d,df,**_): return _empty()
_beta_ta._stub = True
def _beta_tu(d,df,**_): return _empty()
_beta_tu._stub = True
def _beta_fi(d,df,**_): return _empty()
_beta_fi._stub = True
# ============================================================
# CYCLE
# ============================================================
def _ht_dcperiod_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.HT_DCPERIOD(d["close"]))
def _ht_dcperiod_tl(d,df,**_): return _strip_nan(_talib.HT_DCPERIOD(d["close"]))
def _ht_dcperiod_pt(d,df,**_): return _empty()
_ht_dcperiod_pt._stub = True
def _ht_dcperiod_ta(d,df,**_): return _empty()
_ht_dcperiod_ta._stub = True
def _ht_dcperiod_tu(d,df,**_): return _empty()
_ht_dcperiod_tu._stub = True
def _ht_dcperiod_fi(d,df,**_): return _empty()
_ht_dcperiod_fi._stub = True
def _ht_trendmode_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.HT_TRENDMODE(d["close"]).astype(float))
def _ht_trendmode_tl(d,df,**_): return _strip_nan(_talib.HT_TRENDMODE(d["close"]).astype(float))
def _ht_trendmode_pt(d,df,**_): return _empty()
_ht_trendmode_pt._stub = True
def _ht_trendmode_ta(d,df,**_): return _empty()
_ht_trendmode_ta._stub = True
def _ht_trendmode_tu(d,df,**_): return _empty()
_ht_trendmode_tu._stub = True
def _ht_trendmode_fi(d,df,**_): return _empty()
_ht_trendmode_fi._stub = True
# ============================================================
# CANDLESTICK PATTERNS
# ============================================================
def _cdlengulfing_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.CDLENGULFING(d["open"],d["high"],d["low"],d["close"]).astype(float))
def _cdlengulfing_tl(d,df,**_): return _strip_nan(_talib.CDLENGULFING(d["open"],d["high"],d["low"],d["close"]).astype(float))
def _cdlengulfing_pt(d,df,**_): return _empty()
_cdlengulfing_pt._stub = True
def _cdlengulfing_ta(d,df,**_): return _empty()
_cdlengulfing_ta._stub = True
def _cdlengulfing_tu(d,df,**_): return _empty()
_cdlengulfing_tu._stub = True
def _cdlengulfing_fi(d,df,**_): return _empty()
_cdlengulfing_fi._stub = True
def _cdldoji_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.CDLDOJI(d["open"],d["high"],d["low"],d["close"]).astype(float))
def _cdldoji_tl(d,df,**_): return _strip_nan(_talib.CDLDOJI(d["open"],d["high"],d["low"],d["close"]).astype(float))
def _cdldoji_pt(d,df,**_): return _empty()
_cdldoji_pt._stub = True
def _cdldoji_ta(d,df,**_): return _empty()
_cdldoji_ta._stub = True
def _cdldoji_tu(d,df,**_): return _empty()
_cdldoji_tu._stub = True
def _cdldoji_fi(d,df,**_): return _empty()
_cdldoji_fi._stub = True
def _cdlhammer_ft(d,df,**_):
import ferro_ta; return _strip_nan(ferro_ta.CDLHAMMER(d["open"],d["high"],d["low"],d["close"]).astype(float))
def _cdlhammer_tl(d,df,**_): return _strip_nan(_talib.CDLHAMMER(d["open"],d["high"],d["low"],d["close"]).astype(float))
def _cdlhammer_pt(d,df,**_): return _empty()
_cdlhammer_pt._stub = True
def _cdlhammer_ta(d,df,**_): return _empty()
_cdlhammer_ta._stub = True
def _cdlhammer_tu(d,df,**_): return _empty()
_cdlhammer_tu._stub = True
def _cdlhammer_fi(d,df,**_): return _empty()
_cdlhammer_fi._stub = True
# ============================================================
# REGISTRY BUILD
# ============================================================
REGISTRY: dict[tuple[str,Any], Any] = {}
def _reg(ind, ft, tl, pt, ta_, tu, fi):
"""
Register wrappers for a given indicator across all libraries.
Wrappers marked ._stub = True (no-op return _empty()) are not registered,
so execute_indicator raises KeyError for unsupported (lib, ind). Speed
benchmarks then skip those pairs and the table shows N/A.
"""
for lib, fn in [
("ferro_ta", ft),
("talib", tl),
("pandas_ta",pt),
("ta", ta_),
("tulipy", tu),
("finta", fi),
]:
if getattr(fn, "_stub", False):
continue
REGISTRY[(lib, ind)] = fn
_reg("SMA",_sma_ft,_sma_tl,_sma_pt,_sma_ta,_sma_tu,_sma_fi)
_reg("EMA",_ema_ft,_ema_tl,_ema_pt,_ema_ta,_ema_tu,_ema_fi)
_reg("WMA",_wma_ft,_wma_tl,_wma_pt,_wma_ta,_wma_tu,_wma_fi)
_reg("DEMA",_dema_ft,_dema_tl,_dema_pt,_dema_ta,_dema_tu,_dema_fi)
_reg("TEMA",_tema_ft,_tema_tl,_tema_pt,_tema_ta,_tema_tu,_tema_fi)
_reg("T3",_t3_ft,_t3_tl,_t3_pt,_t3_ta,_t3_tu,_t3_fi)
_reg("TRIMA",_trima_ft,_trima_tl,_trima_pt,_trima_ta,_trima_tu,_trima_fi)
_reg("KAMA",_kama_ft,_kama_tl,_kama_pt,_kama_ta,_kama_tu,_kama_fi)
_reg("HULL_MA",_hma_ft,_hma_tl,_hma_pt,_hma_ta,_hma_tu,_hma_fi)
_reg("VWMA",_vwma_ft,_vwma_tl,_vwma_pt,_vwma_ta,_vwma_tu,_vwma_fi)
_reg("MIDPOINT",_midpoint_ft,_midpoint_tl,_midpoint_pt,_midpoint_ta,_midpoint_tu,_midpoint_fi)
_reg("MIDPRICE",_midprice_ft,_midprice_tl,_midprice_pt,_midprice_ta,_midprice_tu,_midprice_fi)
_reg("RSI",_rsi_ft,_rsi_tl,_rsi_pt,_rsi_ta,_rsi_tu,_rsi_fi)
_reg("MACD",_macd_ft,_macd_tl,_macd_pt,_macd_ta,_macd_tu,_macd_fi)
_reg("STOCH",_stoch_ft,_stoch_tl,_stoch_pt,_stoch_ta,_stoch_tu,_stoch_fi)
_reg("CCI",_cci_ft,_cci_tl,_cci_pt,_cci_ta,_cci_tu,_cci_fi)
_reg("WILLR",_willr_ft,_willr_tl,_willr_pt,_willr_ta,_willr_tu,_willr_fi)
_reg("AROON",_aroon_ft,_aroon_tl,_aroon_pt,_aroon_ta,_aroon_tu,_aroon_fi)
_reg("AROONOSC",_aroonosc_ft,_aroonosc_tl,_aroonosc_pt,_aroonosc_ta,_aroonosc_tu,_aroonosc_fi)
_reg("ADX",_adx_ft,_adx_tl,_adx_pt,_adx_ta,_adx_tu,_adx_fi)
_reg("MOM",_mom_ft,_mom_tl,_mom_pt,_mom_ta,_mom_tu,_mom_fi)
_reg("ROC",_roc_ft,_roc_tl,_roc_pt,_roc_ta,_roc_tu,_roc_fi)
_reg("CMO",_cmo_ft,_cmo_tl,_cmo_pt,_cmo_ta,_cmo_tu,_cmo_fi)
_reg("PPO",_ppo_ft,_ppo_tl,_ppo_pt,_ppo_ta,_ppo_tu,_ppo_fi)
_reg("TRIX",_trix_ft,_trix_tl,_trix_pt,_trix_ta,_trix_tu,_trix_fi)
_reg("TSF",_tsf_ft,_tsf_tl,_tsf_pt,_tsf_ta,_tsf_tu,_tsf_fi)
_reg("ULTOSC",_ultosc_ft,_ultosc_tl,_ultosc_pt,_ultosc_ta,_ultosc_tu,_ultosc_fi)
_reg("BOP",_bop_ft,_bop_tl,_bop_pt,_bop_ta,_bop_tu,_bop_fi)
_reg("PLUS_DI",_plusdi_ft,_plusdi_tl,_plusdi_pt,_plusdi_ta,_plusdi_tu,_plusdi_fi)
_reg("MINUS_DI",_minusdi_ft,_minusdi_tl,_minusdi_pt,_minusdi_ta,_minusdi_tu,_minusdi_fi)
_reg("BBANDS",_bb_ft,_bb_tl,_bb_pt,_bb_ta,_bb_tu,_bb_fi)
_reg("ATR",_atr_ft,_atr_tl,_atr_pt,_atr_ta,_atr_tu,_atr_fi)
_reg("NATR",_natr_ft,_natr_tl,_natr_pt,_natr_ta,_natr_tu,_natr_fi)
_reg("TRANGE",_trange_ft,_trange_tl,_trange_pt,_trange_ta,_trange_tu,_trange_fi)
_reg("STDDEV",_stddev_ft,_stddev_tl,_stddev_pt,_stddev_ta,_stddev_tu,_stddev_fi)
_reg("VAR",_var_ft,_var_tl,_var_pt,_var_ta,_var_tu,_var_fi)
_reg("SAR",_sar_ft,_sar_tl,_sar_pt,_sar_ta,_sar_tu,_sar_fi)
_reg("KELTNER_CHANNELS",_kc_ft,_kc_tl,_kc_pt,_kc_ta,_kc_tu,_kc_fi)
_reg("DONCHIAN",_donchian_ft,_donchian_tl,_donchian_pt,_donchian_ta,_donchian_tu,_donchian_fi)
_reg("SUPERTREND",_supertrend_ft,_supertrend_tl,_supertrend_pt,_supertrend_ta,_supertrend_tu,_supertrend_fi)
_reg("CHOPPINESS_INDEX",_chop_ft,_chop_tl,_chop_pt,_chop_ta,_chop_tu,_chop_fi)
_reg("OBV",_obv_ft,_obv_tl,_obv_pt,_obv_ta,_obv_tu,_obv_fi)
_reg("AD",_ad_ft,_ad_tl,_ad_pt,_ad_ta,_ad_tu,_ad_fi)
_reg("ADOSC",_adosc_ft,_adosc_tl,_adosc_pt,_adosc_ta,_adosc_tu,_adosc_fi)
_reg("MFI",_mfi_ft,_mfi_tl,_mfi_pt,_mfi_ta,_mfi_tu,_mfi_fi)
_reg("VWAP",_vwap_ft,_vwap_tl,_vwap_pt,_vwap_ta,_vwap_tu,_vwap_fi)
_reg("AVGPRICE",_avgprice_ft,_avgprice_tl,_avgprice_pt,_avgprice_ta,_avgprice_tu,_avgprice_fi)
_reg("MEDPRICE",_medprice_ft,_medprice_tl,_medprice_pt,_medprice_ta,_medprice_tu,_medprice_fi)
_reg("TYPPRICE",_typprice_ft,_typprice_tl,_typprice_pt,_typprice_ta,_typprice_tu,_typprice_fi)
_reg("WCLPRICE",_wclprice_ft,_wclprice_tl,_wclprice_pt,_wclprice_ta,_wclprice_tu,_wclprice_fi)
_reg("SQRT",_sqrt_ft,_sqrt_tl,_sqrt_pt,_sqrt_ta,_sqrt_tu,_sqrt_fi)
_reg("LOG10",_log10_ft,_log10_tl,_log10_pt,_log10_ta,_log10_tu,_log10_fi)
_reg("ADD",_add_ft,_add_tl,_add_pt,_add_ta,_add_tu,_add_fi)
_reg("LINEARREG",_linearreg_ft,_linearreg_tl,_linearreg_pt,_linearreg_ta,_linearreg_tu,_linearreg_fi)
_reg("LINEARREG_SLOPE",_linreg_slope_ft,_linreg_slope_tl,_linreg_slope_pt,_linreg_slope_ta,_linreg_slope_tu,_linreg_slope_fi)
_reg("CORREL",_correl_ft,_correl_tl,_correl_pt,_correl_ta,_correl_tu,_correl_fi)
_reg("BETA",_beta_ft,_beta_tl,_beta_pt,_beta_ta,_beta_tu,_beta_fi)
_reg("HT_DCPERIOD",_ht_dcperiod_ft,_ht_dcperiod_tl,_ht_dcperiod_pt,_ht_dcperiod_ta,_ht_dcperiod_tu,_ht_dcperiod_fi)
_reg("HT_TRENDMODE",_ht_trendmode_ft,_ht_trendmode_tl,_ht_trendmode_pt,_ht_trendmode_ta,_ht_trendmode_tu,_ht_trendmode_fi)
_reg("CDLENGULFING",_cdlengulfing_ft,_cdlengulfing_tl,_cdlengulfing_pt,_cdlengulfing_ta,_cdlengulfing_tu,_cdlengulfing_fi)
_reg("CDLDOJI",_cdldoji_ft,_cdldoji_tl,_cdldoji_pt,_cdldoji_ta,_cdldoji_tu,_cdldoji_fi)
_reg("CDLHAMMER",_cdlhammer_ft,_cdlhammer_tl,_cdlhammer_pt,_cdlhammer_ta,_cdlhammer_tu,_cdlhammer_fi)
# ============================================================
# METADATA
# ============================================================
INDICATOR_DEFAULTS: dict[str, dict] = {
"SMA":{"timeperiod":20},"EMA":{"timeperiod":20},"WMA":{"timeperiod":14},
"DEMA":{"timeperiod":20},"TEMA":{"timeperiod":20},"T3":{"timeperiod":5},
"TRIMA":{"timeperiod":20},"KAMA":{"timeperiod":10},"HULL_MA":{"timeperiod":16},
"VWMA":{"timeperiod":20},"MIDPOINT":{"timeperiod":14},"MIDPRICE":{"timeperiod":14},
"RSI":{"timeperiod":14},
"MACD":{"fastperiod":12,"slowperiod":26,"signalperiod":9},
"STOCH":{"fastk_period":14,"slowk_period":3,"slowd_period":3},
"CCI":{"timeperiod":14},"WILLR":{"timeperiod":14},
"AROON":{"timeperiod":14},"AROONOSC":{"timeperiod":14},
"ADX":{"timeperiod":14},"MOM":{"timeperiod":10},"ROC":{"timeperiod":10},
"CMO":{"timeperiod":14},"PPO":{"fastperiod":12,"slowperiod":26},
"TRIX":{"timeperiod":18},"TSF":{"timeperiod":14},
"ULTOSC":{"timeperiod1":7,"timeperiod2":14,"timeperiod3":28},
"BOP":{},"PLUS_DI":{"timeperiod":14},"MINUS_DI":{"timeperiod":14},
"BBANDS":{"timeperiod":20,"nbdevup":2.0,"nbdevdn":2.0},
"ATR":{"timeperiod":14},"NATR":{"timeperiod":14},"TRANGE":{},
"STDDEV":{"timeperiod":20},"VAR":{"timeperiod":20},
"SAR":{"acceleration":0.02,"maximum":0.2},
"KELTNER_CHANNELS":{"timeperiod":20},"DONCHIAN":{"timeperiod":20},
"SUPERTREND":{"timeperiod":7},"CHOPPINESS_INDEX":{"timeperiod":14},
"OBV":{},"AD":{},"ADOSC":{"fastperiod":3,"slowperiod":10},
"MFI":{"timeperiod":14},"VWAP":{},
"AVGPRICE":{},"MEDPRICE":{},"TYPPRICE":{},"WCLPRICE":{},
"SQRT":{},"LOG10":{},"ADD":{},
"LINEARREG":{"timeperiod":14},"LINEARREG_SLOPE":{"timeperiod":14},
"CORREL":{"timeperiod":30},"BETA":{"timeperiod":5},
"HT_DCPERIOD":{},"HT_TRENDMODE":{},
"CDLENGULFING":{},"CDLDOJI":{},"CDLHAMMER":{},
}
INDICATOR_NAMES = list(INDICATOR_DEFAULTS.keys())
LIBRARY_NAMES = ["ferro_ta","talib","pandas_ta","ta","tulipy","finta"]
INDICATOR_CATEGORIES: dict[str, list[str]] = {
"Overlap": ["SMA","EMA","WMA","DEMA","TEMA","T3","TRIMA","KAMA","HULL_MA","VWMA","MIDPOINT","MIDPRICE"],
"Momentum": ["RSI","MACD","STOCH","CCI","WILLR","AROON","AROONOSC","ADX","MOM","ROC","CMO","PPO","TRIX","TSF","ULTOSC","BOP","PLUS_DI","MINUS_DI"],
"Volatility": ["BBANDS","ATR","NATR","TRANGE","STDDEV","VAR","SAR","KELTNER_CHANNELS","DONCHIAN","SUPERTREND","CHOPPINESS_INDEX"],
"Volume": ["OBV","AD","ADOSC","MFI","VWAP"],
"Price Transform": ["AVGPRICE","MEDPRICE","TYPPRICE","WCLPRICE"],
"Math": ["SQRT","LOG10","ADD"],
"Statistics": ["LINEARREG","LINEARREG_SLOPE","CORREL","BETA"],
"Cycle": ["HT_DCPERIOD","HT_TRENDMODE"],
"Pattern": ["CDLENGULFING","CDLDOJI","CDLHAMMER"],
}
# Cumulative: compare first-differences not absolute values
CUMULATIVE_INDICATORS = {"OBV","AD","ADOSC"}
# Binary output: use agreement rate not allclose
BINARY_INDICATORS = {"CDLENGULFING","CDLDOJI","CDLHAMMER","HT_TRENDMODE"}
def execute_indicator(library, indicator, data, df=None, **kwargs):
"""Run indicator from library on data dict, return 1-D float64 array."""
if library not in available_libraries():
raise KeyError(f"Library not available in this environment: {library!r}")
key = (library, indicator)
if key not in REGISTRY:
raise KeyError(f"No wrapper for {key!r}")
if df is None:
from benchmarks.data_generator import get_pandas_ohlcv
df = get_pandas_ohlcv(data)
params = {**INDICATOR_DEFAULTS.get(indicator, {}), **kwargs}
return REGISTRY[key](data, df, **params)