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
|
run: pip install uv
|
||||||
|
|
||||||
- name: Run mypy on ferro_ta via 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
|
- 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:
|
test:
|
||||||
name: Test (ubuntu-latest / Python ${{ matrix.python-version }})
|
name: Test (ubuntu-latest / Python ${{ matrix.python-version }})
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install maturin and test dependencies
|
- name: Install maturin and test dependencies
|
||||||
run: |
|
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)
|
- name: Build and install ferro_ta (dev mode)
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ env/
|
|||||||
# WASM build output
|
# WASM build output
|
||||||
wasm/pkg/
|
wasm/pkg/
|
||||||
wasm/pkg-web/
|
wasm/pkg-web/
|
||||||
|
benchmark_vs_talib.json
|
||||||
|
wasm_benchmark.json
|
||||||
|
.wasm_benchmark.prepush.json
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ default_language_version:
|
|||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.8.0
|
rev: v0.15.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--fix]
|
args: [--fix]
|
||||||
@@ -18,6 +18,7 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
exclude: ^conda/meta\.yaml$
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
args: [--maxkb=1000]
|
args: [--maxkb=1000]
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
|
|||||||
@@ -70,23 +70,23 @@ rustflags = ["-C", "target-cpu=native"]
|
|||||||
### Phase 3: Algorithm-Level Optimizations (Target: 5-10x improvement)
|
### Phase 3: Algorithm-Level Optimizations (Target: 5-10x improvement)
|
||||||
|
|
||||||
#### SMA — O(n) running sum
|
#### 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).
|
Target: single-pass running sum (already done in Rust — verify SIMD path is hit).
|
||||||
|
|
||||||
#### BBANDS — Welford's algorithm
|
#### 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.
|
Target: Welford's online algorithm — single pass, better cache utilization.
|
||||||
|
|
||||||
#### ATR/ADX — Avoid redundant True Range calculations
|
#### 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.
|
Target: Compute TR once, share with ATR, NATR, +DI, -DI, ADX in a single pass.
|
||||||
|
|
||||||
#### MACD — Reuse EMA computations
|
#### 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.
|
Target: Single function computes both EMAs in one pass.
|
||||||
|
|
||||||
#### Candlestick Patterns — Batch lookup table
|
#### 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.
|
Target: Pre-compute body/shadow ratios, vectorized pattern matching.
|
||||||
|
|
||||||
### Phase 4: Streaming Precomputation (Target: 100x for incremental updates)
|
### 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
|
> `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.
|
> at the beginning of each output array for the warmup period.
|
||||||
|
|
||||||
|
|||||||
+18
-18
@@ -77,7 +77,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
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."""
|
"""Convert numpy array to list, replacing NaN/Inf with None."""
|
||||||
return [None if not math.isfinite(v) else float(v) for v in arr]
|
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:
|
if len(close) > MAX_SERIES_LENGTH:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=413,
|
status_code=413,
|
||||||
@@ -142,54 +142,54 @@ def _validate_series(close: List[float]) -> np.ndarray:
|
|||||||
|
|
||||||
|
|
||||||
class IndicatorRequest(BaseModel):
|
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")
|
timeperiod: int = Field(default=14, ge=1, description="Look-back period")
|
||||||
|
|
||||||
@field_validator("close")
|
@field_validator("close")
|
||||||
@classmethod
|
@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):
|
if not all(math.isfinite(x) for x in v):
|
||||||
raise ValueError("close series must contain only finite values")
|
raise ValueError("close series must contain only finite values")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class MACDRequest(BaseModel):
|
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)
|
fastperiod: int = Field(default=12, ge=1)
|
||||||
slowperiod: int = Field(default=26, ge=1)
|
slowperiod: int = Field(default=26, ge=1)
|
||||||
signalperiod: int = Field(default=9, ge=1)
|
signalperiod: int = Field(default=9, ge=1)
|
||||||
|
|
||||||
@field_validator("close")
|
@field_validator("close")
|
||||||
@classmethod
|
@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):
|
if not all(math.isfinite(x) for x in v):
|
||||||
raise ValueError("close series must contain only finite values")
|
raise ValueError("close series must contain only finite values")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class BBANDSRequest(BaseModel):
|
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)
|
timeperiod: int = Field(default=5, ge=2)
|
||||||
nbdevup: float = Field(default=2.0, gt=0)
|
nbdevup: float = Field(default=2.0, gt=0)
|
||||||
nbdevdn: float = Field(default=2.0, gt=0)
|
nbdevdn: float = Field(default=2.0, gt=0)
|
||||||
|
|
||||||
@field_validator("close")
|
@field_validator("close")
|
||||||
@classmethod
|
@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):
|
if not all(math.isfinite(x) for x in v):
|
||||||
raise ValueError("close series must contain only finite values")
|
raise ValueError("close series must contain only finite values")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class BacktestRequest(BaseModel):
|
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")
|
strategy: str = Field(default="rsi_30_70")
|
||||||
commission_per_trade: float = Field(default=0.0, ge=0.0)
|
commission_per_trade: float = Field(default=0.0, ge=0.0)
|
||||||
slippage_bps: float = Field(default=0.0, ge=0.0)
|
slippage_bps: float = Field(default=0.0, ge=0.0)
|
||||||
|
|
||||||
@field_validator("close")
|
@field_validator("close")
|
||||||
@classmethod
|
@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):
|
if not all(math.isfinite(x) for x in v):
|
||||||
raise ValueError("close series must contain only finite values")
|
raise ValueError("close series must contain only finite values")
|
||||||
return v
|
return v
|
||||||
@@ -201,13 +201,13 @@ class BacktestRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/health", summary="Health check")
|
@app.get("/health", summary="Health check")
|
||||||
def health() -> Dict[str, str]:
|
def health() -> dict[str, str]:
|
||||||
"""Readiness / liveness probe."""
|
"""Readiness / liveness probe."""
|
||||||
return {"status": "ok", "version": app.version}
|
return {"status": "ok", "version": app.version}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/indicators/sma", summary="Simple Moving Average")
|
@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).
|
"""Compute Simple Moving Average (SMA).
|
||||||
|
|
||||||
Returns ``result``: list of floats (null for warm-up bars).
|
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")
|
@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)."""
|
"""Compute Exponential Moving Average (EMA)."""
|
||||||
c = _validate_series(req.close)
|
c = _validate_series(req.close)
|
||||||
out = np.asarray(ft.EMA(c, timeperiod=req.timeperiod), dtype=np.float64)
|
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")
|
@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)."""
|
"""Compute Relative Strength Index (RSI)."""
|
||||||
c = _validate_series(req.close)
|
c = _validate_series(req.close)
|
||||||
out = np.asarray(ft.RSI(c, timeperiod=req.timeperiod), dtype=np.float64)
|
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")
|
@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).
|
"""Compute MACD (line, signal, histogram).
|
||||||
|
|
||||||
Returns ``result`` with keys ``macd``, ``signal``, ``hist``.
|
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")
|
@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).
|
"""Compute Bollinger Bands (upper, middle, lower).
|
||||||
|
|
||||||
Returns ``result`` with keys ``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")
|
@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.
|
"""Run a vectorized backtest using a named strategy.
|
||||||
|
|
||||||
Strategies: ``rsi_30_70``, ``sma_crossover``, ``macd_crossover``.
|
Strategies: ``rsi_30_70``, ``sma_crossover``, ``macd_crossover``.
|
||||||
|
|||||||
@@ -68,4 +68,4 @@
|
|||||||
"speedup_vs_separate": 2.2811
|
"speedup_vs_separate": 2.2811
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1159,4 +1159,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1379,4 +1379,4 @@
|
|||||||
"outcome": "ferro_ta_win"
|
"outcome": "ferro_ta_win"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,4 +160,4 @@
|
|||||||
"elapsed_ms": 0.0026
|
"elapsed_ms": 0.0026
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,4 +59,4 @@
|
|||||||
"sha256": "f31fd871990c44e24a2259d618ae40a52866d20b95aa6047af3d38b9371c2ab7"
|
"sha256": "f31fd871990c44e24a2259d618ae40a52866d20b95aa6047af3d38b9371c2ab7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,4 +93,4 @@
|
|||||||
"share_of_suite_pct": 0.09
|
"share_of_suite_pct": 0.09
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,4 +282,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,4 +70,4 @@
|
|||||||
"stream_over_batch_ratio": 121.7603
|
"stream_over_batch_ratio": 121.7603
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,13 +48,17 @@ def run_batch_benchmark(
|
|||||||
"SMA",
|
"SMA",
|
||||||
lambda: ferro_ta.batch.batch_sma(close2d, timeperiod=14, parallel=True),
|
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.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",
|
"RSI",
|
||||||
lambda: ferro_ta.batch.batch_rsi(close2d, timeperiod=14, parallel=True),
|
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.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",
|
"ATR",
|
||||||
@@ -201,7 +205,9 @@ def main() -> int:
|
|||||||
if payload["grouped_results"]:
|
if payload["grouped_results"]:
|
||||||
print("\nGrouped Multi-Indicator Calls")
|
print("\nGrouped Multi-Indicator Calls")
|
||||||
print("-" * 64)
|
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)
|
print("-" * 64)
|
||||||
for row in payload["grouped_results"]:
|
for row in payload["grouped_results"]:
|
||||||
print(
|
print(
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ import math
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import tracemalloc
|
import tracemalloc
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|||||||
@@ -70,9 +70,7 @@ def run_simd_benchmark(
|
|||||||
for label, args in variants
|
for label, args in variants
|
||||||
}
|
}
|
||||||
|
|
||||||
portable_rows = {
|
portable_rows = {row["name"]: row for row in reports["portable_release"]["results"]}
|
||||||
row["name"]: row for row in reports["portable_release"]["results"]
|
|
||||||
}
|
|
||||||
simd_rows = {row["name"]: row for row in reports["simd_release"]["results"]}
|
simd_rows = {row["name"]: row for row in reports["simd_release"]["results"]}
|
||||||
|
|
||||||
comparison: list[dict[str, Any]] = []
|
comparison: list[dict[str, Any]] = []
|
||||||
@@ -136,9 +134,7 @@ def main() -> int:
|
|||||||
window=args.window,
|
window=args.window,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(
|
print(f"{'Case':<20} {'Portable (ms)':>14} {'SIMD (ms)':>12} {'SIMD speedup':>14}")
|
||||||
f"{'Case':<20} {'Portable (ms)':>14} {'SIMD (ms)':>12} {'SIMD speedup':>14}"
|
|
||||||
)
|
|
||||||
print("-" * 64)
|
print("-" * 64)
|
||||||
for row in payload["results"]:
|
for row in payload["results"]:
|
||||||
print(
|
print(
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ def _stream_hlcv(
|
|||||||
) -> float:
|
) -> float:
|
||||||
streamer = factory()
|
streamer = factory()
|
||||||
last = np.nan
|
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(
|
last = streamer.update(
|
||||||
float(high_value),
|
float(high_value),
|
||||||
float(low_value),
|
float(low_value),
|
||||||
@@ -154,7 +156,9 @@ def run_streaming_benchmark(
|
|||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
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("--bars", type=int, default=100_000)
|
||||||
parser.add_argument("--seed", type=int, default=2026)
|
parser.add_argument("--seed", type=int, default=2026)
|
||||||
parser.add_argument("--json", dest="json_path")
|
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:
|
if not TALIB_AVAILABLE:
|
||||||
print("Note: ta-lib not installed. Reporting ferro_ta timings only.")
|
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(f"Sizes: {sizes}")
|
||||||
print()
|
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:
|
if size == 1_000_000 and name in SKIP_1M_FOR:
|
||||||
continue
|
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_stats = _summary_stats(ft_samples_ms)
|
||||||
ft_median_ms = float(ft_stats["median_ms"])
|
ft_median_ms = float(ft_stats["median_ms"])
|
||||||
ft_m_bars_s = _throughput_m_bars_s(size, ft_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] = {
|
row: dict[str, Any] = {
|
||||||
"indicator": name,
|
"indicator": name,
|
||||||
@@ -351,11 +359,15 @@ def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, An
|
|||||||
}
|
}
|
||||||
|
|
||||||
if TALIB_AVAILABLE:
|
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_stats = _summary_stats(ta_samples_ms)
|
||||||
ta_median_ms = float(ta_stats["median_ms"])
|
ta_median_ms = float(ta_stats["median_ms"])
|
||||||
ta_m_bars_s = _throughput_m_bars_s(size, ta_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)
|
outcome = _outcome(speedup)
|
||||||
ta_peak_bytes = _python_peak_bytes(
|
ta_peak_bytes = _python_peak_bytes(
|
||||||
ta_run, open_, high, low, close, volume, size
|
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
|
Unsupported (indicator, library) pairs show N/A. Supported pairs missing benchmark
|
||||||
data show ERR (indicating the benchmark run was incomplete or failed).
|
data show ERR (indicating the benchmark run was incomplete or failed).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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 (
|
from benchmarks.wrapper_registry import (
|
||||||
INDICATOR_CATEGORIES,
|
INDICATOR_CATEGORIES,
|
||||||
LIBRARY_NAMES as LIBS,
|
|
||||||
is_supported,
|
is_supported,
|
||||||
)
|
)
|
||||||
|
from benchmarks.wrapper_registry import (
|
||||||
|
LIBRARY_NAMES as LIBS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _all_indicators() -> list[str]:
|
def _all_indicators() -> list[str]:
|
||||||
@@ -34,11 +38,17 @@ def _all_indicators() -> list[str]:
|
|||||||
def main():
|
def main():
|
||||||
p = Path(__file__).parent / "results.json"
|
p = Path(__file__).parent / "results.json"
|
||||||
if not p.exists():
|
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)
|
sys.exit(1)
|
||||||
raw = p.read_text().strip()
|
raw = p.read_text().strip()
|
||||||
if not raw:
|
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)
|
sys.exit(1)
|
||||||
try:
|
try:
|
||||||
data = json.loads(raw)
|
data = json.loads(raw)
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ def main() -> int:
|
|||||||
|
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
if not data.get("talib_available", False):
|
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
|
return 1
|
||||||
|
|
||||||
summary_by_size = {
|
summary_by_size = {
|
||||||
@@ -133,9 +135,7 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if rows < args.min_rows:
|
if rows < args.min_rows:
|
||||||
failures.append(
|
failures.append(f"size={size} rows {rows} < min_rows {args.min_rows}")
|
||||||
f"size={size} rows {rows} < min_rows {args.min_rows}"
|
|
||||||
)
|
|
||||||
if med < median_floor.get(size, float("-inf")):
|
if med < median_floor.get(size, float("-inf")):
|
||||||
failures.append(
|
failures.append(
|
||||||
f"size={size} median_speedup {med:.4f} < floor {median_floor[size]:.4f}"
|
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)):
|
for end in range(window, len(x)):
|
||||||
start = end - window
|
start = end - window
|
||||||
rx = np.array(
|
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,
|
dtype=np.float64,
|
||||||
)
|
)
|
||||||
ry = np.array(
|
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,
|
dtype=np.float64,
|
||||||
)
|
)
|
||||||
mean_x = float(np.sum(rx)) / window
|
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)
|
high = close + rng.uniform(0.1, 2.0, price_bars)
|
||||||
low = 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)
|
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 = [
|
rows = [
|
||||||
(
|
(
|
||||||
@@ -218,7 +229,9 @@ def build_hotspot_report(
|
|||||||
results.sort(key=lambda row: row["fast_ms"], reverse=True)
|
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
|
total_fast_ms = sum(float(row["fast_ms"]) for row in results) or 1.0
|
||||||
for row in results:
|
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 {
|
return {
|
||||||
"metadata": benchmark_metadata(
|
"metadata": benchmark_metadata(
|
||||||
@@ -249,7 +262,9 @@ def main() -> int:
|
|||||||
window=args.window,
|
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)
|
print("-" * 70)
|
||||||
for row in payload["results"]:
|
for row in payload["results"]:
|
||||||
print(
|
print(
|
||||||
|
|||||||
@@ -16094,4 +16094,4 @@
|
|||||||
],
|
],
|
||||||
"datetime": "2026-03-23T17:14:05.427766+00:00",
|
"datetime": "2026-03-23T17:14:05.427766+00:00",
|
||||||
"version": "5.2.3"
|
"version": "5.2.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ def build_indicator_latency_report(*, rounds: int = 5) -> dict[str, Any]:
|
|||||||
|
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
for entry in INDICATOR_SUITE:
|
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(
|
rows.append(
|
||||||
{
|
{
|
||||||
"name": entry["name"],
|
"name": entry["name"],
|
||||||
@@ -192,10 +194,7 @@ def main() -> int:
|
|||||||
fixtures=[FIXTURE_PATH],
|
fixtures=[FIXTURE_PATH],
|
||||||
extra={"output_dir": str(output_dir)},
|
extra={"output_dir": str(output_dir)},
|
||||||
),
|
),
|
||||||
"artifacts": {
|
"artifacts": {name: file_info(path) for name, path in artifacts.items()},
|
||||||
name: file_info(path)
|
|
||||||
for name, path in artifacts.items()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
manifest_path = output_dir / "manifest.json"
|
manifest_path = output_dir / "manifest.json"
|
||||||
_write_json(manifest_path, manifest)
|
_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).
|
Tolerances are based on known algorithmic differences (e.g. Wilder vs SMA seed).
|
||||||
We only compare the overlapping (valid) suffix of each output array.
|
We only compare the overlapping (valid) suffix of each output array.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from benchmarks.data_generator import MEDIUM
|
from benchmarks.data_generator import MEDIUM
|
||||||
from benchmarks.wrapper_registry import (
|
from benchmarks.wrapper_registry import (
|
||||||
execute_indicator,
|
|
||||||
INDICATOR_NAMES,
|
|
||||||
INDICATOR_CATEGORIES,
|
|
||||||
CUMULATIVE_INDICATORS,
|
|
||||||
BINARY_INDICATORS,
|
BINARY_INDICATORS,
|
||||||
|
CUMULATIVE_INDICATORS,
|
||||||
|
INDICATOR_CATEGORIES,
|
||||||
|
INDICATOR_NAMES,
|
||||||
available_libraries,
|
available_libraries,
|
||||||
|
execute_indicator,
|
||||||
is_supported,
|
is_supported,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reference = ferro_ta; compare against each library that has a non-empty result.
|
# Reference = ferro_ta; compare against each library that has a non-empty result.
|
||||||
REFERENCE_LIB = "ferro_ta"
|
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)
|
# Per-indicator tolerances (rtol, atol)
|
||||||
_TOLERANCES: dict[str, tuple[float, float]] = {
|
_TOLERANCES: dict[str, tuple[float, float]] = {
|
||||||
"ATR": (1e-3, 0.05), # Wilder's smoothing seed differs
|
"ATR": (1e-3, 0.05), # Wilder's smoothing seed differs
|
||||||
"NATR": (1e-3, 0.10),
|
"NATR": (1e-3, 0.10),
|
||||||
"BBANDS": (1e-3, 0.20), # ddof=0 vs ddof=1
|
"BBANDS": (1e-3, 0.20), # ddof=0 vs ddof=1
|
||||||
"STDDEV": (1e-3, 0.20),
|
"STDDEV": (1e-3, 0.20),
|
||||||
"VAR": (1e-3, 0.50),
|
"VAR": (1e-3, 0.50),
|
||||||
"MACD": (1e-3, 1e-3), # double EMA seed
|
"MACD": (1e-3, 1.00), # seed differences across libraries
|
||||||
"KAMA": (1e-3, 1e-3),
|
"KAMA": (1e-3, 1e-3),
|
||||||
"STOCH": (1e-3, 0.10), # smoothing method differences
|
"STOCH": (1e-3, 0.10), # smoothing method differences
|
||||||
"SAR": (1e-3, 0.20),
|
"SAR": (1e-3, 0.20),
|
||||||
"ADOSC": (1e-3, 0.20),
|
"ADOSC": (1e-3, 0.20),
|
||||||
"ADX": (1e-3, 0.50), # Wilder's ADX
|
"ADX": (1e-3, 0.50), # Wilder's ADX
|
||||||
"PLUS_DI":(1e-3, 0.50),
|
"PLUS_DI": (1e-3, 0.50),
|
||||||
"MINUS_DI":(1e-3, 0.50),
|
"MINUS_DI": (1e-3, 0.50),
|
||||||
"PPO": (1e-2, 1e-3),
|
"PPO": (1e-2, 1e-3),
|
||||||
"CMO": (1e-3, 0.10),
|
"CMO": (1e-3, 0.10),
|
||||||
"TRIX": (1e-3, 1e-3),
|
"TRIX": (1e-3, 0.05),
|
||||||
"CCI": (1e-3, 0.10),
|
"CCI": (1e-3, 0.10),
|
||||||
"SUPERTREND": (1e-2, 0.50),
|
"SUPERTREND": (1e-2, 0.50),
|
||||||
"KELTNER_CHANNELS": (1e-2, 0.50),
|
"KELTNER_CHANNELS": (1e-2, 0.50),
|
||||||
"DONCHIAN": (1e-4, 1e-4),
|
"DONCHIAN": (1e-4, 1e-4),
|
||||||
"HT_DCPERIOD": (1e-2, 1.0),
|
"HT_DCPERIOD": (1e-2, 2.0),
|
||||||
"VWAP": (1e-3, 0.10),
|
"VWAP": (1e-3, 0.10),
|
||||||
"AROON": (1e-4, 1e-3),
|
"AROON": (1e-4, 1e-3),
|
||||||
"LINEARREG": (1e-4, 1e-4),
|
"LINEARREG": (1e-4, 1e-4),
|
||||||
"LINEARREG_SLOPE": (1e-4, 1e-4),
|
"LINEARREG_SLOPE": (1e-4, 1e-4),
|
||||||
"CORREL": (1e-4, 1e-3),
|
"CORREL": (1e-4, 1e-3),
|
||||||
"BETA": (1e-3, 1e-3),
|
"BETA": (1e-3, 1e-3),
|
||||||
"TSF": (1e-4, 1e-4),
|
"TSF": (1e-4, 1e-4),
|
||||||
"EMA": (1e-3, 0.30), # ta library uses different EMA seed
|
"EMA": (1e-3, 0.30), # ta library uses different EMA seed
|
||||||
"DEMA": (1e-3, 0.50),
|
"DEMA": (1e-3, 0.50),
|
||||||
"TEMA": (1e-3, 0.50),
|
"TEMA": (1e-3, 0.50),
|
||||||
"T3": (1e-3, 0.50),
|
"T3": (1e-3, 0.50),
|
||||||
"HULL_MA":(1e-3, 0.10),
|
"HULL_MA": (1e-3, 0.10),
|
||||||
"WMA": (1e-4, 1e-4),
|
"WMA": (1e-4, 1e-4),
|
||||||
"TRIMA": (1e-4, 1e-4),
|
"TRIMA": (1e-4, 1e-4),
|
||||||
"MACD": (1e-3, 1.00), # seed differences across libraries
|
|
||||||
"TRIX": (1e-3, 0.05),
|
|
||||||
"HT_DCPERIOD": (1e-2, 2.0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_DEFAULT_TOL = (1e-4, 1e-5)
|
_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
|
# Pairs that use correlation check (>=0.95) due to known algorithmic divergence
|
||||||
# Format: (indicator, library) or just indicator (applies to all libs)
|
# Format: (indicator, library) or just indicator (applies to all libs)
|
||||||
_CORRELATION_PAIRS: set[tuple[str, str]] = {
|
_CORRELATION_PAIRS: set[tuple[str, str]] = {
|
||||||
("PPO", "talib"), # different PPO formula normalization
|
("PPO", "talib"), # different PPO formula normalization
|
||||||
("PPO", "pandas_ta"),
|
("PPO", "pandas_ta"),
|
||||||
("PPO", "tulipy"),
|
("PPO", "tulipy"),
|
||||||
("STOCH", "ta"),
|
("STOCH", "ta"),
|
||||||
("SUPERTREND", "pandas_ta"),
|
("SUPERTREND", "pandas_ta"),
|
||||||
("KELTNER_CHANNELS", "pandas_ta"),
|
("KELTNER_CHANNELS", "pandas_ta"),
|
||||||
("KELTNER_CHANNELS", "ta"),
|
("KELTNER_CHANNELS", "ta"),
|
||||||
("EMA", "finta"), # finta EMA uses different initialization
|
("EMA", "finta"), # finta EMA uses different initialization
|
||||||
("KAMA", "pandas_ta"), # pandas_ta KAMA has slightly different seed
|
("KAMA", "pandas_ta"), # pandas_ta KAMA has slightly different seed
|
||||||
("RSI", "ta"), # ta uses SMA warmup vs Wilder
|
("RSI", "ta"), # ta uses SMA warmup vs Wilder
|
||||||
("RSI", "finta"), # same
|
("RSI", "finta"), # same
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pairs that are skipped because they are structurally incompatible
|
# Pairs that are skipped because they are structurally incompatible
|
||||||
_SKIP_PAIRS: set[tuple[str, str]] = {
|
_SKIP_PAIRS: set[tuple[str, str]] = {
|
||||||
("BBANDS", "finta"), # finta normalizes band differently
|
("BBANDS", "finta"), # finta normalizes band differently
|
||||||
("ATR", "finta"), # finta ATR uses simple TR not Wilder
|
("ATR", "finta"), # finta ATR uses simple TR not Wilder
|
||||||
("STDDEV", "finta"), # finta uses population std
|
("STDDEV", "finta"), # finta uses population std
|
||||||
("TRIMA", "finta"), # finta TRIMA uses different formula
|
("TRIMA", "finta"), # finta TRIMA uses different formula
|
||||||
("PPO", "finta"), # finta PPO scaling incompatible
|
("PPO", "finta"), # finta PPO scaling incompatible
|
||||||
("STOCH", "finta"), # finta STOCH formula differs
|
("STOCH", "finta"), # finta STOCH formula differs
|
||||||
("VWAP", "pandas_ta"), # pandas_ta VWAP anchors to session start
|
("VWAP", "pandas_ta"), # pandas_ta VWAP anchors to session start
|
||||||
("HT_TRENDMODE", "talib"), # binary; Hilbert seed diverges
|
("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", "pandas_ta"),
|
||||||
("CMO", "finta"),
|
("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
|
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:]
|
c = cmp[-n:]
|
||||||
if indicator in BINARY_INDICATORS or (indicator, library) in _CORRELATION_PAIRS:
|
if indicator in BINARY_INDICATORS or (indicator, library) in _CORRELATION_PAIRS:
|
||||||
# Use correlation check for structurally different algorithms
|
# 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:
|
if indicator in BINARY_INDICATORS:
|
||||||
agree = np.mean(r == c)
|
agree = np.mean(r == c)
|
||||||
assert agree >= 0.80, f"Binary agreement {agree:.1%} < 80%"
|
assert agree >= 0.80, f"Binary agreement {agree:.1%} < 80%"
|
||||||
else:
|
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:
|
elif indicator in CUMULATIVE_INDICATORS:
|
||||||
dr, dc = np.diff(r), np.diff(c)
|
dr, dc = np.diff(r), np.diff(c)
|
||||||
if len(dr) < 5 or len(dc) < 5:
|
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 ─────────────
|
# ── dynamically generate one test per (indicator, library) pair ─────────────
|
||||||
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
if "indicator" in metafunc.fixturenames and "library" in metafunc.fixturenames:
|
if "indicator" in metafunc.fixturenames and "library" in metafunc.fixturenames:
|
||||||
params = []
|
params = []
|
||||||
@@ -172,6 +176,7 @@ class TestAccuracy:
|
|||||||
|
|
||||||
# ── quick smoke tests that always run (no skip) ──────────────────────────────
|
# ── quick smoke tests that always run (no skip) ──────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
class TestSmoke:
|
class TestSmoke:
|
||||||
"""Sanity checks that ferro_ta returns non-empty finite arrays."""
|
"""Sanity checks that ferro_ta returns non-empty finite arrays."""
|
||||||
|
|
||||||
@@ -182,7 +187,9 @@ class TestSmoke:
|
|||||||
|
|
||||||
arr = execute_indicator("ferro_ta", indicator, MEDIUM)
|
arr = execute_indicator("ferro_ta", indicator, MEDIUM)
|
||||||
assert len(arr) > 0, f"ferro_ta {indicator} returned empty array"
|
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())
|
@pytest.mark.parametrize("category,indicators", INDICATOR_CATEGORIES.items())
|
||||||
def test_category_coverage(self, category, indicators):
|
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
|
Streaming benchmarks are in test_streaming_speed.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from benchmarks.data_generator import LARGE
|
from benchmarks.data_generator import LARGE
|
||||||
from benchmarks.wrapper_registry import (
|
from benchmarks.wrapper_registry import (
|
||||||
execute_indicator,
|
|
||||||
INDICATOR_CATEGORIES,
|
INDICATOR_CATEGORIES,
|
||||||
available_libraries,
|
available_libraries,
|
||||||
|
execute_indicator,
|
||||||
is_supported,
|
is_supported,
|
||||||
)
|
)
|
||||||
|
|
||||||
BENCH_DATA = LARGE # 100k bars for main benchmarks
|
BENCH_DATA = LARGE # 100k bars for main benchmarks
|
||||||
BENCH_LIBS = available_libraries()
|
BENCH_LIBS = available_libraries()
|
||||||
|
|
||||||
|
|
||||||
def _make_bench(indicator: str, library: str):
|
def _make_bench(indicator: str, library: str):
|
||||||
"""Return a benchmark function that runs indicator on library (uses BENCH_DATA)."""
|
"""Return a benchmark function that runs indicator on library (uses BENCH_DATA)."""
|
||||||
|
|
||||||
def _fn():
|
def _fn():
|
||||||
execute_indicator(library, indicator, BENCH_DATA)
|
execute_indicator(library, indicator, BENCH_DATA)
|
||||||
|
|
||||||
_fn.__name__ = f"{library}_{indicator}"
|
_fn.__name__ = f"{library}_{indicator}"
|
||||||
return _fn
|
return _fn
|
||||||
|
|
||||||
|
|
||||||
# ── Parametrize over all (indicator, library) combinations ───────────────────
|
# ── Parametrize over all (indicator, library) combinations ───────────────────
|
||||||
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
if "indicator" in metafunc.fixturenames and "library" in metafunc.fixturenames:
|
if "indicator" in metafunc.fixturenames and "library" in metafunc.fixturenames:
|
||||||
params = []
|
params = []
|
||||||
@@ -53,20 +58,24 @@ class TestSpeed:
|
|||||||
|
|
||||||
# ── Standalone head-to-head for the most important indicators ─────────────────
|
# ── Standalone head-to-head for the most important indicators ─────────────────
|
||||||
|
|
||||||
@pytest.mark.parametrize("indicator,libs", [
|
|
||||||
("SMA", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
@pytest.mark.parametrize(
|
||||||
("EMA", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
"indicator,libs",
|
||||||
("RSI", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
[
|
||||||
("MACD", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
("SMA", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||||
("BBANDS",["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
("EMA", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||||
("ATR", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
("RSI", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||||
("CCI", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
("MACD", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||||
("WILLR", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
("BBANDS", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||||
("OBV", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
("ATR", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||||
("ADX", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
("CCI", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||||
("MFI", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
("WILLR", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||||
("STOCH", ["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):
|
def test_head_to_head(benchmark, indicator, libs):
|
||||||
"""Benchmark ferro_ta vs all peers — for README table generation."""
|
"""Benchmark ferro_ta vs all peers — for README table generation."""
|
||||||
if not is_supported("ferro_ta", indicator):
|
if not is_supported("ferro_ta", indicator):
|
||||||
@@ -77,7 +86,11 @@ def test_head_to_head(benchmark, indicator, libs):
|
|||||||
|
|
||||||
# ── Large dataset benchmarks (100k bars) ─────────────────────────────────────
|
# ── 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):
|
def test_large_dataset(benchmark, indicator):
|
||||||
"""Scaling benchmark at 100k bars for ferro_ta."""
|
"""Scaling benchmark at 100k bars for ferro_ta."""
|
||||||
if not is_supported("ferro_ta", indicator):
|
if not is_supported("ferro_ta", indicator):
|
||||||
|
|||||||
+2521
-616
File diff suppressed because it is too large
Load Diff
@@ -21,13 +21,13 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"import numpy as np\n",
|
|
||||||
"\n",
|
|
||||||
"import ferro_ta.config as config\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.backtest import backtest\n",
|
||||||
"from ferro_ta.pipeline import Pipeline\n",
|
"from ferro_ta.pipeline import Pipeline\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
"from ferro_ta import BBANDS, EMA, RSI, SMA\n",
|
||||||
|
"\n",
|
||||||
"# Synthetic data\n",
|
"# Synthetic data\n",
|
||||||
"np.random.seed(42)\n",
|
"np.random.seed(42)\n",
|
||||||
"n = 300\n",
|
"n = 300\n",
|
||||||
|
|||||||
@@ -207,9 +207,10 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"from ferro_ta import FerroTAValueError\n",
|
|
||||||
"from ferro_ta.exceptions import check_timeperiod\n",
|
"from ferro_ta.exceptions import check_timeperiod\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
"from ferro_ta import FerroTAValueError\n",
|
||||||
|
"\n",
|
||||||
"try:\n",
|
"try:\n",
|
||||||
" check_timeperiod(0)\n",
|
" check_timeperiod(0)\n",
|
||||||
"except FerroTAValueError as e:\n",
|
"except FerroTAValueError as e:\n",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"import numpy as np\n",
|
"import numpy as np\n",
|
||||||
"\n",
|
|
||||||
"from ferro_ta.streaming import (\n",
|
"from ferro_ta.streaming import (\n",
|
||||||
" StreamingATR,\n",
|
" StreamingATR,\n",
|
||||||
" StreamingBBands,\n",
|
" StreamingBBands,\n",
|
||||||
|
|||||||
+55
-33
@@ -2,16 +2,38 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"suite": "batch",
|
"suite": "batch",
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"generated_at_utc": "2026-03-23T21:08:30.877702+00:00",
|
"generated_at_utc": "2026-03-24T09:13:04.010216+00:00",
|
||||||
"python_version": "3.12.11",
|
"python_version": "3.13.5",
|
||||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
"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",
|
"machine": "arm64",
|
||||||
"processor": "arm"
|
"processor": "arm",
|
||||||
|
"cpu_model": "Apple M3 Max",
|
||||||
|
"cpu_count_logical": 14,
|
||||||
|
"total_memory_bytes": 38654705664
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||||
"dirty": true,
|
"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": {
|
"dataset": {
|
||||||
"n_samples": 20000,
|
"n_samples": 20000,
|
||||||
@@ -23,49 +45,49 @@
|
|||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"indicator": "SMA",
|
"indicator": "SMA",
|
||||||
"parallel_ms": 1.9615,
|
"parallel_ms": 7.9306,
|
||||||
"sequential_ms": 2.0423,
|
"sequential_ms": 2.4832,
|
||||||
"loop_ms": 0.8865,
|
"loop_ms": 1.0238,
|
||||||
"parallel_speedup_vs_loop": 0.452,
|
"parallel_speedup_vs_loop": 0.1291,
|
||||||
"sequential_speedup_vs_loop": 0.4341
|
"sequential_speedup_vs_loop": 0.4123
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"indicator": "RSI",
|
"indicator": "RSI",
|
||||||
"parallel_ms": 2.1731,
|
"parallel_ms": 3.9307,
|
||||||
"sequential_ms": 4.3225,
|
"sequential_ms": 5.0938,
|
||||||
"loop_ms": 3.2043,
|
"loop_ms": 3.6883,
|
||||||
"parallel_speedup_vs_loop": 1.4745,
|
"parallel_speedup_vs_loop": 0.9383,
|
||||||
"sequential_speedup_vs_loop": 0.7413
|
"sequential_speedup_vs_loop": 0.7241
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"indicator": "ATR",
|
"indicator": "ATR",
|
||||||
"parallel_ms": 4.2249,
|
"parallel_ms": 9.2546,
|
||||||
"sequential_ms": 6.6175,
|
"sequential_ms": 7.768,
|
||||||
"loop_ms": 4.0382,
|
"loop_ms": 5.2669,
|
||||||
"parallel_speedup_vs_loop": 0.9558,
|
"parallel_speedup_vs_loop": 0.5691,
|
||||||
"sequential_speedup_vs_loop": 0.6102
|
"sequential_speedup_vs_loop": 0.678
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"indicator": "ADX",
|
"indicator": "ADX",
|
||||||
"parallel_ms": 4.8674,
|
"parallel_ms": 9.5489,
|
||||||
"sequential_ms": 7.6402,
|
"sequential_ms": 9.1403,
|
||||||
"loop_ms": 5.1861,
|
"loop_ms": 7.1578,
|
||||||
"parallel_speedup_vs_loop": 1.0655,
|
"parallel_speedup_vs_loop": 0.7496,
|
||||||
"sequential_speedup_vs_loop": 0.6788
|
"sequential_speedup_vs_loop": 0.7831
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grouped_results": [
|
"grouped_results": [
|
||||||
{
|
{
|
||||||
"case": "close_bundle_3",
|
"case": "close_bundle_3",
|
||||||
"grouped_ms": 0.1878,
|
"grouped_ms": 0.466,
|
||||||
"separate_ms": 0.1719,
|
"separate_ms": 0.2003,
|
||||||
"speedup_vs_separate": 0.9155
|
"speedup_vs_separate": 0.4298
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"case": "hlc_bundle_3",
|
"case": "hlc_bundle_3",
|
||||||
"grouped_ms": 0.2958,
|
"grouped_ms": 1.2538,
|
||||||
"separate_ms": 0.4633,
|
"separate_ms": 0.6999,
|
||||||
"speedup_vs_separate": 1.5666
|
"speedup_vs_separate": 0.5582
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,38 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"suite": "indicator_latency",
|
"suite": "indicator_latency",
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"generated_at_utc": "2026-03-23T21:08:30.515962+00:00",
|
"generated_at_utc": "2026-03-24T09:13:02.973728+00:00",
|
||||||
"python_version": "3.12.11",
|
"python_version": "3.13.5",
|
||||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
"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",
|
"machine": "arm64",
|
||||||
"processor": "arm"
|
"processor": "arm",
|
||||||
|
"cpu_model": "Apple M3 Max",
|
||||||
|
"cpu_count_logical": 14,
|
||||||
|
"total_memory_bytes": 38654705664
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||||
"dirty": true,
|
"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": [
|
"fixtures": [
|
||||||
{
|
{
|
||||||
@@ -33,7 +55,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 20
|
"timeperiod": 20
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0202
|
"elapsed_ms": 0.0233
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "WILLR_14",
|
"name": "WILLR_14",
|
||||||
@@ -41,13 +63,13 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 14
|
"timeperiod": 14
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0183
|
"elapsed_ms": 0.0207
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "STOCH",
|
"name": "STOCH",
|
||||||
"inputs": "hlc",
|
"inputs": "hlc",
|
||||||
"kwargs": {},
|
"kwargs": {},
|
||||||
"elapsed_ms": 0.0181
|
"elapsed_ms": 0.0202
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ADX_14",
|
"name": "ADX_14",
|
||||||
@@ -55,7 +77,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 14
|
"timeperiod": 14
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0147
|
"elapsed_ms": 0.0171
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CCI_14",
|
"name": "CCI_14",
|
||||||
@@ -63,13 +85,13 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 14
|
"timeperiod": 14
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0144
|
"elapsed_ms": 0.0164
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "MACD",
|
"name": "MACD",
|
||||||
"inputs": "close",
|
"inputs": "close",
|
||||||
"kwargs": {},
|
"kwargs": {},
|
||||||
"elapsed_ms": 0.0132
|
"elapsed_ms": 0.015
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ATR_14",
|
"name": "ATR_14",
|
||||||
@@ -77,7 +99,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 14
|
"timeperiod": 14
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0108
|
"elapsed_ms": 0.0116
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "RSI_14",
|
"name": "RSI_14",
|
||||||
@@ -85,7 +107,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 14
|
"timeperiod": 14
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0102
|
"elapsed_ms": 0.011
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "STDDEV_20",
|
"name": "STDDEV_20",
|
||||||
@@ -93,7 +115,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 20
|
"timeperiod": 20
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0086
|
"elapsed_ms": 0.0103
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "BETA_5",
|
"name": "BETA_5",
|
||||||
@@ -101,7 +123,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 5
|
"timeperiod": 5
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.008
|
"elapsed_ms": 0.0091
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CORREL_30",
|
"name": "CORREL_30",
|
||||||
@@ -109,15 +131,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 30
|
"timeperiod": 30
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0068
|
"elapsed_ms": 0.0078
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "EMA_20",
|
|
||||||
"inputs": "close",
|
|
||||||
"kwargs": {
|
|
||||||
"timeperiod": 20
|
|
||||||
},
|
|
||||||
"elapsed_ms": 0.0052
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "BBANDS_20",
|
"name": "BBANDS_20",
|
||||||
@@ -125,7 +139,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 20
|
"timeperiod": 20
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.005
|
"elapsed_ms": 0.0062
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "TSF_14",
|
"name": "TSF_14",
|
||||||
@@ -133,7 +147,15 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 14
|
"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",
|
"name": "LINEARREG_14",
|
||||||
@@ -141,7 +163,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 14
|
"timeperiod": 14
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0046
|
"elapsed_ms": 0.0054
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "LINEARREG_SLOPE_14",
|
"name": "LINEARREG_SLOPE_14",
|
||||||
@@ -149,7 +171,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 14
|
"timeperiod": 14
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0045
|
"elapsed_ms": 0.005
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "SMA_20",
|
"name": "SMA_20",
|
||||||
@@ -157,7 +179,7 @@
|
|||||||
"kwargs": {
|
"kwargs": {
|
||||||
"timeperiod": 20
|
"timeperiod": 20
|
||||||
},
|
},
|
||||||
"elapsed_ms": 0.0027
|
"elapsed_ms": 0.0032
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-20
@@ -2,16 +2,38 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"suite": "perf_contract",
|
"suite": "perf_contract",
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"generated_at_utc": "2026-03-23T21:09:13.077731+00:00",
|
"generated_at_utc": "2026-03-24T09:13:08.787230+00:00",
|
||||||
"python_version": "3.12.11",
|
"python_version": "3.13.5",
|
||||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
"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",
|
"machine": "arm64",
|
||||||
"processor": "arm"
|
"processor": "arm",
|
||||||
|
"cpu_model": "Apple M3 Max",
|
||||||
|
"cpu_count_logical": 14,
|
||||||
|
"total_memory_bytes": 38654705664
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||||
"dirty": true,
|
"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": [
|
"fixtures": [
|
||||||
{
|
{
|
||||||
@@ -25,28 +47,23 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"indicator_latency": {
|
"indicator_latency": {
|
||||||
"path": "perf-contract/indicator_latency.json",
|
"path": "perf-contract/indicator_latency.json",
|
||||||
"size_bytes": 3212,
|
"size_bytes": 4041,
|
||||||
"sha256": "1e7f844fe6eb467f07bbb1e4eb918c56861e8cf819ab4802b96e033c6d2570c4"
|
"sha256": "564027c7abed7ecd4ae2ac1720217d96e1e31807f8ac7d5c5393c8fd974f13ed"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"path": "perf-contract/batch.json",
|
"path": "perf-contract/batch.json",
|
||||||
"size_bytes": 1679,
|
"size_bytes": 2507,
|
||||||
"sha256": "202d67b24a88184473f93cec9f866ca34ff60721068e15e7e5d23962f006d169"
|
"sha256": "c50f242a138a0c358a6fa86c420954b4b11a2200598ca2942bcd0fd2bc456fb2"
|
||||||
},
|
},
|
||||||
"streaming": {
|
"streaming": {
|
||||||
"path": "perf-contract/streaming.json",
|
"path": "perf-contract/streaming.json",
|
||||||
"size_bytes": 1939,
|
"size_bytes": 2766,
|
||||||
"sha256": "04476c3d95a9e7d87e40331b28c5ccad9a7aa5abcfc47fb6ac7e929365f68554"
|
"sha256": "d271d3219ec098e443e84ef648ab4220cd2a38e6dc998bc6c9fc545d7fc77716"
|
||||||
},
|
},
|
||||||
"runtime_hotspots": {
|
"runtime_hotspots": {
|
||||||
"path": "perf-contract/runtime_hotspots.json",
|
"path": "perf-contract/runtime_hotspots.json",
|
||||||
"size_bytes": 2365,
|
"size_bytes": 3197,
|
||||||
"sha256": "d37a58bc88def9f1525b9b0f64f0bd7b9c0df9ed4cae260c833697f6a1689ca8"
|
"sha256": "96eafc9a61ac410e47fcdfee6a9db1123cdee0ec05f49a85c631a3975063deed"
|
||||||
},
|
|
||||||
"simd": {
|
|
||||||
"path": "perf-contract/simd.json",
|
|
||||||
"size_bytes": 7670,
|
|
||||||
"sha256": "3f9b341a8206698ed172650752d808621c9b5218f42a2373f0f4485496197b4c"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,38 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"suite": "runtime_hotspots",
|
"suite": "runtime_hotspots",
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"generated_at_utc": "2026-03-23T21:08:34.436165+00:00",
|
"generated_at_utc": "2026-03-24T09:13:08.521805+00:00",
|
||||||
"python_version": "3.12.11",
|
"python_version": "3.13.5",
|
||||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
"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",
|
"machine": "arm64",
|
||||||
"processor": "arm"
|
"processor": "arm",
|
||||||
|
"cpu_model": "Apple M3 Max",
|
||||||
|
"cpu_count_logical": 14,
|
||||||
|
"total_memory_bytes": 38654705664
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||||
"dirty": true,
|
"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": {
|
"dataset": {
|
||||||
"price_bars": 20000,
|
"price_bars": 20000,
|
||||||
@@ -23,74 +45,74 @@
|
|||||||
{
|
{
|
||||||
"category": "python_analysis",
|
"category": "python_analysis",
|
||||||
"name": "iv_zscore",
|
"name": "iv_zscore",
|
||||||
"fast_ms": 28.857,
|
"fast_ms": 32.5847,
|
||||||
"reference_ms": 897.0178,
|
"reference_ms": 990.4233,
|
||||||
"speedup_vs_reference": 31.0849,
|
"speedup_vs_reference": 30.3954,
|
||||||
"share_of_suite_pct": 70.0
|
"share_of_suite_pct": 69.27
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"category": "python_analysis",
|
"category": "python_analysis",
|
||||||
"name": "iv_rank",
|
"name": "iv_rank",
|
||||||
"fast_ms": 10.8496,
|
"fast_ms": 12.2974,
|
||||||
"reference_ms": 199.4376,
|
"reference_ms": 233.0671,
|
||||||
"speedup_vs_reference": 18.3821,
|
"speedup_vs_reference": 18.9525,
|
||||||
"share_of_suite_pct": 26.32
|
"share_of_suite_pct": 26.14
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"category": "python_analysis",
|
"category": "python_analysis",
|
||||||
"name": "iv_percentile",
|
"name": "iv_percentile",
|
||||||
"fast_ms": 0.9124,
|
"fast_ms": 0.9511,
|
||||||
"reference_ms": 79.6557,
|
"reference_ms": 90.5663,
|
||||||
"speedup_vs_reference": 87.3019,
|
"speedup_vs_reference": 95.2202,
|
||||||
"share_of_suite_pct": 2.21
|
"share_of_suite_pct": 2.02
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"category": "ffi_grouping",
|
"category": "ffi_grouping",
|
||||||
"name": "feature_matrix",
|
"name": "feature_matrix",
|
||||||
"fast_ms": 0.2411,
|
"fast_ms": 0.6619,
|
||||||
"reference_ms": 0.2215,
|
"reference_ms": 0.6155,
|
||||||
"speedup_vs_reference": 0.9186,
|
"speedup_vs_reference": 0.9299,
|
||||||
"share_of_suite_pct": 0.58
|
"share_of_suite_pct": 1.41
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"category": "ffi_grouping",
|
"category": "ffi_grouping",
|
||||||
"name": "compute_many_close",
|
"name": "compute_many_close",
|
||||||
"fast_ms": 0.1563,
|
"fast_ms": 0.3175,
|
||||||
"reference_ms": 0.1498,
|
"reference_ms": 0.2437,
|
||||||
"speedup_vs_reference": 0.9587,
|
"speedup_vs_reference": 0.7675,
|
||||||
"share_of_suite_pct": 0.38
|
"share_of_suite_pct": 0.67
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"category": "rust_kernel",
|
"category": "rust_kernel",
|
||||||
"name": "BETA",
|
"name": "BETA",
|
||||||
"fast_ms": 0.0694,
|
"fast_ms": 0.0742,
|
||||||
"reference_ms": 159.2715,
|
"reference_ms": 188.6048,
|
||||||
"speedup_vs_reference": 2294.4163,
|
"speedup_vs_reference": 2541.5695,
|
||||||
"share_of_suite_pct": 0.17
|
"share_of_suite_pct": 0.16
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"category": "rust_kernel",
|
"category": "rust_kernel",
|
||||||
"name": "CORREL",
|
"name": "CORREL",
|
||||||
"fast_ms": 0.0555,
|
"fast_ms": 0.0638,
|
||||||
"reference_ms": 158.2775,
|
"reference_ms": 215.973,
|
||||||
"speedup_vs_reference": 2851.8468,
|
"speedup_vs_reference": 3387.8115,
|
||||||
"share_of_suite_pct": 0.13
|
"share_of_suite_pct": 0.14
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "rust_kernel",
|
|
||||||
"name": "LINEARREG",
|
|
||||||
"fast_ms": 0.0414,
|
|
||||||
"reference_ms": 44.8111,
|
|
||||||
"speedup_vs_reference": 1081.9491,
|
|
||||||
"share_of_suite_pct": 0.1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"category": "rust_kernel",
|
"category": "rust_kernel",
|
||||||
"name": "TSF",
|
"name": "TSF",
|
||||||
"fast_ms": 0.0413,
|
"fast_ms": 0.0441,
|
||||||
"reference_ms": 46.3799,
|
"reference_ms": 49.6108,
|
||||||
"speedup_vs_reference": 1122.1039,
|
"speedup_vs_reference": 1125.3954,
|
||||||
"share_of_suite_pct": 0.1
|
"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": {
|
"metadata": {
|
||||||
"suite": "streaming",
|
"suite": "streaming",
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"generated_at_utc": "2026-03-23T21:08:30.968886+00:00",
|
"generated_at_utc": "2026-03-24T09:13:04.351797+00:00",
|
||||||
"python_version": "3.12.11",
|
"python_version": "3.13.5",
|
||||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
"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",
|
"machine": "arm64",
|
||||||
"processor": "arm"
|
"processor": "arm",
|
||||||
|
"cpu_model": "Apple M3 Max",
|
||||||
|
"cpu_count_logical": 14,
|
||||||
|
"total_memory_bytes": 38654705664
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||||
"dirty": true,
|
"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": {
|
"dataset": {
|
||||||
"n_bars": 20000,
|
"n_bars": 20000,
|
||||||
@@ -22,52 +44,52 @@
|
|||||||
{
|
{
|
||||||
"indicator": "StreamingSMA",
|
"indicator": "StreamingSMA",
|
||||||
"inputs": "close",
|
"inputs": "close",
|
||||||
"stream_total_ms": 0.8808,
|
"stream_total_ms": 0.9927,
|
||||||
"batch_total_ms": 0.0153,
|
"batch_total_ms": 0.0166,
|
||||||
"stream_ns_per_update": 44.04,
|
"stream_ns_per_update": 49.63,
|
||||||
"batch_ns_per_bar": 0.77,
|
"batch_ns_per_bar": 0.83,
|
||||||
"updates_per_second": 22705779.59,
|
"updates_per_second": 20147763.43,
|
||||||
"stream_over_batch_ratio": 57.4469
|
"stream_over_batch_ratio": 59.7092
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"indicator": "StreamingEMA",
|
"indicator": "StreamingEMA",
|
||||||
"inputs": "close",
|
"inputs": "close",
|
||||||
"stream_total_ms": 0.8611,
|
"stream_total_ms": 0.9459,
|
||||||
"batch_total_ms": 0.0408,
|
"batch_total_ms": 0.0435,
|
||||||
"stream_ns_per_update": 43.06,
|
"stream_ns_per_update": 47.3,
|
||||||
"batch_ns_per_bar": 2.04,
|
"batch_ns_per_bar": 2.18,
|
||||||
"updates_per_second": 23225431.81,
|
"updates_per_second": 21143503.9,
|
||||||
"stream_over_batch_ratio": 21.0884
|
"stream_over_batch_ratio": 21.7247
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"indicator": "StreamingRSI",
|
"indicator": "StreamingRSI",
|
||||||
"inputs": "close",
|
"inputs": "close",
|
||||||
"stream_total_ms": 0.9235,
|
"stream_total_ms": 1.0059,
|
||||||
"batch_total_ms": 0.0932,
|
"batch_total_ms": 0.0991,
|
||||||
"stream_ns_per_update": 46.17,
|
"stream_ns_per_update": 50.3,
|
||||||
"batch_ns_per_bar": 4.66,
|
"batch_ns_per_bar": 4.95,
|
||||||
"updates_per_second": 21656740.75,
|
"updates_per_second": 19882356.06,
|
||||||
"stream_over_batch_ratio": 9.9035
|
"stream_over_batch_ratio": 10.1523
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"indicator": "StreamingATR",
|
"indicator": "StreamingATR",
|
||||||
"inputs": "hlc",
|
"inputs": "hlc",
|
||||||
"stream_total_ms": 2.0074,
|
"stream_total_ms": 2.1574,
|
||||||
"batch_total_ms": 0.0935,
|
"batch_total_ms": 0.0992,
|
||||||
"stream_ns_per_update": 100.37,
|
"stream_ns_per_update": 107.87,
|
||||||
"batch_ns_per_bar": 4.68,
|
"batch_ns_per_bar": 4.96,
|
||||||
"updates_per_second": 9963056.99,
|
"updates_per_second": 9270525.53,
|
||||||
"stream_over_batch_ratio": 21.4603
|
"stream_over_batch_ratio": 21.746
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"indicator": "StreamingVWAP",
|
"indicator": "StreamingVWAP",
|
||||||
"inputs": "hlcv",
|
"inputs": "hlcv",
|
||||||
"stream_total_ms": 2.6068,
|
"stream_total_ms": 2.6935,
|
||||||
"batch_total_ms": 0.0223,
|
"batch_total_ms": 0.0252,
|
||||||
"stream_ns_per_update": 130.34,
|
"stream_ns_per_update": 134.68,
|
||||||
"batch_ns_per_bar": 1.11,
|
"batch_ns_per_bar": 1.26,
|
||||||
"updates_per_second": 7672265.38,
|
"updates_per_second": 7425170.07,
|
||||||
"stream_over_batch_ratio": 116.9385
|
"stream_over_batch_ratio": 106.8484
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ ignore = ["E501", "UP006", "UP045", "UP007"]
|
|||||||
"tests/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
"tests/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
||||||
"tests/unit/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
"tests/unit/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
||||||
"tests/integration/*" = ["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/*.py" = ["N802"] # Public API: SMA, RSI, ATR, etc.
|
||||||
"python/ferro_ta/__init__.py" = ["E402", "N802"] # Re-export surface by design
|
"python/ferro_ta/__init__.py" = ["E402", "N802"] # Re-export surface by design
|
||||||
"python/ferro_ta/__init__.pyi" = ["N802", "E402"]
|
"python/ferro_ta/__init__.pyi" = ["N802", "E402"]
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ def rsi_strategy(
|
|||||||
|
|
||||||
c = np.asarray(close, dtype=np.float64)
|
c = np.asarray(close, dtype=np.float64)
|
||||||
return np.asarray(
|
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,
|
dtype=np.float64,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,9 @@ def strategy_payoff(
|
|||||||
return np.zeros_like(grid)
|
return np.zeros_like(grid)
|
||||||
|
|
||||||
try:
|
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:
|
except ValueError as err:
|
||||||
_normalize_rust_error(err)
|
_normalize_rust_error(err)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ __all__ = [
|
|||||||
|
|
||||||
|
|
||||||
def _forward_fill_nan(arr: NDArray[np.float64]) -> NDArray[np.float64]:
|
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 datetime import date, datetime, time
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from itertools import count
|
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
|
import numpy as np
|
||||||
|
|
||||||
@@ -156,38 +156,42 @@ def _object_snapshot(value: Any) -> Any:
|
|||||||
if isinstance(value, enum.Enum):
|
if isinstance(value, enum.Enum):
|
||||||
return value.value
|
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)
|
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:
|
try:
|
||||||
return _normalise_json(value.to_dict(), store_objects=False)
|
return _normalise_json(dynamic_value.to_dict(), store_objects=False)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
if value.__class__.__module__.startswith("pandas"):
|
if dynamic_value.__class__.__module__.startswith("pandas"):
|
||||||
return _normalise_json(value.to_dict(orient="list"), store_objects=False)
|
|
||||||
if value.__class__.__module__.startswith("polars"):
|
|
||||||
return _normalise_json(
|
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:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if hasattr(value, "__dict__"):
|
if hasattr(dynamic_value, "__dict__"):
|
||||||
fields = {
|
fields = {
|
||||||
key: val
|
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 not key.startswith("_") and not callable(val)
|
||||||
}
|
}
|
||||||
if fields:
|
if fields:
|
||||||
return _normalise_json(fields, store_objects=False)
|
return _normalise_json(fields, store_objects=False)
|
||||||
|
|
||||||
slots = getattr(type(value), "__slots__", ())
|
slots = getattr(type(dynamic_value), "__slots__", ())
|
||||||
if slots:
|
if slots:
|
||||||
fields = {}
|
fields = {}
|
||||||
for slot in slots:
|
for slot in slots:
|
||||||
if slot.startswith("_") or not hasattr(value, slot):
|
if slot.startswith("_") or not hasattr(dynamic_value, slot):
|
||||||
continue
|
continue
|
||||||
slot_value = getattr(value, slot)
|
slot_value = getattr(dynamic_value, slot)
|
||||||
if callable(slot_value):
|
if callable(slot_value):
|
||||||
continue
|
continue
|
||||||
fields[slot] = slot_value
|
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)
|
return _normalise_json(value.value, store_objects=store_objects)
|
||||||
|
|
||||||
if isinstance(value, np.ndarray):
|
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):
|
if isinstance(value, np.generic):
|
||||||
return _normalise_json(value.item(), store_objects=store_objects)
|
return _normalise_json(value.item(), store_objects=store_objects)
|
||||||
@@ -408,7 +415,9 @@ def _annotation_includes_custom_class(annotation: Any) -> bool:
|
|||||||
return False
|
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."""
|
"""Map Python annotations to JSON Schema and wrapper annotations."""
|
||||||
enum_type = _is_enum_annotation(annotation)
|
enum_type = _is_enum_annotation(annotation)
|
||||||
if enum_type is not None:
|
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:
|
if "scalarorarray" in lower:
|
||||||
return {
|
return {
|
||||||
"type": _JSON_ANY_TYPE,
|
"type": _JSON_ANY_TYPE,
|
||||||
"description": f"Parameter `{param_name}`. { _REFERENCE_HELP }",
|
"description": f"Parameter `{param_name}`. {_REFERENCE_HELP}",
|
||||||
}, Any
|
}, Any
|
||||||
if "ndarray" in lower or "arraylike" in lower:
|
if "ndarray" in lower or "arraylike" in lower:
|
||||||
return {"type": "array", "items": {}}, list[Any]
|
return {"type": "array", "items": {}}, list[Any]
|
||||||
return {"type": "number"}, float
|
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]
|
return {"type": "array", "items": {}}, list[Any]
|
||||||
if "dict" in lower or "mapping" in lower:
|
if "dict" in lower or "mapping" in lower:
|
||||||
return {"type": "object"}, dict[str, Any]
|
return {"type": "object"}, dict[str, Any]
|
||||||
if "str" in lower or "date" in lower or "datetime" in lower or "time" in lower:
|
if "str" in lower or "date" in lower or "datetime" in lower or "time" in lower:
|
||||||
return {"type": "string"}, str
|
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 {
|
return {
|
||||||
"type": _JSON_ANY_TYPE,
|
"type": _JSON_ANY_TYPE,
|
||||||
"description": f"Parameter `{param_name}`. {_REFERENCE_HELP}",
|
"description": f"Parameter `{param_name}`. {_REFERENCE_HELP}",
|
||||||
@@ -639,7 +655,10 @@ def _invoke_target(
|
|||||||
if not isinstance(extra_kwargs, dict):
|
if not isinstance(extra_kwargs, dict):
|
||||||
raise TypeError("kwargs must be a JSON object")
|
raise TypeError("kwargs must be a JSON object")
|
||||||
keyword_args.update(
|
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
|
continue
|
||||||
|
|
||||||
@@ -689,8 +708,7 @@ def _describe_instance_payload(identifier: str) -> dict[str, Any]:
|
|||||||
def _list_instances_payload() -> list[dict[str, Any]]:
|
def _list_instances_payload() -> list[dict[str, Any]]:
|
||||||
"""Return current stored-object metadata."""
|
"""Return current stored-object metadata."""
|
||||||
return [
|
return [
|
||||||
_describe_instance_payload(identifier)
|
_describe_instance_payload(identifier) for identifier in sorted(_INSTANCE_STORE)
|
||||||
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."
|
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:
|
if "value" not in arguments:
|
||||||
raise KeyError("Missing required argument: value")
|
raise KeyError("Missing required argument: value")
|
||||||
member = _coerce_enum(arguments["value"], enum_type)
|
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,
|
description=description,
|
||||||
input_schema=input_schema,
|
input_schema=input_schema,
|
||||||
wrapper_signature=wrapper_signature,
|
wrapper_signature=wrapper_signature,
|
||||||
invoke=invoke,
|
invoke=invoke_enum,
|
||||||
)
|
)
|
||||||
|
|
||||||
signature = inspect.signature(target)
|
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."
|
or f"Construct a {item['name']} instance. Returns a stored instance reference."
|
||||||
)
|
)
|
||||||
|
|
||||||
def invoke(
|
def invoke_target(
|
||||||
arguments: dict[str, Any],
|
arguments: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
raw_target: Any = target,
|
raw_target: Any = target,
|
||||||
@@ -770,7 +790,7 @@ def _build_public_tool_spec(item: dict[str, str], target: Any) -> _ToolSpec:
|
|||||||
description=description,
|
description=description,
|
||||||
input_schema=input_schema,
|
input_schema=input_schema,
|
||||||
wrapper_signature=wrapper_signature,
|
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")
|
raise ValueError("Only public methods can be called")
|
||||||
method = getattr(value, method_name)
|
method = getattr(value, method_name)
|
||||||
if not callable(method):
|
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 [])]
|
args = [_decode_value(item, Any) for item in (arguments.get("args") or [])]
|
||||||
kwargs = {
|
kwargs = {
|
||||||
str(key): _decode_value(item, Any)
|
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.__name__ = f"tool_{_slugify(spec.name).replace('-', '_')}"
|
||||||
wrapper.__doc__ = spec.description
|
wrapper.__doc__ = spec.description
|
||||||
wrapper.__signature__ = spec.wrapper_signature
|
setattr(wrapper, "__signature__", spec.wrapper_signature)
|
||||||
wrapper.__annotations__ = {
|
wrapper.__annotations__ = {
|
||||||
parameter.name: (
|
parameter.name: (
|
||||||
Any if parameter.annotation is inspect._empty else parameter.annotation
|
Any if parameter.annotation is inspect._empty else parameter.annotation
|
||||||
@@ -1311,7 +1333,9 @@ def create_server() -> Any:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for spec in _tool_catalog().values():
|
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
|
return app
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ def _load_api_info_module(root: Path, module_path: Path):
|
|||||||
python_root = str(root / "python")
|
python_root = str(root / "python")
|
||||||
if python_root not in sys.path:
|
if python_root not in sys.path:
|
||||||
sys.path.insert(0, python_root)
|
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:
|
if spec is None or spec.loader is None:
|
||||||
raise RuntimeError(f"Could not load module spec from {module_path}")
|
raise RuntimeError(f"Could not load module spec from {module_path}")
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
@@ -217,7 +219,9 @@ def _safe_git_head(root: Path) -> str | None:
|
|||||||
return value or 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)
|
python_api = _extract_python_api(root)
|
||||||
rust_core = _extract_core_exports(root)
|
rust_core = _extract_core_exports(root)
|
||||||
wasm_exports = _extract_wasm_exports(root)
|
wasm_exports = _extract_wasm_exports(root)
|
||||||
@@ -279,7 +283,9 @@ def main() -> None:
|
|||||||
output_path = (root / args.output).resolve()
|
output_path = (root / args.output).resolve()
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
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")
|
output_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||||
print(f"Wrote API manifest to {output_path}")
|
print(f"Wrote API manifest to {output_path}")
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import re
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
||||||
|
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ ensure_python_env() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
need_cmd uv
|
need_cmd uv
|
||||||
run_cmd uv sync --extra dev --extra docs
|
run_cmd uv sync --extra dev --extra docs --extra mcp
|
||||||
run_cmd uv run --extra dev --extra docs maturin develop --release
|
run_cmd uv run --extra dev --extra docs --extra mcp maturin develop --release
|
||||||
python_env_ready=1
|
python_env_ready=1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,13 +110,13 @@ run_python_lint() {
|
|||||||
|
|
||||||
run_python_typecheck() {
|
run_python_typecheck() {
|
||||||
need_cmd uv
|
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 mypy --with numpy python -m 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 pyright python -m pyright python/ferro_ta
|
||||||
}
|
}
|
||||||
|
|
||||||
run_python_test() {
|
run_python_test() {
|
||||||
ensure_python_env
|
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() {
|
run_docs() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
SCRIPTS = ROOT / "scripts"
|
SCRIPTS = ROOT / "scripts"
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ def _run_node_conformance() -> dict[str, list[float | None]]:
|
|||||||
if shutil.which("node") is None:
|
if shutil.which("node") is None:
|
||||||
pytest.skip("node is required for wasm/node conformance test")
|
pytest.skip("node is required for wasm/node conformance test")
|
||||||
if not PKG_JS.exists():
|
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)
|
_write_node_conformance_script(SCRIPT)
|
||||||
try:
|
try:
|
||||||
@@ -78,7 +80,9 @@ def _assert_close_with_null_nan(
|
|||||||
) -> None:
|
) -> None:
|
||||||
assert len(actual) == len(expected)
|
assert len(actual) == len(expected)
|
||||||
a = np.array([np.nan if v is None else float(v) for v in actual], dtype=np.float64)
|
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)
|
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"]
|
instance_id = created["instance_id"]
|
||||||
|
|
||||||
described = json.loads(
|
described = json.loads(
|
||||||
handle_call_tool(
|
handle_call_tool("describe_instance", {"instance_id": instance_id})[
|
||||||
"describe_instance", {"instance_id": instance_id}
|
"content"
|
||||||
)["content"][0]["text"]
|
][0]["text"]
|
||||||
)
|
)
|
||||||
method_names = [item["name"] for item in described["methods"]]
|
method_names = [item["name"] for item in described["methods"]]
|
||||||
assert "aggregate" in method_names
|
assert "aggregate" in method_names
|
||||||
@@ -1429,9 +1429,9 @@ class TestMCPCallTool:
|
|||||||
assert "close" in aggregated
|
assert "close" in aggregated
|
||||||
|
|
||||||
deleted = json.loads(
|
deleted = json.loads(
|
||||||
handle_call_tool(
|
handle_call_tool("delete_instance", {"instance_id": instance_id})[
|
||||||
"delete_instance", {"instance_id": instance_id}
|
"content"
|
||||||
)["content"][0]["text"]
|
][0]["text"]
|
||||||
)
|
)
|
||||||
assert deleted["deleted"] is True
|
assert deleted["deleted"] is True
|
||||||
|
|
||||||
@@ -1441,9 +1441,9 @@ class TestMCPCallTool:
|
|||||||
from ferro_ta.mcp import handle_call_tool
|
from ferro_ta.mcp import handle_call_tool
|
||||||
|
|
||||||
wrapped = json.loads(
|
wrapped = json.loads(
|
||||||
handle_call_tool(
|
handle_call_tool("traced", {"func": {"callable": "SMA"}})["content"][0][
|
||||||
"traced", {"func": {"callable": "SMA"}}
|
"text"
|
||||||
)["content"][0]["text"]
|
]
|
||||||
)
|
)
|
||||||
instance_id = wrapped["instance_id"]
|
instance_id = wrapped["instance_id"]
|
||||||
|
|
||||||
|
|||||||
@@ -2315,11 +2315,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyjwt"
|
name = "pyjwt"
|
||||||
version = "2.11.0"
|
version = "2.12.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 = [
|
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]
|
[package.optional-dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user