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();