From 8b22fa6c7307e266bc31a2d997d23d19720c8971 Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 17 Jun 2026 14:21:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(aura-engine):=20WalkForwardResult::named?= =?UTF-8?q?=5Fparams=20=E2=80=94=20mirror=20SweepFamily::named=5Fparams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the typed/named convenience view on `WalkForwardResult`, closing the symmetry cycle 0048 left open: it shares the `(space, tag-free Vec)` idiom with `SweepFamily` but had no named-params accessor, so a consumer hand-wrote `zip_params(&result.space, &result.windows[i].run.chosen_params)`. `named_params(window)` pairs each param-space name with that window's chosen value in slot order, delegating to `zip_params` — the direct mirror of `SweepFamily::named_params`. A derived view, no new stored state. closes #76 --- crates/aura-engine/src/walkforward.rs | 53 ++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/aura-engine/src/walkforward.rs b/crates/aura-engine/src/walkforward.rs index 492e672..aa9343d 100644 --- a/crates/aura-engine/src/walkforward.rs +++ b/crates/aura-engine/src/walkforward.rs @@ -13,7 +13,7 @@ use crate::sweep::run_indexed; use crate::{MetricStats, ParamSpec, RunReport, ScalarKind, Timestamp}; -use aura_core::Cell; +use aura_core::{zip_params, Cell, Scalar}; /// One rolling split's time bounds: an in-sample window and the out-of-sample /// window that follows it. Carries ONLY bounds — never tick data (#71). C2 @@ -165,6 +165,15 @@ pub struct WalkForwardResult { pub stitched_oos_equity: Vec<(Timestamp, f64)>, } +impl WalkForwardResult { + /// The chosen params of the `window`-th outcome paired with their names — a + /// derived view over the carried param-space (reuses [`zip_params`]); the + /// walk-forward mirror of [`SweepFamily::named_params`](crate::SweepFamily). + pub fn named_params(&self, window: usize) -> Vec<(String, Scalar)> { + zip_params(&self.space, &self.windows[window].run.chosen_params) + } +} + /// Roll the windows, run `run_window` on each disjointly in parallel (C1, via the /// shared [`run_indexed`](crate::sweep) core), then stitch the OOS equity into one /// continuous curve. The varying dimension is the *data window* (C12 axis 3). @@ -437,6 +446,48 @@ mod tests { assert_eq!(stab[0].mean, 0.75); } + #[test] + fn named_params_pairs_each_name_with_the_chosen_window_value() { + // #76: WalkForwardResult::named_params(w) is the typed/named view — it + // pairs each param-space NAME with window `w`'s chosen VALUE, in slot + // order, reading the right window. Mirror of SweepFamily::named_params. + // Expected vectors are spelled out explicitly (not via zip_params) so the + // assertion pins the observable pairing, not the implementation. + let mk = |fast: i64, scale: f64| WindowOutcome { + bounds: WindowBounds { + is: (Timestamp(0), Timestamp(0)), + oos: (Timestamp(0), Timestamp(0)), + }, + run: WindowRun { + chosen_params: vec![Cell::from_i64(fast), Cell::from_f64(scale)], + oos_equity: vec![], + oos_report: dummy_report(), + }, + }; + let result = WalkForwardResult { + space: vec![ + ParamSpec { name: "sma.fast".to_string(), kind: ScalarKind::I64 }, + ParamSpec { name: "exposure.scale".to_string(), kind: ScalarKind::F64 }, + ], + windows: vec![mk(8, 0.25), mk(13, 0.75)], + stitched_oos_equity: vec![], + }; + assert_eq!( + result.named_params(0), + vec![ + ("sma.fast".to_string(), Scalar::i64(8)), + ("exposure.scale".to_string(), Scalar::f64(0.25)), + ], + ); + assert_eq!( + result.named_params(1), + vec![ + ("sma.fast".to_string(), Scalar::i64(13)), + ("exposure.scale".to_string(), Scalar::f64(0.75)), + ], + ); + } + #[test] fn roller_rolling_emits_consecutive_oos_windows() { // spec §Testing 1: rolling, origin=0, is_len=10, oos_len=5, step=5, end=24.