Compare commits

..

10 Commits

Author SHA1 Message Date
dependabot[bot] 7b7159fe19 chore(deps): bump actions/deploy-pages from 4 to 5
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 10:23:42 +00:00
Pratik Bhadane 2d776b6f90 chore: prepare v1.0.6 release
Align the Rust, Python, WASM, Conda, docs, and lockfile version markers to 1.0.6 so the published artifacts and release automation agree on a single release number.

Add a proper 1.0.6 changelog entry in both CHANGELOG.md and the docs release notes, covering the pre-push gate, expanded Rust and WASM surface, benchmark refresh, and CI hardening work shipped since v1.0.4.

Validate the release preparation with the repo-managed pre-push suite so version, changelog, Rust, Python, docs, WASM, and API-manifest checks all pass before tagging.
2026-03-24 15:03:00 +05:30
Pratik Bhadane 71b6343e92 feat: refresh benchmark coverage and harden CI tooling
Refresh the benchmark and performance surface across the repo. This updates the benchmark wrappers and helper scripts, regenerates the checked-in benchmark and perf-contract artifacts, and folds in the related roadmap, compatibility, and example notebook changes that belong with this performance-focused pass.

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

Align local tooling with the current project configuration by updating the Ruff pre-commit hook, tightening the API typing and MCP server helpers, and refreshing the lockfile to pick up the audited PyJWT fix while preserving the rest of the staged source changes.
2026-03-24 14:52:20 +05:30
Pratik Bhadane 53566b9d82 feat: expand rust parity, wasm exports, and api conformance
Move several hot Python analysis paths to Rust-backed helpers. This adds Rust implementations for backtest strategy signal generation and the core portfolio loop, options and futures payoff aggregation, Greeks aggregation, ratio calculation, trade extraction, chunked close-only indicator runs, and forward-fill helpers. Wire the Python analysis and data modules to prefer these paths, and add coverage for the new batch fast path.

Expand the WASM package to export WMA, ADX, and MFI from ferro_ta_core, refresh the Node examples, benchmarks, and README, and add a Node-vs-Python conformance test so the browser and node surface stays aligned with the main Python package.

Introduce a generated cross-surface API manifest in docs/, along with scripts to rebuild and verify it from source exports. Enforce manifest freshness in the Python and WASM CI workflows so release candidates catch surface drift before push.
2026-03-24 14:28:51 +05:30
Pratik Bhadane ba77fbd418 devx: add a repo-managed pre-push CI gate
Add a versioned pre-push runner that mirrors the basic required CI suites we can execute locally, including version/changelog checks, Rust fmt/clippy/core checks, Python lint/typecheck/tests, docs, and the WASM gate.

Wire the runner into pre-commit at the pre-push stage, add make prepush and make hooks for manual use and hook installation, and document the workflow in the contribution guides.

Also add pre-commit to the development dependency set and refresh uv.lock so a synced dev environment can install and run the hook reproducibly.
2026-03-24 14:24:44 +05:30
Pratik Bhadane 29c6f5cf84 chore: release v1.0.4 2026-03-24 12:54:41 +05:30
Pratik Bhadane 58a1dc2308 chore: remove .coverage file and update .gitignore to exclude coverage files
- Deleted the .coverage file to clean up the repository.
- Updated .gitignore to ensure .coverage and .coverage.* files are ignored in future commits.
- Revised README.md to enhance clarity and conciseness regarding the library's capabilities and performance.
- Improved documentation for the MCP server, emphasizing its expanded functionality and integration with clients.
2026-03-24 12:49:17 +05:30
Pratik Bhadane b66b05682e docs: finalize 1.0.3 release notes 2026-03-24 11:24:34 +05:30
Pratik Bhadane e4ac7dd4d3 fix: repair release workflow 2026-03-24 11:23:41 +05:30
Pratik Bhadane 1f053c1001 fix: unblock python ci 2026-03-24 11:19:48 +05:30
93 changed files with 12748 additions and 2797 deletions
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -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.
+7 -3
View File
@@ -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'
+3
View File
@@ -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
+6 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+28
View File
@@ -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
View File
@@ -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
View File
@@ -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"] }
+10 -2
View File
@@ -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
+5 -5
View File
@@ -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)
+60 -1019
View File
File diff suppressed because it is too large Load Diff
+261
View File
@@ -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
View File
@@ -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``.
+1 -1
View File
@@ -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
}
]
}
}
+1 -1
View File
@@ -59,4 +59,4 @@
"sha256": "f31fd871990c44e24a2259d618ae40a52866d20b95aa6047af3d38b9371c2ab7"
}
}
}
}
@@ -93,4 +93,4 @@
"share_of_suite_pct": 0.09
}
]
}
}
+1 -1
View File
@@ -282,4 +282,4 @@
]
}
}
}
}
+1 -1
View File
@@ -70,4 +70,4 @@
"stream_over_batch_ratio": 121.7603
}
]
}
}
+9 -3
View File
@@ -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(
+2 -1
View File
@@ -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
+2 -6
View File
@@ -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(
+6 -2
View File
@@ -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")
+18 -6
View File
@@ -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
+13 -3
View File
@@ -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)
+4 -4
View File
@@ -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}"
+20 -5
View File
@@ -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(
+1 -1
View File
@@ -16094,4 +16094,4 @@
],
"datetime": "2026-03-23T17:14:05.427766+00:00",
"version": "5.2.3"
}
}
+4 -5
View File
@@ -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
View File
@@ -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):
+17 -1
View File
@@ -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
View File
@@ -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):
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
{% set name = "ferro-ta" %}
{% set version = "1.0.3" %}
{% set version = "1.0.6" %}
package:
name: {{ name|lower }}
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
------------------
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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>`_
+3 -3
View File
@@ -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",
+2 -1
View File
@@ -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",
-1
View File
@@ -22,7 +22,6 @@
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"from ferro_ta.streaming import (\n",
" StreamingATR,\n",
" StreamingBBands,\n",
+55 -33
View File
@@ -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
}
]
}
}
+53 -31
View File
@@ -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
View File
@@ -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"
}
}
}
}
+69 -47
View File
@@ -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
}
]
}
}
+1 -1
View File
@@ -282,4 +282,4 @@
]
}
}
}
}
+59 -37
View File
@@ -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
View File
@@ -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",
+2 -2
View File
@@ -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
+4
View File
@@ -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)
+6 -24
View File
@@ -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),
)
+25 -69
View File
@@ -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),
)
+2 -5
View File
@@ -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))
# ---------------------------------------------------------------------------
+61 -71
View File
@@ -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),
)
+4 -7
View File
@@ -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))
)
# ---------------------------------------------------------------------------
+38 -9
View File
@@ -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):
+38
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+294
View File
@@ -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()
-1
View File
@@ -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+$")
+53
View File
@@ -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())
+193
View File
@@ -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'
+49
View File
@@ -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(())
}
+244
View File
@@ -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
View File
@@ -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(())
}
+2
View File
@@ -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(())
}
+17
View File
@@ -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(())
}
+443
View File
@@ -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))
}
+30
View File
@@ -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
+3 -1
View File
@@ -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)
+27 -12
View File
@@ -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
+7
View File
@@ -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):
+196 -1
View File
@@ -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
Generated
+298 -161
View File
@@ -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]]
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}");
}
}
}