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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user