feat(cli): walkforward + mc go blueprint-generic (#220 slice 2)
Tasks 3-4 of the verb-axis-generalization plan: - walkforward: the welded r-sma campaign branch moves out of the None arm into the blueprint mode, split on --real (campaign sugar) vs the unchanged synthetic family path; WalkforwardCmd drops --strategy/--fast/--slow; the full-window clip block, wf_ms_sizes and WINNER_SELECTION_METRIC move unchanged; the --select plateau #215 refusal survives. walkforward_summary_json_from_reports is de-welded: the axis-name list is passed in from the invocation instead of the const AXES r-sma quadruple. - mc: McCmd drops the welded flags and gains --axis; the blueprint arm splits on --real (campaign pipeline: requires --axis, refuses --seeds) vs the unchanged synthetic seed family (--seeds); block-len/ resamples/seed defaults and the --name/--trace refusal stay byte-identical. - translate_walkforward/run_walkforward_sugar and translate_mc/ run_mc_sugar adopt SugarInvocation (+ WfWindows/McKnobs); ZERO #[allow(clippy::too_many_arguments)] remain in verb_sugar.rs (#214 complete). - Tests: wf/mc e2e groups migrate argv-only (grade anchors keep all pinned floats verbatim); collateral usage/retired-token tests assert clap's structural rejection of the deleted flags; the synthetic family control groups pass unmodified. Necessary deviation from the plan's literal step text: re-added #[allow(clippy::type_complexity)] on walkforward_args_from (clippy -D warnings flags the 6-tuple; mirrors its siblings). Held quality nits (validate/strip block now quadruplicated across the four dispatch fns; inline exit-mapping recurrence) are deliberate plan-holds — dedup follows as the rule-of-three cleanup at cycle end. Verification: full workspace suite green in the loop's independent verify; build + clippy -D warnings clean. refs #220, refs #214
This commit is contained in:
@@ -1048,12 +1048,9 @@ fn mc_rejects_real_flag_with_usage_exit_2() {
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura mc --real");
|
||||
assert_eq!(out.status.code(), Some(2), "mc --real must exit 2; status: {:?}", out.status);
|
||||
assert!(out.stdout.is_empty(), "mc --real must not emit a run on stdout: {:?}", out.stdout);
|
||||
assert_eq!(out.status.code(), Some(2), "mc --real without a blueprint is a usage error");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("Usage"), "mc --real stderr must carry usage: {stderr:?}");
|
||||
// the exclusion is total: no `runs/` registry write either.
|
||||
assert!(!dir.join("runs").exists(), "mc --real must not start a real run");
|
||||
assert!(stderr.contains("Usage: aura mc"), "usage refusal: {stderr}");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -1099,11 +1096,11 @@ fn family_window_flag_without_real_refuses_with_usage_exit_2() {
|
||||
/// regression that drops the prefix again would NOT be caught by the older
|
||||
/// `family_window_flag_without_real_refuses_with_usage_exit_2` pin above, which
|
||||
/// only checks for the verb token and `--real` — not the `Usage: aura` lead-in.
|
||||
/// An unknown `--strategy` name reaches this same closure on both commands, so
|
||||
/// it is the smallest trigger for each.
|
||||
/// An unknown `--strategy` name reaches sweep's closure; the bare verb reaches
|
||||
/// walkforward's.
|
||||
#[test]
|
||||
fn builtin_branch_usage_lines_lead_with_the_house_style_prefix() {
|
||||
for (args, verb) in [(&["sweep", "--strategy", "bogus"][..], "sweep"), (&["walkforward", "--strategy", "bogus"][..], "walkforward")] {
|
||||
for (args, verb) in [(&["sweep", "--strategy", "bogus"][..], "sweep"), (&["walkforward"][..], "walkforward")] {
|
||||
let dir = temp_cwd("builtin_usage_prefix");
|
||||
let out = Command::new(BIN)
|
||||
.args(args)
|
||||
@@ -1123,8 +1120,9 @@ fn builtin_branch_usage_lines_lead_with_the_house_style_prefix() {
|
||||
/// not just the first. This is the one lockstep-pinned site
|
||||
/// (`mc_rejects_real_flag_with_usage_exit_2` above), but that pin only checks
|
||||
/// for a single `"Usage"` occurrence — a regression that fixed only the first
|
||||
/// alternative (leaving `… | mc --strategy r-sma …` unprefixed) would still
|
||||
/// pass it. `--seeds` with no blueprint file is the built-in branch's own
|
||||
/// alternative (leaving the second `| aura mc <blueprint.json> --real …`
|
||||
/// alternative unprefixed) would still pass it. `--seeds` with no blueprint
|
||||
/// file is the built-in branch's own
|
||||
/// grammar violation, reaching this exact literal. (#159 cut 4: the first
|
||||
/// alternative's literal is now the blueprint-mode grammar, not a bare
|
||||
/// `[--name` — `mc` is blueprint-first.)
|
||||
@@ -1143,7 +1141,7 @@ fn mc_builtin_usage_names_the_program_in_both_alternatives() {
|
||||
"first alternative must name the program: {stderr:?}"
|
||||
);
|
||||
assert!(
|
||||
stderr.contains("| aura mc --strategy r-sma"),
|
||||
stderr.contains("| aura mc <blueprint.json> --real"),
|
||||
"second alternative must ALSO name the program: {stderr:?}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
@@ -1220,12 +1218,19 @@ fn sweep_and_walkforward_reject_retired_r_breakout_token_as_unrecognized() {
|
||||
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
|
||||
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(
|
||||
stderr.starts_with(&want),
|
||||
"`aura {args:?}` must fall into the generic usage error, not a special-cased \
|
||||
message: {stderr:?}"
|
||||
);
|
||||
if verb == "sweep" {
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(
|
||||
stderr.starts_with(&want),
|
||||
"`aura {args:?}` must fall into the generic usage error, not a special-cased \
|
||||
message: {stderr:?}"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
stderr.contains("unexpected argument '--strategy'"),
|
||||
"`aura {args:?}` must be a clap unknown-argument rejection: {stderr:?}"
|
||||
);
|
||||
}
|
||||
assert!(out.stdout.is_empty(), "no family summary on the rejected path: {:?}", out.stdout);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
@@ -1244,8 +1249,15 @@ fn sweep_and_walkforward_reject_retired_r_meanrev_token_as_unrecognized() {
|
||||
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
|
||||
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(stderr.starts_with(&want), "generic usage, not special-cased: {stderr:?}");
|
||||
if verb == "sweep" {
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(stderr.starts_with(&want), "generic usage, not special-cased: {stderr:?}");
|
||||
} else {
|
||||
assert!(
|
||||
stderr.contains("unexpected argument '--strategy'"),
|
||||
"`aura {args:?}` must be a clap unknown-argument rejection: {stderr:?}"
|
||||
);
|
||||
}
|
||||
assert!(out.stdout.is_empty(), "no summary on the rejected path: {:?}", out.stdout);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
@@ -1272,12 +1284,19 @@ fn sweep_and_walkforward_reject_retired_pip_tokens_as_unrecognized() {
|
||||
out.status
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(
|
||||
stderr.starts_with(&want),
|
||||
"`aura {args:?}` must fall into the generic usage error, not a \
|
||||
special-cased message: {stderr:?}"
|
||||
);
|
||||
if verb == "sweep" {
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(
|
||||
stderr.starts_with(&want),
|
||||
"`aura {args:?}` must fall into the generic usage error, not a \
|
||||
special-cased message: {stderr:?}"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
stderr.contains("unexpected argument '--strategy'"),
|
||||
"`aura {args:?}` must be a clap unknown-argument rejection: {stderr:?}"
|
||||
);
|
||||
}
|
||||
assert!(out.stdout.is_empty(), "no summary on the rejected path: {:?}", out.stdout);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
@@ -1495,10 +1514,12 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("walkforward-exact-grade");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -1535,15 +1556,72 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() {
|
||||
assert_eq!(ps[1]["mean"].as_f64(), Some(17.333333333333332), "slow-MA refit mean: {grade_line}");
|
||||
}
|
||||
|
||||
/// Property (#220 walkforward vertical): `aura walkforward --real` IS-refits ANY
|
||||
/// sweepable blueprint through the campaign path, not just the historical r-sma
|
||||
/// candidate — the whole point of dropping `--strategy r-sma`/`--fast`/`--slow`
|
||||
/// for a positional blueprint + generic `--axis`. Every other walkforward E2E
|
||||
/// test here still exercises `r_sma_open.json`; a regression that silently
|
||||
/// special-cased the refit's axis lookup back to a hardcoded `sma_signal.*`
|
||||
/// namespace (e.g. in `walkforward_summary_json_from_reports`'s per-axis
|
||||
/// `raw_matches_wrapped` search) would ship green there while breaking every
|
||||
/// other blueprint. This runs the unrelated r-breakout candidate
|
||||
/// (`channel_hi`/`channel_lo` axes, no `sma_signal` node in sight) end-to-end
|
||||
/// and asserts a well-formed multi-window summary whose `param_stability` has
|
||||
/// exactly one entry per bound axis (2 blueprint axes + the 2 stop columns).
|
||||
/// The window count (9) is roller/data-derived, not strategy-derived, so it is
|
||||
/// pinned exactly against the same GER40 2025 span the r-sma anchor uses.
|
||||
/// Gated on the shared GER40 archive; skips cleanly on a data refusal.
|
||||
#[test]
|
||||
fn walkforward_dissolves_a_non_r_sma_blueprint() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("walkforward-non-r-sma");
|
||||
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "r_breakout_signal.channel_hi.length=10,20",
|
||||
"--axis", "r_breakout_signal.channel_lo.length=10,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
if out.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40 data");
|
||||
return;
|
||||
}
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
let grade_line = stdout
|
||||
.lines()
|
||||
.find(|l| l.starts_with("{\"walkforward\":"))
|
||||
.unwrap_or_else(|| panic!("the walkforward summary line: {stdout}"));
|
||||
let v: serde_json::Value = serde_json::from_str(grade_line).expect("summary line parses as JSON");
|
||||
let wf = &v["walkforward"];
|
||||
assert_eq!(wf["windows"].as_u64(), Some(9), "window count (data/roller-derived): {grade_line}");
|
||||
assert!(wf["oos_r"]["n_trades"].as_u64().is_some(), "pooled OOS trade count present: {grade_line}");
|
||||
let ps = wf["param_stability"].as_array().expect("param_stability is an array");
|
||||
assert_eq!(ps.len(), 4, "one entry per bound axis (channel_hi, channel_lo, stop_length, stop_k): {grade_line}");
|
||||
}
|
||||
|
||||
/// Fork A: the dissolved walkforward binds the stop as a single risk regime, so a
|
||||
/// multi-value --stop-length/--stop-k is refused (exit 2) before any run.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_a_multi_value_stop() {
|
||||
let cwd = temp_cwd("walkforward-multi-stop");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14,20", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
@@ -1558,10 +1636,12 @@ fn walkforward_dissolved_refuses_a_multi_value_stop() {
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_select_plateau() {
|
||||
let cwd = temp_cwd("walkforward-plateau");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--select", "plateau:worst",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -1577,14 +1657,18 @@ fn walkforward_dissolved_refuses_select_plateau() {
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_missing_knobs() {
|
||||
let cwd = temp_cwd("walkforward-missing-knobs");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["walkforward", "--strategy", "r-sma", "--real", "GER40", "--fast", "3", "--slow", "12"])
|
||||
.args([
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "missing --stop-length/--stop-k is a usage refusal");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("requires --fast --slow --stop-length --stop-k"), "missing-knob refusal: {stderr}");
|
||||
assert!(stderr.contains("requires --stop-length --stop-k"), "missing-knob refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// An empty `--real ""` still satisfies clap's `Option::is_some()` dispatch guard, so
|
||||
@@ -1593,10 +1677,12 @@ fn walkforward_dissolved_refuses_missing_knobs() {
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_an_empty_real_symbol() {
|
||||
let cwd = temp_cwd("walkforward-empty-real");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
@@ -1606,16 +1692,51 @@ fn walkforward_dissolved_refuses_an_empty_real_symbol() {
|
||||
assert!(stderr.contains("dissolves only over --real"), "empty-real refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// Property: an `--axis` name that is not among the loaded blueprint's sweepable
|
||||
/// axes is refused before the raw-namespace strip or any archive access (exit 2),
|
||||
/// echoing exactly the name the user typed — the same WRAPPED-probe preflight the
|
||||
/// sibling verbs enforce
|
||||
/// (`aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access`,
|
||||
/// `generalize_refuses_an_unknown_axis_name`). Data-free: no archive/store access.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_an_unknown_axis_name() {
|
||||
let cwd = temp_cwd("walkforward-unknown-axis");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.nope=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "an unknown axis name must exit 2");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("axis \"sma_signal.nope\"") && stderr.contains("sweepable axes"),
|
||||
"must name the unresolved axis exactly as typed: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
out.stdout.is_empty(),
|
||||
"the refusal must not leak an aggregate to stdout, got: {:?}",
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
);
|
||||
assert!(!cwd.join("runs").exists(), "a refused axis name must not start a real run");
|
||||
}
|
||||
|
||||
/// Front-end parity: an unknown `--select` token is refused (exit 2) on the
|
||||
/// dissolved path exactly as it is on the inline path — it must not be silently
|
||||
/// swallowed by `select_is_plateau`'s plateau-only check and fall through to argmax.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_an_unknown_select_token() {
|
||||
let cwd = temp_cwd("walkforward-bad-select");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--select", "bogus",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -1633,10 +1754,12 @@ fn walkforward_dissolved_refuses_an_unknown_select_token() {
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_name_and_trace_together() {
|
||||
let cwd = temp_cwd("walkforward-name-and-trace");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--name", "a", "--trace", "b",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -1662,10 +1785,12 @@ fn walkforward_dissolves_through_the_campaign_path() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("walkforward-dissolves");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -3035,7 +3160,7 @@ fn dual_grammar_stray_positional_is_a_usage_error_not_swallowed() {
|
||||
for argv in [
|
||||
&["run", "bogus"][..],
|
||||
&["sweep", "bogus", "--name", "x"][..],
|
||||
&["walkforward", "bogus", "--strategy", "sma"][..],
|
||||
&["walkforward", "bogus"][..],
|
||||
&["mc", "bogus"][..],
|
||||
] {
|
||||
let out = Command::new(BIN).args(argv).output().expect("spawn aura <sub> <stray-positional>");
|
||||
@@ -3128,10 +3253,12 @@ fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("mc-r-bootstrap-exact-grade");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--block-len", "5", "--resamples", "1000", "--seed", "42",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
@@ -3164,6 +3291,65 @@ fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
|
||||
assert_eq!(mc["prob_le_zero"].as_f64(), Some(0.558), "P(E[R] <= 0): {grade_line}");
|
||||
}
|
||||
|
||||
/// Property (#220 mc vertical): `aura mc --real` R-bootstraps ANY sweepable
|
||||
/// blueprint through the campaign path, not just the historical r-sma candidate
|
||||
/// — the whole point of dropping `--strategy r-sma`/`--fast`/`--slow` for a
|
||||
/// positional blueprint + generic `--axis`. Every other mc E2E test here still
|
||||
/// exercises `r_sma_open.json`; a regression that silently special-cased the
|
||||
/// pipeline's axis lookup back to a hardcoded `sma_signal.*` namespace would
|
||||
/// ship green there while breaking every other blueprint. This runs the
|
||||
/// unrelated r-breakout candidate (`channel_hi`/`channel_lo` axes, no
|
||||
/// `sma_signal` node in sight) end-to-end and asserts a well-formed
|
||||
/// `mc_r_bootstrap` grade: the argv-echoed knobs (`block_len`/`n_resamples`)
|
||||
/// pin exactly, `n_trades`/`e_r.mean`/`prob_le_zero` are asserted present and
|
||||
/// well-typed (their exact values are strategy-specific, already the r-sma
|
||||
/// pin's job above). Gated on the shared GER40 archive; skips cleanly on a
|
||||
/// data refusal.
|
||||
#[test]
|
||||
fn mc_dissolves_a_non_r_sma_blueprint() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("mc-non-r-sma");
|
||||
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "r_breakout_signal.channel_hi.length=10,20",
|
||||
"--axis", "r_breakout_signal.channel_lo.length=10,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--block-len", "5", "--resamples", "1000", "--seed", "42",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
if out.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40 data");
|
||||
return;
|
||||
}
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
let grade_line = stdout
|
||||
.lines()
|
||||
.find(|l| l.starts_with("{\"mc_r_bootstrap\":"))
|
||||
.unwrap_or_else(|| panic!("the mc_r_bootstrap grade line: {stdout}"));
|
||||
let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON");
|
||||
let mc = &v["mc_r_bootstrap"];
|
||||
assert_eq!(mc["block_len"].as_u64(), Some(5), "block_len (argv echo): {grade_line}");
|
||||
assert_eq!(mc["n_resamples"].as_u64(), Some(1000), "n_resamples (argv echo): {grade_line}");
|
||||
assert!(mc["n_trades"].as_u64().is_some_and(|n| n > 0), "pooled OOS trade count present: {grade_line}");
|
||||
assert!(mc["e_r"]["mean"].as_f64().is_some_and(|m| m.is_finite()), "E[R] mean present + finite: {grade_line}");
|
||||
assert!(
|
||||
mc["prob_le_zero"].as_f64().is_some_and(|p| (0.0..=1.0).contains(&p)),
|
||||
"P(E[R] <= 0) present + in [0,1]: {grade_line}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property: `aura mc --strategy r-sma --real` is now thin sugar over the one campaign
|
||||
/// path — a successful run durably auto-registers exactly one generated process
|
||||
/// document, one generated campaign document (carrying the constant "mc" name and the
|
||||
@@ -3178,10 +3364,12 @@ fn mc_dissolves_through_the_campaign_path() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("mc-dissolves");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--block-len", "5", "--resamples", "1000", "--seed", "42",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
@@ -3267,10 +3455,12 @@ fn mc_dissolves_through_the_campaign_path() {
|
||||
#[test]
|
||||
fn mc_dissolved_refuses_a_multi_value_stop() {
|
||||
let cwd = temp_cwd("mc-multi-stop");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0",
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14,20", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
@@ -3280,6 +3470,40 @@ fn mc_dissolved_refuses_a_multi_value_stop() {
|
||||
assert!(stderr.contains("single risk regime"), "stop-regime refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// Property: an `--axis` name that is not among the loaded blueprint's sweepable
|
||||
/// axes is refused before the raw-namespace strip or any archive access (exit 2),
|
||||
/// echoing exactly the name the user typed — mc shares the identical WRAPPED-probe
|
||||
/// preflight `walkforward_dissolved_refuses_an_unknown_axis_name` pins for
|
||||
/// walkforward (both dispatchers now call the same `validate_and_register_axes`
|
||||
/// helper); this test pins mc's own copy so a future edit cannot silently break it
|
||||
/// while walkforward's stays green. Data-free: no archive/store access.
|
||||
#[test]
|
||||
fn mc_dissolved_refuses_an_unknown_axis_name() {
|
||||
let cwd = temp_cwd("mc-unknown-axis");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.nope=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "an unknown axis name must exit 2");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("axis \"sma_signal.nope\"") && stderr.contains("sweepable axes"),
|
||||
"must name the unresolved axis exactly as typed: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
out.stdout.is_empty(),
|
||||
"the refusal must not leak an aggregate to stdout, got: {:?}",
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
);
|
||||
assert!(!cwd.join("runs").exists(), "a refused axis name must not start a real run");
|
||||
}
|
||||
|
||||
/// Property: the one load-bearing decision new to the mc dissolution — `translate_mc`
|
||||
/// maps `campaign.seed` to the mc `--seed` (unlike `translate_walkforward`, which
|
||||
/// hardcodes `seed: 0`) — actually flows through the executor to the emitted grade,
|
||||
@@ -3297,12 +3521,14 @@ fn mc_dissolved_refuses_a_multi_value_stop() {
|
||||
fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let run = |seed: &str| {
|
||||
let cwd = temp_cwd(&format!("mc-seed-{seed}"));
|
||||
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--block-len", "5", "--resamples", "1000", "--seed", seed,
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user