194 lines
5.5 KiB
Python
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())
|