chore: release v1.0.2

This commit is contained in:
Pratik Bhadane
2026-03-24 02:02:10 +05:30
parent 9011250f99
commit 2d5000262f
47 changed files with 3821 additions and 422 deletions
+48
View File
@@ -40,6 +40,30 @@ from benchmarks.data_generator import SMALL, MEDIUM, LARGE
- **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.
## Reproducible Perf Artifacts
Use the perf-contract runner when you want a compact set of machine-readable
artifacts for single-series latency, batch throughput, streaming throughput,
and hotspot attribution in one directory:
```bash
uv run python benchmarks/run_perf_contract.py --output-dir benchmarks/artifacts/latest --skip-talib
```
That command writes:
- `indicator_latency.json` — canonical-fixture timings for the benchmark suite indicators
- `batch.json` — 2-D batch throughput plus grouped multi-indicator timings
- `streaming.json` — streaming update throughput vs batch baselines
- `runtime_hotspots.json` — ranked hotspot report with reference speedups
- `manifest.json` — runtime/git metadata plus hashes for the generated artifacts
For CI or local guardrails, validate the hotspot report with:
```bash
uv run python benchmarks/check_hotspot_regression.py --input benchmarks/artifacts/latest/runtime_hotspots.json
```
---
## Speed comparison (100k bars, median µs — lower is better)
@@ -141,10 +165,34 @@ uv run python benchmarks/bench_vs_talib.py --sizes 10000 100000 --json benchmark
# Optional regression check used in CI
uv run python benchmarks/check_vs_talib_regression.py --input benchmark_vs_talib.json
# Batch throughput + grouped multi-indicator calls
uv run python benchmarks/bench_batch.py --samples 100000 --series 100 --json batch_benchmark.json
# Streaming update throughput vs batch baselines
uv run python benchmarks/bench_streaming.py --bars 100000 --json streaming_benchmark.json
# Ranked hotspot attribution against bundled reference implementations
uv run python benchmarks/profile_runtime_hotspots.py --json runtime_hotspots.json
# Portable vs SIMD-enabled build comparison
uv run python benchmarks/bench_simd.py --json simd_benchmark.json
# One-shot perf artifact bundle
uv run python benchmarks/run_perf_contract.py --output-dir benchmarks/artifacts/latest
```
Without `uv`: use `pytest` and `python` from the same environment where `ferro_ta` and optional libs (e.g. `talib`, `pandas_ta`, `ta`, `tulipy`, `finta`) are installed.
### WASM
From the `wasm/` directory:
```bash
wasm-pack build --target nodejs --out-dir pkg
node bench.js --json ../wasm_benchmark.json
```
---
## Indicator coverage
+71
View File
@@ -0,0 +1,71 @@
{
"metadata": {
"suite": "batch",
"runtime": {
"generated_at_utc": "2026-03-23T20:25:58.345834+00:00",
"python_version": "3.13.5",
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "9011250f992119170242cf17a67834c67b91bcdb",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"n_samples": 100000,
"n_series": 100,
"total_bars": 10000000,
"seed": 42
}
},
"results": [
{
"indicator": "SMA",
"parallel_ms": 37.86,
"sequential_ms": 43.5625,
"loop_ms": 17.8136,
"parallel_speedup_vs_loop": 0.4705,
"sequential_speedup_vs_loop": 0.4089
},
{
"indicator": "RSI",
"parallel_ms": 40.9229,
"sequential_ms": 79.5345,
"loop_ms": 53.3368,
"parallel_speedup_vs_loop": 1.3033,
"sequential_speedup_vs_loop": 0.6706
},
{
"indicator": "ATR",
"parallel_ms": 91.76,
"sequential_ms": 130.1404,
"loop_ms": 99.5885,
"parallel_speedup_vs_loop": 1.0853,
"sequential_speedup_vs_loop": 0.7652
},
{
"indicator": "ADX",
"parallel_ms": 100.1362,
"sequential_ms": 149.3412,
"loop_ms": 125.3319,
"parallel_speedup_vs_loop": 1.2516,
"sequential_speedup_vs_loop": 0.8392
}
],
"grouped_results": [
{
"case": "close_bundle_3",
"grouped_ms": 0.652,
"separate_ms": 0.9124,
"speedup_vs_separate": 1.3994
},
{
"case": "hlc_bundle_3",
"grouped_ms": 1.4784,
"separate_ms": 3.3724,
"speedup_vs_separate": 2.2811
}
]
}
@@ -0,0 +1,262 @@
{
"schema_version": 1,
"command": "python benchmarks/bench_vs_talib.py",
"n_warmup": 1,
"n_runs": 7,
"sizes": [
10000,
100000
],
"talib_available": true,
"runtime": {
"generated_at_utc": "2026-03-23T20:26:40.738132+00:00",
"python_version": "3.13.5",
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
"machine": "arm64"
},
"git": {
"commit": "9011250f992119170242cf17a67834c67b91bcdb",
"dirty": true
},
"summary": {
"total_rows": 24,
"by_size": [
{
"size": 10000,
"rows": 12,
"wins": 8,
"win_rate": 0.6666666666666666,
"median_speedup": 1.0546,
"min_speedup": 0.7174,
"max_speedup": 2.0135
},
{
"size": 100000,
"rows": 12,
"wins": 7,
"win_rate": 0.5833333333333334,
"median_speedup": 1.0896,
"min_speedup": 0.4965,
"max_speedup": 3.6029
}
]
},
"results": [
{
"indicator": "SMA",
"size": 10000,
"ferro_ta_ms": 0.0093,
"talib_ms": 0.0167,
"speedup": 1.7937,
"ferro_ta_m_bars_s": 1076.19,
"talib_m_bars_s": 599.99
},
{
"indicator": "SMA",
"size": 100000,
"ferro_ta_ms": 0.0765,
"talib_ms": 0.1346,
"speedup": 1.7589,
"ferro_ta_m_bars_s": 1306.49,
"talib_m_bars_s": 742.8
},
{
"indicator": "EMA",
"size": 10000,
"ferro_ta_ms": 0.02,
"talib_ms": 0.0214,
"speedup": 1.0687,
"ferro_ta_m_bars_s": 500.0,
"talib_m_bars_s": 467.84
},
{
"indicator": "EMA",
"size": 100000,
"ferro_ta_ms": 0.1998,
"talib_ms": 0.1883,
"speedup": 0.9425,
"ferro_ta_m_bars_s": 500.42,
"talib_m_bars_s": 530.97
},
{
"indicator": "RSI",
"size": 10000,
"ferro_ta_ms": 0.0475,
"talib_ms": 0.048,
"speedup": 1.0088,
"ferro_ta_m_bars_s": 210.34,
"talib_m_bars_s": 208.52
},
{
"indicator": "RSI",
"size": 100000,
"ferro_ta_ms": 0.4804,
"talib_ms": 0.4635,
"speedup": 0.9647,
"ferro_ta_m_bars_s": 208.15,
"talib_m_bars_s": 215.77
},
{
"indicator": "BBANDS",
"size": 10000,
"ferro_ta_ms": 0.0215,
"talib_ms": 0.0433,
"speedup": 2.0135,
"ferro_ta_m_bars_s": 465.12,
"talib_m_bars_s": 230.99
},
{
"indicator": "BBANDS",
"size": 100000,
"ferro_ta_ms": 0.1686,
"talib_ms": 0.4275,
"speedup": 2.535,
"ferro_ta_m_bars_s": 593.03,
"talib_m_bars_s": 233.94
},
{
"indicator": "MACD",
"size": 10000,
"ferro_ta_ms": 0.0495,
"talib_ms": 0.065,
"speedup": 1.3134,
"ferro_ta_m_bars_s": 202.19,
"talib_m_bars_s": 153.95
},
{
"indicator": "MACD",
"size": 100000,
"ferro_ta_ms": 0.4459,
"talib_ms": 0.6334,
"speedup": 1.4205,
"ferro_ta_m_bars_s": 224.28,
"talib_m_bars_s": 157.88
},
{
"indicator": "ATR",
"size": 10000,
"ferro_ta_ms": 0.0483,
"talib_ms": 0.0502,
"speedup": 1.0405,
"ferro_ta_m_bars_s": 207.07,
"talib_m_bars_s": 199.0
},
{
"indicator": "ATR",
"size": 100000,
"ferro_ta_ms": 0.4705,
"talib_ms": 0.4788,
"speedup": 1.0174,
"ferro_ta_m_bars_s": 212.52,
"talib_m_bars_s": 208.88
},
{
"indicator": "STOCH",
"size": 10000,
"ferro_ta_ms": 0.0915,
"talib_ms": 0.066,
"speedup": 0.721,
"ferro_ta_m_bars_s": 109.24,
"talib_m_bars_s": 151.52
},
{
"indicator": "STOCH",
"size": 100000,
"ferro_ta_ms": 1.5285,
"talib_ms": 0.7589,
"speedup": 0.4965,
"ferro_ta_m_bars_s": 65.42,
"talib_m_bars_s": 131.77
},
{
"indicator": "ADX",
"size": 10000,
"ferro_ta_ms": 0.0692,
"talib_ms": 0.0522,
"speedup": 0.7538,
"ferro_ta_m_bars_s": 144.49,
"talib_m_bars_s": 191.7
},
{
"indicator": "ADX",
"size": 100000,
"ferro_ta_ms": 0.6209,
"talib_ms": 0.5731,
"speedup": 0.923,
"ferro_ta_m_bars_s": 161.05,
"talib_m_bars_s": 174.49
},
{
"indicator": "CCI",
"size": 10000,
"ferro_ta_ms": 0.0731,
"talib_ms": 0.0829,
"speedup": 1.1333,
"ferro_ta_m_bars_s": 136.75,
"talib_m_bars_s": 120.66
},
{
"indicator": "CCI",
"size": 100000,
"ferro_ta_ms": 0.7028,
"talib_ms": 0.8164,
"speedup": 1.1617,
"ferro_ta_m_bars_s": 142.3,
"talib_m_bars_s": 122.49
},
{
"indicator": "OBV",
"size": 10000,
"ferro_ta_ms": 0.0156,
"talib_ms": 0.0112,
"speedup": 0.7174,
"ferro_ta_m_bars_s": 640.0,
"talib_m_bars_s": 892.14
},
{
"indicator": "OBV",
"size": 100000,
"ferro_ta_ms": 0.2953,
"talib_ms": 0.2808,
"speedup": 0.9509,
"ferro_ta_m_bars_s": 338.6,
"talib_m_bars_s": 356.08
},
{
"indicator": "MFI",
"size": 10000,
"ferro_ta_ms": 0.0239,
"talib_ms": 0.0203,
"speedup": 0.8516,
"ferro_ta_m_bars_s": 418.85,
"talib_m_bars_s": 491.81
},
{
"indicator": "MFI",
"size": 100000,
"ferro_ta_ms": 0.1721,
"talib_ms": 0.62,
"speedup": 3.6029,
"ferro_ta_m_bars_s": 581.11,
"talib_m_bars_s": 161.29
},
{
"indicator": "WMA",
"size": 10000,
"ferro_ta_ms": 0.0105,
"talib_ms": 0.0203,
"speedup": 1.9363,
"ferro_ta_m_bars_s": 956.21,
"talib_m_bars_s": 493.83
},
{
"indicator": "WMA",
"size": 100000,
"ferro_ta_ms": 0.0869,
"talib_ms": 0.1868,
"speedup": 2.1506,
"ferro_ta_m_bars_s": 1151.08,
"talib_m_bars_s": 535.24
}
]
}
@@ -0,0 +1,163 @@
{
"metadata": {
"suite": "indicator_latency",
"runtime": {
"generated_at_utc": "2026-03-23T20:25:52.160357+00:00",
"python_version": "3.13.5",
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "9011250f992119170242cf17a67834c67b91bcdb",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"fixtures": [
{
"path": "/Users/pratikbhadane/Work/Projects/ferro-ta/benchmarks/fixtures/canonical_ohlcv.npz",
"size_bytes": 75586,
"sha256": "60192f8349fb06cd59ef7f70fd77aa8280399e819d7cc5eed3ca95cf5ee1a89c"
}
],
"dataset": {
"fixture": "/Users/pratikbhadane/Work/Projects/ferro-ta/benchmarks/fixtures/canonical_ohlcv.npz",
"bars": 2000,
"rounds": 5
}
},
"results": [
{
"name": "VAR_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.0229
},
{
"name": "STOCH",
"inputs": "hlc",
"kwargs": {},
"elapsed_ms": 0.0201
},
{
"name": "WILLR_14",
"inputs": "hlc",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0201
},
{
"name": "CCI_14",
"inputs": "hlc",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0163
},
{
"name": "ADX_14",
"inputs": "hlc",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.015
},
{
"name": "MACD",
"inputs": "close",
"kwargs": {},
"elapsed_ms": 0.0135
},
{
"name": "ATR_14",
"inputs": "hlc",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0105
},
{
"name": "RSI_14",
"inputs": "close",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0098
},
{
"name": "STDDEV_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.009
},
{
"name": "BETA_5",
"inputs": "pair_hl",
"kwargs": {
"timeperiod": 5
},
"elapsed_ms": 0.0083
},
{
"name": "CORREL_30",
"inputs": "pair_hl",
"kwargs": {
"timeperiod": 30
},
"elapsed_ms": 0.0076
},
{
"name": "BBANDS_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.0055
},
{
"name": "LINEARREG_14",
"inputs": "close",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0055
},
{
"name": "TSF_14",
"inputs": "close",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0054
},
{
"name": "LINEARREG_SLOPE_14",
"inputs": "close",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0052
},
{
"name": "EMA_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.005
},
{
"name": "SMA_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.0026
}
]
}
+62
View File
@@ -0,0 +1,62 @@
{
"metadata": {
"suite": "perf_contract",
"runtime": {
"generated_at_utc": "2026-03-23T20:26:40.776130+00:00",
"python_version": "3.13.5",
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "9011250f992119170242cf17a67834c67b91bcdb",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"fixtures": [
{
"path": "/Users/pratikbhadane/Work/Projects/ferro-ta/benchmarks/fixtures/canonical_ohlcv.npz",
"size_bytes": 75586,
"sha256": "60192f8349fb06cd59ef7f70fd77aa8280399e819d7cc5eed3ca95cf5ee1a89c"
}
],
"output_dir": "benchmarks/artifacts/latest"
},
"artifacts": {
"indicator_latency": {
"path": "benchmarks/artifacts/latest/indicator_latency.json",
"size_bytes": 3217,
"sha256": "43b88a50a4d7f91e30ff8e57dbf859ae5e76ecabaaf05cfcb7d8db67df920f7f"
},
"batch": {
"path": "benchmarks/artifacts/latest/batch.json",
"size_bytes": 1701,
"sha256": "bc900c885c48ec1903ea4870ca1de8cb9f33c609d2cefa4688cbdd18cb977f11"
},
"streaming": {
"path": "benchmarks/artifacts/latest/streaming.json",
"size_bytes": 1944,
"sha256": "925ba1be66d0d499daa81dfc148b03ac325ad685ce0c71e28ca1fc6927f16415"
},
"runtime_hotspots": {
"path": "benchmarks/artifacts/latest/runtime_hotspots.json",
"size_bytes": 2366,
"sha256": "920553b14b545f211b119c099ec59885de8b9e8056271cb2d2ac34c0c69b0906"
},
"simd": {
"path": "benchmarks/artifacts/latest/simd.json",
"size_bytes": 7700,
"sha256": "d48943a5dfcf4f8d8d2ca42f0004f02f9fc894de7477791b686231da665e3335"
},
"benchmark_vs_talib": {
"path": "benchmarks/artifacts/latest/benchmark_vs_talib.json",
"size_bytes": 5923,
"sha256": "8a4e847517f1334255353982a5266c0323bf433a1eb78dafeff808d5ad3bf7f0"
},
"wasm": {
"path": "benchmarks/artifacts/latest/wasm.json",
"size_bytes": 935,
"sha256": "f31fd871990c44e24a2259d618ae40a52866d20b95aa6047af3d38b9371c2ab7"
}
}
}
@@ -0,0 +1,96 @@
{
"metadata": {
"suite": "runtime_hotspots",
"runtime": {
"generated_at_utc": "2026-03-23T20:26:02.236710+00:00",
"python_version": "3.13.5",
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "9011250f992119170242cf17a67834c67b91bcdb",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"price_bars": 20000,
"iv_bars": 50000,
"window": 252
}
},
"results": [
{
"category": "python_analysis",
"name": "iv_zscore",
"fast_ms": 35.984,
"reference_ms": 944.5804,
"speedup_vs_reference": 26.25,
"share_of_suite_pct": 77.27
},
{
"category": "python_analysis",
"name": "iv_percentile",
"fast_ms": 7.6624,
"reference_ms": 81.581,
"speedup_vs_reference": 10.6469,
"share_of_suite_pct": 16.45
},
{
"category": "python_analysis",
"name": "iv_rank",
"fast_ms": 2.2905,
"reference_ms": 198.2937,
"speedup_vs_reference": 86.5738,
"share_of_suite_pct": 4.92
},
{
"category": "ffi_grouping",
"name": "feature_matrix",
"fast_ms": 0.2872,
"reference_ms": 0.2377,
"speedup_vs_reference": 0.8275,
"share_of_suite_pct": 0.62
},
{
"category": "ffi_grouping",
"name": "compute_many_close",
"fast_ms": 0.1448,
"reference_ms": 0.1505,
"speedup_vs_reference": 1.0391,
"share_of_suite_pct": 0.31
},
{
"category": "rust_kernel",
"name": "BETA",
"fast_ms": 0.0637,
"reference_ms": 164.1752,
"speedup_vs_reference": 2575.2975,
"share_of_suite_pct": 0.14
},
{
"category": "rust_kernel",
"name": "CORREL",
"fast_ms": 0.0553,
"reference_ms": 159.6473,
"speedup_vs_reference": 2885.1573,
"share_of_suite_pct": 0.12
},
{
"category": "rust_kernel",
"name": "LINEARREG",
"fast_ms": 0.0415,
"reference_ms": 47.4665,
"speedup_vs_reference": 1143.77,
"share_of_suite_pct": 0.09
},
{
"category": "rust_kernel",
"name": "TSF",
"fast_ms": 0.0414,
"reference_ms": 47.921,
"speedup_vs_reference": 1157.036,
"share_of_suite_pct": 0.09
}
]
}
+285
View File
@@ -0,0 +1,285 @@
{
"metadata": {
"suite": "simd",
"runtime": {
"generated_at_utc": "2026-03-23T20:26:40.566511+00:00",
"python_version": "3.13.5",
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "9011250f992119170242cf17a67834c67b91bcdb",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"price_bars": 20000,
"iv_bars": 50000,
"window": 252
},
"variants": [
"portable_release",
"simd_release"
]
},
"results": [
{
"name": "BETA",
"category": "rust_kernel",
"portable_ms": 0.0635,
"simd_ms": 0.0636,
"speedup_simd_vs_portable": 0.9984
},
{
"name": "TSF",
"category": "rust_kernel",
"portable_ms": 0.0415,
"simd_ms": 0.0417,
"speedup_simd_vs_portable": 0.9952
},
{
"name": "compute_many_close",
"category": "ffi_grouping",
"portable_ms": 0.1548,
"simd_ms": 0.1572,
"speedup_simd_vs_portable": 0.9847
},
{
"name": "iv_zscore",
"category": "python_analysis",
"portable_ms": 36.0643,
"simd_ms": 37.2041,
"speedup_simd_vs_portable": 0.9694
},
{
"name": "feature_matrix",
"category": "ffi_grouping",
"portable_ms": 0.2556,
"simd_ms": 0.2667,
"speedup_simd_vs_portable": 0.9584
},
{
"name": "iv_percentile",
"category": "python_analysis",
"portable_ms": 7.7548,
"simd_ms": 8.1565,
"speedup_simd_vs_portable": 0.9508
},
{
"name": "LINEARREG",
"category": "rust_kernel",
"portable_ms": 0.0416,
"simd_ms": 0.0443,
"speedup_simd_vs_portable": 0.9391
},
{
"name": "iv_rank",
"category": "python_analysis",
"portable_ms": 2.2813,
"simd_ms": 2.4386,
"speedup_simd_vs_portable": 0.9355
},
{
"name": "CORREL",
"category": "rust_kernel",
"portable_ms": 0.0552,
"simd_ms": 0.0633,
"speedup_simd_vs_portable": 0.872
}
],
"reports": {
"portable_release": {
"metadata": {
"suite": "runtime_hotspots",
"runtime": {
"generated_at_utc": "2026-03-23T20:26:06.920513+00:00",
"python_version": "3.13.5",
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "9011250f992119170242cf17a67834c67b91bcdb",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"price_bars": 20000,
"iv_bars": 50000,
"window": 252
}
},
"results": [
{
"category": "python_analysis",
"name": "iv_zscore",
"fast_ms": 36.0643,
"reference_ms": 908.5403,
"speedup_vs_reference": 25.1922,
"share_of_suite_pct": 77.2
},
{
"category": "python_analysis",
"name": "iv_percentile",
"fast_ms": 7.7548,
"reference_ms": 82.2352,
"speedup_vs_reference": 10.6045,
"share_of_suite_pct": 16.6
},
{
"category": "python_analysis",
"name": "iv_rank",
"fast_ms": 2.2813,
"reference_ms": 202.5375,
"speedup_vs_reference": 88.7803,
"share_of_suite_pct": 4.88
},
{
"category": "ffi_grouping",
"name": "feature_matrix",
"fast_ms": 0.2556,
"reference_ms": 0.2252,
"speedup_vs_reference": 0.8812,
"share_of_suite_pct": 0.55
},
{
"category": "ffi_grouping",
"name": "compute_many_close",
"fast_ms": 0.1548,
"reference_ms": 0.1508,
"speedup_vs_reference": 0.9742,
"share_of_suite_pct": 0.33
},
{
"category": "rust_kernel",
"name": "BETA",
"fast_ms": 0.0635,
"reference_ms": 162.8972,
"speedup_vs_reference": 2563.6148,
"share_of_suite_pct": 0.14
},
{
"category": "rust_kernel",
"name": "CORREL",
"fast_ms": 0.0552,
"reference_ms": 163.1357,
"speedup_vs_reference": 2952.6826,
"share_of_suite_pct": 0.12
},
{
"category": "rust_kernel",
"name": "LINEARREG",
"fast_ms": 0.0416,
"reference_ms": 48.0097,
"speedup_vs_reference": 1153.3863,
"share_of_suite_pct": 0.09
},
{
"category": "rust_kernel",
"name": "TSF",
"fast_ms": 0.0415,
"reference_ms": 47.9395,
"speedup_vs_reference": 1155.1696,
"share_of_suite_pct": 0.09
}
]
},
"simd_release": {
"metadata": {
"suite": "runtime_hotspots",
"runtime": {
"generated_at_utc": "2026-03-23T20:26:25.789478+00:00",
"python_version": "3.13.5",
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "9011250f992119170242cf17a67834c67b91bcdb",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"price_bars": 20000,
"iv_bars": 50000,
"window": 252
}
},
"results": [
{
"category": "python_analysis",
"name": "iv_zscore",
"fast_ms": 37.2041,
"reference_ms": 930.7842,
"speedup_vs_reference": 25.0183,
"share_of_suite_pct": 76.81
},
{
"category": "python_analysis",
"name": "iv_percentile",
"fast_ms": 8.1565,
"reference_ms": 88.3639,
"speedup_vs_reference": 10.8336,
"share_of_suite_pct": 16.84
},
{
"category": "python_analysis",
"name": "iv_rank",
"fast_ms": 2.4386,
"reference_ms": 221.0389,
"speedup_vs_reference": 90.6424,
"share_of_suite_pct": 5.03
},
{
"category": "ffi_grouping",
"name": "feature_matrix",
"fast_ms": 0.2667,
"reference_ms": 0.2436,
"speedup_vs_reference": 0.9134,
"share_of_suite_pct": 0.55
},
{
"category": "ffi_grouping",
"name": "compute_many_close",
"fast_ms": 0.1572,
"reference_ms": 0.1593,
"speedup_vs_reference": 1.0135,
"share_of_suite_pct": 0.32
},
{
"category": "rust_kernel",
"name": "BETA",
"fast_ms": 0.0636,
"reference_ms": 172.9198,
"speedup_vs_reference": 2717.7961,
"share_of_suite_pct": 0.13
},
{
"category": "rust_kernel",
"name": "CORREL",
"fast_ms": 0.0633,
"reference_ms": 170.0262,
"speedup_vs_reference": 2686.3776,
"share_of_suite_pct": 0.13
},
{
"category": "rust_kernel",
"name": "LINEARREG",
"fast_ms": 0.0443,
"reference_ms": 50.5614,
"speedup_vs_reference": 1141.5474,
"share_of_suite_pct": 0.09
},
{
"category": "rust_kernel",
"name": "TSF",
"fast_ms": 0.0417,
"reference_ms": 50.9599,
"speedup_vs_reference": 1221.8259,
"share_of_suite_pct": 0.09
}
]
}
}
}
@@ -0,0 +1,73 @@
{
"metadata": {
"suite": "streaming",
"runtime": {
"generated_at_utc": "2026-03-23T20:25:58.628657+00:00",
"python_version": "3.13.5",
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "9011250f992119170242cf17a67834c67b91bcdb",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"n_bars": 100000,
"seed": 2026
}
},
"results": [
{
"indicator": "StreamingSMA",
"inputs": "close",
"stream_total_ms": 4.5729,
"batch_total_ms": 0.0685,
"stream_ns_per_update": 45.73,
"batch_ns_per_bar": 0.68,
"updates_per_second": 21867879.95,
"stream_over_batch_ratio": 66.7989
},
{
"indicator": "StreamingEMA",
"inputs": "close",
"stream_total_ms": 4.535,
"batch_total_ms": 0.1969,
"stream_ns_per_update": 45.35,
"batch_ns_per_bar": 1.97,
"updates_per_second": 22050716.65,
"stream_over_batch_ratio": 23.0301
},
{
"indicator": "StreamingRSI",
"inputs": "close",
"stream_total_ms": 4.6421,
"batch_total_ms": 0.4597,
"stream_ns_per_update": 46.42,
"batch_ns_per_bar": 4.6,
"updates_per_second": 21541858.52,
"stream_over_batch_ratio": 10.098
},
{
"indicator": "StreamingATR",
"inputs": "hlc",
"stream_total_ms": 10.2098,
"batch_total_ms": 0.4599,
"stream_ns_per_update": 102.1,
"batch_ns_per_bar": 4.6,
"updates_per_second": 9794518.83,
"stream_over_batch_ratio": 22.2012
},
{
"indicator": "StreamingVWAP",
"inputs": "hlcv",
"stream_total_ms": 12.5109,
"batch_total_ms": 0.1027,
"stream_ns_per_update": 125.11,
"batch_ns_per_bar": 1.03,
"updates_per_second": 7993046.05,
"stream_over_batch_ratio": 121.7603
}
]
}
+46
View File
@@ -0,0 +1,46 @@
{
"metadata": {
"suite": "wasm",
"runtime": {
"generated_at_utc": "2026-03-23T20:15:04.885Z",
"node_version": "v25.8.1",
"platform": "darwin",
"arch": "arm64"
},
"dataset": {
"bars": 100000
}
},
"results": [
{
"indicator": "SMA",
"elapsed_ms": 0.1702,
"ns_per_bar": 1.7,
"million_bars_per_second": 587.52
},
{
"indicator": "EMA",
"elapsed_ms": 0.2923,
"ns_per_bar": 2.92,
"million_bars_per_second": 342.08
},
{
"indicator": "RSI",
"elapsed_ms": 0.5962,
"ns_per_bar": 5.96,
"million_bars_per_second": 167.73
},
{
"indicator": "ATR",
"elapsed_ms": 0.642,
"ns_per_bar": 6.42,
"million_bars_per_second": 155.75
},
{
"indicator": "BBANDS",
"elapsed_ms": 1.7411,
"ns_per_bar": 17.41,
"million_bars_per_second": 57.43
}
]
}
+206 -50
View File
@@ -1,65 +1,221 @@
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
from typing import Any
import numpy as np
import ferro_ta
def _time_fn(fn, *args, **kwargs):
times = []
# Warmup
try:
from benchmarks.metadata import benchmark_metadata
except ModuleNotFoundError: # pragma: no cover - script execution fallback
from metadata import benchmark_metadata
def _time_fn(fn, *args, rounds: int = 5, **kwargs) -> float:
fn(*args, **kwargs)
for _ in range(5):
times: list[float] = []
for _ in range(rounds):
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")
def run_batch_benchmark(
*,
n_samples: int = 100_000,
n_series: int = 100,
seed: int = 42,
) -> dict[str, Any]:
rng = np.random.default_rng(seed)
close2d = rng.uniform(100.0, 200.0, (n_samples, n_series))
high2d = close2d + rng.uniform(0.1, 2.0, (n_samples, n_series))
low2d = close2d - rng.uniform(0.1, 2.0, (n_samples, n_series))
close1d = close2d[:, 0]
high1d = high2d[:, 0]
low1d = low2d[:, 0]
# 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")
batch_rows: list[dict[str, Any]] = []
grouped_rows: list[dict[str, Any]] = []
# 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")
indicators = [
(
"SMA",
lambda: ferro_ta.batch.batch_sma(close2d, timeperiod=14, parallel=True),
lambda: ferro_ta.batch.batch_sma(close2d, timeperiod=14, parallel=False),
lambda: [ferro_ta.SMA(close2d[:, j], timeperiod=14) for j in range(n_series)],
),
(
"RSI",
lambda: ferro_ta.batch.batch_rsi(close2d, timeperiod=14, parallel=True),
lambda: ferro_ta.batch.batch_rsi(close2d, timeperiod=14, parallel=False),
lambda: [ferro_ta.RSI(close2d[:, j], timeperiod=14) for j in range(n_series)],
),
(
"ATR",
lambda: ferro_ta.batch.batch_atr(
high2d, low2d, close2d, timeperiod=14, parallel=True
),
lambda: ferro_ta.batch.batch_atr(
high2d, low2d, close2d, timeperiod=14, parallel=False
),
lambda: [
ferro_ta.ATR(high2d[:, j], low2d[:, j], close2d[:, j], timeperiod=14)
for j in range(n_series)
],
),
(
"ADX",
lambda: ferro_ta.batch.batch_adx(
high2d, low2d, close2d, timeperiod=14, parallel=True
),
lambda: ferro_ta.batch.batch_adx(
high2d, low2d, close2d, timeperiod=14, parallel=False
),
lambda: [
ferro_ta.ADX(high2d[:, j], low2d[:, j], close2d[:, j], timeperiod=14)
for j in range(n_series)
],
),
]
# 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")
for name, parallel_fn, sequential_fn, loop_fn in indicators:
batch_parallel_s = _time_fn(parallel_fn)
batch_sequential_s = _time_fn(sequential_fn)
loop_s = _time_fn(loop_fn)
batch_rows.append(
{
"indicator": name,
"parallel_ms": round(batch_parallel_s * 1000, 4),
"sequential_ms": round(batch_sequential_s * 1000, 4),
"loop_ms": round(loop_s * 1000, 4),
"parallel_speedup_vs_loop": round(loop_s / batch_parallel_s, 4),
"sequential_speedup_vs_loop": round(loop_s / batch_sequential_s, 4),
}
)
if __name__ == '__main__':
main()
grouped_cases = [
(
"close_bundle_3",
lambda: ferro_ta.batch.compute_many(
[
("SMA", {"timeperiod": 10}),
("EMA", {"timeperiod": 12}),
("RSI", {"timeperiod": 14}),
],
close=close1d,
),
lambda: (
ferro_ta.SMA(close1d, timeperiod=10),
ferro_ta.EMA(close1d, timeperiod=12),
ferro_ta.RSI(close1d, timeperiod=14),
),
),
(
"hlc_bundle_3",
lambda: ferro_ta.batch.compute_many(
[
("ATR", {"timeperiod": 14}),
("ADX", {"timeperiod": 14}),
("CCI", {"timeperiod": 14}),
],
close=close1d,
high=high1d,
low=low1d,
),
lambda: (
ferro_ta.ATR(high1d, low1d, close1d, timeperiod=14),
ferro_ta.ADX(high1d, low1d, close1d, timeperiod=14),
ferro_ta.CCI(high1d, low1d, close1d, timeperiod=14),
),
),
]
for name, grouped_fn, separate_fn in grouped_cases:
grouped_s = _time_fn(grouped_fn)
separate_s = _time_fn(separate_fn)
grouped_rows.append(
{
"case": name,
"grouped_ms": round(grouped_s * 1000, 4),
"separate_ms": round(separate_s * 1000, 4),
"speedup_vs_separate": round(separate_s / grouped_s, 4),
}
)
return {
"metadata": benchmark_metadata(
"batch",
extra={
"dataset": {
"n_samples": n_samples,
"n_series": n_series,
"total_bars": n_samples * n_series,
"seed": seed,
}
},
),
"results": batch_rows,
"grouped_results": grouped_rows,
}
def main() -> int:
parser = argparse.ArgumentParser(description="Benchmark batch indicator execution.")
parser.add_argument("--samples", type=int, default=100_000)
parser.add_argument("--series", type=int, default=100)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--json", dest="json_path")
args = parser.parse_args()
payload = run_batch_benchmark(
n_samples=args.samples,
n_series=args.series,
seed=args.seed,
)
dataset = payload["metadata"]["dataset"]
print(
"Batch Benchmark: "
f"{dataset['n_samples']} bars, {dataset['n_series']} series "
f"(Total: {dataset['total_bars'] / 1e6:.1f} M bars)"
)
print("-" * 74)
print(
f"{'Indicator':<12} {'Parallel (ms)':>14} {'Sequential (ms)':>16} "
f"{'Loop (ms)':>12} {'P speedup':>10}"
)
print("-" * 74)
for row in payload["results"]:
print(
f"{row['indicator']:<12} {row['parallel_ms']:14.1f} "
f"{row['sequential_ms']:16.1f} {row['loop_ms']:12.1f} "
f"{row['parallel_speedup_vs_loop']:10.2f}x"
)
if payload["grouped_results"]:
print("\nGrouped Multi-Indicator Calls")
print("-" * 64)
print(f"{'Case':<18} {'Grouped (ms)':>14} {'Separate (ms)':>16} {'Speedup':>12}")
print("-" * 64)
for row in payload["grouped_results"]:
print(
f"{row['case']:<18} {row['grouped_ms']:14.1f} "
f"{row['separate_ms']:16.1f} {row['speedup_vs_separate']:12.2f}x"
)
if args.json_path:
json_path = Path(args.json_path)
json_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
print(f"\nWrote JSON results to {json_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+158
View File
@@ -0,0 +1,158 @@
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
try:
from benchmarks.metadata import benchmark_metadata
except ModuleNotFoundError: # pragma: no cover - script execution fallback
from metadata import benchmark_metadata
ROOT = Path(__file__).resolve().parents[1]
def _run(cmd: list[str], *, cwd: Path = ROOT) -> None:
subprocess.run(cmd, cwd=cwd, check=True)
def _profile_variant(
*,
label: str,
maturin_args: list[str],
price_bars: int,
iv_bars: int,
window: int,
) -> dict[str, Any]:
_run([sys.executable, "-m", "maturin", "develop", "--release", *maturin_args])
with tempfile.TemporaryDirectory(prefix=f"ferro_ta_{label}_") as tmp_dir:
json_path = Path(tmp_dir) / "runtime_hotspots.json"
_run(
[
sys.executable,
"benchmarks/profile_runtime_hotspots.py",
"--price-bars",
str(price_bars),
"--iv-bars",
str(iv_bars),
"--window",
str(window),
"--json",
str(json_path),
]
)
payload = json.loads(json_path.read_text(encoding="utf-8"))
return payload
def run_simd_benchmark(
*,
price_bars: int = 20_000,
iv_bars: int = 50_000,
window: int = 252,
) -> dict[str, Any]:
variants = [
("portable_release", []),
("simd_release", ["--features", "simd"]),
]
reports = {
label: _profile_variant(
label=label,
maturin_args=args,
price_bars=price_bars,
iv_bars=iv_bars,
window=window,
)
for label, args in variants
}
portable_rows = {
row["name"]: row for row in reports["portable_release"]["results"]
}
simd_rows = {row["name"]: row for row in reports["simd_release"]["results"]}
comparison: list[dict[str, Any]] = []
for name in sorted(portable_rows):
portable = portable_rows[name]
simd = simd_rows.get(name)
if simd is None:
continue
portable_ms = float(portable["fast_ms"])
simd_ms = float(simd["fast_ms"])
comparison.append(
{
"name": name,
"category": portable["category"],
"portable_ms": round(portable_ms, 4),
"simd_ms": round(simd_ms, 4),
"speedup_simd_vs_portable": round(
portable_ms / simd_ms if simd_ms > 0.0 else float("inf"), 4
),
}
)
comparison.sort(
key=lambda row: float(row["speedup_simd_vs_portable"]), reverse=True
)
# Restore the default portable editable build so the workspace ends in the
# distributable configuration.
_run([sys.executable, "-m", "maturin", "develop", "--release"])
return {
"metadata": benchmark_metadata(
"simd",
extra={
"dataset": {
"price_bars": price_bars,
"iv_bars": iv_bars,
"window": window,
},
"variants": [label for label, _ in variants],
},
),
"results": comparison,
"reports": reports,
}
def main() -> int:
parser = argparse.ArgumentParser(
description="Benchmark portable vs SIMD-enabled ferro-ta builds."
)
parser.add_argument("--price-bars", type=int, default=20_000)
parser.add_argument("--iv-bars", type=int, default=50_000)
parser.add_argument("--window", type=int, default=252)
parser.add_argument("--json", dest="json_path")
args = parser.parse_args()
payload = run_simd_benchmark(
price_bars=args.price_bars,
iv_bars=args.iv_bars,
window=args.window,
)
print(
f"{'Case':<20} {'Portable (ms)':>14} {'SIMD (ms)':>12} {'SIMD speedup':>14}"
)
print("-" * 64)
for row in payload["results"]:
print(
f"{row['name']:<20} {row['portable_ms']:14.4f} "
f"{row['simd_ms']:12.4f} {row['speedup_simd_vs_portable']:14.2f}x"
)
if args.json_path:
path = Path(args.json_path)
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
print(f"\nWrote JSON results to {path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+189
View File
@@ -0,0 +1,189 @@
from __future__ import annotations
import argparse
import json
import time
from collections.abc import Callable
from pathlib import Path
from typing import Any
import numpy as np
import ferro_ta as ft
try:
from benchmarks.metadata import benchmark_metadata
except ModuleNotFoundError: # pragma: no cover - script execution fallback
from metadata import benchmark_metadata
def _time_min(fn: Callable[[], object], rounds: int = 5) -> float:
fn()
samples: list[float] = []
for _ in range(rounds):
t0 = time.perf_counter()
fn()
samples.append(time.perf_counter() - t0)
return min(samples)
def _stream_close(close: np.ndarray, factory: Callable[[], Any]) -> float:
streamer = factory()
last = np.nan
for value in close:
last = streamer.update(float(value))
return float(last) if not np.isnan(last) else np.nan
def _stream_hlc(
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
factory: Callable[[], Any],
) -> float:
streamer = factory()
last = np.nan
for high_value, low_value, close_value in zip(high, low, close):
last = streamer.update(float(high_value), float(low_value), float(close_value))
return float(last) if not np.isnan(last) else np.nan
def _stream_hlcv(
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
volume: np.ndarray,
factory: Callable[[], Any],
) -> float:
streamer = factory()
last = np.nan
for high_value, low_value, close_value, volume_value in zip(high, low, close, volume):
last = streamer.update(
float(high_value),
float(low_value),
float(close_value),
float(volume_value),
)
return float(last) if not np.isnan(last) else np.nan
def run_streaming_benchmark(
*,
n_bars: int = 100_000,
seed: int = 2026,
) -> dict[str, Any]:
rng = np.random.default_rng(seed)
close = 100.0 + np.cumsum(rng.normal(0.0, 1.0, n_bars)).astype(np.float64)
high = close + rng.uniform(0.1, 2.0, n_bars)
low = close - rng.uniform(0.1, 2.0, n_bars)
volume = rng.uniform(1_000.0, 100_000.0, n_bars)
cases = [
(
"StreamingSMA",
"close",
lambda: _stream_close(close, lambda: ft.StreamingSMA(period=20)),
lambda: ft.SMA(close, timeperiod=20),
),
(
"StreamingEMA",
"close",
lambda: _stream_close(close, lambda: ft.StreamingEMA(period=20)),
lambda: ft.EMA(close, timeperiod=20),
),
(
"StreamingRSI",
"close",
lambda: _stream_close(close, lambda: ft.StreamingRSI(period=14)),
lambda: ft.RSI(close, timeperiod=14),
),
(
"StreamingATR",
"hlc",
lambda: _stream_hlc(
high,
low,
close,
lambda: ft.StreamingATR(period=14),
),
lambda: ft.ATR(high, low, close, timeperiod=14),
),
(
"StreamingVWAP",
"hlcv",
lambda: _stream_hlcv(
high,
low,
close,
volume,
lambda: ft.StreamingVWAP(),
),
lambda: ft.VWAP(high, low, close, volume),
),
]
rows: list[dict[str, Any]] = []
for name, input_kind, stream_fn, batch_fn in cases:
stream_s = _time_min(stream_fn)
batch_s = _time_min(batch_fn)
rows.append(
{
"indicator": name,
"inputs": input_kind,
"stream_total_ms": round(stream_s * 1000.0, 4),
"batch_total_ms": round(batch_s * 1000.0, 4),
"stream_ns_per_update": round(stream_s * 1e9 / n_bars, 2),
"batch_ns_per_bar": round(batch_s * 1e9 / n_bars, 2),
"updates_per_second": round(n_bars / stream_s, 2),
"stream_over_batch_ratio": round(stream_s / batch_s, 4),
}
)
return {
"metadata": benchmark_metadata(
"streaming",
extra={
"dataset": {
"n_bars": n_bars,
"seed": seed,
}
},
),
"results": rows,
}
def main() -> int:
parser = argparse.ArgumentParser(description="Benchmark streaming indicator execution.")
parser.add_argument("--bars", type=int, default=100_000)
parser.add_argument("--seed", type=int, default=2026)
parser.add_argument("--json", dest="json_path")
args = parser.parse_args()
payload = run_streaming_benchmark(n_bars=args.bars, seed=args.seed)
dataset = payload["metadata"]["dataset"]
print(f"Streaming Benchmark: {dataset['n_bars']} bars")
print("-" * 86)
print(
f"{'Indicator':<16} {'Stream (ms)':>12} {'Batch (ms)':>12} "
f"{'ns/update':>12} {'upd/s':>12} {'ratio':>10}"
)
print("-" * 86)
for row in payload["results"]:
print(
f"{row['indicator']:<16} {row['stream_total_ms']:12.2f} "
f"{row['batch_total_ms']:12.2f} {row['stream_ns_per_update']:12.2f} "
f"{row['updates_per_second']:12.1f} {row['stream_over_batch_ratio']:10.2f}"
)
if args.json_path:
path = Path(args.json_path)
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
print(f"\nWrote JSON results to {path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Validate hotspot benchmark JSON against conservative speedup floors.
This gate is intentionally lightweight: it checks that the optimized paths
remain faster than their bundled reference implementations and that all
expected cases were present in the report.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
def _parse_threshold_items(items: list[str]) -> dict[str, float]:
thresholds: dict[str, float] = {}
for item in items:
if "=" not in item:
raise ValueError(f"Invalid threshold '{item}', expected NAME=VALUE")
name, value_s = item.split("=", 1)
thresholds[name] = float(value_s)
return thresholds
def main() -> int:
parser = argparse.ArgumentParser(
description="Check hotspot benchmark JSON against regression thresholds."
)
parser.add_argument(
"--input",
default="runtime_hotspots.json",
help="Path to JSON produced by benchmarks/profile_runtime_hotspots.py",
)
parser.add_argument(
"--min-speedup",
action="append",
default=[
"CORREL=2.0",
"BETA=2.0",
"LINEARREG=2.0",
"TSF=2.0",
"iv_rank=1.1",
"iv_percentile=1.1",
"iv_zscore=1.05",
"compute_many_close=0.85",
"feature_matrix=0.80",
],
help="Required minimum speedup per named case, e.g. CORREL=5.0 (repeatable)",
)
parser.add_argument(
"--min-cases",
type=int,
default=9,
help="Minimum number of benchmark rows expected in the report",
)
args = parser.parse_args()
path = Path(args.input)
if not path.exists():
print(f"ERROR: hotspot benchmark file not found: {path}")
return 1
payload = json.loads(path.read_text(encoding="utf-8"))
rows = payload.get("results", [])
if len(rows) < args.min_cases:
print(
f"ERROR: hotspot report contains {len(rows)} rows, expected at least {args.min_cases}"
)
return 1
thresholds = _parse_threshold_items(args.min_speedup)
rows_by_name = {str(row.get("name")): row for row in rows}
failures: list[str] = []
for name, floor in thresholds.items():
row = rows_by_name.get(name)
if row is None:
failures.append(f"missing row for {name}")
continue
speedup = float(row.get("speedup_vs_reference", 0.0))
fast_ms = float(row.get("fast_ms", 0.0))
reference_ms = float(row.get("reference_ms", 0.0))
print(
f"{name}: fast_ms={fast_ms:.4f}, reference_ms={reference_ms:.4f}, "
f"speedup={speedup:.4f}"
)
if fast_ms <= 0.0 or reference_ms <= 0.0:
failures.append(f"{name} has non-positive timing values")
if speedup < floor:
failures.append(f"{name} speedup {speedup:.4f} < floor {floor:.4f}")
if failures:
print("FAILED hotspot regression policy:")
for failure in failures:
print(f" - {failure}")
return 1
print("PASS hotspot regression policy.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+79
View File
@@ -0,0 +1,79 @@
from __future__ import annotations
import hashlib
import platform
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def git_info() -> dict[str, Any]:
"""Best-effort git metadata for reproducible benchmark artifacts."""
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
try:
branch = subprocess.check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
except Exception:
branch = None
return {"commit": commit, "dirty": dirty, "branch": branch}
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(),
"processor": platform.processor() or None,
}
def file_info(path: str | Path) -> dict[str, Any]:
file_path = Path(path)
data = file_path.read_bytes()
return {
"path": str(file_path),
"size_bytes": file_path.stat().st_size,
"sha256": hashlib.sha256(data).hexdigest(),
}
def benchmark_metadata(
suite: str,
*,
fixtures: list[str | Path] | None = None,
extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
metadata: dict[str, Any] = {
"suite": suite,
"runtime": runtime_info(),
"git": git_info(),
}
if fixtures:
metadata["fixtures"] = [file_info(path) for path in fixtures]
if extra:
metadata.update(extra)
return metadata
+269
View File
@@ -0,0 +1,269 @@
from __future__ import annotations
import argparse
import json
import time
from collections.abc import Callable
from pathlib import Path
from typing import Any
import numpy as np
import ferro_ta as ft
from ferro_ta.analysis.features import feature_matrix
from ferro_ta.analysis.options import iv_percentile, iv_rank, iv_zscore
from ferro_ta.data.batch import compute_many
try:
from benchmarks.metadata import benchmark_metadata
except ModuleNotFoundError: # pragma: no cover - script execution fallback
from metadata import benchmark_metadata
def _time_min(fn: Callable[[], object], rounds: int = 5) -> float:
fn()
samples: list[float] = []
for _ in range(rounds):
t0 = time.perf_counter()
fn()
samples.append(time.perf_counter() - t0)
return min(samples) * 1000.0
def _naive_correl(x: np.ndarray, y: np.ndarray, window: int) -> np.ndarray:
out = np.full(len(x), np.nan, dtype=np.float64)
for end in range(window - 1, len(x)):
x_window = x[end + 1 - window : end + 1]
y_window = y[end + 1 - window : end + 1]
mean_x = float(np.sum(x_window)) / window
mean_y = float(np.sum(y_window)) / window
cov = float(np.sum((x_window - mean_x) * (y_window - mean_y)))
std_x = float(np.sqrt(np.sum((x_window - mean_x) ** 2)))
std_y = float(np.sqrt(np.sum((y_window - mean_y) ** 2)))
denom = std_x * std_y
out[end] = cov / denom if denom != 0.0 else np.nan
return out
def _naive_beta(x: np.ndarray, y: np.ndarray, window: int) -> np.ndarray:
out = np.full(len(x), np.nan, dtype=np.float64)
for end in range(window, len(x)):
start = end - window
rx = np.array(
[x[idx + 1] / x[idx] - 1.0 if x[idx] != 0.0 else np.nan for idx in range(start, end)],
dtype=np.float64,
)
ry = np.array(
[y[idx + 1] / y[idx] - 1.0 if y[idx] != 0.0 else np.nan for idx in range(start, end)],
dtype=np.float64,
)
mean_x = float(np.sum(rx)) / window
mean_y = float(np.sum(ry)) / window
cov = float(np.sum((rx - mean_x) * (ry - mean_y))) / window
var_x = float(np.sum((rx - mean_x) ** 2)) / window
out[end] = cov / var_x if var_x != 0.0 else np.nan
return out
def _naive_linearreg(series: np.ndarray, timeperiod: int, x_value: float) -> np.ndarray:
out = np.full(len(series), np.nan, dtype=np.float64)
xs = np.arange(timeperiod, dtype=np.float64)
sum_x = float(np.sum(xs))
sum_x2 = float(np.sum(xs * xs))
for end in range(timeperiod - 1, len(series)):
window = series[end + 1 - timeperiod : end + 1]
sum_y = float(np.sum(window))
sum_xy = float(np.sum(xs * window))
denom = timeperiod * sum_x2 - sum_x * sum_x
slope = (timeperiod * sum_xy - sum_x * sum_y) / denom if denom != 0.0 else 0.0
intercept = (sum_y - slope * sum_x) / timeperiod
out[end] = intercept + slope * x_value
return out
def _old_iv_rank(iv: np.ndarray, window: int) -> np.ndarray:
out = np.full(len(iv), np.nan, dtype=np.float64)
for idx in range(window - 1, len(iv)):
win = iv[idx - window + 1 : idx + 1]
lower = float(np.nanmin(win))
upper = float(np.nanmax(win))
out[idx] = 0.0 if upper == lower else (iv[idx] - lower) / (upper - lower)
return out
def _old_iv_percentile(iv: np.ndarray, window: int) -> np.ndarray:
out = np.full(len(iv), np.nan, dtype=np.float64)
for idx in range(window - 1, len(iv)):
win = iv[idx - window + 1 : idx + 1]
out[idx] = float(np.sum(win <= iv[idx])) / window
return out
def _old_iv_zscore(iv: np.ndarray, window: int) -> np.ndarray:
out = np.full(len(iv), np.nan, dtype=np.float64)
for idx in range(window - 1, len(iv)):
win = iv[idx - window + 1 : idx + 1]
mean = float(np.nanmean(win))
std = float(np.nanstd(win, ddof=0))
out[idx] = np.nan if std == 0.0 else (iv[idx] - mean) / std
return out
def build_hotspot_report(
*,
price_bars: int = 20_000,
iv_bars: int = 50_000,
window: int = 252,
) -> dict[str, Any]:
rng = np.random.default_rng(2026)
close = 100 + np.cumsum(rng.normal(0, 1, price_bars)).astype(np.float64)
high = close + rng.uniform(0.1, 2.0, price_bars)
low = close - rng.uniform(0.1, 2.0, price_bars)
iv = rng.uniform(10.0, 40.0, iv_bars).astype(np.float64)
ohlcv = {"close": close, "high": high, "low": low, "volume": np.full(price_bars, 1000.0)}
rows = [
(
"rust_kernel",
"CORREL",
lambda: ft.CORREL(high, low, timeperiod=30),
lambda: _naive_correl(high, low, 30),
),
(
"rust_kernel",
"BETA",
lambda: ft.BETA(high, low, timeperiod=5),
lambda: _naive_beta(high, low, 5),
),
(
"rust_kernel",
"LINEARREG",
lambda: ft.LINEARREG(close, timeperiod=14),
lambda: _naive_linearreg(close, 14, 13.0),
),
(
"rust_kernel",
"TSF",
lambda: ft.TSF(close, timeperiod=14),
lambda: _naive_linearreg(close, 14, 14.0),
),
(
"python_analysis",
"iv_rank",
lambda: iv_rank(iv, window),
lambda: _old_iv_rank(iv, window),
),
(
"python_analysis",
"iv_percentile",
lambda: iv_percentile(iv, window),
lambda: _old_iv_percentile(iv, window),
),
(
"python_analysis",
"iv_zscore",
lambda: iv_zscore(iv, window),
lambda: _old_iv_zscore(iv, window),
),
(
"ffi_grouping",
"compute_many_close",
lambda: compute_many(
[
("SMA", {"timeperiod": 10}),
("EMA", {"timeperiod": 12}),
("RSI", {"timeperiod": 14}),
],
close=close,
),
lambda: (
ft.SMA(close, timeperiod=10),
ft.EMA(close, timeperiod=12),
ft.RSI(close, timeperiod=14),
),
),
(
"ffi_grouping",
"feature_matrix",
lambda: feature_matrix(
ohlcv,
[
("SMA", {"timeperiod": 10}),
("ATR", {"timeperiod": 14}),
("ADX", {"timeperiod": 14}),
],
),
lambda: {
"SMA": ft.SMA(close, timeperiod=10),
"ATR": ft.ATR(high, low, close, timeperiod=14),
"ADX": ft.ADX(high, low, close, timeperiod=14),
},
),
]
results: list[dict[str, Any]] = []
for category, name, fast_fn, reference_fn in rows:
fast_ms = _time_min(fast_fn)
reference_ms = _time_min(reference_fn, rounds=1)
results.append(
{
"category": category,
"name": name,
"fast_ms": round(fast_ms, 4),
"reference_ms": round(reference_ms, 4),
"speedup_vs_reference": round(reference_ms / fast_ms, 4),
}
)
results.sort(key=lambda row: row["fast_ms"], reverse=True)
total_fast_ms = sum(float(row["fast_ms"]) for row in results) or 1.0
for row in results:
row["share_of_suite_pct"] = round(float(row["fast_ms"]) / total_fast_ms * 100.0, 2)
return {
"metadata": benchmark_metadata(
"runtime_hotspots",
extra={
"dataset": {
"price_bars": price_bars,
"iv_bars": iv_bars,
"window": window,
}
},
),
"results": results,
}
def main() -> int:
parser = argparse.ArgumentParser(description="Profile ferro-ta runtime hotspots.")
parser.add_argument("--price-bars", type=int, default=20_000)
parser.add_argument("--iv-bars", type=int, default=50_000)
parser.add_argument("--window", type=int, default=252)
parser.add_argument("--json", dest="json_path")
args = parser.parse_args()
payload = build_hotspot_report(
price_bars=args.price_bars,
iv_bars=args.iv_bars,
window=args.window,
)
print(f"{'Category':<16} {'Case':<18} {'Fast (ms)':>10} {'Ref (ms)':>10} {'Speedup':>10}")
print("-" * 70)
for row in payload["results"]:
print(
f"{row['category']:<16} {row['name']:<18} {row['fast_ms']:10.2f} "
f"{row['reference_ms']:10.2f} {row['speedup_vs_reference']:10.2f}x"
)
if args.json_path:
path = Path(args.json_path)
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
print(f"\nWrote JSON results to {path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+211
View File
@@ -0,0 +1,211 @@
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
from typing import Any
import numpy as np
try:
from benchmarks.bench_batch import run_batch_benchmark
from benchmarks.bench_simd import run_simd_benchmark
from benchmarks.bench_streaming import run_streaming_benchmark
from benchmarks.bench_vs_talib import run_comparison
from benchmarks.metadata import benchmark_metadata, file_info
from benchmarks.profile_runtime_hotspots import build_hotspot_report
from benchmarks.test_benchmark_suite import (
FIXTURE_PATH,
INDICATOR_SUITE,
_run_indicator,
)
except ModuleNotFoundError: # pragma: no cover - script execution fallback
from bench_batch import run_batch_benchmark
from bench_simd import run_simd_benchmark
from bench_streaming import run_streaming_benchmark
from bench_vs_talib import run_comparison
from metadata import benchmark_metadata, file_info
from profile_runtime_hotspots import build_hotspot_report
from test_benchmark_suite import FIXTURE_PATH, INDICATOR_SUITE, _run_indicator
def _time_min(fn, rounds: int = 5) -> float:
fn()
samples: list[float] = []
for _ in range(rounds):
t0 = time.perf_counter()
fn()
samples.append(time.perf_counter() - t0)
return min(samples) * 1000.0
def build_indicator_latency_report(*, rounds: int = 5) -> dict[str, Any]:
if not FIXTURE_PATH.exists():
raise FileNotFoundError(
f"Canonical fixture not found: {FIXTURE_PATH}. "
"Run benchmarks/fixtures/generate_canonical.py first."
)
fixture = np.load(FIXTURE_PATH)
ohlcv = {key: fixture[key] for key in fixture.files}
rows: list[dict[str, Any]] = []
for entry in INDICATOR_SUITE:
elapsed_ms = _time_min(lambda entry=entry: _run_indicator(entry, ohlcv), rounds=rounds)
rows.append(
{
"name": entry["name"],
"inputs": entry["inputs"],
"kwargs": entry["kwargs"],
"elapsed_ms": round(elapsed_ms, 4),
}
)
rows.sort(key=lambda row: float(row["elapsed_ms"]), reverse=True)
return {
"metadata": benchmark_metadata(
"indicator_latency",
fixtures=[FIXTURE_PATH],
extra={
"dataset": {
"fixture": str(FIXTURE_PATH),
"bars": len(ohlcv["close"]),
"rounds": rounds,
}
},
),
"results": rows,
}
def _write_json(path: Path, payload: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser(
description="Generate reproducible performance baseline artifacts."
)
parser.add_argument(
"--output-dir",
default="benchmarks/artifacts/latest",
help="Directory where benchmark JSON artifacts are written",
)
parser.add_argument("--indicator-rounds", type=int, default=5)
parser.add_argument("--batch-samples", type=int, default=100_000)
parser.add_argument("--batch-series", type=int, default=100)
parser.add_argument("--batch-seed", type=int, default=42)
parser.add_argument("--streaming-bars", type=int, default=100_000)
parser.add_argument("--streaming-seed", type=int, default=2026)
parser.add_argument("--price-bars", type=int, default=20_000)
parser.add_argument("--iv-bars", type=int, default=50_000)
parser.add_argument("--window", type=int, default=252)
parser.add_argument(
"--skip-simd",
action="store_true",
help="Skip portable-vs-SIMD comparison",
)
parser.add_argument(
"--talib-sizes",
type=int,
nargs="+",
default=[10_000, 100_000],
help="Bar counts used for the TA-Lib comparison suite",
)
parser.add_argument(
"--skip-talib",
action="store_true",
help="Skip the TA-Lib comparison artifact",
)
args = parser.parse_args()
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
artifacts: dict[str, str] = {}
indicator_path = output_dir / "indicator_latency.json"
_write_json(
indicator_path,
build_indicator_latency_report(rounds=args.indicator_rounds),
)
artifacts["indicator_latency"] = str(indicator_path)
batch_path = output_dir / "batch.json"
_write_json(
batch_path,
run_batch_benchmark(
n_samples=args.batch_samples,
n_series=args.batch_series,
seed=args.batch_seed,
),
)
artifacts["batch"] = str(batch_path)
streaming_path = output_dir / "streaming.json"
_write_json(
streaming_path,
run_streaming_benchmark(
n_bars=args.streaming_bars,
seed=args.streaming_seed,
),
)
artifacts["streaming"] = str(streaming_path)
hotspot_path = output_dir / "runtime_hotspots.json"
_write_json(
hotspot_path,
build_hotspot_report(
price_bars=args.price_bars,
iv_bars=args.iv_bars,
window=args.window,
),
)
artifacts["runtime_hotspots"] = str(hotspot_path)
if not args.skip_simd:
simd_path = output_dir / "simd.json"
_write_json(
simd_path,
run_simd_benchmark(
price_bars=args.price_bars,
iv_bars=args.iv_bars,
window=args.window,
),
)
artifacts["simd"] = str(simd_path)
if not args.skip_talib:
talib_path = output_dir / "benchmark_vs_talib.json"
run_comparison(args.talib_sizes, str(talib_path))
artifacts["benchmark_vs_talib"] = str(talib_path)
wasm_path = output_dir / "wasm.json"
if wasm_path.exists():
artifacts["wasm"] = str(wasm_path)
manifest = {
"metadata": benchmark_metadata(
"perf_contract",
fixtures=[FIXTURE_PATH],
extra={"output_dir": str(output_dir)},
),
"artifacts": {
name: file_info(path)
for name, path in artifacts.items()
},
}
manifest_path = output_dir / "manifest.json"
_write_json(manifest_path, manifest)
print(f"Generated performance contract artifacts in {output_dir}")
for name, path in artifacts.items():
print(f" - {name}: {path}")
print(f" - manifest: {manifest_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+45 -14
View File
@@ -32,7 +32,8 @@ from __future__ import annotations
import pathlib
import time
from typing import Any, Callable, Dict, List
from collections.abc import Callable
from typing import Any
import numpy as np
import pytest
@@ -46,7 +47,7 @@ BASELINE_PATH = pathlib.Path(__file__).parent / "baselines.npz"
@pytest.fixture(scope="session")
def ohlcv() -> Dict[str, np.ndarray]:
def ohlcv() -> dict[str, np.ndarray]:
"""Load canonical OHLCV fixture."""
if not FIXTURE_PATH.exists():
pytest.skip(f"Canonical fixture not found: {FIXTURE_PATH}")
@@ -60,7 +61,7 @@ def ohlcv() -> Dict[str, np.ndarray]:
# Each entry: (name, callable, kwargs)
# The callable receives (close,) or (high, low, close,) based on 'inputs' key.
INDICATOR_SUITE: List[Dict[str, Any]] = [
INDICATOR_SUITE: list[dict[str, Any]] = [
{
"name": "SMA_20",
"inputs": "close",
@@ -131,6 +132,20 @@ INDICATOR_SUITE: List[Dict[str, Any]] = [
"fn_name": "LINEARREG",
"kwargs": {"timeperiod": 14},
},
{
"name": "LINEARREG_SLOPE_14",
"inputs": "close",
"fn": None,
"fn_name": "LINEARREG_SLOPE",
"kwargs": {"timeperiod": 14},
},
{
"name": "TSF_14",
"inputs": "close",
"fn": None,
"fn_name": "TSF",
"kwargs": {"timeperiod": 14},
},
{
"name": "VAR_20",
"inputs": "close",
@@ -138,6 +153,20 @@ INDICATOR_SUITE: List[Dict[str, Any]] = [
"fn_name": "VAR",
"kwargs": {"timeperiod": 20},
},
{
"name": "CORREL_30",
"inputs": "pair_hl",
"fn": None,
"fn_name": "CORREL",
"kwargs": {"timeperiod": 30},
},
{
"name": "BETA_5",
"inputs": "pair_hl",
"fn": None,
"fn_name": "BETA",
"kwargs": {"timeperiod": 5},
},
{
"name": "CCI_14",
"inputs": "hlc",
@@ -161,12 +190,14 @@ def _load_fn(fn_name: str) -> Callable[..., Any]:
return getattr(ft, fn_name)
def _run_indicator(entry: Dict[str, Any], data: Dict[str, np.ndarray]) -> np.ndarray:
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
elif entry["inputs"] == "hlc":
result = fn(data["high"], data["low"], data["close"], **entry["kwargs"])
else: # pair_hl
result = fn(data["high"], data["low"], **entry["kwargs"])
if isinstance(result, tuple):
result = result[0]
return np.asarray(result, dtype=np.float64)
@@ -184,7 +215,7 @@ class TestNumericalRegression:
"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]
self, entry: dict[str, Any], ohlcv: dict[str, np.ndarray]
) -> None:
"""Indicator output length must equal input length."""
out = _run_indicator(entry, ohlcv)
@@ -196,7 +227,7 @@ class TestNumericalRegression:
"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]
self, entry: dict[str, Any], ohlcv: dict[str, np.ndarray]
) -> None:
"""First bar must be NaN (warm-up)."""
out = _run_indicator(entry, ohlcv)
@@ -205,7 +236,7 @@ class TestNumericalRegression:
@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:
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"
@@ -214,7 +245,7 @@ class TestNumericalRegression:
"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]
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)
@@ -230,7 +261,7 @@ class TestNumericalRegression:
"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]
self, entry: dict[str, Any], ohlcv: dict[str, np.ndarray]
) -> None:
"""Compare last 10 values to stored baselines."""
baselines = np.load(BASELINE_PATH)
@@ -265,8 +296,8 @@ class TestPerformance:
)
def test_timing(
self,
entry: Dict[str, Any],
ohlcv: Dict[str, np.ndarray],
entry: dict[str, Any],
ohlcv: dict[str, np.ndarray],
request: pytest.FixtureRequest,
) -> None:
"""Time the indicator on the canonical dataset."""
@@ -302,7 +333,7 @@ class TestPerformance:
# ---------------------------------------------------------------------------
def update_baselines(ohlcv_data: Dict[str, np.ndarray]) -> None:
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::
@@ -314,7 +345,7 @@ def update_baselines(ohlcv_data: Dict[str, np.ndarray]) -> None:
update_baselines(data)
"
"""
store: Dict[str, np.ndarray] = {}
store: dict[str, np.ndarray] = {}
for entry in INDICATOR_SUITE:
out = _run_indicator(entry, ohlcv_data)
valid = out[~np.isnan(out)]