Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b7159fe19 | |||
| 2d776b6f90 | |||
| 71b6343e92 | |||
| 53566b9d82 | |||
| ba77fbd418 | |||
| 29c6f5cf84 | |||
| 58a1dc2308 | |||
| b66b05682e | |||
| e4ac7dd4d3 | |||
| 1f053c1001 |
@@ -81,7 +81,7 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CI gate — all required jobs must pass before this job succeeds.
|
||||
|
||||
@@ -41,10 +41,10 @@ jobs:
|
||||
run: pip install uv
|
||||
|
||||
- name: Run mypy on ferro_ta via uv
|
||||
run: uv run --with mypy --with numpy mypy python/ferro_ta --ignore-missing-imports --no-error-summary
|
||||
run: uv run --with mypy --with numpy python -m mypy python/ferro_ta --ignore-missing-imports --no-error-summary
|
||||
|
||||
- name: Run pyright on ferro_ta via uv
|
||||
run: uv run --with pyright pyright python/ferro_ta
|
||||
run: uv run --with pyright python -m pyright python/ferro_ta
|
||||
|
||||
test:
|
||||
name: Test (ubuntu-latest / Python ${{ matrix.python-version }})
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Install maturin and test dependencies
|
||||
run: |
|
||||
pip install maturin numpy pytest pytest-cov pandas polars hypothesis pyyaml
|
||||
pip install maturin numpy pytest pytest-cov pandas polars hypothesis pyyaml mcp
|
||||
|
||||
- name: Build and install ferro_ta (dev mode)
|
||||
run: |
|
||||
@@ -73,6 +73,10 @@ jobs:
|
||||
- name: Run unit tests with coverage
|
||||
run: pytest tests/unit/ tests/integration/ -v --cov=ferro_ta --cov-report=xml --cov-report=term-missing --cov-fail-under=65
|
||||
|
||||
- name: Check API manifest is current
|
||||
if: matrix.python-version == '3.12'
|
||||
run: python scripts/check_api_manifest.py
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: matrix.python-version == '3.12'
|
||||
|
||||
@@ -35,6 +35,9 @@ jobs:
|
||||
working-directory: wasm
|
||||
run: wasm-pack build --target nodejs --out-dir pkg
|
||||
|
||||
- name: Check API manifest is current
|
||||
run: python3 scripts/check_api_manifest.py
|
||||
|
||||
- name: Benchmark WASM package
|
||||
working-directory: wasm
|
||||
run: node bench.js --json ../wasm_benchmark.json
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: Release
|
||||
|
||||
# Triggered when a version tag is pushed (e.g. v1.0.2).
|
||||
# Triggered when a version tag is pushed (e.g. v1.0.3).
|
||||
# Creates a GitHub Release marked as "published", which in turn
|
||||
# triggers the build-wheels and publish jobs in CI.yml.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -25,6 +25,10 @@ jobs:
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
|
||||
echo "Expected a semantic-version tag like v1.0.3 or v1.0.3-rc1, got: $GITHUB_REF_NAME"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Extract changelog entry for this version
|
||||
|
||||
+6
-1
@@ -31,6 +31,11 @@ env/
|
||||
# WASM build output
|
||||
wasm/pkg/
|
||||
wasm/pkg-web/
|
||||
benchmark_vs_talib.json
|
||||
wasm_benchmark.json
|
||||
.wasm_benchmark.prepush.json
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.xml
|
||||
.hypothesis/
|
||||
|
||||
@@ -39,4 +44,4 @@ coverage.xml
|
||||
|
||||
# DS Store in all directories
|
||||
.DS_Store
|
||||
*.DS_Store
|
||||
*.DS_Store
|
||||
|
||||
+13
-2
@@ -1,12 +1,12 @@
|
||||
# Pre-commit hooks for ferro-ta
|
||||
# Install: pre-commit install
|
||||
# Install: pre-commit install --hook-type pre-commit --hook-type pre-push
|
||||
# Run: pre-commit run --all-files
|
||||
default_language_version:
|
||||
python: python3
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.0
|
||||
rev: v0.15.7
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
@@ -18,6 +18,7 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
exclude: ^conda/meta\.yaml$
|
||||
- id: check-added-large-files
|
||||
args: [--maxkb=1000]
|
||||
- id: check-merge-conflict
|
||||
@@ -31,3 +32,13 @@ repos:
|
||||
# entry: mypy python/ferro_ta --ignore-missing-imports
|
||||
# language: system
|
||||
# pass_filenames: false
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: ci-basic-pre-push
|
||||
name: ci basic pre-push
|
||||
entry: scripts/pre_push_checks.sh
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
stages: [pre-push]
|
||||
|
||||
+78
-1
@@ -9,6 +9,72 @@ and the project uses [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.6] — 2026-03-24
|
||||
|
||||
### Added
|
||||
|
||||
- Added a repo-managed pre-push gate that mirrors the core local CI and
|
||||
release checks, including version and changelog validation, Rust formatting
|
||||
and clippy, Python linting and type checks, tests, docs, and the WASM smoke
|
||||
suite.
|
||||
- Added generated API manifest tooling and CI coverage so Python and WASM
|
||||
export drift is detected before release candidates are pushed.
|
||||
- Expanded the Rust-backed implementation surface for analysis and data-heavy
|
||||
workflows, including backtest signal generation, portfolio loops, payoff and
|
||||
Greeks aggregation, chunked indicator execution, and related helper paths.
|
||||
- Expanded the WASM package surface with additional indicator exports such as
|
||||
`WMA`, `ADX`, and `MFI`, along with refreshed Node examples and conformance
|
||||
coverage against the Python package.
|
||||
|
||||
### Changed
|
||||
|
||||
- Refreshed benchmark wrappers, perf-contract artifacts, and benchmark
|
||||
comparison helpers so the checked-in performance evidence stays aligned with
|
||||
the current feature set.
|
||||
- Hardened Python CI and local tooling so they run the same typecheck and test
|
||||
entrypoints, including installing the optional MCP dependency needed by the
|
||||
MCP server tests.
|
||||
- Updated local pre-commit integration to match the current Ruff
|
||||
configuration and refreshed locked dependencies to pick up the audited
|
||||
PyJWT security fix.
|
||||
|
||||
### Fixed
|
||||
|
||||
- One-off benchmark output files produced in the repository root are now
|
||||
ignored so local benchmarking no longer dirties the repo by default.
|
||||
- Tightened API typing and MCP helper behavior so the stricter lint and
|
||||
typecheck pipeline passes consistently before release.
|
||||
|
||||
## [1.0.4] — 2026-03-24
|
||||
|
||||
### Added
|
||||
|
||||
- The optional MCP server now exposes the broad public ferro-ta callable
|
||||
surface, including exact top-level exports, non-top-level public analysis and
|
||||
tooling functions, and generic stored-instance tools for stateful classes and
|
||||
returned callables.
|
||||
- Added a dedicated `TA_LIB_COMPATIBILITY.md` document so the full TA-Lib
|
||||
coverage matrix remains available without bloating the project homepage.
|
||||
|
||||
### Changed
|
||||
|
||||
- Reworked the root README into a shorter product-first landing page with a
|
||||
compatibility summary and docs map, and refreshed MCP documentation to match
|
||||
the expanded server behavior.
|
||||
- Updated the MCP implementation to use generated tool registration over the
|
||||
public API while keeping the legacy lowercase aliases (`sma`, `ema`, `rsi`,
|
||||
`macd`, `backtest`) available for existing clients.
|
||||
- Refreshed locked Python dependency resolutions for the latest low-risk direct
|
||||
updates in this release cycle.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The repository no longer tracks the stray `.coverage` artifact, and coverage
|
||||
outputs are now ignored consistently.
|
||||
- MCP tests now cover generated tool discovery, stored-instance workflows, and
|
||||
callable-reference execution paths so the broader server surface does not
|
||||
regress silently.
|
||||
|
||||
## [1.0.3] — 2026-03-24
|
||||
|
||||
### Added
|
||||
@@ -32,6 +98,15 @@ and the project uses [Semantic Versioning](https://semver.org/).
|
||||
benchmark caveats, support-matrix pages, and more explicit release/version
|
||||
consistency guidance.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Python CI now recognizes the top-level metadata API in type stubs, and the
|
||||
derivatives benchmark smoke test no longer depends on importing the
|
||||
`benchmarks` package from an installed wheel layout.
|
||||
- The tag-driven GitHub Release workflow now uses a valid glob trigger and an
|
||||
explicit semantic-version validation step, so pushing `v1.0.3`-style tags
|
||||
correctly creates the release that fans out into the publish jobs.
|
||||
|
||||
## [1.0.2] — 2026-03-24
|
||||
|
||||
### Performance
|
||||
@@ -316,7 +391,9 @@ and the project uses [Semantic Versioning](https://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
[Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.3...HEAD
|
||||
[Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.6...HEAD
|
||||
[1.0.6]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.4...v1.0.6
|
||||
[1.0.4]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.3...v1.0.4
|
||||
[1.0.3]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.2...v1.0.3
|
||||
[1.0.2]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.1...v1.0.2
|
||||
[1.0.1]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.0...v1.0.1
|
||||
|
||||
@@ -36,6 +36,34 @@ uv run ruff check python/ tests/
|
||||
uv run mypy python/ferro_ta --ignore-missing-imports
|
||||
```
|
||||
|
||||
## Git hooks and pre-push checks
|
||||
|
||||
Install the repo-managed git hooks after syncing your environment:
|
||||
|
||||
```bash
|
||||
make hooks
|
||||
```
|
||||
|
||||
That installs both the existing `pre-commit` hook and a `pre-push` hook that
|
||||
runs the local CI gate before anything is pushed.
|
||||
|
||||
To run the same gate manually:
|
||||
|
||||
```bash
|
||||
make prepush
|
||||
```
|
||||
|
||||
To run only part of it while iterating:
|
||||
|
||||
```bash
|
||||
make prepush CHECKS="version changelog python_lint"
|
||||
```
|
||||
|
||||
The pre-push runner covers the basic required CI categories we can execute
|
||||
locally: version/changelog checks, Rust fmt/clippy/core checks, Python
|
||||
lint/typecheck/tests, docs, and WASM. It intentionally skips the multi-version
|
||||
matrix, audit, and benchmark-regression jobs.
|
||||
|
||||
## Alternative: set up with plain pip
|
||||
|
||||
```bash
|
||||
|
||||
Generated
+2
-2
@@ -207,7 +207,7 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "ferro_ta"
|
||||
version = "1.0.3"
|
||||
version = "1.0.6"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"ferro_ta_core",
|
||||
@@ -222,7 +222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ferro_ta_core"
|
||||
version = "1.0.3"
|
||||
version = "1.0.6"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"wide",
|
||||
|
||||
+9
-2
@@ -5,9 +5,16 @@ resolver = "2"
|
||||
|
||||
[package]
|
||||
name = "ferro_ta"
|
||||
version = "1.0.3"
|
||||
version = "1.0.6"
|
||||
edition = "2021"
|
||||
description = "Rust-powered Python technical analysis library with a TA-Lib-compatible API"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/pratikbhadane24/ferro-ta"
|
||||
homepage = "https://github.com/pratikbhadane24/ferro-ta#readme"
|
||||
documentation = "https://pratikbhadane24.github.io/ferro-ta/"
|
||||
keywords = ["technical-analysis", "trading", "indicators", "finance", "ta-lib"]
|
||||
categories = ["finance", "mathematics"]
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
@@ -23,7 +30,7 @@ ndarray = "0.16"
|
||||
rayon = "1.10"
|
||||
log = "0.4"
|
||||
pyo3-log = "0.12"
|
||||
ferro_ta_core = { path = "crates/ferro_ta_core", version = "1.0.3" }
|
||||
ferro_ta_core = { path = "crates/ferro_ta_core", version = "1.0.6" }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ferro-ta development Makefile
|
||||
# Usage: make <target>
|
||||
|
||||
.PHONY: help dev build test lint typecheck fmt docs clean bench version
|
||||
.PHONY: help dev build test lint typecheck fmt docs clean bench version audit prepush hooks
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@@ -17,12 +17,14 @@ help:
|
||||
@echo " make bench Run Rust criterion benchmarks (ferro_ta_core)"
|
||||
@echo " make version Bump tracked version strings (set VERSION=X.Y.Z)"
|
||||
@echo " make audit Run cargo-audit + pip-audit"
|
||||
@echo " make prepush Run the local pre-push CI gate (set CHECKS='version rust_fmt' to scope it)"
|
||||
@echo " make hooks Install pre-commit and pre-push git hooks"
|
||||
@echo " make clean Remove build artefacts"
|
||||
|
||||
dev:
|
||||
pip install uv
|
||||
uv pip install --system maturin numpy pytest pytest-cov pandas polars hypothesis pyyaml \
|
||||
sphinx sphinx-rtd-theme ruff mypy pyright
|
||||
sphinx sphinx-rtd-theme ruff mypy pyright pre-commit
|
||||
|
||||
build:
|
||||
maturin develop --release
|
||||
@@ -57,6 +59,12 @@ audit:
|
||||
cargo audit
|
||||
uv run --with pip-audit pip-audit
|
||||
|
||||
prepush:
|
||||
bash scripts/pre_push_checks.sh $(CHECKS)
|
||||
|
||||
hooks:
|
||||
uv run --with pre-commit pre-commit install --hook-type pre-commit --hook-type pre-push
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
rm -rf dist/ docs/_build/ coverage.xml .coverage *.egg-info
|
||||
|
||||
@@ -70,23 +70,23 @@ rustflags = ["-C", "target-cpu=native"]
|
||||
### Phase 3: Algorithm-Level Optimizations (Target: 5-10x improvement)
|
||||
|
||||
#### SMA — O(n) running sum
|
||||
Current: recomputes each window.
|
||||
Current: recomputes each window.
|
||||
Target: single-pass running sum (already done in Rust — verify SIMD path is hit).
|
||||
|
||||
#### BBANDS — Welford's algorithm
|
||||
Current: compute mean, then variance in two passes.
|
||||
Current: compute mean, then variance in two passes.
|
||||
Target: Welford's online algorithm — single pass, better cache utilization.
|
||||
|
||||
#### ATR/ADX — Avoid redundant True Range calculations
|
||||
Current: ATR → ADX each compute TR independently.
|
||||
Current: ATR → ADX each compute TR independently.
|
||||
Target: Compute TR once, share with ATR, NATR, +DI, -DI, ADX in a single pass.
|
||||
|
||||
#### MACD — Reuse EMA computations
|
||||
Current: Compute fast EMA and slow EMA separately.
|
||||
Current: Compute fast EMA and slow EMA separately.
|
||||
Target: Single function computes both EMAs in one pass.
|
||||
|
||||
#### Candlestick Patterns — Batch lookup table
|
||||
Current: Sequential condition checks per bar.
|
||||
Current: Sequential condition checks per bar.
|
||||
Target: Pre-compute body/shadow ratios, vectorized pattern matching.
|
||||
|
||||
### Phase 4: Streaming Precomputation (Target: 100x for incremental updates)
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
# TA-Lib Compatibility
|
||||
|
||||
`ferro-ta` covers **100% of TA-Lib's function set** (`162+` indicators). This
|
||||
file keeps the full GitHub-facing parity matrix in one place so the root
|
||||
`README.md` can stay product-focused.
|
||||
|
||||
See also:
|
||||
|
||||
- [docs/migration_talib.rst](docs/migration_talib.rst)
|
||||
- [docs/compatibility/talib.md](docs/compatibility/talib.md)
|
||||
- [docs/support_matrix.rst](docs/support_matrix.rst)
|
||||
|
||||
## Legend
|
||||
|
||||
|
||||
| Symbol | Meaning |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| ✅ Exact | Values match TA-Lib to floating-point precision |
|
||||
| ✅ Close | Values match after a short convergence window (EMA-seed difference) |
|
||||
| ⚠️ Corr | Strong correlation (> 0.95) but not numerically identical (Wilder smoothing seed or algorithm variant) |
|
||||
| ⚠️ Shape | Same output shape / NaN structure; values differ due to algorithm variant |
|
||||
| ❌ | Not yet implemented |
|
||||
|
||||
|
||||
## Overlap Studies
|
||||
|
||||
|
||||
| TA-Lib Function | ferro-ta | Accuracy | Notes |
|
||||
| --------------- | -------- | -------- | ----------------------------------------------------- |
|
||||
| `BBANDS` | ✅ | ✅ Exact | Bollinger Bands |
|
||||
| `DEMA` | ✅ | ✅ Close | Double EMA; converges after ~20 bars |
|
||||
| `EMA` | ✅ | ✅ Close | Exponential Moving Average; converges after ~20 bars |
|
||||
| `KAMA` | ✅ | ✅ Exact | Kaufman Adaptive MA (values match after seed bar) |
|
||||
| `MA` | ✅ | ✅ Exact | Moving average (generic, type-selectable) |
|
||||
| `MAMA` | ✅ | ⚠️ Corr | MESA Adaptive MA |
|
||||
| `MAVP` | ✅ | ✅ Exact | MA with variable period |
|
||||
| `MIDPOINT` | ✅ | ✅ Exact | Midpoint over period |
|
||||
| `MIDPRICE` | ✅ | ✅ Exact | Midpoint price over period |
|
||||
| `SAR` | ✅ | ⚠️ Shape | Parabolic SAR (same shape; reversal history diverges) |
|
||||
| `SAREXT` | ✅ | ⚠️ Shape | Parabolic SAR Extended |
|
||||
| `SMA` | ✅ | ✅ Exact | Simple Moving Average |
|
||||
| `T3` | ✅ | ✅ Close | Triple Exponential MA (T3); converges after ~50 bars |
|
||||
| `TEMA` | ✅ | ✅ Close | Triple EMA; converges after ~20 bars |
|
||||
| `TRIMA` | ✅ | ✅ Exact | Triangular Moving Average |
|
||||
| `WMA` | ✅ | ✅ Exact | Weighted Moving Average |
|
||||
|
||||
|
||||
## Momentum Indicators
|
||||
|
||||
|
||||
| TA-Lib Function | ferro-ta | Accuracy | Notes |
|
||||
| --------------- | -------- | -------- | ---------------------------------------------------------------------------- |
|
||||
| `ADX` | ✅ | ✅ Close | Avg Directional Movement Index (TA-Lib Wilder sum-seeding) |
|
||||
| `ADXR` | ✅ | ✅ Close | ADX Rating (inherits ADX; TA-Lib seeding) |
|
||||
| `APO` | ✅ | ✅ Close | Absolute Price Oscillator (EMA-based) |
|
||||
| `AROON` | ✅ | ✅ Exact | Aroon Up/Down |
|
||||
| `AROONOSC` | ✅ | ✅ Exact | Aroon Oscillator |
|
||||
| `BOP` | ✅ | ✅ Exact | Balance Of Power |
|
||||
| `CCI` | ✅ | ✅ Exact | Commodity Channel Index (TA-Lib-compatible MAD formula) |
|
||||
| `CMO` | ✅ | ✅ Close | Chande Momentum Oscillator (rolling window, TA-Lib-compatible) |
|
||||
| `DX` | ✅ | ✅ Close | Directional Movement Index (TA-Lib Wilder sum-seeding) |
|
||||
| `MACD` | ✅ | ✅ Close | MACD (EMA-based; converges after ~30 bars) |
|
||||
| `MACDEXT` | ✅ | ✅ Close | MACD with controllable MA type (EMA-based; converges) |
|
||||
| `MACDFIX` | ✅ | ✅ Close | MACD Fixed 12/26 (EMA-based; converges) |
|
||||
| `MFI` | ✅ | ✅ Exact | Money Flow Index |
|
||||
| `MINUS_DI` | ✅ | ✅ Close | Minus Directional Indicator (TA-Lib Wilder sum-seeding) |
|
||||
| `MINUS_DM` | ✅ | ✅ Close | Minus Directional Movement (TA-Lib Wilder sum-seeding) |
|
||||
| `MOM` | ✅ | ✅ Exact | Momentum |
|
||||
| `PLUS_DI` | ✅ | ✅ Close | Plus Directional Indicator (TA-Lib Wilder sum-seeding) |
|
||||
| `PLUS_DM` | ✅ | ✅ Close | Plus Directional Movement (TA-Lib Wilder sum-seeding) |
|
||||
| `PPO` | ✅ | ✅ Close | Percentage Price Oscillator (EMA-based) |
|
||||
| `ROC` | ✅ | ✅ Exact | Rate of Change |
|
||||
| `ROCP` | ✅ | ✅ Exact | Rate of Change Percentage |
|
||||
| `ROCR` | ✅ | ✅ Exact | Rate of Change Ratio |
|
||||
| `ROCR100` | ✅ | ✅ Exact | Rate of Change Ratio × 100 |
|
||||
| `RSI` | ✅ | ✅ Close | Relative Strength Index (TA-Lib Wilder seeding; converges after ~1 seed bar) |
|
||||
| `STOCH` | ✅ | ✅ Close | Stochastic (TA-Lib-compatible SMA smoothing for slowk and slowd) |
|
||||
| `STOCHF` | ✅ | ✅ Exact | Stochastic Fast (%K exact; %D NaN offset ±2) |
|
||||
| `STOCHRSI` | ✅ | ✅ Close | Stochastic RSI (TA-Lib-compatible; SMA fastd, Wilder-seeded RSI) |
|
||||
| `TRIX` | ✅ | ✅ Close | 1-day ROC of Triple EMA (EMA-based; converges) |
|
||||
| `ULTOSC` | ✅ | ✅ Exact | Ultimate Oscillator |
|
||||
| `WILLR` | ✅ | ✅ Exact | Williams' %R |
|
||||
|
||||
|
||||
## Volume Indicators
|
||||
|
||||
|
||||
| TA-Lib Function | ferro-ta | Accuracy | Notes |
|
||||
| --------------- | -------- | -------- | ------------------------------------------------------------------ |
|
||||
| `AD` | ✅ | ✅ Exact | Chaikin A/D Line |
|
||||
| `ADOSC` | ✅ | ✅ Exact | Chaikin A/D Oscillator |
|
||||
| `OBV` | ✅ | ✅ Exact | On Balance Volume (increments identical; constant offset at bar 0) |
|
||||
|
||||
|
||||
## Volatility Indicators
|
||||
|
||||
|
||||
| TA-Lib Function | ferro-ta | Accuracy | Notes |
|
||||
| --------------- | -------- | -------- | ----------------------------------------------------------------------- |
|
||||
| `ATR` | ✅ | ✅ Close | Average True Range (TA-Lib Wilder seeding; matches from bar timeperiod) |
|
||||
| `NATR` | ✅ | ✅ Close | Normalized ATR (TA-Lib Wilder seeding) |
|
||||
| `TRANGE` | ✅ | ✅ Exact | True Range (bar 0 differs; all others identical) |
|
||||
|
||||
|
||||
## Cycle Indicators
|
||||
|
||||
|
||||
| TA-Lib Function | ferro-ta | Accuracy | Notes |
|
||||
| --------------- | -------- | -------- | ---------------------------------------------------------- |
|
||||
| `HT_DCPERIOD` | ✅ | ⚠️ Shape | Hilbert Transform Dominant Cycle Period (Ehlers algorithm) |
|
||||
| `HT_DCPHASE` | ✅ | ⚠️ Shape | Hilbert Transform Dominant Cycle Phase |
|
||||
| `HT_PHASOR` | ✅ | ⚠️ Shape | Hilbert Transform Phasor Components (inphase, quadrature) |
|
||||
| `HT_SINE` | ✅ | ⚠️ Shape | Hilbert Transform SineWave (sine, leadsine) |
|
||||
| `HT_TRENDLINE` | ✅ | ⚠️ Shape | Hilbert Transform Instantaneous Trendline |
|
||||
| `HT_TRENDMODE` | ✅ | ⚠️ Shape | Hilbert Transform Trend vs Cycle Mode (1=trend, 0=cycle) |
|
||||
|
||||
|
||||
## Price Transformations
|
||||
|
||||
|
||||
| TA-Lib Function | ferro-ta | Accuracy | Notes |
|
||||
| --------------- | -------- | -------- | -------------------- |
|
||||
| `AVGPRICE` | ✅ | ✅ Exact | Average Price |
|
||||
| `MEDPRICE` | ✅ | ✅ Exact | Median Price |
|
||||
| `TYPPRICE` | ✅ | ✅ Exact | Typical Price |
|
||||
| `WCLPRICE` | ✅ | ✅ Exact | Weighted Close Price |
|
||||
|
||||
|
||||
## Statistic Functions
|
||||
|
||||
|
||||
| TA-Lib Function | ferro-ta | Accuracy | Notes |
|
||||
| --------------------- | -------- | -------- | ----------------------------------------------------------- |
|
||||
| `BETA` | ✅ | ✅ Close | Beta coefficient (returns-based regression matching TA-Lib) |
|
||||
| `CORREL` | ✅ | ✅ Exact | Pearson Correlation Coefficient |
|
||||
| `LINEARREG` | ✅ | ✅ Exact | Linear Regression |
|
||||
| `LINEARREG_ANGLE` | ✅ | ✅ Exact | Linear Regression Angle |
|
||||
| `LINEARREG_INTERCEPT` | ✅ | ✅ Exact | Linear Regression Intercept |
|
||||
| `LINEARREG_SLOPE` | ✅ | ✅ Exact | Linear Regression Slope |
|
||||
| `STDDEV` | ✅ | ✅ Exact | Standard Deviation |
|
||||
| `TSF` | ✅ | ✅ Exact | Time Series Forecast |
|
||||
| `VAR` | ✅ | ✅ Exact | Variance |
|
||||
|
||||
|
||||
## Pattern Recognition
|
||||
|
||||
`ferro-ta` implements all 61 candlestick patterns. All return the same
|
||||
`{-100, 0, 100}` convention as TA-Lib. Pattern thresholds may differ slightly
|
||||
from the full TA-Lib implementation.
|
||||
|
||||
|
||||
| TA-Lib Function | ferro-ta | Notes |
|
||||
| --------------------- | -------- | --------------------------------------------------- |
|
||||
| `CDL2CROWS` | ✅ | Two Crows |
|
||||
| `CDL3BLACKCROWS` | ✅ | Three Black Crows |
|
||||
| `CDL3INSIDE` | ✅ | Three Inside Up/Down |
|
||||
| `CDL3LINESTRIKE` | ✅ | Three-Line Strike |
|
||||
| `CDL3OUTSIDE` | ✅ | Three Outside Up/Down |
|
||||
| `CDL3STARSINSOUTH` | ✅ | Three Stars In The South |
|
||||
| `CDL3WHITESOLDIERS` | ✅ | Three Advancing White Soldiers |
|
||||
| `CDLABANDONEDBABY` | ✅ | Abandoned Baby |
|
||||
| `CDLADVANCEBLOCK` | ✅ | Advance Block |
|
||||
| `CDLBELTHOLD` | ✅ | Belt-hold |
|
||||
| `CDLBREAKAWAY` | ✅ | Breakaway |
|
||||
| `CDLCLOSINGMARUBOZU` | ✅ | Closing Marubozu |
|
||||
| `CDLCONCEALBABYSWALL` | ✅ | Concealing Baby Swallow |
|
||||
| `CDLCOUNTERATTACK` | ✅ | Counterattack |
|
||||
| `CDLDARKCLOUDCOVER` | ✅ | Dark Cloud Cover |
|
||||
| `CDLDOJI` | ✅ | Doji |
|
||||
| `CDLDOJISTAR` | ✅ | Doji Star |
|
||||
| `CDLDRAGONFLYDOJI` | ✅ | Dragonfly Doji |
|
||||
| `CDLENGULFING` | ✅ | Engulfing Pattern |
|
||||
| `CDLEVENINGDOJISTAR` | ✅ | Evening Doji Star |
|
||||
| `CDLEVENINGSTAR` | ✅ | Evening Star |
|
||||
| `CDLGAPSIDESIDEWHITE` | ✅ | Up/Down-gap side-by-side white lines |
|
||||
| `CDLGRAVESTONEDOJI` | ✅ | Gravestone Doji |
|
||||
| `CDLHAMMER` | ✅ | Hammer |
|
||||
| `CDLHANGINGMAN` | ✅ | Hanging Man |
|
||||
| `CDLHARAMI` | ✅ | Harami Pattern |
|
||||
| `CDLHARAMICROSS` | ✅ | Harami Cross Pattern |
|
||||
| `CDLHIGHWAVE` | ✅ | High-Wave Candle |
|
||||
| `CDLHIKKAKE` | ✅ | Hikkake Pattern |
|
||||
| `CDLHIKKAKEMOD` | ✅ | Modified Hikkake Pattern |
|
||||
| `CDLHOMINGPIGEON` | ✅ | Homing Pigeon |
|
||||
| `CDLIDENTICAL3CROWS` | ✅ | Identical Three Crows |
|
||||
| `CDLINNECK` | ✅ | In-Neck Pattern |
|
||||
| `CDLINVERTEDHAMMER` | ✅ | Inverted Hammer |
|
||||
| `CDLKICKING` | ✅ | Kicking |
|
||||
| `CDLKICKINGBYLENGTH` | ✅ | Kicking by the longer Marubozu |
|
||||
| `CDLLADDERBOTTOM` | ✅ | Ladder Bottom |
|
||||
| `CDLLONGLEGGEDDOJI` | ✅ | Long Legged Doji |
|
||||
| `CDLLONGLINE` | ✅ | Long Line Candle |
|
||||
| `CDLMARUBOZU` | ✅ | Marubozu |
|
||||
| `CDLMATCHINGLOW` | ✅ | Matching Low |
|
||||
| `CDLMATHOLD` | ✅ | Mat Hold |
|
||||
| `CDLMORNINGDOJISTAR` | ✅ | Morning Doji Star |
|
||||
| `CDLMORNINGSTAR` | ✅ | Morning Star |
|
||||
| `CDLONNECK` | ✅ | On-Neck Pattern |
|
||||
| `CDLPIERCING` | ✅ | Piercing Pattern |
|
||||
| `CDLRICKSHAWMAN` | ✅ | Rickshaw Man |
|
||||
| `CDLRISEFALL3METHODS` | ✅ | Rising/Falling Three Methods |
|
||||
| `CDLSEPARATINGLINES` | ✅ | Separating Lines |
|
||||
| `CDLSHOOTINGSTAR` | ✅ | Shooting Star |
|
||||
| `CDLSHORTLINE` | ✅ | Short Line Candle |
|
||||
| `CDLSPINNINGTOP` | ✅ | Spinning Top |
|
||||
| `CDLSTALLEDPATTERN` | ✅ | Stalled Pattern |
|
||||
| `CDLSTICKSANDWICH` | ✅ | Stick Sandwich |
|
||||
| `CDLTAKURI` | ✅ | Takuri (Dragonfly Doji with very long lower shadow) |
|
||||
| `CDLTASUKIGAP` | ✅ | Tasuki Gap |
|
||||
| `CDLTHRUSTING` | ✅ | Thrusting Pattern |
|
||||
| `CDLTRISTAR` | ✅ | Tristar Pattern |
|
||||
| `CDLUNIQUE3RIVER` | ✅ | Unique 3 River |
|
||||
| `CDLUPSIDEGAP2CROWS` | ✅ | Upside Gap Two Crows |
|
||||
| `CDLXSIDEGAP3METHODS` | ✅ | Upside/Downside Gap Three Methods |
|
||||
|
||||
|
||||
## Math Operators / Math Transforms
|
||||
|
||||
`ferro-ta` provides TA-Lib-compatible wrappers for all arithmetic and
|
||||
math-transform functions. Rolling functions (`SUM`, `MAX`, `MIN`) produce `NaN`
|
||||
for the first `timeperiod - 1` bars.
|
||||
|
||||
|
||||
| TA-Lib Function | ferro-ta | Notes |
|
||||
| ------------------------ | -------- | ----------------------------- |
|
||||
| `ADD` | ✅ | Element-wise addition |
|
||||
| `SUB` | ✅ | Element-wise subtraction |
|
||||
| `MULT` | ✅ | Element-wise multiplication |
|
||||
| `DIV` | ✅ | Element-wise division |
|
||||
| `SUM` | ✅ | Rolling sum over *timeperiod* |
|
||||
| `MAX` / `MAXINDEX` | ✅ | Rolling maximum / index |
|
||||
| `MIN` / `MININDEX` | ✅ | Rolling minimum / index |
|
||||
| `ACOS` / `ASIN` / `ATAN` | ✅ | Arc trig transforms |
|
||||
| `CEIL` / `FLOOR` | ✅ | Round up / down |
|
||||
| `COS` / `SIN` / `TAN` | ✅ | Trig transforms |
|
||||
| `COSH` / `SINH` / `TANH` | ✅ | Hyperbolic transforms |
|
||||
| `EXP` / `LN` / `LOG10` | ✅ | Exponential / log transforms |
|
||||
| `SQRT` | ✅ | Square root |
|
||||
|
||||
|
||||
## Implementation Coverage Summary
|
||||
|
||||
|
||||
| Category | Implemented | Not Implemented |
|
||||
| --------------------------- | ----------- | --------------- |
|
||||
| Overlap Studies | 19 | 0 |
|
||||
| Momentum Indicators | 28 | 0 |
|
||||
| Volume Indicators | 3 | 0 |
|
||||
| Volatility Indicators | 3 | 0 |
|
||||
| Cycle Indicators | 6 | 0 |
|
||||
| Price Transforms | 4 | 0 |
|
||||
| Statistic Functions | 9 | 0 |
|
||||
| Pattern Recognition | 61 | 0 |
|
||||
| Math Operators / Transforms | 24 | 0 |
|
||||
| Extended Indicators | 10 | - |
|
||||
| Streaming Classes | 9 | - |
|
||||
| **Total** | **162+** | **0** |
|
||||
|
||||
|
||||
> `ferro-ta` implements 100% of TA-Lib's function set. NaN values are placed
|
||||
> at the beginning of each output array for the warmup period.
|
||||
+18
-18
@@ -77,7 +77,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -117,12 +117,12 @@ app = FastAPI(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _nan_to_none(arr: np.ndarray) -> List[Optional[float]]:
|
||||
def _nan_to_none(arr: np.ndarray) -> list[float | None]:
|
||||
"""Convert numpy array to list, replacing NaN/Inf with None."""
|
||||
return [None if not math.isfinite(v) else float(v) for v in arr]
|
||||
|
||||
|
||||
def _validate_series(close: List[float]) -> np.ndarray:
|
||||
def _validate_series(close: list[float]) -> np.ndarray:
|
||||
if len(close) > MAX_SERIES_LENGTH:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
@@ -142,54 +142,54 @@ def _validate_series(close: List[float]) -> np.ndarray:
|
||||
|
||||
|
||||
class IndicatorRequest(BaseModel):
|
||||
close: List[float] = Field(..., description="Close price series")
|
||||
close: list[float] = Field(..., description="Close price series")
|
||||
timeperiod: int = Field(default=14, ge=1, description="Look-back period")
|
||||
|
||||
@field_validator("close")
|
||||
@classmethod
|
||||
def close_must_be_finite(cls, v: List[float]) -> List[float]:
|
||||
def close_must_be_finite(cls, v: list[float]) -> list[float]:
|
||||
if not all(math.isfinite(x) for x in v):
|
||||
raise ValueError("close series must contain only finite values")
|
||||
return v
|
||||
|
||||
|
||||
class MACDRequest(BaseModel):
|
||||
close: List[float] = Field(..., description="Close price series")
|
||||
close: list[float] = Field(..., description="Close price series")
|
||||
fastperiod: int = Field(default=12, ge=1)
|
||||
slowperiod: int = Field(default=26, ge=1)
|
||||
signalperiod: int = Field(default=9, ge=1)
|
||||
|
||||
@field_validator("close")
|
||||
@classmethod
|
||||
def close_must_be_finite(cls, v: List[float]) -> List[float]:
|
||||
def close_must_be_finite(cls, v: list[float]) -> list[float]:
|
||||
if not all(math.isfinite(x) for x in v):
|
||||
raise ValueError("close series must contain only finite values")
|
||||
return v
|
||||
|
||||
|
||||
class BBANDSRequest(BaseModel):
|
||||
close: List[float] = Field(..., description="Close price series")
|
||||
close: list[float] = Field(..., description="Close price series")
|
||||
timeperiod: int = Field(default=5, ge=2)
|
||||
nbdevup: float = Field(default=2.0, gt=0)
|
||||
nbdevdn: float = Field(default=2.0, gt=0)
|
||||
|
||||
@field_validator("close")
|
||||
@classmethod
|
||||
def close_must_be_finite(cls, v: List[float]) -> List[float]:
|
||||
def close_must_be_finite(cls, v: list[float]) -> list[float]:
|
||||
if not all(math.isfinite(x) for x in v):
|
||||
raise ValueError("close series must contain only finite values")
|
||||
return v
|
||||
|
||||
|
||||
class BacktestRequest(BaseModel):
|
||||
close: List[float] = Field(..., description="Close price series")
|
||||
close: list[float] = Field(..., description="Close price series")
|
||||
strategy: str = Field(default="rsi_30_70")
|
||||
commission_per_trade: float = Field(default=0.0, ge=0.0)
|
||||
slippage_bps: float = Field(default=0.0, ge=0.0)
|
||||
|
||||
@field_validator("close")
|
||||
@classmethod
|
||||
def close_must_be_finite(cls, v: List[float]) -> List[float]:
|
||||
def close_must_be_finite(cls, v: list[float]) -> list[float]:
|
||||
if not all(math.isfinite(x) for x in v):
|
||||
raise ValueError("close series must contain only finite values")
|
||||
return v
|
||||
@@ -201,13 +201,13 @@ class BacktestRequest(BaseModel):
|
||||
|
||||
|
||||
@app.get("/health", summary="Health check")
|
||||
def health() -> Dict[str, str]:
|
||||
def health() -> dict[str, str]:
|
||||
"""Readiness / liveness probe."""
|
||||
return {"status": "ok", "version": app.version}
|
||||
|
||||
|
||||
@app.post("/indicators/sma", summary="Simple Moving Average")
|
||||
def compute_sma(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
def compute_sma(req: IndicatorRequest) -> dict[str, Any]:
|
||||
"""Compute Simple Moving Average (SMA).
|
||||
|
||||
Returns ``result``: list of floats (null for warm-up bars).
|
||||
@@ -218,7 +218,7 @@ def compute_sma(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/indicators/ema", summary="Exponential Moving Average")
|
||||
def compute_ema(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
def compute_ema(req: IndicatorRequest) -> dict[str, Any]:
|
||||
"""Compute Exponential Moving Average (EMA)."""
|
||||
c = _validate_series(req.close)
|
||||
out = np.asarray(ft.EMA(c, timeperiod=req.timeperiod), dtype=np.float64)
|
||||
@@ -226,7 +226,7 @@ def compute_ema(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/indicators/rsi", summary="Relative Strength Index")
|
||||
def compute_rsi(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
def compute_rsi(req: IndicatorRequest) -> dict[str, Any]:
|
||||
"""Compute Relative Strength Index (RSI)."""
|
||||
c = _validate_series(req.close)
|
||||
out = np.asarray(ft.RSI(c, timeperiod=req.timeperiod), dtype=np.float64)
|
||||
@@ -234,7 +234,7 @@ def compute_rsi(req: IndicatorRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/indicators/macd", summary="MACD")
|
||||
def compute_macd(req: MACDRequest) -> Dict[str, Any]:
|
||||
def compute_macd(req: MACDRequest) -> dict[str, Any]:
|
||||
"""Compute MACD (line, signal, histogram).
|
||||
|
||||
Returns ``result`` with keys ``macd``, ``signal``, ``hist``.
|
||||
@@ -256,7 +256,7 @@ def compute_macd(req: MACDRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/indicators/bbands", summary="Bollinger Bands")
|
||||
def compute_bbands(req: BBANDSRequest) -> Dict[str, Any]:
|
||||
def compute_bbands(req: BBANDSRequest) -> dict[str, Any]:
|
||||
"""Compute Bollinger Bands (upper, middle, lower).
|
||||
|
||||
Returns ``result`` with keys ``upper``, ``middle``, ``lower``.
|
||||
@@ -278,7 +278,7 @@ def compute_bbands(req: BBANDSRequest) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@app.post("/backtest", summary="Vectorized backtest")
|
||||
def run_backtest(req: BacktestRequest) -> Dict[str, Any]:
|
||||
def run_backtest(req: BacktestRequest) -> dict[str, Any]:
|
||||
"""Run a vectorized backtest using a named strategy.
|
||||
|
||||
Strategies: ``rsi_30_70``, ``sma_crossover``, ``macd_crossover``.
|
||||
|
||||
@@ -68,4 +68,4 @@
|
||||
"speedup_vs_separate": 2.2811
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,4 +1159,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1379,4 +1379,4 @@
|
||||
"outcome": "ferro_ta_win"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,4 +160,4 @@
|
||||
"elapsed_ms": 0.0026
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,4 @@
|
||||
"sha256": "f31fd871990c44e24a2259d618ae40a52866d20b95aa6047af3d38b9371c2ab7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,4 +93,4 @@
|
||||
"share_of_suite_pct": 0.09
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,4 +282,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,4 +70,4 @@
|
||||
"stream_over_batch_ratio": 121.7603
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,13 +48,17 @@ def run_batch_benchmark(
|
||||
"SMA",
|
||||
lambda: ferro_ta.batch.batch_sma(close2d, timeperiod=14, parallel=True),
|
||||
lambda: ferro_ta.batch.batch_sma(close2d, timeperiod=14, parallel=False),
|
||||
lambda: [ferro_ta.SMA(close2d[:, j], timeperiod=14) for j in range(n_series)],
|
||||
lambda: [
|
||||
ferro_ta.SMA(close2d[:, j], timeperiod=14) for j in range(n_series)
|
||||
],
|
||||
),
|
||||
(
|
||||
"RSI",
|
||||
lambda: ferro_ta.batch.batch_rsi(close2d, timeperiod=14, parallel=True),
|
||||
lambda: ferro_ta.batch.batch_rsi(close2d, timeperiod=14, parallel=False),
|
||||
lambda: [ferro_ta.RSI(close2d[:, j], timeperiod=14) for j in range(n_series)],
|
||||
lambda: [
|
||||
ferro_ta.RSI(close2d[:, j], timeperiod=14) for j in range(n_series)
|
||||
],
|
||||
),
|
||||
(
|
||||
"ATR",
|
||||
@@ -201,7 +205,9 @@ def main() -> int:
|
||||
if payload["grouped_results"]:
|
||||
print("\nGrouped Multi-Indicator Calls")
|
||||
print("-" * 64)
|
||||
print(f"{'Case':<18} {'Grouped (ms)':>14} {'Separate (ms)':>16} {'Speedup':>12}")
|
||||
print(
|
||||
f"{'Case':<18} {'Grouped (ms)':>14} {'Separate (ms)':>16} {'Speedup':>12}"
|
||||
)
|
||||
print("-" * 64)
|
||||
for row in payload["grouped_results"]:
|
||||
print(
|
||||
|
||||
@@ -26,9 +26,10 @@ import math
|
||||
import sys
|
||||
import time
|
||||
import tracemalloc
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -70,9 +70,7 @@ def run_simd_benchmark(
|
||||
for label, args in variants
|
||||
}
|
||||
|
||||
portable_rows = {
|
||||
row["name"]: row for row in reports["portable_release"]["results"]
|
||||
}
|
||||
portable_rows = {row["name"]: row for row in reports["portable_release"]["results"]}
|
||||
simd_rows = {row["name"]: row for row in reports["simd_release"]["results"]}
|
||||
|
||||
comparison: list[dict[str, Any]] = []
|
||||
@@ -136,9 +134,7 @@ def main() -> int:
|
||||
window=args.window,
|
||||
)
|
||||
|
||||
print(
|
||||
f"{'Case':<20} {'Portable (ms)':>14} {'SIMD (ms)':>12} {'SIMD speedup':>14}"
|
||||
)
|
||||
print(f"{'Case':<20} {'Portable (ms)':>14} {'SIMD (ms)':>12} {'SIMD speedup':>14}")
|
||||
print("-" * 64)
|
||||
for row in payload["results"]:
|
||||
print(
|
||||
|
||||
@@ -57,7 +57,9 @@ def _stream_hlcv(
|
||||
) -> float:
|
||||
streamer = factory()
|
||||
last = np.nan
|
||||
for high_value, low_value, close_value, volume_value in zip(high, low, close, volume):
|
||||
for high_value, low_value, close_value, volume_value in zip(
|
||||
high, low, close, volume
|
||||
):
|
||||
last = streamer.update(
|
||||
float(high_value),
|
||||
float(low_value),
|
||||
@@ -154,7 +156,9 @@ def run_streaming_benchmark(
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Benchmark streaming indicator execution.")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Benchmark streaming indicator execution."
|
||||
)
|
||||
parser.add_argument("--bars", type=int, default=100_000)
|
||||
parser.add_argument("--seed", type=int, default=2026)
|
||||
parser.add_argument("--json", dest="json_path")
|
||||
|
||||
@@ -309,9 +309,13 @@ def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, An
|
||||
|
||||
if not TALIB_AVAILABLE:
|
||||
print("Note: ta-lib not installed. Reporting ferro_ta timings only.")
|
||||
print("Install with: pip install ta-lib (or conda install ta-lib) for comparison.\n")
|
||||
print(
|
||||
"Install with: pip install ta-lib (or conda install ta-lib) for comparison.\n"
|
||||
)
|
||||
|
||||
print(f"\nferro_ta vs TA-Lib — median of {N_RUNS} measured runs after {N_WARMUP} warmup")
|
||||
print(
|
||||
f"\nferro_ta vs TA-Lib — median of {N_RUNS} measured runs after {N_WARMUP} warmup"
|
||||
)
|
||||
print(f"Sizes: {sizes}")
|
||||
print()
|
||||
|
||||
@@ -328,11 +332,15 @@ def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, An
|
||||
if size == 1_000_000 and name in SKIP_1M_FOR:
|
||||
continue
|
||||
|
||||
ft_samples_ms = _timed_runs_ms(ft_run, open_, high, low, close, volume, size)
|
||||
ft_samples_ms = _timed_runs_ms(
|
||||
ft_run, open_, high, low, close, volume, size
|
||||
)
|
||||
ft_stats = _summary_stats(ft_samples_ms)
|
||||
ft_median_ms = float(ft_stats["median_ms"])
|
||||
ft_m_bars_s = _throughput_m_bars_s(size, ft_median_ms)
|
||||
ft_peak_bytes = _python_peak_bytes(ft_run, open_, high, low, close, volume, size)
|
||||
ft_peak_bytes = _python_peak_bytes(
|
||||
ft_run, open_, high, low, close, volume, size
|
||||
)
|
||||
|
||||
row: dict[str, Any] = {
|
||||
"indicator": name,
|
||||
@@ -351,11 +359,15 @@ def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, An
|
||||
}
|
||||
|
||||
if TALIB_AVAILABLE:
|
||||
ta_samples_ms = _timed_runs_ms(ta_run, open_, high, low, close, volume, size)
|
||||
ta_samples_ms = _timed_runs_ms(
|
||||
ta_run, open_, high, low, close, volume, size
|
||||
)
|
||||
ta_stats = _summary_stats(ta_samples_ms)
|
||||
ta_median_ms = float(ta_stats["median_ms"])
|
||||
ta_m_bars_s = _throughput_m_bars_s(size, ta_median_ms)
|
||||
speedup = ta_median_ms / ft_median_ms if ft_median_ms > 0 else float("inf")
|
||||
speedup = (
|
||||
ta_median_ms / ft_median_ms if ft_median_ms > 0 else float("inf")
|
||||
)
|
||||
outcome = _outcome(speedup)
|
||||
ta_peak_bytes = _python_peak_bytes(
|
||||
ta_run, open_, high, low, close, volume, size
|
||||
|
||||
@@ -9,7 +9,9 @@ Reads results.json and prints a markdown table: all indicators × all libraries.
|
||||
Unsupported (indicator, library) pairs show N/A. Supported pairs missing benchmark
|
||||
data show ERR (indicating the benchmark run was incomplete or failed).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -21,9 +23,11 @@ if _root not in (Path(p).resolve() for p in sys.path):
|
||||
|
||||
from benchmarks.wrapper_registry import (
|
||||
INDICATOR_CATEGORIES,
|
||||
LIBRARY_NAMES as LIBS,
|
||||
is_supported,
|
||||
)
|
||||
from benchmarks.wrapper_registry import (
|
||||
LIBRARY_NAMES as LIBS,
|
||||
)
|
||||
|
||||
|
||||
def _all_indicators() -> list[str]:
|
||||
@@ -34,11 +38,17 @@ def _all_indicators() -> list[str]:
|
||||
def main():
|
||||
p = Path(__file__).parent / "results.json"
|
||||
if not p.exists():
|
||||
print("Run: pytest benchmarks/test_speed.py --benchmark-only --benchmark-json=benchmarks/results.json -v", file=sys.stderr)
|
||||
print(
|
||||
"Run: pytest benchmarks/test_speed.py --benchmark-only --benchmark-json=benchmarks/results.json -v",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
raw = p.read_text().strip()
|
||||
if not raw:
|
||||
print("results.json is empty. Run the full benchmark suite first.", file=sys.stderr)
|
||||
print(
|
||||
"results.json is empty. Run the full benchmark suite first.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
|
||||
@@ -88,7 +88,9 @@ def main() -> int:
|
||||
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not data.get("talib_available", False):
|
||||
print("ERROR: TA-Lib was not available; cannot enforce TA-Lib regression policy.")
|
||||
print(
|
||||
"ERROR: TA-Lib was not available; cannot enforce TA-Lib regression policy."
|
||||
)
|
||||
return 1
|
||||
|
||||
summary_by_size = {
|
||||
@@ -133,9 +135,7 @@ def main() -> int:
|
||||
)
|
||||
|
||||
if rows < args.min_rows:
|
||||
failures.append(
|
||||
f"size={size} rows {rows} < min_rows {args.min_rows}"
|
||||
)
|
||||
failures.append(f"size={size} rows {rows} < min_rows {args.min_rows}")
|
||||
if med < median_floor.get(size, float("-inf")):
|
||||
failures.append(
|
||||
f"size={size} median_speedup {med:.4f} < floor {median_floor[size]:.4f}"
|
||||
|
||||
@@ -50,11 +50,17 @@ def _naive_beta(x: np.ndarray, y: np.ndarray, window: int) -> np.ndarray:
|
||||
for end in range(window, len(x)):
|
||||
start = end - window
|
||||
rx = np.array(
|
||||
[x[idx + 1] / x[idx] - 1.0 if x[idx] != 0.0 else np.nan for idx in range(start, end)],
|
||||
[
|
||||
x[idx + 1] / x[idx] - 1.0 if x[idx] != 0.0 else np.nan
|
||||
for idx in range(start, end)
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
ry = np.array(
|
||||
[y[idx + 1] / y[idx] - 1.0 if y[idx] != 0.0 else np.nan for idx in range(start, end)],
|
||||
[
|
||||
y[idx + 1] / y[idx] - 1.0 if y[idx] != 0.0 else np.nan
|
||||
for idx in range(start, end)
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
mean_x = float(np.sum(rx)) / window
|
||||
@@ -120,7 +126,12 @@ def build_hotspot_report(
|
||||
high = close + rng.uniform(0.1, 2.0, price_bars)
|
||||
low = close - rng.uniform(0.1, 2.0, price_bars)
|
||||
iv = rng.uniform(10.0, 40.0, iv_bars).astype(np.float64)
|
||||
ohlcv = {"close": close, "high": high, "low": low, "volume": np.full(price_bars, 1000.0)}
|
||||
ohlcv = {
|
||||
"close": close,
|
||||
"high": high,
|
||||
"low": low,
|
||||
"volume": np.full(price_bars, 1000.0),
|
||||
}
|
||||
|
||||
rows = [
|
||||
(
|
||||
@@ -218,7 +229,9 @@ def build_hotspot_report(
|
||||
results.sort(key=lambda row: row["fast_ms"], reverse=True)
|
||||
total_fast_ms = sum(float(row["fast_ms"]) for row in results) or 1.0
|
||||
for row in results:
|
||||
row["share_of_suite_pct"] = round(float(row["fast_ms"]) / total_fast_ms * 100.0, 2)
|
||||
row["share_of_suite_pct"] = round(
|
||||
float(row["fast_ms"]) / total_fast_ms * 100.0, 2
|
||||
)
|
||||
|
||||
return {
|
||||
"metadata": benchmark_metadata(
|
||||
@@ -249,7 +262,9 @@ def main() -> int:
|
||||
window=args.window,
|
||||
)
|
||||
|
||||
print(f"{'Category':<16} {'Case':<18} {'Fast (ms)':>10} {'Ref (ms)':>10} {'Speedup':>10}")
|
||||
print(
|
||||
f"{'Category':<16} {'Case':<18} {'Fast (ms)':>10} {'Ref (ms)':>10} {'Speedup':>10}"
|
||||
)
|
||||
print("-" * 70)
|
||||
for row in payload["results"]:
|
||||
print(
|
||||
|
||||
@@ -16094,4 +16094,4 @@
|
||||
],
|
||||
"datetime": "2026-03-23T17:14:05.427766+00:00",
|
||||
"version": "5.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ def build_indicator_latency_report(*, rounds: int = 5) -> dict[str, Any]:
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
for entry in INDICATOR_SUITE:
|
||||
elapsed_ms = _time_min(lambda entry=entry: _run_indicator(entry, ohlcv), rounds=rounds)
|
||||
elapsed_ms = _time_min(
|
||||
lambda entry=entry: _run_indicator(entry, ohlcv), rounds=rounds
|
||||
)
|
||||
rows.append(
|
||||
{
|
||||
"name": entry["name"],
|
||||
@@ -192,10 +194,7 @@ def main() -> int:
|
||||
fixtures=[FIXTURE_PATH],
|
||||
extra={"output_dir": str(output_dir)},
|
||||
),
|
||||
"artifacts": {
|
||||
name: file_info(path)
|
||||
for name, path in artifacts.items()
|
||||
},
|
||||
"artifacts": {name: file_info(path) for name, path in artifacts.items()},
|
||||
}
|
||||
manifest_path = output_dir / "manifest.json"
|
||||
_write_json(manifest_path, manifest)
|
||||
|
||||
+60
-53
@@ -5,65 +5,66 @@ For each indicator we compare ferro_ta output against every available reference
|
||||
Tolerances are based on known algorithmic differences (e.g. Wilder vs SMA seed).
|
||||
We only compare the overlapping (valid) suffix of each output array.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from benchmarks.data_generator import MEDIUM
|
||||
from benchmarks.wrapper_registry import (
|
||||
execute_indicator,
|
||||
INDICATOR_NAMES,
|
||||
INDICATOR_CATEGORIES,
|
||||
CUMULATIVE_INDICATORS,
|
||||
BINARY_INDICATORS,
|
||||
CUMULATIVE_INDICATORS,
|
||||
INDICATOR_CATEGORIES,
|
||||
INDICATOR_NAMES,
|
||||
available_libraries,
|
||||
execute_indicator,
|
||||
is_supported,
|
||||
)
|
||||
|
||||
# Reference = ferro_ta; compare against each library that has a non-empty result.
|
||||
REFERENCE_LIB = "ferro_ta"
|
||||
COMPARISON_LIBS = [l for l in available_libraries() if l != REFERENCE_LIB]
|
||||
COMPARISON_LIBS = [
|
||||
library for library in available_libraries() if library != REFERENCE_LIB
|
||||
]
|
||||
|
||||
# Per-indicator tolerances (rtol, atol)
|
||||
_TOLERANCES: dict[str, tuple[float, float]] = {
|
||||
"ATR": (1e-3, 0.05), # Wilder's smoothing seed differs
|
||||
"NATR": (1e-3, 0.10),
|
||||
"BBANDS": (1e-3, 0.20), # ddof=0 vs ddof=1
|
||||
"ATR": (1e-3, 0.05), # Wilder's smoothing seed differs
|
||||
"NATR": (1e-3, 0.10),
|
||||
"BBANDS": (1e-3, 0.20), # ddof=0 vs ddof=1
|
||||
"STDDEV": (1e-3, 0.20),
|
||||
"VAR": (1e-3, 0.50),
|
||||
"MACD": (1e-3, 1e-3), # double EMA seed
|
||||
"KAMA": (1e-3, 1e-3),
|
||||
"STOCH": (1e-3, 0.10), # smoothing method differences
|
||||
"SAR": (1e-3, 0.20),
|
||||
"ADOSC": (1e-3, 0.20),
|
||||
"ADX": (1e-3, 0.50), # Wilder's ADX
|
||||
"PLUS_DI":(1e-3, 0.50),
|
||||
"MINUS_DI":(1e-3, 0.50),
|
||||
"PPO": (1e-2, 1e-3),
|
||||
"CMO": (1e-3, 0.10),
|
||||
"TRIX": (1e-3, 1e-3),
|
||||
"CCI": (1e-3, 0.10),
|
||||
"VAR": (1e-3, 0.50),
|
||||
"MACD": (1e-3, 1.00), # seed differences across libraries
|
||||
"KAMA": (1e-3, 1e-3),
|
||||
"STOCH": (1e-3, 0.10), # smoothing method differences
|
||||
"SAR": (1e-3, 0.20),
|
||||
"ADOSC": (1e-3, 0.20),
|
||||
"ADX": (1e-3, 0.50), # Wilder's ADX
|
||||
"PLUS_DI": (1e-3, 0.50),
|
||||
"MINUS_DI": (1e-3, 0.50),
|
||||
"PPO": (1e-2, 1e-3),
|
||||
"CMO": (1e-3, 0.10),
|
||||
"TRIX": (1e-3, 0.05),
|
||||
"CCI": (1e-3, 0.10),
|
||||
"SUPERTREND": (1e-2, 0.50),
|
||||
"KELTNER_CHANNELS": (1e-2, 0.50),
|
||||
"DONCHIAN": (1e-4, 1e-4),
|
||||
"HT_DCPERIOD": (1e-2, 1.0),
|
||||
"VWAP": (1e-3, 0.10),
|
||||
"AROON": (1e-4, 1e-3),
|
||||
"HT_DCPERIOD": (1e-2, 2.0),
|
||||
"VWAP": (1e-3, 0.10),
|
||||
"AROON": (1e-4, 1e-3),
|
||||
"LINEARREG": (1e-4, 1e-4),
|
||||
"LINEARREG_SLOPE": (1e-4, 1e-4),
|
||||
"CORREL": (1e-4, 1e-3),
|
||||
"BETA": (1e-3, 1e-3),
|
||||
"TSF": (1e-4, 1e-4),
|
||||
"EMA": (1e-3, 0.30), # ta library uses different EMA seed
|
||||
"DEMA": (1e-3, 0.50),
|
||||
"TEMA": (1e-3, 0.50),
|
||||
"T3": (1e-3, 0.50),
|
||||
"HULL_MA":(1e-3, 0.10),
|
||||
"WMA": (1e-4, 1e-4),
|
||||
"TRIMA": (1e-4, 1e-4),
|
||||
"MACD": (1e-3, 1.00), # seed differences across libraries
|
||||
"TRIX": (1e-3, 0.05),
|
||||
"HT_DCPERIOD": (1e-2, 2.0),
|
||||
"BETA": (1e-3, 1e-3),
|
||||
"TSF": (1e-4, 1e-4),
|
||||
"EMA": (1e-3, 0.30), # ta library uses different EMA seed
|
||||
"DEMA": (1e-3, 0.50),
|
||||
"TEMA": (1e-3, 0.50),
|
||||
"T3": (1e-3, 0.50),
|
||||
"HULL_MA": (1e-3, 0.10),
|
||||
"WMA": (1e-4, 1e-4),
|
||||
"TRIMA": (1e-4, 1e-4),
|
||||
}
|
||||
|
||||
_DEFAULT_TOL = (1e-4, 1e-5)
|
||||
@@ -71,33 +72,33 @@ _DEFAULT_TOL = (1e-4, 1e-5)
|
||||
# Pairs that use correlation check (>=0.95) due to known algorithmic divergence
|
||||
# Format: (indicator, library) or just indicator (applies to all libs)
|
||||
_CORRELATION_PAIRS: set[tuple[str, str]] = {
|
||||
("PPO", "talib"), # different PPO formula normalization
|
||||
("PPO", "talib"), # different PPO formula normalization
|
||||
("PPO", "pandas_ta"),
|
||||
("PPO", "tulipy"),
|
||||
("STOCH", "ta"),
|
||||
("SUPERTREND", "pandas_ta"),
|
||||
("KELTNER_CHANNELS", "pandas_ta"),
|
||||
("KELTNER_CHANNELS", "ta"),
|
||||
("EMA", "finta"), # finta EMA uses different initialization
|
||||
("KAMA", "pandas_ta"), # pandas_ta KAMA has slightly different seed
|
||||
("RSI", "ta"), # ta uses SMA warmup vs Wilder
|
||||
("RSI", "finta"), # same
|
||||
("EMA", "finta"), # finta EMA uses different initialization
|
||||
("KAMA", "pandas_ta"), # pandas_ta KAMA has slightly different seed
|
||||
("RSI", "ta"), # ta uses SMA warmup vs Wilder
|
||||
("RSI", "finta"), # same
|
||||
}
|
||||
|
||||
# Pairs that are skipped because they are structurally incompatible
|
||||
_SKIP_PAIRS: set[tuple[str, str]] = {
|
||||
("BBANDS", "finta"), # finta normalizes band differently
|
||||
("ATR", "finta"), # finta ATR uses simple TR not Wilder
|
||||
("STDDEV", "finta"), # finta uses population std
|
||||
("TRIMA", "finta"), # finta TRIMA uses different formula
|
||||
("PPO", "finta"), # finta PPO scaling incompatible
|
||||
("STOCH", "finta"), # finta STOCH formula differs
|
||||
("VWAP", "pandas_ta"), # pandas_ta VWAP anchors to session start
|
||||
("BBANDS", "finta"), # finta normalizes band differently
|
||||
("ATR", "finta"), # finta ATR uses simple TR not Wilder
|
||||
("STDDEV", "finta"), # finta uses population std
|
||||
("TRIMA", "finta"), # finta TRIMA uses different formula
|
||||
("PPO", "finta"), # finta PPO scaling incompatible
|
||||
("STOCH", "finta"), # finta STOCH formula differs
|
||||
("VWAP", "pandas_ta"), # pandas_ta VWAP anchors to session start
|
||||
("HT_TRENDMODE", "talib"), # binary; Hilbert seed diverges
|
||||
("CMO", "talib"), # ferro_ta CMO smoothing variant corr < 0.90
|
||||
("CMO", "talib"), # ferro_ta CMO smoothing variant corr < 0.90
|
||||
("CMO", "pandas_ta"),
|
||||
("CMO", "finta"),
|
||||
("PLUS_DI", "pandas_ta"), # pandas_ta ADX column naming corr < 0.70
|
||||
("PLUS_DI", "pandas_ta"), # pandas_ta ADX column naming corr < 0.70
|
||||
}
|
||||
|
||||
MIN_OVERLAP = 30 # minimum points to make comparison meaningful
|
||||
@@ -114,12 +115,14 @@ def _compare(ref: np.ndarray, cmp: np.ndarray, indicator: str, library: str) ->
|
||||
c = cmp[-n:]
|
||||
if indicator in BINARY_INDICATORS or (indicator, library) in _CORRELATION_PAIRS:
|
||||
# Use correlation check for structurally different algorithms
|
||||
corr = np.corrcoef(r, c)[0, 1] if not indicator in BINARY_INDICATORS else None
|
||||
corr = np.corrcoef(r, c)[0, 1] if indicator not in BINARY_INDICATORS else None
|
||||
if indicator in BINARY_INDICATORS:
|
||||
agree = np.mean(r == c)
|
||||
assert agree >= 0.80, f"Binary agreement {agree:.1%} < 80%"
|
||||
else:
|
||||
assert corr >= 0.90, f"Correlation {corr:.4f} < 0.90 (structural divergence)"
|
||||
assert corr >= 0.90, (
|
||||
f"Correlation {corr:.4f} < 0.90 (structural divergence)"
|
||||
)
|
||||
elif indicator in CUMULATIVE_INDICATORS:
|
||||
dr, dc = np.diff(r), np.diff(c)
|
||||
if len(dr) < 5 or len(dc) < 5:
|
||||
@@ -136,6 +139,7 @@ def _compare(ref: np.ndarray, cmp: np.ndarray, indicator: str, library: str) ->
|
||||
|
||||
# ── dynamically generate one test per (indicator, library) pair ─────────────
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
if "indicator" in metafunc.fixturenames and "library" in metafunc.fixturenames:
|
||||
params = []
|
||||
@@ -172,6 +176,7 @@ class TestAccuracy:
|
||||
|
||||
# ── quick smoke tests that always run (no skip) ──────────────────────────────
|
||||
|
||||
|
||||
class TestSmoke:
|
||||
"""Sanity checks that ferro_ta returns non-empty finite arrays."""
|
||||
|
||||
@@ -182,7 +187,9 @@ class TestSmoke:
|
||||
|
||||
arr = execute_indicator("ferro_ta", indicator, MEDIUM)
|
||||
assert len(arr) > 0, f"ferro_ta {indicator} returned empty array"
|
||||
assert np.all(np.isfinite(arr)), f"ferro_ta {indicator} has non-finite values: {arr[~np.isfinite(arr)][:5]}"
|
||||
assert np.all(np.isfinite(arr)), (
|
||||
f"ferro_ta {indicator} has non-finite values: {arr[~np.isfinite(arr)][:5]}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("category,indicators", INDICATOR_CATEGORIES.items())
|
||||
def test_category_coverage(self, category, indicators):
|
||||
|
||||
@@ -10,11 +10,27 @@ Run with:
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from ferro_ta.analysis.options import implied_volatility, option_price
|
||||
# Ensure direct benchmark test runs can import local package from `python/`.
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PYTHON_SRC = ROOT / "python"
|
||||
if str(PYTHON_SRC) not in sys.path:
|
||||
sys.path.insert(0, str(PYTHON_SRC))
|
||||
|
||||
HAS_FERRO_EXTENSION = True
|
||||
try:
|
||||
from ferro_ta.analysis.options import implied_volatility, option_price
|
||||
except ModuleNotFoundError:
|
||||
HAS_FERRO_EXTENSION = False
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not HAS_FERRO_EXTENSION, reason="ferro_ta extension is not built"
|
||||
)
|
||||
|
||||
|
||||
def _sample_chain(n: int = 1000) -> tuple[np.ndarray, ...]:
|
||||
|
||||
+31
-18
@@ -6,31 +6,36 @@ Run: pytest benchmarks/test_speed.py --benchmark-only -v
|
||||
|
||||
Streaming benchmarks are in test_streaming_speed.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from benchmarks.data_generator import LARGE
|
||||
from benchmarks.wrapper_registry import (
|
||||
execute_indicator,
|
||||
INDICATOR_CATEGORIES,
|
||||
available_libraries,
|
||||
execute_indicator,
|
||||
is_supported,
|
||||
)
|
||||
|
||||
BENCH_DATA = LARGE # 100k bars for main benchmarks
|
||||
BENCH_LIBS = available_libraries()
|
||||
BENCH_DATA = LARGE # 100k bars for main benchmarks
|
||||
BENCH_LIBS = available_libraries()
|
||||
|
||||
|
||||
def _make_bench(indicator: str, library: str):
|
||||
"""Return a benchmark function that runs indicator on library (uses BENCH_DATA)."""
|
||||
|
||||
def _fn():
|
||||
execute_indicator(library, indicator, BENCH_DATA)
|
||||
|
||||
_fn.__name__ = f"{library}_{indicator}"
|
||||
return _fn
|
||||
|
||||
|
||||
# ── Parametrize over all (indicator, library) combinations ───────────────────
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
if "indicator" in metafunc.fixturenames and "library" in metafunc.fixturenames:
|
||||
params = []
|
||||
@@ -53,20 +58,24 @@ class TestSpeed:
|
||||
|
||||
# ── Standalone head-to-head for the most important indicators ─────────────────
|
||||
|
||||
@pytest.mark.parametrize("indicator,libs", [
|
||||
("SMA", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("EMA", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("RSI", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("MACD", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("BBANDS",["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("ATR", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("CCI", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("WILLR", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("OBV", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("ADX", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("MFI", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
("STOCH", ["ferro_ta","talib","tulipy","pandas_ta","ta","finta"]),
|
||||
])
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"indicator,libs",
|
||||
[
|
||||
("SMA", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("EMA", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("RSI", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("MACD", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("BBANDS", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("ATR", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("CCI", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("WILLR", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("OBV", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("ADX", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("MFI", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
("STOCH", ["ferro_ta", "talib", "tulipy", "pandas_ta", "ta", "finta"]),
|
||||
],
|
||||
)
|
||||
def test_head_to_head(benchmark, indicator, libs):
|
||||
"""Benchmark ferro_ta vs all peers — for README table generation."""
|
||||
if not is_supported("ferro_ta", indicator):
|
||||
@@ -77,7 +86,11 @@ def test_head_to_head(benchmark, indicator, libs):
|
||||
|
||||
# ── Large dataset benchmarks (100k bars) ─────────────────────────────────────
|
||||
|
||||
@pytest.mark.parametrize("indicator", ["SMA","EMA","RSI","MACD","ATR","BBANDS","OBV","CCI","ADX","MFI"])
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"indicator",
|
||||
["SMA", "EMA", "RSI", "MACD", "ATR", "BBANDS", "OBV", "CCI", "ADX", "MFI"],
|
||||
)
|
||||
def test_large_dataset(benchmark, indicator):
|
||||
"""Scaling benchmark at 100k bars for ferro_ta."""
|
||||
if not is_supported("ferro_ta", indicator):
|
||||
|
||||
+2521
-616
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
{% set name = "ferro-ta" %}
|
||||
{% set version = "1.0.3" %}
|
||||
{% set version = "1.0.6" %}
|
||||
|
||||
package:
|
||||
name: {{ name|lower }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ferro_ta_core"
|
||||
version = "1.0.3"
|
||||
version = "1.0.6"
|
||||
edition = "2021"
|
||||
description = "Pure Rust core indicator library — no PyO3, no numpy dependency"
|
||||
license = "MIT"
|
||||
|
||||
@@ -13,7 +13,7 @@ PyO3, NumPy, or Python runtime dependency, which makes it a good fit for:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ferro_ta_core = "1.0.3"
|
||||
ferro_ta_core = "1.0.6"
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
@@ -20,7 +20,8 @@ a Python technical analysis library.
|
||||
`docs/agentic.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/agentic.md>`_.
|
||||
* - MCP server
|
||||
- Experimental or adjacent
|
||||
- Exposes selected ferro-ta capabilities to MCP-compatible clients. See
|
||||
- FastMCP-based server exposing selected ferro-ta capabilities to
|
||||
MCP-compatible clients. See
|
||||
`docs/mcp.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/mcp.md>`_.
|
||||
* - WASM package
|
||||
- Experimental
|
||||
|
||||
+1
-1
@@ -198,6 +198,6 @@ while True:
|
||||
|
||||
- `ferro_ta.tools` — module source.
|
||||
- `ferro_ta.workflow` — module source.
|
||||
- `docs/mcp.md` — MCP server for Cursor/Claude integration.
|
||||
- `docs/mcp.md` — MCP server for MCP-compatible clients.
|
||||
- `ferro_ta.backtest` — backtest harness.
|
||||
- `ferro_ta.registry` — indicator registry.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+27
-1
@@ -1,7 +1,31 @@
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
These docs track package version ``1.0.3``.
|
||||
These docs track package version ``1.0.6``.
|
||||
|
||||
1.0.6 (2026-03-24)
|
||||
------------------
|
||||
|
||||
- Added a repo-managed pre-push gate so the core Rust, Python, docs, and WASM
|
||||
checks can be run locally before release.
|
||||
- Expanded Rust-backed analysis/data helpers, broadened the WASM exports, and
|
||||
added cross-surface API manifest verification plus Node conformance checks.
|
||||
- Refreshed benchmark coverage and perf artifacts, aligned Python CI with the
|
||||
local tooling flow, and updated the locked security fixes needed for a clean
|
||||
release pass.
|
||||
|
||||
1.0.4 (2026-03-24)
|
||||
------------------
|
||||
|
||||
- Expanded the optional MCP server from a small hand-written subset to the
|
||||
broader public ferro-ta callable surface, including stateful class support
|
||||
through stored-instance management tools.
|
||||
- Split the root documentation so the full TA-Lib compatibility matrix lives in
|
||||
``TA_LIB_COMPATIBILITY.md`` while the README stays product-first and shorter.
|
||||
- Refreshed MCP docs/tests and updated locked low-risk Python dependencies as
|
||||
part of the release cleanup pass.
|
||||
- Stopped tracking the stray ``.coverage`` artifact and aligned ignore rules
|
||||
for local coverage outputs.
|
||||
|
||||
1.0.3 (2026-03-24)
|
||||
------------------
|
||||
@@ -12,6 +36,8 @@ These docs track package version ``1.0.3``.
|
||||
pricing, IV, Greeks, and Black-76 comparisons.
|
||||
- Simplified release version bumps with a single script and updated release
|
||||
guidance.
|
||||
- Fixed Python CI/type-stub gaps around the new metadata API and corrected the
|
||||
tag-driven GitHub Release workflow trigger used for publish automation.
|
||||
|
||||
1.0.2 (2026-03-24)
|
||||
------------------
|
||||
|
||||
@@ -26,6 +26,28 @@ Prerequisites: Rust stable toolchain, Python 3.10+, and ``maturin``.
|
||||
pytest tests/
|
||||
|
||||
|
||||
Git hooks and pre-push checks
|
||||
-----------------------------
|
||||
|
||||
Install the repository-managed hooks after setting up the environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
make hooks
|
||||
|
||||
Run the same push gate manually with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
make prepush
|
||||
|
||||
You can scope it to selected checks while iterating:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
make prepush CHECKS="version changelog python_lint"
|
||||
|
||||
|
||||
Adding a new indicator
|
||||
-----------------------
|
||||
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ Adjacent and experimental tooling:
|
||||
|
||||
- Derivatives analytics — see :doc:`derivatives`
|
||||
- Agentic workflow and LangChain tool wrappers — see `Agentic guide <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/agentic.md>`_
|
||||
- MCP server for Cursor/Claude integration — see `MCP guide <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/mcp.md>`_
|
||||
- MCP server for MCP-compatible clients — see `MCP guide <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/mcp.md>`_
|
||||
- WASM, plugins, and other optional surfaces — see :doc:`adjacent_tooling`
|
||||
|
||||
Installation
|
||||
|
||||
+120
-54
@@ -1,42 +1,56 @@
|
||||
# MCP Server — Connect ferro-ta in Cursor
|
||||
# MCP Server
|
||||
|
||||
ferro-ta ships an MCP (Model Context Protocol) server that exposes
|
||||
indicators and backtest tools to AI agents. This guide shows how to run
|
||||
the server and connect it to Cursor or any MCP-compatible client.
|
||||
ferro-ta ships an optional MCP (Model Context Protocol) server built on the
|
||||
official Python SDK's FastMCP layer. The server now exposes the broad public
|
||||
ferro-ta callable surface instead of a tiny hand-picked subset.
|
||||
|
||||
That means MCP clients can use:
|
||||
|
||||
- Exact top-level ferro-ta exports such as `SMA`, `RSI`, `MACD`, `about`,
|
||||
`methods`, `info`, `benchmark`, and `traced`
|
||||
- Non-top-level public tools such as `compute_indicator`, `run_backtest`,
|
||||
`check_cross`, `aggregate_ticks`, `TickAggregator`, and `AlertManager`
|
||||
- Legacy lowercase convenience aliases: `sma`, `ema`, `rsi`, `macd`,
|
||||
and `backtest`
|
||||
- Generic instance tools for stateful classes and stored callables:
|
||||
`list_instances`, `describe_instance`, `call_instance_method`,
|
||||
`call_stored_callable`, and `delete_instance`
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
The MCP server requires no additional dependencies beyond ferro_ta itself.
|
||||
For the full MCP SDK integration (recommended), install the optional extra:
|
||||
Install the optional MCP extra:
|
||||
|
||||
```bash
|
||||
pip install "ferro-ta[mcp]"
|
||||
```
|
||||
|
||||
or install the `mcp` package separately:
|
||||
If you are working from this repository, you can install the same extra into
|
||||
the project environment with:
|
||||
|
||||
```bash
|
||||
pip install "mcp>=1.0"
|
||||
uv sync --extra mcp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running the server
|
||||
|
||||
Run the server over stdio:
|
||||
|
||||
```bash
|
||||
python -m ferro_ta.mcp
|
||||
```
|
||||
|
||||
The server listens on stdin/stdout using JSON-RPC 2.0 (the MCP protocol).
|
||||
The command exits immediately with an install hint if the optional `mcp`
|
||||
dependency is missing.
|
||||
|
||||
---
|
||||
|
||||
## Connect in Cursor
|
||||
|
||||
1. Open Cursor settings (Command Palette → "Open User Settings (JSON)").
|
||||
2. Find or create the `mcpServers` section:
|
||||
Add the server to Cursor's MCP settings:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -44,82 +58,134 @@ The server listens on stdin/stdout using JSON-RPC 2.0 (the MCP protocol).
|
||||
"ferro-ta": {
|
||||
"command": "python",
|
||||
"args": ["-m", "ferro_ta.mcp"],
|
||||
"description": "ferro_ta — Technical Analysis MCP server"
|
||||
"description": "ferro-ta technical analysis tools"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Reload Cursor (Command Palette → "Developer: Reload Window").
|
||||
4. The ferro-ta tools will appear in the Tools panel.
|
||||
You can place this in your user settings JSON or in a workspace-level
|
||||
`.cursor/mcp.json`.
|
||||
|
||||
### Workspace-level config
|
||||
---
|
||||
|
||||
You can also add the config to your project's `.cursor/mcp.json`:
|
||||
## Tool naming
|
||||
|
||||
The MCP server prefers the real ferro-ta API names.
|
||||
|
||||
- Use exact public names when possible, for example `SMA`, `MACD`,
|
||||
`compute_indicator`, `trade_stats`, `TickAggregator`, or `AlertManager`
|
||||
- Use the legacy lowercase aliases only when you want the old MCP-friendly
|
||||
shortcuts and result shapes
|
||||
- Use `about`, `methods`, `indicators`, and `info` to discover what is
|
||||
available from inside an MCP client
|
||||
|
||||
---
|
||||
|
||||
## Stateful classes and object references
|
||||
|
||||
Class tools return stored object references instead of plain text placeholders.
|
||||
For example, calling `TickAggregator` or `AlertManager` returns a payload like:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ferro-ta": {
|
||||
"command": "python",
|
||||
"args": ["-m", "ferro_ta.mcp"]
|
||||
}
|
||||
}
|
||||
"instance_id": "tickaggregator-0001",
|
||||
"type": "ferro_ta.data.aggregation.TickAggregator",
|
||||
"repr": "TickAggregator(rule='tick:2')"
|
||||
}
|
||||
```
|
||||
|
||||
Use that `instance_id` with:
|
||||
|
||||
- `describe_instance` to inspect the stored object and list public methods
|
||||
- `call_instance_method` to call methods like `aggregate`, `update`,
|
||||
`run_backtest`, or `to_dict`
|
||||
- `delete_instance` to remove stored objects when you are done
|
||||
|
||||
If a tool returns a stored callable, use `call_stored_callable`.
|
||||
|
||||
---
|
||||
|
||||
## Callable references
|
||||
|
||||
Some ferro-ta APIs accept other callables, for example `benchmark`,
|
||||
`log_call`, `traced`, or `multi_timeframe(indicator=...)`.
|
||||
|
||||
Pass public ferro-ta callables using:
|
||||
|
||||
```json
|
||||
{"callable": "SMA"}
|
||||
```
|
||||
|
||||
Pass stored objects using:
|
||||
|
||||
```json
|
||||
{"instance_id": "function-0001"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example prompts
|
||||
|
||||
Once connected, you can ask Claude (or any MCP-enabled AI) things like:
|
||||
Once connected, you can ask an MCP-compatible client things like:
|
||||
|
||||
> "Compute RSI(14) on this price series: [100, 102, 101, 105, 108, 104, 107]"
|
||||
> "Run `SMA` with `close=[100, 101, 102, 103, 104]` and `timeperiod=3`."
|
||||
|
||||
> "Run a backtest with the rsi_30_70 strategy on [100, 101, 99, 103, 106, 102, 108, 105, 109, 112, 108, 111]"
|
||||
> "Use `compute_indicator` to calculate `MACD` for this close series."
|
||||
|
||||
> "List all available ferro_ta indicators"
|
||||
> "Call `about` and summarize the current ferro-ta API surface."
|
||||
|
||||
> "What does the SMA indicator do?"
|
||||
> "Create a `TickAggregator` with `rule='tick:50'`, aggregate this tick data,
|
||||
> then delete the instance."
|
||||
|
||||
> "Benchmark `SMA` over this price series using a callable reference."
|
||||
|
||||
---
|
||||
|
||||
## Available tools
|
||||
## Programmatic use
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `sma` | Simple Moving Average |
|
||||
| `ema` | Exponential Moving Average |
|
||||
| `rsi` | Relative Strength Index |
|
||||
| `macd` | MACD line, signal, histogram |
|
||||
| `backtest` | Vectorized backtest (rsi_30_70, sma_crossover, macd_crossover) |
|
||||
| `list_indicators` | List all registered indicators |
|
||||
| `describe_indicator` | Describe a named indicator |
|
||||
|
||||
---
|
||||
|
||||
## Programmatic use (Python client)
|
||||
|
||||
You can also use the MCP handlers directly in Python without the server:
|
||||
Use the server entrypoint:
|
||||
|
||||
```python
|
||||
from ferro_ta.mcp import handle_list_tools, handle_call_tool
|
||||
import numpy as np
|
||||
from ferro_ta.mcp import create_server
|
||||
|
||||
server = create_server()
|
||||
# server.run(transport="stdio")
|
||||
```
|
||||
|
||||
Or call the handlers directly without starting the server:
|
||||
|
||||
```python
|
||||
from ferro_ta.mcp import handle_call_tool, handle_list_tools
|
||||
import json
|
||||
|
||||
# List tools
|
||||
tools = handle_list_tools()
|
||||
print([t["name"] for t in tools["tools"]])
|
||||
print(len(tools["tools"]))
|
||||
|
||||
# Call RSI
|
||||
close = list(np.cumprod(1 + np.random.default_rng(0).normal(0, 0.01, 50)) * 100)
|
||||
result = handle_call_tool("rsi", {"close": close, "timeperiod": 14})
|
||||
print(result)
|
||||
close = [100, 101, 102, 103, 104]
|
||||
result = handle_call_tool("SMA", {"close": close, "timeperiod": 3})
|
||||
print(json.loads(result["content"][0]["text"]))
|
||||
|
||||
aggregator = json.loads(
|
||||
handle_call_tool("TickAggregator", {"rule": "tick:2"})["content"][0]["text"]
|
||||
)
|
||||
bars = handle_call_tool(
|
||||
"call_instance_method",
|
||||
{
|
||||
"instance_id": aggregator["instance_id"],
|
||||
"method": "aggregate",
|
||||
"args": [{"price": [1, 2, 3, 4], "size": [1, 1, 1, 1]}],
|
||||
},
|
||||
)
|
||||
print(json.loads(bars["content"][0]["text"]))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- `ferro_ta.mcp` — module source.
|
||||
- `ferro_ta.tools` — underlying tool functions.
|
||||
- `docs/agentic.md` — LangChain and workflow integration.
|
||||
- `python -m ferro_ta.mcp` - stdio MCP entrypoint
|
||||
- `ferro_ta.mcp.create_server()` - FastMCP server factory
|
||||
- `ferro_ta.tools.api_info` - API discovery helpers used by the MCP catalog
|
||||
- `ferro_ta.tools` - stable wrappers such as `compute_indicator`
|
||||
- `docs/agentic.md` - workflow and agent integration notes
|
||||
|
||||
@@ -107,7 +107,7 @@ For source builds, packaging details, and platform notes, see
|
||||
Release status
|
||||
--------------
|
||||
|
||||
These docs track package version ``1.0.3``.
|
||||
These docs track package version ``1.0.6``.
|
||||
|
||||
- Release notes by version: :doc:`changelog`
|
||||
- Canonical project changelog: `CHANGELOG.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/CHANGELOG.md>`_
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"import ferro_ta.config as config\n",
|
||||
"from ferro_ta import BBANDS, EMA, RSI, SMA\n",
|
||||
"import numpy as np\n",
|
||||
"from ferro_ta.backtest import backtest\n",
|
||||
"from ferro_ta.pipeline import Pipeline\n",
|
||||
"\n",
|
||||
"from ferro_ta import BBANDS, EMA, RSI, SMA\n",
|
||||
"\n",
|
||||
"# Synthetic data\n",
|
||||
"np.random.seed(42)\n",
|
||||
"n = 300\n",
|
||||
|
||||
@@ -207,9 +207,10 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from ferro_ta import FerroTAValueError\n",
|
||||
"from ferro_ta.exceptions import check_timeperiod\n",
|
||||
"\n",
|
||||
"from ferro_ta import FerroTAValueError\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" check_timeperiod(0)\n",
|
||||
"except FerroTAValueError as e:\n",
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"from ferro_ta.streaming import (\n",
|
||||
" StreamingATR,\n",
|
||||
" StreamingBBands,\n",
|
||||
|
||||
+55
-33
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "batch",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:08:30.877702+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:04.010216+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"dataset": {
|
||||
"n_samples": 20000,
|
||||
@@ -23,49 +45,49 @@
|
||||
"results": [
|
||||
{
|
||||
"indicator": "SMA",
|
||||
"parallel_ms": 1.9615,
|
||||
"sequential_ms": 2.0423,
|
||||
"loop_ms": 0.8865,
|
||||
"parallel_speedup_vs_loop": 0.452,
|
||||
"sequential_speedup_vs_loop": 0.4341
|
||||
"parallel_ms": 7.9306,
|
||||
"sequential_ms": 2.4832,
|
||||
"loop_ms": 1.0238,
|
||||
"parallel_speedup_vs_loop": 0.1291,
|
||||
"sequential_speedup_vs_loop": 0.4123
|
||||
},
|
||||
{
|
||||
"indicator": "RSI",
|
||||
"parallel_ms": 2.1731,
|
||||
"sequential_ms": 4.3225,
|
||||
"loop_ms": 3.2043,
|
||||
"parallel_speedup_vs_loop": 1.4745,
|
||||
"sequential_speedup_vs_loop": 0.7413
|
||||
"parallel_ms": 3.9307,
|
||||
"sequential_ms": 5.0938,
|
||||
"loop_ms": 3.6883,
|
||||
"parallel_speedup_vs_loop": 0.9383,
|
||||
"sequential_speedup_vs_loop": 0.7241
|
||||
},
|
||||
{
|
||||
"indicator": "ATR",
|
||||
"parallel_ms": 4.2249,
|
||||
"sequential_ms": 6.6175,
|
||||
"loop_ms": 4.0382,
|
||||
"parallel_speedup_vs_loop": 0.9558,
|
||||
"sequential_speedup_vs_loop": 0.6102
|
||||
"parallel_ms": 9.2546,
|
||||
"sequential_ms": 7.768,
|
||||
"loop_ms": 5.2669,
|
||||
"parallel_speedup_vs_loop": 0.5691,
|
||||
"sequential_speedup_vs_loop": 0.678
|
||||
},
|
||||
{
|
||||
"indicator": "ADX",
|
||||
"parallel_ms": 4.8674,
|
||||
"sequential_ms": 7.6402,
|
||||
"loop_ms": 5.1861,
|
||||
"parallel_speedup_vs_loop": 1.0655,
|
||||
"sequential_speedup_vs_loop": 0.6788
|
||||
"parallel_ms": 9.5489,
|
||||
"sequential_ms": 9.1403,
|
||||
"loop_ms": 7.1578,
|
||||
"parallel_speedup_vs_loop": 0.7496,
|
||||
"sequential_speedup_vs_loop": 0.7831
|
||||
}
|
||||
],
|
||||
"grouped_results": [
|
||||
{
|
||||
"case": "close_bundle_3",
|
||||
"grouped_ms": 0.1878,
|
||||
"separate_ms": 0.1719,
|
||||
"speedup_vs_separate": 0.9155
|
||||
"grouped_ms": 0.466,
|
||||
"separate_ms": 0.2003,
|
||||
"speedup_vs_separate": 0.4298
|
||||
},
|
||||
{
|
||||
"case": "hlc_bundle_3",
|
||||
"grouped_ms": 0.2958,
|
||||
"separate_ms": 0.4633,
|
||||
"speedup_vs_separate": 1.5666
|
||||
"grouped_ms": 1.2538,
|
||||
"separate_ms": 0.6999,
|
||||
"speedup_vs_separate": 0.5582
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "indicator_latency",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:08:30.515962+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:02.973728+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"fixtures": [
|
||||
{
|
||||
@@ -33,7 +55,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0202
|
||||
"elapsed_ms": 0.0233
|
||||
},
|
||||
{
|
||||
"name": "WILLR_14",
|
||||
@@ -41,13 +63,13 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0183
|
||||
"elapsed_ms": 0.0207
|
||||
},
|
||||
{
|
||||
"name": "STOCH",
|
||||
"inputs": "hlc",
|
||||
"kwargs": {},
|
||||
"elapsed_ms": 0.0181
|
||||
"elapsed_ms": 0.0202
|
||||
},
|
||||
{
|
||||
"name": "ADX_14",
|
||||
@@ -55,7 +77,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0147
|
||||
"elapsed_ms": 0.0171
|
||||
},
|
||||
{
|
||||
"name": "CCI_14",
|
||||
@@ -63,13 +85,13 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0144
|
||||
"elapsed_ms": 0.0164
|
||||
},
|
||||
{
|
||||
"name": "MACD",
|
||||
"inputs": "close",
|
||||
"kwargs": {},
|
||||
"elapsed_ms": 0.0132
|
||||
"elapsed_ms": 0.015
|
||||
},
|
||||
{
|
||||
"name": "ATR_14",
|
||||
@@ -77,7 +99,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0108
|
||||
"elapsed_ms": 0.0116
|
||||
},
|
||||
{
|
||||
"name": "RSI_14",
|
||||
@@ -85,7 +107,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0102
|
||||
"elapsed_ms": 0.011
|
||||
},
|
||||
{
|
||||
"name": "STDDEV_20",
|
||||
@@ -93,7 +115,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0086
|
||||
"elapsed_ms": 0.0103
|
||||
},
|
||||
{
|
||||
"name": "BETA_5",
|
||||
@@ -101,7 +123,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 5
|
||||
},
|
||||
"elapsed_ms": 0.008
|
||||
"elapsed_ms": 0.0091
|
||||
},
|
||||
{
|
||||
"name": "CORREL_30",
|
||||
@@ -109,15 +131,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 30
|
||||
},
|
||||
"elapsed_ms": 0.0068
|
||||
},
|
||||
{
|
||||
"name": "EMA_20",
|
||||
"inputs": "close",
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0052
|
||||
"elapsed_ms": 0.0078
|
||||
},
|
||||
{
|
||||
"name": "BBANDS_20",
|
||||
@@ -125,7 +139,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.005
|
||||
"elapsed_ms": 0.0062
|
||||
},
|
||||
{
|
||||
"name": "TSF_14",
|
||||
@@ -133,7 +147,15 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0048
|
||||
"elapsed_ms": 0.0056
|
||||
},
|
||||
{
|
||||
"name": "EMA_20",
|
||||
"inputs": "close",
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0055
|
||||
},
|
||||
{
|
||||
"name": "LINEARREG_14",
|
||||
@@ -141,7 +163,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0046
|
||||
"elapsed_ms": 0.0054
|
||||
},
|
||||
{
|
||||
"name": "LINEARREG_SLOPE_14",
|
||||
@@ -149,7 +171,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 14
|
||||
},
|
||||
"elapsed_ms": 0.0045
|
||||
"elapsed_ms": 0.005
|
||||
},
|
||||
{
|
||||
"name": "SMA_20",
|
||||
@@ -157,7 +179,7 @@
|
||||
"kwargs": {
|
||||
"timeperiod": 20
|
||||
},
|
||||
"elapsed_ms": 0.0027
|
||||
"elapsed_ms": 0.0032
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+37
-20
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "perf_contract",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:09:13.077731+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:08.787230+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"fixtures": [
|
||||
{
|
||||
@@ -25,28 +47,23 @@
|
||||
"artifacts": {
|
||||
"indicator_latency": {
|
||||
"path": "perf-contract/indicator_latency.json",
|
||||
"size_bytes": 3212,
|
||||
"sha256": "1e7f844fe6eb467f07bbb1e4eb918c56861e8cf819ab4802b96e033c6d2570c4"
|
||||
"size_bytes": 4041,
|
||||
"sha256": "564027c7abed7ecd4ae2ac1720217d96e1e31807f8ac7d5c5393c8fd974f13ed"
|
||||
},
|
||||
"batch": {
|
||||
"path": "perf-contract/batch.json",
|
||||
"size_bytes": 1679,
|
||||
"sha256": "202d67b24a88184473f93cec9f866ca34ff60721068e15e7e5d23962f006d169"
|
||||
"size_bytes": 2507,
|
||||
"sha256": "c50f242a138a0c358a6fa86c420954b4b11a2200598ca2942bcd0fd2bc456fb2"
|
||||
},
|
||||
"streaming": {
|
||||
"path": "perf-contract/streaming.json",
|
||||
"size_bytes": 1939,
|
||||
"sha256": "04476c3d95a9e7d87e40331b28c5ccad9a7aa5abcfc47fb6ac7e929365f68554"
|
||||
"size_bytes": 2766,
|
||||
"sha256": "d271d3219ec098e443e84ef648ab4220cd2a38e6dc998bc6c9fc545d7fc77716"
|
||||
},
|
||||
"runtime_hotspots": {
|
||||
"path": "perf-contract/runtime_hotspots.json",
|
||||
"size_bytes": 2365,
|
||||
"sha256": "d37a58bc88def9f1525b9b0f64f0bd7b9c0df9ed4cae260c833697f6a1689ca8"
|
||||
},
|
||||
"simd": {
|
||||
"path": "perf-contract/simd.json",
|
||||
"size_bytes": 7670,
|
||||
"sha256": "3f9b341a8206698ed172650752d808621c9b5218f42a2373f0f4485496197b4c"
|
||||
"size_bytes": 3197,
|
||||
"sha256": "96eafc9a61ac410e47fcdfee6a9db1123cdee0ec05f49a85c631a3975063deed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "runtime_hotspots",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:08:34.436165+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:08.521805+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"dataset": {
|
||||
"price_bars": 20000,
|
||||
@@ -23,74 +45,74 @@
|
||||
{
|
||||
"category": "python_analysis",
|
||||
"name": "iv_zscore",
|
||||
"fast_ms": 28.857,
|
||||
"reference_ms": 897.0178,
|
||||
"speedup_vs_reference": 31.0849,
|
||||
"share_of_suite_pct": 70.0
|
||||
"fast_ms": 32.5847,
|
||||
"reference_ms": 990.4233,
|
||||
"speedup_vs_reference": 30.3954,
|
||||
"share_of_suite_pct": 69.27
|
||||
},
|
||||
{
|
||||
"category": "python_analysis",
|
||||
"name": "iv_rank",
|
||||
"fast_ms": 10.8496,
|
||||
"reference_ms": 199.4376,
|
||||
"speedup_vs_reference": 18.3821,
|
||||
"share_of_suite_pct": 26.32
|
||||
"fast_ms": 12.2974,
|
||||
"reference_ms": 233.0671,
|
||||
"speedup_vs_reference": 18.9525,
|
||||
"share_of_suite_pct": 26.14
|
||||
},
|
||||
{
|
||||
"category": "python_analysis",
|
||||
"name": "iv_percentile",
|
||||
"fast_ms": 0.9124,
|
||||
"reference_ms": 79.6557,
|
||||
"speedup_vs_reference": 87.3019,
|
||||
"share_of_suite_pct": 2.21
|
||||
"fast_ms": 0.9511,
|
||||
"reference_ms": 90.5663,
|
||||
"speedup_vs_reference": 95.2202,
|
||||
"share_of_suite_pct": 2.02
|
||||
},
|
||||
{
|
||||
"category": "ffi_grouping",
|
||||
"name": "feature_matrix",
|
||||
"fast_ms": 0.2411,
|
||||
"reference_ms": 0.2215,
|
||||
"speedup_vs_reference": 0.9186,
|
||||
"share_of_suite_pct": 0.58
|
||||
"fast_ms": 0.6619,
|
||||
"reference_ms": 0.6155,
|
||||
"speedup_vs_reference": 0.9299,
|
||||
"share_of_suite_pct": 1.41
|
||||
},
|
||||
{
|
||||
"category": "ffi_grouping",
|
||||
"name": "compute_many_close",
|
||||
"fast_ms": 0.1563,
|
||||
"reference_ms": 0.1498,
|
||||
"speedup_vs_reference": 0.9587,
|
||||
"share_of_suite_pct": 0.38
|
||||
"fast_ms": 0.3175,
|
||||
"reference_ms": 0.2437,
|
||||
"speedup_vs_reference": 0.7675,
|
||||
"share_of_suite_pct": 0.67
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "BETA",
|
||||
"fast_ms": 0.0694,
|
||||
"reference_ms": 159.2715,
|
||||
"speedup_vs_reference": 2294.4163,
|
||||
"share_of_suite_pct": 0.17
|
||||
"fast_ms": 0.0742,
|
||||
"reference_ms": 188.6048,
|
||||
"speedup_vs_reference": 2541.5695,
|
||||
"share_of_suite_pct": 0.16
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "CORREL",
|
||||
"fast_ms": 0.0555,
|
||||
"reference_ms": 158.2775,
|
||||
"speedup_vs_reference": 2851.8468,
|
||||
"share_of_suite_pct": 0.13
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "LINEARREG",
|
||||
"fast_ms": 0.0414,
|
||||
"reference_ms": 44.8111,
|
||||
"speedup_vs_reference": 1081.9491,
|
||||
"share_of_suite_pct": 0.1
|
||||
"fast_ms": 0.0638,
|
||||
"reference_ms": 215.973,
|
||||
"speedup_vs_reference": 3387.8115,
|
||||
"share_of_suite_pct": 0.14
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "TSF",
|
||||
"fast_ms": 0.0413,
|
||||
"reference_ms": 46.3799,
|
||||
"speedup_vs_reference": 1122.1039,
|
||||
"share_of_suite_pct": 0.1
|
||||
"fast_ms": 0.0441,
|
||||
"reference_ms": 49.6108,
|
||||
"speedup_vs_reference": 1125.3954,
|
||||
"share_of_suite_pct": 0.09
|
||||
},
|
||||
{
|
||||
"category": "rust_kernel",
|
||||
"name": "LINEARREG",
|
||||
"fast_ms": 0.044,
|
||||
"reference_ms": 57.9675,
|
||||
"speedup_vs_reference": 1318.7009,
|
||||
"share_of_suite_pct": 0.09
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,4 +282,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,38 @@
|
||||
"metadata": {
|
||||
"suite": "streaming",
|
||||
"runtime": {
|
||||
"generated_at_utc": "2026-03-23T21:08:30.968886+00:00",
|
||||
"python_version": "3.12.11",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit",
|
||||
"generated_at_utc": "2026-03-24T09:13:04.351797+00:00",
|
||||
"python_version": "3.13.5",
|
||||
"python_implementation": "CPython",
|
||||
"python_executable": "/Users/pratikbhadane/Work/Projects/ferro-ta/.venv/bin/python3",
|
||||
"platform": "macOS-26.3.1-arm64-arm-64bit-Mach-O",
|
||||
"system": "Darwin",
|
||||
"release": "25.3.0",
|
||||
"machine": "arm64",
|
||||
"processor": "arm"
|
||||
"processor": "arm",
|
||||
"cpu_model": "Apple M3 Max",
|
||||
"cpu_count_logical": 14,
|
||||
"total_memory_bytes": 38654705664
|
||||
},
|
||||
"git": {
|
||||
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
|
||||
"commit": "53566b9d82898fa4f95c5190156c969a0bd8e8e3",
|
||||
"dirty": true,
|
||||
"branch": "feat/performace-1.0.2"
|
||||
"branch": "main"
|
||||
},
|
||||
"build": {
|
||||
"rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-apple-darwin\nrelease: 1.93.1\nLLVM version: 21.1.8",
|
||||
"cargo": "cargo 1.93.1 (083ac5135 2025-12-15)",
|
||||
"cargo_release_profile": {
|
||||
"lto": true,
|
||||
"codegen-units": 1
|
||||
},
|
||||
"rustflags": null,
|
||||
"cargo_build_rustflags": null,
|
||||
"maturin_flags": null
|
||||
},
|
||||
"packages": {
|
||||
"numpy": "2.2.6",
|
||||
"ferro-ta": "1.0.4"
|
||||
},
|
||||
"dataset": {
|
||||
"n_bars": 20000,
|
||||
@@ -22,52 +44,52 @@
|
||||
{
|
||||
"indicator": "StreamingSMA",
|
||||
"inputs": "close",
|
||||
"stream_total_ms": 0.8808,
|
||||
"batch_total_ms": 0.0153,
|
||||
"stream_ns_per_update": 44.04,
|
||||
"batch_ns_per_bar": 0.77,
|
||||
"updates_per_second": 22705779.59,
|
||||
"stream_over_batch_ratio": 57.4469
|
||||
"stream_total_ms": 0.9927,
|
||||
"batch_total_ms": 0.0166,
|
||||
"stream_ns_per_update": 49.63,
|
||||
"batch_ns_per_bar": 0.83,
|
||||
"updates_per_second": 20147763.43,
|
||||
"stream_over_batch_ratio": 59.7092
|
||||
},
|
||||
{
|
||||
"indicator": "StreamingEMA",
|
||||
"inputs": "close",
|
||||
"stream_total_ms": 0.8611,
|
||||
"batch_total_ms": 0.0408,
|
||||
"stream_ns_per_update": 43.06,
|
||||
"batch_ns_per_bar": 2.04,
|
||||
"updates_per_second": 23225431.81,
|
||||
"stream_over_batch_ratio": 21.0884
|
||||
"stream_total_ms": 0.9459,
|
||||
"batch_total_ms": 0.0435,
|
||||
"stream_ns_per_update": 47.3,
|
||||
"batch_ns_per_bar": 2.18,
|
||||
"updates_per_second": 21143503.9,
|
||||
"stream_over_batch_ratio": 21.7247
|
||||
},
|
||||
{
|
||||
"indicator": "StreamingRSI",
|
||||
"inputs": "close",
|
||||
"stream_total_ms": 0.9235,
|
||||
"batch_total_ms": 0.0932,
|
||||
"stream_ns_per_update": 46.17,
|
||||
"batch_ns_per_bar": 4.66,
|
||||
"updates_per_second": 21656740.75,
|
||||
"stream_over_batch_ratio": 9.9035
|
||||
"stream_total_ms": 1.0059,
|
||||
"batch_total_ms": 0.0991,
|
||||
"stream_ns_per_update": 50.3,
|
||||
"batch_ns_per_bar": 4.95,
|
||||
"updates_per_second": 19882356.06,
|
||||
"stream_over_batch_ratio": 10.1523
|
||||
},
|
||||
{
|
||||
"indicator": "StreamingATR",
|
||||
"inputs": "hlc",
|
||||
"stream_total_ms": 2.0074,
|
||||
"batch_total_ms": 0.0935,
|
||||
"stream_ns_per_update": 100.37,
|
||||
"batch_ns_per_bar": 4.68,
|
||||
"updates_per_second": 9963056.99,
|
||||
"stream_over_batch_ratio": 21.4603
|
||||
"stream_total_ms": 2.1574,
|
||||
"batch_total_ms": 0.0992,
|
||||
"stream_ns_per_update": 107.87,
|
||||
"batch_ns_per_bar": 4.96,
|
||||
"updates_per_second": 9270525.53,
|
||||
"stream_over_batch_ratio": 21.746
|
||||
},
|
||||
{
|
||||
"indicator": "StreamingVWAP",
|
||||
"inputs": "hlcv",
|
||||
"stream_total_ms": 2.6068,
|
||||
"batch_total_ms": 0.0223,
|
||||
"stream_ns_per_update": 130.34,
|
||||
"batch_ns_per_bar": 1.11,
|
||||
"updates_per_second": 7672265.38,
|
||||
"stream_over_batch_ratio": 116.9385
|
||||
"stream_total_ms": 2.6935,
|
||||
"batch_total_ms": 0.0252,
|
||||
"stream_ns_per_update": 134.68,
|
||||
"batch_ns_per_bar": 1.26,
|
||||
"updates_per_second": 7425170.07,
|
||||
"stream_over_batch_ratio": 106.8484
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ferro-ta"
|
||||
version = "1.0.3"
|
||||
version = "1.0.6"
|
||||
description = "Rust-powered Python technical analysis library with a TA-Lib-compatible API"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
@@ -53,6 +53,7 @@ dev = [
|
||||
"hypothesis>=6.0",
|
||||
"pandas>=1.0",
|
||||
"polars>=0.19",
|
||||
"pre-commit>=3.0",
|
||||
"ruff>=0.3",
|
||||
"mypy>=1.0",
|
||||
"pyright>=1.1",
|
||||
@@ -103,7 +104,9 @@ ignore = ["E501", "UP006", "UP045", "UP007"]
|
||||
"tests/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
||||
"tests/unit/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
||||
"tests/integration/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
|
||||
"benchmarks/*" = ["E402", "E741"]
|
||||
"python/ferro_ta/*.py" = ["N802"] # Public API: SMA, RSI, ATR, etc.
|
||||
"python/ferro_ta/__init__.py" = ["E402", "N802"] # Re-export surface by design
|
||||
"python/ferro_ta/__init__.pyi" = ["N802", "E402"]
|
||||
|
||||
[tool.ruff.format]
|
||||
@@ -129,6 +132,7 @@ dev = [
|
||||
"hypothesis>=6.0",
|
||||
"pandas>=1.0",
|
||||
"polars>=0.19",
|
||||
"pre-commit>=3.0",
|
||||
"ruff>=0.3",
|
||||
"mypy>=1.0",
|
||||
"pyright>=1.1",
|
||||
|
||||
@@ -59,11 +59,11 @@ array([ nan, nan, 11. , 12. , 13. , 13.5, 13.33...])
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re as _re
|
||||
import sys as _sys
|
||||
from importlib.metadata import PackageNotFoundError as _PackageNotFoundError
|
||||
from importlib.metadata import version as _dist_version
|
||||
from pathlib import Path as _Path
|
||||
import re as _re
|
||||
import sys as _sys
|
||||
|
||||
try:
|
||||
import tomllib as _tomllib
|
||||
|
||||
@@ -13,6 +13,8 @@ from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
_F = TypeVar("_F", bound=Callable[..., Any])
|
||||
|
||||
__version__: str
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Overlap Studies
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -739,8 +741,10 @@ class FerroTAInputError(FerroTAError, ValueError):
|
||||
# API discovery (ferro_ta.api_info)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def about() -> dict[str, Any]: ...
|
||||
def indicators(category: str | None = None) -> list[dict[str, Any]]: ...
|
||||
def info(func_or_name: Callable[..., Any] | str) -> dict[str, Any]: ...
|
||||
def methods(category: str | None = None) -> list[dict[str, Any]]: ...
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Logging utilities (ferro_ta.logging_utils)
|
||||
|
||||
@@ -38,6 +38,9 @@ from typing import Any, Optional
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from ferro_ta._ferro_ta import (
|
||||
extract_trades as _rust_extract_trades,
|
||||
)
|
||||
from ferro_ta._ferro_ta import (
|
||||
monthly_contribution as _rust_monthly_contribution,
|
||||
)
|
||||
@@ -199,31 +202,10 @@ def from_backtest(result: Any) -> tuple[NDArray[np.float64], NDArray[np.float64]
|
||||
"""
|
||||
pos = np.asarray(result.positions, dtype=np.float64)
|
||||
ret = np.asarray(result.strategy_returns, dtype=np.float64)
|
||||
n = len(pos)
|
||||
|
||||
pnl_list: list[float] = []
|
||||
hold_list: list[float] = []
|
||||
|
||||
i = 0
|
||||
while i < n:
|
||||
if pos[i] == 0.0:
|
||||
i += 1
|
||||
continue
|
||||
# Start of a trade
|
||||
j = i + 1
|
||||
while j < n and pos[j] == pos[i]:
|
||||
j += 1
|
||||
# Trade from i to j-1
|
||||
trade_pnl = float(np.sum(ret[i:j]))
|
||||
pnl_list.append(trade_pnl)
|
||||
hold_list.append(float(j - i))
|
||||
i = j
|
||||
|
||||
if not pnl_list:
|
||||
return np.empty(0, dtype=np.float64), np.empty(0, dtype=np.float64)
|
||||
pnl, hold = _rust_extract_trades(pos, ret)
|
||||
return (
|
||||
np.array(pnl_list, dtype=np.float64),
|
||||
np.array(hold_list, dtype=np.float64),
|
||||
np.asarray(pnl, dtype=np.float64),
|
||||
np.asarray(hold, dtype=np.float64),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ from typing import Optional, Union
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from ferro_ta._ferro_ta import backtest_core as _rust_backtest_core
|
||||
from ferro_ta._ferro_ta import macd_crossover_signals as _rust_macd_crossover_signals
|
||||
from ferro_ta._ferro_ta import rsi_threshold_signals as _rust_rsi_threshold_signals
|
||||
from ferro_ta._ferro_ta import sma_crossover_signals as _rust_sma_crossover_signals
|
||||
from ferro_ta.core.exceptions import FerroTAInputError, FerroTAValueError
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -149,16 +153,16 @@ def rsi_strategy(
|
||||
overbought : float
|
||||
RSI level above which a short (-1) signal is generated (default 70).
|
||||
"""
|
||||
from ferro_ta import RSI # local import to avoid circular dep
|
||||
|
||||
if timeperiod < 1:
|
||||
raise FerroTAValueError(f"timeperiod must be >= 1, got {timeperiod}")
|
||||
|
||||
c = np.asarray(close, dtype=np.float64)
|
||||
rsi = np.asarray(RSI(c, timeperiod=timeperiod), dtype=np.float64)
|
||||
signals = np.where(rsi <= oversold, 1.0, np.where(rsi >= overbought, -1.0, 0.0))
|
||||
signals[np.isnan(rsi)] = np.nan
|
||||
return signals
|
||||
return np.asarray(
|
||||
_rust_rsi_threshold_signals(
|
||||
c, int(timeperiod), float(oversold), float(overbought)
|
||||
),
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
|
||||
def sma_crossover_strategy(
|
||||
@@ -183,8 +187,6 @@ def sma_crossover_strategy(
|
||||
slow : int
|
||||
Slow SMA period (default 30).
|
||||
"""
|
||||
from ferro_ta import SMA # local import
|
||||
|
||||
if fast < 1:
|
||||
raise FerroTAValueError(f"fast must be >= 1, got {fast}")
|
||||
if slow < 1:
|
||||
@@ -193,13 +195,10 @@ def sma_crossover_strategy(
|
||||
raise FerroTAValueError(f"fast ({fast}) must be less than slow ({slow})")
|
||||
|
||||
c = np.asarray(close, dtype=np.float64)
|
||||
sma_fast = np.asarray(SMA(c, timeperiod=fast), dtype=np.float64)
|
||||
sma_slow = np.asarray(SMA(c, timeperiod=slow), dtype=np.float64)
|
||||
signals = np.where(sma_fast > sma_slow, 1.0, -1.0).astype(np.float64)
|
||||
# Warm-up: NaN where either MA is NaN
|
||||
warmup = np.isnan(sma_fast) | np.isnan(sma_slow)
|
||||
signals[warmup] = np.nan
|
||||
return signals
|
||||
return np.asarray(
|
||||
_rust_sma_crossover_signals(c, int(fast), int(slow)),
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
|
||||
def macd_crossover_strategy(
|
||||
@@ -227,8 +226,6 @@ def macd_crossover_strategy(
|
||||
signalperiod : int
|
||||
Signal line EMA period (default 9).
|
||||
"""
|
||||
from ferro_ta import MACD # local import
|
||||
|
||||
if fastperiod < 1 or slowperiod < 1 or signalperiod < 1:
|
||||
raise FerroTAValueError("MACD periods must be >= 1")
|
||||
if fastperiod >= slowperiod:
|
||||
@@ -237,15 +234,12 @@ def macd_crossover_strategy(
|
||||
)
|
||||
|
||||
c = np.asarray(close, dtype=np.float64)
|
||||
macd_line, signal_line, _ = MACD(
|
||||
c, fastperiod=fastperiod, slowperiod=slowperiod, signalperiod=signalperiod
|
||||
return np.asarray(
|
||||
_rust_macd_crossover_signals(
|
||||
c, int(fastperiod), int(slowperiod), int(signalperiod)
|
||||
),
|
||||
dtype=np.float64,
|
||||
)
|
||||
macd_line = np.asarray(macd_line, dtype=np.float64)
|
||||
signal_line = np.asarray(signal_line, dtype=np.float64)
|
||||
signals = np.where(macd_line > signal_line, 1.0, -1.0).astype(np.float64)
|
||||
warmup = np.isnan(macd_line) | np.isnan(signal_line)
|
||||
signals[warmup] = np.nan
|
||||
return signals
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -342,52 +336,14 @@ def backtest(
|
||||
# Compute signals
|
||||
# ------------------------------------------------------------------
|
||||
signals = np.asarray(strategy_fn(c, **strategy_kwargs), dtype=np.float64)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Positions: lag signals by 1 bar to avoid look-ahead bias
|
||||
# ------------------------------------------------------------------
|
||||
positions = np.empty_like(signals)
|
||||
positions[0] = 0.0
|
||||
positions[1:] = signals[:-1]
|
||||
# Replace NaN in positions with 0 (flat)
|
||||
positions = np.nan_to_num(positions, nan=0.0)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Returns
|
||||
# ------------------------------------------------------------------
|
||||
bar_returns: np.ndarray = np.empty(len(c), dtype=np.float64)
|
||||
bar_returns[0] = 0.0
|
||||
bar_returns[1:] = np.diff(c) / c[:-1]
|
||||
|
||||
strategy_returns = positions * bar_returns
|
||||
position_changed = np.concatenate([[False], positions[1:] != positions[:-1]])
|
||||
|
||||
# Slippage: on each position change, reduce return by slippage_bps/10000 (one-way)
|
||||
if slippage_bps > 0:
|
||||
strategy_returns = strategy_returns.copy()
|
||||
strategy_returns[position_changed] -= slippage_bps / 10_000.0
|
||||
|
||||
# Cumulative equity: with optional commission per trade
|
||||
if commission_per_trade <= 0:
|
||||
equity = np.cumprod(1.0 + strategy_returns)
|
||||
else:
|
||||
gross_equity = np.cumprod(1.0 + strategy_returns)
|
||||
if np.any(gross_equity == 0.0):
|
||||
equity = np.empty(len(c), dtype=np.float64)
|
||||
equity[0] = 1.0
|
||||
for i in range(1, len(c)):
|
||||
equity[i] = equity[i - 1] * (1.0 + strategy_returns[i])
|
||||
if position_changed[i]:
|
||||
equity[i] -= commission_per_trade
|
||||
else:
|
||||
commissions = position_changed.astype(np.float64) * commission_per_trade
|
||||
discounted_commissions = np.cumsum(commissions / gross_equity)
|
||||
equity = gross_equity * (1.0 - discounted_commissions)
|
||||
positions, bar_returns, strategy_returns, equity = _rust_backtest_core(
|
||||
c, signals, float(commission_per_trade), float(slippage_bps)
|
||||
)
|
||||
|
||||
return BacktestResult(
|
||||
signals=signals,
|
||||
positions=positions,
|
||||
bar_returns=bar_returns,
|
||||
strategy_returns=strategy_returns,
|
||||
positions=np.asarray(positions, dtype=np.float64),
|
||||
bar_returns=np.asarray(bar_returns, dtype=np.float64),
|
||||
strategy_returns=np.asarray(strategy_returns, dtype=np.float64),
|
||||
equity=np.asarray(equity, dtype=np.float64),
|
||||
)
|
||||
|
||||
@@ -40,6 +40,7 @@ from __future__ import annotations
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from ferro_ta._ferro_ta import ratio as _rust_ratio
|
||||
from ferro_ta._ferro_ta import relative_strength as _rust_rel_strength
|
||||
from ferro_ta._ferro_ta import rolling_beta as _rust_rolling_beta
|
||||
from ferro_ta._ferro_ta import spread as _rust_spread
|
||||
@@ -162,11 +163,7 @@ def ratio(
|
||||
>>> list(ratio(a, b))
|
||||
[2.0, 3.0, 3.0]
|
||||
"""
|
||||
av = _to_f64(a)
|
||||
bv = _to_f64(b)
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
result = np.where(bv == 0, np.nan, av / bv)
|
||||
return result
|
||||
return _rust_ratio(_to_f64(a), _to_f64(b))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -11,10 +11,16 @@ from typing import Any
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from ferro_ta._ferro_ta import aggregate_greeks_legs as _rust_aggregate_greeks_legs
|
||||
from ferro_ta._ferro_ta import strategy_payoff_dense as _rust_strategy_payoff_dense
|
||||
from ferro_ta._ferro_ta import strategy_payoff_legs as _rust_strategy_payoff_legs
|
||||
from ferro_ta.analysis.options import OptionGreeks
|
||||
from ferro_ta.analysis.options import greeks as option_greeks
|
||||
from ferro_ta.analysis.options_strategy import DerivativesStrategy, StrategyLeg
|
||||
from ferro_ta.core.exceptions import FerroTAInputError, FerroTAValueError
|
||||
from ferro_ta.core.exceptions import (
|
||||
FerroTAInputError,
|
||||
FerroTAValueError,
|
||||
_normalize_rust_error,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PayoffLeg",
|
||||
@@ -79,14 +85,23 @@ def option_leg_payoff(
|
||||
) -> NDArray[np.float64]:
|
||||
"""Expiry payoff for a single option leg."""
|
||||
grid = _coerce_spot_grid(spot_grid)
|
||||
sign = _side_sign(side) * float(quantity) * float(multiplier)
|
||||
if option_type == "call":
|
||||
intrinsic = np.maximum(grid - float(strike), 0.0)
|
||||
elif option_type == "put":
|
||||
intrinsic = np.maximum(float(strike) - grid, 0.0)
|
||||
else:
|
||||
_side_sign(side)
|
||||
if option_type not in {"call", "put"}:
|
||||
raise FerroTAValueError("option_type must be 'call' or 'put'.")
|
||||
return sign * (intrinsic - float(premium))
|
||||
return np.asarray(
|
||||
_rust_strategy_payoff_dense(
|
||||
grid,
|
||||
np.array([0], dtype=np.int64), # option
|
||||
np.array([1 if side == "long" else -1], dtype=np.int64),
|
||||
np.array([1 if option_type == "call" else -1], dtype=np.int64),
|
||||
np.array([float(strike)], dtype=np.float64),
|
||||
np.array([float(premium)], dtype=np.float64),
|
||||
np.array([0.0], dtype=np.float64),
|
||||
np.array([float(quantity)], dtype=np.float64),
|
||||
np.array([float(multiplier)], dtype=np.float64),
|
||||
),
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
|
||||
def futures_leg_payoff(
|
||||
@@ -99,8 +114,21 @@ def futures_leg_payoff(
|
||||
) -> NDArray[np.float64]:
|
||||
"""P/L profile for a futures leg."""
|
||||
grid = _coerce_spot_grid(spot_grid)
|
||||
sign = _side_sign(side) * float(quantity) * float(multiplier)
|
||||
return sign * (grid - float(entry_price))
|
||||
_side_sign(side)
|
||||
return np.asarray(
|
||||
_rust_strategy_payoff_dense(
|
||||
grid,
|
||||
np.array([1], dtype=np.int64), # future
|
||||
np.array([1 if side == "long" else -1], dtype=np.int64),
|
||||
np.array([-1], dtype=np.int64),
|
||||
np.array([0.0], dtype=np.float64),
|
||||
np.array([0.0], dtype=np.float64),
|
||||
np.array([float(entry_price)], dtype=np.float64),
|
||||
np.array([float(quantity)], dtype=np.float64),
|
||||
np.array([float(multiplier)], dtype=np.float64),
|
||||
),
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
|
||||
def _mapping_to_leg(mapping: Mapping[str, Any]) -> PayoffLeg:
|
||||
@@ -141,31 +169,15 @@ def strategy_payoff(
|
||||
"""Aggregate expiry payoff across option and futures legs."""
|
||||
grid = _coerce_spot_grid(spot_grid)
|
||||
normalized = _normalize_legs(legs, strategy=strategy)
|
||||
total = np.zeros_like(grid)
|
||||
for leg in normalized:
|
||||
if leg.instrument == "option":
|
||||
if leg.strike is None:
|
||||
raise FerroTAValueError("Option payoff legs require strike.")
|
||||
total += option_leg_payoff(
|
||||
grid,
|
||||
strike=float(leg.strike),
|
||||
premium=float(leg.premium),
|
||||
option_type=str(leg.option_type),
|
||||
side=str(leg.side),
|
||||
quantity=float(leg.quantity),
|
||||
multiplier=float(leg.multiplier),
|
||||
)
|
||||
else:
|
||||
if leg.entry_price is None:
|
||||
raise FerroTAValueError("Futures payoff legs require entry_price.")
|
||||
total += futures_leg_payoff(
|
||||
grid,
|
||||
entry_price=float(leg.entry_price),
|
||||
side=str(leg.side),
|
||||
quantity=float(leg.quantity),
|
||||
multiplier=float(leg.multiplier),
|
||||
)
|
||||
return total
|
||||
if len(normalized) == 0:
|
||||
return np.zeros_like(grid)
|
||||
|
||||
try:
|
||||
return np.asarray(
|
||||
_rust_strategy_payoff_legs(grid, normalized), dtype=np.float64
|
||||
)
|
||||
except ValueError as err:
|
||||
_normalize_rust_error(err)
|
||||
|
||||
|
||||
def aggregate_greeks(
|
||||
@@ -176,42 +188,20 @@ def aggregate_greeks(
|
||||
) -> OptionGreeks:
|
||||
"""Aggregate Greeks across option and futures legs."""
|
||||
normalized = _normalize_legs(legs, strategy=strategy)
|
||||
totals = {
|
||||
"delta": 0.0,
|
||||
"gamma": 0.0,
|
||||
"vega": 0.0,
|
||||
"theta": 0.0,
|
||||
"rho": 0.0,
|
||||
}
|
||||
for leg in normalized:
|
||||
leg_sign = _side_sign(leg.side) * float(leg.quantity) * float(leg.multiplier)
|
||||
if leg.instrument == "future":
|
||||
totals["delta"] += leg_sign
|
||||
continue
|
||||
if leg.strike is None or leg.volatility is None or leg.time_to_expiry is None:
|
||||
raise FerroTAValueError(
|
||||
"Option legs require strike, volatility, and time_to_expiry for Greeks aggregation."
|
||||
)
|
||||
leg_greeks = option_greeks(
|
||||
float(spot),
|
||||
float(leg.strike),
|
||||
float(leg.rate),
|
||||
float(leg.time_to_expiry),
|
||||
float(leg.volatility),
|
||||
option_type=str(leg.option_type),
|
||||
model="bsm",
|
||||
carry=float(leg.carry),
|
||||
if len(normalized) == 0:
|
||||
return OptionGreeks(0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
try:
|
||||
delta, gamma, vega, theta, rho = _rust_aggregate_greeks_legs(
|
||||
float(spot), normalized
|
||||
)
|
||||
totals["delta"] += leg_sign * float(leg_greeks.delta)
|
||||
totals["gamma"] += leg_sign * float(leg_greeks.gamma)
|
||||
totals["vega"] += leg_sign * float(leg_greeks.vega)
|
||||
totals["theta"] += leg_sign * float(leg_greeks.theta)
|
||||
totals["rho"] += leg_sign * float(leg_greeks.rho)
|
||||
except ValueError as err:
|
||||
_normalize_rust_error(err)
|
||||
|
||||
return OptionGreeks(
|
||||
totals["delta"],
|
||||
totals["gamma"],
|
||||
totals["vega"],
|
||||
totals["theta"],
|
||||
totals["rho"],
|
||||
float(delta),
|
||||
float(gamma),
|
||||
float(vega),
|
||||
float(theta),
|
||||
float(rho),
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import Any, Optional, Union
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from ferro_ta._ferro_ta import forward_fill_nan as _rust_forward_fill_nan
|
||||
from ferro_ta._utils import _to_f64
|
||||
from ferro_ta.data.batch import compute_many
|
||||
|
||||
@@ -32,13 +33,9 @@ __all__ = [
|
||||
|
||||
|
||||
def _forward_fill_nan(arr: NDArray[np.float64]) -> NDArray[np.float64]:
|
||||
mask = np.isnan(arr)
|
||||
if not mask.any():
|
||||
return arr
|
||||
|
||||
last_valid = np.where(~mask, np.arange(len(arr)), 0)
|
||||
np.maximum.accumulate(last_valid, out=last_valid)
|
||||
return arr[last_valid]
|
||||
return np.asarray(
|
||||
_rust_forward_fill_nan(np.ascontiguousarray(arr, dtype=np.float64))
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -6,16 +6,16 @@ This module provides a 2-D batch API that accepts a 2-D numpy array
|
||||
a 2-D output array of the same shape.
|
||||
|
||||
For the most common indicators — SMA, EMA, RSI — the 2-D path is handled
|
||||
entirely in Rust (a single GIL release for all columns). The generic
|
||||
``batch_apply`` is available for other indicators that do not have a Rust
|
||||
batch implementation.
|
||||
entirely in Rust (a single GIL release for all columns). ``batch_apply``
|
||||
also dispatches these indicators to Rust when possible; other indicators
|
||||
use the generic Python fallback path.
|
||||
|
||||
Functions
|
||||
---------
|
||||
batch_sma — SMA on every column of a 2-D array (Rust fast path for 2-D)
|
||||
batch_ema — EMA on every column of a 2-D array (Rust fast path for 2-D)
|
||||
batch_rsi — RSI on every column of a 2-D array (Rust fast path for 2-D)
|
||||
batch_apply — Generic batch wrapper (Python loop) for any arbitrary indicator
|
||||
batch_apply — Generic batch wrapper with Rust fast-path for SMA/EMA/RSI
|
||||
|
||||
Usage
|
||||
-----
|
||||
@@ -92,6 +92,27 @@ _HLC_FASTPATH_DEFAULTS: dict[str, int] = {
|
||||
"WILLR": 14,
|
||||
}
|
||||
|
||||
_BATCH_FASTPATH_DEFAULTS: dict[str, int] = {
|
||||
"SMA": 30,
|
||||
"EMA": 30,
|
||||
"RSI": 14,
|
||||
}
|
||||
|
||||
|
||||
def _resolve_batch_fastpath(
|
||||
fn: Callable[..., np.ndarray],
|
||||
kwargs: dict[str, object],
|
||||
) -> tuple[str, int] | None:
|
||||
name = getattr(fn, "__name__", "").upper()
|
||||
if name not in _BATCH_FASTPATH_DEFAULTS:
|
||||
return None
|
||||
if set(kwargs) - {"timeperiod"}:
|
||||
return None
|
||||
raw = kwargs.get("timeperiod", _BATCH_FASTPATH_DEFAULTS[name])
|
||||
if not isinstance(raw, int):
|
||||
return None
|
||||
return name, int(raw)
|
||||
|
||||
|
||||
def _normalize_indicator_spec(
|
||||
spec: str | tuple[str, dict[str, object]] | tuple[str, dict[str, object], object],
|
||||
@@ -225,11 +246,9 @@ def batch_apply(
|
||||
) -> np.ndarray:
|
||||
"""Apply any single-series indicator *fn* to every column of *data*.
|
||||
|
||||
This is the generic fallback batch executor — it calls *fn* once per
|
||||
column in a Python loop. For the common indicators SMA, EMA, and RSI
|
||||
prefer the dedicated :func:`batch_sma`, :func:`batch_ema`, and
|
||||
:func:`batch_rsi` functions, which use a Rust-side loop and avoid
|
||||
per-column Python round-trips.
|
||||
For recognized close-only indicators (SMA/EMA/RSI with default or
|
||||
``timeperiod`` argument only), this function dispatches to the Rust
|
||||
batch kernels. Otherwise it falls back to a Python per-column loop.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -265,6 +284,16 @@ def batch_apply(
|
||||
if arr.ndim != 2:
|
||||
raise ValueError(f"batch_apply expects 1-D or 2-D input; got {arr.ndim}-D")
|
||||
|
||||
fastpath = _resolve_batch_fastpath(fn, kwargs)
|
||||
if fastpath is not None:
|
||||
indicator, timeperiod = fastpath
|
||||
contiguous = np.ascontiguousarray(arr)
|
||||
if indicator == "SMA":
|
||||
return np.asarray(_rust_batch_sma(contiguous, timeperiod, True))
|
||||
if indicator == "EMA":
|
||||
return np.asarray(_rust_batch_ema(contiguous, timeperiod, True))
|
||||
return np.asarray(_rust_batch_rsi(contiguous, timeperiod, True))
|
||||
|
||||
n_samples, n_series = arr.shape
|
||||
result = np.empty((n_samples, n_series), dtype=np.float64)
|
||||
for j in range(n_series):
|
||||
|
||||
@@ -27,6 +27,7 @@ Rust backend
|
||||
ferro_ta._ferro_ta.make_chunk_ranges
|
||||
ferro_ta._ferro_ta.trim_overlap
|
||||
ferro_ta._ferro_ta.stitch_chunks
|
||||
ferro_ta._ferro_ta.chunk_apply_close_indicator
|
||||
|
||||
Notes
|
||||
-----
|
||||
@@ -49,6 +50,9 @@ from typing import Any
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from ferro_ta._ferro_ta import (
|
||||
chunk_apply_close_indicator as _rust_chunk_apply_close_indicator,
|
||||
)
|
||||
from ferro_ta._ferro_ta import (
|
||||
make_chunk_ranges as _rust_make_chunk_ranges,
|
||||
)
|
||||
@@ -67,6 +71,26 @@ __all__ = [
|
||||
"stitch_chunks",
|
||||
]
|
||||
|
||||
_FASTPATH_DEFAULT_PERIODS: dict[str, int] = {
|
||||
"SMA": 30,
|
||||
"EMA": 30,
|
||||
"RSI": 14,
|
||||
}
|
||||
|
||||
|
||||
def _resolve_chunk_fastpath(
|
||||
fn: Callable[..., Any], fn_kwargs: dict[str, Any]
|
||||
) -> tuple[str, int] | None:
|
||||
name = getattr(fn, "__name__", "").upper()
|
||||
if name not in _FASTPATH_DEFAULT_PERIODS:
|
||||
return None
|
||||
if set(fn_kwargs) - {"timeperiod"}:
|
||||
return None
|
||||
raw = fn_kwargs.get("timeperiod", _FASTPATH_DEFAULT_PERIODS[name])
|
||||
if not isinstance(raw, int):
|
||||
return None
|
||||
return name, int(raw)
|
||||
|
||||
|
||||
def make_chunk_ranges(
|
||||
n: int,
|
||||
@@ -190,6 +214,20 @@ def chunk_apply(
|
||||
if n == 0:
|
||||
return np.empty(0, dtype=np.float64)
|
||||
|
||||
fastpath = _resolve_chunk_fastpath(fn, fn_kwargs)
|
||||
if fastpath is not None:
|
||||
indicator, timeperiod = fastpath
|
||||
return np.asarray(
|
||||
_rust_chunk_apply_close_indicator(
|
||||
np.ascontiguousarray(s),
|
||||
indicator,
|
||||
int(timeperiod),
|
||||
int(chunk_size),
|
||||
int(overlap),
|
||||
),
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
ranges = make_chunk_ranges(n, chunk_size, overlap)
|
||||
if len(ranges) == 0:
|
||||
result = fn(s, **fn_kwargs)
|
||||
|
||||
+1318
-392
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build a cross-surface API manifest for ferro-ta.
|
||||
|
||||
The generated manifest summarizes:
|
||||
- Python indicator/method exposure (from ferro_ta.tools.api_info)
|
||||
- Core Rust crate public functions (ferro_ta_core)
|
||||
- WASM/Node exported functions (from wasm pkg d.ts)
|
||||
|
||||
Output is written to `docs/api_manifest.json`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import datetime as _dt
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _load_api_info_module(root: Path, module_path: Path):
|
||||
python_root = str(root / "python")
|
||||
if python_root not in sys.path:
|
||||
sys.path.insert(0, python_root)
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"ferro_ta_tools_api_info", module_path
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load module spec from {module_path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module) # type: ignore[assignment]
|
||||
return module
|
||||
|
||||
|
||||
def _module_file(root: Path, module_name: str) -> Path | None:
|
||||
module_rel = module_name.replace(".", "/")
|
||||
file_path = root / "python" / f"{module_rel}.py"
|
||||
if file_path.exists():
|
||||
return file_path
|
||||
init_path = root / "python" / module_rel / "__init__.py"
|
||||
if init_path.exists():
|
||||
return init_path
|
||||
return None
|
||||
|
||||
|
||||
def _extract_dunder_all(file_path: Path) -> list[str]:
|
||||
try:
|
||||
source = file_path.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source, filename=str(file_path))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
exports: list[str] = []
|
||||
for node in tree.body:
|
||||
value_node = None
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == "__all__":
|
||||
value_node = node.value
|
||||
break
|
||||
elif isinstance(node, ast.AnnAssign):
|
||||
target = node.target
|
||||
if isinstance(target, ast.Name) and target.id == "__all__":
|
||||
value_node = node.value
|
||||
if value_node is None:
|
||||
continue
|
||||
try:
|
||||
value = ast.literal_eval(value_node)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(value, str):
|
||||
exports = [value]
|
||||
elif isinstance(value, (list, tuple)):
|
||||
exports = [item for item in value if isinstance(item, str)]
|
||||
return exports
|
||||
|
||||
|
||||
def _module_exports(root: Path, module_name: str) -> list[str]:
|
||||
file_path = _module_file(root, module_name)
|
||||
if file_path is None:
|
||||
return []
|
||||
return _extract_dunder_all(file_path)
|
||||
|
||||
|
||||
def _extract_python_api(root: Path) -> dict[str, Any]:
|
||||
module_path = root / "python" / "ferro_ta" / "tools" / "api_info.py"
|
||||
api_info_module = _load_api_info_module(root, module_path)
|
||||
|
||||
category_modules = dict(getattr(api_info_module, "_CATEGORY_MODULES", {}))
|
||||
method_modules = dict(getattr(api_info_module, "_METHOD_MODULES", {}))
|
||||
|
||||
indicators: list[dict[str, Any]] = []
|
||||
seen_indicators: set[str] = set()
|
||||
for category, module_name in category_modules.items():
|
||||
for name in _module_exports(root, module_name):
|
||||
if name in seen_indicators:
|
||||
continue
|
||||
seen_indicators.add(name)
|
||||
indicators.append(
|
||||
{
|
||||
"name": name,
|
||||
"category": category,
|
||||
"module": module_name,
|
||||
"doc": "",
|
||||
"params": [],
|
||||
}
|
||||
)
|
||||
|
||||
methods: list[dict[str, Any]] = []
|
||||
seen_methods: set[tuple[str, str]] = set()
|
||||
for category, module_name in method_modules.items():
|
||||
for name in _module_exports(root, module_name):
|
||||
key = (module_name, name)
|
||||
if key in seen_methods:
|
||||
continue
|
||||
seen_methods.add(key)
|
||||
methods.append(
|
||||
{
|
||||
"name": name,
|
||||
"category": category,
|
||||
"module": module_name,
|
||||
"doc": "",
|
||||
"params": [],
|
||||
}
|
||||
)
|
||||
|
||||
indicators.sort(key=lambda entry: entry["name"])
|
||||
methods.sort(key=lambda entry: (entry["category"], entry["name"]))
|
||||
|
||||
categories = sorted({entry["category"] for entry in indicators})
|
||||
|
||||
if not indicators:
|
||||
raise RuntimeError(
|
||||
"No Python indicators discovered from source exports. "
|
||||
"Check `python/ferro_ta/tools/api_info.py` mappings and module __all__ declarations."
|
||||
)
|
||||
|
||||
return {
|
||||
"indicator_count": len(indicators),
|
||||
"method_count": len(methods),
|
||||
"categories": categories,
|
||||
"indicators": indicators,
|
||||
"methods": methods,
|
||||
}
|
||||
|
||||
|
||||
def _extract_core_exports(root: Path) -> list[dict[str, str]]:
|
||||
core_src = root / "crates" / "ferro_ta_core" / "src"
|
||||
entries: list[dict[str, str]] = []
|
||||
|
||||
for rs_file in sorted(core_src.rglob("*.rs")):
|
||||
rel = rs_file.relative_to(core_src).as_posix()
|
||||
module = rel[:-3].replace("/", ".")
|
||||
text = rs_file.read_text(encoding="utf-8")
|
||||
for match in re.finditer(r"(?m)^\s*pub\s+fn\s+([A-Za-z0-9_]+)\s*\(", text):
|
||||
entries.append(
|
||||
{
|
||||
"module": module,
|
||||
"function": match.group(1),
|
||||
"file": rel,
|
||||
}
|
||||
)
|
||||
|
||||
entries.sort(key=lambda item: (item["module"], item["function"]))
|
||||
return entries
|
||||
|
||||
|
||||
def _extract_wasm_exports(root: Path) -> list[str]:
|
||||
exports: set[str] = set()
|
||||
|
||||
# Source exports are the canonical declaration of the WASM/Node API and
|
||||
# avoid drift when a stale wasm/pkg folder is present locally.
|
||||
wasm_lib = root / "wasm" / "src" / "lib.rs"
|
||||
if wasm_lib.exists():
|
||||
text = wasm_lib.read_text(encoding="utf-8")
|
||||
for match in re.finditer(
|
||||
r"(?ms)#\s*\[wasm_bindgen(?:\([^\)]*\))?\]\s*pub\s+fn\s+([A-Za-z0-9_]+)\s*\(",
|
||||
text,
|
||||
):
|
||||
exports.add(match.group(1))
|
||||
if exports:
|
||||
return sorted(exports)
|
||||
|
||||
# Fallback to generated declarations if source parsing did not find exports.
|
||||
dts_path = root / "wasm" / "pkg" / "ferro_ta_wasm.d.ts"
|
||||
if dts_path.exists():
|
||||
for line in dts_path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("export function "):
|
||||
name = line[len("export function ") :].split("(")[0].strip()
|
||||
if name:
|
||||
exports.add(name)
|
||||
|
||||
return sorted(exports)
|
||||
|
||||
|
||||
def _safe_git_head(root: Path) -> str | None:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return None
|
||||
value = completed.stdout.strip()
|
||||
return value or None
|
||||
|
||||
|
||||
def build_manifest(
|
||||
root: Path, include_runtime_metadata: bool = False
|
||||
) -> dict[str, Any]:
|
||||
python_api = _extract_python_api(root)
|
||||
rust_core = _extract_core_exports(root)
|
||||
wasm_exports = _extract_wasm_exports(root)
|
||||
|
||||
python_indicator_names = {entry["name"] for entry in python_api["indicators"]}
|
||||
python_indicator_names_lc = {name.lower() for name in python_indicator_names}
|
||||
wasm_set = set(wasm_exports)
|
||||
wasm_set_lc = {name.lower() for name in wasm_set}
|
||||
common_with_wasm = sorted(python_indicator_names_lc.intersection(wasm_set_lc))
|
||||
|
||||
manifest: dict[str, Any] = {
|
||||
"surfaces": {
|
||||
"python": python_api,
|
||||
"rust_core": {
|
||||
"public_function_count": len(rust_core),
|
||||
"functions": rust_core,
|
||||
},
|
||||
"wasm_node": {
|
||||
"export_count": len(wasm_exports),
|
||||
"exports": wasm_exports,
|
||||
},
|
||||
},
|
||||
"parity_summary": {
|
||||
"python_indicator_count": len(python_indicator_names_lc),
|
||||
"wasm_export_count": len(wasm_set),
|
||||
"common_python_wasm_count": len(common_with_wasm),
|
||||
"common_python_wasm": common_with_wasm,
|
||||
"python_only_vs_wasm": sorted(python_indicator_names_lc - wasm_set_lc),
|
||||
"wasm_only_vs_python": sorted(wasm_set_lc - python_indicator_names_lc),
|
||||
},
|
||||
}
|
||||
|
||||
if include_runtime_metadata:
|
||||
manifest["generated_at_utc"] = _dt.datetime.now(tz=_dt.UTC).isoformat()
|
||||
manifest["git_head"] = _safe_git_head(root)
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Build cross-surface API manifest")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path("docs/api_manifest.json"),
|
||||
help="Output JSON path relative to repo root (default: docs/api_manifest.json)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-runtime-metadata",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Include non-deterministic metadata fields (timestamp, git head). "
|
||||
"Disabled by default to keep manifest reproducible for CI checks."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = _repo_root()
|
||||
output_path = (root / args.output).resolve()
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manifest = build_manifest(
|
||||
root, include_runtime_metadata=args.include_runtime_metadata
|
||||
)
|
||||
output_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"Wrote API manifest to {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -15,7 +15,6 @@ import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check that docs/api_manifest.json is up-to-date.
|
||||
|
||||
This script regenerates the deterministic manifest in-memory and compares it to
|
||||
the committed file. It exits non-zero if drift is detected.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
python_root = str(root / "python")
|
||||
if python_root not in sys.path:
|
||||
sys.path.insert(0, python_root)
|
||||
scripts_root = str(root / "scripts")
|
||||
if scripts_root not in sys.path:
|
||||
sys.path.insert(0, scripts_root)
|
||||
|
||||
from build_api_manifest import build_manifest
|
||||
|
||||
manifest_path = root / "docs" / "api_manifest.json"
|
||||
|
||||
if not manifest_path.exists():
|
||||
print(
|
||||
"docs/api_manifest.json is missing. Run:\n"
|
||||
" python scripts/build_api_manifest.py --output docs/api_manifest.json"
|
||||
)
|
||||
return 1
|
||||
|
||||
expected = build_manifest(root, include_runtime_metadata=False)
|
||||
actual = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
|
||||
if actual != expected:
|
||||
print(
|
||||
"docs/api_manifest.json is out of date.\n"
|
||||
"Run:\n"
|
||||
" python scripts/build_api_manifest.py --output docs/api_manifest.json\n"
|
||||
"and commit the updated file."
|
||||
)
|
||||
return 1
|
||||
|
||||
print("docs/api_manifest.json is up to date.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+193
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
AVAILABLE_CHECKS=(
|
||||
version
|
||||
changelog
|
||||
rust_fmt
|
||||
rust_clippy
|
||||
rust_core
|
||||
rust_bench
|
||||
python_lint
|
||||
python_typecheck
|
||||
python_test
|
||||
docs
|
||||
wasm
|
||||
manifest
|
||||
)
|
||||
|
||||
DEFAULT_CHECKS=("${AVAILABLE_CHECKS[@]}")
|
||||
|
||||
python_env_ready=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/pre_push_checks.sh
|
||||
scripts/pre_push_checks.sh <check> [<check> ...]
|
||||
scripts/pre_push_checks.sh --list
|
||||
|
||||
Runs the repo's basic local CI gate before push. By default it covers:
|
||||
version changelog rust_fmt rust_clippy rust_core rust_bench
|
||||
python_lint python_typecheck python_test docs wasm manifest
|
||||
|
||||
Notes:
|
||||
- This mirrors the required CI categories we can run locally.
|
||||
- It intentionally skips the multi-Python test matrix, audit jobs, perf smoke,
|
||||
and benchmark-regression jobs.
|
||||
EOF
|
||||
}
|
||||
|
||||
list_checks() {
|
||||
printf '%s\n' "${AVAILABLE_CHECKS[@]}"
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
local command_name="$1"
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
echo "Missing required command: $command_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
printf ' +'
|
||||
printf ' %q' "$@"
|
||||
printf '\n'
|
||||
"$@"
|
||||
}
|
||||
|
||||
ensure_python_env() {
|
||||
if [[ "$python_env_ready" -eq 1 ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
need_cmd uv
|
||||
run_cmd uv sync --extra dev --extra docs --extra mcp
|
||||
run_cmd uv run --extra dev --extra docs --extra mcp maturin develop --release
|
||||
python_env_ready=1
|
||||
}
|
||||
|
||||
run_version() {
|
||||
need_cmd python3
|
||||
run_cmd python3 scripts/bump_version.py --check
|
||||
}
|
||||
|
||||
run_changelog() {
|
||||
need_cmd python3
|
||||
run_cmd python3 scripts/check_changelog.py
|
||||
}
|
||||
|
||||
run_rust_fmt() {
|
||||
need_cmd cargo
|
||||
run_cmd cargo fmt --all -- --check
|
||||
}
|
||||
|
||||
run_rust_clippy() {
|
||||
need_cmd cargo
|
||||
run_cmd cargo clippy --release -- -D warnings
|
||||
}
|
||||
|
||||
run_rust_core() {
|
||||
need_cmd cargo
|
||||
run_cmd cargo build -p ferro_ta_core
|
||||
run_cmd cargo test -p ferro_ta_core
|
||||
}
|
||||
|
||||
run_rust_bench() {
|
||||
need_cmd cargo
|
||||
run_cmd cargo bench -p ferro_ta_core --no-run
|
||||
}
|
||||
|
||||
run_python_lint() {
|
||||
need_cmd uv
|
||||
run_cmd uv run --with ruff ruff check python/ tests/
|
||||
run_cmd uv run --with ruff ruff format --check python/ tests/
|
||||
}
|
||||
|
||||
run_python_typecheck() {
|
||||
need_cmd uv
|
||||
run_cmd uv run --with mypy --with numpy python -m mypy python/ferro_ta --ignore-missing-imports --no-error-summary
|
||||
run_cmd uv run --with pyright python -m pyright python/ferro_ta
|
||||
}
|
||||
|
||||
run_python_test() {
|
||||
ensure_python_env
|
||||
run_cmd uv run --extra dev --extra mcp --with pytest-cov pytest tests/unit/ tests/integration/ -v --cov=ferro_ta --cov-report=term-missing --cov-fail-under=65
|
||||
}
|
||||
|
||||
run_docs() {
|
||||
ensure_python_env
|
||||
run_cmd uv run --extra docs python -m sphinx -b html docs docs/_build -W --keep-going
|
||||
}
|
||||
|
||||
run_wasm() {
|
||||
need_cmd node
|
||||
need_cmd wasm-pack
|
||||
local benchmark_json="../.wasm_benchmark.prepush.json"
|
||||
(
|
||||
cd wasm
|
||||
trap 'rm -f "$benchmark_json"' EXIT
|
||||
run_cmd wasm-pack test --node
|
||||
run_cmd wasm-pack build --target nodejs --out-dir pkg
|
||||
run_cmd node bench.js --json "$benchmark_json"
|
||||
)
|
||||
}
|
||||
|
||||
run_manifest() {
|
||||
need_cmd python3
|
||||
run_cmd python3 scripts/check_api_manifest.py
|
||||
}
|
||||
|
||||
run_check() {
|
||||
local check_name="$1"
|
||||
case "$check_name" in
|
||||
version) run_version ;;
|
||||
changelog) run_changelog ;;
|
||||
rust_fmt) run_rust_fmt ;;
|
||||
rust_clippy) run_rust_clippy ;;
|
||||
rust_core) run_rust_core ;;
|
||||
rust_bench) run_rust_bench ;;
|
||||
python_lint) run_python_lint ;;
|
||||
python_typecheck) run_python_typecheck ;;
|
||||
python_test) run_python_test ;;
|
||||
docs) run_docs ;;
|
||||
wasm) run_wasm ;;
|
||||
manifest) run_manifest ;;
|
||||
*)
|
||||
echo "Unknown check: $check_name" >&2
|
||||
echo "Use --list to see supported checks." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "--list" ]]; then
|
||||
list_checks
|
||||
exit 0
|
||||
fi
|
||||
|
||||
selected_checks=()
|
||||
if [[ "$#" -gt 0 ]]; then
|
||||
selected_checks=("$@")
|
||||
else
|
||||
selected_checks=("${DEFAULT_CHECKS[@]}")
|
||||
fi
|
||||
|
||||
total_checks="${#selected_checks[@]}"
|
||||
index=0
|
||||
for check_name in "${selected_checks[@]}"; do
|
||||
index=$((index + 1))
|
||||
printf '\n[%d/%d] %s\n' "$index" "$total_checks" "$check_name"
|
||||
run_check "$check_name"
|
||||
done
|
||||
|
||||
printf '\nAll selected pre-push checks passed.\n'
|
||||
@@ -191,6 +191,54 @@ pub fn signal_attribution<'py>(
|
||||
Ok((labels.into_pyarray(py), contributions.into_pyarray(py)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// extract_trades
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Extract trade-level pnl and hold durations from positions and strategy returns.
|
||||
///
|
||||
/// A trade is a maximal contiguous run of non-zero position values.
|
||||
#[pyfunction]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn extract_trades<'py>(
|
||||
py: Python<'py>,
|
||||
positions: PyReadonlyArray1<'py, f64>,
|
||||
strategy_returns: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
|
||||
let pos = positions.as_slice()?;
|
||||
let ret = strategy_returns.as_slice()?;
|
||||
let n = pos.len();
|
||||
if n != ret.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"positions and strategy_returns must have the same length",
|
||||
));
|
||||
}
|
||||
|
||||
let mut pnl = Vec::<f64>::new();
|
||||
let mut hold = Vec::<f64>::new();
|
||||
|
||||
let mut i = 0usize;
|
||||
while i < n {
|
||||
if pos[i] == 0.0 {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let mut j = i + 1;
|
||||
while j < n && pos[j] == pos[i] {
|
||||
j += 1;
|
||||
}
|
||||
let mut trade_pnl = 0.0_f64;
|
||||
for v in ret.iter().take(j).skip(i) {
|
||||
trade_pnl += *v;
|
||||
}
|
||||
pnl.push(trade_pnl);
|
||||
hold.push((j - i) as f64);
|
||||
i = j;
|
||||
}
|
||||
|
||||
Ok((pnl.into_pyarray(py), hold.into_pyarray(py)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -199,5 +247,6 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(trade_stats, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(monthly_contribution, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(signal_attribution, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(extract_trades, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
//! Rust-backed strategy signal generation and backtest core.
|
||||
//!
|
||||
//! These functions move the hot loops from Python into Rust while preserving
|
||||
//! the public Python behavior.
|
||||
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
fn nan_to_num_with_numpy_defaults(v: f64) -> f64 {
|
||||
if v.is_nan() {
|
||||
0.0
|
||||
} else if v.is_infinite() {
|
||||
if v.is_sign_positive() {
|
||||
f64::MAX
|
||||
} else {
|
||||
-f64::MAX
|
||||
}
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Strategy signal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// RSI threshold strategy:
|
||||
/// +1 when RSI <= oversold, -1 when RSI >= overbought, 0 otherwise.
|
||||
/// Warm-up bars are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, timeperiod = 14, oversold = 30.0, overbought = 70.0))]
|
||||
pub fn rsi_threshold_signals<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
timeperiod: usize,
|
||||
oversold: f64,
|
||||
overbought: f64,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(timeperiod, "timeperiod", 1)?;
|
||||
let prices = close.as_slice()?;
|
||||
let rsi = ferro_ta_core::momentum::rsi(prices, timeperiod);
|
||||
let out: Vec<f64> = rsi
|
||||
.iter()
|
||||
.map(|&v| {
|
||||
if v.is_nan() {
|
||||
f64::NAN
|
||||
} else if v <= oversold {
|
||||
1.0
|
||||
} else if v >= overbought {
|
||||
-1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// SMA crossover strategy:
|
||||
/// +1 when fast SMA > slow SMA, -1 otherwise. Warm-up bars are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, fast = 10, slow = 30))]
|
||||
pub fn sma_crossover_signals<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
fast: usize,
|
||||
slow: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(fast, "fast", 1)?;
|
||||
validation::validate_timeperiod(slow, "slow", 1)?;
|
||||
if fast >= slow {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"fast ({fast}) must be less than slow ({slow})"
|
||||
)));
|
||||
}
|
||||
let prices = close.as_slice()?;
|
||||
let sma_fast = ferro_ta_core::overlap::sma(prices, fast);
|
||||
let sma_slow = ferro_ta_core::overlap::sma(prices, slow);
|
||||
let out: Vec<f64> = sma_fast
|
||||
.iter()
|
||||
.zip(sma_slow.iter())
|
||||
.map(|(&f, &s)| {
|
||||
if f.is_nan() || s.is_nan() {
|
||||
f64::NAN
|
||||
} else if f > s {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// MACD crossover strategy:
|
||||
/// +1 when MACD line > signal line, -1 otherwise. Warm-up bars are NaN.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, fastperiod = 12, slowperiod = 26, signalperiod = 9))]
|
||||
pub fn macd_crossover_signals<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
fastperiod: usize,
|
||||
slowperiod: usize,
|
||||
signalperiod: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
validation::validate_timeperiod(fastperiod, "fastperiod", 1)?;
|
||||
validation::validate_timeperiod(slowperiod, "slowperiod", 1)?;
|
||||
validation::validate_timeperiod(signalperiod, "signalperiod", 1)?;
|
||||
if fastperiod >= slowperiod {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"fastperiod ({fastperiod}) must be less than slowperiod ({slowperiod})"
|
||||
)));
|
||||
}
|
||||
|
||||
let prices = close.as_slice()?;
|
||||
let (macd_line, signal_line, _) =
|
||||
ferro_ta_core::overlap::macd(prices, fastperiod, slowperiod, signalperiod);
|
||||
let out: Vec<f64> = macd_line
|
||||
.iter()
|
||||
.zip(signal_line.iter())
|
||||
.map(|(&m, &s)| {
|
||||
if m.is_nan() || s.is_nan() {
|
||||
f64::NAN
|
||||
} else if m > s {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Backtest core
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Backtest core loop over close prices and strategy signals.
|
||||
///
|
||||
/// Returns `(positions, bar_returns, strategy_returns, equity)`.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (close, signals, commission_per_trade = 0.0, slippage_bps = 0.0))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn backtest_core<'py>(
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
signals: PyReadonlyArray1<'py, f64>,
|
||||
commission_per_trade: f64,
|
||||
slippage_bps: f64,
|
||||
) -> PyResult<(
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
)> {
|
||||
let c = close.as_slice()?;
|
||||
let s = signals.as_slice()?;
|
||||
let n = c.len();
|
||||
validation::validate_equal_length(&[(n, "close"), (s.len(), "signals")])?;
|
||||
|
||||
let mut positions = vec![0.0_f64; n];
|
||||
if n > 1 {
|
||||
for i in 1..n {
|
||||
positions[i] = nan_to_num_with_numpy_defaults(s[i - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut bar_returns = vec![0.0_f64; n];
|
||||
for i in 1..n {
|
||||
bar_returns[i] = (c[i] - c[i - 1]) / c[i - 1];
|
||||
}
|
||||
|
||||
let mut strategy_returns = vec![0.0_f64; n];
|
||||
for i in 0..n {
|
||||
strategy_returns[i] = positions[i] * bar_returns[i];
|
||||
}
|
||||
|
||||
let mut position_changed = vec![false; n];
|
||||
for i in 1..n {
|
||||
position_changed[i] = positions[i] != positions[i - 1];
|
||||
}
|
||||
|
||||
if slippage_bps > 0.0 {
|
||||
let slip = slippage_bps / 10_000.0;
|
||||
for i in 0..n {
|
||||
if position_changed[i] {
|
||||
strategy_returns[i] -= slip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut equity = vec![1.0_f64; n];
|
||||
if n > 0 {
|
||||
if commission_per_trade <= 0.0 {
|
||||
let mut gross = 1.0_f64;
|
||||
for i in 0..n {
|
||||
gross *= 1.0 + strategy_returns[i];
|
||||
equity[i] = gross;
|
||||
}
|
||||
} else {
|
||||
let mut gross_equity = vec![1.0_f64; n];
|
||||
let mut gross = 1.0_f64;
|
||||
for i in 0..n {
|
||||
gross *= 1.0 + strategy_returns[i];
|
||||
gross_equity[i] = gross;
|
||||
}
|
||||
|
||||
if gross_equity.contains(&0.0) {
|
||||
equity[0] = 1.0;
|
||||
for i in 1..n {
|
||||
equity[i] = equity[i - 1] * (1.0 + strategy_returns[i]);
|
||||
if position_changed[i] {
|
||||
equity[i] -= commission_per_trade;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut discounted_commissions = 0.0_f64;
|
||||
for i in 0..n {
|
||||
if position_changed[i] {
|
||||
discounted_commissions += commission_per_trade / gross_equity[i];
|
||||
}
|
||||
equity[i] = gross_equity[i] * (1.0 - discounted_commissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
positions.into_pyarray(py),
|
||||
bar_returns.into_pyarray(py),
|
||||
strategy_returns.into_pyarray(py),
|
||||
equity.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(rsi_threshold_signals, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(sma_crossover_signals, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(macd_crossover_signals, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(backtest_core, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
+130
-5
@@ -8,11 +8,15 @@
|
||||
//!
|
||||
//! Functions
|
||||
//! ---------
|
||||
//! - `trim_overlap` — remove the first *overlap* elements from an array
|
||||
//! (to strip the warm-up from a chunk's indicator output).
|
||||
//! - `stitch_chunks` — concatenate trimmed chunk results into one array.
|
||||
//! - `make_chunk_ranges` — compute start/end indices for a series given chunk
|
||||
//! size and overlap, for use by the Python caller.
|
||||
//! - `trim_overlap` — remove the first *overlap* elements from
|
||||
//! an array (to strip the warm-up from a chunk's indicator output).
|
||||
//! - `stitch_chunks` — concatenate trimmed chunk results into one
|
||||
//! array.
|
||||
//! - `make_chunk_ranges` — compute start/end indices for a series
|
||||
//! given chunk size and overlap, for use by the Python caller.
|
||||
//! - `chunk_apply_close_indicator`— run chunked close-only indicators fully in
|
||||
//! Rust (SMA/EMA/RSI).
|
||||
//! - `forward_fill_nan` — forward-fill NaN values in a 1-D array.
|
||||
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
@@ -132,6 +136,125 @@ pub fn make_chunk_ranges<'py>(
|
||||
Ok(ranges.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// chunk_apply_close_indicator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn compute_close_indicator(
|
||||
indicator: &str,
|
||||
series: &[f64],
|
||||
timeperiod: usize,
|
||||
) -> PyResult<Vec<f64>> {
|
||||
match indicator {
|
||||
"SMA" => Ok(ferro_ta_core::overlap::sma(series, timeperiod)),
|
||||
"EMA" => Ok(ferro_ta_core::overlap::ema(series, timeperiod)),
|
||||
"RSI" => Ok(ferro_ta_core::momentum::rsi(series, timeperiod)),
|
||||
_ => Err(PyValueError::new_err(format!(
|
||||
"chunk_apply_close_indicator does not support indicator '{indicator}'"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run chunked execution for close-only indicators in Rust.
|
||||
///
|
||||
/// Parameters
|
||||
/// ----------
|
||||
/// series : 1-D float64 array
|
||||
/// indicator : one of {"SMA", "EMA", "RSI"}
|
||||
/// timeperiod : indicator period (>= 1)
|
||||
/// chunk_size : output bars per chunk (>= 1)
|
||||
/// overlap : warm-up bars prepended to each chunk
|
||||
///
|
||||
/// Returns
|
||||
/// -------
|
||||
/// 1-D float64 array with the same length as `series`.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (series, indicator, timeperiod, chunk_size = 10_000, overlap = 100))]
|
||||
pub fn chunk_apply_close_indicator<'py>(
|
||||
py: Python<'py>,
|
||||
series: PyReadonlyArray1<'py, f64>,
|
||||
indicator: &str,
|
||||
timeperiod: usize,
|
||||
chunk_size: usize,
|
||||
overlap: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
if timeperiod == 0 {
|
||||
return Err(PyValueError::new_err("timeperiod must be >= 1"));
|
||||
}
|
||||
if chunk_size == 0 {
|
||||
return Err(PyValueError::new_err("chunk_size must be >= 1"));
|
||||
}
|
||||
|
||||
let values = series.as_slice()?;
|
||||
if values.is_empty() {
|
||||
return Ok(Vec::<f64>::new().into_pyarray(py));
|
||||
}
|
||||
|
||||
let name = indicator.to_ascii_uppercase();
|
||||
let n = values.len();
|
||||
let mut stitched: Vec<f64> = Vec::with_capacity(n);
|
||||
let mut start = 0usize;
|
||||
let mut chunk_index = 0usize;
|
||||
|
||||
loop {
|
||||
let end = (start + chunk_size + overlap).min(n);
|
||||
let chunk = &values[start..end];
|
||||
let out = compute_close_indicator(name.as_str(), chunk, timeperiod)?;
|
||||
|
||||
let discard = if chunk_index == 0 { 0 } else { overlap };
|
||||
if discard > out.len() {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"overlap ({discard}) must be <= chunk output length ({})",
|
||||
out.len()
|
||||
)));
|
||||
}
|
||||
stitched.extend_from_slice(&out[discard..]);
|
||||
|
||||
if end >= n {
|
||||
break;
|
||||
}
|
||||
start = end.saturating_sub(overlap);
|
||||
chunk_index += 1;
|
||||
}
|
||||
|
||||
if stitched.len() != n {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"internal chunk stitching error: expected output length {n}, got {}",
|
||||
stitched.len()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(stitched.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// forward_fill_nan
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Forward-fill NaN values in a 1-D array.
|
||||
///
|
||||
/// Leading NaN values are preserved until the first non-NaN value appears.
|
||||
#[pyfunction]
|
||||
pub fn forward_fill_nan<'py>(
|
||||
py: Python<'py>,
|
||||
values: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let input = values.as_slice()?;
|
||||
let mut out = Vec::with_capacity(input.len());
|
||||
let mut last = f64::NAN;
|
||||
|
||||
for &value in input {
|
||||
if value.is_nan() {
|
||||
out.push(last);
|
||||
} else {
|
||||
last = value;
|
||||
out.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -140,5 +263,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(trim_overlap, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(stitch_chunks, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(make_chunk_ranges, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(chunk_apply_close_indicator, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(forward_fill_nan, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod aggregation;
|
||||
pub mod alerts;
|
||||
pub mod attribution;
|
||||
pub mod backtest;
|
||||
pub mod batch;
|
||||
pub mod chunked;
|
||||
pub mod crypto;
|
||||
@@ -70,5 +71,6 @@ fn _ferro_ta(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
chunked::register(m)?;
|
||||
regime::register(m)?;
|
||||
attribution::register(m)?;
|
||||
backtest::register(m)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
mod chain;
|
||||
mod greeks;
|
||||
mod iv;
|
||||
mod payoff;
|
||||
mod pricing;
|
||||
mod surface;
|
||||
|
||||
@@ -63,5 +64,21 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::chain::select_strike_delta, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::payoff::strategy_payoff_dense,
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::payoff::strategy_payoff_legs,
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::payoff::aggregate_greeks_dense,
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::payoff::aggregate_greeks_legs,
|
||||
m
|
||||
)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyAny, PyTuple};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Instrument {
|
||||
Option,
|
||||
Future,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Side {
|
||||
Long,
|
||||
Short,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum OptionType {
|
||||
Call,
|
||||
Put,
|
||||
}
|
||||
|
||||
impl Side {
|
||||
fn sign(self) -> f64 {
|
||||
match self {
|
||||
Side::Long => 1.0,
|
||||
Side::Short => -1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_instrument(v: i64) -> PyResult<Instrument> {
|
||||
match v {
|
||||
0 => Ok(Instrument::Option),
|
||||
1 => Ok(Instrument::Future),
|
||||
_ => Err(PyValueError::new_err(
|
||||
"instrument must be 0 (option) or 1 (future)",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_side(v: i64) -> PyResult<Side> {
|
||||
match v {
|
||||
1 => Ok(Side::Long),
|
||||
-1 => Ok(Side::Short),
|
||||
_ => Err(PyValueError::new_err("side must be 1 (long) or -1 (short)")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_option_type(v: i64) -> PyResult<OptionType> {
|
||||
match v {
|
||||
1 => Ok(OptionType::Call),
|
||||
-1 => Ok(OptionType::Put),
|
||||
_ => Err(PyValueError::new_err(
|
||||
"option_type must be 1 (call) or -1 (put)",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_instrument_label(v: &str) -> PyResult<Instrument> {
|
||||
match v.to_ascii_lowercase().as_str() {
|
||||
"option" => Ok(Instrument::Option),
|
||||
"future" => Ok(Instrument::Future),
|
||||
_ => Err(PyValueError::new_err(
|
||||
"instrument must be 'option' or 'future'",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_side_label(v: &str) -> PyResult<Side> {
|
||||
match v.to_ascii_lowercase().as_str() {
|
||||
"long" => Ok(Side::Long),
|
||||
"short" => Ok(Side::Short),
|
||||
_ => Err(PyValueError::new_err("side must be 'long' or 'short'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_option_type_label(v: &str) -> PyResult<OptionType> {
|
||||
match v.to_ascii_lowercase().as_str() {
|
||||
"call" => Ok(OptionType::Call),
|
||||
"put" => Ok(OptionType::Put),
|
||||
_ => Err(PyValueError::new_err("option_type must be 'call' or 'put'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn leg_attr_string(leg: &Bound<'_, PyAny>, name: &str) -> PyResult<String> {
|
||||
let value = leg
|
||||
.getattr(name)
|
||||
.map_err(|_| PyValueError::new_err(format!("leg missing '{name}' attribute")))?;
|
||||
value.extract::<String>().map_err(|_| {
|
||||
PyValueError::new_err(format!(
|
||||
"leg field '{name}' has invalid type; expected string"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn leg_attr_f64(leg: &Bound<'_, PyAny>, name: &str) -> PyResult<f64> {
|
||||
let value = leg
|
||||
.getattr(name)
|
||||
.map_err(|_| PyValueError::new_err(format!("leg missing '{name}' attribute")))?;
|
||||
value.extract::<f64>().map_err(|_| {
|
||||
PyValueError::new_err(format!(
|
||||
"leg field '{name}' has invalid type; expected float"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn leg_attr_optional_string(leg: &Bound<'_, PyAny>, name: &str) -> PyResult<Option<String>> {
|
||||
let value = leg
|
||||
.getattr(name)
|
||||
.map_err(|_| PyValueError::new_err(format!("leg missing '{name}' attribute")))?;
|
||||
if value.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
value.extract::<String>().map(Some).map_err(|_| {
|
||||
PyValueError::new_err(format!(
|
||||
"leg field '{name}' has invalid type; expected string or None"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn leg_attr_optional_f64(leg: &Bound<'_, PyAny>, name: &str) -> PyResult<Option<f64>> {
|
||||
let value = leg
|
||||
.getattr(name)
|
||||
.map_err(|_| PyValueError::new_err(format!("leg missing '{name}' attribute")))?;
|
||||
if value.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
value.extract::<f64>().map(Some).map_err(|_| {
|
||||
PyValueError::new_err(format!(
|
||||
"leg field '{name}' has invalid type; expected float or None"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute aggregate strategy payoff over a spot grid.
|
||||
///
|
||||
/// Encoded arrays (same length = n_legs):
|
||||
/// - `instruments`: 0=option, 1=future
|
||||
/// - `sides`: 1=long, -1=short
|
||||
/// - `option_types`: 1=call, -1=put (ignored for futures)
|
||||
/// - `strikes`: strike for options, ignored for futures
|
||||
/// - `premiums`: premium for options, ignored for futures
|
||||
/// - `entry_prices`: entry price for futures, ignored for options
|
||||
/// - `quantities`, `multipliers`: applied to both instruments
|
||||
#[pyfunction]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn strategy_payoff_dense<'py>(
|
||||
py: Python<'py>,
|
||||
spot_grid: PyReadonlyArray1<'py, f64>,
|
||||
instruments: PyReadonlyArray1<'py, i64>,
|
||||
sides: PyReadonlyArray1<'py, i64>,
|
||||
option_types: PyReadonlyArray1<'py, i64>,
|
||||
strikes: PyReadonlyArray1<'py, f64>,
|
||||
premiums: PyReadonlyArray1<'py, f64>,
|
||||
entry_prices: PyReadonlyArray1<'py, f64>,
|
||||
quantities: PyReadonlyArray1<'py, f64>,
|
||||
multipliers: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let grid = spot_grid.as_slice()?;
|
||||
let inst = instruments.as_slice()?;
|
||||
let side = sides.as_slice()?;
|
||||
let opt_t = option_types.as_slice()?;
|
||||
let strike = strikes.as_slice()?;
|
||||
let premium = premiums.as_slice()?;
|
||||
let entry = entry_prices.as_slice()?;
|
||||
let qty = quantities.as_slice()?;
|
||||
let mult = multipliers.as_slice()?;
|
||||
|
||||
let n_legs = inst.len();
|
||||
if side.len() != n_legs
|
||||
|| opt_t.len() != n_legs
|
||||
|| strike.len() != n_legs
|
||||
|| premium.len() != n_legs
|
||||
|| entry.len() != n_legs
|
||||
|| qty.len() != n_legs
|
||||
|| mult.len() != n_legs
|
||||
{
|
||||
return Err(PyValueError::new_err(
|
||||
"All leg arrays must have the same length",
|
||||
));
|
||||
}
|
||||
|
||||
let mut total = vec![0.0_f64; grid.len()];
|
||||
|
||||
for leg_idx in 0..n_legs {
|
||||
let instrument = parse_instrument(inst[leg_idx])?;
|
||||
let side_sign = parse_side(side[leg_idx])?.sign();
|
||||
let leg_scale = side_sign * qty[leg_idx] * mult[leg_idx];
|
||||
|
||||
match instrument {
|
||||
Instrument::Option => {
|
||||
let otype = parse_option_type(opt_t[leg_idx])?;
|
||||
let k = strike[leg_idx];
|
||||
let p = premium[leg_idx];
|
||||
for (i, &s) in grid.iter().enumerate() {
|
||||
let intrinsic = match otype {
|
||||
OptionType::Call => (s - k).max(0.0),
|
||||
OptionType::Put => (k - s).max(0.0),
|
||||
};
|
||||
total[i] += leg_scale * (intrinsic - p);
|
||||
}
|
||||
}
|
||||
Instrument::Future => {
|
||||
let e = entry[leg_idx];
|
||||
for (i, &s) in grid.iter().enumerate() {
|
||||
total[i] += leg_scale * (s - e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Compute aggregate strategy payoff from Python leg objects.
|
||||
///
|
||||
/// `legs` is expected to be a sequence of `PayoffLeg`-like objects
|
||||
/// with attributes used by `ferro_ta.analysis.derivatives_payoff`.
|
||||
#[pyfunction]
|
||||
pub fn strategy_payoff_legs<'py>(
|
||||
py: Python<'py>,
|
||||
spot_grid: PyReadonlyArray1<'py, f64>,
|
||||
legs: Bound<'py, PyTuple>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let grid = spot_grid.as_slice()?;
|
||||
let mut total = vec![0.0_f64; grid.len()];
|
||||
|
||||
for leg in legs.iter() {
|
||||
let instrument = parse_instrument_label(&leg_attr_string(&leg, "instrument")?)?;
|
||||
let side_sign = parse_side_label(&leg_attr_string(&leg, "side")?)?.sign();
|
||||
let quantity = leg_attr_f64(&leg, "quantity")?;
|
||||
let multiplier = leg_attr_f64(&leg, "multiplier")?;
|
||||
let leg_scale = side_sign * quantity * multiplier;
|
||||
|
||||
match instrument {
|
||||
Instrument::Option => {
|
||||
let otype_raw =
|
||||
leg_attr_optional_string(&leg, "option_type")?.ok_or_else(|| {
|
||||
PyValueError::new_err("Option payoff legs require option_type.")
|
||||
})?;
|
||||
let otype = parse_option_type_label(&otype_raw)?;
|
||||
let strike = leg_attr_optional_f64(&leg, "strike")?
|
||||
.ok_or_else(|| PyValueError::new_err("Option payoff legs require strike."))?;
|
||||
let premium = leg_attr_f64(&leg, "premium")?;
|
||||
|
||||
for (i, &s) in grid.iter().enumerate() {
|
||||
let intrinsic = match otype {
|
||||
OptionType::Call => (s - strike).max(0.0),
|
||||
OptionType::Put => (strike - s).max(0.0),
|
||||
};
|
||||
total[i] += leg_scale * (intrinsic - premium);
|
||||
}
|
||||
}
|
||||
Instrument::Future => {
|
||||
let entry_price = leg_attr_optional_f64(&leg, "entry_price")?.ok_or_else(|| {
|
||||
PyValueError::new_err("Futures payoff legs require entry_price.")
|
||||
})?;
|
||||
for (i, &s) in grid.iter().enumerate() {
|
||||
total[i] += leg_scale * (s - entry_price);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total.into_pyarray(py))
|
||||
}
|
||||
|
||||
/// Aggregate Greeks over multiple legs.
|
||||
///
|
||||
/// Encodings match `strategy_payoff_dense`.
|
||||
#[pyfunction]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn aggregate_greeks_dense(
|
||||
spot: f64,
|
||||
instruments: PyReadonlyArray1<'_, i64>,
|
||||
sides: PyReadonlyArray1<'_, i64>,
|
||||
option_types: PyReadonlyArray1<'_, i64>,
|
||||
strikes: PyReadonlyArray1<'_, f64>,
|
||||
volatilities: PyReadonlyArray1<'_, f64>,
|
||||
time_to_expiries: PyReadonlyArray1<'_, f64>,
|
||||
rates: PyReadonlyArray1<'_, f64>,
|
||||
carries: PyReadonlyArray1<'_, f64>,
|
||||
quantities: PyReadonlyArray1<'_, f64>,
|
||||
multipliers: PyReadonlyArray1<'_, f64>,
|
||||
) -> PyResult<(f64, f64, f64, f64, f64)> {
|
||||
let inst = instruments.as_slice()?;
|
||||
let side = sides.as_slice()?;
|
||||
let opt_t = option_types.as_slice()?;
|
||||
let strike = strikes.as_slice()?;
|
||||
let vol = volatilities.as_slice()?;
|
||||
let tte = time_to_expiries.as_slice()?;
|
||||
let rate = rates.as_slice()?;
|
||||
let carry = carries.as_slice()?;
|
||||
let qty = quantities.as_slice()?;
|
||||
let mult = multipliers.as_slice()?;
|
||||
|
||||
let n_legs = inst.len();
|
||||
if side.len() != n_legs
|
||||
|| opt_t.len() != n_legs
|
||||
|| strike.len() != n_legs
|
||||
|| vol.len() != n_legs
|
||||
|| tte.len() != n_legs
|
||||
|| rate.len() != n_legs
|
||||
|| carry.len() != n_legs
|
||||
|| qty.len() != n_legs
|
||||
|| mult.len() != n_legs
|
||||
{
|
||||
return Err(PyValueError::new_err(
|
||||
"All leg arrays must have the same length",
|
||||
));
|
||||
}
|
||||
|
||||
let mut delta = 0.0_f64;
|
||||
let mut gamma = 0.0_f64;
|
||||
let mut vega = 0.0_f64;
|
||||
let mut theta = 0.0_f64;
|
||||
let mut rho = 0.0_f64;
|
||||
|
||||
for i in 0..n_legs {
|
||||
let instrument = parse_instrument(inst[i])?;
|
||||
let side_sign = parse_side(side[i])?.sign();
|
||||
let leg_scale = side_sign * qty[i] * mult[i];
|
||||
match instrument {
|
||||
Instrument::Future => {
|
||||
delta += leg_scale;
|
||||
}
|
||||
Instrument::Option => {
|
||||
if vol[i].is_nan() || tte[i].is_nan() {
|
||||
return Err(PyValueError::new_err(
|
||||
"Option legs require strike, volatility, and time_to_expiry for Greeks aggregation.",
|
||||
));
|
||||
}
|
||||
let kind = match parse_option_type(opt_t[i])? {
|
||||
OptionType::Call => ferro_ta_core::options::OptionKind::Call,
|
||||
OptionType::Put => ferro_ta_core::options::OptionKind::Put,
|
||||
};
|
||||
let greeks = ferro_ta_core::options::greeks::model_greeks(
|
||||
ferro_ta_core::options::OptionEvaluation {
|
||||
contract: ferro_ta_core::options::OptionContract {
|
||||
model: ferro_ta_core::options::PricingModel::BlackScholes,
|
||||
underlying: spot,
|
||||
strike: strike[i],
|
||||
rate: rate[i],
|
||||
carry: carry[i],
|
||||
time_to_expiry: tte[i],
|
||||
kind,
|
||||
},
|
||||
volatility: vol[i],
|
||||
},
|
||||
);
|
||||
delta += leg_scale * greeks.delta;
|
||||
gamma += leg_scale * greeks.gamma;
|
||||
vega += leg_scale * greeks.vega;
|
||||
theta += leg_scale * greeks.theta;
|
||||
rho += leg_scale * greeks.rho;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((delta, gamma, vega, theta, rho))
|
||||
}
|
||||
|
||||
/// Aggregate Greeks from Python leg objects.
|
||||
#[pyfunction]
|
||||
pub fn aggregate_greeks_legs(
|
||||
spot: f64,
|
||||
legs: Bound<'_, PyTuple>,
|
||||
) -> PyResult<(f64, f64, f64, f64, f64)> {
|
||||
let mut delta = 0.0_f64;
|
||||
let mut gamma = 0.0_f64;
|
||||
let mut vega = 0.0_f64;
|
||||
let mut theta = 0.0_f64;
|
||||
let mut rho = 0.0_f64;
|
||||
|
||||
for leg in legs.iter() {
|
||||
let instrument = parse_instrument_label(&leg_attr_string(&leg, "instrument")?)?;
|
||||
let side_sign = parse_side_label(&leg_attr_string(&leg, "side")?)?.sign();
|
||||
let quantity = leg_attr_f64(&leg, "quantity")?;
|
||||
let multiplier = leg_attr_f64(&leg, "multiplier")?;
|
||||
let leg_scale = side_sign * quantity * multiplier;
|
||||
|
||||
match instrument {
|
||||
Instrument::Future => {
|
||||
delta += leg_scale;
|
||||
}
|
||||
Instrument::Option => {
|
||||
let otype_raw =
|
||||
leg_attr_optional_string(&leg, "option_type")?.ok_or_else(|| {
|
||||
PyValueError::new_err(
|
||||
"Option legs require option_type for Greeks aggregation.",
|
||||
)
|
||||
})?;
|
||||
let otype = parse_option_type_label(&otype_raw)?;
|
||||
let strike = leg_attr_optional_f64(&leg, "strike")?.ok_or_else(|| {
|
||||
PyValueError::new_err(
|
||||
"Option legs require strike, volatility, and time_to_expiry for Greeks aggregation.",
|
||||
)
|
||||
})?;
|
||||
let volatility = leg_attr_optional_f64(&leg, "volatility")?.ok_or_else(|| {
|
||||
PyValueError::new_err(
|
||||
"Option legs require strike, volatility, and time_to_expiry for Greeks aggregation.",
|
||||
)
|
||||
})?;
|
||||
let time_to_expiry =
|
||||
leg_attr_optional_f64(&leg, "time_to_expiry")?.ok_or_else(|| {
|
||||
PyValueError::new_err(
|
||||
"Option legs require strike, volatility, and time_to_expiry for Greeks aggregation.",
|
||||
)
|
||||
})?;
|
||||
let rate = leg_attr_f64(&leg, "rate")?;
|
||||
let carry = leg_attr_f64(&leg, "carry")?;
|
||||
|
||||
let kind = match otype {
|
||||
OptionType::Call => ferro_ta_core::options::OptionKind::Call,
|
||||
OptionType::Put => ferro_ta_core::options::OptionKind::Put,
|
||||
};
|
||||
let greeks = ferro_ta_core::options::greeks::model_greeks(
|
||||
ferro_ta_core::options::OptionEvaluation {
|
||||
contract: ferro_ta_core::options::OptionContract {
|
||||
model: ferro_ta_core::options::PricingModel::BlackScholes,
|
||||
underlying: spot,
|
||||
strike,
|
||||
rate,
|
||||
carry,
|
||||
time_to_expiry,
|
||||
kind,
|
||||
},
|
||||
volatility,
|
||||
},
|
||||
);
|
||||
delta += leg_scale * greeks.delta;
|
||||
gamma += leg_scale * greeks.gamma;
|
||||
vega += leg_scale * greeks.vega;
|
||||
theta += leg_scale * greeks.theta;
|
||||
rho += leg_scale * greeks.rho;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((delta, gamma, vega, theta, rho))
|
||||
}
|
||||
@@ -350,6 +350,35 @@ pub fn spread<'py>(
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ratio
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compute the ratio between two series: A / B.
|
||||
///
|
||||
/// Where B is 0, returns NaN.
|
||||
#[pyfunction]
|
||||
pub fn ratio<'py>(
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let av = a.as_slice()?;
|
||||
let bv = b.as_slice()?;
|
||||
let n = av.len();
|
||||
if n == 0 || bv.len() != n {
|
||||
return Err(PyValueError::new_err(
|
||||
"a and b must be non-empty and equal length",
|
||||
));
|
||||
}
|
||||
let result: Vec<f64> = av
|
||||
.iter()
|
||||
.zip(bv.iter())
|
||||
.map(|(&x, &y)| if y == 0.0 { f64::NAN } else { x / y })
|
||||
.collect();
|
||||
Ok(result.into_pyarray(py))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// zscore_series
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -449,6 +478,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(correlation_matrix, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(relative_strength, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(spread, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(ratio, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(zscore_series, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(compose_weighted, m)?)?;
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPTS = ROOT / "scripts"
|
||||
if str(ROOT / "python") not in sys.path:
|
||||
sys.path.insert(0, str(ROOT / "python"))
|
||||
if str(SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
from build_api_manifest import build_manifest
|
||||
|
||||
|
||||
def test_api_manifest_is_deterministic_and_current() -> None:
|
||||
manifest_path = ROOT / "docs" / "api_manifest.json"
|
||||
assert manifest_path.exists(), "docs/api_manifest.json is missing"
|
||||
|
||||
expected = build_manifest(ROOT, include_runtime_metadata=False)
|
||||
actual = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert actual == expected
|
||||
@@ -233,7 +233,9 @@ def test_methods_returns_public_callables():
|
||||
result = ferro_ta.methods()
|
||||
assert isinstance(result, list)
|
||||
assert any(d["name"] == "SMA" and d["category"] == "top_level" for d in result)
|
||||
assert any(d["name"] == "option_price" and d["category"] == "options" for d in result)
|
||||
assert any(
|
||||
d["name"] == "option_price" and d["category"] == "options" for d in result
|
||||
)
|
||||
|
||||
|
||||
def test_about_reports_version_and_counts():
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import ferro_ta
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
WASM_DIR = ROOT / "wasm"
|
||||
PKG_JS = WASM_DIR / "pkg" / "ferro_ta_wasm.js"
|
||||
SCRIPT = WASM_DIR / "conformance_node.js"
|
||||
|
||||
|
||||
def _write_node_conformance_script(path: Path) -> None:
|
||||
path.write_text(
|
||||
"""
|
||||
const wasm = require("./pkg/ferro_ta_wasm.js");
|
||||
|
||||
function toArray(x) {
|
||||
return Array.from(x, (v) => (Number.isNaN(v) ? null : Number(v)));
|
||||
}
|
||||
|
||||
const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.1, 45.42, 45.84, 46.08, 45.89, 46.03, 46.21, 46.02, 45.78]);
|
||||
const high = new Float64Array([44.71, 44.5, 44.6, 44.09, 44.79, 45.2, 45.44, 45.73, 46.01, 46.44, 46.21, 46.39, 46.53, 46.3, 46.12]);
|
||||
const low = new Float64Array([43.9, 43.8, 43.9, 43.2, 43.9, 44.2, 44.6, 44.8, 45.2, 45.5, 45.4, 45.5, 45.7, 45.6, 45.4]);
|
||||
const volume = new Float64Array([1200, 1320, 1250, 1460, 1500, 1670, 1720, 1810, 1900, 2020, 1980, 2100, 2170, 2140, 2080]);
|
||||
|
||||
const payload = {
|
||||
sma: toArray(wasm.sma(close, 5)),
|
||||
ema: toArray(wasm.ema(close, 5)),
|
||||
wma: toArray(wasm.wma(close, 5)),
|
||||
rsi: toArray(wasm.rsi(close, 5)),
|
||||
adx: toArray(wasm.adx(high, low, close, 5)),
|
||||
mfi: toArray(wasm.mfi(high, low, close, volume, 5)),
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(payload));
|
||||
""".strip()
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _run_node_conformance() -> dict[str, list[float | None]]:
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node is required for wasm/node conformance test")
|
||||
if not PKG_JS.exists():
|
||||
pytest.skip(
|
||||
"wasm/pkg not found; run `wasm-pack build --target nodejs --out-dir pkg`"
|
||||
)
|
||||
|
||||
_write_node_conformance_script(SCRIPT)
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["node", str(SCRIPT)],
|
||||
cwd=WASM_DIR,
|
||||
text=True,
|
||||
)
|
||||
finally:
|
||||
if SCRIPT.exists():
|
||||
SCRIPT.unlink()
|
||||
return json.loads(out)
|
||||
|
||||
|
||||
def _to_jsonable(arr: np.ndarray) -> list[float | None]:
|
||||
vals = np.asarray(arr, dtype=np.float64)
|
||||
return [None if np.isnan(x) else float(x) for x in vals]
|
||||
|
||||
|
||||
def _assert_close_with_null_nan(
|
||||
actual: list[float | None],
|
||||
expected: list[float | None],
|
||||
*,
|
||||
atol: float,
|
||||
) -> None:
|
||||
assert len(actual) == len(expected)
|
||||
a = np.array([np.nan if v is None else float(v) for v in actual], dtype=np.float64)
|
||||
e = np.array(
|
||||
[np.nan if v is None else float(v) for v in expected], dtype=np.float64
|
||||
)
|
||||
np.testing.assert_allclose(a, e, atol=atol, rtol=0.0, equal_nan=True)
|
||||
|
||||
|
||||
def test_wasm_node_matches_python_core_indicators() -> None:
|
||||
close = np.array(
|
||||
[
|
||||
44.34,
|
||||
44.09,
|
||||
44.15,
|
||||
43.61,
|
||||
44.33,
|
||||
44.83,
|
||||
45.10,
|
||||
45.42,
|
||||
45.84,
|
||||
46.08,
|
||||
45.89,
|
||||
46.03,
|
||||
46.21,
|
||||
46.02,
|
||||
45.78,
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
high = np.array(
|
||||
[
|
||||
44.71,
|
||||
44.50,
|
||||
44.60,
|
||||
44.09,
|
||||
44.79,
|
||||
45.20,
|
||||
45.44,
|
||||
45.73,
|
||||
46.01,
|
||||
46.44,
|
||||
46.21,
|
||||
46.39,
|
||||
46.53,
|
||||
46.30,
|
||||
46.12,
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
low = np.array(
|
||||
[
|
||||
43.90,
|
||||
43.80,
|
||||
43.90,
|
||||
43.20,
|
||||
43.90,
|
||||
44.20,
|
||||
44.60,
|
||||
44.80,
|
||||
45.20,
|
||||
45.50,
|
||||
45.40,
|
||||
45.50,
|
||||
45.70,
|
||||
45.60,
|
||||
45.40,
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
volume = np.array(
|
||||
[
|
||||
1200.0,
|
||||
1320.0,
|
||||
1250.0,
|
||||
1460.0,
|
||||
1500.0,
|
||||
1670.0,
|
||||
1720.0,
|
||||
1810.0,
|
||||
1900.0,
|
||||
2020.0,
|
||||
1980.0,
|
||||
2100.0,
|
||||
2170.0,
|
||||
2140.0,
|
||||
2080.0,
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
node_payload = _run_node_conformance()
|
||||
|
||||
py_expected = {
|
||||
"sma": _to_jsonable(ferro_ta.SMA(close, 5)),
|
||||
"ema": _to_jsonable(ferro_ta.EMA(close, 5)),
|
||||
"wma": _to_jsonable(ferro_ta.WMA(close, 5)),
|
||||
"rsi": _to_jsonable(ferro_ta.RSI(close, 5)),
|
||||
"adx": _to_jsonable(ferro_ta.ADX(high, low, close, 5)),
|
||||
"mfi": _to_jsonable(ferro_ta.MFI(high, low, close, volume, 5)),
|
||||
}
|
||||
|
||||
for name, expected in py_expected.items():
|
||||
assert name in node_payload
|
||||
_assert_close_with_null_nan(node_payload[name], expected, atol=1e-9)
|
||||
@@ -1,3 +1,7 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
@@ -220,19 +224,30 @@ class TestStrategyAndPayoff:
|
||||
|
||||
class TestDerivativesBenchmarking:
|
||||
def test_derivatives_benchmark_smoke(self, tmp_path):
|
||||
from benchmarks.bench_derivatives_compare import run_benchmark
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
script = root / "benchmarks" / "bench_derivatives_compare.py"
|
||||
output_path = tmp_path / "derivatives_benchmark.json"
|
||||
result = run_benchmark(
|
||||
sizes=[32],
|
||||
accuracy_size=16,
|
||||
json_path=str(output_path),
|
||||
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script),
|
||||
"--sizes",
|
||||
"32",
|
||||
"--accuracy-size",
|
||||
"16",
|
||||
"--json",
|
||||
str(output_path),
|
||||
],
|
||||
cwd=root,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert completed.returncode == 0, completed.stdout + completed.stderr
|
||||
assert output_path.is_file()
|
||||
assert result["accuracy"]["results"]
|
||||
assert result["speed"]["results"]
|
||||
assert any(
|
||||
row["provider"] == "ferro_ta" for row in result["accuracy"]["results"]
|
||||
)
|
||||
assert any(row["provider"] == "ferro_ta" for row in result["speed"]["results"])
|
||||
payload = output_path.read_text(encoding="utf-8")
|
||||
assert '"accuracy"' in payload
|
||||
assert '"speed"' in payload
|
||||
assert '"provider": "ferro_ta"' in payload
|
||||
|
||||
@@ -626,6 +626,13 @@ class TestBatchApply:
|
||||
with pytest.raises(ValueError, match="1-D or 2-D"):
|
||||
batch_apply(np.zeros((5, 5, 5)), SMA, timeperiod=3)
|
||||
|
||||
def test_sma_fastpath_matches_batch_sma(self):
|
||||
from ferro_ta.data.batch import batch_sma
|
||||
|
||||
fast = batch_apply(self.C2D, SMA, timeperiod=10)
|
||||
direct = batch_sma(self.C2D, timeperiod=10)
|
||||
assert np.allclose(fast, direct, equal_nan=True)
|
||||
|
||||
|
||||
class TestBatchShapeValidation:
|
||||
def test_batch_atr_shape_mismatch_raises(self):
|
||||
|
||||
@@ -4,6 +4,9 @@ regime detection, performance attribution, and dashboard helpers.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import runpy
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
@@ -1218,7 +1221,22 @@ class TestMCPListTools:
|
||||
|
||||
result = handle_list_tools()
|
||||
names = [t["name"] for t in result["tools"]]
|
||||
for expected in ("sma", "ema", "rsi", "macd", "backtest", "list_indicators"):
|
||||
assert len(names) > 250
|
||||
for expected in (
|
||||
"sma",
|
||||
"ema",
|
||||
"rsi",
|
||||
"macd",
|
||||
"backtest",
|
||||
"SMA",
|
||||
"compute_indicator",
|
||||
"about",
|
||||
"check_cross",
|
||||
"TickAggregator",
|
||||
"call_instance_method",
|
||||
"call_stored_callable",
|
||||
"delete_instance",
|
||||
):
|
||||
assert expected in names, f"Expected tool '{expected}' not found"
|
||||
|
||||
def test_each_tool_has_schema(self):
|
||||
@@ -1280,6 +1298,57 @@ class TestMCPCallTool:
|
||||
assert "final_equity" in payload
|
||||
assert "n_trades" in payload
|
||||
|
||||
def test_top_level_sma_call(self):
|
||||
import json
|
||||
|
||||
from ferro_ta.mcp import handle_call_tool
|
||||
|
||||
close = list(np.linspace(100, 110, 30))
|
||||
result = handle_call_tool("SMA", {"close": close, "timeperiod": 5})
|
||||
payload = json.loads(result["content"][0]["text"])
|
||||
assert len(payload) == 30
|
||||
|
||||
def test_compute_indicator_call(self):
|
||||
import json
|
||||
|
||||
from ferro_ta.mcp import handle_call_tool
|
||||
|
||||
close = list(_make_close(100))
|
||||
result = handle_call_tool(
|
||||
"compute_indicator",
|
||||
{
|
||||
"name": "MACD",
|
||||
"args": [close],
|
||||
},
|
||||
)
|
||||
payload = json.loads(result["content"][0]["text"])
|
||||
assert "macd" in payload
|
||||
|
||||
def test_about_call(self):
|
||||
import json
|
||||
|
||||
from ferro_ta.mcp import handle_call_tool
|
||||
|
||||
result = handle_call_tool("about", {})
|
||||
payload = json.loads(result["content"][0]["text"])
|
||||
assert payload["indicator_count"] >= 200
|
||||
assert payload["method_count"] >= 400
|
||||
|
||||
def test_check_cross_call(self):
|
||||
import json
|
||||
|
||||
from ferro_ta.mcp import handle_call_tool
|
||||
|
||||
result = handle_call_tool(
|
||||
"check_cross",
|
||||
{
|
||||
"fast": [1.0, 2.0, 3.0, 2.0, 1.0],
|
||||
"slow": [2.0, 2.0, 2.0, 2.0, 2.0],
|
||||
},
|
||||
)
|
||||
payload = json.loads(result["content"][0]["text"])
|
||||
assert len(payload) == 5
|
||||
|
||||
def test_list_indicators_call(self):
|
||||
import json
|
||||
|
||||
@@ -1322,3 +1391,129 @@ class TestMCPCallTool:
|
||||
"backtest", {"close": close, "strategy": "no_strategy"}
|
||||
)
|
||||
assert result.get("isError") is True or "content" in result
|
||||
|
||||
def test_tick_aggregator_instance_lifecycle(self):
|
||||
import json
|
||||
|
||||
from ferro_ta.mcp import handle_call_tool
|
||||
|
||||
created = json.loads(
|
||||
handle_call_tool("TickAggregator", {"rule": "tick:2"})["content"][0]["text"]
|
||||
)
|
||||
instance_id = created["instance_id"]
|
||||
|
||||
described = json.loads(
|
||||
handle_call_tool("describe_instance", {"instance_id": instance_id})[
|
||||
"content"
|
||||
][0]["text"]
|
||||
)
|
||||
method_names = [item["name"] for item in described["methods"]]
|
||||
assert "aggregate" in method_names
|
||||
|
||||
aggregated = json.loads(
|
||||
handle_call_tool(
|
||||
"call_instance_method",
|
||||
{
|
||||
"instance_id": instance_id,
|
||||
"method": "aggregate",
|
||||
"args": [
|
||||
{
|
||||
"price": [1.0, 2.0, 3.0, 4.0],
|
||||
"size": [1.0, 1.0, 1.0, 1.0],
|
||||
}
|
||||
],
|
||||
},
|
||||
)["content"][0]["text"]
|
||||
)
|
||||
assert "open" in aggregated
|
||||
assert "close" in aggregated
|
||||
|
||||
deleted = json.loads(
|
||||
handle_call_tool("delete_instance", {"instance_id": instance_id})[
|
||||
"content"
|
||||
][0]["text"]
|
||||
)
|
||||
assert deleted["deleted"] is True
|
||||
|
||||
def test_stored_callable_can_be_invoked(self):
|
||||
import json
|
||||
|
||||
from ferro_ta.mcp import handle_call_tool
|
||||
|
||||
wrapped = json.loads(
|
||||
handle_call_tool("traced", {"func": {"callable": "SMA"}})["content"][0][
|
||||
"text"
|
||||
]
|
||||
)
|
||||
instance_id = wrapped["instance_id"]
|
||||
|
||||
called = json.loads(
|
||||
handle_call_tool(
|
||||
"call_stored_callable",
|
||||
{
|
||||
"instance_id": instance_id,
|
||||
"args": [[1.0, 2.0, 3.0, 4.0, 5.0]],
|
||||
"kwargs": {"timeperiod": 3},
|
||||
},
|
||||
)["content"][0]["text"]
|
||||
)
|
||||
assert len(called) == 5
|
||||
|
||||
handle_call_tool("delete_instance", {"instance_id": instance_id})
|
||||
|
||||
def test_benchmark_accepts_callable_reference(self):
|
||||
import json
|
||||
|
||||
from ferro_ta.mcp import handle_call_tool
|
||||
|
||||
result = handle_call_tool(
|
||||
"benchmark",
|
||||
{
|
||||
"func": {"callable": "SMA"},
|
||||
"args": [[1.0, 2.0, 3.0, 4.0, 5.0]],
|
||||
"kwargs": {"timeperiod": 3},
|
||||
"n": 2,
|
||||
"warmup": 0,
|
||||
},
|
||||
)
|
||||
payload = json.loads(result["content"][0]["text"])
|
||||
assert payload["n"] == 2.0
|
||||
assert "mean_ms" in payload
|
||||
|
||||
|
||||
class TestMCPServer:
|
||||
def test_create_server_requires_mcp_dependency(self, monkeypatch):
|
||||
import ferro_ta.mcp as mcp_mod
|
||||
|
||||
real_import_module = importlib.import_module
|
||||
|
||||
def fake_import_module(name, package=None):
|
||||
if name.startswith("mcp"):
|
||||
raise ImportError("No module named 'mcp'")
|
||||
return real_import_module(name, package)
|
||||
|
||||
mcp_mod.create_server.cache_clear()
|
||||
monkeypatch.setattr(importlib, "import_module", fake_import_module)
|
||||
|
||||
with pytest.raises(RuntimeError, match='pip install "ferro-ta\\[mcp\\]"'):
|
||||
mcp_mod.create_server()
|
||||
|
||||
def test_main_entrypoint_invokes_run_server(self, monkeypatch):
|
||||
import ferro_ta.mcp as mcp_mod
|
||||
|
||||
calls: list[str] = []
|
||||
|
||||
monkeypatch.setattr(mcp_mod, "run_server", lambda: calls.append("called"))
|
||||
runpy.run_module("ferro_ta.mcp.__main__", run_name="__main__")
|
||||
|
||||
assert calls == ["called"]
|
||||
|
||||
def test_create_server_registers_generated_tools(self):
|
||||
import ferro_ta.mcp as mcp_mod
|
||||
|
||||
server = mcp_mod.create_server()
|
||||
tool_names = [tool.name for tool in server._tool_manager.list_tools()]
|
||||
|
||||
assert "SMA" in tool_names
|
||||
assert "TickAggregator" in tool_names
|
||||
assert "call_instance_method" in tool_names
|
||||
|
||||
@@ -180,6 +180,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.5"
|
||||
@@ -515,19 +524,24 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cuda-bindings"
|
||||
version = "12.9.4"
|
||||
version = "13.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-pathfinder", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ef/184aa775e970fc089942cd9ec6302e6e44679d4c14549c6a7ea45bf7f798/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6", size = 6329075, upload-time = "2026-03-11T00:12:32.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -538,6 +552,49 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/02/59a5bc738a09def0b49aea0e460bdf97f65206d0d041246147cf6207e69c/cuda_pathfinder-1.4.1-py3-none-any.whl", hash = "sha256:40793006082de88e0950753655e55558a446bed9a7d9d0bcb48b2506d50ed82a", size = 43903, upload-time = "2026-03-06T21:05:24.372Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cuda-toolkit"
|
||||
version = "13.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
cublas = [
|
||||
{ name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
cudart = [
|
||||
{ name = "nvidia-cuda-runtime", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
cufft = [
|
||||
{ name = "nvidia-cufft", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
cufile = [
|
||||
{ name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
|
||||
]
|
||||
cupti = [
|
||||
{ name = "nvidia-cuda-cupti", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
curand = [
|
||||
{ name = "nvidia-curand", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
cusolver = [
|
||||
{ name = "nvidia-cusolver", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
cusparse = [
|
||||
{ name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
nvjitlink = [
|
||||
{ name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
nvrtc = [
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
nvtx = [
|
||||
{ name = "nvidia-nvtx", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cycler"
|
||||
version = "0.12.1"
|
||||
@@ -547,6 +604,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.21.2"
|
||||
@@ -593,7 +659,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.135.1"
|
||||
version = "0.135.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -602,14 +668,14 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ferro-ta"
|
||||
version = "1.0.3"
|
||||
version = "1.0.6"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
@@ -645,6 +711,7 @@ dev = [
|
||||
{ name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.14'" },
|
||||
{ name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "polars" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pyyaml" },
|
||||
@@ -683,6 +750,7 @@ dev = [
|
||||
{ name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "pandas-ta", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "polars" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pyyaml" },
|
||||
@@ -709,6 +777,7 @@ requires-dist = [
|
||||
{ name = "polars", marker = "extra == 'all'", specifier = ">=0.19" },
|
||||
{ name = "polars", marker = "extra == 'dev'", specifier = ">=0.19" },
|
||||
{ name = "polars", marker = "extra == 'polars'", specifier = ">=0.19" },
|
||||
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.0" },
|
||||
{ name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1" },
|
||||
{ name = "pytest", marker = "extra == 'all'", specifier = ">=7.0" },
|
||||
{ name = "pytest", marker = "extra == 'benchmark'", specifier = ">=7.0" },
|
||||
@@ -735,6 +804,7 @@ dev = [
|
||||
{ name = "pandas", specifier = ">=1.0" },
|
||||
{ name = "pandas-ta", marker = "python_full_version >= '3.12'", specifier = ">=0.3" },
|
||||
{ name = "polars", specifier = ">=0.19" },
|
||||
{ name = "pre-commit", specifier = ">=3.0" },
|
||||
{ name = "pyright", specifier = ">=1.1" },
|
||||
{ name = "pytest", specifier = ">=7.0" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
@@ -876,6 +946,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f7/5cc291d701094754a1d327b44d80a44971e13962881d9a400235726171da/hypothesis-6.151.9-py3-none-any.whl", hash = "sha256:7b7220585c67759b1b1ef839b1e6e9e3d82ed468cfc1ece43c67184848d7edd9", size = 529307, upload-time = "2026-02-16T22:59:20.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
@@ -1583,137 +1662,152 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cublas-cu12"
|
||||
version = "12.8.4.1"
|
||||
name = "nvidia-cublas"
|
||||
version = "13.1.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-cupti-cu12"
|
||||
version = "12.8.90"
|
||||
name = "nvidia-cuda-cupti"
|
||||
version = "13.0.85"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-nvrtc-cu12"
|
||||
version = "12.8.93"
|
||||
name = "nvidia-cuda-nvrtc"
|
||||
version = "13.0.88"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-runtime-cu12"
|
||||
version = "12.8.90"
|
||||
name = "nvidia-cuda-runtime"
|
||||
version = "13.0.96"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cudnn-cu12"
|
||||
version = "9.10.2.21"
|
||||
name = "nvidia-cudnn-cu13"
|
||||
version = "9.19.0.56"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
{ name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cufft-cu12"
|
||||
version = "11.3.3.83"
|
||||
name = "nvidia-cufft"
|
||||
version = "12.0.0.61"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
{ name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cufile-cu12"
|
||||
version = "1.13.1.3"
|
||||
name = "nvidia-cufile"
|
||||
version = "1.15.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-curand-cu12"
|
||||
version = "10.3.9.90"
|
||||
name = "nvidia-curand"
|
||||
version = "10.4.0.35"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cusolver-cu12"
|
||||
version = "11.7.3.90"
|
||||
name = "nvidia-cusolver"
|
||||
version = "12.0.4.66"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
{ name = "nvidia-cusparse-cu12", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
{ name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
{ name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
{ name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cusparse-cu12"
|
||||
version = "12.5.8.93"
|
||||
name = "nvidia-cusparse"
|
||||
version = "12.6.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
{ name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cusparselt-cu12"
|
||||
version = "0.7.1"
|
||||
name = "nvidia-cusparselt-cu13"
|
||||
version = "0.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nccl-cu12"
|
||||
version = "2.27.5"
|
||||
name = "nvidia-nccl-cu13"
|
||||
version = "2.28.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nvjitlink-cu12"
|
||||
version = "12.8.93"
|
||||
name = "nvidia-nvjitlink"
|
||||
version = "13.0.88"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nvshmem-cu12"
|
||||
name = "nvidia-nvshmem-cu13"
|
||||
version = "3.4.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nvtx-cu12"
|
||||
version = "12.8.90"
|
||||
name = "nvidia-nvtx"
|
||||
version = "13.0.85"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1983,6 +2077,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
@@ -1994,30 +2097,46 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "polars"
|
||||
version = "1.38.1"
|
||||
version = "1.39.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "polars-runtime-32" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/5e/208a24471a433bcd0e9a6889ac49025fd4daad2815c8220c5bd2576e5f1b/polars-1.38.1.tar.gz", hash = "sha256:803a2be5344ef880ad625addfb8f641995cfd777413b08a10de0897345778239", size = 717667, upload-time = "2026-02-06T18:13:23.013Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/ab/f19e592fce9e000da49c96bf35e77cef67f9cb4b040bfa538a2764c0263e/polars-1.39.3.tar.gz", hash = "sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c", size = 728987, upload-time = "2026-03-20T11:16:24.836Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl", hash = "sha256:a29479c48fed4984d88b656486d221f638cba45d3e961631a50ee5fdde38cb2c", size = 810368, upload-time = "2026-02-06T18:11:55.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/db/08f4ca10c5018813e7e0b59e4472302328b3d2ab1512f5a2157a814540e0/polars-1.39.3-py3-none-any.whl", hash = "sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56", size = 823985, upload-time = "2026-03-20T11:14:23.619Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polars-runtime-32"
|
||||
version = "1.38.1"
|
||||
version = "1.39.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/4b/04d6b3fb7cf336fbe12fbc4b43f36d1783e11bb0f2b1e3980ec44878df06/polars_runtime_32-1.38.1.tar.gz", hash = "sha256:04f20ed1f5c58771f34296a27029dc755a9e4b1390caeaef8f317e06fdfce2ec", size = 2812631, upload-time = "2026-02-06T18:13:25.206Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/39/c8688696bc22b6c501e3b82ef3be10e543c07a785af5660f30997cd22dd2/polars_runtime_32-1.39.3.tar.gz", hash = "sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d", size = 2872335, upload-time = "2026-03-20T11:16:26.581Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/a2/a00defbddadd8cf1042f52380dcba6b6592b03bac8e3b34c436b62d12d3b/polars_runtime_32-1.38.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:18154e96044724a0ac38ce155cf63aa03c02dd70500efbbf1a61b08cadd269ef", size = 44108001, upload-time = "2026-02-06T18:11:58.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/fb/599ff3709e6a303024efd7edfd08cf8de55c6ac39527d8f41cbc4399385f/polars_runtime_32-1.38.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c49acac34cc4049ed188f1eb67d6ff3971a39b4af7f7b734b367119970f313ac", size = 40230140, upload-time = "2026-02-06T18:12:01.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/8c/3ac18d6f89dc05fe2c7c0ee1dc5b81f77a5c85ad59898232c2500fe2ebbf/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fef2ef2626a954e010e006cc8e4de467ecf32d08008f130cea1c78911f545323", size = 41994039, upload-time = "2026-02-06T18:12:04.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/5a/61d60ec5cc0ab37cbd5a699edb2f9af2875b7fdfdfb2a4608ca3cc5f0448/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5f7a8125e2d50e2e060296551c929aec09be23a9edcb2b12ca923f555a5ba", size = 45755804, upload-time = "2026-02-06T18:12:07.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/54/02cd4074c98c361ccd3fec3bcb0bd68dbc639c0550c42a4436b0ff0f3ccf/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:10d19cd9863e129273b18b7fcaab625b5c8143c2d22b3e549067b78efa32e4fa", size = 42159605, upload-time = "2026-02-06T18:12:10.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/f3/b2a5e720cc56eaa38b4518e63aa577b4bbd60e8b05a00fe43ca051be5879/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61e8d73c614b46a00d2f853625a7569a2e4a0999333e876354ac81d1bf1bb5e2", size = 45336615, upload-time = "2026-02-06T18:12:14.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/8d/ee2e4b7de948090cfb3df37d401c521233daf97bfc54ddec5d61d1d31618/polars_runtime_32-1.38.1-cp310-abi3-win_amd64.whl", hash = "sha256:08c2b3b93509c1141ac97891294ff5c5b0c548a373f583eaaea873a4bf506437", size = 45680732, upload-time = "2026-02-06T18:12:19.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/18/72c216f4ab0c82b907009668f79183ae029116ff0dd245d56ef58aac48e7/polars_runtime_32-1.38.1-cp310-abi3-win_arm64.whl", hash = "sha256:6d07d0cc832bfe4fb54b6e04218c2c27afcfa6b9498f9f6bbf262a00d58cc7c4", size = 41639413, upload-time = "2026-02-06T18:12:22.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/74/1b41205f7368c9375ab1dea91178eaa20435fe3eff036390a53a7660b416/polars_runtime_32-1.39.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9", size = 45273243, upload-time = "2026-03-20T11:14:26.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/bf/297716b3095fe719be20fcf7af1d2b6ab069c38199bbace2469608a69b3a/polars_runtime_32-1.39.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562", size = 40842924, upload-time = "2026-03-20T11:14:31.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/3e/e65236d9d0d9babfa0ecba593413c06530fca60a8feb8f66243aa5dba92e/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14", size = 43220650, upload-time = "2026-03-20T11:14:35.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/15/fc3e43f3fdf3f20b7dfb5abe871ab6162cf8fb4aeabf4cfad822d5dc4c79/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed", size = 46877498, upload-time = "2026-03-20T11:14:40.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/81/bd5f895919e32c6ab0a7786cd0c0ca961cb03152c47c3645808b54383f31/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2", size = 43380176, upload-time = "2026-03-20T11:14:45.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/3e/c86433c3b5ec0315bdfc7640d0c15d41f1216c0103a0eab9a9b5147d6c4c/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4", size = 46485933, upload-time = "2026-03-20T11:14:51.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/ce/200b310cf91f98e652eb6ea09fdb3a9718aa0293ebf113dce325797c8572/polars_runtime_32-1.39.3-cp310-abi3-win_amd64.whl", hash = "sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339", size = 46995458, upload-time = "2026-03-20T11:14:56.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/76/2d48927e0aa2abbdde08cbf4a2536883b73277d47fbeca95e952de86df34/polars_runtime_32-1.39.3-cp310-abi3-win_arm64.whl", hash = "sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860", size = 41857648, upload-time = "2026-03-20T11:15:01.142Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
{ name = "identify" },
|
||||
{ name = "nodeenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2196,11 +2315,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.11.0"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -2282,6 +2404,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-discovery"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/90/bcce6b46823c9bec1757c964dc37ed332579be512e17a30e9698095dcae4/python_discovery-1.2.0.tar.gz", hash = "sha256:7d33e350704818b09e3da2bd419d37e21e7c30db6e0977bb438916e06b41b5b1", size = 58055, upload-time = "2026-03-19T01:43:08.248Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/3c/2005227cb951df502412de2fa781f800663cccbef8d90ec6f1b371ac2c0d/python_discovery-1.2.0-py3-none-any.whl", hash = "sha256:1e108f1bbe2ed0ef089823d28805d5ad32be8e734b86a5f212bf89b71c266e4a", size = 31524, upload-time = "2026-03-19T01:43:07.045Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
@@ -2557,36 +2692,36 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.5"
|
||||
version = "0.15.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "82.0.0"
|
||||
version = "81.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2963,75 +3098,54 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "torch"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "cuda-bindings", marker = "sys_platform == 'linux'" },
|
||||
{ name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" },
|
||||
{ name = "filelock" },
|
||||
{ name = "fsspec" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "sympy" },
|
||||
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "triton", marker = "sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ee/efbd56687be60ef9af0c9c0ebe106964c07400eade5b0af8902a1d8cd58c/torch-2.10.0-3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a1ff626b884f8c4e897c4c33782bdacdff842a165fee79817b1dd549fdda1321", size = 915510070, upload-time = "2026-03-11T14:16:39.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/bb/d820f90e69cda6c8169b32a0c6a3ab7b17bf7990b8f2c680077c24a3c14c/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d", size = 79411450, upload-time = "2026-01-21T16:25:30.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/f2/c1690994afe461aae2d0cac62251e6802a703dec0a6c549c02ecd0de92a9/torch-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c0d7fcfbc0c4e8bb5ebc3907cbc0c6a0da1b8f82b1fc6e14e914fa0b9baf74e", size = 80526521, upload-time = "2026-03-23T18:12:06.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/f0/98ae802fa8c09d3149b0c8690741f3f5753c90e779bd28c9613257295945/torch-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4cf8687f4aec3900f748d553483ef40e0ac38411c3c48d0a86a438f6d7a99b18", size = 419723025, upload-time = "2026-03-23T18:11:43.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/1e/18a9b10b4bd34f12d4e561c52b0ae7158707b8193c6cfc0aad2b48167090/torch-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1b32ceda909818a03b112006709b02be1877240c31750a8d9c6b7bf5f2d8a6e5", size = 530589207, upload-time = "2026-03-23T18:11:23.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/40/2d532e8c0e23705be9d1debce5bc37b68d59a39bda7584c26fe9668076fe/torch-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3c712ae6fb8e7a949051a953fc412fe0a6940337336c3b6f905e905dac5157f", size = 114518313, upload-time = "2026-03-23T18:11:58.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3051,12 +3165,19 @@ name = "triton"
|
||||
version = "3.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" },
|
||||
]
|
||||
|
||||
@@ -3101,16 +3222,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.41.0"
|
||||
version = "0.42.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" },
|
||||
{ name = "h11", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "21.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "python-discovery" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Generated
+6
-1
@@ -47,10 +47,15 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "ferro_ta_core"
|
||||
version = "1.0.6"
|
||||
|
||||
[[package]]
|
||||
name = "ferro_ta_wasm"
|
||||
version = "1.0.0"
|
||||
version = "1.0.6"
|
||||
dependencies = [
|
||||
"ferro_ta_core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ferro_ta_wasm"
|
||||
version = "1.0.3"
|
||||
version = "1.0.6"
|
||||
edition = "2021"
|
||||
description = "WebAssembly bindings for ferro-ta technical analysis indicators"
|
||||
license = "MIT"
|
||||
@@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
ferro_ta_core = { path = "../crates/ferro_ta_core", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
+20
-5
@@ -11,7 +11,7 @@ npm install ferro-ta-wasm
|
||||
```
|
||||
|
||||
```javascript
|
||||
const { sma, ema, rsi, bbands, atr, obv, macd } = require('ferro-ta-wasm');
|
||||
const { sma, ema, wma, rsi, adx, mfi, bbands, atr, obv, macd } = require('ferro-ta-wasm');
|
||||
|
||||
const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10]);
|
||||
const smaOut = sma(close, 3);
|
||||
@@ -28,20 +28,23 @@ console.log('SMA:', Array.from(smaOut));
|
||||
|------------|---------------|----------------------------------------------------|---------|
|
||||
| Overlap | `sma` | `close: Float64Array, timeperiod: number` | `Float64Array` |
|
||||
| Overlap | `ema` | `close: Float64Array, timeperiod: number` | `Float64Array` |
|
||||
| Overlap | `wma` | `close: Float64Array, timeperiod: number` | `Float64Array` |
|
||||
| Overlap | `bbands` | `close, timeperiod, nbdevup, nbdevdn` | `Array[upper, middle, lower]` |
|
||||
| Momentum | `rsi` | `close: Float64Array, timeperiod: number` | `Float64Array` |
|
||||
| Momentum | `adx` | `high, low, close: Float64Array, timeperiod` | `Float64Array` |
|
||||
| Momentum | `macd` | `close, fastperiod, slowperiod, signalperiod` | `Array[macd, signal, hist]` |
|
||||
| Momentum | `mom` | `close: Float64Array, timeperiod: number` | `Float64Array` |
|
||||
| Momentum | `stochf` | `high, low, close, fastk_period, fastd_period` | `Array[fastk, fastd]` |
|
||||
| Volatility | `atr` | `high, low, close: Float64Array, timeperiod` | `Float64Array` |
|
||||
| Volume | `obv` | `close: Float64Array, volume: Float64Array` | `Float64Array` |
|
||||
| Volume | `mfi` | `high, low, close, volume: Float64Array, timeperiod` | `Float64Array` |
|
||||
|
||||
### Adding more indicators
|
||||
|
||||
All implementations are self-contained in `src/lib.rs` — no external crate dependency needed.
|
||||
WASM exports live in `src/lib.rs` and can either implement logic directly or delegate to `ferro_ta_core`.
|
||||
To add a new indicator:
|
||||
|
||||
1. Implement the algorithm in a `#[wasm_bindgen]` function in `src/lib.rs`.
|
||||
1. Add a `#[wasm_bindgen]` export in `src/lib.rs` (prefer delegating to `ferro_ta_core` where possible).
|
||||
2. Add at least two `#[wasm_bindgen_test]` tests covering output length and a known value.
|
||||
3. Update this README table.
|
||||
4. Run `wasm-pack test --node` to verify.
|
||||
@@ -79,7 +82,7 @@ wasm-pack build --target web --out-dir pkg-web
|
||||
## Usage (Node.js)
|
||||
|
||||
```javascript
|
||||
const { sma, ema, rsi, bbands, atr, obv, macd } = require('./pkg/ferro_ta_wasm.js');
|
||||
const { sma, ema, wma, rsi, adx, mfi, bbands, atr, obv, macd } = require('./pkg/ferro_ta_wasm.js');
|
||||
|
||||
const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10]);
|
||||
|
||||
@@ -91,6 +94,10 @@ console.log('SMA:', Array.from(smaOut)); // [ NaN, NaN, 44.193, ... ]
|
||||
const rsiOut = rsi(close, 5);
|
||||
console.log('RSI:', Array.from(rsiOut));
|
||||
|
||||
// WMA (period 5)
|
||||
const wmaOut = wma(close, 5);
|
||||
console.log('WMA:', Array.from(wmaOut));
|
||||
|
||||
// Bollinger Bands (period 5, ±2σ) — returns [upper, middle, lower]
|
||||
const [upper, middle, lower] = bbands(close, 5, 2.0, 2.0);
|
||||
console.log('BBANDS upper:', Array.from(upper));
|
||||
@@ -107,10 +114,18 @@ const low = new Float64Array([43.0, 44.0, 45.0, 44.0, 43.0, 42.0, 43.0]);
|
||||
const atrOut = atr(high, low, close, 3);
|
||||
console.log('ATR:', Array.from(atrOut));
|
||||
|
||||
// ADX (period 3)
|
||||
const adxOut = adx(high, low, close, 3);
|
||||
console.log('ADX:', Array.from(adxOut));
|
||||
|
||||
// OBV
|
||||
const volume = new Float64Array([1000, 1200, 900, 1500, 800, 600, 700]);
|
||||
const obvOut = obv(close, volume);
|
||||
console.log('OBV:', Array.from(obvOut));
|
||||
|
||||
// MFI (period 3)
|
||||
const mfiOut = mfi(high, low, close, volume, 3);
|
||||
console.log('MFI:', Array.from(mfiOut));
|
||||
```
|
||||
|
||||
## Usage (Browser)
|
||||
@@ -150,7 +165,7 @@ from source:
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only 9 indicators are currently exposed (SMA, EMA, BBANDS, RSI, MACD, MOM, STOCHF, ATR, OBV).
|
||||
- Only 12 indicators are currently exposed (SMA, EMA, WMA, BBANDS, RSI, ADX, MACD, MOM, STOCHF, ATR, OBV, MFI).
|
||||
Additional indicators will be added following the same pattern in `src/lib.rs`.
|
||||
- Large arrays (> 10M bars) may be slow due to JS↔WASM memory copies. For high-throughput
|
||||
use cases prefer the Python (PyO3) binding.
|
||||
|
||||
+7
-2
@@ -23,14 +23,16 @@ function makeSeries(length) {
|
||||
const close = new Float64Array(length);
|
||||
const high = new Float64Array(length);
|
||||
const low = new Float64Array(length);
|
||||
const volume = new Float64Array(length);
|
||||
let value = 100.0;
|
||||
for (let idx = 0; idx < length; idx += 1) {
|
||||
value += Math.sin(idx / 13.0) * 0.35 + Math.cos(idx / 29.0) * 0.18;
|
||||
close[idx] = value;
|
||||
high[idx] = value + 1.25;
|
||||
low[idx] = value - 1.10;
|
||||
volume[idx] = 1000.0 + Math.abs(Math.sin(idx / 7.0) * 300.0) + (idx % 100);
|
||||
}
|
||||
return { close, high, low };
|
||||
return { close, high, low, volume };
|
||||
}
|
||||
|
||||
function timeMin(fn, rounds = 7) {
|
||||
@@ -45,11 +47,14 @@ function timeMin(fn, rounds = 7) {
|
||||
}
|
||||
|
||||
function runBenchmark({ bars }) {
|
||||
const { close, high, low } = makeSeries(bars);
|
||||
const { close, high, low, volume } = makeSeries(bars);
|
||||
const cases = [
|
||||
["SMA", () => wasm.sma(close, 20)],
|
||||
["EMA", () => wasm.ema(close, 20)],
|
||||
["WMA", () => wasm.wma(close, 20)],
|
||||
["RSI", () => wasm.rsi(close, 14)],
|
||||
["ADX", () => wasm.adx(high, low, close, 14)],
|
||||
["MFI", () => wasm.mfi(high, low, close, volume, 14)],
|
||||
["ATR", () => wasm.atr(high, low, close, 14)],
|
||||
["BBANDS", () => wasm.bbands(close, 20, 2.0, 2.0)],
|
||||
];
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ferro-ta-wasm",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.6",
|
||||
"description": "WebAssembly bindings for ferro-ta technical analysis indicators",
|
||||
"main": "pkg/ferro_ta_wasm.js",
|
||||
"types": "pkg/ferro_ta_wasm.d.ts",
|
||||
|
||||
+158
@@ -10,6 +10,7 @@ and `MACD`).
|
||||
## Overlap Studies
|
||||
- [`sma`] — Simple Moving Average
|
||||
- [`ema`] — Exponential Moving Average
|
||||
- [`wma`] — Weighted Moving Average
|
||||
- [`bbands`] — Bollinger Bands (returns `[upper, middle, lower]`)
|
||||
|
||||
## Momentum Indicators
|
||||
@@ -17,12 +18,14 @@ and `MACD`).
|
||||
- [`macd`] — Moving Average Convergence/Divergence (returns `[macd, signal, hist]`)
|
||||
- [`mom`] — Momentum (close[i] - close[i-period])
|
||||
- [`stochf`] — Fast Stochastic (returns `[fastk, fastd]`)
|
||||
- [`adx`] — Average Directional Movement Index
|
||||
|
||||
## Volatility Indicators
|
||||
- [`atr`] — Average True Range (Wilder smoothing)
|
||||
|
||||
## Volume Indicators
|
||||
- [`obv`] — On-Balance Volume
|
||||
- [`mfi`] — Money Flow Index
|
||||
*/
|
||||
|
||||
use js_sys::{Array, Float64Array};
|
||||
@@ -325,6 +328,24 @@ pub fn obv(close: &Float64Array, volume: &Float64Array) -> Float64Array {
|
||||
from_vec(result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WMA — Weighted Moving Average
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Weighted Moving Average.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `timeperiod` – look-back window (default 30, minimum 1).
|
||||
///
|
||||
/// # Returns
|
||||
/// `Float64Array` with the first `timeperiod - 1` values set to `NaN`.
|
||||
#[wasm_bindgen]
|
||||
pub fn wma(close: &Float64Array, timeperiod: usize) -> Float64Array {
|
||||
let prices = to_vec(close);
|
||||
from_vec(ferro_ta_core::overlap::wma(&prices, timeperiod))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MOM — Momentum
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -433,6 +454,70 @@ pub fn stochf(
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ADX — Average Directional Movement Index
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Average Directional Movement Index (Wilder smoothing).
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `high` – `Float64Array` of high prices.
|
||||
/// - `low` – `Float64Array` of low prices.
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `timeperiod` – look-back period (default 14, minimum 1).
|
||||
///
|
||||
/// # Returns
|
||||
/// `Float64Array`; warm-up values are `NaN`.
|
||||
#[wasm_bindgen]
|
||||
pub fn adx(
|
||||
high: &Float64Array,
|
||||
low: &Float64Array,
|
||||
close: &Float64Array,
|
||||
timeperiod: usize,
|
||||
) -> Float64Array {
|
||||
let h = to_vec(high);
|
||||
let l = to_vec(low);
|
||||
let c = to_vec(close);
|
||||
if h.len() != l.len() || h.len() != c.len() {
|
||||
return from_vec(vec![f64::NAN; c.len()]);
|
||||
}
|
||||
from_vec(ferro_ta_core::momentum::adx(&h, &l, &c, timeperiod))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MFI — Money Flow Index
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Money Flow Index.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `high` – `Float64Array` of high prices.
|
||||
/// - `low` – `Float64Array` of low prices.
|
||||
/// - `close` – `Float64Array` of close prices.
|
||||
/// - `volume` – `Float64Array` of volume values.
|
||||
/// - `timeperiod` – look-back period (default 14, minimum 1).
|
||||
///
|
||||
/// # Returns
|
||||
/// `Float64Array`; warm-up values are `NaN`.
|
||||
#[wasm_bindgen]
|
||||
pub fn mfi(
|
||||
high: &Float64Array,
|
||||
low: &Float64Array,
|
||||
close: &Float64Array,
|
||||
volume: &Float64Array,
|
||||
timeperiod: usize,
|
||||
) -> Float64Array {
|
||||
let h = to_vec(high);
|
||||
let l = to_vec(low);
|
||||
let c = to_vec(close);
|
||||
let v = to_vec(volume);
|
||||
let n = c.len();
|
||||
if h.len() != n || l.len() != n || v.len() != n {
|
||||
return from_vec(vec![f64::NAN; n]);
|
||||
}
|
||||
from_vec(ferro_ta_core::volume::mfi(&h, &l, &c, &v, timeperiod))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MACD — Moving Average Convergence/Divergence
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -861,4 +946,77 @@ mod tests {
|
||||
assert!(v >= 0.0 && v <= 100.0, "fastk value {v} out of [0, 100]");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// WMA tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_wma_output_length() {
|
||||
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let out = wma(&close, 3);
|
||||
assert_eq!(out.length(), 5);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_wma_known_value() {
|
||||
// WMA(3) at index 2 = (1*1 + 2*2 + 3*3) / 6 = 14/6
|
||||
let close = make_arr(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let out = wma(&close, 3);
|
||||
let mut vals = vec![0.0f64; 5];
|
||||
out.copy_to(&mut vals);
|
||||
assert!(vals[0].is_nan());
|
||||
assert!(vals[1].is_nan());
|
||||
assert!((vals[2] - (14.0 / 6.0)).abs() < 1e-10);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ADX tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_adx_output_length() {
|
||||
let h = make_arr(&[10.0, 11.0, 12.0, 13.0, 13.5, 14.0, 14.5, 15.0]);
|
||||
let l = make_arr(&[9.0, 9.5, 10.5, 11.5, 12.0, 12.5, 13.0, 13.5]);
|
||||
let c = make_arr(&[9.5, 10.5, 11.5, 12.0, 13.0, 13.5, 14.0, 14.5]);
|
||||
let out = adx(&h, &l, &c, 3);
|
||||
assert_eq!(out.length(), 8);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_adx_values_in_range() {
|
||||
let h = make_arr(&[10.0, 11.0, 12.0, 13.0, 13.5, 14.0, 14.5, 15.0]);
|
||||
let l = make_arr(&[9.0, 9.5, 10.5, 11.5, 12.0, 12.5, 13.0, 13.5]);
|
||||
let c = make_arr(&[9.5, 10.5, 11.5, 12.0, 13.0, 13.5, 14.0, 14.5]);
|
||||
let out = adx(&h, &l, &c, 3);
|
||||
for v in get_finite(&out) {
|
||||
assert!((0.0..=100.0).contains(&v), "ADX out of range: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// MFI tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_mfi_output_length() {
|
||||
let h = make_arr(&[10.0, 11.0, 12.0, 11.5, 12.5, 13.0, 13.5]);
|
||||
let l = make_arr(&[9.0, 9.5, 10.5, 10.0, 11.0, 11.5, 12.0]);
|
||||
let c = make_arr(&[9.5, 10.5, 11.5, 11.0, 12.0, 12.5, 13.0]);
|
||||
let v = make_arr(&[100.0, 110.0, 120.0, 130.0, 125.0, 140.0, 150.0]);
|
||||
let out = mfi(&h, &l, &c, &v, 3);
|
||||
assert_eq!(out.length(), 7);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_mfi_values_in_range() {
|
||||
let h = make_arr(&[10.0, 11.0, 12.0, 11.5, 12.5, 13.0, 13.5]);
|
||||
let l = make_arr(&[9.0, 9.5, 10.5, 10.0, 11.0, 11.5, 12.0]);
|
||||
let c = make_arr(&[9.5, 10.5, 11.5, 11.0, 12.0, 12.5, 13.0]);
|
||||
let v = make_arr(&[100.0, 110.0, 120.0, 130.0, 125.0, 140.0, 150.0]);
|
||||
let out = mfi(&h, &l, &c, &v, 3);
|
||||
for val in get_finite(&out) {
|
||||
assert!((0.0..=100.0).contains(&val), "MFI out of range: {val}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user