Compare commits

...
8 Commits
Author SHA1 Message Date
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
Pratik Bhadane 13cb0dc95a chore: release v1.0.3 2026-03-24 11:09:48 +05:30
Pratik BhadaneandClaude Sonnet 4.6 0382c4e302 ci: add release.yml to auto-publish on version tag push
Pushing v* tag now triggers:
  1. release.yml — creates a GitHub Release (published, not draft),
     pulling the release notes from CHANGELOG.md automatically.
  2. CI.yml (release: published event) — builds wheels for Linux /
     macOS / Windows × Python 3.10-3.13, sdist, publishes to PyPI
     via trusted publisher, publishes ferro_ta_core to crates.io,
     and generates SBOMs.

Pre-release tags (e.g. v1.1.0-rc.1) are marked as pre-release on
GitHub automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 03:00:06 +05:30
Pratik Bhadane 7736effc27 fix(bench): adjust feature_matrix speedup floor to 0.40 for cross-architecture compatibility 2026-03-24 02:49:29 +05:30
49 changed files with 8012 additions and 2045 deletions
BIN
View File
Binary file not shown.
+60
View File
@@ -0,0 +1,60 @@
name: Release
# 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*.*.*"
permissions:
contents: write
jobs:
create-release:
name: Create GitHub Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Extract version from tag
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
id: changelog
env:
TAG_NAME: ${{ github.ref_name }}
run: |
python3 - <<'PY'
import re, os, pathlib
version = os.environ["TAG_NAME"].lstrip("v")
text = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8")
pattern = rf"## \[{re.escape(version)}\].*?\n(.*?)(?=\n## |\Z)"
match = re.search(pattern, text, re.DOTALL)
body = match.group(1).strip() if match else f"Release {version}"
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"body<<EOF\n{body}\nEOF\n")
PY
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: v${{ steps.version.outputs.version }}
body: ${{ steps.changelog.outputs.body }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
+3 -1
View File
@@ -31,6 +31,8 @@ env/
# WASM build output # WASM build output
wasm/pkg/ wasm/pkg/
wasm/pkg-web/ wasm/pkg-web/
.coverage
.coverage.*
coverage.xml coverage.xml
.hypothesis/ .hypothesis/
@@ -39,4 +41,4 @@ coverage.xml
# DS Store in all directories # DS Store in all directories
.DS_Store .DS_Store
*.DS_Store *.DS_Store
+65 -1
View File
@@ -9,6 +9,68 @@ and the project uses [Semantic Versioning](https://semver.org/).
## [Unreleased] ## [Unreleased]
## [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
- Public package metadata helpers: `ferro_ta.__version__`, `ferro_ta.about()`,
and `ferro_ta.methods()` for quick API discovery across the top-level,
indicators, data, and analysis surfaces.
- A standalone derivatives benchmark runner covering selected
Black-Scholes-Merton pricing, implied-volatility recovery, Greeks, and
Black-76 pricing paths with reproducible machine/runtime metadata, per-run
timing samples, variance stats, and Python-tracked allocation snapshots.
- A one-command version bump helper, `scripts/bump_version.py`, plus `make version
VERSION=X.Y.Z` for aligned Cargo, Python, WASM, Conda, and docs release
surfaces.
### Changed
- Tightened the homepage and docs product narrative so the core Rust-backed
Python TA library leads, while adjacent tooling is called out separately.
- Strengthened benchmark evidence and support documentation with clearer
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 ## [1.0.2] — 2026-03-24
### Performance ### Performance
@@ -293,7 +355,9 @@ and the project uses [Semantic Versioning](https://semver.org/).
--- ---
[Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.2...HEAD [Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.4...HEAD
[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.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 [1.0.1]: https://github.com/pratikbhadane24/ferro-ta/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/pratikbhadane24/ferro-ta/releases/tag/v1.0.0 [1.0.0]: https://github.com/pratikbhadane24/ferro-ta/releases/tag/v1.0.0
Generated
+2 -2
View File
@@ -207,7 +207,7 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "ferro_ta" name = "ferro_ta"
version = "1.0.2" version = "1.0.4"
dependencies = [ dependencies = [
"criterion", "criterion",
"ferro_ta_core", "ferro_ta_core",
@@ -222,7 +222,7 @@ dependencies = [
[[package]] [[package]]
name = "ferro_ta_core" name = "ferro_ta_core"
version = "1.0.2" version = "1.0.4"
dependencies = [ dependencies = [
"criterion", "criterion",
"wide", "wide",
+9 -2
View File
@@ -5,9 +5,16 @@ resolver = "2"
[package] [package]
name = "ferro_ta" name = "ferro_ta"
version = "1.0.2" version = "1.0.4"
edition = "2021" edition = "2021"
description = "Rust-powered Python technical analysis library with a TA-Lib-compatible API"
license = "MIT" 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 publish = false
[lib] [lib]
@@ -23,7 +30,7 @@ ndarray = "0.16"
rayon = "1.10" rayon = "1.10"
log = "0.4" log = "0.4"
pyo3-log = "0.12" pyo3-log = "0.12"
ferro_ta_core = { path = "crates/ferro_ta_core", version = "1.0.2" } ferro_ta_core = { path = "crates/ferro_ta_core", version = "1.0.4" }
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.8", features = ["html_reports"] } criterion = { version = "0.8", features = ["html_reports"] }
+6 -1
View File
@@ -1,7 +1,7 @@
# ferro-ta development Makefile # ferro-ta development Makefile
# Usage: make <target> # Usage: make <target>
.PHONY: help dev build test lint typecheck fmt docs clean bench .PHONY: help dev build test lint typecheck fmt docs clean bench version
# Default target # Default target
help: help:
@@ -15,6 +15,7 @@ help:
@echo " make typecheck Run mypy + pyright type checkers" @echo " make typecheck Run mypy + pyright type checkers"
@echo " make docs Build the Sphinx documentation" @echo " make docs Build the Sphinx documentation"
@echo " make bench Run Rust criterion benchmarks (ferro_ta_core)" @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 audit Run cargo-audit + pip-audit"
@echo " make clean Remove build artefacts" @echo " make clean Remove build artefacts"
@@ -48,6 +49,10 @@ docs:
bench: bench:
cargo bench -p ferro_ta_core cargo bench -p ferro_ta_core
version:
@test -n "$(VERSION)" || (echo "Usage: make version VERSION=X.Y.Z" && exit 1)
python3 scripts/bump_version.py "$(VERSION)"
audit: audit:
cargo audit cargo audit
uv run --with pip-audit pip-audit uv run --with pip-audit pip-audit
+70 -1045
View File
File diff suppressed because it is too large Load Diff
+28 -7
View File
@@ -42,6 +42,8 @@ Before starting a release:
- [ ] `CHANGELOG.md` has a `## [X.Y.Z]` section (not `[Unreleased]`) with all - [ ] `CHANGELOG.md` has a `## [X.Y.Z]` section (not `[Unreleased]`) with all
changes since the last release documented under `### Added`, `### Changed`, changes since the last release documented under `### Added`, `### Changed`,
`### Fixed`, `### Removed` headings. `### Fixed`, `### Removed` headings.
- [ ] Public docs match the release: `docs/conf.py`, `docs/changelog.rst`, and
`docs/support_matrix.rst` reflect the version and current support status.
--- ---
@@ -62,26 +64,39 @@ Example: current version is `0.1.0` and you are adding new indicators → new ve
## Step 2 — Sync version everywhere ## Step 2 — Sync version everywhere
These files must carry **the same version string** (e.g. `0.2.0`). Update all before tagging: These files must carry **the same version string** (e.g. `0.2.0`). The easiest
way to do that is:
```bash
python3 scripts/bump_version.py 0.2.0
python3 scripts/bump_version.py --check
```
That script updates the tracked release-version carriers for you.
Files covered by the bump script:
| File | Location | | File | Location |
|------|----------| |------|----------|
| `Cargo.toml` | Root (source of truth) | | `Cargo.toml` | Root (source of truth) |
| `crates/ferro_ta_core/Cargo.toml` | Same version for crates.io publish | | `crates/ferro_ta_core/Cargo.toml` | Same version for crates.io publish |
| `crates/ferro_ta_core/README.md` | Installation snippet should show the current crate version |
| `pyproject.toml` | Root | | `pyproject.toml` | Root |
| `wasm/package.json` | `"version": "0.2.0"` | | `wasm/package.json` | Package version |
| `conda/meta.yaml` | Conda recipe version |
| `docs/conf.py` | Default Sphinx release must resolve to the same version |
**`Cargo.toml`** (root): **`Cargo.toml`** (root):
```toml ```toml
[package] [package]
name = "ferro_ta" name = "ferro_ta"
version = "0.2.0" # ← update here version = "X.Y.Z" # ← or use scripts/bump_version.py X.Y.Z
``` ```
**`pyproject.toml`**: **`pyproject.toml`**:
```toml ```toml
[project] [project]
version = "0.2.0" # ← must match Cargo.toml exactly version = "X.Y.Z" # ← must match Cargo.toml exactly
``` ```
> **Rule:** `Cargo.toml` is the source of truth. Sync the others to match before tagging. > **Rule:** `Cargo.toml` is the source of truth. Sync the others to match before tagging.
@@ -91,18 +106,24 @@ version = "0.2.0" # ← must match Cargo.toml exactly
## Step 3 — Update CHANGELOG.md ## Step 3 — Update CHANGELOG.md
1. Open `CHANGELOG.md`. 1. Open `CHANGELOG.md`.
2. Rename the `[Unreleased]` section to `[0.2.0] — YYYY-MM-DD` (today's date). 2. Rename the `[Unreleased]` section to `[X.Y.Z] — YYYY-MM-DD` (today's date).
3. Add a fresh empty `[Unreleased]` section at the top. 3. Add a fresh empty `[Unreleased]` section at the top.
4. Update the comparison links at the bottom: 4. Update the comparison links at the bottom:
```markdown ```markdown
[Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/v0.2.0...HEAD [Unreleased]: https://github.com/pratikbhadane24/ferro-ta/compare/vX.Y.Z...HEAD
[0.2.0]: https://github.com/pratikbhadane24/ferro-ta/compare/v0.1.0...v0.2.0 [X.Y.Z]: https://github.com/pratikbhadane24/ferro-ta/compare/vPREVIOUS...vX.Y.Z
``` ```
Follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format: Follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format:
`Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`. `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
Also update the docs-facing release surfaces for the same version:
- `docs/changelog.rst` with a concise release-notes entry
- `docs/support_matrix.rst` if supported versions, tested wheels, or module
stability changed
--- ---
## Step 4 — Commit the version bump ## Step 4 — Commit the version bump
+262
View File
@@ -0,0 +1,262 @@
# 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.
+23 -6
View File
@@ -21,16 +21,33 @@ Currently supported: **3.10, 3.11, 3.12, 3.13** (see `pyproject.toml`).
## Release playbook ## Release playbook
1. **Bump the version** in `Cargo.toml` and `pyproject.toml` to the new version ### Fast path
(e.g. `1.0.1`).
2. **Update `CHANGELOG.md`**: move the `[Unreleased]` block to a new dated section 1. **Bump tracked version files with one command**:
```bash
python3 scripts/bump_version.py 1.0.3
```
or:
```bash
make version VERSION=1.0.3
```
2. **Verify everything matches**:
```bash
python3 scripts/bump_version.py --check
```
3. **Update `CHANGELOG.md`**: move the `[Unreleased]` block to a new dated section
`[1.0.1] — YYYY-MM-DD` and open a fresh `[Unreleased]` block. `[1.0.1] — YYYY-MM-DD` and open a fresh `[Unreleased]` block.
3. **Commit** the version bump and changelog update with message 4. **Commit** the version bump and changelog update with message
`chore: release v1.0.1`. `chore: release v1.0.1`.
4. **Create a tag**: `git tag v1.0.1 && git push origin v1.0.1`. 5. **Create a tag**: `git tag v1.0.1 && git push origin v1.0.1`.
5. **Create a GitHub Release** for tag `v1.0.1` — the CI `build-wheels` and 6. **Create a GitHub Release** for tag `v1.0.1` — the CI `build-wheels` and
`publish` jobs trigger automatically on `release: published`. `publish` jobs trigger automatically on `release: published`.
The bump script updates the tracked release-version carriers that are easy to
miss manually: root Cargo, Python packaging, the core crate, the core crate
README install snippet, the WASM package, the Conda recipe, and the docs pages
that show the current released version.
## Breaking-change policy ## Breaking-change policy
- Removing an indicator or changing its signature is a **MAJOR** change. - Removing an indicator or changing its signature is a **MAJOR** change.
+1 -1
View File
@@ -106,7 +106,7 @@ MAX_SERIES_LENGTH = int(os.environ.get("MAX_SERIES_LENGTH", "100000"))
app = FastAPI( app = FastAPI(
title="ferro-ta API", title="ferro-ta API",
description="REST API for ferro-ta technical analysis indicators and backtesting.", description="REST API for ferro-ta technical analysis indicators and backtesting.",
version="1.0.0", version=ft.__version__,
docs_url="/docs", docs_url="/docs",
redoc_url="/redoc", redoc_url="/redoc",
) )
+51 -6
View File
@@ -1,14 +1,21 @@
# ferro-ta Benchmark Suite # ferro-ta Benchmark Suite
> **62 indicators × 6 libraries** — accuracy and speed verified on **100,000 bars** (LARGE dataset). > Reproducible speed and accuracy comparisons across 62 indicators and the
> libraries available in your environment.
## Overview ## Overview
The benchmark suite compares **ferro-ta** against five popular Python technical-analysis libraries on a common dataset and shared wrappers so timings are directly comparable. The benchmark suite compares **ferro-ta** against other Python
technical-analysis libraries on a common dataset and shared wrappers so the
results are easier to reproduce and critique.
It is not designed to prove that ferro-ta wins everywhere. It is designed to
show where ferro-ta is faster, where it only ties, and where another library
still wins.
| Library | Notes | | Library | Notes |
|-----------|-------| |-----------|-------|
| **TA-Lib** | C extension; gold standard for accuracy and speed | | **TA-Lib** | C extension; widely used comparison baseline |
| **pandas-ta** | Pure Python; broad indicator set | | **pandas-ta** | Pure Python; broad indicator set |
| **ta** | Simple API; some indicators use O(n²) loops and are very slow | | **ta** | Simple API; some indicators use O(n²) loops and are very slow |
| **Tulipy** | C extension; truncated output (no leading NaN padding) | | **Tulipy** | C extension; truncated output (no leading NaN padding) |
@@ -37,9 +44,23 @@ from benchmarks.data_generator import SMALL, MEDIUM, LARGE
- **Harness:** [pytest-benchmark](https://pytest-benchmark.readthedocs.io/) with `benchmark.pedantic(..., iterations=5, rounds=20, warmup_rounds=2)`. - **Harness:** [pytest-benchmark](https://pytest-benchmark.readthedocs.io/) with `benchmark.pedantic(..., iterations=5, rounds=20, warmup_rounds=2)`.
- **Reported metric:** **Median time per call** in **microseconds (µs)** — lower is better. - **Reported metric:** **Median time per call** in **microseconds (µs)** — lower is better.
- **Machine info:** Stored in `benchmarks/results.json` (`machine_info`, `commit_info`) for reproducibility. - **TA-Lib head-to-head JSON:** `benchmarks/bench_vs_talib.py` records per-run samples, variance stats, machine/runtime/build metadata, and Python-tracked peak allocation snapshots.
- **Machine info:** Stored in the generated JSON artifacts for reproducibility.
- **Libraries:** Only libraries present in the environment are benchmarked; missing ones are skipped. - **Libraries:** Only libraries present in the environment are benchmarked; missing ones are skipped.
## Current checked-in TA-Lib artifact
The checked-in `benchmarks/artifacts/latest/benchmark_vs_talib.json` artifact
uses contiguous `float64` arrays at 10k and 100k bars on an Apple M3 Max,
CPython 3.13.5, and Rust 1.91.1 with the default release profile
(`lto = true`, `codegen-units = 1`).
- ferro-ta is ahead outside the tie band on 6 of 12 rows at 10k bars and 6 of 12 rows at 100k bars.
- TA-Lib still wins in the current artifact on `STOCH` and `ADX`, and remains close on `EMA`, `RSI`, `ATR`, and `OBV` depending on size.
- The public claim should therefore be read as "often faster on selected indicators," not "faster everywhere."
- When publishing performance statements, point readers to the raw JSON artifact, not just the summary table.
- The artifact now includes per-run samples, variance stats, and Python-tracked allocation snapshots for each compared indicator.
## Reproducible Perf Artifacts ## Reproducible Perf Artifacts
Use the perf-contract runner when you want a compact set of machine-readable Use the perf-contract runner when you want a compact set of machine-readable
@@ -140,7 +161,7 @@ The speed table includes **all 62 indicators**. **Number** = median µs; **N/A**
**Takeaways:** **Takeaways:**
- **`ta`** is 20350× slower on ATR, CCI, ADX, MFI (O(n²) Python loops). - **`ta`** is 20350× slower on ATR, CCI, ADX, MFI (O(n²) Python loops).
- **ferro-ta** is typically 24× faster than **pandas-ta** across indicators. - **ferro-ta** is often materially faster than **pandas-ta** on the checked-in 100k-bar table.
- **TA-Lib** and **Tulipy** (C extensions) are strong; ferro-ta is competitive and avoids native dependencies. - **TA-Lib** and **Tulipy** (C extensions) are strong; ferro-ta is competitive and avoids native dependencies.
--- ---
@@ -160,9 +181,14 @@ uv run pytest benchmarks/test_speed.py --benchmark-only -k "test_large_dataset"
# Regenerate the Speed Comparison markdown table from results.json # Regenerate the Speed Comparison markdown table from results.json
uv run python benchmarks/benchmark_table.py uv run python benchmarks/benchmark_table.py
# TA-Lib head-to-head with machine-readable summary + git/runtime metadata # TA-Lib head-to-head with machine/runtime/build metadata, per-run samples,
# variance stats, and Python-tracked allocation snapshots
uv run python benchmarks/bench_vs_talib.py --sizes 10000 100000 --json benchmark_vs_talib.json uv run python benchmarks/bench_vs_talib.py --sizes 10000 100000 --json benchmark_vs_talib.json
# Selected derivatives analytics comparison (BSM price, IV, Greeks, Black-76)
# against built-in analytical references plus optional installed libraries
uv run python benchmarks/bench_derivatives_compare.py --sizes 1000 10000 --json benchmark_derivatives_compare.json
# Optional regression check used in CI # Optional regression check used in CI
uv run python benchmarks/check_vs_talib_regression.py --input benchmark_vs_talib.json uv run python benchmarks/check_vs_talib_regression.py --input benchmark_vs_talib.json
@@ -184,6 +210,25 @@ uv run python benchmarks/run_perf_contract.py --output-dir benchmarks/artifacts/
Without `uv`: use `pytest` and `python` from the same environment where `ferro_ta` and optional libs (e.g. `talib`, `pandas_ta`, `ta`, `tulipy`, `finta`) are installed. Without `uv`: use `pytest` and `python` from the same environment where `ferro_ta` and optional libs (e.g. `talib`, `pandas_ta`, `ta`, `tulipy`, `finta`) are installed.
### Derivatives analytics
`benchmarks/bench_derivatives_compare.py` focuses on selected options-analytics
paths rather than the full surface area:
- `BSM` call pricing
- call implied-volatility recovery
- call Greeks
- `Black-76` call pricing
The script always includes two analytical baselines:
- `reference_numpy` — pure NumPy formulas with vectorized IV bisection
- `reference_python_loop` — scalar `math`-based reference for sanity checking
If `py_vollib` is installed, it is added automatically as an extra baseline.
The output JSON includes runtime/build metadata, per-run timing samples,
variance stats, and Python-tracked peak allocation snapshots.
### WASM ### WASM
From the `wasm/` directory: From the `wasm/` directory:
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+210 -112
View File
@@ -1,37 +1,34 @@
""" """
ferro_ta vs TA-Lib speed comparison. ferro_ta vs TA-Lib speed comparison.
Measures throughput (M bars/s) for both libraries on the same data and parameters, Measures throughput (M bars/s) for both libraries on the same synthetic data
and reports speedup (talib_time / ferro_ta_time; > 1 means ferro_ta is faster). and parameters. The output is intentionally evidence-heavy:
Requirements: - median timings
pip install ta-lib # or conda install ta-lib - per-run timing samples
- variability stats
- Python-tracked peak allocation snapshots
- machine, runtime, and build metadata
Run: This is meant to support a narrow claim: ferro-ta is often faster on selected
python benchmarks/bench_vs_talib.py indicators, not universally faster.
python benchmarks/bench_vs_talib.py --json results.json
python benchmarks/bench_vs_talib.py --sizes 10000 100000 # default: 10k, 100k, 1M
If ta-lib is not installed, the script still runs and reports ferro_ta timings only (no speedup).
Methodology: same synthetic data, same parameters, median of 7 runs after warmup.
Environment: document Python version and OS when publishing results.
""" """
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from datetime import datetime, timezone
import json import json
import platform import math
import subprocess
import sys import sys
import time import time
import tracemalloc
from typing import Any from typing import Any
import numpy as np import numpy as np
try: try:
import talib # noqa: F401 import talib # noqa: F401
TALIB_AVAILABLE = True TALIB_AVAILABLE = True
except ImportError: except ImportError:
TALIB_AVAILABLE = False TALIB_AVAILABLE = False
@@ -39,6 +36,11 @@ except ImportError:
import ferro_ta import ferro_ta
try:
from benchmarks.metadata import benchmark_metadata, package_versions
except ModuleNotFoundError: # pragma: no cover - script execution fallback
from metadata import benchmark_metadata, package_versions
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Configuration # Configuration
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -46,100 +48,138 @@ import ferro_ta
N_WARMUP = 1 N_WARMUP = 1
N_RUNS = 7 N_RUNS = 7
DEFAULT_SIZES = [10_000, 100_000, 1_000_000] DEFAULT_SIZES = [10_000, 100_000, 1_000_000]
TIE_EPSILON = 0.05
_rng = np.random.default_rng(42) _rng = np.random.default_rng(42)
def _git_info() -> dict[str, Any]: def _median(values: list[float]) -> float:
"""Best-effort git metadata for benchmark reproducibility.""" ordered = sorted(values)
try: mid = len(ordered) // 2
commit = subprocess.check_output( if len(ordered) % 2:
["git", "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL return ordered[mid]
).strip() return (ordered[mid - 1] + ordered[mid]) / 2.0
except Exception:
commit = None
try:
dirty = bool(
subprocess.check_output(
["git", "status", "--porcelain"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
)
except Exception:
dirty = None
return {"commit": commit, "dirty": dirty}
def _runtime_info() -> dict[str, Any]: def _summary_stats(samples_ms: list[float]) -> dict[str, float]:
if not samples_ms:
return {
"median_ms": 0.0,
"mean_ms": 0.0,
"min_ms": 0.0,
"max_ms": 0.0,
"stddev_ms": 0.0,
"cv_pct": 0.0,
}
mean_ms = sum(samples_ms) / len(samples_ms)
variance = (
sum((sample - mean_ms) ** 2 for sample in samples_ms) / (len(samples_ms) - 1)
if len(samples_ms) > 1
else 0.0
)
stddev_ms = math.sqrt(variance)
cv_pct = (stddev_ms / mean_ms * 100.0) if mean_ms else 0.0
return { return {
"generated_at_utc": datetime.now(timezone.utc).isoformat(), "median_ms": round(_median(samples_ms), 4),
"python_version": sys.version.split()[0], "mean_ms": round(mean_ms, 4),
"platform": platform.platform(), "min_ms": round(min(samples_ms), 4),
"machine": platform.machine(), "max_ms": round(max(samples_ms), 4),
"stddev_ms": round(stddev_ms, 4),
"cv_pct": round(cv_pct, 3),
} }
def _outcome(speedup: float) -> str:
if speedup > 1.0 + TIE_EPSILON:
return "ferro_ta_win"
if speedup < 1.0 - TIE_EPSILON:
return "talib_win"
return "tie"
def _summary_for_size(results: list[dict[str, Any]], size: int) -> dict[str, Any]: def _summary_for_size(results: list[dict[str, Any]], size: int) -> dict[str, Any]:
rows = [r for r in results if r.get("size") == size and "speedup" in r] rows = [row for row in results if row.get("size") == size and "speedup" in row]
if not rows: if not rows:
return {"size": size, "rows": 0} return {"size": size, "rows": 0}
speedups = [float(r["speedup"]) for r in rows] speedups = [float(row["speedup"]) for row in rows]
wins = sum(1 for s in speedups if s > 1.0) wins = sum(1 for row in rows if row.get("outcome") == "ferro_ta_win")
speedups_sorted = sorted(speedups) ties = sum(1 for row in rows if row.get("outcome") == "tie")
mid = len(speedups_sorted) // 2 losses = sum(1 for row in rows if row.get("outcome") == "talib_win")
if len(speedups_sorted) % 2:
median = speedups_sorted[mid]
else:
median = (speedups_sorted[mid - 1] + speedups_sorted[mid]) / 2.0
return { return {
"size": size, "size": size,
"rows": len(rows), "rows": len(rows),
"wins": wins, "wins": wins,
"win_rate": wins / len(rows), "ties": ties,
"median_speedup": round(median, 4), "losses": losses,
"win_rate": round(wins / len(rows), 4),
"non_loss_rate": round((wins + ties) / len(rows), 4),
"median_speedup": round(_median(speedups), 4),
"min_speedup": round(min(speedups), 4), "min_speedup": round(min(speedups), 4),
"max_speedup": round(max(speedups), 4), "max_speedup": round(max(speedups), 4),
"talib_wins_or_ties": [
row["indicator"]
for row in rows
if row.get("outcome") in {"talib_win", "tie"}
],
} }
def _synthetic_ohlcv(n: int) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: def _synthetic_ohlcv(
# Generate OHLCV so that ta crate DataItem constraints hold: low >= 0, volume >= 0, n: int,
# and low <= open, close <= high, high >= open (see ta DataItemBuilder::build). ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
# Generate OHLCV so that ta crate DataItem constraints hold: low >= 0,
# volume >= 0, and low <= open, close <= high, high >= open.
close = 100.0 + np.cumsum(_rng.standard_normal(n) * 0.5) close = 100.0 + np.cumsum(_rng.standard_normal(n) * 0.5)
open_ = close + _rng.standard_normal(n) * 0.2 open_ = close + _rng.standard_normal(n) * 0.2
high = np.maximum(open_, close) + np.abs(_rng.standard_normal(n) * 0.3) high = np.maximum(open_, close) + np.abs(_rng.standard_normal(n) * 0.3)
low = np.minimum(open_, close) - np.abs(_rng.standard_normal(n) * 0.3) low = np.minimum(open_, close) - np.abs(_rng.standard_normal(n) * 0.3)
# Enforce high >= low and low >= 0 (ta requires non-negative prices)
high = np.maximum(high, low) high = np.maximum(high, low)
low = np.maximum(low, 0.0) low = np.maximum(low, 0.0)
high = np.maximum(high, low) # again after clamping low high = np.maximum(high, low)
open_ = np.clip(open_, low, high) open_ = np.clip(open_, low, high)
close = np.clip(close, low, high) close = np.clip(close, low, high)
volume = np.abs(_rng.standard_normal(n) * 1_000_000) + 500_000 volume = np.abs(_rng.standard_normal(n) * 1_000_000) + 500_000
return open_, high, low, close, volume return open_, high, low, close, volume
def _median_time_ms(fn, *args, **kwargs) -> float: def _timed_runs_ms(fn, *args, **kwargs) -> list[float]:
for _ in range(N_WARMUP): for _ in range(N_WARMUP):
fn(*args, **kwargs) fn(*args, **kwargs)
times = []
samples_ms: list[float] = []
for _ in range(N_RUNS): for _ in range(N_RUNS):
t0 = time.perf_counter() t0 = time.perf_counter()
fn(*args, **kwargs) fn(*args, **kwargs)
times.append((time.perf_counter() - t0) * 1000) samples_ms.append((time.perf_counter() - t0) * 1000.0)
times.sort() return samples_ms
return times[len(times) // 2]
def _python_peak_bytes(fn, *args, **kwargs) -> int | None:
try:
tracemalloc.start()
tracemalloc.reset_peak()
fn(*args, **kwargs)
_, peak = tracemalloc.get_traced_memory()
return int(peak)
except Exception:
return None
finally:
tracemalloc.stop()
def _throughput_m_bars_s(size: int, median_ms: float) -> float:
if median_ms <= 0:
return 0.0
return (size / 1e6) / (median_ms / 1000.0)
# ---------------------------------------------------------------------------
# Benchmarked callables
# ---------------------------------------------------------------------------
# Each entry: (label, ferro_ta_callable, talib_callable, needs_ohlcv)
# ferro_ta_callable / talib_callable receive (open_, high, low, close, volume) and size;
# they return (args, ft_kwargs, ta_kwargs) or we use a simpler convention:
# we pass (o, h, l, c, v) and size; each runner knows how to slice and call.
def _run_ft_sma(o, h, l, c, v, n): def _run_ft_sma(o, h, l, c, v, n):
return ferro_ta.SMA(c[:n], timeperiod=14) return ferro_ta.SMA(c[:n], timeperiod=14)
@@ -236,7 +276,6 @@ def _run_ta_wma(o, h, l, c, v, n):
return talib.WMA(c[:n], timeperiod=14) return talib.WMA(c[:n], timeperiod=14)
# List of (indicator_name, ft_runner, ta_runner); skip 1M for very slow indicators if needed
COMPARISON_CASES = [ COMPARISON_CASES = [
("SMA", _run_ft_sma, _run_ta_sma), ("SMA", _run_ft_sma, _run_ta_sma),
("EMA", _run_ft_ema, _run_ta_ema), ("EMA", _run_ft_ema, _run_ta_ema),
@@ -252,14 +291,14 @@ COMPARISON_CASES = [
("WMA", _run_ft_wma, _run_ta_wma), ("WMA", _run_ft_wma, _run_ta_wma),
] ]
# For STOCH/ADX and other heavier indicators, optionally skip 1M to keep runtime reasonable
SKIP_1M_FOR = {"STOCH", "ADX"} SKIP_1M_FOR = {"STOCH", "ADX"}
def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, Any]]: def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, Any]]:
max_size = max(sizes) max_size = max(sizes)
open_, high, low, close, volume = _synthetic_ohlcv(max_size) open_, high, low, close, volume = _synthetic_ohlcv(max_size)
results = [] results: list[dict[str, Any]] = []
col_label = 10 col_label = 10
col_size = 10 col_size = 10
col_ft_ms = 12 col_ft_ms = 12
@@ -269,12 +308,13 @@ def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, An
col_ta_m = 12 col_ta_m = 12
if not TALIB_AVAILABLE: if not TALIB_AVAILABLE:
print("Note: ta-lib not installed — reporting ferro_ta timings only (no speedup).") 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} runs (after {N_WARMUP} warmup)") print(f"\nferro_ta vs TA-Lib — median of {N_RUNS} measured runs after {N_WARMUP} warmup")
print(f"Sizes: {sizes}") print(f"Sizes: {sizes}")
print() print()
header = ( header = (
f"{'Indicator':<{col_label}} {'Size':<{col_size}} " f"{'Indicator':<{col_label}} {'Size':<{col_size}} "
f"{'ferro_ta(ms)':<{col_ft_ms}} {'TA-Lib(ms)':<{col_ta_ms}} " f"{'ferro_ta(ms)':<{col_ft_ms}} {'TA-Lib(ms)':<{col_ta_ms}} "
@@ -287,82 +327,140 @@ def run_comparison(sizes: list[int], json_path: str | None) -> list[dict[str, An
for size in sizes: for size in sizes:
if size == 1_000_000 and name in SKIP_1M_FOR: if size == 1_000_000 and name in SKIP_1M_FOR:
continue continue
ms_ft = _median_time_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)
row: dict[str, Any] = {
"indicator": name,
"size": size,
"input_layout": {
"dtype": "float64",
"contiguous": True,
},
"ferro_ta_ms": round(ft_median_ms, 4),
"ferro_ta_m_bars_s": round(ft_m_bars_s, 2),
"ferro_ta_runs_ms": [round(sample, 4) for sample in ft_samples_ms],
"ferro_ta_stats": ft_stats,
"python_peak_allocation_bytes": {
"ferro_ta": ft_peak_bytes,
},
}
if TALIB_AVAILABLE: if TALIB_AVAILABLE:
ms_ta = _median_time_ms(ta_run, open_, high, low, close, volume, size) ta_samples_ms = _timed_runs_ms(ta_run, open_, high, low, close, volume, size)
speedup = ms_ta / ms_ft if ms_ft > 0 else float("inf") ta_stats = _summary_stats(ta_samples_ms)
m_bars_ft = (size / 1e6) / (ms_ft / 1000) if ms_ft > 0 else 0 ta_median_ms = float(ta_stats["median_ms"])
m_bars_ta = (size / 1e6) / (ms_ta / 1000) if ms_ta > 0 else 0 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")
outcome = _outcome(speedup)
ta_peak_bytes = _python_peak_bytes(
ta_run, open_, high, low, close, volume, size
)
print( print(
f"{name:<{col_label}} {size:<{col_size}} " f"{name:<{col_label}} {size:<{col_size}} "
f"{ms_ft:<{col_ft_ms}.3f} {ms_ta:<{col_ta_ms}.3f} " f"{ft_median_ms:<{col_ft_ms}.3f} {ta_median_ms:<{col_ta_ms}.3f} "
f"{speedup:<{col_speedup}.2f}x {m_bars_ft:<{col_ft_m}.1f} {m_bars_ta:<{col_ta_m}.1f}" f"{speedup:<{col_speedup}.2f}x {ft_m_bars_s:<{col_ft_m}.1f} {ta_m_bars_s:<{col_ta_m}.1f}"
) )
row = {
"indicator": name, row.update(
"size": size, {
"ferro_ta_ms": round(ms_ft, 4), "talib_ms": round(ta_median_ms, 4),
"talib_ms": round(ms_ta, 4), "talib_m_bars_s": round(ta_m_bars_s, 2),
"speedup": round(speedup, 4), "talib_runs_ms": [round(sample, 4) for sample in ta_samples_ms],
"ferro_ta_m_bars_s": round(m_bars_ft, 2), "talib_stats": ta_stats,
"talib_m_bars_s": round(m_bars_ta, 2), "speedup": round(speedup, 4),
} "outcome": outcome,
}
)
row["python_peak_allocation_bytes"]["talib"] = ta_peak_bytes
else: else:
m_bars_ft = (size / 1e6) / (ms_ft / 1000) if ms_ft > 0 else 0
print( print(
f"{name:<{col_label}} {size:<{col_size}} " f"{name:<{col_label}} {size:<{col_size}} "
f"{ms_ft:<{col_ft_ms}.3f} {'N/A':<{col_ta_ms}} " f"{ft_median_ms:<{col_ft_ms}.3f} {'N/A':<{col_ta_ms}} "
f"{'N/A':<{col_speedup}} {m_bars_ft:<{col_ft_m}.1f} {'N/A':<{col_ta_m}}" f"{'N/A':<{col_speedup}} {ft_m_bars_s:<{col_ft_m}.1f} {'N/A':<{col_ta_m}}"
) )
row = {
"indicator": name,
"size": size,
"ferro_ta_ms": round(ms_ft, 4),
"ferro_ta_m_bars_s": round(m_bars_ft, 2),
}
results.append(row) results.append(row)
print() print()
if TALIB_AVAILABLE and results: if TALIB_AVAILABLE and results:
wins = sum(1 for r in results if r.get("speedup", 0) > 1) wins = sum(1 for row in results if row.get("outcome") == "ferro_ta_win")
total = len(results) total = len([row for row in results if "speedup" in row])
print(f"Summary: ferro_ta faster on {wins}/{total} rows (speedup > 1).") print(f"Summary: ferro_ta ahead outside the tie band on {wins}/{total} rows.")
print() print()
if json_path: if json_path:
metadata = benchmark_metadata(
"benchmark_vs_talib",
extra={
"dataset": {
"generator": "synthetic_ohlcv",
"sizes": sizes,
"dtype": "float64",
"array_layout": "C-contiguous",
"seed": 42,
},
"methodology": {
"warmup_runs": N_WARMUP,
"measured_runs": N_RUNS,
"reported_metric": "median_ms",
"speedup_definition": "talib_median_ms / ferro_ta_median_ms",
"tie_band": f"{1.0 - TIE_EPSILON:.2f} to {1.0 + TIE_EPSILON:.2f}",
"input_layout_notes": (
"Benchmarks use contiguous float64 arrays. If your workload "
"passes non-contiguous arrays or other dtypes, benchmark that "
"separately because wrapper overhead can dominate."
),
"allocation_notes": (
"python_peak_allocation_bytes is a tracemalloc snapshot of "
"Python-tracked allocations only; it is not a full native RSS "
"or allocator profile."
),
},
"packages": package_versions("numpy", "ferro-ta", "TA-Lib"),
},
)
out = { out = {
"schema_version": 1, "schema_version": 2,
"command": "python benchmarks/bench_vs_talib.py", "command": " ".join(["python", *sys.argv]),
"n_warmup": N_WARMUP, "n_warmup": N_WARMUP,
"n_runs": N_RUNS, "n_runs": N_RUNS,
"sizes": sizes, "sizes": sizes,
"talib_available": TALIB_AVAILABLE, "talib_available": TALIB_AVAILABLE,
"runtime": _runtime_info(), "runtime": metadata["runtime"],
"git": _git_info(), "git": metadata["git"],
"metadata": metadata,
"summary": { "summary": {
"total_rows": len(results), "total_rows": len(results),
"by_size": [_summary_for_size(results, s) for s in sizes], "by_size": [_summary_for_size(results, size) for size in sizes],
}, },
"results": results, "results": results,
} }
if not TALIB_AVAILABLE: if not TALIB_AVAILABLE:
out["note"] = "ferro_ta only ta-lib not installed" out["note"] = "ferro_ta only; ta-lib not installed"
with open(json_path, "w") as f: with open(json_path, "w", encoding="utf-8") as handle:
json.dump(out, f, indent=2) json.dump(out, handle, indent=2)
print(f"Results written to {json_path}") print(f"Results written to {json_path}")
return results return results
def main() -> int: def main() -> int:
ap = argparse.ArgumentParser(description="ferro_ta vs TA-Lib speed comparison") parser = argparse.ArgumentParser(description="ferro_ta vs TA-Lib speed comparison")
ap.add_argument("--json", default=None, help="Write results to JSON file") parser.add_argument("--json", default=None, help="Write results to JSON file")
ap.add_argument( parser.add_argument(
"--sizes", "--sizes",
type=int, type=int,
nargs="+", nargs="+",
default=DEFAULT_SIZES, default=DEFAULT_SIZES,
help="Bar counts to benchmark (default: 10000 100000 1000000)", help="Bar counts to benchmark (default: 10000 100000 1000000)",
) )
args = ap.parse_args() args = parser.parse_args()
run_comparison(args.sizes, args.json) run_comparison(args.sizes, args.json)
return 0 return 0
+135 -22
View File
@@ -1,56 +1,167 @@
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import os
import platform import platform
import re
import subprocess import subprocess
import sys import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from importlib import metadata as importlib_metadata
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
try:
def git_info() -> dict[str, Any]: import tomllib
"""Best-effort git metadata for reproducible benchmark artifacts.""" except ImportError: # pragma: no cover
try: try:
commit = subprocess.check_output( import tomli as tomllib # type: ignore[no-redef]
["git", "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL except ImportError: # pragma: no cover
).strip() tomllib = None # type: ignore[assignment]
except Exception:
commit = None
try:
dirty = bool(
subprocess.check_output(
["git", "status", "--porcelain"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
)
except Exception:
dirty = None
_ROOT = Path(__file__).resolve().parent.parent
def _run_cmd(command: list[str]) -> str | None:
try: try:
branch = subprocess.check_output( return subprocess.check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"], command,
text=True, text=True,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
).strip() ).strip()
except Exception: except Exception:
branch = None return None
return {"commit": commit, "dirty": dirty, "branch": branch}
def _read_toml(path: Path) -> dict[str, Any] | None:
if tomllib is None or not path.exists():
return None
try:
with path.open("rb") as handle:
return tomllib.load(handle)
except Exception:
return None
def _cpu_model() -> str | None:
if sys.platform == "darwin":
return (
_run_cmd(["sysctl", "-n", "machdep.cpu.brand_string"])
or _run_cmd(["sysctl", "-n", "hw.model"])
or platform.processor()
or None
)
if sys.platform.startswith("linux"):
cpuinfo = Path("/proc/cpuinfo")
if cpuinfo.exists():
text = cpuinfo.read_text(encoding="utf-8", errors="ignore")
for pattern in (r"model name\s+:\s+(.+)", r"Hardware\s+:\s+(.+)"):
match = re.search(pattern, text)
if match:
return match.group(1).strip()
return platform.processor() or None
if sys.platform.startswith("win"):
return os.environ.get("PROCESSOR_IDENTIFIER") or platform.processor() or None
return platform.processor() or None
def _total_memory_bytes() -> int | None:
if sys.platform == "darwin":
raw = _run_cmd(["sysctl", "-n", "hw.memsize"])
return int(raw) if raw and raw.isdigit() else None
if sys.platform.startswith("linux"):
meminfo = Path("/proc/meminfo")
if meminfo.exists():
text = meminfo.read_text(encoding="utf-8", errors="ignore")
match = re.search(r"MemTotal:\s+(\d+)\s+kB", text)
if match:
return int(match.group(1)) * 1024
return None
if sys.platform.startswith("win"): # pragma: no cover
try:
import ctypes
class MEMORYSTATUSEX(ctypes.Structure):
_fields_ = [
("dwLength", ctypes.c_ulong),
("dwMemoryLoad", ctypes.c_ulong),
("ullTotalPhys", ctypes.c_ulonglong),
("ullAvailPhys", ctypes.c_ulonglong),
("ullTotalPageFile", ctypes.c_ulonglong),
("ullAvailPageFile", ctypes.c_ulonglong),
("ullTotalVirtual", ctypes.c_ulonglong),
("ullAvailVirtual", ctypes.c_ulonglong),
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
]
status = MEMORYSTATUSEX()
status.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status))
return int(status.ullTotalPhys)
except Exception:
return None
return None
def _cargo_release_profile() -> dict[str, Any] | None:
cargo_toml = _read_toml(_ROOT / "Cargo.toml")
if not cargo_toml:
return None
profile = cargo_toml.get("profile", {}).get("release")
return profile if isinstance(profile, dict) else None
def git_info() -> dict[str, Any]:
"""Best-effort git metadata for reproducible benchmark artifacts."""
return {
"commit": _run_cmd(["git", "rev-parse", "HEAD"]),
"dirty": bool(_run_cmd(["git", "status", "--porcelain"]) or ""),
"branch": _run_cmd(["git", "rev-parse", "--abbrev-ref", "HEAD"]),
}
def runtime_info() -> dict[str, Any]: def runtime_info() -> dict[str, Any]:
return { return {
"generated_at_utc": datetime.now(timezone.utc).isoformat(), "generated_at_utc": datetime.now(timezone.utc).isoformat(),
"python_version": sys.version.split()[0], "python_version": sys.version.split()[0],
"python_implementation": platform.python_implementation(),
"python_executable": sys.executable,
"platform": platform.platform(), "platform": platform.platform(),
"system": platform.system(),
"release": platform.release(),
"machine": platform.machine(), "machine": platform.machine(),
"processor": platform.processor() or None, "processor": platform.processor() or None,
"cpu_model": _cpu_model(),
"cpu_count_logical": os.cpu_count(),
"total_memory_bytes": _total_memory_bytes(),
} }
def build_info() -> dict[str, Any]:
return {
"rustc": _run_cmd(["rustc", "-Vv"]),
"cargo": _run_cmd(["cargo", "-VV"]) or _run_cmd(["cargo", "-V"]),
"cargo_release_profile": _cargo_release_profile(),
"rustflags": os.environ.get("RUSTFLAGS"),
"cargo_build_rustflags": os.environ.get("CARGO_BUILD_RUSTFLAGS"),
"maturin_flags": os.environ.get("MATURIN_EXTRA_ARGS"),
}
def package_versions(*names: str) -> dict[str, str | None]:
versions: dict[str, str | None] = {}
for name in names:
try:
versions[name] = importlib_metadata.version(name)
except importlib_metadata.PackageNotFoundError:
versions[name] = None
return versions
def file_info(path: str | Path) -> dict[str, Any]: def file_info(path: str | Path) -> dict[str, Any]:
file_path = Path(path) file_path = Path(path)
data = file_path.read_bytes() data = file_path.read_bytes()
@@ -71,6 +182,8 @@ def benchmark_metadata(
"suite": suite, "suite": suite,
"runtime": runtime_info(), "runtime": runtime_info(),
"git": git_info(), "git": git_info(),
"build": build_info(),
"packages": package_versions("numpy", "ferro-ta"),
} }
if fixtures: if fixtures:
metadata["fixtures"] = [file_info(path) for path in fixtures] metadata["fixtures"] = [file_info(path) for path in fixtures]
+17 -1
View File
@@ -10,11 +10,27 @@ Run with:
from __future__ import annotations from __future__ import annotations
import importlib.util import importlib.util
import sys
from pathlib import Path
import numpy as np import numpy as np
import pytest 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, ...]: def _sample_chain(n: int = 1000) -> tuple[np.ndarray, ...]:
+6 -5
View File
@@ -1,5 +1,5 @@
{% set name = "ferro-ta" %} {% set name = "ferro-ta" %}
{% set version = "1.0.0" %} {% set version = "1.0.4" %}
package: package:
name: {{ name|lower }} name: {{ name|lower }}
@@ -39,11 +39,12 @@ about:
home: https://github.com/pratikbhadane24/ferro-ta home: https://github.com/pratikbhadane24/ferro-ta
license: MIT license: MIT
license_family: MIT license_family: MIT
summary: A fast Technical Analysis library TA-Lib alternative powered by Rust and PyO3 summary: Rust-powered Python technical analysis library with a TA-Lib-compatible API
description: | description: |
ferro-ta is a drop-in TA-Lib alternative with pre-compiled wheels for all ferro-ta is a Rust-powered Python technical analysis library with a
major platforms. It provides 155+ indicators via a Rust core and PyO3 TA-Lib-compatible API and pre-compiled wheels for the supported platforms.
bindings, with optional pandas / streaming APIs. It provides 155+ indicators via a Rust core and PyO3 bindings, with
optional pandas and streaming APIs.
doc_url: https://github.com/pratikbhadane24/ferro-ta doc_url: https://github.com/pratikbhadane24/ferro-ta
dev_url: https://github.com/pratikbhadane24/ferro-ta dev_url: https://github.com/pratikbhadane24/ferro-ta
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ferro_ta_core" name = "ferro_ta_core"
version = "1.0.2" version = "1.0.4"
edition = "2021" edition = "2021"
description = "Pure Rust core indicator library — no PyO3, no numpy dependency" description = "Pure Rust core indicator library — no PyO3, no numpy dependency"
license = "MIT" license = "MIT"
+1 -1
View File
@@ -13,7 +13,7 @@ PyO3, NumPy, or Python runtime dependency, which makes it a good fit for:
```toml ```toml
[dependencies] [dependencies]
ferro_ta_core = "1.0.2" ferro_ta_core = "1.0.4"
``` ```
## Design ## Design
+48
View File
@@ -0,0 +1,48 @@
Adjacent Tooling
================
These modules are useful, but they are secondary to ferro-ta's core identity as
a Python technical analysis library.
.. list-table::
:header-rows: 1
* - Area
- Status
- What it is
* - Derivatives analytics
- Adjacent
- Options pricing, Greeks, implied volatility helpers, futures basis,
curve, and roll utilities. See :doc:`derivatives`.
* - Agent workflow wrappers
- Adjacent
- Tool and workflow helpers for agent-style integrations. See
`docs/agentic.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/agentic.md>`_.
* - MCP server
- Experimental or adjacent
- 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
- Browser and Node.js package with a smaller indicator subset. See
`wasm/README.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/wasm/README.md>`_.
* - GPU backend
- Experimental
- Optional PyTorch-backed acceleration for a limited subset of indicators.
See `docs/gpu-backend.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/gpu-backend.md>`_.
* - Plugin system
- Experimental
- Registry and plugin packaging model for custom indicators. See
:doc:`plugins`.
How to read the project
-----------------------
When evaluating ferro-ta:
- Start with the core library docs, migration guide, support matrix, and benchmarks.
- Treat adjacent tooling as opt-in layers, not as proof that the core indicator
library is broader or more stable than it is.
- Check the release notes and stability policy before depending on experimental
surfaces in production.
+1 -1
View File
@@ -198,6 +198,6 @@ while True:
- `ferro_ta.tools` — module source. - `ferro_ta.tools` — module source.
- `ferro_ta.workflow` — 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.backtest` — backtest harness.
- `ferro_ta.registry` — indicator registry. - `ferro_ta.registry` — indicator registry.
+122 -21
View File
@@ -1,20 +1,130 @@
Benchmarks Benchmarks
========== ==========
The authoritative benchmark workflow is in ``benchmarks/``: The benchmark suite is meant to support a narrow claim: ferro-ta is often
faster on selected indicators, and the evidence is published in a reproducible
form.
What is published
-----------------
The authoritative benchmark workflow lives in ``benchmarks/``:
- Cross-library speed suite: ``benchmarks/test_speed.py`` - Cross-library speed suite: ``benchmarks/test_speed.py``
- Cross-library accuracy suite: ``benchmarks/test_accuracy.py`` - Cross-library accuracy suite: ``benchmarks/test_accuracy.py``
- TA-Lib head-to-head speed script: ``benchmarks/bench_vs_talib.py`` - TA-Lib head-to-head script: ``benchmarks/bench_vs_talib.py``
- Table generation from benchmark JSON: ``benchmarks/benchmark_table.py`` - Table generation from benchmark JSON: ``benchmarks/benchmark_table.py``
- Perf-contract artifact bundle: ``benchmarks/run_perf_contract.py``
Run the cross-library speed suite on 100,000 bars: Latest checked-in TA-Lib artifact
---------------------------------
The current checked-in TA-Lib comparison artifact benchmarks contiguous
``float64`` arrays at 10k and 100k bars on an ``Apple M3 Max`` with 14 logical
cores, about 38.7 GB RAM, ``CPython 3.13.5``, and ``Rust 1.91.1`` using the
default release profile (``lto = true``, ``codegen-units = 1``).
Summary from ``benchmarks/artifacts/latest/benchmark_vs_talib.json``:
.. list-table::
:header-rows: 1
* - Size
- Rows
- ferro-ta wins
- Median speedup
- TA-Lib wins or ties
* - ``10,000``
- 12
- 6
- ``1.0850x``
- ``EMA``, ``RSI``, ``ATR``, ``STOCH``, ``ADX``, ``OBV``
* - ``100,000``
- 12
- 6
- ``1.0784x``
- ``EMA``, ``RSI``, ``ATR``, ``STOCH``, ``ADX``, ``OBV``
Examples from the 100k-bar run:
.. list-table::
:header-rows: 1
* - Indicator
- ferro-ta
- TA-Lib
- Speedup
- Read
* - ``SMA``
- ``0.0985 ms``
- ``0.2241 ms``
- ``2.2751x``
- clear ferro-ta win
* - ``BBANDS``
- ``0.2122 ms``
- ``0.4966 ms``
- ``2.3402x``
- clear ferro-ta win
* - ``MACD``
- ``0.5152 ms``
- ``0.7111 ms``
- ``1.3801x``
- ferro-ta win
* - ``STOCH``
- ``1.7064 ms``
- ``0.7603 ms``
- ``0.4455x``
- TA-Lib win
* - ``ADX``
- ``0.7910 ms``
- ``0.5769 ms``
- ``0.7294x``
- TA-Lib win
* - ``ATR``
- ``0.5087 ms``
- ``0.5147 ms``
- ``1.0118x``
- tie on this machine
Methodology notes
-----------------
- The head-to-head script uses the same synthetic OHLCV generator, the same
parameters, and the same contiguous ``float64`` array layout for both
libraries.
- Reported speedup is ``TA-Lib median time / ferro-ta median time``.
- The script uses 1 warmup run and 7 measured runs per case, and now records
the full per-run timing samples, not just one selected number.
- Published JSON artifacts include machine/runtime metadata, git metadata, Rust
toolchain and build-profile metadata, per-run variance statistics, and
Python-tracked peak allocation snapshots.
- Allocation snapshots are based on ``tracemalloc`` and capture Python-tracked
allocations only; they are not full native RSS profiles.
- If your workload uses non-contiguous arrays, different dtypes, or different
batch sizes, benchmark that exact workload. Those factors can materially
change the result.
Reproduce the TA-Lib comparison
-------------------------------
.. code-block:: bash
pip install ta-lib
python benchmarks/bench_vs_talib.py --sizes 10000 100000 --json benchmark_vs_talib.json
The JSON output is the main artifact to review when publishing performance
claims.
Cross-library suite
-------------------
Run the broader speed suite on 100,000 bars:
.. code-block:: bash .. code-block:: bash
uv run pytest benchmarks/test_speed.py --benchmark-only --benchmark-json=benchmarks/results.json -v uv run pytest benchmarks/test_speed.py --benchmark-only --benchmark-json=benchmarks/results.json -v
Selected results on a modern CPU (100,000 bars): Selected throughput examples from the checked-in table:
.. list-table:: .. list-table::
:header-rows: 1 :header-rows: 1
@@ -38,25 +148,16 @@ Selected results on a modern CPU (100,000 bars):
* - ``STOCH`` * - ``STOCH``
- 33 M bars/s - 33 M bars/s
Multi-size and JSON output Perf-contract artifacts
-------------------------- -----------------------
To build the markdown comparison table from the JSON output: Use the perf-contract runner when you want a compact, machine-readable artifact
bundle for single-series latency, batch throughput, streaming throughput, and
hotspot attribution:
.. code-block:: bash .. code-block:: bash
uv run python benchmarks/benchmark_table.py uv run python benchmarks/run_perf_contract.py --output-dir benchmarks/artifacts/latest
Comparison with TA-Lib See ``benchmarks/README.md`` for the detailed benchmark playbook and the
---------------------- checked-in comparison tables.
To measure speedup vs TA-Lib on the same data and parameters, run:
.. code-block:: bash
pip install ta-lib
python benchmarks/bench_vs_talib.py --sizes 10000 100000 --json benchmark_vs_talib.json
See the README “Performance vs TA-Lib” section for methodology and a
representative comparison table. The script prints a table of median times and
speedup (TA-Lib time / ferro_ta time); use ``--json out.json`` to save results.
+45 -43
View File
@@ -1,55 +1,57 @@
Changelog Release Notes
========= =============
1.0.0 (2026) These docs track package version ``1.0.4``.
------------
**Candlestick Pattern Parity (61/61)** 1.0.4 (2026-03-24)
------------------
- All 61 TA-Lib candlestick patterns implemented in Rust - Expanded the optional MCP server from a small hand-written subset to the
- ``{-100, 0, 100}`` convention, consistent with TA-Lib 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.
**Numerical Parity** 1.0.3 (2026-03-24)
------------------
- RSI, ATR/NATR, CCI, BETA, STOCH, STOCHRSI, ADX/DX/DI/DM all rewritten to match TA-Lib seeding - Added top-level package metadata helpers such as ``ferro_ta.__version__``,
- Removed dependency on ``ta`` crate for these indicators ``ferro_ta.about()``, and ``ferro_ta.methods()``.
- Added a standalone derivatives benchmark artifact for selected options
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.
**Streaming / Incremental API** 1.0.2 (2026-03-24)
------------------
- New :mod:`ferro_ta.streaming` module with bar-by-bar stateful classes - Improved rolling statistical kernels and several Python analysis hotspots.
- ``StreamingSMA``, ``StreamingEMA``, ``StreamingRSI``, ``StreamingATR``, ``StreamingBBands``, ``StreamingMACD``, ``StreamingStoch``, ``StreamingVWAP``, ``StreamingSupertrend`` - Added reproducible perf-contract artifacts, TA-Lib regression guards, and
updated benchmark tooling.
- Tightened the public benchmark documentation so claims, caveats, and evidence
live closer together.
**Pandas Integration** 1.0.1 (2026-03-24)
------------------
- All indicators transparently accept ``pandas.Series`` and return ``Series`` with original index preserved - Improved release automation for PyPI, crates.io, and npm.
- Multi-output functions return tuples of ``Series`` - Fixed CI workflow issues that caused otherwise healthy release jobs to fail.
- Ensured the published WASM package includes its built ``pkg/`` artifacts.
**Math Operators / Transforms** 1.0.0 (2026-03-23)
------------------
- 24 functions: arithmetic (ADD/SUB/MULT/DIV), rolling (SUM/MAX/MIN/MAXINDEX/MININDEX), element-wise math transforms - First stable release of the Rust-backed Python technical analysis library.
- SUM uses vectorized cumsum (220× faster than a naive loop) - Shipped broad TA-Lib coverage, streaming APIs, extended indicators, and the
initial Sphinx documentation set.
- Added the benchmark suite, release playbook, and compatibility/testing
scaffolding for stable releases.
**Documentation** For the canonical project changelog, including the full per-version details,
see `CHANGELOG.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/CHANGELOG.md>`_.
- Sphinx documentation setup with API reference, quickstart guide, and benchmarks page
**Benchmarking Suite**
- ``benchmarks/test_speed.py`` for authoritative ``pytest-benchmark`` speed runs
- ``benchmarks/bench_vs_talib.py`` for TA-Lib head-to-head comparisons
**Extended Indicators**
- ``VWAP`` — cumulative or rolling window
- ``SUPERTREND`` — ATR-based trend signal
**Additional Extended Indicators**
- ``ICHIMOKU`` — Ichimoku Cloud (Tenkan, Kijun, Senkou A/B, Chikou)
- ``DONCHIAN`` — Donchian Channels (upper, middle, lower)
- ``PIVOT_POINTS`` — Classic, Fibonacci, and Camarilla pivot points
**Type Stubs & Packaging**
- ``python/ferro_ta/__init__.pyi`` type stub for IDE auto-completion
- ``pyproject.toml``: added optional extras (benchmark, pandas, docs, all), project URLs, Python 3.103.13 classifiers
+32 -2
View File
@@ -4,7 +4,17 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html # https://www.sphinx-doc.org/en/master/usage/configuration.html
import os import os
import re
import sys import sys
from pathlib import Path
try:
import tomllib
except ImportError: # pragma: no cover
try:
import tomli as tomllib # type: ignore[no-redef]
except ImportError: # pragma: no cover
tomllib = None # type: ignore[assignment]
# Add the python source directory so autodoc can import ferro_ta # Add the python source directory so autodoc can import ferro_ta
# Only add if ferro_ta is not already installed (e.g. from a wheel in CI) # Only add if ferro_ta is not already installed (e.g. from a wheel in CI)
@@ -17,8 +27,28 @@ except ImportError:
project = "ferro-ta" project = "ferro-ta"
copyright = "2024, pratikbhadane24" copyright = "2024, pratikbhadane24"
author = "pratikbhadane24" author = "pratikbhadane24"
# Version from env (e.g. set in CI from git tag) or default
release = os.environ.get("FERRO_TA_VERSION", "1.0.0")
def _default_release() -> str:
if tomllib is None:
return "0+unknown"
pyproject_toml = Path(__file__).resolve().parents[1] / "pyproject.toml"
try:
if tomllib is not None:
with pyproject_toml.open("rb") as handle:
data = tomllib.load(handle)
return data.get("project", {}).get("version", "0+unknown")
text = pyproject_toml.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
if match:
return match.group(1)
return "0+unknown"
except Exception:
return "0+unknown"
# Version from env (e.g. set in CI from git tag) or default to pyproject.toml
release = os.environ.get("FERRO_TA_VERSION", _default_release())
version = release version = release
# -- General configuration ---------------------------------------------------- # -- General configuration ----------------------------------------------------
+39 -16
View File
@@ -3,43 +3,66 @@ ferro-ta Documentation
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
:caption: Contents :caption: Core Library
quickstart quickstart
migration_talib migration_talib
support_matrix
pandas_api pandas_api
error_handling error_handling
api/index api/index
streaming streaming
extended
batch batch
derivatives extended
.. toctree::
:maxdepth: 2
:caption: Evidence and Releases
benchmarks benchmarks
plugins
changelog changelog
.. toctree::
:maxdepth: 2
:caption: Adjacent and Experimental
derivatives
adjacent_tooling
plugins
contributing contributing
Overview Overview
-------- --------
**ferro-ta** is a fast Technical Analysis library — a drop-in alternative to TA-Lib **ferro-ta** is a Rust-powered Python technical analysis library focused on a
powered by Rust and PyO3. TA-Lib-compatible API for NumPy-centered workloads.
Features: .. important::
Performance varies by indicator, array layout, warmup, build flags, and
machine. ferro-ta is often faster on selected indicators, not universally
faster. See :doc:`benchmarks` for the reproducible workflow, methodology
notes, and the indicators where TA-Lib still wins or ties in the current
checked-in artifact.
Core library:
- 160+ indicators covering all TA-Lib categories - 160+ indicators covering all TA-Lib categories
- 10 extended indicators not in TA-Lib (VWAP, Supertrend, Ichimoku Cloud, …) - TA-Lib-style imports such as ``ferro_ta.SMA(close, timeperiod=20)``
- Batch execution API — run indicators on 2-D arrays of multiple series - Pre-built wheels for the supported Python/OS matrix
- Pure Rust core library (``crates/ferro_ta_core``) — no PyO3 / numpy dependency - Pure Rust core library (``crates/ferro_ta_core``) — no PyO3 / numpy dependency
- Batch execution API — run indicators on 2-D arrays of multiple series
- Streaming / bar-by-bar API for live trading - Streaming / bar-by-bar API for live trading
- Transparent pandas.Series support - Transparent pandas.Series support
- Math operators and transforms
- Type stubs (.pyi) for IDE auto-completion - Type stubs (.pyi) for IDE auto-completion
- WASM binding for browser/Node.js use - 10 extended indicators not in TA-Lib (VWAP, Supertrend, Ichimoku Cloud, ...)
- Options/IV helpers and derivatives analytics — see :doc:`derivatives`
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>`_ - 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>`_
- Sphinx documentation - WASM, plugins, and other optional surfaces — see :doc:`adjacent_tooling`
Installation Installation
~~~~~~~~~~~~ ~~~~~~~~~~~~
@@ -70,11 +93,11 @@ Further Reading
- `Architecture <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/architecture.md>`_ — Rust/Python layout, two-crate design, binding flow. - `Architecture <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/architecture.md>`_ — Rust/Python layout, two-crate design, binding flow.
- `Performance Guide <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/performance.md>`_ — when to use raw numpy vs pandas/polars, batch notes, tips. - `Performance Guide <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/performance.md>`_ — when to use raw numpy vs pandas/polars, batch notes, tips.
- `API Stability <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/stability.md>`_ — stability tiers, versioning, and deprecation policy. - `API Stability <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/stability.md>`_ — stability tiers, versioning, and deprecation policy.
- :doc:`support_matrix` — parity status, tested wheel targets, supported Python versions, and experimental modules.
- `Rust-First Policy <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/rust_first.md>`_ — all compute logic belongs in Rust; how to add new indicators. - `Rust-First Policy <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/rust_first.md>`_ — all compute logic belongs in Rust; how to add new indicators.
- `Out-of-Core Execution <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/out-of-core.md>`_ — chunked processing and Dask integration. - `Out-of-Core Execution <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/out-of-core.md>`_ — chunked processing and Dask integration.
- :doc:`derivatives` — IV helpers, options pricing/Greeks/IV, futures analytics, strategy schemas, and payoff helpers. - :doc:`derivatives` — IV helpers, options pricing/Greeks/IV, futures analytics, strategy schemas, and payoff helpers.
- `Agentic Workflow <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/agentic.md>`_ — tools.py, workflow.py, LangChain integration. - :doc:`adjacent_tooling` — optional surfaces such as derivatives, MCP, WASM, GPU, plugins, and agent-oriented integrations.
- `MCP Server <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/mcp.md>`_ — run ferro-ta as an MCP server in Cursor/Claude.
Indices and tables Indices and tables
================== ==================
+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 ferro-ta ships an optional MCP (Model Context Protocol) server built on the
indicators and backtest tools to AI agents. This guide shows how to run official Python SDK's FastMCP layer. The server now exposes the broad public
the server and connect it to Cursor or any MCP-compatible client. 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 ## Installation
The MCP server requires no additional dependencies beyond ferro_ta itself. Install the optional MCP extra:
For the full MCP SDK integration (recommended), install the optional extra:
```bash ```bash
pip install "ferro-ta[mcp]" 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 ```bash
pip install "mcp>=1.0" uv sync --extra mcp
``` ```
--- ---
## Running the server ## Running the server
Run the server over stdio:
```bash ```bash
python -m ferro_ta.mcp 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 ## Connect in Cursor
1. Open Cursor settings (Command Palette → "Open User Settings (JSON)"). Add the server to Cursor's MCP settings:
2. Find or create the `mcpServers` section:
```json ```json
{ {
@@ -44,82 +58,134 @@ The server listens on stdin/stdout using JSON-RPC 2.0 (the MCP protocol).
"ferro-ta": { "ferro-ta": {
"command": "python", "command": "python",
"args": ["-m", "ferro_ta.mcp"], "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"). You can place this in your user settings JSON or in a workspace-level
4. The ferro-ta tools will appear in the Tools panel. `.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 ```json
{ {
"mcpServers": { "instance_id": "tickaggregator-0001",
"ferro-ta": { "type": "ferro_ta.data.aggregation.TickAggregator",
"command": "python", "repr": "TickAggregator(rule='tick:2')"
"args": ["-m", "ferro_ta.mcp"]
}
}
} }
``` ```
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 ## 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 | Use the server entrypoint:
|------|-------------|
| `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:
```python ```python
from ferro_ta.mcp import handle_list_tools, handle_call_tool from ferro_ta.mcp import create_server
import numpy as np
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() tools = handle_list_tools()
print([t["name"] for t in tools["tools"]]) print(len(tools["tools"]))
# Call RSI close = [100, 101, 102, 103, 104]
close = list(np.cumprod(1 + np.random.default_rng(0).normal(0, 0.01, 50)) * 100) result = handle_call_tool("SMA", {"close": close, "timeperiod": 3})
result = handle_call_tool("rsi", {"close": close, "timeperiod": 14}) print(json.loads(result["content"][0]["text"]))
print(result)
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 ## See also
- `ferro_ta.mcp` — module source. - `python -m ferro_ta.mcp` - stdio MCP entrypoint
- `ferro_ta.tools` — underlying tool functions. - `ferro_ta.mcp.create_server()` - FastMCP server factory
- `docs/agentic.md` — LangChain and workflow integration. - `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
+117
View File
@@ -0,0 +1,117 @@
Support Matrix
==============
The primary product is the Python technical analysis library: TA-Lib-style
indicator calls backed by a Rust implementation.
Indicator compatibility
-----------------------
.. list-table::
:header-rows: 1
* - Status
- Scope
- Notes
* - Exact parity
- Common TA-Lib-compatible indicators such as ``SMA``, ``WMA``,
``BBANDS``, ``RSI``, ``ATR``, ``NATR``, ``CCI``, ``STOCH``,
``STOCHRSI``, and most candlestick patterns
- Matches TA-Lib numerically within floating-point tolerance in the
current comparison suite.
* - Approximate parity
- EMA-family indicators (``EMA``, ``DEMA``, ``TEMA``, ``T3``, ``MACD``),
``MAMA`` / ``FAMA``, ``SAR`` / ``SAREXT``, and ``HT_*`` cycle
indicators
- Same API and intended use, with convergence-window or floating-point
differences documented in the migration guide.
* - Intentionally different
- ferro-ta-only indicators such as ``VWAP``, ``SUPERTREND``,
``ICHIMOKU``, ``DONCHIAN``, ``KELTNER_CHANNELS``, ``HULL_MA``,
``CHANDELIER_EXIT``, ``VWMA``, and ``CHOPPINESS_INDEX``
- These extend the library beyond TA-Lib and are not parity claims.
For migration details and known indicator-specific differences, see
:doc:`migration_talib`.
Module status
-------------
.. list-table::
:header-rows: 1
* - Surface
- Status
- Notes
* - Top-level indicators and category submodules
- Stable core
- This is the main supported surface of the project.
* - ``ferro_ta.batch``
- Supported
- Public API is supported; internal dispatch may evolve.
* - ``ferro_ta.streaming``
- Supported, still evolving
- Suitable for live workflows; some API details are still marked
experimental in the stability policy.
* - ``ferro_ta.extended``
- Supported extension
- Useful indicators beyond TA-Lib, but not part of drop-in parity claims.
* - ``ferro_ta.analysis.*``
- Adjacent tooling
- Useful analytics helpers, but not the primary product story.
* - MCP, WASM, GPU, plugin, and agent-oriented tooling
- Experimental or adjacent
- Evaluate these independently from the core indicator library.
Supported Python versions
-------------------------
.. list-table::
:header-rows: 1
* - Python
- Status
* - 3.13
- Supported and tested in CI
* - 3.12
- Supported and tested in CI
* - 3.11
- Supported and tested in CI
* - 3.10
- Supported and tested in CI
* - < 3.10
- Not supported
Tested wheel targets
--------------------
.. list-table::
:header-rows: 1
* - OS
- Architecture
- Wheel status
* - Linux
- ``x86_64`` (manylinux2014 / ``manylinux_2_17``)
- Tested wheel target
* - macOS
- ``universal2``
- Tested wheel target for Intel and Apple Silicon
* - Windows
- ``x86_64``
- Tested wheel target
For source builds, packaging details, and platform notes, see
`PLATFORMS.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/PLATFORMS.md>`_.
Release status
--------------
These docs track package version ``1.0.4``.
- Release notes by version: :doc:`changelog`
- Canonical project changelog: `CHANGELOG.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/CHANGELOG.md>`_
- Stability policy: `docs/stability.md <https://github.com/pratikbhadane24/ferro-ta/blob/main/docs/stability.md>`_
If the package version, docs version, or support matrix disagree, treat that as
a documentation bug.
+71
View File
@@ -0,0 +1,71 @@
{
"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",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"n_samples": 20000,
"n_series": 32,
"total_bars": 640000,
"seed": 42
}
},
"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
},
{
"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
},
{
"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
},
{
"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
}
],
"grouped_results": [
{
"case": "close_bundle_3",
"grouped_ms": 0.1878,
"separate_ms": 0.1719,
"speedup_vs_separate": 0.9155
},
{
"case": "hlc_bundle_3",
"grouped_ms": 0.2958,
"separate_ms": 0.4633,
"speedup_vs_separate": 1.5666
}
]
}
+163
View File
@@ -0,0 +1,163 @@
{
"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",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"fixtures": [
{
"path": "/Users/pratikbhadane/Work/Projects/ferro-ta/benchmarks/fixtures/canonical_ohlcv.npz",
"size_bytes": 75586,
"sha256": "60192f8349fb06cd59ef7f70fd77aa8280399e819d7cc5eed3ca95cf5ee1a89c"
}
],
"dataset": {
"fixture": "/Users/pratikbhadane/Work/Projects/ferro-ta/benchmarks/fixtures/canonical_ohlcv.npz",
"bars": 2000,
"rounds": 5
}
},
"results": [
{
"name": "VAR_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.0202
},
{
"name": "WILLR_14",
"inputs": "hlc",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0183
},
{
"name": "STOCH",
"inputs": "hlc",
"kwargs": {},
"elapsed_ms": 0.0181
},
{
"name": "ADX_14",
"inputs": "hlc",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0147
},
{
"name": "CCI_14",
"inputs": "hlc",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0144
},
{
"name": "MACD",
"inputs": "close",
"kwargs": {},
"elapsed_ms": 0.0132
},
{
"name": "ATR_14",
"inputs": "hlc",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0108
},
{
"name": "RSI_14",
"inputs": "close",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0102
},
{
"name": "STDDEV_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.0086
},
{
"name": "BETA_5",
"inputs": "pair_hl",
"kwargs": {
"timeperiod": 5
},
"elapsed_ms": 0.008
},
{
"name": "CORREL_30",
"inputs": "pair_hl",
"kwargs": {
"timeperiod": 30
},
"elapsed_ms": 0.0068
},
{
"name": "EMA_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.0052
},
{
"name": "BBANDS_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.005
},
{
"name": "TSF_14",
"inputs": "close",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0048
},
{
"name": "LINEARREG_14",
"inputs": "close",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0046
},
{
"name": "LINEARREG_SLOPE_14",
"inputs": "close",
"kwargs": {
"timeperiod": 14
},
"elapsed_ms": 0.0045
},
{
"name": "SMA_20",
"inputs": "close",
"kwargs": {
"timeperiod": 20
},
"elapsed_ms": 0.0027
}
]
}
+52
View File
@@ -0,0 +1,52 @@
{
"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",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"fixtures": [
{
"path": "/Users/pratikbhadane/Work/Projects/ferro-ta/benchmarks/fixtures/canonical_ohlcv.npz",
"size_bytes": 75586,
"sha256": "60192f8349fb06cd59ef7f70fd77aa8280399e819d7cc5eed3ca95cf5ee1a89c"
}
],
"output_dir": "perf-contract"
},
"artifacts": {
"indicator_latency": {
"path": "perf-contract/indicator_latency.json",
"size_bytes": 3212,
"sha256": "1e7f844fe6eb467f07bbb1e4eb918c56861e8cf819ab4802b96e033c6d2570c4"
},
"batch": {
"path": "perf-contract/batch.json",
"size_bytes": 1679,
"sha256": "202d67b24a88184473f93cec9f866ca34ff60721068e15e7e5d23962f006d169"
},
"streaming": {
"path": "perf-contract/streaming.json",
"size_bytes": 1939,
"sha256": "04476c3d95a9e7d87e40331b28c5ccad9a7aa5abcfc47fb6ac7e929365f68554"
},
"runtime_hotspots": {
"path": "perf-contract/runtime_hotspots.json",
"size_bytes": 2365,
"sha256": "d37a58bc88def9f1525b9b0f64f0bd7b9c0df9ed4cae260c833697f6a1689ca8"
},
"simd": {
"path": "perf-contract/simd.json",
"size_bytes": 7670,
"sha256": "3f9b341a8206698ed172650752d808621c9b5218f42a2373f0f4485496197b4c"
}
}
}
+96
View File
@@ -0,0 +1,96 @@
{
"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",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"price_bars": 20000,
"iv_bars": 50000,
"window": 252
}
},
"results": [
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"category": "rust_kernel",
"name": "BETA",
"fast_ms": 0.0694,
"reference_ms": 159.2715,
"speedup_vs_reference": 2294.4163,
"share_of_suite_pct": 0.17
},
{
"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
},
{
"category": "rust_kernel",
"name": "TSF",
"fast_ms": 0.0413,
"reference_ms": 46.3799,
"speedup_vs_reference": 1122.1039,
"share_of_suite_pct": 0.1
}
]
}
+285
View File
@@ -0,0 +1,285 @@
{
"metadata": {
"suite": "simd",
"runtime": {
"generated_at_utc": "2026-03-23T21:09:13.028473+00:00",
"python_version": "3.12.11",
"platform": "macOS-26.3.1-arm64-arm-64bit",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"price_bars": 20000,
"iv_bars": 50000,
"window": 252
},
"variants": [
"portable_release",
"simd_release"
]
},
"results": [
{
"name": "compute_many_close",
"category": "ffi_grouping",
"portable_ms": 0.1711,
"simd_ms": 0.1618,
"speedup_simd_vs_portable": 1.0575
},
{
"name": "iv_percentile",
"category": "python_analysis",
"portable_ms": 0.9126,
"simd_ms": 0.9096,
"speedup_simd_vs_portable": 1.0033
},
{
"name": "iv_zscore",
"category": "python_analysis",
"portable_ms": 29.0039,
"simd_ms": 28.9123,
"speedup_simd_vs_portable": 1.0032
},
{
"name": "iv_rank",
"category": "python_analysis",
"portable_ms": 10.8629,
"simd_ms": 10.862,
"speedup_simd_vs_portable": 1.0001
},
{
"name": "BETA",
"category": "rust_kernel",
"portable_ms": 0.0696,
"simd_ms": 0.0696,
"speedup_simd_vs_portable": 1.0
},
{
"name": "CORREL",
"category": "rust_kernel",
"portable_ms": 0.0555,
"simd_ms": 0.0556,
"speedup_simd_vs_portable": 0.9982
},
{
"name": "TSF",
"category": "rust_kernel",
"portable_ms": 0.0414,
"simd_ms": 0.0415,
"speedup_simd_vs_portable": 0.9976
},
{
"name": "LINEARREG",
"category": "rust_kernel",
"portable_ms": 0.0413,
"simd_ms": 0.0416,
"speedup_simd_vs_portable": 0.9928
},
{
"name": "feature_matrix",
"category": "ffi_grouping",
"portable_ms": 0.2543,
"simd_ms": 0.2634,
"speedup_simd_vs_portable": 0.9655
}
],
"reports": {
"portable_release": {
"metadata": {
"suite": "runtime_hotspots",
"runtime": {
"generated_at_utc": "2026-03-23T21:08:38.698373+00:00",
"python_version": "3.12.11",
"platform": "macOS-26.3.1-arm64-arm-64bit",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"price_bars": 20000,
"iv_bars": 50000,
"window": 252
}
},
"results": [
{
"category": "python_analysis",
"name": "iv_zscore",
"fast_ms": 29.0039,
"reference_ms": 903.5384,
"speedup_vs_reference": 31.1523,
"share_of_suite_pct": 70.04
},
{
"category": "python_analysis",
"name": "iv_rank",
"fast_ms": 10.8629,
"reference_ms": 201.621,
"speedup_vs_reference": 18.5606,
"share_of_suite_pct": 26.23
},
{
"category": "python_analysis",
"name": "iv_percentile",
"fast_ms": 0.9126,
"reference_ms": 80.0849,
"speedup_vs_reference": 87.7523,
"share_of_suite_pct": 2.2
},
{
"category": "ffi_grouping",
"name": "feature_matrix",
"fast_ms": 0.2543,
"reference_ms": 0.2222,
"speedup_vs_reference": 0.874,
"share_of_suite_pct": 0.61
},
{
"category": "ffi_grouping",
"name": "compute_many_close",
"fast_ms": 0.1711,
"reference_ms": 0.1413,
"speedup_vs_reference": 0.8257,
"share_of_suite_pct": 0.41
},
{
"category": "rust_kernel",
"name": "BETA",
"fast_ms": 0.0696,
"reference_ms": 162.9168,
"speedup_vs_reference": 2341.2971,
"share_of_suite_pct": 0.17
},
{
"category": "rust_kernel",
"name": "CORREL",
"fast_ms": 0.0555,
"reference_ms": 163.8589,
"speedup_vs_reference": 2950.1793,
"share_of_suite_pct": 0.13
},
{
"category": "rust_kernel",
"name": "TSF",
"fast_ms": 0.0414,
"reference_ms": 49.2846,
"speedup_vs_reference": 1189.9901,
"share_of_suite_pct": 0.1
},
{
"category": "rust_kernel",
"name": "LINEARREG",
"fast_ms": 0.0413,
"reference_ms": 47.5241,
"speedup_vs_reference": 1149.7853,
"share_of_suite_pct": 0.1
}
]
},
"simd_release": {
"metadata": {
"suite": "runtime_hotspots",
"runtime": {
"generated_at_utc": "2026-03-23T21:08:57.755423+00:00",
"python_version": "3.12.11",
"platform": "macOS-26.3.1-arm64-arm-64bit",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"price_bars": 20000,
"iv_bars": 50000,
"window": 252
}
},
"results": [
{
"category": "python_analysis",
"name": "iv_zscore",
"fast_ms": 28.9123,
"reference_ms": 909.2586,
"speedup_vs_reference": 31.4489,
"share_of_suite_pct": 69.98
},
{
"category": "python_analysis",
"name": "iv_rank",
"fast_ms": 10.862,
"reference_ms": 198.2076,
"speedup_vs_reference": 18.2478,
"share_of_suite_pct": 26.29
},
{
"category": "python_analysis",
"name": "iv_percentile",
"fast_ms": 0.9096,
"reference_ms": 78.1232,
"speedup_vs_reference": 85.889,
"share_of_suite_pct": 2.2
},
{
"category": "ffi_grouping",
"name": "feature_matrix",
"fast_ms": 0.2634,
"reference_ms": 0.2219,
"speedup_vs_reference": 0.8425,
"share_of_suite_pct": 0.64
},
{
"category": "ffi_grouping",
"name": "compute_many_close",
"fast_ms": 0.1618,
"reference_ms": 0.155,
"speedup_vs_reference": 0.9583,
"share_of_suite_pct": 0.39
},
{
"category": "rust_kernel",
"name": "BETA",
"fast_ms": 0.0696,
"reference_ms": 161.4576,
"speedup_vs_reference": 2320.3601,
"share_of_suite_pct": 0.17
},
{
"category": "rust_kernel",
"name": "CORREL",
"fast_ms": 0.0556,
"reference_ms": 162.6189,
"speedup_vs_reference": 2923.4861,
"share_of_suite_pct": 0.13
},
{
"category": "rust_kernel",
"name": "LINEARREG",
"fast_ms": 0.0416,
"reference_ms": 48.029,
"speedup_vs_reference": 1155.0143,
"share_of_suite_pct": 0.1
},
{
"category": "rust_kernel",
"name": "TSF",
"fast_ms": 0.0415,
"reference_ms": 47.55,
"speedup_vs_reference": 1145.783,
"share_of_suite_pct": 0.1
}
]
}
}
}
+73
View File
@@ -0,0 +1,73 @@
{
"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",
"machine": "arm64",
"processor": "arm"
},
"git": {
"commit": "2d5000262f0f1439546bd4872235aae0333880a4",
"dirty": true,
"branch": "feat/performace-1.0.2"
},
"dataset": {
"n_bars": 20000,
"seed": 2026
}
},
"results": [
{
"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
},
{
"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
},
{
"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
},
{
"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
},
{
"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
}
]
}
+3 -2
View File
@@ -4,8 +4,8 @@ build-backend = "maturin"
[project] [project]
name = "ferro-ta" name = "ferro-ta"
version = "1.0.2" version = "1.0.4"
description = "A fast Technical Analysis library TA-Lib alternative powered by Rust and PyO3" description = "Rust-powered Python technical analysis library with a TA-Lib-compatible API"
readme = "README.md" readme = "README.md"
license = { text = "MIT" } license = { text = "MIT" }
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -104,6 +104,7 @@ ignore = ["E501", "UP006", "UP045", "UP007"]
"tests/unit/*" = ["N801", "N802", "N806", "E402", "E741", "F811"] "tests/unit/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
"tests/integration/*" = ["N801", "N802", "N806", "E402", "E741", "F811"] "tests/integration/*" = ["N801", "N802", "N806", "E402", "E741", "F811"]
"python/ferro_ta/*.py" = ["N802"] # Public API: SMA, RSI, ATR, etc. "python/ferro_ta/*.py" = ["N802"] # Public API: SMA, RSI, ATR, etc.
"python/ferro_ta/__init__.py" = ["E402", "N802"] # Re-export surface by design
"python/ferro_ta/__init__.pyi" = ["N802", "E402"] "python/ferro_ta/__init__.pyi" = ["N802", "E402"]
[tool.ruff.format] [tool.ruff.format]
+50 -2
View File
@@ -59,7 +59,51 @@ array([ nan, nan, 11. , 12. , 13. , 13.5, 13.33...])
from __future__ import annotations from __future__ import annotations
import re as _re
import sys as _sys 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
try:
import tomllib as _tomllib
except ImportError: # pragma: no cover
try:
import tomli as _tomllib # type: ignore[no-redef]
except ImportError: # pragma: no cover
_tomllib = None # type: ignore[assignment]
def _detect_version() -> str:
try:
return _dist_version("ferro-ta")
except _PackageNotFoundError:
pass
if _tomllib is not None:
pyproject_toml = _Path(__file__).resolve().parents[2] / "pyproject.toml"
if pyproject_toml.is_file():
try:
with pyproject_toml.open("rb") as handle:
data = _tomllib.load(handle)
return data.get("project", {}).get("version", "0+unknown")
except Exception:
pass
pyproject_toml = _Path(__file__).resolve().parents[2] / "pyproject.toml"
if pyproject_toml.is_file():
try:
text = pyproject_toml.read_text(encoding="utf-8")
match = _re.search(r'^version\s*=\s*"([^"]+)"', text, _re.MULTILINE)
if match:
return match.group(1)
except Exception:
pass
return "0+unknown"
__version__ = _detect_version()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Exceptions — exported at the top level for convenient catching # Exceptions — exported at the top level for convenient catching
@@ -281,6 +325,7 @@ from ferro_ta.indicators.volume import ( # noqa: F401
) )
__all__ = [ __all__ = [
"__version__",
# Overlap Studies # Overlap Studies
"SMA", "SMA",
"EMA", "EMA",
@@ -459,7 +504,9 @@ __all__ = [
"VWMA", "VWMA",
"CHOPPINESS_INDEX", "CHOPPINESS_INDEX",
# API discovery # API discovery
"about",
"indicators", "indicators",
"methods",
"info", "info",
# Logging utilities # Logging utilities
"enable_debug", "enable_debug",
@@ -585,9 +632,10 @@ from ferro_ta.tools.alerts import ( # noqa: F401, E402
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# API discovery helpers — ferro_ta.indicators() and ferro_ta.info() # API discovery helpers — ferro_ta.about(), ferro_ta.methods(),
# ferro_ta.indicators(), and ferro_ta.info()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
from ferro_ta.tools.api_info import indicators, info # noqa: F401, E402 from ferro_ta.tools.api_info import about, indicators, info, methods # noqa: F401, E402
_ALIASED_SUBMODULES = { _ALIASED_SUBMODULES = {
"batch": batch, "batch": batch,
+4
View File
@@ -13,6 +13,8 @@ from numpy.typing import ArrayLike, NDArray
_F = TypeVar("_F", bound=Callable[..., Any]) _F = TypeVar("_F", bound=Callable[..., Any])
__version__: str
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Overlap Studies # Overlap Studies
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -739,8 +741,10 @@ class FerroTAInputError(FerroTAError, ValueError):
# API discovery (ferro_ta.api_info) # API discovery (ferro_ta.api_info)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def about() -> dict[str, Any]: ...
def indicators(category: str | None = None) -> list[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 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) # Logging utilities (ferro_ta.logging_utils)
File diff suppressed because it is too large Load Diff
+81 -3
View File
@@ -1,19 +1,23 @@
""" """
ferro_ta.api_info API discovery helpers. ferro_ta.api_info API discovery helpers.
Provides :func:`indicators` and :func:`info` for exploring the ferro_ta Provides :func:`indicators`, :func:`methods`, :func:`about`, and :func:`info`
indicator catalogue without reading source code. for exploring the ferro_ta public API without reading source code.
Usage Usage
----- -----
>>> import ferro_ta >>> import ferro_ta
>>> ferro_ta.indicators() # all indicators, sorted >>> ferro_ta.indicators() # all indicators, sorted
>>> ferro_ta.indicators(category="momentum") # filter by category >>> ferro_ta.indicators(category="momentum") # filter by category
>>> ferro_ta.methods() # public callables across modules
>>> ferro_ta.about()["version"] # package metadata summary
>>> ferro_ta.info(ferro_ta.SMA) # parameter docs for SMA >>> ferro_ta.info(ferro_ta.SMA) # parameter docs for SMA
API API
--- ---
indicators(category=None) Return list of dicts describing every indicator. indicators(category=None) Return list of dicts describing every indicator.
methods(category=None) Return list of public callables across modules.
about() Return package/version/module summary metadata.
info(func_or_name) Return a dict with full signature/docstring info. info(func_or_name) Return a dict with full signature/docstring info.
""" """
@@ -23,7 +27,7 @@ import importlib
import inspect import inspect
from typing import Any from typing import Any
__all__ = ["indicators", "info"] __all__ = ["indicators", "methods", "about", "info"]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Category → module mapping used by indicators() # Category → module mapping used by indicators()
@@ -52,6 +56,20 @@ _CATEGORY_MODULES: dict[str, str] = {
"regime": "ferro_ta.analysis.regime", "regime": "ferro_ta.analysis.regime",
} }
_METHOD_MODULES: dict[str, str] = {
"top_level": "ferro_ta",
**_CATEGORY_MODULES,
"options": "ferro_ta.analysis.options",
"futures": "ferro_ta.analysis.futures",
"backtest": "ferro_ta.analysis.backtest",
"options_strategy": "ferro_ta.analysis.options_strategy",
"derivatives_payoff": "ferro_ta.analysis.derivatives_payoff",
"attribution": "ferro_ta.analysis.attribution",
"cross_asset": "ferro_ta.analysis.cross_asset",
"tools": "ferro_ta.tools.tools",
"viz": "ferro_ta.tools.viz",
}
def _iter_module_callables( def _iter_module_callables(
module_name: str, module_name: str,
@@ -137,6 +155,66 @@ def indicators(category: str | None = None) -> list[dict[str, Any]]:
return result return result
def methods(category: str | None = None) -> list[dict[str, Any]]:
"""Return public callables across ferro_ta modules.
Parameters
----------
category : str | None
Optional key from :data:`_METHOD_MODULES`, such as ``"top_level"``,
``"options"``, ``"futures"``, or ``"batch"``.
"""
cats: dict[str, str] = (
{category: _METHOD_MODULES[category]}
if category is not None
else _METHOD_MODULES
)
result: list[dict[str, Any]] = []
seen: set[tuple[str, str]] = set()
for cat, mod_name in cats.items():
for name, func in _iter_module_callables(mod_name):
key = (mod_name, name)
if key in seen:
continue
seen.add(key)
doc = inspect.getdoc(func) or ""
first_line = doc.splitlines()[0] if doc else ""
try:
sig = inspect.signature(func)
params = list(sig.parameters.keys())
except (ValueError, TypeError):
params = []
result.append(
{
"name": name,
"category": cat,
"module": mod_name,
"doc": first_line,
"params": params,
}
)
result.sort(key=lambda d: (d["category"], d["name"]))
return result
def about() -> dict[str, Any]:
"""Return a small metadata summary for the installed ferro_ta package."""
import ferro_ta # noqa: PLC0415
top_level_exports = sorted(getattr(ferro_ta, "__all__", []))
return {
"name": "ferro-ta",
"version": getattr(ferro_ta, "__version__", "0+unknown"),
"top_level_export_count": len(top_level_exports),
"indicator_count": len(indicators()),
"method_count": len(methods()),
"categories": sorted(_METHOD_MODULES.keys()),
"top_level_exports": top_level_exports,
}
def info(func_or_name: Any) -> dict[str, Any]: def info(func_or_name: Any) -> dict[str, Any]:
"""Return detailed information about an indicator function. """Return detailed information about an indicator function.
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""Update or verify ferro-ta version strings across release files.
Usage
-----
python3 scripts/bump_version.py 1.0.3
python3 scripts/bump_version.py --check
python3 scripts/bump_version.py --show
"""
from __future__ import annotations
import argparse
import re
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
@dataclass(frozen=True)
class VersionCarrier:
label: str
path: Path
pattern: str
replacement: str
def read(self) -> str:
text = self.path.read_text(encoding="utf-8")
match = re.search(self.pattern, text, flags=re.MULTILINE)
if not match:
raise ValueError(f"Could not find version for {self.label} in {self.path}")
return match.group(2)
def write(self, version: str) -> bool:
text = self.path.read_text(encoding="utf-8")
updated, count = re.subn(
self.pattern,
rf"\g<1>{version}\g<3>",
text,
count=1,
flags=re.MULTILINE,
)
if count != 1:
raise ValueError(f"Could not update {self.label} in {self.path}")
changed = updated != text
if changed:
self.path.write_text(updated, encoding="utf-8")
return changed
CARRIERS = [
VersionCarrier(
"cargo_root",
ROOT / "Cargo.toml",
r'(?m)^(version = ")([^"]+)(")$',
r"\g<1>{version}\g<3>",
),
VersionCarrier(
"cargo_core_dep",
ROOT / "Cargo.toml",
r'(ferro_ta_core = \{ path = "crates/ferro_ta_core", version = ")([^"]+)(" \})',
r"\g<1>{version}\g<3>",
),
VersionCarrier(
"cargo_core_crate",
ROOT / "crates" / "ferro_ta_core" / "Cargo.toml",
r'(?m)^(version = ")([^"]+)(")$',
r"\g<1>{version}\g<3>",
),
VersionCarrier(
"cargo_core_readme",
ROOT / "crates" / "ferro_ta_core" / "README.md",
r'(ferro_ta_core = ")([^"]+)(")',
r"\g<1>{version}\g<3>",
),
VersionCarrier(
"pyproject",
ROOT / "pyproject.toml",
r'(?m)^(version = ")([^"]+)(")$',
r"\g<1>{version}\g<3>",
),
VersionCarrier(
"wasm_cargo",
ROOT / "wasm" / "Cargo.toml",
r'(?m)^(version = ")([^"]+)(")$',
r"\g<1>{version}\g<3>",
),
VersionCarrier(
"wasm_package",
ROOT / "wasm" / "package.json",
r'("version": ")([^"]+)(")',
r"\g<1>{version}\g<3>",
),
VersionCarrier(
"conda",
ROOT / "conda" / "meta.yaml",
r'({% set version = ")([^"]+)(" %})',
r"\g<1>{version}\g<3>",
),
VersionCarrier(
"docs_changelog",
ROOT / "docs" / "changelog.rst",
r"(These docs track package version ``)([^`]+)(``\.)",
r"\g<1>{version}\g<3>",
),
VersionCarrier(
"docs_support_matrix",
ROOT / "docs" / "support_matrix.rst",
r"(These docs track package version ``)([^`]+)(``\.)",
r"\g<1>{version}\g<3>",
),
]
def _read_versions() -> dict[str, str]:
return {carrier.label: carrier.read() for carrier in CARRIERS}
def _print_versions(versions: dict[str, str]) -> None:
for label, version in versions.items():
print(f"{label:20} {version}")
def _check_versions() -> int:
versions = _read_versions()
unique = sorted(set(versions.values()))
_print_versions(versions)
if len(unique) != 1:
print()
print(f"ERROR: version mismatch detected: {', '.join(unique)}")
return 1
print()
print(f"OK: all tracked versions match {unique[0]}")
return 0
def _set_version(version: str) -> int:
if not SEMVER_RE.match(version):
print(f"ERROR: expected MAJOR.MINOR.PATCH, got {version!r}")
return 1
changed_paths: list[Path] = []
for carrier in CARRIERS:
if carrier.write(version):
changed_paths.append(carrier.path)
if changed_paths:
print(f"Updated version to {version}:")
for path in sorted(set(changed_paths)):
print(f" - {path.relative_to(ROOT)}")
else:
print(f"No changes needed. All tracked files already use {version}.")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("version", nargs="?", help="New version to write")
parser.add_argument(
"--check",
action="store_true",
help="Fail if tracked version strings do not match",
)
parser.add_argument(
"--show",
action="store_true",
help="Print tracked version strings without modifying files",
)
args = parser.parse_args()
if args.check:
return _check_versions()
if args.show:
_print_versions(_read_versions())
return 0
if args.version:
return _set_version(args.version)
parser.print_help()
return 1
if __name__ == "__main__":
raise SystemExit(main())
+21
View File
@@ -227,6 +227,27 @@ def test_indicators_returns_list():
assert "ATR" in names assert "ATR" in names
def test_methods_returns_public_callables():
import ferro_ta
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
)
def test_about_reports_version_and_counts():
import ferro_ta
meta = ferro_ta.about()
assert meta["version"] == ferro_ta.__version__
assert meta["indicator_count"] > 20
assert meta["method_count"] >= meta["indicator_count"]
assert "__version__" in meta["top_level_exports"]
def test_indicators_filter_by_category(): def test_indicators_filter_by_category():
import ferro_ta import ferro_ta
+35
View File
@@ -1,3 +1,7 @@
import subprocess
import sys
from pathlib import Path
import numpy as np import numpy as np
import pytest import pytest
@@ -216,3 +220,34 @@ class TestStrategyAndPayoff:
assert payoff[1] == pytest.approx(-3.0) assert payoff[1] == pytest.approx(-3.0)
assert greeks.delta > 0.0 assert greeks.delta > 0.0
assert greeks.gamma > 0.0 assert greeks.gamma > 0.0
class TestDerivativesBenchmarking:
def test_derivatives_benchmark_smoke(self, tmp_path):
root = Path(__file__).resolve().parents[2]
script = root / "benchmarks" / "bench_derivatives_compare.py"
output_path = tmp_path / "derivatives_benchmark.json"
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()
payload = output_path.read_text(encoding="utf-8")
assert '"accuracy"' in payload
assert '"speed"' in payload
assert '"provider": "ferro_ta"' in payload
+72 -1
View File
@@ -641,6 +641,9 @@ class TestBatchShapeValidation:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
import os import os
import re
import runpy
import subprocess
try: try:
import tomllib # Python 3.11+ import tomllib # Python 3.11+
@@ -673,8 +676,41 @@ def _read_pyproject_version() -> str:
return data["project"]["version"] return data["project"]["version"]
def _read_conda_version() -> str:
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
conda_meta = os.path.join(root, "conda", "meta.yaml")
text = open(conda_meta).read()
match = re.search(r'{% set version = "([^"]+)" %}', text)
if not match:
raise ValueError("Could not find conda version")
return match.group(1)
def _read_docs_release() -> str:
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
conf_py = os.path.join(root, "docs", "conf.py")
old_env = os.environ.pop("FERRO_TA_VERSION", None)
try:
data = runpy.run_path(conf_py)
return data["release"]
finally:
if old_env is not None:
os.environ["FERRO_TA_VERSION"] = old_env
def _run_bump_version_check() -> subprocess.CompletedProcess[str]:
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
return subprocess.run(
["python3", "scripts/bump_version.py", "--check"],
cwd=root,
text=True,
capture_output=True,
check=False,
)
class TestVersionConsistency: class TestVersionConsistency:
"""Cargo.toml and pyproject.toml must have the same version string.""" """Public version strings should stay aligned with the package version."""
def test_versions_match(self): def test_versions_match(self):
try: try:
@@ -687,6 +723,41 @@ class TestVersionConsistency:
f"pyproject.toml={pyproject_ver!r}" f"pyproject.toml={pyproject_ver!r}"
) )
def test_package_version_matches_project_version(self):
cargo_ver = _read_cargo_version()
assert ferro_ta.__version__ == cargo_ver
def test_conda_version_matches_project_version(self):
cargo_ver = _read_cargo_version()
conda_ver = _read_conda_version()
assert conda_ver == cargo_ver
def test_docs_release_matches_project_version(self):
cargo_ver = _read_cargo_version()
docs_release = _read_docs_release()
assert docs_release == cargo_ver
def test_docs_changelog_mentions_current_version(self):
cargo_ver = _read_cargo_version()
root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
changelog_rst = os.path.join(root, "docs", "changelog.rst")
text = open(changelog_rst).read()
assert cargo_ver in text
def test_api_version_matches_project_version(self):
cargo_ver = _read_cargo_version()
try:
from api.main import app
except Exception:
pytest.skip("api/main.py not importable")
assert app.version == cargo_ver
def test_bump_version_check_passes(self):
result = _run_bump_version_check()
assert result.returncode == 0, result.stdout + result.stderr
def test_release_md_exists(self): def test_release_md_exists(self):
"""RELEASE.md must exist in the repository root.""" """RELEASE.md must exist in the repository root."""
root = os.path.dirname( root = os.path.dirname(
+196 -1
View File
@@ -4,6 +4,9 @@ regime detection, performance attribution, and dashboard helpers.
from __future__ import annotations from __future__ import annotations
import importlib
import runpy
import numpy as np import numpy as np
import pytest import pytest
@@ -1218,7 +1221,22 @@ class TestMCPListTools:
result = handle_list_tools() result = handle_list_tools()
names = [t["name"] for t in result["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" assert expected in names, f"Expected tool '{expected}' not found"
def test_each_tool_has_schema(self): def test_each_tool_has_schema(self):
@@ -1280,6 +1298,57 @@ class TestMCPCallTool:
assert "final_equity" in payload assert "final_equity" in payload
assert "n_trades" 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): def test_list_indicators_call(self):
import json import json
@@ -1322,3 +1391,129 @@ class TestMCPCallTool:
"backtest", {"close": close, "strategy": "no_strategy"} "backtest", {"close": close, "strategy": "no_strategy"}
) )
assert result.get("isError") is True or "content" in result 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
+207 -158
View File
@@ -515,19 +515,24 @@ wheels = [
[[package]] [[package]]
name = "cuda-bindings" name = "cuda-bindings"
version = "12.9.4" version = "13.2.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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')" }, { 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 = [ 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/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/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/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/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/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/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/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/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/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/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/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/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/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]] [[package]]
@@ -538,6 +543,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" }, { 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]] [[package]]
name = "cycler" name = "cycler"
version = "0.12.1" version = "0.12.1"
@@ -593,7 +641,7 @@ wheels = [
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.135.1" version = "0.135.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "annotated-doc" }, { name = "annotated-doc" },
@@ -602,14 +650,14 @@ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "typing-inspection" }, { 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 = [ 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]] [[package]]
name = "ferro-ta" name = "ferro-ta"
version = "1.0.2" version = "1.0.3"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "numpy" }, { name = "numpy" },
@@ -1583,137 +1631,152 @@ wheels = [
] ]
[[package]] [[package]]
name = "nvidia-cublas-cu12" name = "nvidia-cublas"
version = "12.8.4.1" version = "13.1.0.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-cuda-cupti-cu12" name = "nvidia-cuda-cupti"
version = "12.8.90" version = "13.0.85"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-cuda-nvrtc-cu12" name = "nvidia-cuda-nvrtc"
version = "12.8.93" version = "13.0.88"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-cuda-runtime-cu12" name = "nvidia-cuda-runtime"
version = "12.8.90" version = "13.0.96"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-cudnn-cu12" name = "nvidia-cudnn-cu13"
version = "9.10.2.21" version = "9.19.0.56"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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 = [ 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]] [[package]]
name = "nvidia-cufft-cu12" name = "nvidia-cufft"
version = "11.3.3.83" version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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 = [ 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]] [[package]]
name = "nvidia-cufile-cu12" name = "nvidia-cufile"
version = "1.13.1.3" version = "1.15.1.6"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-curand-cu12" name = "nvidia-curand"
version = "10.3.9.90" version = "10.4.0.35"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-cusolver-cu12" name = "nvidia-cusolver"
version = "11.7.3.90" version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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')" },
{ 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-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-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 = [ 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]] [[package]]
name = "nvidia-cusparse-cu12" name = "nvidia-cusparse"
version = "12.5.8.93" version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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 = [ 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]] [[package]]
name = "nvidia-cusparselt-cu12" name = "nvidia-cusparselt-cu13"
version = "0.7.1" version = "0.8.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-nccl-cu12" name = "nvidia-nccl-cu13"
version = "2.27.5" version = "2.28.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-nvjitlink-cu12" name = "nvidia-nvjitlink"
version = "12.8.93" version = "13.0.88"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-nvshmem-cu12" name = "nvidia-nvshmem-cu13"
version = "3.4.5" version = "3.4.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
name = "nvidia-nvtx-cu12" name = "nvidia-nvtx"
version = "12.8.90" version = "13.0.85"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]
@@ -1994,30 +2057,30 @@ wheels = [
[[package]] [[package]]
name = "polars" name = "polars"
version = "1.38.1" version = "1.39.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "polars-runtime-32" }, { 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 = [ 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]] [[package]]
name = "polars-runtime-32" name = "polars-runtime-32"
version = "1.38.1" version = "1.39.3"
source = { registry = "https://pypi.org/simple" } 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 = [ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]] [[package]]
@@ -2557,36 +2620,36 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.5" version = "0.15.7"
source = { registry = "https://pypi.org/simple" } 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 = [ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]] [[package]]
name = "setuptools" name = "setuptools"
version = "82.0.0" version = "81.0.0"
source = { registry = "https://pypi.org/simple" } 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 = [ 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]] [[package]]
@@ -2963,75 +3026,54 @@ wheels = [
[[package]] [[package]]
name = "torch" name = "torch"
version = "2.10.0" version = "2.11.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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 = "filelock" },
{ name = "fsspec" }, { name = "fsspec" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { 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 = "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-cudnn-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "setuptools" },
{ 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 = "sympy" }, { name = "sympy" },
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "triton", marker = "sys_platform == 'linux'" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
wheels = [ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" },
{ 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" },
] ]
[[package]] [[package]]
@@ -3051,12 +3093,19 @@ name = "triton"
version = "3.6.0" version = "3.6.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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/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/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/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/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/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/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" }, { 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 +3150,16 @@ wheels = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.41.0" version = "0.42.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, { 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 = "h11", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { 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 = [ 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]] [[package]]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ferro_ta_wasm" name = "ferro_ta_wasm"
version = "1.0.2" version = "1.0.4"
edition = "2021" edition = "2021"
description = "WebAssembly bindings for ferro-ta technical analysis indicators" description = "WebAssembly bindings for ferro-ta technical analysis indicators"
license = "MIT" license = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ferro-ta-wasm", "name": "ferro-ta-wasm",
"version": "1.0.2", "version": "1.0.4",
"description": "WebAssembly bindings for ferro-ta technical analysis indicators", "description": "WebAssembly bindings for ferro-ta technical analysis indicators",
"main": "pkg/ferro_ta_wasm.js", "main": "pkg/ferro_ta_wasm.js",
"types": "pkg/ferro_ta_wasm.d.ts", "types": "pkg/ferro_ta_wasm.d.ts",