diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 45358f1..2d5e5de 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -3078,11 +3078,33 @@ fn blueprint_mc_family(doc: &str, n_seeds: u64, data: &DataSource) -> Result = (1..=n_seeds).collect(); let base_point: Vec = Vec::new(); - Ok(monte_carlo(&base_point, &seeds, |seed, _base| { + let family = monte_carlo(&base_point, &seeds, |seed, _base| { let sources = synthetic_walk_sources(seed); let window = window_of(&sources).expect("non-empty synthetic walk"); run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo) - })) + }); + // Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's + // metrics are bit-identical to the first, no seed reached a distinguishable realization — + // the strategy never warmed over the fixed synthetic walk (e.g. a lookback as deep as the + // walk is long), so the "distribution" is a single point masquerading as a family: a wrong + // result with no error. Compare `metrics`, not the whole `RunReport` — the manifest's + // `seed` differs per draw by construction, so a whole-report compare could never detect the + // collapse; the metrics are the realization the seed is meant to move. A single-draw MC + // (n == 1) is trivially "all identical" and is NOT this cross-seed condition, so it passes. + if family.draws.len() >= 2 + && family + .draws + .iter() + .all(|d| d.report.metrics == family.draws[0].report.metrics) + { + return Err( + "mc is vacuous: every seed produced an identical result — the strategy never warmed \ + over the synthetic walk, so no seed reached a distinguishable realization; use a \ + shallower-lookback blueprint or a longer walk" + .to_string(), + ); + } + Ok(family) } /// `aura sweep --axis = …`: sweep a loaded signal over its @@ -5486,6 +5508,24 @@ mod tests { assert!(m[0] != m[1] || m[1] != m[2], "seeds must yield differing realizations"); } + #[test] + fn blueprint_mc_family_rejects_vacuous_deep_lookback() { + // Property: the mc family builder REFUSES a silent-vacuous Monte-Carlo — one where + // every per-seed draw collapses to a bit-identical realization — by RETURNING a named + // error rather than an `Ok` family that looks like a real (but indistinguishable) + // distribution. A CLOSED deep-lookback signal whose slow SMA length (60) equals the + // fixed 60-bar synthetic walk never warms, so every seed yields zero trades and thus + // identical metrics; that is a wrong result with no error (C10 refuse-don't-guess). + let deep = stage1_signal(Some(2), Some(60)); // slow len == walk len -> never warms + let doc = blueprint_to_json(&deep).expect("serializes"); + let err = blueprint_mc_family(&doc, 3, &DataSource::Synthetic) + .expect_err("a vacuous (all-identical) Monte-Carlo is rejected, not returned"); + assert!( + err.contains("vacuous") || err.contains("identical"), + "names the vacuous/degenerate condition: {err}" + ); + } + #[test] fn blueprint_mc_family_rejects_an_open_blueprint() { // Property: the mc family builder REFUSES an open blueprint (free knobs) by RETURNING