feat: init the repo
This commit is contained in:
@@ -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 20–350× slower on ATR, CCI, ADX, MFI (O(n²) Python loops).
|
||||
- **ferro-ta** is typically 2–4× 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)
|
||||
@@ -0,0 +1 @@
|
||||
"""benchmarks package — cross-library accuracy and speed comparison suite."""
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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.
@@ -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})")
|
||||
+16097
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user