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.
This commit is contained in:
@@ -41,10 +41,10 @@ jobs:
|
||||
run: pip install uv
|
||||
|
||||
- name: Run mypy on ferro_ta via uv
|
||||
run: uv run --with mypy --with numpy mypy python/ferro_ta --ignore-missing-imports --no-error-summary
|
||||
run: uv run --with mypy --with numpy python -m mypy python/ferro_ta --ignore-missing-imports --no-error-summary
|
||||
|
||||
- name: Run pyright on ferro_ta via uv
|
||||
run: uv run --with pyright pyright python/ferro_ta
|
||||
run: uv run --with pyright python -m pyright python/ferro_ta
|
||||
|
||||
test:
|
||||
name: Test (ubuntu-latest / Python ${{ matrix.python-version }})
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Install maturin and test dependencies
|
||||
run: |
|
||||
pip install maturin numpy pytest pytest-cov pandas polars hypothesis pyyaml
|
||||
pip install maturin numpy pytest pytest-cov pandas polars hypothesis pyyaml mcp
|
||||
|
||||
- name: Build and install ferro_ta (dev mode)
|
||||
run: |
|
||||
|
||||
@@ -31,6 +31,9 @@ env/
|
||||
# WASM build output
|
||||
wasm/pkg/
|
||||
wasm/pkg-web/
|
||||
benchmark_vs_talib.json
|
||||
wasm_benchmark.json
|
||||
.wasm_benchmark.prepush.json
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.xml
|
||||
|
||||
@@ -6,7 +6,7 @@ default_language_version:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.0
|
||||
rev: v0.15.7
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
@@ -18,6 +18,7 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
exclude: ^conda/meta\.yaml$
|
||||
- id: check-added-large-files
|
||||
args: [--maxkb=1000]
|
||||
- id: check-merge-conflict
|
||||
|
||||
@@ -70,23 +70,23 @@ rustflags = ["-C", "target-cpu=native"]
|
||||
### Phase 3: Algorithm-Level Optimizations (Target: 5-10x improvement)
|
||||
|
||||
#### SMA — O(n) running sum
|
||||
Current: recomputes each window.
|
||||
Current: recomputes each window.
|
||||
Target: single-pass running sum (already done in Rust — verify SIMD path is hit).
|
||||
|
||||
#### BBANDS — Welford's algorithm
|
||||
Current: compute mean, then variance in two passes.
|
||||
Current: compute mean, then variance in two passes.
|
||||
Target: Welford's online algorithm — single pass, better cache utilization.
|
||||
|
||||
#### ATR/ADX — Avoid redundant True Range calculations
|
||||
Current: ATR → ADX each compute TR independently.
|
||||
Current: ATR → ADX each compute TR independently.
|
||||
Target: Compute TR once, share with ATR, NATR, +DI, -DI, ADX in a single pass.
|
||||
|
||||
#### MACD — Reuse EMA computations
|
||||
Current: Compute fast EMA and slow EMA separately.
|
||||
Current: Compute fast EMA and slow EMA separately.
|
||||
Target: Single function computes both EMAs in one pass.
|
||||
|
||||
#### Candlestick Patterns — Batch lookup table
|
||||
Current: Sequential condition checks per bar.
|
||||
Current: Sequential condition checks per bar.
|
||||
Target: Pre-compute body/shadow ratios, vectorized pattern matching.
|
||||
|
||||
### Phase 4: Streaming Precomputation (Target: 100x for incremental updates)
|
||||
|
||||
@@ -259,4 +259,3 @@ for the first `timeperiod - 1` bars.
|
||||
|
||||
> `ferro-ta` implements 100% of TA-Lib's function set. NaN values are placed
|
||||
> at the beginning of each output array for the warmup period.
|
||||
|
||||
|
||||
+18
-18
@@ -77,7 +77,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -117,12 +117,12 @@ app = FastAPI(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _nan_to_none(arr: np.ndarray) -> List[Optional[float]]:
|
||||
def _nan_to_none(arr: np.ndarray) -> list[float | None]:
|
||||
"""Convert numpy array to list, replacing NaN/Inf with None."""
|
||||
return [None if not math.isfinite(v) else float(v) for v in arr]
|
||||
|
||||
|
||||
def _validate_series(close: List[float]) -> np.ndarray:
|
||||
def _validate_series(close: list[float]) -> np.ndarray:
|
||||
if len(close) > MAX_SERIES_LENGTH:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
@@ -142,54 +142,54 @@ def _validate_series(close: List[float]) -> np.ndarray:
|
||||
|
||||
|
||||
class IndicatorRequest(BaseModel):
|
||||
close: List[float] = Field(..., description="Close price series")
|
||||
close: list[float] = Field(..., description="Close price series")
|
||||
timeperiod: int = Field(default=14, ge=1, description="Look-back period")
|
||||
|
||||
@field_validator("close")
|
||||
@classmethod
|
||||
def close_must_be_finite(cls, v: List[float]) -> List[float]:
|
||||
def close_must_be_finite(cls, v: list[float]) -> list[float]:
|
||||
if not all(math.isfinite(x) for x in v):
|
||||
raise ValueError("close series must contain only finite values")
|
||||
return v
|
||||
|
||||
|
||||
class MACDRequest(BaseModel):
|
||||
close: List[float] = Field(..., description="Close price series")
|
||||
close: list[float] = Field(..., description="Close price series")
|
||||
fastperiod: int = Field(default=12, ge=1)
|
||||
slowperiod: int = Field(default=26, ge=1)
|
||||
signalperiod: int = Field(default=9, ge=1)
|
||||
|
||||
@field_validator("close")
|
||||
@classmethod
|
||||
def close_must_be_finite(cls, v: List[float]) -> List[float]:
|
||||
def close_must_be_finite(cls, v: list[float]) -> list[float]:
|
||||
if not all(math.isfinite(x) for x in v):
|
||||
raise ValueError("close series must contain only finite values")
|
||||
return v
|
||||
|
||||
|
||||
class BBANDSRequest(BaseModel):
|
||||
close: List[float] = Field(..., description="Close price series")
|
||||
close: list[float] = Field(..., description="Close price series")
|
||||
timeperiod: int = Field(default=5, ge=2)
|
||||
nbdevup: float = Field(default=2.0, gt=0)
|
||||
nbdevdn: float = Field(default=2.0, gt=0)
|
||||
|
||||
@field_validator("close")
|
||||
@classmethod
|
||||
def close_must_be_finite(cls, v: List[float]) -> List[float]:
|
||||
def close_must_be_finite(cls, v: list[float]) -> list[float]:
|
||||
if not all(math.isfinite(x) for x in v):
|
||||
raise ValueError("close series must contain only finite values")
|
||||
return v
|
||||
|
||||
|
||||
class BacktestRequest(BaseModel):
|
||||
close: List[float] = Field(..., description="Close price series")
|
||||
close: list[float] = Field(..., description="Close price series")
|
||||
strategy: str = Field(default="rsi_30_70")
|
||||
commission_per_trade: float = Field(default=0.0, ge=0.0)
|
||||
slippage_bps: float = Field(default=0.0, ge=0.0)
|
||||
|
||||
@field_validator("close")
|
||||
@classmethod
|
||||
def close_must_be_finite(cls, v: List[float]) -> List[float]:
|
||||
def close_must_be_finite(cls, v: list[float]) -> list[float]:
|
||||
if not all(math.isfinite(x) for x in v):
|
||||
raise ValueError("close series must contain only finite values")
|
||||
return v
|
||||
@@ -201,13 +201,13 @@ class BacktestRequest(BaseModel):
|
||||
|
||||
|
||||
@app.get("/health", summary="Health check")
|
||||
def health() -> Dict[str, str]:
|
||||
def health() -> dict[str, str]:
|
||||
"""Readiness / liveness probe."""
|
||||
return {"status": "ok", "version": app.version}
|
||||
|
||||
|
||||
@app.post("/indicators/sma", summary="Simple Moving Average")
|
||||
def compute_sma(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
def compute_sma(req: IndicatorRequest) -> dict[str, Any]:
|
||||
"""Compute Simple Moving Average (SMA).
|
||||
|
||||
Returns ``result``: list of floats (null for warm-up bars).
|
||||
@@ -218,7 +218,7 @@ def compute_sma(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/indicators/ema", summary="Exponential Moving Average")
|
||||
def compute_ema(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
def compute_ema(req: IndicatorRequest) -> dict[str, Any]:
|
||||
"""Compute Exponential Moving Average (EMA)."""
|
||||
c = _validate_series(req.close)
|
||||
out = np.asarray(ft.EMA(c, timeperiod=req.timeperiod), dtype=np.float64)
|
||||
@@ -226,7 +226,7 @@ def compute_ema(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/indicators/rsi", summary="Relative Strength Index")
|
||||
def compute_rsi(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
def compute_rsi(req: IndicatorRequest) -> dict[str, Any]:
|
||||
"""Compute Relative Strength Index (RSI)."""
|
||||
c = _validate_series(req.close)
|
||||
out = np.asarray(ft.RSI(c, timeperiod=req.timeperiod), dtype=np.float64)
|
||||
@@ -234,7 +234,7 @@ def compute_rsi(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/indicators/macd", summary="MACD")
|
||||
def compute_macd(req: MACDRequest) -> Dict[str, Any]:
|
||||
def compute_macd(req: MACDRequest) -> dict[str, Any]:
|
||||
"""Compute MACD (line, signal, histogram).
|
||||
|
||||
Returns ``result`` with keys ``macd``, ``signal``, ``hist``.
|
||||
@@ -256,7 +256,7 @@ def compute_macd(req: MACDRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/indicators/bbands", summary="Bollinger Bands")
|
||||
def compute_bbands(req: BBANDSRequest) -> Dict[str, Any]:
|
||||
def compute_bbands(req: BBANDSRequest) -> dict[str, Any]:
|
||||
"""Compute Bollinger Bands (upper, middle, lower).
|
||||
|
||||
Returns ``result`` with keys ``upper``, ``middle``, ``lower``.
|
||||
@@ -278,7 +278,7 @@ def compute_bbands(req: BBANDSRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/backtest", summary="Vectorized backtest")
|
||||
def run_backtest(req: BacktestRequest) -> Dict[str, Any]:
|
||||
def run_backtest(req: BacktestRequest) -> dict[str, Any]:
|
||||
"""Run a vectorized backtest using a named strategy.
|
||||
|
||||
Strategies: ``rsi_30_70``, ``sma_crossover``, ``macd_crossover``.
|
||||
|
||||
@@ -68,4 +68,4 @@
|
||||
"speedup_vs_separate": 2.2811
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,4 +1159,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1379,4 +1379,4 @@
|
||||
"outcome": "ferro_ta_win"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,4 +160,4 @@
|
||||
"elapsed_ms": 0.0026
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,4 @@
|
||||
"sha256": "f31fd871990c44e24a2259d618ae40a52866d20b95aa6047af3d38b9371c2ab7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,4 +93,4 @@
|
||||
"share_of_suite_pct": 0.09
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,4 +282,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,4 +70,4 @@
|
||||
"stream_over_batch_ratio": 121.7603
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,13 +48,17 @@ def run_batch_benchmark(
|
||||
"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)],
|
||||
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)],
|
||||
lambda: [
|
||||
ferro_ta.RSI(close2d[:, j], timeperiod=14) for j in range(n_series)
|
||||
],
|
||||
),
|
||||
(
|
||||
"ATR",
|
||||
@@ -201,7 +205,9 @@ def main() -> int:
|
||||
if payload["grouped_results"]:
|
||||
print("\nGrouped Multi-Indicator Calls")
|
||||
print("-" * 64)
|
||||
print(f"{'Case':<18} {'Grouped (ms)':>14} {'Separate (ms)':>16} {'Speedup':>12}")
|
||||
print(
|
||||
f"{'Case':<18} {'Grouped (ms)':>14} {'Separate (ms)':>16} {'Speedup':>12}"
|
||||
)
|
||||
print("-" * 64)
|
||||
for row in payload["grouped_results"]:
|
||||
print(
|
||||
|
||||
@@ -26,9 +26,10 @@ import math
|
||||
import sys
|
||||
import time
|
||||
import tracemalloc
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -70,9 +70,7 @@ def run_simd_benchmark(
|
||||
for label, args in variants
|
||||
}
|
||||
|
||||
portable_rows = {
|
||||
row["name"]: row for row in reports["portable_release"]["results"]
|
||||
}
|
||||
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]] = []
|
||||
@@ -136,9 +134,7 @@ def main() -> int:
|
||||
window=args.window,
|
||||
)
|
||||
|
||||
print(
|
||||
f"{'Case':<20} {'Portable (ms)':>14} {'SIMD (ms)':>12} {'SIMD speedup':>14}"
|
||||
)
|
||||
print(f"{'Case':<20} {'Portable (ms)':>14} {'SIMD (ms)':>12} {'SIMD speedup':>14}")
|
||||
print("-" * 64)
|
||||
for row in payload["results"]:
|
||||
print(
|
||||
|
||||
@@ -57,7 +57,9 @@ def _stream_hlcv(
|
||||
) -> float:
|
||||
streamer = factory()
|
||||
last = np.nan
|
||||
for high_value, low_value, close_value, volume_value in zip(high, low, close, volume):
|
||||
for high_value, low_value, close_value, volume_value in zip(
|
||||
high, low, close, volume
|
||||
):
|
||||
last = streamer.update(
|
||||
float(high_value),
|
||||
float(low_value),
|
||||
@@ -154,7 +156,9 @@ def run_streaming_benchmark(
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Benchmark streaming indicator execution.")
|
||||
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")
|
||||
|
||||
@@ -309,9 +309,13 @@ def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, An
|
||||
|
||||
if not TALIB_AVAILABLE:
|
||||
print("Note: ta-lib not installed. Reporting ferro_ta timings only.")
|
||||
print("Install with: pip install ta-lib (or conda install ta-lib) for comparison.\n")
|
||||
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} measured runs after {N_WARMUP} warmup")
|
||||
print(
|
||||
f"\nferro_ta vs TA-Lib — median of {N_RUNS} measured runs after {N_WARMUP} warmup"
|
||||
)
|
||||
print(f"Sizes: {sizes}")
|
||||
print()
|
||||
|
||||
@@ -328,11 +332,15 @@ def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, An
|
||||
if size == 1_000_000 and name in SKIP_1M_FOR:
|
||||
continue
|
||||
|
||||
ft_samples_ms = _timed_runs_ms(ft_run, open_, high, low, close, volume, size)
|
||||
ft_samples_ms = _timed_runs_ms(
|
||||
ft_run, open_, high, low, close, volume, size
|
||||
)
|
||||
ft_stats = _summary_stats(ft_samples_ms)
|
||||
ft_median_ms = float(ft_stats["median_ms"])
|
||||
ft_m_bars_s = _throughput_m_bars_s(size, ft_median_ms)
|
||||
ft_peak_bytes = _python_peak_bytes(ft_run, open_, high, low, close, volume, size)
|
||||
ft_peak_bytes = _python_peak_bytes(
|
||||
ft_run, open_, high, low, close, volume, size
|
||||
)
|
||||
|
||||
row: dict[str, Any] = {
|
||||
"indicator": name,
|
||||
@@ -351,11 +359,15 @@ def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, An
|
||||
}
|
||||
|
||||
if TALIB_AVAILABLE:
|
||||
ta_samples_ms = _timed_runs_ms(ta_run, open_, high, low, close, volume, size)
|
||||
ta_samples_ms = _timed_runs_ms(
|
||||
ta_run, open_, high, low, close, volume, size
|
||||
)
|
||||
ta_stats = _summary_stats(ta_samples_ms)
|
||||
ta_median_ms = float(ta_stats["median_ms"])
|
||||
ta_m_bars_s = _throughput_m_bars_s(size, ta_median_ms)
|
||||
speedup = ta_median_ms / ft_median_ms if ft_median_ms > 0 else float("inf")
|
||||
speedup = (
|
||||
ta_median_ms / ft_median_ms if ft_median_ms > 0 else float("inf")
|
||||
)
|
||||
outcome = _outcome(speedup)
|
||||
ta_peak_bytes = _python_peak_bytes(
|
||||
ta_run, open_, high, low, close, volume, size
|
||||
|
||||
@@ -9,7 +9,9 @@ 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
|
||||
@@ -21,9 +23,11 @@ if _root not in (Path(p).resolve() for p in sys.path):
|
||||
|
||||
from benchmarks.wrapper_registry import (
|
||||
INDICATOR_CATEGORIES,
|
||||
LIBRARY_NAMES as LIBS,
|
||||
is_supported,
|
||||
)
|
||||
from benchmarks.wrapper_registry import (
|
||||
LIBRARY_NAMES as LIBS,
|
||||
)
|
||||
|
||||
|
||||
def _all_indicators() -> list[str]:
|
||||
@@ -34,11 +38,17 @@ def _all_indicators() -> list[str]:
|
||||
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)
|
||||
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)
|
||||
print(
|
||||
"results.json is empty. Run the full benchmark suite first.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
|
||||
@@ -88,7 +88,9 @@ def main() -> int:
|
||||
|
||||
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.")
|
||||
print(
|
||||
"ERROR: TA-Lib was not available; cannot enforce TA-Lib regression policy."
|
||||
)
|
||||
return 1
|
||||
|
||||
summary_by_size = {
|
||||
@@ -133,9 +135,7 @@ def main() -> int:
|
||||
)
|
||||
|
||||
if rows < args.min_rows:
|
||||
failures.append(
|
||||
f"size={size} rows {rows} < min_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}"
|
||||
|
||||
@@ -50,11 +50,17 @@ def _naive_beta(x: np.ndarray, y: np.ndarray, window: int) -> np.ndarray:
|
||||
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)],
|
||||
[
|
||||
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)],
|
||||
[
|
||||
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
|
||||
@@ -120,7 +126,12 @@ def build_hotspot_report(
|
||||
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)}
|
||||
ohlcv = {
|
||||
"close": close,
|
||||
"high": high,
|
||||
"low": low,
|
||||
"volume": np.full(price_bars, 1000.0),
|
||||
}
|
||||
|
||||
rows = [
|
||||
(
|
||||
@@ -218,7 +229,9 @@ def build_hotspot_report(
|
||||
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)
|
||||
row["share_of_suite_pct"] = round(
|
||||
float(row["fast_ms"]) / total_fast_ms * 100.0, 2
|
||||
)
|
||||
|
||||
return {
|
||||
"metadata": benchmark_metadata(
|
||||
@@ -249,7 +262,9 @@ def main() -> int:
|
||||
window=args.window,
|
||||
)
|
||||
|
||||
print(f"{'Category':<16} {'Case':<18} {'Fast (ms)':>10} {'Ref (ms)':>10} {'Speedup':>10}")
|
||||
print(
|
||||
f"{'Category':<16} {'Case':<18} {'Fast (ms)':>10} {'Ref (ms)':>10} {'Speedup':>10}"
|
||||
)
|
||||
print("-" * 70)
|
||||
for row in payload["results"]:
|
||||
print(
|
||||
|
||||
@@ -16094,4 +16094,4 @@
|
||||
],
|
||||
"datetime": "2026-03-23T17:14:05.427766+00:00",
|
||||
"version": "5.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ def build_indicator_latency_report(*, rounds: int = 5) -> dict[str, Any]:
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
for entry in INDICATOR_SUITE:
|
||||
elapsed_ms = _time_min(lambda entry=entry: _run_indicator(entry, ohlcv), rounds=rounds)
|
||||
elapsed_ms = _time_min(
|
||||
lambda entry=entry: _run_indicator(entry, ohlcv), rounds=rounds
|
||||
)
|
||||
rows.append(
|
||||
{
|
||||
"name": entry["name"],
|
||||
@@ -192,10 +194,7 @@ def main() -> int:
|
||||
fixtures=[FIXTURE_PATH],
|
||||
extra={"output_dir": str(output_dir)},
|
||||
),
|
||||
"artifacts": {
|
||||
name: file_info(path)
|
||||
for name, path in artifacts.items()
|
||||
},
|
||||
"artifacts": {name: file_info(path) for name, path in artifacts.items()},
|
||||
}
|
||||
manifest_path = output_dir / "manifest.json"
|
||||
_write_json(manifest_path, manifest)
|
||||
|
||||
+60
-53
@@ -5,65 +5,66 @@ For each indicator we compare ferro_ta output against every available reference
|
||||
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,
|
||||
CUMULATIVE_INDICATORS,
|
||||
INDICATOR_CATEGORIES,
|
||||
INDICATOR_NAMES,
|
||||
available_libraries,
|
||||
execute_indicator,
|
||||
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]
|
||||
COMPARISON_LIBS = [
|
||||
library for library in available_libraries() if library != 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
|
||||
"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),
|
||||
"VAR": (1e-3, 0.50),
|
||||
"MACD": (1e-3, 1.00), # seed differences across libraries
|
||||
"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, 0.05),
|
||||
"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),
|
||||
"HT_DCPERIOD": (1e-2, 2.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),
|
||||
"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),
|
||||
}
|
||||
|
||||
_DEFAULT_TOL = (1e-4, 1e-5)
|
||||
@@ -71,33 +72,33 @@ _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", "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
|
||||
("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
|
||||
("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", "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
|
||||
("PLUS_DI", "pandas_ta"), # pandas_ta ADX column naming corr < 0.70
|
||||
}
|
||||
|
||||
MIN_OVERLAP = 30 # minimum points to make comparison meaningful
|
||||
@@ -114,12 +115,14 @@ def _compare(ref: np.ndarray, cmp: np.ndarray, indicator: str, library: str) ->
|
||||
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
|
||||
corr = np.corrcoef(r, c)[0, 1] if indicator not 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)"
|
||||
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:
|
||||
@@ -136,6 +139,7 @@ def _compare(ref: np.ndarray, cmp: np.ndarray, indicator: str, library: str) ->
|
||||
|
||||
# ── dynamically generate one test per (indicator, library) pair ─────────────
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
if "indicator" in metafunc.fixturenames and "library" in metafunc.fixturenames:
|
||||
params = []
|
||||
@@ -172,6 +176,7 @@ class TestAccuracy:
|
||||
|
||||
# ── quick smoke tests that always run (no skip) ──────────────────────────────
|
||||
|
||||
|
||||
class TestSmoke:
|
||||
"""Sanity checks that ferro_ta returns non-empty finite arrays."""
|
||||
|
||||
@@ -182,7 +187,9 @@ class TestSmoke:
|
||||
|
||||
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]}"
|
||||
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):
|
||||
|
||||
+31
-18
@@ -6,31 +6,36 @@ Run: pytest benchmarks/test_speed.py --benchmark-only -v
|
||||
|
||||
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,
|
||||
execute_indicator,
|
||||
is_supported,
|
||||
)
|
||||
|
||||
BENCH_DATA = LARGE # 100k bars for main benchmarks
|
||||
BENCH_LIBS = available_libraries()
|
||||
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 = []
|
||||
@@ -53,20 +58,24 @@ class TestSpeed:
|
||||
|
||||
# ── 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"]),
|
||||
])
|
||||
|
||||
@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):
|
||||
@@ -77,7 +86,11 @@ def test_head_to_head(benchmark, indicator, libs):
|
||||
|
||||
# ── Large dataset benchmarks (100k bars) ─────────────────────────────────────
|
||||
|
||||
@pytest.mark.parametrize("indicator", ["SMA","EMA","RSI","MACD","ATR","BBANDS","OBV","CCI","ADX","MFI"])
|
||||
|
||||
@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):
|
||||
|
||||
+2521
-616
File diff suppressed because it is too large
Load Diff
@@ -21,13 +21,13 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"import ferro_ta.config as config\n",
|
||||
"from ferro_ta import BBANDS, EMA, RSI, SMA\n",
|
||||
"import numpy as np\n",
|
||||
"from ferro_ta.backtest import backtest\n",
|
||||
"from ferro_ta.pipeline import Pipeline\n",
|
||||
"\n",
|
||||
"from ferro_ta import BBANDS, EMA, RSI, SMA\n",
|
||||
"\n",
|
||||
"# Synthetic data\n",
|
||||
"np.random.seed(42)\n",
|
||||
"n = 300\n",
|
||||
|
||||
@@ -207,9 +207,10 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from ferro_ta import FerroTAValueError\n",
|
||||
"from ferro_ta.exceptions import check_timeperiod\n",
|
||||
"\n",
|
||||
"from ferro_ta import FerroTAValueError\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" check_timeperiod(0)\n",
|
||||
"except FerroTAValueError as e:\n",
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"from ferro_ta.streaming import (\n",
|
||||
" StreamingATR,\n",
|
||||
" StreamingBBands,\n",
|
||||
|
||||
+55
-33
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "batch",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:08:30.877702+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:04.010216+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"dataset": {
|
||||
"n_samples": 20000,
|
||||
@@ -23,49 +45,49 @@
|
||||
"results": [
|
||||
{
|
||||
"indicator": "SMA",
|
||||
"parallel_ms": 1.9615,
|
||||
"sequential_ms": 2.0423,
|
||||
"loop_ms": 0.8865,
|
||||
"parallel_speedup_vs_loop": 0.452,
|
||||
"sequential_speedup_vs_loop": 0.4341
|
||||
"parallel_ms": 7.9306,
|
||||
"sequential_ms": 2.4832,
|
||||
"loop_ms": 1.0238,
|
||||
"parallel_speedup_vs_loop": 0.1291,
|
||||
"sequential_speedup_vs_loop": 0.4123
|
||||
},
|
||||
{
|
||||
"indicator": "RSI",
|
||||
"parallel_ms": 2.1731,
|
||||
"sequential_ms": 4.3225,
|
||||
"loop_ms": 3.2043,
|
||||
"parallel_speedup_vs_loop": 1.4745,
|
||||
"sequential_speedup_vs_loop": 0.7413
|
||||
"parallel_ms": 3.9307,
|
||||
"sequential_ms": 5.0938,
|
||||
"loop_ms": 3.6883,
|
||||
"parallel_speedup_vs_loop": 0.9383,
|
||||
"sequential_speedup_vs_loop": 0.7241
|
||||
},
|
||||
{
|
||||
"indicator": "ATR",
|
||||
"parallel_ms": 4.2249,
|
||||
"sequential_ms": 6.6175,
|
||||
"loop_ms": 4.0382,
|
||||
"parallel_speedup_vs_loop": 0.9558,
|
||||
"sequential_speedup_vs_loop": 0.6102
|
||||
"parallel_ms": 9.2546,
|
||||
"sequential_ms": 7.768,
|
||||
"loop_ms": 5.2669,
|
||||
"parallel_speedup_vs_loop": 0.5691,
|
||||
"sequential_speedup_vs_loop": 0.678
|
||||
},
|
||||
{
|
||||
"indicator": "ADX",
|
||||
"parallel_ms": 4.8674,
|
||||
"sequential_ms": 7.6402,
|
||||
"loop_ms": 5.1861,
|
||||
"parallel_speedup_vs_loop": 1.0655,
|
||||
"sequential_speedup_vs_loop": 0.6788
|
||||
"parallel_ms": 9.5489,
|
||||
"sequential_ms": 9.1403,
|
||||
"loop_ms": 7.1578,
|
||||
"parallel_speedup_vs_loop": 0.7496,
|
||||
"sequential_speedup_vs_loop": 0.7831
|
||||
}
|
||||
],
|
||||
"grouped_results": [
|
||||
{
|
||||
"case": "close_bundle_3",
|
||||
"grouped_ms": 0.1878,
|
||||
"separate_ms": 0.1719,
|
||||
"speedup_vs_separate": 0.9155
|
||||
"grouped_ms": 0.466,
|
||||
"separate_ms": 0.2003,
|
||||
"speedup_vs_separate": 0.4298
|
||||
},
|
||||
{
|
||||
"case": "hlc_bundle_3",
|
||||
"grouped_ms": 0.2958,
|
||||
"separate_ms": 0.4633,
|
||||
"speedup_vs_separate": 1.5666
|
||||
"grouped_ms": 1.2538,
|
||||
"separate_ms": 0.6999,
|
||||
"speedup_vs_separate": 0.5582
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "indicator_latency",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:08:30.515962+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:02.973728+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"fixtures": [
|
||||
{
|
||||
@@ -33,7 +55,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0202
|
||||
"elapsed_ms": 0.0233
|
||||
},
|
||||
{
|
||||
"name": "WILLR_14",
|
||||
@@ -41,13 +63,13 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0183
|
||||
"elapsed_ms": 0.0207
|
||||
},
|
||||
{
|
||||
"name": "STOCH",
|
||||
"inputs": "hlc",
|
||||
"kwargs": {},
|
||||
"elapsed_ms": 0.0181
|
||||
"elapsed_ms": 0.0202
|
||||
},
|
||||
{
|
||||
"name": "ADX_14",
|
||||
@@ -55,7 +77,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0147
|
||||
"elapsed_ms": 0.0171
|
||||
},
|
||||
{
|
||||
"name": "CCI_14",
|
||||
@@ -63,13 +85,13 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0144
|
||||
"elapsed_ms": 0.0164
|
||||
},
|
||||
{
|
||||
"name": "MACD",
|
||||
"inputs": "close",
|
||||
"kwargs": {},
|
||||
"elapsed_ms": 0.0132
|
||||
"elapsed_ms": 0.015
|
||||
},
|
||||
{
|
||||
"name": "ATR_14",
|
||||
@@ -77,7 +99,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0108
|
||||
"elapsed_ms": 0.0116
|
||||
},
|
||||
{
|
||||
"name": "RSI_14",
|
||||
@@ -85,7 +107,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0102
|
||||
"elapsed_ms": 0.011
|
||||
},
|
||||
{
|
||||
"name": "STDDEV_20",
|
||||
@@ -93,7 +115,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0086
|
||||
"elapsed_ms": 0.0103
|
||||
},
|
||||
{
|
||||
"name": "BETA_5",
|
||||
@@ -101,7 +123,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 5
|
||||
},
|
||||
"elapsed_ms": 0.008
|
||||
"elapsed_ms": 0.0091
|
||||
},
|
||||
{
|
||||
"name": "CORREL_30",
|
||||
@@ -109,15 +131,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 30
|
||||
},
|
||||
"elapsed_ms": 0.0068
|
||||
},
|
||||
{
|
||||
"name": "EMA_20",
|
||||
"inputs": "close",
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0052
|
||||
"elapsed_ms": 0.0078
|
||||
},
|
||||
{
|
||||
"name": "BBANDS_20",
|
||||
@@ -125,7 +139,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.005
|
||||
"elapsed_ms": 0.0062
|
||||
},
|
||||
{
|
||||
"name": "TSF_14",
|
||||
@@ -133,7 +147,15 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0048
|
||||
"elapsed_ms": 0.0056
|
||||
},
|
||||
{
|
||||
"name": "EMA_20",
|
||||
"inputs": "close",
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0055
|
||||
},
|
||||
{
|
||||
"name": "LINEARREG_14",
|
||||
@@ -141,7 +163,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0046
|
||||
"elapsed_ms": 0.0054
|
||||
},
|
||||
{
|
||||
"name": "LINEARREG_SLOPE_14",
|
||||
@@ -149,7 +171,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0045
|
||||
"elapsed_ms": 0.005
|
||||
},
|
||||
{
|
||||
"name": "SMA_20",
|
||||
@@ -157,7 +179,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0027
|
||||
"elapsed_ms": 0.0032
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+37
-20
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "perf_contract",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:09:13.077731+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:08.787230+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"fixtures": [
|
||||
{
|
||||
@@ -25,28 +47,23 @@
|
||||
"artifacts": {
|
||||
"indicator_latency": {
|
||||
"path": "perf-contract/indicator_latency.json",
|
||||
"size_bytes": 3212,
|
||||
"sha256": "1e7f844fe6eb467f07bbb1e4eb918c56861e8cf819ab4802b96e033c6d2570c4"
|
||||
"size_bytes": 4041,
|
||||
"sha256": "564027c7abed7ecd4ae2ac1720217d96e1e31807f8ac7d5c5393c8fd974f13ed"
|
||||
},
|
||||
"batch": {
|
||||
"path": "perf-contract/batch.json",
|
||||
"size_bytes": 1679,
|
||||
"sha256": "202d67b24a88184473f93cec9f866ca34ff60721068e15e7e5d23962f006d169"
|
||||
"size_bytes": 2507,
|
||||
"sha256": "c50f242a138a0c358a6fa86c420954b4b11a2200598ca2942bcd0fd2bc456fb2"
|
||||
},
|
||||
"streaming": {
|
||||
"path": "perf-contract/streaming.json",
|
||||
"size_bytes": 1939,
|
||||
"sha256": "04476c3d95a9e7d87e40331b28c5ccad9a7aa5abcfc47fb6ac7e929365f68554"
|
||||
"size_bytes": 2766,
|
||||
"sha256": "d271d3219ec098e443e84ef648ab4220cd2a38e6dc998bc6c9fc545d7fc77716"
|
||||
},
|
||||
"runtime_hotspots": {
|
||||
"path": "perf-contract/runtime_hotspots.json",
|
||||
"size_bytes": 2365,
|
||||
"sha256": "d37a58bc88def9f1525b9b0f64f0bd7b9c0df9ed4cae260c833697f6a1689ca8"
|
||||
},
|
||||
"simd": {
|
||||
"path": "perf-contract/simd.json",
|
||||
"size_bytes": 7670,
|
||||
"sha256": "3f9b341a8206698ed172650752d808621c9b5218f42a2373f0f4485496197b4c"
|
||||
"size_bytes": 3197,
|
||||
"sha256": "96eafc9a61ac410e47fcdfee6a9db1123cdee0ec05f49a85c631a3975063deed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "runtime_hotspots",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:08:34.436165+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:08.521805+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"dataset": {
|
||||
"price_bars": 20000,
|
||||
@@ -23,74 +45,74 @@
|
||||
{
|
||||
"category": "python_analysis",
|
||||
"name": "iv_zscore",
|
||||
"fast_ms": 28.857,
|
||||
"reference_ms": 897.0178,
|
||||
"speedup_vs_reference": 31.0849,
|
||||
"share_of_suite_pct": 70.0
|
||||
"fast_ms": 32.5847,
|
||||
"reference_ms": 990.4233,
|
||||
"speedup_vs_reference": 30.3954,
|
||||
"share_of_suite_pct": 69.27
|
||||
},
|
||||
{
|
||||
"category": "python_analysis",
|
||||
"name": "iv_rank",
|
||||
"fast_ms": 10.8496,
|
||||
"reference_ms": 199.4376,
|
||||
"speedup_vs_reference": 18.3821,
|
||||
"share_of_suite_pct": 26.32
|
||||
"fast_ms": 12.2974,
|
||||
"reference_ms": 233.0671,
|
||||
"speedup_vs_reference": 18.9525,
|
||||
"share_of_suite_pct": 26.14
|
||||
},
|
||||
{
|
||||
"category": "python_analysis",
|
||||
"name": "iv_percentile",
|
||||
"fast_ms": 0.9124,
|
||||
"reference_ms": 79.6557,
|
||||
"speedup_vs_reference": 87.3019,
|
||||
"share_of_suite_pct": 2.21
|
||||
"fast_ms": 0.9511,
|
||||
"reference_ms": 90.5663,
|
||||
"speedup_vs_reference": 95.2202,
|
||||
"share_of_suite_pct": 2.02
|
||||
},
|
||||
{
|
||||
"category": "ffi_grouping",
|
||||
"name": "feature_matrix",
|
||||
"fast_ms": 0.2411,
|
||||
"reference_ms": 0.2215,
|
||||
"speedup_vs_reference": 0.9186,
|
||||
"share_of_suite_pct": 0.58
|
||||
"fast_ms": 0.6619,
|
||||
"reference_ms": 0.6155,
|
||||
"speedup_vs_reference": 0.9299,
|
||||
"share_of_suite_pct": 1.41
|
||||
},
|
||||
{
|
||||
"category": "ffi_grouping",
|
||||
"name": "compute_many_close",
|
||||
"fast_ms": 0.1563,
|
||||
"reference_ms": 0.1498,
|
||||
"speedup_vs_reference": 0.9587,
|
||||
"share_of_suite_pct": 0.38
|
||||
"fast_ms": 0.3175,
|
||||
"reference_ms": 0.2437,
|
||||
"speedup_vs_reference": 0.7675,
|
||||
"share_of_suite_pct": 0.67
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "BETA",
|
||||
"fast_ms": 0.0694,
|
||||
"reference_ms": 159.2715,
|
||||
"speedup_vs_reference": 2294.4163,
|
||||
"share_of_suite_pct": 0.17
|
||||
"fast_ms": 0.0742,
|
||||
"reference_ms": 188.6048,
|
||||
"speedup_vs_reference": 2541.5695,
|
||||
"share_of_suite_pct": 0.16
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "CORREL",
|
||||
"fast_ms": 0.0555,
|
||||
"reference_ms": 158.2775,
|
||||
"speedup_vs_reference": 2851.8468,
|
||||
"share_of_suite_pct": 0.13
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "LINEARREG",
|
||||
"fast_ms": 0.0414,
|
||||
"reference_ms": 44.8111,
|
||||
"speedup_vs_reference": 1081.9491,
|
||||
"share_of_suite_pct": 0.1
|
||||
"fast_ms": 0.0638,
|
||||
"reference_ms": 215.973,
|
||||
"speedup_vs_reference": 3387.8115,
|
||||
"share_of_suite_pct": 0.14
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "TSF",
|
||||
"fast_ms": 0.0413,
|
||||
"reference_ms": 46.3799,
|
||||
"speedup_vs_reference": 1122.1039,
|
||||
"share_of_suite_pct": 0.1
|
||||
"fast_ms": 0.0441,
|
||||
"reference_ms": 49.6108,
|
||||
"speedup_vs_reference": 1125.3954,
|
||||
"share_of_suite_pct": 0.09
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "LINEARREG",
|
||||
"fast_ms": 0.044,
|
||||
"reference_ms": 57.9675,
|
||||
"speedup_vs_reference": 1318.7009,
|
||||
"share_of_suite_pct": 0.09
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,4 +282,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "streaming",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:08:30.968886+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:04.351797+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"dataset": {
|
||||
"n_bars": 20000,
|
||||
@@ -22,52 +44,52 @@
|
||||
{
|
||||
"indicator": "StreamingSMA",
|
||||
"inputs": "close",
|
||||
"stream_total_ms": 0.8808,
|
||||
"batch_total_ms": 0.0153,
|
||||
"stream_ns_per_update": 44.04,
|
||||
"batch_ns_per_bar": 0.77,
|
||||
"updates_per_second": 22705779.59,
|
||||
"stream_over_batch_ratio": 57.4469
|
||||
"stream_total_ms": 0.9927,
|
||||
"batch_total_ms": 0.0166,
|
||||
"stream_ns_per_update": 49.63,
|
||||
"batch_ns_per_bar": 0.83,
|
||||
"updates_per_second": 20147763.43,
|
||||
"stream_over_batch_ratio": 59.7092
|
||||
},
|
||||
{
|
||||
"indicator": "StreamingEMA",
|
||||
"inputs": "close",
|
||||
"stream_total_ms": 0.8611,
|
||||
"batch_total_ms": 0.0408,
|
||||
"stream_ns_per_update": 43.06,
|
||||
"batch_ns_per_bar": 2.04,
|
||||
"updates_per_second": 23225431.81,
|
||||
"stream_over_batch_ratio": 21.0884
|
||||
"stream_total_ms": 0.9459,
|
||||
"batch_total_ms": 0.0435,
|
||||
"stream_ns_per_update": 47.3,
|
||||
"batch_ns_per_bar": 2.18,
|
||||
"updates_per_second": 21143503.9,
|
||||
"stream_over_batch_ratio": 21.7247
|
||||
},
|
||||
{
|
||||
"indicator": "StreamingRSI",
|
||||
"inputs": "close",
|
||||
"stream_total_ms": 0.9235,
|
||||
"batch_total_ms": 0.0932,
|
||||
"stream_ns_per_update": 46.17,
|
||||
"batch_ns_per_bar": 4.66,
|
||||
"updates_per_second": 21656740.75,
|
||||
"stream_over_batch_ratio": 9.9035
|
||||
"stream_total_ms": 1.0059,
|
||||
"batch_total_ms": 0.0991,
|
||||
"stream_ns_per_update": 50.3,
|
||||
"batch_ns_per_bar": 4.95,
|
||||
"updates_per_second": 19882356.06,
|
||||
"stream_over_batch_ratio": 10.1523
|
||||
},
|
||||
{
|
||||
"indicator": "StreamingATR",
|
||||
"inputs": "hlc",
|
||||
"stream_total_ms": 2.0074,
|
||||
"batch_total_ms": 0.0935,
|
||||
"stream_ns_per_update": 100.37,
|
||||
"batch_ns_per_bar": 4.68,
|
||||
"updates_per_second": 9963056.99,
|
||||
"stream_over_batch_ratio": 21.4603
|
||||
"stream_total_ms": 2.1574,
|
||||
"batch_total_ms": 0.0992,
|
||||
"stream_ns_per_update": 107.87,
|
||||
"batch_ns_per_bar": 4.96,
|
||||
"updates_per_second": 9270525.53,
|
||||
"stream_over_batch_ratio": 21.746
|
||||
},
|
||||
{
|
||||
"indicator": "StreamingVWAP",
|
||||
"inputs": "hlcv",
|
||||
"stream_total_ms": 2.6068,
|
||||
"batch_total_ms": 0.0223,
|
||||
"stream_ns_per_update": 130.34,
|
||||
"batch_ns_per_bar": 1.11,
|
||||
"updates_per_second": 7672265.38,
|
||||
"stream_over_batch_ratio": 116.9385
|
||||
"stream_total_ms": 2.6935,
|
||||
"batch_total_ms": 0.0252,
|
||||
"stream_ns_per_update": 134.68,
|
||||
"batch_ns_per_bar": 1.26,
|
||||
"updates_per_second": 7425170.07,
|
||||
"stream_over_batch_ratio": 106.8484
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ ignore = ["E501", "UP006", "UP045", "UP007"]
|
||||
"tests/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
||||
"tests/unit/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
||||
"tests/integration/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
||||
"benchmarks/*" = ["E402", "E741"]
|
||||
"python/ferro_ta/*.py" = ["N802"] # Public API: SMA, RSI, ATR, etc.
|
||||
"python/ferro_ta/__init__.py" = ["E402", "N802"] # Re-export surface by design
|
||||
"python/ferro_ta/__init__.pyi" = ["N802", "E402"]
|
||||
|
||||
@@ -158,7 +158,9 @@ def rsi_strategy(
|
||||
|
||||
c = np.asarray(close, dtype=np.float64)
|
||||
return np.asarray(
|
||||
_rust_rsi_threshold_signals(c, int(timeperiod), float(oversold), float(overbought)),
|
||||
_rust_rsi_threshold_signals(
|
||||
c, int(timeperiod), float(oversold), float(overbought)
|
||||
),
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
|
||||
@@ -173,7 +173,9 @@ def strategy_payoff(
|
||||
return np.zeros_like(grid)
|
||||
|
||||
try:
|
||||
return np.asarray(_rust_strategy_payoff_legs(grid, normalized), dtype=np.float64)
|
||||
return np.asarray(
|
||||
_rust_strategy_payoff_legs(grid, normalized), dtype=np.float64
|
||||
)
|
||||
except ValueError as err:
|
||||
_normalize_rust_error(err)
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ __all__ = [
|
||||
|
||||
|
||||
def _forward_fill_nan(arr: NDArray[np.float64]) -> NDArray[np.float64]:
|
||||
return np.asarray(_rust_forward_fill_nan(np.ascontiguousarray(arr, dtype=np.float64)))
|
||||
return np.asarray(
|
||||
_rust_forward_fill_nan(np.ascontiguousarray(arr, dtype=np.float64))
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -12,7 +12,7 @@ from collections.abc import Callable, Mapping
|
||||
from datetime import date, datetime, time
|
||||
from functools import lru_cache
|
||||
from itertools import count
|
||||
from typing import Any, get_args, get_origin, get_type_hints
|
||||
from typing import Any, cast, get_args, get_origin, get_type_hints
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -156,38 +156,42 @@ def _object_snapshot(value: Any) -> Any:
|
||||
if isinstance(value, enum.Enum):
|
||||
return value.value
|
||||
|
||||
if dataclasses.is_dataclass(value):
|
||||
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
||||
return _normalise_json(dataclasses.asdict(value), store_objects=False)
|
||||
|
||||
if hasattr(value, "to_dict") and callable(value.to_dict):
|
||||
dynamic_value = cast(Any, value)
|
||||
|
||||
if hasattr(dynamic_value, "to_dict") and callable(dynamic_value.to_dict):
|
||||
try:
|
||||
return _normalise_json(value.to_dict(), store_objects=False)
|
||||
return _normalise_json(dynamic_value.to_dict(), store_objects=False)
|
||||
except TypeError:
|
||||
if value.__class__.__module__.startswith("pandas"):
|
||||
return _normalise_json(value.to_dict(orient="list"), store_objects=False)
|
||||
if value.__class__.__module__.startswith("polars"):
|
||||
if dynamic_value.__class__.__module__.startswith("pandas"):
|
||||
return _normalise_json(
|
||||
value.to_dict(as_series=False), store_objects=False
|
||||
dynamic_value.to_dict(orient="list"), store_objects=False
|
||||
)
|
||||
if dynamic_value.__class__.__module__.startswith("polars"):
|
||||
return _normalise_json(
|
||||
dynamic_value.to_dict(as_series=False), store_objects=False
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if hasattr(value, "__dict__"):
|
||||
if hasattr(dynamic_value, "__dict__"):
|
||||
fields = {
|
||||
key: val
|
||||
for key, val in vars(value).items()
|
||||
for key, val in vars(dynamic_value).items()
|
||||
if not key.startswith("_") and not callable(val)
|
||||
}
|
||||
if fields:
|
||||
return _normalise_json(fields, store_objects=False)
|
||||
|
||||
slots = getattr(type(value), "__slots__", ())
|
||||
slots = getattr(type(dynamic_value), "__slots__", ())
|
||||
if slots:
|
||||
fields = {}
|
||||
for slot in slots:
|
||||
if slot.startswith("_") or not hasattr(value, slot):
|
||||
if slot.startswith("_") or not hasattr(dynamic_value, slot):
|
||||
continue
|
||||
slot_value = getattr(value, slot)
|
||||
slot_value = getattr(dynamic_value, slot)
|
||||
if callable(slot_value):
|
||||
continue
|
||||
fields[slot] = slot_value
|
||||
@@ -215,7 +219,10 @@ def _normalise_json(value: Any, *, store_objects: bool = True) -> Any:
|
||||
return _normalise_json(value.value, store_objects=store_objects)
|
||||
|
||||
if isinstance(value, np.ndarray):
|
||||
return [_normalise_json(item, store_objects=store_objects) for item in value.tolist()]
|
||||
return [
|
||||
_normalise_json(item, store_objects=store_objects)
|
||||
for item in value.tolist()
|
||||
]
|
||||
|
||||
if isinstance(value, np.generic):
|
||||
return _normalise_json(value.item(), store_objects=store_objects)
|
||||
@@ -408,7 +415,9 @@ def _annotation_includes_custom_class(annotation: Any) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _schema_and_py_type(annotation: Any, *, param_name: str) -> tuple[dict[str, Any], Any]:
|
||||
def _schema_and_py_type(
|
||||
annotation: Any, *, param_name: str
|
||||
) -> tuple[dict[str, Any], Any]:
|
||||
"""Map Python annotations to JSON Schema and wrapper annotations."""
|
||||
enum_type = _is_enum_annotation(annotation)
|
||||
if enum_type is not None:
|
||||
@@ -437,18 +446,25 @@ def _schema_and_py_type(annotation: Any, *, param_name: str) -> tuple[dict[str,
|
||||
if "scalarorarray" in lower:
|
||||
return {
|
||||
"type": _JSON_ANY_TYPE,
|
||||
"description": f"Parameter `{param_name}`. { _REFERENCE_HELP }",
|
||||
"description": f"Parameter `{param_name}`. {_REFERENCE_HELP}",
|
||||
}, Any
|
||||
if "ndarray" in lower or "arraylike" in lower:
|
||||
return {"type": "array", "items": {}}, list[Any]
|
||||
return {"type": "number"}, float
|
||||
if "list" in lower or "tuple" in lower or "sequence" in lower or "iterable" in lower:
|
||||
if (
|
||||
"list" in lower
|
||||
or "tuple" in lower
|
||||
or "sequence" in lower
|
||||
or "iterable" in lower
|
||||
):
|
||||
return {"type": "array", "items": {}}, list[Any]
|
||||
if "dict" in lower or "mapping" in lower:
|
||||
return {"type": "object"}, dict[str, Any]
|
||||
if "str" in lower or "date" in lower or "datetime" in lower or "time" in lower:
|
||||
return {"type": "string"}, str
|
||||
if _annotation_includes_callable(annotation) or _annotation_includes_custom_class(annotation):
|
||||
if _annotation_includes_callable(annotation) or _annotation_includes_custom_class(
|
||||
annotation
|
||||
):
|
||||
return {
|
||||
"type": _JSON_ANY_TYPE,
|
||||
"description": f"Parameter `{param_name}`. {_REFERENCE_HELP}",
|
||||
@@ -639,7 +655,10 @@ def _invoke_target(
|
||||
if not isinstance(extra_kwargs, dict):
|
||||
raise TypeError("kwargs must be a JSON object")
|
||||
keyword_args.update(
|
||||
{str(key): _decode_value(item, Any) for key, item in extra_kwargs.items()}
|
||||
{
|
||||
str(key): _decode_value(item, Any)
|
||||
for key, item in extra_kwargs.items()
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -689,8 +708,7 @@ def _describe_instance_payload(identifier: str) -> dict[str, Any]:
|
||||
def _list_instances_payload() -> list[dict[str, Any]]:
|
||||
"""Return current stored-object metadata."""
|
||||
return [
|
||||
_describe_instance_payload(identifier)
|
||||
for identifier in sorted(_INSTANCE_STORE)
|
||||
_describe_instance_payload(identifier) for identifier in sorted(_INSTANCE_STORE)
|
||||
]
|
||||
|
||||
|
||||
@@ -719,7 +737,9 @@ def _build_public_tool_spec(item: dict[str, str], target: Any) -> _ToolSpec:
|
||||
or f"Construct a {item['name']} enum member. Returns a stored instance reference."
|
||||
)
|
||||
|
||||
def invoke(arguments: dict[str, Any], *, enum_type: type[enum.Enum] = target) -> Any:
|
||||
def invoke_enum(
|
||||
arguments: dict[str, Any], *, enum_type: type[enum.Enum] = target
|
||||
) -> Any:
|
||||
if "value" not in arguments:
|
||||
raise KeyError("Missing required argument: value")
|
||||
member = _coerce_enum(arguments["value"], enum_type)
|
||||
@@ -730,7 +750,7 @@ def _build_public_tool_spec(item: dict[str, str], target: Any) -> _ToolSpec:
|
||||
description=description,
|
||||
input_schema=input_schema,
|
||||
wrapper_signature=wrapper_signature,
|
||||
invoke=invoke,
|
||||
invoke=invoke_enum,
|
||||
)
|
||||
|
||||
signature = inspect.signature(target)
|
||||
@@ -746,7 +766,7 @@ def _build_public_tool_spec(item: dict[str, str], target: Any) -> _ToolSpec:
|
||||
or f"Construct a {item['name']} instance. Returns a stored instance reference."
|
||||
)
|
||||
|
||||
def invoke(
|
||||
def invoke_target(
|
||||
arguments: dict[str, Any],
|
||||
*,
|
||||
raw_target: Any = target,
|
||||
@@ -770,7 +790,7 @@ def _build_public_tool_spec(item: dict[str, str], target: Any) -> _ToolSpec:
|
||||
description=description,
|
||||
input_schema=input_schema,
|
||||
wrapper_signature=wrapper_signature,
|
||||
invoke=invoke,
|
||||
invoke=invoke_target,
|
||||
)
|
||||
|
||||
|
||||
@@ -1136,7 +1156,9 @@ def _instance_management_specs() -> list[_ToolSpec]:
|
||||
raise ValueError("Only public methods can be called")
|
||||
method = getattr(value, method_name)
|
||||
if not callable(method):
|
||||
raise TypeError(f"{method_name!r} is not callable on {arguments['instance_id']!r}")
|
||||
raise TypeError(
|
||||
f"{method_name!r} is not callable on {arguments['instance_id']!r}"
|
||||
)
|
||||
args = [_decode_value(item, Any) for item in (arguments.get("args") or [])]
|
||||
kwargs = {
|
||||
str(key): _decode_value(item, Any)
|
||||
@@ -1247,7 +1269,7 @@ def _make_fastmcp_wrapper(spec: _ToolSpec) -> Callable[..., Any]:
|
||||
|
||||
wrapper.__name__ = f"tool_{_slugify(spec.name).replace('-', '_')}"
|
||||
wrapper.__doc__ = spec.description
|
||||
wrapper.__signature__ = spec.wrapper_signature
|
||||
setattr(wrapper, "__signature__", spec.wrapper_signature)
|
||||
wrapper.__annotations__ = {
|
||||
parameter.name: (
|
||||
Any if parameter.annotation is inspect._empty else parameter.annotation
|
||||
@@ -1311,7 +1333,9 @@ def create_server() -> Any:
|
||||
)
|
||||
|
||||
for spec in _tool_catalog().values():
|
||||
app.add_tool(_make_fastmcp_wrapper(spec), name=spec.name, description=spec.description)
|
||||
app.add_tool(
|
||||
_make_fastmcp_wrapper(spec), name=spec.name, description=spec.description
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@ def _load_api_info_module(root: Path, module_path: Path):
|
||||
python_root = str(root / "python")
|
||||
if python_root not in sys.path:
|
||||
sys.path.insert(0, python_root)
|
||||
spec = importlib.util.spec_from_file_location("ferro_ta_tools_api_info", module_path)
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"ferro_ta_tools_api_info", module_path
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load module spec from {module_path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
@@ -217,7 +219,9 @@ def _safe_git_head(root: Path) -> str | None:
|
||||
return value or None
|
||||
|
||||
|
||||
def build_manifest(root: Path, include_runtime_metadata: bool = False) -> dict[str, Any]:
|
||||
def build_manifest(
|
||||
root: Path, include_runtime_metadata: bool = False
|
||||
) -> dict[str, Any]:
|
||||
python_api = _extract_python_api(root)
|
||||
rust_core = _extract_core_exports(root)
|
||||
wasm_exports = _extract_wasm_exports(root)
|
||||
@@ -279,7 +283,9 @@ def main() -> None:
|
||||
output_path = (root / args.output).resolve()
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manifest = build_manifest(root, include_runtime_metadata=args.include_runtime_metadata)
|
||||
manifest = build_manifest(
|
||||
root, include_runtime_metadata=args.include_runtime_metadata
|
||||
)
|
||||
output_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"Wrote API manifest to {output_path}")
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ ensure_python_env() {
|
||||
fi
|
||||
|
||||
need_cmd uv
|
||||
run_cmd uv sync --extra dev --extra docs
|
||||
run_cmd uv run --extra dev --extra docs maturin develop --release
|
||||
run_cmd uv sync --extra dev --extra docs --extra mcp
|
||||
run_cmd uv run --extra dev --extra docs --extra mcp maturin develop --release
|
||||
python_env_ready=1
|
||||
}
|
||||
|
||||
@@ -110,13 +110,13 @@ run_python_lint() {
|
||||
|
||||
run_python_typecheck() {
|
||||
need_cmd uv
|
||||
run_cmd uv run --with mypy --with numpy mypy python/ferro_ta --ignore-missing-imports --no-error-summary
|
||||
run_cmd uv run --with pyright pyright python/ferro_ta
|
||||
run_cmd uv run --with mypy --with numpy python -m mypy python/ferro_ta --ignore-missing-imports --no-error-summary
|
||||
run_cmd uv run --with pyright python -m pyright python/ferro_ta
|
||||
}
|
||||
|
||||
run_python_test() {
|
||||
ensure_python_env
|
||||
run_cmd uv run --extra dev --with pytest-cov pytest tests/unit/ tests/integration/ -v --cov=ferro_ta --cov-report=term-missing --cov-fail-under=65
|
||||
run_cmd uv run --extra dev --extra mcp --with pytest-cov pytest tests/unit/ tests/integration/ -v --cov=ferro_ta --cov-report=term-missing --cov-fail-under=65
|
||||
}
|
||||
|
||||
run_docs() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPTS = ROOT / "scripts"
|
||||
|
||||
@@ -50,7 +50,9 @@ def _run_node_conformance() -> dict[str, list[float | None]]:
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node is required for wasm/node conformance test")
|
||||
if not PKG_JS.exists():
|
||||
pytest.skip("wasm/pkg not found; run `wasm-pack build --target nodejs --out-dir pkg`")
|
||||
pytest.skip(
|
||||
"wasm/pkg not found; run `wasm-pack build --target nodejs --out-dir pkg`"
|
||||
)
|
||||
|
||||
_write_node_conformance_script(SCRIPT)
|
||||
try:
|
||||
@@ -78,7 +80,9 @@ def _assert_close_with_null_nan(
|
||||
) -> None:
|
||||
assert len(actual) == len(expected)
|
||||
a = np.array([np.nan if v is None else float(v) for v in actual], dtype=np.float64)
|
||||
e = np.array([np.nan if v is None else float(v) for v in expected], dtype=np.float64)
|
||||
e = np.array(
|
||||
[np.nan if v is None else float(v) for v in expected], dtype=np.float64
|
||||
)
|
||||
np.testing.assert_allclose(a, e, atol=atol, rtol=0.0, equal_nan=True)
|
||||
|
||||
|
||||
|
||||
@@ -1403,9 +1403,9 @@ class TestMCPCallTool:
|
||||
instance_id = created["instance_id"]
|
||||
|
||||
described = json.loads(
|
||||
handle_call_tool(
|
||||
"describe_instance", {"instance_id": instance_id}
|
||||
)["content"][0]["text"]
|
||||
handle_call_tool("describe_instance", {"instance_id": instance_id})[
|
||||
"content"
|
||||
][0]["text"]
|
||||
)
|
||||
method_names = [item["name"] for item in described["methods"]]
|
||||
assert "aggregate" in method_names
|
||||
@@ -1429,9 +1429,9 @@ class TestMCPCallTool:
|
||||
assert "close" in aggregated
|
||||
|
||||
deleted = json.loads(
|
||||
handle_call_tool(
|
||||
"delete_instance", {"instance_id": instance_id}
|
||||
)["content"][0]["text"]
|
||||
handle_call_tool("delete_instance", {"instance_id": instance_id})[
|
||||
"content"
|
||||
][0]["text"]
|
||||
)
|
||||
assert deleted["deleted"] is True
|
||||
|
||||
@@ -1441,9 +1441,9 @@ class TestMCPCallTool:
|
||||
from ferro_ta.mcp import handle_call_tool
|
||||
|
||||
wrapped = json.loads(
|
||||
handle_call_tool(
|
||||
"traced", {"func": {"callable": "SMA"}}
|
||||
)["content"][0]["text"]
|
||||
handle_call_tool("traced", {"func": {"callable": "SMA"}})["content"][0][
|
||||
"text"
|
||||
]
|
||||
)
|
||||
instance_id = wrapped["instance_id"]
|
||||
|
||||
|
||||
@@ -2315,11 +2315,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.11.0"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
Reference in New Issue
Block a user