From 111a018695f321db4e3fdd3ae97a75c05373829e Mon Sep 17 00:00:00 2001 From: Mayank Lavania Date: Sat, 11 Jul 2026 15:28:37 +0530 Subject: [PATCH 1/3] refactor(ffi): unify Python bindings on PyO3 abi3, retire uniffi (5ipk.10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quantwave-python was the last uniffi cdylib: it shipped a py3-none wheel while quantwave-plugins and quantwave-backtest-py shipped cp39-abi3. The custom build_unified_wheel.py existed only to reconcile those mismatched tags — the root cause of the 9gek.1 wheel-tag bug class. Port the entire uniffi surface (50 Records, 35 streaming Objects, 1 Enum, 90 exports, 11 declarative macros) to PyO3 abi3. The transform swaps only attributes — every struct/enum/fn/field name is preserved — so the Python-visible surface (getattr(_quantwave, ...)) is byte-identical. The two symbols uniffi re-cased via heck (BullBearHMM->BullBearHmm, OIZonesResult->OiZonesResult) are pinned with #[pyclass(name = ...)]. Now all three crates build cp39-abi3 → the wheel-tag bug is killed at the source and the merge script becomes a trivial abi3 .so bundler (with a uniform-tag assertion guarding against future non-abi3 regressions). Also: - move backtest.rs into quantwave-backtest-py (delete the cross-crate [lib] path = ../quantwave-python/src/backtest.rs) - generate_api_stubs.py: detect streaming classes via #[pyclass] + inner field (honoring name attrs) instead of #[derive(uniffi::Object)] - drop uniffi-bindgen from pyproject build-requires, ci.yml, release.yml - docs: UniFFI -> PyO3 (roadmap, validation) Incremental scope (user decision): single-crate collapse deferred to a fast-follow bead. Validated: gold parity 28/28, options parity + doctests 53/53, unified wheel installs clean (cp39-abi3, 3 abi3 .so, 221 indicators), all 15 CI sanity checks green, rustfmt clean. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- Cargo.lock | 397 +-------- docs/generated/validation_stats.json | 2 +- docs/roadmap.md | 2 +- docs/validation.md | 4 +- quantwave-backtest-py/Cargo.toml | 1 - .../src/lib.rs | 0 quantwave-python/Cargo.toml | 5 +- quantwave-python/build.rs | 3 - quantwave-python/pyproject.toml | 2 +- quantwave-python/src/lib.rs | 838 +++++++++++++----- scripts/build_unified_wheel.py | 173 +--- scripts/generate_api_stubs.py | 15 +- 14 files changed, 675 insertions(+), 771 deletions(-) rename quantwave-python/src/backtest.rs => quantwave-backtest-py/src/lib.rs (100%) delete mode 100644 quantwave-python/build.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5727ba7f2..b26f3daba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,7 @@ jobs: run: | python -m venv .venv source .venv/bin/activate - pip install maturin "uniffi-bindgen==0.31.0" pytest numpy polars + pip install maturin pytest numpy polars - name: Build quantwave + plugins (editable) run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93410c483..b8d3ff6d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: Install build tools - run: pip install maturin uniffi-bindgen==0.31.0 wheel + run: pip install maturin wheel - name: Build unified wheel (core + backtest + plugins) run: python scripts/build_unified_wheel.py --out dist diff --git a/Cargo.lock b/Cargo.lock index 72d43d0d7..04cf5000f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,48 +96,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed51fe0f224d1d4ea768be38c51f9f831dee9d05c163c11fba0b8c44387b1fc3" -[[package]] -name = "askama" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" -dependencies = [ - "askama_derive", - "itoa", - "percent-encoding", - "serde", - "serde_json", -] - -[[package]] -name = "askama_derive" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" -dependencies = [ - "askama_parser", - "basic-toml", - "memchr", - "proc-macro2", - "quote", - "rustc-hash", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "askama_parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" -dependencies = [ - "memchr", - "serde", - "serde_derive", - "winnow 0.7.15", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -198,15 +156,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -269,38 +218,6 @@ dependencies = [ "serde", ] -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - [[package]] name = "cast" version = "0.3.0" @@ -393,7 +310,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", - "clap_derive", ] [[package]] @@ -404,19 +320,6 @@ checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", ] [[package]] @@ -662,15 +565,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "fs-err" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" -dependencies = [ - "autocfg", -] - [[package]] name = "futures" version = "0.3.32" @@ -803,17 +697,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "goblin" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" -dependencies = [ - "log", - "plain", - "scroll", -] - [[package]] name = "half" version = "2.7.1" @@ -1086,12 +969,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "mio" version = "1.2.0" @@ -1133,16 +1010,6 @@ dependencies = [ "syn", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "now" version = "0.1.3" @@ -1297,12 +1164,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "planus" version = "0.3.1" @@ -2091,8 +1952,8 @@ name = "quantwave-python" version = "0.6.1" dependencies = [ "paste", + "pyo3", "quantwave-core", - "uniffi", ] [[package]] @@ -2300,12 +2161,6 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - [[package]] name = "rustfft" version = "6.4.1" @@ -2381,35 +2236,11 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scroll" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" -dependencies = [ - "scroll_derive", -] - -[[package]] -name = "scroll_derive" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] [[package]] name = "serde" @@ -2467,15 +2298,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2528,12 +2350,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - [[package]] name = "socket2" version = "0.6.3" @@ -2605,12 +2421,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum_macros" version = "0.26.4" @@ -2678,15 +2488,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "smawk", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -2779,45 +2580,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.3", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - [[package]] name = "transpose" version = "0.2.3" @@ -2882,139 +2644,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "uniffi" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5f2297ee5b893405bed1a6929faec4713a061df158ecf5198089f23910d470" -dependencies = [ - "anyhow", - "camino", - "cargo_metadata", - "clap", - "uniffi_bindgen", - "uniffi_build", - "uniffi_core", - "uniffi_macros", - "uniffi_pipeline", -] - -[[package]] -name = "uniffi_bindgen" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bc0c60a9607e7ab77a2ad47ec5530178015014839db25af7512447d2238016c" -dependencies = [ - "anyhow", - "askama", - "camino", - "cargo_metadata", - "fs-err", - "glob", - "goblin", - "heck", - "indexmap", - "once_cell", - "serde", - "tempfile", - "textwrap", - "toml", - "uniffi_internal_macros", - "uniffi_meta", - "uniffi_pipeline", - "uniffi_udl", -] - -[[package]] -name = "uniffi_build" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c39413c43b955e4aa8a4e2b34bbd1b6b5ff6bd85532b52f9eb92fbe88c14458" -dependencies = [ - "anyhow", - "camino", - "uniffi_bindgen", -] - -[[package]] -name = "uniffi_core" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77baf5d539fe2e1ad6805e942dbc5dbdeb2b83eb5f2b3a6535d422ca4b02a12f" -dependencies = [ - "anyhow", - "bytes", - "once_cell", - "static_assertions", -] - -[[package]] -name = "uniffi_internal_macros" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b42137524f4be6400fcaca9d02c1d4ecb6ad917e4013c0b93235526d8396e5" -dependencies = [ - "anyhow", - "indexmap", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "uniffi_macros" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9273ec45330d8fe9a3701b7b983cea7a4e218503359831967cb95d26b873561" -dependencies = [ - "camino", - "fs-err", - "once_cell", - "proc-macro2", - "quote", - "serde", - "syn", - "toml", - "uniffi_meta", -] - -[[package]] -name = "uniffi_meta" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431d2f443e7828a6c29d188de98b6771a6491ee98bba2d4372643bf93f988a18" -dependencies = [ - "anyhow", - "siphasher", - "uniffi_internal_macros", - "uniffi_pipeline", -] - -[[package]] -name = "uniffi_pipeline" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "761ef74f6175e15603d0424cc5f98854c5baccfe7bf4ccb08e5816f9ab8af689" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "tempfile", - "uniffi_internal_macros", -] - -[[package]] -name = "uniffi_udl" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68773ec0e1c067b6505a73bbf6a5782f31a7f9209333a0df97b87565c46bf370" -dependencies = [ - "anyhow", - "textwrap", - "uniffi_meta", - "weedle2", -] - [[package]] name = "unindent" version = "0.2.4" @@ -3170,15 +2799,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "weedle2" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" -dependencies = [ - "nom", -] - [[package]] name = "wide" version = "0.7.33" @@ -3411,21 +3031,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" - [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/docs/generated/validation_stats.json b/docs/generated/validation_stats.json index 3ec494101..353ed2105 100644 --- a/docs/generated/validation_stats.json +++ b/docs/generated/validation_stats.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-07-10T15:39:00Z", + "generated_at": "2026-07-11T09:36:20Z", "indicator_count": 221, "metadata_with_gold_file": 217, "gold_fixture_count": 28, diff --git a/docs/roadmap.md b/docs/roadmap.md index d29d71253..993267edf 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -34,7 +34,7 @@ quantwave-core Next — single mathematical truth quantwave-polars lf.ta.*() + lf.ta.features.* quantwave-plugins Polars expression plugins (zero-copy) quantwave-backtest Backtest engine + tearsheets -quantwave-python UniFFI + PyO3 Python package +quantwave-python PyO3 (abi3) Python package ``` --- diff --git a/docs/validation.md b/docs/validation.md index e21eb010c..fb903c20c 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -7,7 +7,7 @@ QuantWave's primary credibility claim is **correctness**: one mathematical imple ## Coverage snapshot -**Last updated:** 2026-07-10T15:39:00Z (UTC) +**Last updated:** 2026-07-11T09:36:20Z (UTC) | Metric | Count | Source | |--------|------:|--------| @@ -89,7 +89,7 @@ Dedicated integration tests in `quantwave-core/tests/test_all_talib_parity.rs` a ## Python cross-language parity -The unified PyPI wheel bundles UniFFI core + PyO3 backtest + Polars plugins. Python gold parity tests assert that **streaming classes** match the same JSON vectors as Rust — catching FFI field swaps, warmup mishandling, or ABI mismatches before release. +The unified PyPI wheel bundles the PyO3 (abi3) core + backtest + Polars plugins extensions. Python gold parity tests assert that **streaming classes** match the same JSON vectors as Rust — catching FFI field swaps, warmup mishandling, or ABI mismatches before release. CI runs `tests/python/test_gold_parity.py` on **Linux and macOS** after `maturin develop` (see `.github/workflows/ci.yml`). diff --git a/quantwave-backtest-py/Cargo.toml b/quantwave-backtest-py/Cargo.toml index 6b8b9a56c..abfa99e65 100644 --- a/quantwave-backtest-py/Cargo.toml +++ b/quantwave-backtest-py/Cargo.toml @@ -13,7 +13,6 @@ keywords.workspace = true [lib] name = "quantwave_backtest" -path = "../quantwave-python/src/backtest.rs" crate-type = ["cdylib"] [dependencies] diff --git a/quantwave-python/src/backtest.rs b/quantwave-backtest-py/src/lib.rs similarity index 100% rename from quantwave-python/src/backtest.rs rename to quantwave-backtest-py/src/lib.rs diff --git a/quantwave-python/Cargo.toml b/quantwave-python/Cargo.toml index 2a84a3120..42745927f 100644 --- a/quantwave-python/Cargo.toml +++ b/quantwave-python/Cargo.toml @@ -16,9 +16,6 @@ name = "quantwave_python" crate-type = ["cdylib"] [dependencies] -uniffi = { version = "0.31", features = ["cli"] } +pyo3 = { version = "0.23", features = ["extension-module", "abi3-py39"] } quantwave-core = { workspace = true } paste = "1.0.15" - -[build-dependencies] -uniffi = { version = "0.31", features = ["build"] } diff --git a/quantwave-python/build.rs b/quantwave-python/build.rs deleted file mode 100644 index 0bc4a3fce..000000000 --- a/quantwave-python/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - // No UDL needed for attribute-based UniFFI -} diff --git a/quantwave-python/pyproject.toml b/quantwave-python/pyproject.toml index 57c75ac8b..356066de0 100644 --- a/quantwave-python/pyproject.toml +++ b/quantwave-python/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=1.0,<2.0", "uniffi-bindgen==0.31.0"] +requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [project] diff --git a/quantwave-python/src/lib.rs b/quantwave-python/src/lib.rs index a5e536941..025c41208 100644 --- a/quantwave-python/src/lib.rs +++ b/quantwave-python/src/lib.rs @@ -1,3 +1,5 @@ +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; use quantwave_core::indicators::alligator::Alligator as CoreAlligator; use quantwave_core::indicators::alma::ALMA as CoreALMA; use quantwave_core::indicators::amfm::{ @@ -111,13 +113,9 @@ use quantwave_core::indicators::zero_lag::ZeroLag as CoreZeroLag; use quantwave_core::traits::Next; // ML Feature extractors (for quantwave-gw7s canonical notebook + validation) -use quantwave_core::features::cyber_cycle::{ - CyberCycleFeatureExtractor as CoreCyberCycleFE, CyberCycleFeatures as CoreCyberCycleF, -}; +use quantwave_core::features::cyber_cycle::CyberCycleFeatureExtractor as CoreCyberCycleFE; use quantwave_core::features::griffiths_dominant_cycle::GriffithsDominantCycleFeatureExtractor as CoreGriffithsDCFE; -use quantwave_core::features::hurst::{ - HurstFeatureExtractor as CoreHurstFE, HurstFeatures as CoreHurstF, -}; +use quantwave_core::features::hurst::HurstFeatureExtractor as CoreHurstFE; use quantwave_core::features::instantaneous_trendline::InstantaneousTrendlineFeatureExtractor as CoreITFE; use quantwave_core::features::regime::regime_to_features as core_regime_to_features; use quantwave_core::features::trendflex::TrendflexFeatureExtractor as CoreTrendflexFE; @@ -134,89 +132,99 @@ use quantwave_core::regimes::gaussian_hmm::{ use quantwave_core::regimes::hmm::HMM as CoreHMM; use quantwave_core::regimes::hmm_forecast::{ forecast_state as core_forecast_state, forecast_volatility as core_forecast_volatility, - pseudo_residuals as core_pseudo_residuals, }; use paste::paste; use std::sync::Mutex; - -uniffi::setup_scaffolding!(); - // --- Records --- -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct SuperTrendResult { pub value: f64, pub direction: i8, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct MacdResult { pub macd: f64, pub signal: f64, pub histogram: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct BbandsResult { pub upper: f64, pub middle: f64, pub lower: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct StochResult { pub k: f64, pub d: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct MamaResult { pub mama: f64, pub fama: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct AroonResult { pub up: f64, pub down: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct IchimokuResult { pub tenkan: f64, pub kijun: f64, pub senkou_a: f64, pub senkou_b: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct AlligatorResult { pub jaw: f64, pub teeth: f64, pub lips: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct AtrTsResult { pub stop: f64, pub direction: i8, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct DonchianResult { pub upper: f64, pub middle: f64, pub lower: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct EmdResult { pub trend: f64, pub upper: f64, pub lower: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct EhlersLoopsResult { pub price_rms: f64, pub vol_rms: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct FractalsResult { pub bearish: bool, pub bullish: bool, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct HeikinAshiResult { pub open: f64, pub high: f64, @@ -225,20 +233,23 @@ pub struct HeikinAshiResult { } // --- PA / MarketStructure + Geometric foundation (quantwave-5thj) --- -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct SwingPointResult { pub bar: u64, pub price: f64, pub is_high: bool, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct FlipEventResult { pub is_bearish: bool, pub price: f64, pub bar: u64, pub structure_strength: u32, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct MarketStructureStateResult { pub bias: u32, // 0=Neutral, 1=Bullish, 2=Bearish pub last_swing_high: Option, @@ -247,7 +258,8 @@ pub struct MarketStructureStateResult { pub swing_depth_used: u32, pub bar_index: u64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct FlagPatternResult { pub id: u32, pub is_bull: bool, @@ -260,7 +272,8 @@ pub struct FlagPatternResult { pub breakout_confirmed: bool, pub breakout_price: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct HsPatternResult { pub id: u32, pub is_bearish: bool, @@ -270,29 +283,34 @@ pub struct HsPatternResult { pub breakout_confirmed: bool, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct GeometricNextResult { pub market_structure: MarketStructureStateResult, pub flag: Option, pub hs: Option, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct KeltnerResult { pub upper: f64, pub middle: f64, pub lower: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct PairsRotationResult { pub ratio: f64, pub angle: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct PhasorResult { pub in_phase: f64, pub quadrature: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct PivotPointsResult { pub p: f64, pub r1: f64, @@ -300,7 +318,8 @@ pub struct PivotPointsResult { pub r2: f64, pub s2: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct SystemEvaluatorResult { pub average_win_loss_ratio: f64, pub average_trade: f64, @@ -310,93 +329,112 @@ pub struct SystemEvaluatorResult { pub weighted_average_trade: f64, pub theoretical_consecutive_losers: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct UltimateBandsResult { pub upper: f64, pub middle: f64, pub lower: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct UltimateChannelResult { pub upper: f64, pub center: f64, pub lower: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct VortexResult { pub plus: f64, pub minus: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct WaveTrendResult { pub wt1: f64, pub wt2: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct VossPredictorResult { pub filt: f64, pub voss: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct CycleTrendAnalyticsResult { pub cycle: f64, pub trend: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct ZeroLagResult { pub value: f64, pub trigger: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct CyberCycleResult { pub value: f64, pub trigger: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct HtSineResult { pub sine: f64, pub leadsine: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct VolumeProfileResult { pub poc: f64, pub vah: f64, pub val: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct PmaResult { pub pma: f64, pub predict: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct TrendRocResult { pub trend: f64, pub roc: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct UdmaResult { pub fast: f64, pub slow: f64, } -#[derive(uniffi::Record)] +// name = "OiZonesResult" preserves the uniffi-era Python symbol (uniffi re-cased the +// `OI` capital-run via heck). +#[pyclass(name = "OiZonesResult", get_all)] +#[derive(Clone)] pub struct OIZonesResult { pub resistance_strikes: Vec, pub support_strikes: Vec, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct GexResult { pub ce_gex: f64, pub pe_gex: f64, pub net_gex: f64, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct StraddleResult { pub atm_strike: f64, pub straddle_premium: f64, pub implied_move_pct: f64, } -#[derive(uniffi::Enum)] +#[pyclass(eq, eq_int)] +#[derive(Clone, PartialEq)] pub enum SwissMode { EMA, SMA, @@ -429,7 +467,7 @@ impl From for SwissArmyKnifeMode { macro_rules! export_1_in_1_out { ($name:ident, $core_type:ty, ($($param:ident: $param_type:ty),*)) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* series: Vec) -> Vec { let mut indicator = <$core_type>::new($( $param as _ ),*); series.iter().map(|&x| indicator.next(x)).collect() @@ -438,9 +476,9 @@ macro_rules! export_1_in_1_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, input: f64) -> f64 { self.inner.lock().unwrap().next(input) } } } @@ -450,7 +488,7 @@ macro_rules! export_1_in_1_out { macro_rules! export_1_in_record_out { ($name:ident, $core_type:ty, $result_type:ty, ($($param:ident: $param_type:ty),*), $res_var:ident, $body:expr) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* series: Vec) -> Vec<$result_type> { let mut indicator = <$core_type>::new($( $param as _ ),*); series.iter().map(|&x| { let $res_var = indicator.next(x); $body }).collect() @@ -459,9 +497,9 @@ macro_rules! export_1_in_record_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, input: f64) -> $result_type { let $res_var = self.inner.lock().unwrap().next(input); $body } } } @@ -471,7 +509,7 @@ macro_rules! export_1_in_record_out { macro_rules! export_1_in_vec_out { ($name:ident, $core_type:ty, ($($param:ident: $param_type:ty),*)) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* series: Vec) -> Vec> { let mut indicator = <$core_type>::new($( $param as _ ),*); series.iter().map(|&x| indicator.next(x)).collect() @@ -480,9 +518,9 @@ macro_rules! export_1_in_vec_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, input: f64) -> Vec { self.inner.lock().unwrap().next(input) } } } @@ -492,7 +530,7 @@ macro_rules! export_1_in_vec_out { macro_rules! export_ohlc_in_1_out { ($name:ident, $core_type:ty, ($($param:ident: $param_type:ty),*)) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* high: Vec, low: Vec, close: Vec) -> Vec { let mut indicator = <$core_type>::new($( $param as _ ),*); high.iter().zip(low.iter()).zip(close.iter()).map(|((&h, &l), &c)| indicator.next((h, l, c))).collect() @@ -501,9 +539,9 @@ macro_rules! export_ohlc_in_1_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, high: f64, low: f64, close: f64) -> f64 { self.inner.lock().unwrap().next((high, low, close)) } } } @@ -513,7 +551,7 @@ macro_rules! export_ohlc_in_1_out { macro_rules! export_ohlc_in_record_out { ($name:ident, $core_type:ty, $result_type:ty, ($($param:ident: $param_type:ty),*), $res_var:ident, $body:expr) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* high: Vec, low: Vec, close: Vec) -> Vec<$result_type> { let mut indicator = <$core_type>::new($( $param as _ ),*); high.iter().zip(low.iter()).zip(close.iter()).map(|((&h, &l), &c)| { let $res_var = indicator.next((h, l, c)); $body }).collect() @@ -522,9 +560,9 @@ macro_rules! export_ohlc_in_record_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, high: f64, low: f64, close: f64) -> $result_type { let $res_var = self.inner.lock().unwrap().next((high, low, close)); $body } } } @@ -534,7 +572,7 @@ macro_rules! export_ohlc_in_record_out { macro_rules! export_hl_in_1_out { ($name:ident, $core_type:ty, ($($param:ident: $param_type:ty),*)) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* high: Vec, low: Vec) -> Vec { let mut indicator = <$core_type>::new($( $param as _ ),*); high.iter().zip(low.iter()).map(|(&h, &l)| indicator.next((h, l))).collect() @@ -543,9 +581,9 @@ macro_rules! export_hl_in_1_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, high: f64, low: f64) -> f64 { self.inner.lock().unwrap().next((high, low)) } } } @@ -555,7 +593,7 @@ macro_rules! export_hl_in_1_out { macro_rules! export_hl_in_record_out { ($name:ident, $core_type:ty, $result_type:ty, ($($param:ident: $param_type:ty),*), $res_var:ident, $body:expr) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* high: Vec, low: Vec) -> Vec<$result_type> { let mut indicator = <$core_type>::new($( $param as _ ),*); high.iter().zip(low.iter()).map(|(&h, &l)| { let $res_var = indicator.next((h, l)); $body }).collect() @@ -564,9 +602,9 @@ macro_rules! export_hl_in_record_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, high: f64, low: f64) -> $result_type { let $res_var = self.inner.lock().unwrap().next((high, low)); $body } } } @@ -576,7 +614,7 @@ macro_rules! export_hl_in_record_out { macro_rules! export_co_in_1_out { ($name:ident, $core_type:ty, ($($param:ident: $param_type:ty),*)) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* close: Vec, open: Vec) -> Vec { let mut indicator = <$core_type>::new($( $param as _ ),*); close.iter().zip(open.iter()).map(|(&c, &o)| indicator.next((c, o))).collect() @@ -585,9 +623,9 @@ macro_rules! export_co_in_1_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, close: f64, open: f64) -> f64 { self.inner.lock().unwrap().next((close, open)) } } } @@ -597,7 +635,7 @@ macro_rules! export_co_in_1_out { macro_rules! export_co_in_record_out { ($name:ident, $core_type:ty, $result_type:ty, ($($param:ident: $param_type:ty),*), $res_var:ident, $body:expr) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* close: Vec, open: Vec) -> Vec<$result_type> { let mut indicator = <$core_type>::new($( $param as _ ),*); close.iter().zip(open.iter()).map(|(&c, &o)| { let $res_var = indicator.next((c, o)); $body }).collect() @@ -606,9 +644,9 @@ macro_rules! export_co_in_record_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, close: f64, open: f64) -> $result_type { let $res_var = self.inner.lock().unwrap().next((close, open)); $body } } } @@ -618,7 +656,7 @@ macro_rules! export_co_in_record_out { macro_rules! export_pv_in_1_out { ($name:ident, $core_type:ty, ($($param:ident: $param_type:ty),*)) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* price: Vec, volume: Vec) -> Vec { let mut indicator = <$core_type>::new($( $param as _ ),*); price.iter().zip(volume.iter()).map(|(&p, &v)| indicator.next((p, v))).collect() @@ -627,9 +665,9 @@ macro_rules! export_pv_in_1_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, price: f64, volume: f64) -> f64 { self.inner.lock().unwrap().next((price, volume)) } } } @@ -639,7 +677,7 @@ macro_rules! export_pv_in_1_out { macro_rules! export_pv_in_record_out { ($name:ident, $core_type:ty, $result_type:ty, ($($param:ident: $param_type:ty),*), $res_var:ident, $body:expr) => { paste! { - #[uniffi::export] + #[pyfunction] pub fn [<$name:lower>]($( $param: $param_type , )* price: Vec, volume: Vec) -> Vec<$result_type> { let mut indicator = <$core_type>::new($( $param as _ ),*); price.iter().zip(volume.iter()).map(|(&p, &v)| { let $res_var = indicator.next((p, v)); $body }).collect() @@ -648,9 +686,9 @@ macro_rules! export_pv_in_record_out { - Period > Length: If a period parameter exceeds the input length, outputs will be NaN until the warmup is satisfied. - NaN Inputs: NaN values in inputs propagate as NaN in the output for the duration of the rolling window. - Negative Params: Negative period/length parameters will raise a ValueError."] - #[derive(uniffi::Object)] pub struct $name { inner: Mutex<$core_type> } - #[uniffi::export] impl $name { - #[uniffi::constructor] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } + #[pyclass] pub struct $name { inner: Mutex<$core_type> } + #[pymethods] impl $name { + #[new] pub fn new($( $param: $param_type ),*) -> Self { Self { inner: Mutex::new(<$core_type>::new($( $param as _ ),*)) } } pub fn next(&self, price: f64, volume: f64) -> $result_type { let $res_var = self.inner.lock().unwrap().next((price, volume)); $body } } } @@ -669,7 +707,7 @@ export_ohlc_in_1_out!(Atr, ATR, (period: u64)); export_ohlc_in_1_out!(Adx, ADX, (period: u64)); export_ohlc_in_1_out!(Cci, CCI, (period: u64)); -#[uniffi::export] +#[pyfunction] pub fn stoch( high: Vec, low: Vec, @@ -694,13 +732,13 @@ pub fn stoch( }) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct Stoch { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl Stoch { - #[uniffi::constructor] + #[new] pub fn new(fastk: u64, slowk: u64, slowd: u64) -> Self { Self { inner: Mutex::new(STOCH::new( @@ -729,7 +767,7 @@ export_ohlc_in_1_out!(Willr, WILLR, (period: u64)); export_1_in_1_out!(Dema, DEMA, (period: u64)); export_1_in_1_out!(Tema, TEMA, (period: u64)); -#[uniffi::export] +#[pyfunction] pub fn ichimoku( high: Vec, low: Vec, @@ -751,13 +789,13 @@ pub fn ichimoku( }) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct Ichimoku { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl Ichimoku { - #[uniffi::constructor] + #[new] pub fn new(tenkan: u64, kijun: u64, senkou_b: u64) -> Self { Self { inner: Mutex::new(CoreIchimoku::new( @@ -812,7 +850,7 @@ export_1_in_1_out!(CorrelationTrend, CoreCorrelationTrend, (length: u64)); export_1_in_1_out!(CyberneticOscillator, CoreCyberneticOscillator, (hp_length: u64, lp_length: u64, rms_len: u64)); export_hl_in_1_out!(Dmh, CoreDMH, (length: u64)); -#[uniffi::export] +#[pyfunction] pub fn donchian(high: Vec, low: Vec, period: u64) -> Vec { let mut it = CoreDonchian::new(period as usize); high.iter() @@ -827,13 +865,13 @@ pub fn donchian(high: Vec, low: Vec, period: u64) -> Vec, } -#[uniffi::export] +#[pymethods] impl Donchian { - #[uniffi::constructor] + #[new] pub fn new(period: u64) -> Self { Self { inner: Mutex::new(CoreDonchian::new(period as usize)), @@ -881,7 +919,7 @@ export_1_in_vec_out!(GriffithsSpectrum, CoreGriffithsSpectrum, (lower_bound: u64 export_1_in_1_out!(Hamming, CoreHamming, (period: u64, pedestal: f64)); export_1_in_1_out!(Hann, CoreHann, (period: u64)); -#[uniffi::export] +#[pyfunction] pub fn heikin_ashi( open: Vec, high: Vec, @@ -904,13 +942,13 @@ pub fn heikin_ashi( }) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct HeikinAshi { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl HeikinAshi { - #[uniffi::constructor] + #[new] pub fn new() -> Self { Self { inner: Mutex::new(CoreHeikinAshi::new()), @@ -933,7 +971,7 @@ export_1_in_1_out!(EhlersWma4, CoreEhlersWma4, ()); export_1_in_1_out!(InstantaneousTrendline, CoreInstantaneousTrendline, ()); export_1_in_record_out!(UndersampledDoubleMa, CoreUDMA, UdmaResult, (fast_len: u64, slow_len: u64, samp_per: u64), res, UdmaResult { fast: res.0, slow: res.1 }); -#[uniffi::export] +#[pyfunction] pub fn keltner( high: Vec, low: Vec, @@ -956,13 +994,13 @@ pub fn keltner( }) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct Keltner { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl Keltner { - #[uniffi::constructor] + #[new] pub fn new(ema_period: u64, atr_period: u64, multiplier: f64) -> Self { Self { inner: Mutex::new(CoreKeltner::new( @@ -1000,7 +1038,7 @@ export_1_in_record_out!( ); export_co_in_1_out!(OcPriceRsi, CoreOcPriceRSI, (period: u64)); -#[uniffi::export] +#[pyfunction] pub fn pivot_points(high: Vec, low: Vec, close: Vec) -> Vec { let mut it = CorePivotPoints::new(); high.iter() @@ -1018,13 +1056,13 @@ pub fn pivot_points(high: Vec, low: Vec, close: Vec) -> Vec, } -#[uniffi::export] +#[pymethods] impl PivotPoints { - #[uniffi::constructor] + #[new] pub fn new() -> Self { Self { inner: Mutex::new(CorePivotPoints::new()), @@ -1058,18 +1096,18 @@ export_1_in_record_out!( } ); -#[uniffi::export] +#[pyfunction] pub fn swiss_army_knife(series: Vec, mode: SwissMode, period: u64, delta: f64) -> Vec { let mut it = CoreSwissArmyKnife::new(mode.into(), period as usize, delta); series.iter().map(|&x| it.next(x)).collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct SwissArmyKnife { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl SwissArmyKnife { - #[uniffi::constructor] + #[new] pub fn new(mode: SwissMode, period: u64, delta: f64) -> Self { Self { inner: Mutex::new(CoreSwissArmyKnife::new(mode.into(), period as usize, delta)), @@ -1088,13 +1126,13 @@ export_1_in_record_out!( res, res.into() ); -#[derive(uniffi::Object)] +#[pyclass] pub struct RobustnessEvaluator { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl RobustnessEvaluator { - #[uniffi::constructor] + #[new] pub fn new() -> Self { Self { inner: Mutex::new(CoreRobustnessEvaluator::new()), @@ -1110,7 +1148,7 @@ impl RobustnessEvaluator { export_ohlc_in_record_out!(TtmSqueeze, CoreTtmSqueeze, SuperTrendResult, (period: u64, mult_bb: f64, mult_kc: f64), res, SuperTrendResult { value: res.0, direction: if res.1 { 1 } else { 0 } }); -#[uniffi::export] +#[pyfunction] pub fn ultimate_bands(series: Vec, length: u64, num_sds: f64) -> Vec { let mut it = CoreUltimateBands::new(length as usize, num_sds); series @@ -1125,13 +1163,13 @@ pub fn ultimate_bands(series: Vec, length: u64, num_sds: f64) -> Vec, } -#[uniffi::export] +#[pymethods] impl UltimateBands { - #[uniffi::constructor] + #[new] pub fn new(length: u64, num_sds: f64) -> Self { Self { inner: Mutex::new(CoreUltimateBands::new(length as usize, num_sds)), @@ -1147,7 +1185,7 @@ impl UltimateBands { } } -#[uniffi::export] +#[pyfunction] pub fn ultimate_channel( high: Vec, low: Vec, @@ -1170,13 +1208,13 @@ pub fn ultimate_channel( }) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct UltimateChannel { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl UltimateChannel { - #[uniffi::constructor] + #[new] pub fn new(length: u64, str_length: u64, num_strs: f64) -> Self { Self { inner: Mutex::new(CoreUltimateChannel::new( @@ -1199,7 +1237,7 @@ impl UltimateChannel { export_1_in_1_out!(UltimateSmoother, CoreUltimateSmoother, (period: u64)); export_1_in_1_out!(Usi, CoreUSI, (length: u64)); -#[uniffi::export] +#[pyfunction] pub fn ad(high: Vec, low: Vec, close: Vec, volume: Vec) -> Vec { let mut it = CoreAD::new(); high.iter() @@ -1209,13 +1247,13 @@ pub fn ad(high: Vec, low: Vec, close: Vec, volume: Vec) -> V .map(|(((&h, &l), &c), &v)| it.next((h, l, c, v))) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct Ad { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl Ad { - #[uniffi::constructor] + #[new] pub fn new() -> Self { Self { inner: Mutex::new(CoreAD::new()), @@ -1226,7 +1264,7 @@ impl Ad { } } -#[uniffi::export] +#[pyfunction] pub fn adosc( high: Vec, low: Vec, @@ -1243,13 +1281,13 @@ pub fn adosc( .map(|(((&h, &l), &c), &v)| it.next((h, l, c, v))) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct Adosc { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl Adosc { - #[uniffi::constructor] + #[new] pub fn new(fast: u64, slow: u64) -> Self { Self { inner: Mutex::new(CoreADOSC::new(fast as usize, slow as usize)), @@ -1263,7 +1301,7 @@ impl Adosc { export_pv_in_1_out!(Obv, CoreOBV, ()); export_ohlc_in_record_out!(Vortex, CoreVortex, VortexResult, (period: u64), res, VortexResult { plus: res.0, minus: res.1 }); -#[uniffi::export] +#[pyfunction] pub fn anchored_vwap(price: Vec, volume: Vec, anchor: Vec) -> Vec { let mut it = CoreAnchoredVWAP::new(); price @@ -1273,13 +1311,13 @@ pub fn anchored_vwap(price: Vec, volume: Vec, anchor: Vec) -> Ve .map(|((&p, &v), &a)| it.next((p, v, a))) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct AnchoredVwap { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl AnchoredVwap { - #[uniffi::constructor] + #[new] pub fn new() -> Self { Self { inner: Mutex::new(CoreAnchoredVWAP::new()), @@ -1298,7 +1336,7 @@ export_1_in_1_out!(Rsih, CoreRSIH, (length: u64)); export_1_in_record_out!(VossPredictor, CoreVossPredictor, VossPredictorResult, (period: u64, predict: u64), res, VossPredictorResult { filt: res.0, voss: res.1 }); export_1_in_1_out!(SyntheticOscillator, CoreSyntheticOscillator, (lower_bound: u64, upper_bound: u64)); -#[uniffi::export] +#[pyfunction] pub fn cycletrendanalytics( series: Vec, min_length: u64, @@ -1316,13 +1354,13 @@ pub fn cycletrendanalytics( }) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct CycleTrendAnalytics { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl CycleTrendAnalytics { - #[uniffi::constructor] + #[new] pub fn new(min_length: u64, max_length: u64) -> Self { Self { inner: Mutex::new(CoreCycleTrendAnalytics::new( @@ -1381,7 +1419,7 @@ export_1_in_1_out!(RocketRsi, CoreRocketRSI, (rsi_length: u64, smooth_length: u6 export_1_in_1_out!(Trendflex, CoreTrendflex, (length: u64)); export_1_in_1_out!(TruncatedBandpass, CoreTruncatedBandpass, (period: u64, bandwidth: f64, length: u64)); -#[uniffi::export] +#[pyfunction] pub fn volumeprofile( price: Vec, volume: Vec, @@ -1402,13 +1440,13 @@ pub fn volumeprofile( }) .collect() } -#[derive(uniffi::Object)] +#[pyclass] pub struct VolumeProfile { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl VolumeProfile { - #[uniffi::constructor] + #[new] pub fn new(period: u64, bins: u64) -> Self { Self { inner: Mutex::new(CoreVolumeProfile::new(period as usize, bins as usize)), @@ -1443,35 +1481,35 @@ impl From // --- Options India --- -#[uniffi::export] +#[pyfunction] pub fn bs_call_price(s: f64, k: f64, r: f64, t: f64, sigma: f64) -> f64 { options_india::bs_call_price(s, k, r, t, sigma) } -#[uniffi::export] +#[pyfunction] pub fn bs_put_price(s: f64, k: f64, r: f64, t: f64, sigma: f64) -> f64 { options_india::bs_put_price(s, k, r, t, sigma) } -#[uniffi::export] +#[pyfunction] pub fn bs_delta(s: f64, k: f64, r: f64, t: f64, sigma: f64, is_call: bool) -> f64 { options_india::bs_delta(s, k, r, t, sigma, is_call) } -#[uniffi::export] +#[pyfunction] pub fn bs_gamma(s: f64, k: f64, r: f64, t: f64, sigma: f64) -> f64 { options_india::bs_gamma(s, k, r, t, sigma) } -#[uniffi::export] +#[pyfunction] pub fn bs_theta(s: f64, k: f64, r: f64, t: f64, sigma: f64, is_call: bool) -> f64 { options_india::bs_theta(s, k, r, t, sigma, is_call) } -#[uniffi::export] +#[pyfunction] pub fn bs_vega(s: f64, k: f64, r: f64, t: f64, sigma: f64) -> f64 { options_india::bs_vega(s, k, r, t, sigma) } -#[uniffi::export] +#[pyfunction] pub fn bs_rho(s: f64, k: f64, r: f64, t: f64, sigma: f64, is_call: bool) -> f64 { options_india::bs_rho(s, k, r, t, sigma, is_call) } -#[uniffi::export] +#[pyfunction] pub fn implied_vol( market_price: f64, s: f64, @@ -1493,19 +1531,19 @@ pub fn implied_vol( Some(iv) } } -#[uniffi::export] +#[pyfunction] pub fn max_pain(strikes: Vec, ce_oi: Vec, pe_oi: Vec, lot_size: u32) -> f64 { options_india::max_pain(&strikes, &ce_oi, &pe_oi, lot_size) } -#[uniffi::export] +#[pyfunction] pub fn strike_pcr(ce_oi: Vec, pe_oi: Vec) -> Vec { options_india::strike_pcr(&ce_oi, &pe_oi) } -#[uniffi::export] +#[pyfunction] pub fn chain_pcr(ce_oi: Vec, pe_oi: Vec) -> f64 { options_india::chain_pcr(&ce_oi, &pe_oi) } -#[uniffi::export] +#[pyfunction] pub fn oi_zones(strikes: Vec, ce_oi: Vec, pe_oi: Vec, n: u64) -> OIZonesResult { let zones = options_india::oi_zones(&strikes, &ce_oi, &pe_oi, n as usize); OIZonesResult { @@ -1513,7 +1551,7 @@ pub fn oi_zones(strikes: Vec, ce_oi: Vec, pe_oi: Vec, n: u64) -> support_strikes: zones.support_strikes, } } -#[uniffi::export] +#[pyfunction] pub fn gex_per_strike( spot: f64, strikes: Vec, @@ -1534,11 +1572,11 @@ pub fn gex_per_strike( }) .collect() } -#[uniffi::export] +#[pyfunction] pub fn gex_flip_strike(strikes: Vec, net_gex: Vec) -> Option { options_india::gex_flip_strike(&strikes, &net_gex) } -#[uniffi::export] +#[pyfunction] pub fn atm_straddle( spot: f64, strikes: Vec, @@ -1552,19 +1590,19 @@ pub fn atm_straddle( implied_move_pct: pct, } } -#[uniffi::export] +#[pyfunction] pub fn synthetic_futures(strikes: Vec, ce_ltp: Vec, pe_ltp: Vec) -> Vec { options_india::synthetic_futures(&strikes, &ce_ltp, &pe_ltp) } -#[uniffi::export] +#[pyfunction] pub fn moneyness(spot: f64, strike: f64) -> String { options_india::moneyness(spot, strike).to_string() } -#[uniffi::export] +#[pyfunction] pub fn nse_lot_size(symbol: String) -> Option { options_india::nse_lot_size(&symbol) } -#[uniffi::export] +#[pyfunction] pub fn nse_risk_free_rate() -> f64 { options_india::NSE_RISK_FREE_RATE } @@ -1577,7 +1615,8 @@ pub fn nse_risk_free_rate() -> f64 { // Sources: features/mod.rs + the four * .rs files (wrapping Ehlers + Hurst). // ============================================================================ -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct CyberCycleFeaturesResult { pub cycle: f64, pub trigger: f64, @@ -1585,13 +1624,13 @@ pub struct CyberCycleFeaturesResult { pub trigger_signal: f64, } -#[derive(uniffi::Object)] +#[pyclass] pub struct CyberCycleFeatureExtractor { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl CyberCycleFeatureExtractor { - #[uniffi::constructor] + #[new] pub fn new(length: u64) -> Self { Self { inner: Mutex::new(CoreCyberCycleFE::new(length as usize)), @@ -1607,7 +1646,7 @@ impl CyberCycleFeatureExtractor { } } } -#[uniffi::export] +#[pyfunction] pub fn cyber_cycle_features(length: u64, series: Vec) -> Vec { let mut ext = CoreCyberCycleFE::new(length as usize); series @@ -1624,20 +1663,21 @@ pub fn cyber_cycle_features(length: u64, series: Vec) -> Vec -99 sentinel for FFI simplicity in some consumers) pub regime_label: i32, } -#[derive(uniffi::Object)] +#[pyclass] pub struct HurstFeatureExtractor { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl HurstFeatureExtractor { - #[uniffi::constructor] + #[new] pub fn new(period: u64) -> Self { Self { inner: Mutex::new(CoreHurstFE::new(period as usize)), @@ -1651,7 +1691,7 @@ impl HurstFeatureExtractor { } } } -#[uniffi::export] +#[pyfunction] pub fn hurst_features(period: u64, series: Vec) -> Vec { let mut ext = CoreHurstFE::new(period as usize); series @@ -1666,19 +1706,20 @@ pub fn hurst_features(period: u64, series: Vec) -> Vec .collect() } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct InstantaneousTrendlineFeaturesResult { pub trend: f64, pub strength: f64, } -#[derive(uniffi::Object)] +#[pyclass] pub struct InstantaneousTrendlineFeatureExtractor { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl InstantaneousTrendlineFeatureExtractor { - #[uniffi::constructor] + #[new] pub fn new() -> Self { Self { inner: Mutex::new(CoreITFE::new()), @@ -1692,7 +1733,7 @@ impl InstantaneousTrendlineFeatureExtractor { } } } -#[uniffi::export] +#[pyfunction] pub fn instantaneous_trendline_features( series: Vec, ) -> Vec { @@ -1709,18 +1750,19 @@ pub fn instantaneous_trendline_features( .collect() } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct TrendflexFeaturesResult { pub trendflex: f64, } -#[derive(uniffi::Object)] +#[pyclass] pub struct TrendflexFeatureExtractor { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl TrendflexFeatureExtractor { - #[uniffi::constructor] + #[new] pub fn new(length: u64) -> Self { Self { inner: Mutex::new(CoreTrendflexFE::new(length as usize)), @@ -1733,7 +1775,7 @@ impl TrendflexFeatureExtractor { } } } -#[uniffi::export] +#[pyfunction] pub fn trendflex_features(length: u64, series: Vec) -> Vec { let mut ext = CoreTrendflexFE::new(length as usize); series @@ -1751,18 +1793,19 @@ pub fn trendflex_features(length: u64, series: Vec) -> Vec, } -#[uniffi::export] +#[pymethods] impl GriffithsDominantCycleFeatureExtractor { - #[uniffi::constructor] + #[new] pub fn new(lower: u64, upper: u64, length: u64) -> Self { Self { inner: Mutex::new(CoreGriffithsDCFE::new( @@ -1779,7 +1822,7 @@ impl GriffithsDominantCycleFeatureExtractor { } } } -#[uniffi::export] +#[pyfunction] pub fn griffiths_dominant_cycle_features( lower: u64, upper: u64, @@ -1798,13 +1841,15 @@ pub fn griffiths_dominant_cycle_features( .collect() } -#[derive(uniffi::Object)] +// name = "BullBearHmm" preserves the uniffi-era Python symbol (uniffi re-cased the +// `HMM` capital-run via heck); keeps the generated _ta registry / metadata unchanged. +#[pyclass(name = "BullBearHmm")] pub struct BullBearHMM { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl BullBearHMM { - #[uniffi::constructor] + #[new] pub fn bull_bear() -> Self { Self { inner: Mutex::new(CoreHMM::bull_bear()), @@ -1825,7 +1870,8 @@ impl BullBearHMM { } } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct GaussianHmmParamsPy { pub n_states: u32, pub delta: Vec, @@ -1836,7 +1882,8 @@ pub struct GaussianHmmParamsPy { pub lambdas: Vec, } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct GaussianHmmFitResultPy { pub params: GaussianHmmParamsPy, pub log_likelihood: f64, @@ -1891,7 +1938,7 @@ fn core_params_from_py(params: &GaussianHmmParamsPy) -> CoreGaussianHmmParams { .expect("invalid HMM params") } -#[uniffi::export] +#[pyfunction] pub fn fit_gaussian_hmm( observations: Vec, n_states: u32, @@ -1932,7 +1979,8 @@ pub fn fit_gaussian_hmm( } } -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct GaussianHmmDiagnosticsPy { pub pseudo_residuals: Vec, pub decode_weighted_means: Vec, @@ -1943,7 +1991,7 @@ pub struct GaussianHmmDiagnosticsPy { pub forecast_mean_h1: f64, } -#[uniffi::export] +#[pyfunction] pub fn gaussian_hmm_diagnostics( params: GaussianHmmParamsPy, observations: Vec, @@ -1969,7 +2017,7 @@ pub fn gaussian_hmm_diagnostics( } } -#[uniffi::export] +#[pyfunction] pub fn gaussian_hmm_forecast_vol( params: GaussianHmmParamsPy, current_state_probs: Vec, @@ -1980,7 +2028,7 @@ pub fn gaussian_hmm_forecast_vol( .expect("forecast vol failed") } -#[uniffi::export] +#[pyfunction] pub fn gaussian_hmm_forecast_state( params: GaussianHmmParamsPy, current_state_probs: Vec, @@ -1991,13 +2039,13 @@ pub fn gaussian_hmm_forecast_state( .expect("forecast state failed") } -#[derive(uniffi::Object)] +#[pyclass] pub struct GaussianHmmFilterPy { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl GaussianHmmFilterPy { - #[uniffi::constructor] + #[new] pub fn from_params(params: GaussianHmmParamsPy) -> Self { let core_params = core_params_from_py(¶ms); Self { @@ -2018,12 +2066,13 @@ impl GaussianHmmFilterPy { } // Simple regime -> feature vector helper (for completeness; notebook primarily uses the extractors) -#[derive(uniffi::Record)] +#[pyclass(get_all)] +#[derive(Clone)] pub struct RegimeFeaturesResult { pub regime_vector: Vec, // 5-elem one-hot style pub regime_label: i32, // 0=Bull,1=Bear,2=Crisis,3=Steady,4+=Cluster } -#[uniffi::export] +#[pyfunction] pub fn regime_to_features(regime_id: u32) -> RegimeFeaturesResult { let regime = match regime_id { 0 => quantwave_core::regimes::MarketRegime::Bull, @@ -2055,13 +2104,13 @@ pub fn regime_to_features(regime_id: u32) -> RegimeFeaturesResult { use quantwave_core::indicators::geometric_patterns::GeometricPatternScanner as CoreGeo; use quantwave_core::indicators::market_structure::{Bias as CoreBias, MarketStructure as CoreMS}; -#[derive(uniffi::Object)] +#[pyclass] pub struct MarketStructure { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl MarketStructure { - #[uniffi::constructor] + #[new] pub fn new(swing_strength: u64) -> Self { Self { inner: Mutex::new(CoreMS::new(swing_strength as usize)), @@ -2098,13 +2147,13 @@ impl MarketStructure { } } -#[derive(uniffi::Object)] +#[pyclass] pub struct GeometricPatternScanner { inner: Mutex, } -#[uniffi::export] +#[pymethods] impl GeometricPatternScanner { - #[uniffi::constructor] + #[new] pub fn new(swing_strength: u64) -> Self { Self { inner: Mutex::new(CoreGeo::new(swing_strength as usize)), @@ -2167,7 +2216,7 @@ impl GeometricPatternScanner { } // Batch helpers for notebook convenience (equivalent to Polars collect over synthetic/real series) -#[uniffi::export] +#[pyfunction] pub fn market_structure_batch( swing_strength: u64, highs: Vec, @@ -2207,3 +2256,350 @@ pub fn market_structure_batch( }) .collect() } + +// --- PyO3 module registration (generated by transform_lib.py; replaces the uniffi scaffolding macro) --- +#[pymodule] +fn _quantwave(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(stoch, m)?)?; + m.add_function(wrap_pyfunction!(ichimoku, m)?)?; + m.add_function(wrap_pyfunction!(donchian, m)?)?; + m.add_function(wrap_pyfunction!(heikin_ashi, m)?)?; + m.add_function(wrap_pyfunction!(keltner, m)?)?; + m.add_function(wrap_pyfunction!(pivot_points, m)?)?; + m.add_function(wrap_pyfunction!(swiss_army_knife, m)?)?; + m.add_function(wrap_pyfunction!(ultimate_bands, m)?)?; + m.add_function(wrap_pyfunction!(ultimate_channel, m)?)?; + m.add_function(wrap_pyfunction!(ad, m)?)?; + m.add_function(wrap_pyfunction!(adosc, m)?)?; + m.add_function(wrap_pyfunction!(anchored_vwap, m)?)?; + m.add_function(wrap_pyfunction!(cycletrendanalytics, m)?)?; + m.add_function(wrap_pyfunction!(volumeprofile, m)?)?; + m.add_function(wrap_pyfunction!(bs_call_price, m)?)?; + m.add_function(wrap_pyfunction!(bs_put_price, m)?)?; + m.add_function(wrap_pyfunction!(bs_delta, m)?)?; + m.add_function(wrap_pyfunction!(bs_gamma, m)?)?; + m.add_function(wrap_pyfunction!(bs_theta, m)?)?; + m.add_function(wrap_pyfunction!(bs_vega, m)?)?; + m.add_function(wrap_pyfunction!(bs_rho, m)?)?; + m.add_function(wrap_pyfunction!(implied_vol, m)?)?; + m.add_function(wrap_pyfunction!(max_pain, m)?)?; + m.add_function(wrap_pyfunction!(strike_pcr, m)?)?; + m.add_function(wrap_pyfunction!(chain_pcr, m)?)?; + m.add_function(wrap_pyfunction!(oi_zones, m)?)?; + m.add_function(wrap_pyfunction!(gex_per_strike, m)?)?; + m.add_function(wrap_pyfunction!(gex_flip_strike, m)?)?; + m.add_function(wrap_pyfunction!(atm_straddle, m)?)?; + m.add_function(wrap_pyfunction!(synthetic_futures, m)?)?; + m.add_function(wrap_pyfunction!(moneyness, m)?)?; + m.add_function(wrap_pyfunction!(nse_lot_size, m)?)?; + m.add_function(wrap_pyfunction!(nse_risk_free_rate, m)?)?; + m.add_function(wrap_pyfunction!(cyber_cycle_features, m)?)?; + m.add_function(wrap_pyfunction!(hurst_features, m)?)?; + m.add_function(wrap_pyfunction!(instantaneous_trendline_features, m)?)?; + m.add_function(wrap_pyfunction!(trendflex_features, m)?)?; + m.add_function(wrap_pyfunction!(griffiths_dominant_cycle_features, m)?)?; + m.add_function(wrap_pyfunction!(fit_gaussian_hmm, m)?)?; + m.add_function(wrap_pyfunction!(gaussian_hmm_diagnostics, m)?)?; + m.add_function(wrap_pyfunction!(gaussian_hmm_forecast_vol, m)?)?; + m.add_function(wrap_pyfunction!(gaussian_hmm_forecast_state, m)?)?; + m.add_function(wrap_pyfunction!(regime_to_features, m)?)?; + m.add_function(wrap_pyfunction!(market_structure_batch, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(sma, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ema, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(wma, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(rsi, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(supertrend, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(macd, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(atr, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(adx, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(cci, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(aroon, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(mama, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(kama, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(t3, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(sar, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(mom, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(roc, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(willr, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(dema, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(tema, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(cg, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(cybercycle, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(fisher, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(inversefisher, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(supersmoother, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(bandpass, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(roofingfilter, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(zerolag, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(choppinessindex, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(classiclaguerre, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(alligator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(alma, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(atrts, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(butterworth2, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(butterworth3, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(channelcycle, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(continuationindex, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(correlationcycle, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(correlationtrend, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(cyberneticoscillator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(dmh, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(dsma, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(emd, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(amdetector, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(fmdemodulator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ehlersautocorrelation, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ehlersfilter, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ehlersloops, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ehlersstochastic, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ehlersultimateoscillator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(fisherhighpass, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(fourierseries, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(fourierdominantcycle, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(fractals, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(frama, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(gaussian, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(generalizedlaguerre, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(griffithsdominantcycle, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(griffithspredictor, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(griffithsspectrum, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(hamming, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(hann, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(highpass, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(hma, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ehlerswma4, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(instantaneoustrendline, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(undersampleddoublema, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(laguerrefilter, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(laguerreoscillator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(laguerrersi, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(noiseelimination, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(pairsrotation, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(phasor, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ocpricersi, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(oneeurofilter, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(projectedmovingaverage, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(precisiontrend, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(reversionindex, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(sinewave, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(systemevaluator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ttmsqueeze, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(ultimatesmoother, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(usi, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(obv, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(vortex, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(wavetrend, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(simplepredictor, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(mad, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(mesastochastic, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(rsih, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(vosspredictor, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(syntheticoscillator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(madh, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(stc, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(homodynediscriminator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(universaloscillator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(trianglefilter, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(htdcperiod, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(htphasor, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(htdcphase, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(htsine, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(httrendmode, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(hurstexponent, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(fracdiff, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(kalmanfilter, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(marketstate, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(recursivemedian, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(recursivemedianoscillator, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(reflex, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(rocketrsi, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(trendflex, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(truncatedbandpass, m)?)?; + Ok(()) +} diff --git a/scripts/build_unified_wheel.py b/scripts/build_unified_wheel.py index e9737f8ea..ab5ee32fd 100644 --- a/scripts/build_unified_wheel.py +++ b/scripts/build_unified_wheel.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 -"""Build a single quantwave PyPI wheel bundling core, backtest, and Polars plugins.""" +"""Build a single quantwave PyPI wheel bundling core, backtest, and Polars plugins. + +Since quantwave-5ipk.10 all three crates are PyO3 ``abi3-py39`` extensions, so every +per-crate wheel already carries the SAME ``cp39-abi3-`` tag. There is no +tag to reconcile — this script just drops the extra ``.so`` files into the base wheel +and repacks. (Before 5ipk.10, quantwave-python was a uniffi ``py3-none`` wheel and this +script hand-reconciled the mismatched tags — the source of the 9gek.1 wheel-tag bug.) +""" from __future__ import annotations @@ -10,133 +17,45 @@ import sys import tempfile import zipfile -from dataclasses import dataclass from pathlib import Path ROOT = Path(__file__).resolve().parent.parent -@dataclass(frozen=True) -class WheelTag: - python: str - abi: str - platform: str - - @classmethod - def parse(cls, tag: str) -> WheelTag: - parts = tag.split("-", 2) - if len(parts) != 3: - raise ValueError(f"invalid wheel tag: {tag!r}") - return cls(python=parts[0], abi=parts[1], platform=parts[2]) - - def serialize(self) -> str: - return f"{self.python}-{self.abi}-{self.platform}" - - @property - def is_py3_none(self) -> bool: - return self.python == "py3" and self.abi == "none" - - @property - def is_abi3(self) -> bool: - return self.abi == "abi3" - - @property - def cp_version(self) -> int | None: - m = re.fullmatch(r"cp(\d+)", self.python) - return int(m.group(1)) if m else None - - -def workspace_version() -> str: - cargo = (ROOT / "Cargo.toml").read_text(encoding="utf-8") - m = re.search(r'^\s*version\s*=\s*"([^"]+)"', cargo, re.M) - if not m: - raise SystemExit("Could not read workspace version from Cargo.toml") - return m.group(1) - - def run(cmd: list[str], **kwargs) -> None: print("+", " ".join(cmd), flush=True) subprocess.check_call(cmd, cwd=ROOT, **kwargs) -def read_wheel_tag(path: Path) -> WheelTag: +def read_wheel_tag(path: Path) -> str: with zipfile.ZipFile(path) as zf: wheel_entries = [n for n in zf.namelist() if n.endswith(".dist-info/WHEEL")] if not wheel_entries: raise ValueError(f"{path.name}: missing WHEEL metadata") - raw = zf.read(wheel_entries[0]).decode("utf-8") - for line in raw.splitlines(): + for line in zf.read(wheel_entries[0]).decode("utf-8").splitlines(): if line.startswith("Tag: "): - return WheelTag.parse(line.removeprefix("Tag: ").strip()) + return line.removeprefix("Tag: ").strip() raise ValueError(f"{path.name}: Tag line missing") -def restrictive_tag(tags: list[WheelTag]) -> WheelTag: - """Intersection of compatibility: never inherit the least restrictive tag.""" - if not tags: - raise ValueError("no wheel tags to merge") - - platforms = {t.platform for t in tags} - if len(platforms) != 1: - raise ValueError(f"cannot merge wheels with different platforms: {platforms}") - - specific_cp: int | None = None - abi3_min: int | None = None - any_py3_none = False - - for tag in tags: - if tag.is_py3_none: - any_py3_none = True - continue - if tag.is_abi3: - cp = tag.cp_version - if cp is None: - raise ValueError(f"abi3 tag without cp prefix: {tag.serialize()}") - abi3_min = cp if abi3_min is None else max(abi3_min, cp) - continue - cp = tag.cp_version - if cp is not None and tag.abi == f"cp{cp}": - specific_cp = cp if specific_cp is None else max(specific_cp, cp) - continue - raise ValueError(f"unsupported wheel tag: {tag.serialize()}") - - if specific_cp is not None: - return WheelTag(f"cp{specific_cp}", f"cp{specific_cp}", tags[0].platform) - if abi3_min is not None: - return WheelTag(f"cp{abi3_min}", "abi3", tags[0].platform) - if any_py3_none: - return WheelTag("py3", "none", tags[0].platform) - raise ValueError("could not derive merged wheel tag") - - -def write_wheel_tag(dist_info: Path, tag: WheelTag) -> None: - wheel_file = dist_info / "WHEEL" - lines = wheel_file.read_text(encoding="utf-8").splitlines() - out: list[str] = [] - replaced = False - for line in lines: - if line.startswith("Tag: "): - out.append(f"Tag: {tag.serialize()}") - replaced = True - else: - out.append(line) - if not replaced: - out.append(f"Tag: {tag.serialize()}") - wheel_file.write_text("\n".join(out) + "\n", encoding="utf-8") +def assert_uniform_abi3(paths: list[Path]) -> str: + """Every crate must ship the same cp3x-abi3- tag (5ipk.10 invariant).""" + tags = {p.name: read_wheel_tag(p) for p in paths} + distinct = set(tags.values()) + if len(distinct) != 1: + raise SystemExit(f"wheels have divergent tags (expected one abi3 tag): {tags}") + tag = distinct.pop() + if not re.fullmatch(r"cp\d+-abi3-.+", tag): + raise SystemExit( + f"expected a cp3x-abi3- tag but got {tag!r}; is a crate still " + "building a non-abi3 (uniffi py3-none or cpXY) wheel?" + ) + return tag def maturin_build(manifest: Path, out: Path, release: bool) -> None: out.mkdir(parents=True, exist_ok=True) - cmd = [ - sys.executable, - "-m", - "maturin", - "build", - "--manifest-path", - str(manifest), - "--out", - str(out), - ] + cmd = [sys.executable, "-m", "maturin", "build", "--manifest-path", str(manifest), "--out", str(out)] if release: cmd.append("--release") run(cmd) @@ -146,23 +65,19 @@ def pick_wheel(dist: Path, prefix: str) -> Path: wheels = sorted(dist.glob(f"{prefix}*.whl")) if not wheels: raise SystemExit(f"No wheel matching {prefix!r} in {dist}") - if len(wheels) > 1: - # Prefer platform-specific wheels over py3-none-any when both exist. - platform = [w for w in wheels if "py3-none" not in w.name] - wheels = platform or wheels return wheels[-1] -def merge_wheels(base: Path, extras: list[Path], out_dir: Path) -> Path: +def bundle(base: Path, extras: list[Path], out_dir: Path) -> Path: + """Unpack the base wheel, copy the extras' payload (their .so files) into it, repack. + + All wheels share one abi3 tag, so the base wheel's tag is kept verbatim. + """ try: import wheel # noqa: F401 except ImportError: subprocess.check_call([sys.executable, "-m", "pip", "install", "wheel"]) - input_tags = [read_wheel_tag(base)] + [read_wheel_tag(extra) for extra in extras] - merged_tag = restrictive_tag(input_tags) - print(f"Merged wheel tag: {merged_tag.serialize()} (from {[t.serialize() for t in input_tags]})") - with tempfile.TemporaryDirectory(prefix="quantwave-wheel-") as td: work = Path(td) unpack_root = work / "unpack" @@ -170,8 +85,8 @@ def merge_wheels(base: Path, extras: list[Path], out_dir: Path) -> Path: run([sys.executable, "-m", "wheel", "unpack", str(base), "-d", str(unpack_root)]) merged = next(unpack_root.iterdir()) - for extra in extras: - extra_unpack = work / "extra" + for i, extra in enumerate(extras): + extra_unpack = work / f"extra{i}" extra_unpack.mkdir() run([sys.executable, "-m", "wheel", "unpack", str(extra), "-d", str(extra_unpack)]) extra_root = next(extra_unpack.iterdir()) @@ -182,28 +97,16 @@ def merge_wheels(base: Path, extras: list[Path], out_dir: Path) -> Path: continue if any(part.endswith(".dist-info") for part in src.parts): continue - rel = src.relative_to(extra_root) - dst = merged / rel + dst = merged / src.relative_to(extra_root) dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst) - shutil.rmtree(extra_unpack) - - dist_info_dirs = list(merged.glob("*.dist-info")) - if not dist_info_dirs: - raise SystemExit("merged wheel missing .dist-info") - write_wheel_tag(dist_info_dirs[0], merged_tag) out_dir.mkdir(parents=True, exist_ok=True) run([sys.executable, "-m", "wheel", "pack", str(merged), "-d", str(out_dir)]) packed = sorted(out_dir.glob("quantwave-*.whl")) if not packed: raise SystemExit(f"wheel pack did not produce quantwave-*.whl in {out_dir}") - final = out_dir / f"quantwave-{workspace_version()}-{merged_tag.serialize()}.whl" - if packed[-1] != final: - if final.exists(): - final.unlink() - packed[-1].rename(final) - return final + return packed[-1] def main() -> int: @@ -226,11 +129,13 @@ def main() -> int: backtest = pick_wheel(staging, "quantwave_backtest_native-") plugins = pick_wheel(staging, "quantwave_plugins-") - args.out.mkdir(parents=True, exist_ok=True) - final = merge_wheels(core, [backtest, plugins], args.out) + tag = assert_uniform_abi3([core, backtest, plugins]) + print(f"All crates ship uniform tag: {tag}") + + final = bundle(core, [backtest, plugins], args.out) print(f"Unified wheel: {final}") return 0 if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/scripts/generate_api_stubs.py b/scripts/generate_api_stubs.py index c1bd6bd32..f87ee69f8 100644 --- a/scripts/generate_api_stubs.py +++ b/scripts/generate_api_stubs.py @@ -102,11 +102,16 @@ def load_manual_aliases() -> dict[str, dict[str, Any]]: def parse_native_symbols() -> tuple[set[str], set[str]]: text = LIB_RS.read_text(encoding="utf-8") batch = set(re.findall(r"^pub fn ([a-z][a-z0-9_]*)\(", text, re.M)) - streaming = set(re.findall(r"^#\[derive\(uniffi::Object\)\] pub struct (\w+)", text, re.M)) - for m in re.finditer(r"^#\[derive\(uniffi::Object\)\]\s+pub struct (\w+)", text, re.M): - streaming.add(m.group(1)) - for m in re.finditer(r"^pub struct (\w+) \{ inner:", text, re.M): - streaming.add(m.group(1)) + streaming: set[str] = set() + # Hand-written streaming classes: a #[pyclass] struct with an `inner:` state field. + # Honor an explicit #[pyclass(name = "...")] (PyO3 keeps the raw Rust identifier, + # so the re-cased uniffi-era symbols are pinned via a name attr). + for m in re.finditer( + r"#\[pyclass(\([^)]*\))?\]\s*\npub struct (\w+)\s*\{[^}]*?\binner\s*:", text + ): + attrs, struct = m.group(1) or "", m.group(2) + name_attr = re.search(r'name\s*=\s*"([^"]+)"', attrs) + streaming.add(name_attr.group(1) if name_attr else struct) for type_name in re.findall(r"export_[a-z0-9_]+!\s*\(\s*(\w+)", text): batch.add(pascal_to_snake(type_name)) streaming.add(type_name) From 79d7433ea637de4169e23202ff1d7f2495104ffa Mon Sep 17 00:00:00 2001 From: Mayank Lavania Date: Sun, 12 Jul 2026 12:03:35 +0530 Subject: [PATCH 2/3] fix(ffi): expose named constructors as staticmethods; drop dead uniffi-bindgen Pre-push wheel smoke caught two named uniffi constructors that the blanket `#[uniffi::constructor]` -> `#[new]` transform mis-converted: BullBearHMM:: bull_bear and GaussianHmmFilterPy::from_params. uniffi exposed these named constructors as classmethods (Class.bull_bear()), so they map to `#[staticmethod]`, not `#[new]` (which would only allow Class()). Also remove the now-dead `uniffi-bindgen==0.31.0` install from the local verify path (quantwave_verify.sh, git-hooks/pre-push, verify_cache.py). Verified: BullBearHmm.bull_bear().next() and build_feature_matrix work; full unified-wheel pypi_smoke_test passes. Co-Authored-By: Claude Opus 4.8 --- docs/validation.md | 2 +- quantwave-python/src/lib.rs | 7 +++++-- scripts/git-hooks/pre-push | 2 +- scripts/quantwave_verify.sh | 2 +- scripts/verify_cache.py | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/validation.md b/docs/validation.md index fb903c20c..1667bc97c 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -7,7 +7,7 @@ QuantWave's primary credibility claim is **correctness**: one mathematical imple ## Coverage snapshot -**Last updated:** 2026-07-11T09:36:20Z (UTC) +**Last updated:** 2026-07-11T10:01:18Z (UTC) | Metric | Count | Source | |--------|------:|--------| diff --git a/quantwave-python/src/lib.rs b/quantwave-python/src/lib.rs index 025c41208..108d9631e 100644 --- a/quantwave-python/src/lib.rs +++ b/quantwave-python/src/lib.rs @@ -1849,7 +1849,9 @@ pub struct BullBearHMM { } #[pymethods] impl BullBearHMM { - #[new] + // Named alternate constructor: uniffi exposed `#[uniffi::constructor]`-with-a-name + // as a classmethod (`BullBearHmm.bull_bear()`), so it maps to #[staticmethod], not #[new]. + #[staticmethod] pub fn bull_bear() -> Self { Self { inner: Mutex::new(CoreHMM::bull_bear()), @@ -2045,7 +2047,8 @@ pub struct GaussianHmmFilterPy { } #[pymethods] impl GaussianHmmFilterPy { - #[new] + // Named alternate constructor (see BullBearHMM::bull_bear): classmethod, not #[new]. + #[staticmethod] pub fn from_params(params: GaussianHmmParamsPy) -> Self { let core_params = core_params_from_py(¶ms); Self { diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push index 8d233bd32..c546a10a8 100755 --- a/scripts/git-hooks/pre-push +++ b/scripts/git-hooks/pre-push @@ -31,7 +31,7 @@ fi source .venv/bin/activate python3 scripts/verify_cache.py ensure pip-deps -- \ - python3 -m pip install -q pytest polars maturin "uniffi-bindgen==0.31.0" wheel + python3 -m pip install -q pytest polars maturin wheel python3 scripts/verify_cache.py ensure maturin-python -- \ maturin develop --manifest-path quantwave-python/Cargo.toml --release -q diff --git a/scripts/quantwave_verify.sh b/scripts/quantwave_verify.sh index e25f89be6..7ed46062b 100755 --- a/scripts/quantwave_verify.sh +++ b/scripts/quantwave_verify.sh @@ -87,7 +87,7 @@ fi if [[ "$SKIP_WHEEL" -eq 0 ]]; then run_cached wheel bash -c ' echo "-- unified PyPI wheel build + smoke test" - python3 -m pip install -q wheel maturin "uniffi-bindgen==0.31.0" 2>/dev/null || true + python3 -m pip install -q wheel maturin 2>/dev/null || true python3 scripts/build_unified_wheel.py --out dist WHEEL="$(ls -t dist/quantwave-*.whl 2>/dev/null | head -1)" test -n "$WHEEL" || { echo "no quantwave wheel in dist/"; exit 1; } diff --git a/scripts/verify_cache.py b/scripts/verify_cache.py index 913c50978..07e34ce52 100755 --- a/scripts/verify_cache.py +++ b/scripts/verify_cache.py @@ -87,7 +87,7 @@ } PIP_DEPS_KEY = "pip-deps" -PIP_DEPS_SPEC = "pytest polars maturin uniffi-bindgen==0.31.0 wheel" +PIP_DEPS_SPEC = "pytest polars maturin wheel" def cache_enabled() -> bool: From a19f59d7695bc20a971e9a62ca3b92df471b7270 Mon Sep 17 00:00:00 2001 From: Mayank Lavania Date: Sun, 12 Jul 2026 12:45:34 +0530 Subject: [PATCH 3/3] fix(dx): bind native indicator symbols at top level (qw.FracDiff, qw.fracdiff) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dynamic top-level population only bound indicators by SLUG (qw.frac_diff), so the native streaming-class and batch-fn names documented as available (qw.FracDiff, qw.fracdiff, qw.rsi, qw.SuperTrend) were missing — a pre-existing gap left when the old hardcoded `class ta:` aliases were replaced by slug-only population. (Confirmed failing identically on main/uniffi, so not a 5ipk.10 regression; surfaced while validating the PyO3 migration.) Restore native-name binding by iterating dir(_quantwave): bind public callables (classes + batch fns) not already defined, skipping result records/errors (_is_internal_symbol) and the deprecated options helpers (_OPTIONS_SYMBOLS stay routed through __getattr__ so qw.bs_call_price still warns). Auto-syncs as new indicators are added. Fixes test_frac_diff.py (3/3). No regressions: gold parity, options deprecation, metadata codegen, streaming readiness all green (97 passed). The 2 remaining talib failures (test_list_functions/test_rsi_callable) are a distinct TA-Lib map-completeness bug, tracked separately. Co-Authored-By: Claude Opus 4.8 --- quantwave-python/python/quantwave/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/quantwave-python/python/quantwave/__init__.py b/quantwave-python/python/quantwave/__init__.py index 2903cc536..112723b9c 100644 --- a/quantwave-python/python/quantwave/__init__.py +++ b/quantwave-python/python/quantwave/__init__.py @@ -242,6 +242,24 @@ def _resolve_ta_binding(slug: str, entry: dict) -> object: stacklevel=1, ) +# Backward-compat: expose native symbols at the top level by their exact native +# name — streaming classes (``qw.FracDiff``, ``qw.SuperTrend``) and batch functions +# (``qw.fracdiff``, ``qw.rsi``). The slug-based ``.ta`` bindings above take +# precedence (we never overwrite an existing name); result records / errors are +# skipped via ``_is_internal_symbol``. Iterating ``dir(_quantwave)`` keeps this in +# sync as new indicators are added, restoring the aliases the old hardcoded +# ``class ta:`` used to provide. +for _native_name in dir(_quantwave): + if _native_name.startswith("_") or _native_name in globals(): + continue + if _is_internal_symbol(_native_name) or _native_name in _OPTIONS_SYMBOLS: + # Options helpers stay deprecated-only at the top level (routed through + # __getattr__ so ``qw.bs_call_price`` still warns); use ``qw.options.*``. + continue + _native_obj = getattr(_quantwave, _native_name) + if callable(_native_obj): + globals()[_native_name] = _native_obj + # --- Basic Discovery API --- def _build_indicator_names() -> set[str]: