Files
ferro-ta/benchmarks/bench_streaming.py
Pratik Bhadane 71b6343e92 feat: refresh benchmark coverage and harden CI tooling
Refresh the benchmark and performance surface across the repo. This updates the benchmark wrappers and helper scripts, regenerates the checked-in benchmark and perf-contract artifacts, and folds in the related roadmap, compatibility, and example notebook changes that belong with this performance-focused pass.

Harden the Python CI and local pre-push flow so the same checks pass reliably in both places. The workflow and pre-push script now use module-safe uv typecheck invocations, the Python test environment installs the optional MCP dependency needed by the MCP server tests, and one-off root benchmark outputs are ignored to keep the repo clean.

Align local tooling with the current project configuration by updating the Ruff pre-commit hook, tightening the API typing and MCP server helpers, and refreshing the lockfile to pick up the audited PyJWT fix while preserving the rest of the staged source changes.
2026-03-24 14:52:20 +05:30

194 lines
5.5 KiB
Python

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())