feat(cli): fit the injected walk-forward roller to a short real window (#239)

fit_wf_ms_sizes (pure, unit-tested) passes the fixed 90/30/30-day roller
through byte-identically whenever it fits the resolved campaign window,
and scales it down preserving the 3:1 IS:OOS ratio (step = OOS, one roll
at the minimum) when the window is shorter — so mc/walkforward --real
over a ~30-day window run to completion instead of dead-ending at the
executor with a remedy unreachable from the verb surface. Both dispatch
sites consume the fit; the executor's own check stays for authored
process documents. Verb help documents the fit.

closes #239
This commit is contained in:
2026-07-11 17:14:51 +02:00
parent a081c14532
commit 2a63c5d889
+61 -4
View File
@@ -2365,11 +2365,15 @@ enum Command {
Graph(GraphCmd),
/// Sweep a parameter grid over a strategy or a loaded blueprint.
Sweep(SweepCmd),
/// Walk-forward validation over a strategy or a loaded blueprint.
/// Walk-forward validation over a strategy or a loaded blueprint. Over --real, the
/// fixed 90/30-day roller fits to a --from/--to window shorter than it, preserving
/// the 3:1 IS:OOS ratio, instead of refusing the window outright.
Walkforward(WalkforwardCmd),
/// Grade one candidate across multiple instruments.
Generalize(GeneralizeCmd),
/// Monte-Carlo over synthetic draws, an R-bootstrap, or a loaded blueprint.
/// Monte-Carlo over synthetic draws, an R-bootstrap, or a loaded blueprint. Over
/// --real, the fixed 90/30-day walk-forward roller fits to a --from/--to window
/// shorter than it, preserving the 3:1 IS:OOS ratio, instead of refusing outright.
Mc(McCmd),
/// List or inspect recorded run families.
Runs(RunsCmd),
@@ -2686,6 +2690,32 @@ fn wf_ms_sizes() -> (u64, u64, u64) {
)
}
/// Fit the fixed real-archive walk-forward roller (`wf_ms_sizes`, 90/30/30 days)
/// to a resolved campaign window `(from_ms, to_ms)` (#239). When the fixed
/// IS+OOS span fits inside the window, the sizes come back byte-identical (every
/// existing anchor/e2e over a year-plus window pins this branch). When the window
/// is shorter than IS+OOS, the roller scales DOWN to the window, preserving the
/// fixed 3:1 IS:OOS ratio, with `step_ms == oos_ms` (a window of exactly IS+OOS
/// then yields exactly one roll — never zero). Pure and unit-testable; both
/// `dispatch_walkforward`/`dispatch_mc` consume it in place of the unconditional
/// `wf_ms_sizes()` stamp. The executor's own fit check (`campaign_run.rs`) stays
/// untouched — it still validates an AUTHORED document's declared window as-is.
fn fit_wf_ms_sizes(from_ms: i64, to_ms: i64) -> (u64, u64, u64) {
let (is_ms, oos_ms, step_ms) = wf_ms_sizes();
let span_ms = to_ms.saturating_sub(from_ms).max(0) as u64;
if is_ms + oos_ms <= span_ms {
return (is_ms, oos_ms, step_ms);
}
// Preserve the fixed IS:OOS ratio derived from the constants themselves (not a
// bare literal): OOS = span/(ratio+1) (floor), IS = ratio*OOS, so
// IS+OOS = (ratio+1)*OOS <= span always holds, and step == OOS (one roll over
// the fit window, matching the fixed-size branch's `step_ms == oos_ms`).
let ratio = is_ms / oos_ms;
let oos_fit = span_ms / (ratio + 1);
let is_fit = oos_fit * ratio;
(is_fit, oos_fit, oos_fit)
}
/// Resolve a single optional stop knob (`--stop-length`/`--stop-k`): an absent flag
/// defaults to `default`; a present one parses via [`parse_csv_list`] and refuses
/// unless it holds exactly one value (the stop is a risk regime, not a swept axis).
@@ -3421,7 +3451,7 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
DataChoice::Real { symbol: symbol.clone(), from_ms: from, to_ms: to },
env,
);
let (is_ms, oos_ms, step_ms) = wf_ms_sizes();
let (is_ms, oos_ms, step_ms) = fit_wf_ms_sizes(from_ms, to_ms);
let inv = verb_sugar::SugarInvocation {
axes: &raw_axes,
name,
@@ -3536,7 +3566,7 @@ fn dispatch_mc(a: McCmd, env: &project::Env) {
DataChoice::Real { symbol: symbol.clone(), from_ms: from, to_ms: to },
env,
);
let (is_ms, oos_ms, step_ms) = wf_ms_sizes();
let (is_ms, oos_ms, step_ms) = fit_wf_ms_sizes(from_ms, to_ms);
let inv = verb_sugar::SugarInvocation {
axes: &raw_axes,
name,
@@ -4709,6 +4739,33 @@ mod tests {
assert_eq!(WF_REAL_STEP_NS, 30 * day_ns);
}
/// Property: a window that already fits the fixed 90/30/30-day roller passes
/// it through byte-identical (the year-plus anchor/e2e grade pins rely on this
/// branch never perturbing the fixed sizes).
#[test]
fn fit_wf_ms_sizes_passes_through_when_the_window_already_fits() {
let day_ms: i64 = 24 * 60 * 60 * 1_000;
let from_ms = 0;
let to_ms = 121 * day_ms; // > 90 + 30 days
assert_eq!(fit_wf_ms_sizes(from_ms, to_ms), wf_ms_sizes());
}
/// Property: a window shorter than IS+OOS scales the roller DOWN to the
/// window, preserving the fixed 3:1 IS:OOS ratio, with `step_ms == oos_ms`
/// (one roll over the fit window) and `is_ms + oos_ms <= span_ms` always.
#[test]
fn fit_wf_ms_sizes_scales_down_to_a_short_window_at_3_to_1() {
let day_ms: i64 = 24 * 60 * 60 * 1_000;
let from_ms = 0;
let to_ms = 30 * day_ms; // far shorter than the fixed 90+30-day roller
let span_ms = (to_ms - from_ms) as u64;
let (is_ms, oos_ms, step_ms) = fit_wf_ms_sizes(from_ms, to_ms);
assert_eq!(is_ms, oos_ms * 3, "the fit preserves the fixed 3:1 IS:OOS ratio");
assert_eq!(step_ms, oos_ms, "one roll over the fit window: step == oos");
assert!(is_ms + oos_ms <= span_ms, "the fit roller must fit inside the window");
assert!(oos_ms > 0, "a 30-day window must yield a non-degenerate fit");
}
#[test]
fn sim_optimal_manifest_renders_per_instrument_pip() {
let m = sim_optimal_manifest(vec![], (Timestamp(1), Timestamp(2)), 0, 1.0);