From dcbc51f43234f320c26de0ea02ed43ebb76cd26e Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Fri, 22 May 2026 20:22:16 +0200 Subject: [PATCH] fix(loader): anchor sampler enforces lookback lower bound Multi-resolution sequence builder reads from [anchor + total - lookback, anchor + total), so anchor must be in [max(0, lookback - total), snapshots.len() - (total + max_horizon)). The previous gen_range(0..max_anchor) underflowed for default 3-scale (lookback=1510, total=32) at small anchors. Compute min_anchor = lookback.saturating_sub(total) and sample gen_range(min_anchor..max_anchor) with the ensure! upgraded to check max_anchor > min_anchor. Caught by implementer self-review of SDD-MR Task 3. --- crates/ml-alpha/src/data/loader.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/ml-alpha/src/data/loader.rs b/crates/ml-alpha/src/data/loader.rs index 972d0f628..c9f601801 100644 --- a/crates/ml-alpha/src/data/loader.rs +++ b/crates/ml-alpha/src/data/loader.rs @@ -471,14 +471,24 @@ impl MultiHorizonLoader { let n_files = self.files_loaded.len(); let file_idx = self.rng.gen_range(0..n_files); let lf = &self.files_loaded[file_idx]; - let needed = self.cfg.multi_resolution.total_positions() + max_horizon; + // Multi-resolution anchor sampling has TWO bounds: + // * Upper: anchor + total + max_horizon < snapshots.len() + // (room for the forward-label window after the input edge) + // * Lower: anchor + total >= lookback + // (room to look back across coarsest aggregation scale) + // For the default 3-scale config (total=32, lookback=1510) the lower + // bound is binding: anchor must be ≥ lookback - total = 1478. + let total = self.cfg.multi_resolution.total_positions(); + let lookback = self.cfg.multi_resolution.required_lookback_ticks(); + let min_anchor = lookback.saturating_sub(total); + let max_anchor = lf.snapshots.len().saturating_sub(total + max_horizon); anyhow::ensure!( - lf.snapshots.len() > needed, - "file too short for total_positions={} max_horizon={}: {} <= {}", - self.cfg.multi_resolution.total_positions(), max_horizon, lf.snapshots.len(), needed + max_anchor > min_anchor, + "file too short for multi-resolution(total={total}, lookback={lookback}) max_horizon={max_horizon}: \ + snapshots.len()={} cannot span [{min_anchor}, {max_anchor})", + lf.snapshots.len() ); - let max_anchor = lf.snapshots.len() - needed; - let anchor: usize = self.rng.gen_range(0..max_anchor); + let anchor: usize = self.rng.gen_range(min_anchor..max_anchor); let mut labels: [Vec; N_HORIZONS] = Default::default(); let mut outcome_prof_long: [Vec; N_HORIZONS] = Default::default();