From 24f1dc8716ea610c98c13034fcb7b49c27a6530d Mon Sep 17 00:00:00 2001 From: Mayank Lavania Date: Mon, 13 Jul 2026 22:07:41 +0530 Subject: [PATCH] fix(python): constructable GaussianHmmParamsPy + dict-like backtest metrics (nhkk) Two record-port gaps from the uniffi->PyO3 migration, surfaced by relocating the previously-CI-invisible tests: 1. GaussianHmmParamsPy is an INPUT params record that Python constructs (qw.GaussianHmmParamsPy(n_states=..., delta=..., ...)), but the ported #[pyclass(get_all)] had no #[new], so construction raised "No constructor defined". Add a #[new] (lambdas defaults to empty = Gaussian mode). Audited all 49 records: this is the only one Python constructs; the rest are read-only outputs. 2. PerformanceMetrics / BacktestStats define __getitem__(str) + keys() but no __contains__, so `"sharpe_ratio" in metrics` fell back to integer-index iteration (getattr(self, 0)) -> "TypeError: attribute name must be string, not 'int'". Add __contains__ and __iter__ so they behave dict-like. Fixes test_hmm_forecast (2), test_ml_feature_backtest_parity, test_strategy_backtest. Full suite 6 -> 2 failures (remaining: pandas-only benchmark test + a stale LazyFrame.ta test_parity, both unrelated). Co-Authored-By: Claude Opus 4.8 --- .../python/quantwave/backtest_types.py | 18 ++++++++++++-- quantwave-py/src/indicators.rs | 24 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/quantwave-py/python/quantwave/backtest_types.py b/quantwave-py/python/quantwave/backtest_types.py index 793fbfc82..b8c0a6760 100644 --- a/quantwave-py/python/quantwave/backtest_types.py +++ b/quantwave-py/python/quantwave/backtest_types.py @@ -28,10 +28,18 @@ def as_dict(self) -> dict[str, float]: def keys(self): return self.as_dict().keys() - + def __getitem__(self, key: str): return getattr(self, key) + def __contains__(self, key: object) -> bool: + # Without this, ``"x" in metrics`` falls back to integer-index iteration + # via __getitem__ (getattr(self, 0)) and raises TypeError. + return isinstance(key, str) and key in self.as_dict() + + def __iter__(self): + return iter(self.keys()) + @dataclass(frozen=True) class BacktestStats: """Core summary statistics from a backtest run.""" @@ -50,6 +58,12 @@ def as_dict(self) -> dict[str, float]: def keys(self): return self.as_dict().keys() - + def __getitem__(self, key: str): return getattr(self, key) + + def __contains__(self, key: object) -> bool: + return isinstance(key, str) and key in self.as_dict() + + def __iter__(self): + return iter(self.keys()) diff --git a/quantwave-py/src/indicators.rs b/quantwave-py/src/indicators.rs index b6d8bcf1c..52baefde9 100644 --- a/quantwave-py/src/indicators.rs +++ b/quantwave-py/src/indicators.rs @@ -1883,6 +1883,30 @@ pub struct GaussianHmmParamsPy { /// Per-state λ (empty → 1.0 per state, Gaussian mode). pub lambdas: Vec, } +#[pymethods] +impl GaussianHmmParamsPy { + // Constructable from Python (input params, not a read-only result record): + // `qw.GaussianHmmParamsPy(n_states, delta, gamma_flat, means, stds, lambdas=[])`. + #[new] + #[pyo3(signature = (n_states, delta, gamma_flat, means, stds, lambdas = Vec::new()))] + pub fn new( + n_states: u32, + delta: Vec, + gamma_flat: Vec, + means: Vec, + stds: Vec, + lambdas: Vec, + ) -> Self { + Self { + n_states, + delta, + gamma_flat, + means, + stds, + lambdas, + } + } +} #[pyclass(get_all)] #[derive(Clone)]