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:
2026-07-09 14:48:59 +02:00
parent 4f629ee2ab
commit e7efe1f765
3 changed files with 660 additions and 533 deletions
+260 -267
View File
@@ -786,31 +786,29 @@ fn walkforward_summary_json(result: &WalkForwardResult) -> String {
/// The walk-forward summary line reconstructed from the recorded per-window OOS /// The walk-forward summary line reconstructed from the recorded per-window OOS
/// reports (the campaign path's `WalkForward` `StageFamily.reports`) rather than a /// reports (the campaign path's `WalkForward` `StageFamily.reports`) rather than a
/// live `WalkForwardResult`. Byte-identical to `walkforward_summary_json`: /// live `WalkForwardResult`. `stitched_total_pips` = the per-window `total_pips`
/// `stitched_total_pips` = the per-window `total_pips` summed left-to-right in roll /// summed left-to-right in roll order (the engine `stitch` folds each segment's
/// order (the engine `stitch` folds each segment's final cumulative value, and a /// final cumulative value, and a window's OOS segment ends at its `total_pips`);
/// window's OOS segment ends at its `total_pips`); `param_stability` reduces each /// `param_stability` reduces each IS-refit axis in `axes` over the per-window
/// r-sma IS-refit axis over the per-window chosen params (read from each report's /// chosen params (read from each report's `manifest.params` via
/// `manifest.params` via [`campaign_run::raw_matches_wrapped`] — the blueprint axes /// [`campaign_run::raw_matches_wrapped`] — blueprint axes are recorded wrapped
/// `fast.length`/`slow.length` are recorded wrapped as `sma_signal.fast.length` etc. /// (e.g. `sma_signal.fast.length`) by the strategy's own param space, while
/// by the strategy's own param space, while `stop_length`/`stop_k` ride the risk /// `stop_length`/`stop_k` ride the risk regime unwrapped, so an exact-name match
/// regime unwrapped, so an exact-name match would miss the wrapped pair) through the /// would miss the wrapped ones) through the same `MetricStats::from_values`; the
/// same `MetricStats::from_values`; the `oos_r` block pools the per-window `trade_rs` /// `oos_r` block pools the per-window `trade_rs` through `r_metrics_from_rs`.
/// through `r_metrics_from_rs`. Canonical JSON (C14). /// Canonical JSON (C14).
/// ///
/// The axis list + order mirror what `r_sma_space` yields (fast, slow, stop_length, /// `axes` carries the invocation's raw axis names in argv order, followed by the
/// stop_k) — the same order + values the inline `param_stability` reduces. A wrong /// stop columns when a regime is bound (#220 — no axis name is hardcoded here).
/// order or axis would fail the committed exact-grade anchor loudly (it pins /// Order matters: the committed exact-grade anchor pins
/// `param_stability[0].mean` = the fast-MA refit mean), never silently. /// `param_stability[0].mean` = the first axis's refit mean.
fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String { fn walkforward_summary_json_from_reports(reports: &[RunReport], axes: &[String]) -> String {
let total: f64 = reports.iter().map(|r| r.metrics.total_pips).sum(); let total: f64 = reports.iter().map(|r| r.metrics.total_pips).sum();
// The four r-sma axis names, in `r_sma_space` order. Coercion to `f64` is // Coercion to `f64` is decided per-value at runtime by the `Scalar` variant
// decided per-value at runtime by the `Scalar` variant match below (i64 axes // match below (i64 axes cast value-as-f64, the f64 axis passes through),
// cast value-as-f64, the f64 axis passes through), exactly as // exactly as `param_stability`'s per-`ScalarKind` coerce does — the axis
// `param_stability`'s per-`ScalarKind` coerce does — this list carries no // list carries no per-axis type flag.
// per-axis type flag. let param_stability: Vec<aura_engine::MetricStats> = axes
const AXES: [&str; 4] = ["fast.length", "slow.length", "stop_length", "stop_k"];
let param_stability: Vec<aura_engine::MetricStats> = AXES
.iter() .iter()
.map(|axis| { .map(|axis| {
let vals: Vec<f64> = reports let vals: Vec<f64> = reports
@@ -821,7 +819,7 @@ fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String {
.params .params
.iter() .iter()
.find(|(name, _)| campaign_run::raw_matches_wrapped(axis, name)) .find(|(name, _)| campaign_run::raw_matches_wrapped(axis, name))
.expect("each r-sma walk-forward window records its chosen axis"); .expect("each walk-forward window records its chosen axis");
match v { match v {
Scalar::I64(i) => *i as f64, Scalar::I64(i) => *i as f64,
Scalar::F64(f) => *f, Scalar::F64(f) => *f,
@@ -2187,11 +2185,8 @@ struct SweepCmd {
#[derive(Args)] #[derive(Args)]
struct WalkforwardCmd { struct WalkforwardCmd {
/// A loaded blueprint (.json); omit for the built-in --strategy grammar. /// A loaded blueprint (.json, required — both grammars are blueprint-first).
blueprint: Option<String>, blueprint: Option<String>,
/// Built-in strategy: r-sma (the R-scored SMA-cross sugar over the shipped example).
#[arg(long)]
strategy: Option<String>,
/// Real instrument symbol to validate over (recorded data); omit for the synthetic stream. /// Real instrument symbol to validate over (recorded data); omit for the synthetic stream.
#[arg(long)] #[arg(long)]
real: Option<String>, real: Option<String>,
@@ -2207,16 +2202,10 @@ struct WalkforwardCmd {
/// Family name that also persists each OOS window's taps (mutually exclusive with --name). /// Family name that also persists each OOS window's taps (mutually exclusive with --name).
#[arg(long)] #[arg(long)]
trace: Option<String>, trace: Option<String>,
/// Fast-MA IS-refit grid axis (comma-separated values). /// Campaign-path stop length (single value; --real mode).
#[arg(long)]
fast: Option<String>,
/// Slow-MA IS-refit grid axis (comma-separated values).
#[arg(long)]
slow: Option<String>,
/// Stop-length IS-refit grid axis (comma-separated values).
#[arg(long)] #[arg(long)]
stop_length: Option<String>, stop_length: Option<String>,
/// Stop-k IS-refit grid axis (comma-separated values). /// Campaign-path stop-k multiple (single value; --real mode).
#[arg(long)] #[arg(long)]
stop_k: Option<String>, stop_k: Option<String>,
/// In-sample winner selection: argmax | plateau:mean | plateau:worst (default argmax). /// In-sample winner selection: argmax | plateau:mean | plateau:worst (default argmax).
@@ -2231,10 +2220,8 @@ struct WalkforwardCmd {
struct McCmd { struct McCmd {
/// A loaded blueprint (.json); omit for the built-in grammar. /// A loaded blueprint (.json); omit for the built-in grammar.
blueprint: Option<String>, blueprint: Option<String>,
/// Built-in strategy: r-sma (the R-scored SMA-cross sugar over the shipped example). /// Real instrument symbol for the R-bootstrap campaign path (recorded data); omit
#[arg(long)] /// for the synthetic seed family.
strategy: Option<String>,
/// Real instrument symbol for the R-bootstrap (recorded data); requires --strategy r-sma.
#[arg(long)] #[arg(long)]
real: Option<String>, real: Option<String>,
/// Window start (Unix ms, inclusive); requires --real. /// Window start (Unix ms, inclusive); requires --real.
@@ -2249,16 +2236,13 @@ struct McCmd {
/// Family name that also persists each seed's taps (mutually exclusive with --name). /// Family name that also persists each seed's taps (mutually exclusive with --name).
#[arg(long)] #[arg(long)]
trace: Option<String>, trace: Option<String>,
/// Fast-MA grid axis for the R-bootstrap (comma-separated values). /// Blueprint IS-refit axis `<name>=<csv>` (repeatable, >=1 required; --real mode).
#[arg(long)] #[arg(long)]
fast: Option<String>, axis: Vec<String>,
/// Slow-MA grid axis for the R-bootstrap (comma-separated values). /// Campaign-path stop length (single value; --real mode).
#[arg(long)]
slow: Option<String>,
/// Stop-length grid axis for the R-bootstrap (comma-separated values).
#[arg(long)] #[arg(long)]
stop_length: Option<String>, stop_length: Option<String>,
/// Stop-k grid axis for the R-bootstrap (comma-separated values). /// Campaign-path stop-k multiple (single value; --real mode).
#[arg(long)] #[arg(long)]
stop_k: Option<String>, stop_k: Option<String>,
/// Moving-block bootstrap block length (R-bootstrap; default 1). /// Moving-block bootstrap block length (R-bootstrap; default 1).
@@ -2322,25 +2306,24 @@ fn select_is_plateau(select: Option<&str>) -> bool {
} }
/// Convert `WalkforwardCmd` into the resolved argument shape the walkforward sugar /// Convert `WalkforwardCmd` into the resolved argument shape the walkforward sugar
/// consumes (the dissolved `--strategy r-sma --real` branch). Single instrument, /// consumes (the dissolved `.json --real` branch). Single instrument, single-value
/// multi-value fast/slow (the IS-refit grid), single-value stop (Fork A: the stop is /// stop (Fork A: the stop is a risk regime, not a swept axis); `parse_csv_list` is
/// a risk regime, not a swept axis); all four knobs required. `parse_csv_list` /// generic over `T: FromStr`, so the same helper parses the i64 length and the f64
/// (`:1586`) is generic over `T: FromStr`, so the same helper parses the i64 grids /// k; the local `knobs` closure (captureless → `Copy`, mirroring `mc_args_from`'s
/// and the f64 stop; the local `knobs` closure (captureless → `Copy`, mirroring /// idiom) is reused across both `ok_or_else` calls. `--name`/`--trace` are
/// `generalize_args_from`'s idiom) is reused across the four `ok_or_else` calls. /// mutually exclusive, matching the flag's own documented contract and the inline
/// `--name`/`--trace` are mutually exclusive here too, matching the flag's own /// path's `name_persist` refusal; an omitted flag defaults the family name to
/// documented contract and the inline path's `name_persist` refusal. /// "walkforward". The IS-refit `--axis` grid is parsed separately at the dispatch
/// site, not here.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn walkforward_args_from( fn walkforward_args_from(
a: &WalkforwardCmd, a: &WalkforwardCmd,
) -> Result<(String, String, Vec<i64>, Vec<i64>, i64, f64, Option<i64>, Option<i64>), String> { ) -> Result<(String, String, i64, f64, Option<i64>, Option<i64>), String> {
let symbol = match a.real.as_deref() { let symbol = match a.real.as_deref() {
None | Some("") => return Err("walkforward --strategy r-sma dissolves only over --real <SYMBOL>".to_string()), None | Some("") => return Err("walkforward dissolves only over --real <SYMBOL>".to_string()),
Some(s) => s.to_string(), Some(s) => s.to_string(),
}; };
let knobs = || "walkforward requires --fast --slow --stop-length --stop-k (fast/slow may be CSV grids; the stop is single-value)".to_string(); let knobs = || "walkforward requires --stop-length --stop-k (single-value; the stop is a risk regime)".to_string();
let fast: Vec<i64> = parse_csv_list(a.fast.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
let slow: Vec<i64> = parse_csv_list(a.slow.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
let stop_length: Vec<i64> = parse_csv_list(a.stop_length.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?; let stop_length: Vec<i64> = parse_csv_list(a.stop_length.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
let stop_k: Vec<f64> = parse_csv_list(a.stop_k.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?; let stop_k: Vec<f64> = parse_csv_list(a.stop_k.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
if stop_length.len() != 1 || stop_k.len() != 1 { if stop_length.len() != 1 || stop_k.len() != 1 {
@@ -2354,35 +2337,30 @@ fn walkforward_args_from(
(None, Some(t)) => t.to_string(), (None, Some(t)) => t.to_string(),
(None, None) => "walkforward".to_string(), (None, None) => "walkforward".to_string(),
}; };
Ok((name, symbol, fast, slow, stop_length[0], stop_k[0], a.from, a.to)) Ok((name, symbol, stop_length[0], stop_k[0], a.from, a.to))
} }
/// Convert `McCmd` into the resolved argument shape the mc sugar consumes (the /// Convert `McCmd` into the resolved argument shape the mc sugar consumes (the
/// dissolved `--strategy r-sma --real` branch). Single instrument, multi-value /// `--real` campaign branch). Single instrument, single-value stop (Fork A: the
/// fast/slow (the IS-refit grid), single-value stop (Fork A: the stop is a risk /// stop is a risk regime, not a swept axis). `--block-len`/`--resamples`/`--seed`
/// regime, not a swept axis); all four grid knobs required. `--block-len`/ /// default to `1`/`1000`/`1` — the same defaults the retired real-R dispatch
/// `--resamples`/`--seed` default to `1`/`1000`/`1` — the same defaults the retired /// used, so an omitted flag produces the same document either way. The
/// real-R dispatch used, so an omitted flag produces the same document either way. The usize->u32 conversion for `block_len`/`resamples` /// usize->u32 conversion lands HERE, at the argv boundary where the CLI's
/// lands HERE, at the argv boundary where the CLI's `usize` meets the document's /// `usize` meets the document's `u32` vocabulary (`StageBlock::MonteCarlo`).
/// `u32` vocabulary (`StageBlock::MonteCarlo`), keeping every downstream signature /// `--name`/`--trace` are rejected: the R-bootstrap records without a family
/// in the document's native type. `--name`/`--trace` are rejected: the inline mc /// name (the campaign name is a constant "mc").
/// R-path rejects them too (`main.rs:5047`), so the sugar preserves that (the
/// R-bootstrap records without a family name — the campaign name is a constant
/// "mc").
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn mc_args_from( fn mc_args_from(
a: &McCmd, a: &McCmd,
) -> Result<(String, String, Vec<i64>, Vec<i64>, i64, f64, u32, u32, u64, Option<i64>, Option<i64>), String> { ) -> Result<(String, String, i64, f64, u32, u32, u64, Option<i64>, Option<i64>), String> {
let symbol = match a.real.as_deref() { let symbol = match a.real.as_deref() {
None | Some("") => return Err("mc --strategy r-sma dissolves only over --real <SYMBOL>".to_string()), None | Some("") => return Err("mc dissolves only over --real <SYMBOL>".to_string()),
Some(s) => s.to_string(), Some(s) => s.to_string(),
}; };
if a.name.is_some() || a.trace.is_some() { if a.name.is_some() || a.trace.is_some() {
return Err("mc --strategy r-sma: --name/--trace are not accepted (the R-bootstrap records without a family name)".to_string()); return Err("mc --real: --name/--trace are not accepted (the R-bootstrap records without a family name)".to_string());
} }
let knobs = || "mc requires --fast --slow --stop-length --stop-k (fast/slow may be CSV grids; the stop is single-value)".to_string(); let knobs = || "mc requires --stop-length --stop-k (single-value; the stop is a risk regime)".to_string();
let fast: Vec<i64> = parse_csv_list(a.fast.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
let slow: Vec<i64> = parse_csv_list(a.slow.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
let stop_length: Vec<i64> = parse_csv_list(a.stop_length.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?; let stop_length: Vec<i64> = parse_csv_list(a.stop_length.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
let stop_k: Vec<f64> = parse_csv_list(a.stop_k.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?; let stop_k: Vec<f64> = parse_csv_list(a.stop_k.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
if stop_length.len() != 1 || stop_k.len() != 1 { if stop_length.len() != 1 || stop_k.len() != 1 {
@@ -2391,7 +2369,7 @@ fn mc_args_from(
let block_len = a.block_len.map(|v| v as u32).unwrap_or(1); let block_len = a.block_len.map(|v| v as u32).unwrap_or(1);
let resamples = a.resamples.map(|v| v as u32).unwrap_or(1000); let resamples = a.resamples.map(|v| v as u32).unwrap_or(1000);
let seed = a.seed.unwrap_or(1); let seed = a.seed.unwrap_or(1);
Ok(("mc".to_string(), symbol, fast, slow, stop_length[0], stop_k[0], block_len, resamples, seed, a.from, a.to)) Ok(("mc".to_string(), symbol, stop_length[0], stop_k[0], block_len, resamples, seed, a.from, a.to))
} }
/// Convert `GeneralizeCmd` into the resolved argument shape the generalize sugar /// Convert `GeneralizeCmd` into the resolved argument shape the generalize sugar
@@ -2483,6 +2461,54 @@ fn parse_axes(
Ok(axes) Ok(axes)
} }
/// The two distinguishable ways `validate_and_register_axes` can fail, carrying
/// the two distinct exit codes its four call sites (sweep/generalize/walkforward/mc)
/// preserved before the dedup: an unknown axis name is a usage error (exit 2, echoed
/// before the archive is touched); a registry write failure is a runtime error (exit 1).
enum AxisRegisterError {
UnknownAxis(String),
Registry(String),
}
/// Validate every `--axis` name against the blueprint's WRAPPED probe namespace
/// (`blueprint_axis_probe`/`--list-axes`), then canonicalize + register the
/// blueprint by topology hash and strip every axis name to the RAW campaign
/// namespace (the sweep sequence, #210 c0110). A raw-form or fat-fingered axis
/// name is refused here, echoing exactly what the user typed, before the
/// archive is touched or the blueprint is registered — shared by every
/// campaign-path dispatcher (sweep/generalize/walkforward/mc all landed the
/// identical block; #220 slice-1 deferred this dedup to "once wf/mc land the
/// same block", rule-of-three now exceeded 4x).
#[allow(clippy::type_complexity)]
fn validate_and_register_axes(
doc: &str,
axes: &[(String, Vec<Scalar>)],
env: &project::Env,
) -> Result<(String, Vec<(String, Vec<Scalar>)>), AxisRegisterError> {
let space = blueprint_axis_probe(doc, env).param_space();
for (n, _) in axes {
if !space.iter().any(|p| &p.name == n) {
return Err(AxisRegisterError::UnknownAxis(format!(
"axis \"{n}\" is not one of this blueprint's \
sweepable axes — run 'aura sweep <bp> --list-axes' \
to see them"
)));
}
}
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary");
let canonical = blueprint_to_json(&blueprint).expect("a loaded blueprint re-serializes");
let reg = env.registry();
let topo = topology_hash(&blueprint);
reg.put_blueprint(&topo, &canonical)
.map_err(|e| AxisRegisterError::Registry(e.to_string()))?;
let raw_axes: Vec<(String, Vec<Scalar>)> = axes
.iter()
.map(|(n, v)| (campaign_run::wrapped_to_raw_axis(n).to_string(), v.clone()))
.collect();
Ok((canonical, raw_axes))
}
/// `aura run`: the loaded-blueprint branch (an existing `.json` first-positional) or /// `aura run`: the loaded-blueprint branch (an existing `.json` first-positional) or
/// the built-in harness-kind dispatch. /// the built-in harness-kind dispatch.
fn dispatch_run(a: RunCmd, env: &project::Env) { fn dispatch_run(a: RunCmd, env: &project::Env) {
@@ -2612,28 +2638,17 @@ fn dispatch_generalize(a: GeneralizeCmd, env: &project::Env) {
std::process::exit(2); std::process::exit(2);
} }
} }
// Validate every --axis name against the WRAPPED probe namespace before let (canonical, raw_axes) =
// stripping to the raw campaign namespace (the sweep sequence, #210 c0110). validate_and_register_axes(&doc, &axes, env).unwrap_or_else(|e| match e {
let space = blueprint_axis_probe(&doc, env).param_space(); AxisRegisterError::UnknownAxis(m) => {
for (n, _) in &axes { eprintln!("aura: {m}");
if !space.iter().any(|p| &p.name == n) { std::process::exit(2);
eprintln!( }
"aura: axis \"{n}\" is not one of this blueprint's \ AxisRegisterError::Registry(m) => {
sweepable axes — run 'aura sweep <bp> --list-axes' \ eprintln!("aura: {m}");
to see them" std::process::exit(1);
); }
std::process::exit(2); });
}
}
let blueprint = blueprint_from_json(&doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary");
let canonical = blueprint_to_json(&blueprint).expect("a loaded blueprint re-serializes");
let reg = env.registry();
let topo = topology_hash(&blueprint);
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(1);
});
// Window: the explicit --from/--to when both present (byte-identical to the // Window: the explicit --from/--to when both present (byte-identical to the
// retired welded path, verified by the exact-grade anchor); otherwise the // retired welded path, verified by the exact-grade anchor); otherwise the
// first symbol's full archive window as the single shared campaign window. // first symbol's full archive window as the single shared campaign window.
@@ -2651,12 +2666,6 @@ fn dispatch_generalize(a: GeneralizeCmd, env: &project::Env) {
) )
} }
}; };
// Campaign documents speak the RAW campaign-axis namespace; `--axis` is
// typed against the WRAPPED probe. Strip one segment (the sweep strip).
let raw_axes: Vec<(String, Vec<Scalar>)> = axes
.iter()
.map(|(n, v)| (campaign_run::wrapped_to_raw_axis(n).to_string(), v.clone()))
.collect();
let inv = verb_sugar::SugarInvocation { let inv = verb_sugar::SugarInvocation {
axes: &raw_axes, axes: &raw_axes,
name, name,
@@ -2783,27 +2792,20 @@ fn dispatch_sweep(a: SweepCmd, env: &project::Env) {
// strategy ref resolves against this one write; a second // strategy ref resolves against this one write; a second
// put under `content_id_of(&canonical)` would target the // put under `content_id_of(&canonical)` would target the
// identical key and is not needed. // identical key and is not needed.
// FIX (#210 c0110 finding "wrapped-name refusal mangles the // Axis validation + canonicalize/register + the wrapped->raw
// axis"): validate every `--axis` name against the WRAPPED // strip are single-sourced in `validate_and_register_axes`
// probe namespace (`blueprint_axis_probe`/`--list-axes` — // (data-free, so this fires before the archive is touched).
// the same names #203's strip later consumes) BEFORE let (canonical, raw_axes) = validate_and_register_axes(&doc, &axes, env)
// stripping/translating to the raw campaign namespace. A .unwrap_or_else(|e| match e {
// raw-form or fat-fingered name is refused here, echoing AxisRegisterError::UnknownAxis(m) => {
// exactly what the user typed, instead of surfacing a eprintln!("aura: {m}");
// stripped-then-mangled fragment deep inside a member run. std::process::exit(2);
// Data-free (the probe only reloads the blueprint), so }
// this fires before the archive is touched. AxisRegisterError::Registry(m) => {
let space = blueprint_axis_probe(&doc, env).param_space(); eprintln!("aura: {m}");
for (n, _) in &axes { std::process::exit(1);
if !space.iter().any(|p| &p.name == n) { }
eprintln!( });
"aura: axis \"{n}\" is not one of this blueprint's \
sweepable axes — run 'aura sweep <bp> --list-axes' \
to see them"
);
std::process::exit(2);
}
}
let symbol = symbol.clone(); let symbol = symbol.clone();
let source = DataSource::from_choice(data, env); let source = DataSource::from_choice(data, env);
let (from_ts, to_ts) = source.full_window(env); let (from_ts, to_ts) = source.full_window(env);
@@ -2822,29 +2824,6 @@ fn dispatch_sweep(a: SweepCmd, env: &project::Env) {
aura_ingest::epoch_ns_to_unix_ms(from_ts), aura_ingest::epoch_ns_to_unix_ms(from_ts),
aura_ingest::epoch_ns_to_unix_ms(to_ts), aura_ingest::epoch_ns_to_unix_ms(to_ts),
); );
let blueprint = blueprint_from_json(&doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary");
let canonical =
blueprint_to_json(&blueprint).expect("a loaded blueprint re-serializes");
let reg = env.registry();
let topo = topology_hash(&blueprint);
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(1);
});
// Campaign documents speak the RAW
// campaign-axis namespace (`bind_axes`'s convention, #203) —
// everything after the wrapper's one node segment — while
// `--axis` on a blueprint file is typed against the WRAPPED
// probe (`blueprint_axis_probe`/`list_blueprint_axes`, e.g.
// `sma_signal.fast.length`). Strip that one segment before
// generating the campaign doc, or `validate_campaign_refs`
// (which checks axes against the UNWRAPPED blueprint's own
// `param_space`) refuses every axis as unknown.
let raw_axes: Vec<(String, Vec<Scalar>)> = axes
.iter()
.map(|(n, v)| (campaign_run::wrapped_to_raw_axis(n).to_string(), v.clone()))
.collect();
let inv = verb_sugar::SugarInvocation { let inv = verb_sugar::SugarInvocation {
axes: &raw_axes, axes: &raw_axes,
name: name.clone(), name: name.clone(),
@@ -2874,12 +2853,14 @@ fn dispatch_sweep(a: SweepCmd, env: &project::Env) {
} }
} }
/// `aura walkforward`: IS-refit walk-forward over a loaded blueprint (an existing /// `aura walkforward`: IS-refit walk-forward over a loaded blueprint — the
/// `.json` first-positional) or the built-in `--strategy` walk-forward. /// synthetic in-process family (no `--real`) or the campaign path (`--real`,
/// #220: arbitrary user blueprint + axes, formerly the welded r-sma branch).
fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) { fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
// Single-sourced: both arms below read this one closure (house style, #179).
let usage = || "Usage: aura walkforward <blueprint.json> --axis <name>=<csv> [--axis …] [--select <argmax|plateau:mean|plateau:worst>] [--name <n>] | aura walkforward <blueprint.json> --real <SYMBOL> --axis <name>=<csv> [--axis …] --stop-length <n> --stop-k <x> [--from <ms>] [--to <ms>] [--name <n> | --trace <n>]".to_string();
match is_blueprint_file(&a.blueprint) { match is_blueprint_file(&a.blueprint) {
Some(path) => { Some(path) => {
let usage = || "Usage: aura walkforward <blueprint.json> --axis <name>=<csv> [--axis …] [--select <argmax|plateau:mean|plateau:worst>] [--name <n>]".to_string();
let doc = std::fs::read_to_string(path).unwrap_or_else(|e| { let doc = std::fs::read_to_string(path).unwrap_or_else(|e| {
eprintln!("aura: {path}: {e}"); eprintln!("aura: {path}: {e}");
std::process::exit(2); std::process::exit(2);
@@ -2888,16 +2869,6 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
eprintln!("aura: {path}: {msg}"); eprintln!("aura: {path}: {msg}");
std::process::exit(2); std::process::exit(2);
} }
if a.strategy.is_some()
|| a.fast.is_some()
|| a.slow.is_some()
|| a.stop_length.is_some()
|| a.stop_k.is_some()
|| a.trace.is_some()
{
eprintln!("aura: {}", usage());
std::process::exit(2);
}
let axes = parse_axes(&a.axis, &usage).unwrap_or_else(|m| { let axes = parse_axes(&a.axis, &usage).unwrap_or_else(|m| {
eprintln!("aura: {m}"); eprintln!("aura: {m}");
std::process::exit(2); std::process::exit(2);
@@ -2906,27 +2877,10 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
eprintln!("aura: walkforward <blueprint.json> requires >= 1 --axis to re-fit per window"); eprintln!("aura: walkforward <blueprint.json> requires >= 1 --axis to re-fit per window");
std::process::exit(2); std::process::exit(2);
} }
let select = match a.select.as_deref() { if a.real.is_some() {
Some(s) => parse_select(s).unwrap_or_else(|()| { // The campaign path (#220): the real-archive execution routes
eprintln!("aura: {}", usage()); // through the one campaign executor, over the user's own
std::process::exit(2); // blueprint and axes (formerly the welded r-sma branch).
}),
None => Selection::Argmax,
};
let name = a.name.clone().unwrap_or_else(|| "walkforward".to_string());
run_blueprint_walkforward(&doc, &axes, &name, DataSource::Synthetic, select, env);
}
None => {
let usage = || "Usage: aura walkforward <blueprint.json> --axis <name>=<csv> [--axis …] [--select <argmax|plateau:mean|plateau:worst>] [--name <n>] | aura walkforward --strategy r-sma --real <SYMBOL> [--from <ms>] [--to <ms>] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>]".to_string();
if a.blueprint.is_some() || !a.axis.is_empty() {
eprintln!("aura: {}", usage());
std::process::exit(2);
}
// Dissolution split (#210, Reading A): only the real-archive r-sma
// execution routes through the campaign path. Everything else — synthetic
// (any strategy) and non-r-sma real — stays on the inline handler below,
// fenced until #159.
if a.strategy.as_deref() == Some("r-sma") && a.real.is_some() {
if let Some(s) = a.select.as_deref() if let Some(s) = a.select.as_deref()
&& parse_select(s).is_err() && parse_select(s).is_err()
{ {
@@ -2937,18 +2891,25 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
eprintln!("aura: --select plateau is not yet available on the campaign path; see #215"); eprintln!("aura: --select plateau is not yet available on the campaign path; see #215");
std::process::exit(2); std::process::exit(2);
} }
let (name, symbol, fast, slow, stop_length, stop_k, from, to) = let (name, symbol, stop_length, stop_k, from, to) =
walkforward_args_from(&a).unwrap_or_else(|m| { walkforward_args_from(&a).unwrap_or_else(|m| {
eprintln!("aura: {m}"); eprintln!("aura: {m}");
std::process::exit(2); std::process::exit(2);
}); });
let canonical = include_str!("../examples/r_sma_open.json"); // Axis validation + canonicalize/register + the wrapped->raw
let reg = env.registry(); // strip are single-sourced in `validate_and_register_axes`
let topo = content_id(canonical); // (data-free, so this fires before the archive is touched).
reg.put_blueprint(&topo, canonical).unwrap_or_else(|e| { let (canonical, raw_axes) = validate_and_register_axes(&doc, &axes, env)
eprintln!("aura: {e}"); .unwrap_or_else(|e| match e {
std::process::exit(1); AxisRegisterError::UnknownAxis(m) => {
}); eprintln!("aura: {m}");
std::process::exit(2);
}
AxisRegisterError::Registry(m) => {
eprintln!("aura: {m}");
std::process::exit(1);
}
});
// Unlike `dispatch_generalize` (a single run per instrument, insensitive // Unlike `dispatch_generalize` (a single run per instrument, insensitive
// to a day's edge shift), `blueprint_walkforward_family` sources its span // to a day's edge shift), `blueprint_walkforward_family` sources its span
// from `DataSource::wf_full_span`, which — for Real — ALWAYS clips // from `DataSource::wf_full_span`, which — for Real — ALWAYS clips
@@ -2967,9 +2928,24 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
aura_ingest::epoch_ns_to_unix_ms(to_ts), aura_ingest::epoch_ns_to_unix_ms(to_ts),
); );
let (is_ms, oos_ms, step_ms) = wf_ms_sizes(); let (is_ms, oos_ms, step_ms) = wf_ms_sizes();
let inv = verb_sugar::SugarInvocation {
axes: &raw_axes,
name,
symbols: vec![symbol],
from_ms,
to_ms,
blueprint_canonical: &canonical,
stop: Some(verb_sugar::VolStop { length: stop_length, k: stop_k }),
};
verb_sugar::run_walkforward_sugar( verb_sugar::run_walkforward_sugar(
&fast, &slow, stop_length, stop_k, WINNER_SELECTION_METRIC, &inv,
is_ms, oos_ms, step_ms, &name, &symbol, from_ms, to_ms, canonical, env, WINNER_SELECTION_METRIC,
verb_sugar::WfWindows {
in_sample_ms: is_ms,
out_of_sample_ms: oos_ms,
step_ms,
},
env,
) )
.unwrap_or_else(|m| { .unwrap_or_else(|m| {
eprintln!("aura: {m}"); eprintln!("aura: {m}");
@@ -2977,18 +2953,36 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
}); });
return; return;
} }
// Synthetic in-process family path — unchanged (#220 non-goal).
if a.stop_length.is_some() || a.stop_k.is_some() || a.trace.is_some() {
eprintln!("aura: {}", usage());
std::process::exit(2);
}
let select = match a.select.as_deref() {
Some(s) => parse_select(s).unwrap_or_else(|()| {
eprintln!("aura: {}", usage());
std::process::exit(2);
}),
None => Selection::Argmax,
};
let name = a.name.clone().unwrap_or_else(|| "walkforward".to_string());
run_blueprint_walkforward(&doc, &axes, &name, DataSource::Synthetic, select, env);
}
None => {
eprintln!("aura: {}", usage()); eprintln!("aura: {}", usage());
std::process::exit(2); std::process::exit(2);
} }
} }
} }
/// `aura mc`: loaded-blueprint Monte-Carlo (an existing `.json` first-positional), or /// `aura mc`: Monte-Carlo over a loaded blueprint — the synthetic seed family
/// the built-in synthetic seed-resweep / r-sma R-bootstrap split. /// (`--seeds`, closed blueprint) or the R-bootstrap campaign path (`--real`,
/// #220: arbitrary user blueprint + axes, formerly the welded r-sma branch).
fn dispatch_mc(a: McCmd, env: &project::Env) { fn dispatch_mc(a: McCmd, env: &project::Env) {
// Single-sourced: every arm below reads this one closure (house style, #179).
let usage = || "Usage: aura mc <blueprint.json> --seeds <n> [--name <n>] | aura mc <blueprint.json> --real <SYMBOL> --axis <name>=<csv> [--axis …] --stop-length <n> --stop-k <x> [--block-len <n>] [--resamples <n>] [--seed <n>] [--from <ms>] [--to <ms>]".to_string();
match is_blueprint_file(&a.blueprint) { match is_blueprint_file(&a.blueprint) {
Some(path) => { Some(path) => {
let usage = "Usage: aura mc <blueprint.json> --seeds <n> [--name <n>]";
let doc = std::fs::read_to_string(path).unwrap_or_else(|e| { let doc = std::fs::read_to_string(path).unwrap_or_else(|e| {
eprintln!("aura: {path}: {e}"); eprintln!("aura: {path}: {e}");
std::process::exit(2); std::process::exit(2);
@@ -2997,57 +2991,42 @@ fn dispatch_mc(a: McCmd, env: &project::Env) {
eprintln!("aura: {path}: {msg}"); eprintln!("aura: {path}: {msg}");
std::process::exit(2); std::process::exit(2);
} }
// A built-in-only flag with a blueprint file is not in this grammar if a.real.is_some() {
// (MC over a loaded blueprint is synthetic-only this cycle). // The campaign path (#220): blueprint + --real + --axis routes
if a.strategy.is_some() // the R-bootstrap pipeline through the one campaign executor.
|| a.real.is_some() // The two mc modes stay disjoint: --seeds belongs to the
|| a.from.is_some() // synthetic seed family only.
|| a.to.is_some() if a.seeds.is_some() {
|| a.fast.is_some() eprintln!("aura: {}", usage());
|| a.slow.is_some()
|| a.stop_length.is_some()
|| a.stop_k.is_some()
|| a.block_len.is_some()
|| a.resamples.is_some()
|| a.seed.is_some()
|| a.trace.is_some()
{
eprintln!("aura: {usage}");
std::process::exit(2);
}
let n_seeds = match a.seeds {
Some(n) if n > 0 => n,
_ => {
eprintln!("aura: {usage}");
std::process::exit(2); std::process::exit(2);
} }
}; let (name, symbol, stop_length, stop_k, block_len, resamples, seed, from, to) =
let name = a.name.clone().unwrap_or_else(|| "mc".to_string());
run_blueprint_mc(&doc, n_seeds, &name, DataSource::Synthetic, env);
}
None => {
let usage = || "Usage: aura mc <blueprint.json> --seeds <n> [--name <n>] | aura mc --strategy r-sma --real <SYMBOL> [--from <ms>] [--to <ms>] [--fast <csv>] [--slow <csv>] [--stop-length <csv>] [--stop-k <csv>] [--block-len <n>] [--resamples <n>] [--seed <n>]".to_string();
if a.blueprint.is_some() || a.seeds.is_some() {
eprintln!("aura: {}", usage());
std::process::exit(2);
}
// Dissolution split (#210, Reading A): only the real-archive r-sma
// R-bootstrap execution routes through the campaign path. Everything else —
// synthetic-r-sma (no --real), the synthetic seed-resweep, the blueprint
// path — stays on the inline handler below, fenced until #159.
if a.strategy.as_deref() == Some("r-sma") && a.real.is_some() {
let (name, symbol, fast, slow, stop_length, stop_k, block_len, resamples, seed, from, to) =
mc_args_from(&a).unwrap_or_else(|m| { mc_args_from(&a).unwrap_or_else(|m| {
eprintln!("aura: {m}"); eprintln!("aura: {m}");
std::process::exit(2); std::process::exit(2);
}); });
let canonical = include_str!("../examples/r_sma_open.json"); let axes = parse_axes(&a.axis, &usage).unwrap_or_else(|m| {
let reg = env.registry(); eprintln!("aura: {m}");
let topo = content_id(canonical); std::process::exit(2);
reg.put_blueprint(&topo, canonical).unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(1);
}); });
if axes.is_empty() {
eprintln!("aura: {}", usage());
std::process::exit(2);
}
// Axis validation + canonicalize/register + the wrapped->raw
// strip are single-sourced in `validate_and_register_axes`
// (data-free, so this fires before the archive is touched).
let (canonical, raw_axes) = validate_and_register_axes(&doc, &axes, env)
.unwrap_or_else(|e| match e {
AxisRegisterError::UnknownAxis(m) => {
eprintln!("aura: {m}");
std::process::exit(2);
}
AxisRegisterError::Registry(m) => {
eprintln!("aura: {m}");
std::process::exit(1);
}
});
// `blueprint_walkforward_family` sources its span from // `blueprint_walkforward_family` sources its span from
// `DataSource::wf_full_span`, which for Real ALWAYS clips `--from`/`--to` // `DataSource::wf_full_span`, which for Real ALWAYS clips `--from`/`--to`
// to the archive's actual first/last bar in range: a holiday/weekend edge // to the archive's actual first/last bar in range: a holiday/weekend edge
@@ -3064,10 +3043,25 @@ fn dispatch_mc(a: McCmd, env: &project::Env) {
aura_ingest::epoch_ns_to_unix_ms(to_ts), aura_ingest::epoch_ns_to_unix_ms(to_ts),
); );
let (is_ms, oos_ms, step_ms) = wf_ms_sizes(); let (is_ms, oos_ms, step_ms) = wf_ms_sizes();
let inv = verb_sugar::SugarInvocation {
axes: &raw_axes,
name,
symbols: vec![symbol],
from_ms,
to_ms,
blueprint_canonical: &canonical,
stop: Some(verb_sugar::VolStop { length: stop_length, k: stop_k }),
};
verb_sugar::run_mc_sugar( verb_sugar::run_mc_sugar(
&fast, &slow, stop_length, stop_k, WINNER_SELECTION_METRIC, &inv,
is_ms, oos_ms, step_ms, resamples, block_len, seed, &name, &symbol, from_ms, to_ms, WINNER_SELECTION_METRIC,
canonical, env, verb_sugar::WfWindows {
in_sample_ms: is_ms,
out_of_sample_ms: oos_ms,
step_ms,
},
verb_sugar::McKnobs { resamples, block_len, seed },
env,
) )
.unwrap_or_else(|m| { .unwrap_or_else(|m| {
eprintln!("aura: {m}"); eprintln!("aura: {m}");
@@ -3075,24 +3069,32 @@ fn dispatch_mc(a: McCmd, env: &project::Env) {
}); });
return; return;
} }
// Any r-sma-only knob reaching here (past the dissolved sugar path above) // Synthetic seed family (unchanged, #220 non-goal): closed
// is a usage error now — the inline R-bootstrap execution retired with // blueprint, --seeds only; every campaign-mode flag is rejected.
// `run_r_sma` (#159); the sole real-R execution is the sugar path above. if a.from.is_some()
let r_path = a.strategy.is_some()
|| a.real.is_some()
|| a.from.is_some()
|| a.to.is_some() || a.to.is_some()
|| a.fast.is_some()
|| a.slow.is_some()
|| a.stop_length.is_some() || a.stop_length.is_some()
|| a.stop_k.is_some() || a.stop_k.is_some()
|| a.block_len.is_some() || a.block_len.is_some()
|| a.resamples.is_some() || a.resamples.is_some()
|| a.seed.is_some(); || a.seed.is_some()
if r_path { || a.trace.is_some()
|| !a.axis.is_empty()
{
eprintln!("aura: {}", usage()); eprintln!("aura: {}", usage());
std::process::exit(2); std::process::exit(2);
} }
let n_seeds = match a.seeds {
Some(n) if n > 0 => n,
_ => {
eprintln!("aura: {}", usage());
std::process::exit(2);
}
};
let name = a.name.clone().unwrap_or_else(|| "mc".to_string());
run_blueprint_mc(&doc, n_seeds, &name, DataSource::Synthetic, env);
}
None => {
eprintln!("aura: {}", usage()); eprintln!("aura: {}", usage());
std::process::exit(2); std::process::exit(2);
} }
@@ -4141,14 +4143,12 @@ mod tests {
fn bare_mc_cmd() -> McCmd { fn bare_mc_cmd() -> McCmd {
McCmd { McCmd {
blueprint: None, blueprint: None,
strategy: None,
real: None, real: None,
from: None, from: None,
to: None, to: None,
name: None, name: None,
trace: None, trace: None,
fast: None, axis: Vec::new(),
slow: None,
stop_length: None, stop_length: None,
stop_k: None, stop_k: None,
block_len: None, block_len: None,
@@ -4186,15 +4186,10 @@ mod tests {
#[test] #[test]
fn mc_args_from_refuses_missing_knobs() { fn mc_args_from_refuses_missing_knobs() {
let a = McCmd { let a = McCmd { real: Some("GER40".to_string()), ..bare_mc_cmd() };
real: Some("GER40".to_string()),
fast: Some("3".to_string()),
slow: Some("12".to_string()),
..bare_mc_cmd()
};
let err = mc_args_from(&a).unwrap_err(); let err = mc_args_from(&a).unwrap_err();
assert!( assert!(
err.contains("requires --fast --slow --stop-length --stop-k"), err.contains("requires --stop-length --stop-k"),
"missing-knob refusal: {err}" "missing-knob refusal: {err}"
); );
} }
@@ -4203,8 +4198,6 @@ mod tests {
fn mc_args_from_refuses_a_multi_value_stop() { fn mc_args_from_refuses_a_multi_value_stop() {
let a = McCmd { let a = McCmd {
real: Some("GER40".to_string()), real: Some("GER40".to_string()),
fast: Some("3".to_string()),
slow: Some("12".to_string()),
stop_length: Some("14,20".to_string()), stop_length: Some("14,20".to_string()),
stop_k: Some("2.0".to_string()), stop_k: Some("2.0".to_string()),
..bare_mc_cmd() ..bare_mc_cmd()
+124 -216
View File
@@ -26,10 +26,9 @@ pub(crate) struct GeneratedSweep {
/// One dissolved-verb invocation's shared shape (#214/#220): arbitrary RAW /// One dissolved-verb invocation's shared shape (#214/#220): arbitrary RAW
/// axes (post `wrapped_to_raw_axis` strip), the family name, the instrument /// axes (post `wrapped_to_raw_axis` strip), the family name, the instrument
/// list, the shared window, the canonical blueprint, and the optional single /// list, the shared window, the canonical blueprint, and the optional single
/// Vol stop regime (`None` binds no regime — sweep's contract). Sweep and /// Vol stop regime (`None` binds no regime — sweep's contract). All four
/// generalize route their dispatch args through this shape today; /// dissolved verbs (sweep, generalize, walkforward, mc) route their dispatch
/// walkforward/mc still build their args directly via their own /// args through this shape.
/// `--fast`/`--slow` builders, pending their own #220 dissolution.
pub(crate) struct SugarInvocation<'a> { pub(crate) struct SugarInvocation<'a> {
pub axes: &'a [(String, Vec<Scalar>)], pub axes: &'a [(String, Vec<Scalar>)],
pub name: String, pub name: String,
@@ -48,6 +47,22 @@ pub(crate) struct VolStop {
pub k: f64, pub k: f64,
} }
/// The walk-forward roller sizes (ms) a wf/mc invocation carries.
#[derive(Clone, Copy)]
pub(crate) struct WfWindows {
pub in_sample_ms: u64,
pub out_of_sample_ms: u64,
pub step_ms: u64,
}
/// The mc-specific bootstrap knobs.
#[derive(Clone, Copy)]
pub(crate) struct McKnobs {
pub resamples: u32,
pub block_len: u32,
pub seed: u64,
}
/// The generic doc-axes map: every invocation axis under its arbitrary raw /// The generic doc-axes map: every invocation axis under its arbitrary raw
/// bind-key (the sweep shape, adopted by all four verbs — #220). /// bind-key (the sweep shape, adopted by all four verbs — #220).
fn doc_axes_from(axes: &[(String, Vec<Scalar>)]) -> Result<BTreeMap<String, Axis>, String> { fn doc_axes_from(axes: &[(String, Vec<Scalar>)]) -> Result<BTreeMap<String, Axis>, String> {
@@ -349,29 +364,17 @@ pub(crate) struct GeneratedWalkforward {
pub campaign: CampaignDoc, pub campaign: CampaignDoc,
} }
/// Translate one `aura walkforward --strategy r-sma --real` invocation into its two /// Translate one `aura walkforward --real` invocation into its two generated
/// generated documents: a `[std::sweep(argmax), std::walk_forward]` process (the /// documents: a `[std::sweep(argmax), std::walk_forward]` process (the sweep
/// sweep only enumerates the IS-refit survivor grid for the wf stage; its recorded /// only enumerates the IS-refit survivor grid for the wf stage; its recorded
/// argmax selection is not part of the summary) and a campaign running the fast/slow /// argmax selection is not part of the summary) and a campaign running the
/// grid over one instrument under a single risk regime that carries the stop. The /// invocation's axis grid over one instrument under its single risk regime.
/// roller sizes (`in_sample_ms`/`out_of_sample_ms`/`step_ms`) come from the caller /// The roller sizes come from `w` (ms). `inv.blueprint_canonical` is the user's
/// (ms). `blueprint_canonical` is the bare `sma_signal` blueprint already stored by /// blueprint already stored by topology hash; its content id is the strategy ref.
/// topology hash; its content id is the strategy ref.
#[allow(clippy::too_many_arguments)]
pub(crate) fn translate_walkforward( pub(crate) fn translate_walkforward(
fast: &[i64], inv: &SugarInvocation,
slow: &[i64],
stop_length: i64,
stop_k: f64,
metric: &str, metric: &str,
in_sample_ms: u64, w: WfWindows,
out_of_sample_ms: u64,
step_ms: u64,
name: &str,
symbol: &str,
from_ms: i64,
to_ms: i64,
blueprint_canonical: &str,
) -> Result<GeneratedWalkforward, String> { ) -> Result<GeneratedWalkforward, String> {
let process = ProcessDoc { let process = ProcessDoc {
format_version: FORMAT_VERSION, format_version: FORMAT_VERSION,
@@ -387,39 +390,30 @@ pub(crate) fn translate_walkforward(
}), }),
}, },
StageBlock::WalkForward { StageBlock::WalkForward {
in_sample_ms, in_sample_ms: w.in_sample_ms,
out_of_sample_ms, out_of_sample_ms: w.out_of_sample_ms,
step_ms, step_ms: w.step_ms,
mode: WfMode::Rolling, mode: WfMode::Rolling,
metric: metric.to_string(), metric: metric.to_string(),
select: SelectRule::Argmax, select: SelectRule::Argmax,
}, },
], ],
}; };
let mut doc_axes: BTreeMap<String, Axis> = BTreeMap::new();
doc_axes.insert(
"fast.length".to_string(),
axis_from_values("fast.length", &fast.iter().map(|&v| Scalar::i64(v)).collect::<Vec<_>>())?,
);
doc_axes.insert(
"slow.length".to_string(),
axis_from_values("slow.length", &slow.iter().map(|&v| Scalar::i64(v)).collect::<Vec<_>>())?,
);
let campaign = CampaignDoc { let campaign = CampaignDoc {
format_version: FORMAT_VERSION, format_version: FORMAT_VERSION,
kind: DocKind::Campaign, kind: DocKind::Campaign,
name: name.to_string(), name: inv.name.clone(),
description: None, description: None,
data: DataSection { data: DataSection {
instruments: vec![symbol.to_string()], instruments: inv.symbols.clone(),
windows: vec![Window { from_ms, to_ms }], windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }],
}, },
strategies: vec![StrategyEntry { strategies: vec![StrategyEntry {
r#ref: DocRef::ContentId(content_id_of(blueprint_canonical)), r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)),
axes: doc_axes, axes: doc_axes_from(inv.axes)?,
}], }],
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) }, process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
risk: vec![RiskRegime::Vol { length: stop_length, k: stop_k }], risk: risk_from(inv.stop),
seed: 0, seed: 0,
presentation: Presentation { presentation: Presentation {
persist_taps: vec![], persist_taps: vec![],
@@ -445,34 +439,17 @@ pub(crate) fn register_generated_wf(
/// Run one dissolved `walkforward` invocation end-to-end: register the generated /// Run one dissolved `walkforward` invocation end-to-end: register the generated
/// documents, run through the one campaign path, then reprint the per-window member /// documents, run through the one campaign path, then reprint the per-window member
/// lines + the summary line from the recorded `WalkForward` family. The stdout /// lines + the summary line from the recorded `WalkForward` family. The stdout
/// summary line is byte-identical to the inline path (the committed exact-grade /// summary line is byte-identical to the retired welded path (the committed
/// anchor is the gate). /// exact-grade anchor is the gate).
#[allow(clippy::too_many_arguments)]
pub(crate) fn run_walkforward_sugar( pub(crate) fn run_walkforward_sugar(
fast: &[i64], inv: &SugarInvocation,
slow: &[i64],
stop_length: i64,
stop_k: f64,
metric: &str, metric: &str,
in_sample_ms: u64, w: WfWindows,
out_of_sample_ms: u64,
step_ms: u64,
name: &str,
symbol: &str,
from_ms: i64,
to_ms: i64,
blueprint_canonical: &str,
env: &crate::project::Env, env: &crate::project::Env,
) -> Result<(), String> { ) -> Result<(), String> {
let generated = translate_walkforward( let generated = translate_walkforward(inv, metric, w)?;
fast, slow, stop_length, stop_k, metric, in_sample_ms, out_of_sample_ms, step_ms, let probe_params = probe_params_from(inv.axes);
name, symbol, from_ms, to_ms, blueprint_canonical, validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?;
)?;
let probe_params: Vec<(String, Scalar)> = vec![
("fast.length".to_string(), Scalar::i64(fast[0])),
("slow.length".to_string(), Scalar::i64(slow[0])),
];
validate_before_register(&generated.process, &generated.campaign, blueprint_canonical, &probe_params, env)?;
let reg = env.registry(); let reg = env.registry();
let (_process_id, campaign_id) = register_generated_wf(&reg, &generated)?; let (_process_id, campaign_id) = register_generated_wf(&reg, &generated)?;
let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?; let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?;
@@ -489,7 +466,14 @@ pub(crate) fn run_walkforward_sugar(
for report in &wf.reports { for report in &wf.reports {
println!("{}", crate::family_member_line(&wf.family_id, report)); println!("{}", crate::family_member_line(&wf.family_id, report));
} }
println!("{}", crate::walkforward_summary_json_from_reports(&wf.reports)); // Summary axes: the invocation's raw axis names in argv order, then the
// stop columns when a regime is bound (#220 — the renderer hardcodes none).
let mut summary_axes: Vec<String> = inv.axes.iter().map(|(n, _)| n.clone()).collect();
if inv.stop.is_some() {
summary_axes.push("stop_length".to_string());
summary_axes.push("stop_k".to_string());
}
println!("{}", crate::walkforward_summary_json_from_reports(&wf.reports, &summary_axes));
Ok(()) Ok(())
} }
@@ -500,36 +484,22 @@ pub(crate) struct GeneratedMc {
pub campaign: CampaignDoc, pub campaign: CampaignDoc,
} }
/// Translate one `aura mc --strategy r-sma --real` invocation into its two generated /// Translate one `aura mc --real` invocation into its two generated documents:
/// documents: a `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` process /// a `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` process (the
/// (the sweep enumerates the IS-refit survivor grid for the wf stage; the terminal /// sweep enumerates the IS-refit survivor grid for the wf stage; the terminal
/// monte_carlo stage pools the per-window OOS trade-R series and r-bootstraps `E[R]`) /// monte_carlo stage pools the per-window OOS trade-R series and r-bootstraps
/// and a campaign running the fast/slow grid over one instrument under a single risk /// `E[R]`) and a campaign running the invocation's axis grid over one instrument
/// regime that carries the stop. Unlike `translate_walkforward` (which hardcodes /// under its single risk regime. Unlike `translate_walkforward` (which hardcodes
/// `seed: 0`), `translate_mc` sets `campaign.seed = seed` (the mc `--seed`): the wf /// `seed: 0`), `translate_mc` sets `campaign.seed = mc.seed` (the mc `--seed`):
/// winners are argmax hence deflation-seed-independent (the shipped walkforward /// the wf winners are argmax hence deflation-seed-independent (the shipped
/// anchor proved this), so remapping the campaign seed leaves the pooled series /// walkforward anchor proved this), so remapping the campaign seed leaves the
/// unchanged while making the terminal `r_bootstrap` at that seed reproduce the /// pooled series unchanged while making the terminal `r_bootstrap` at that seed
/// inline bootstrap. The roller sizes (`in_sample_ms`/`out_of_sample_ms`/`step_ms`) /// reproduce the retired inline bootstrap.
/// and `blueprint_canonical` come from the caller, exactly as for walkforward.
#[allow(clippy::too_many_arguments)]
pub(crate) fn translate_mc( pub(crate) fn translate_mc(
fast: &[i64], inv: &SugarInvocation,
slow: &[i64],
stop_length: i64,
stop_k: f64,
metric: &str, metric: &str,
in_sample_ms: u64, w: WfWindows,
out_of_sample_ms: u64, mc: McKnobs,
step_ms: u64,
resamples: u32,
block_len: u32,
seed: u64,
name: &str,
symbol: &str,
from_ms: i64,
to_ms: i64,
blueprint_canonical: &str,
) -> Result<GeneratedMc, String> { ) -> Result<GeneratedMc, String> {
let process = ProcessDoc { let process = ProcessDoc {
format_version: FORMAT_VERSION, format_version: FORMAT_VERSION,
@@ -545,41 +515,32 @@ pub(crate) fn translate_mc(
}), }),
}, },
StageBlock::WalkForward { StageBlock::WalkForward {
in_sample_ms, in_sample_ms: w.in_sample_ms,
out_of_sample_ms, out_of_sample_ms: w.out_of_sample_ms,
step_ms, step_ms: w.step_ms,
mode: WfMode::Rolling, mode: WfMode::Rolling,
metric: metric.to_string(), metric: metric.to_string(),
select: SelectRule::Argmax, select: SelectRule::Argmax,
}, },
StageBlock::MonteCarlo { resamples, block_len }, StageBlock::MonteCarlo { resamples: mc.resamples, block_len: mc.block_len },
], ],
}; };
let mut doc_axes: BTreeMap<String, Axis> = BTreeMap::new();
doc_axes.insert(
"fast.length".to_string(),
axis_from_values("fast.length", &fast.iter().map(|&v| Scalar::i64(v)).collect::<Vec<_>>())?,
);
doc_axes.insert(
"slow.length".to_string(),
axis_from_values("slow.length", &slow.iter().map(|&v| Scalar::i64(v)).collect::<Vec<_>>())?,
);
let campaign = CampaignDoc { let campaign = CampaignDoc {
format_version: FORMAT_VERSION, format_version: FORMAT_VERSION,
kind: DocKind::Campaign, kind: DocKind::Campaign,
name: name.to_string(), name: inv.name.clone(),
description: None, description: None,
data: DataSection { data: DataSection {
instruments: vec![symbol.to_string()], instruments: inv.symbols.clone(),
windows: vec![Window { from_ms, to_ms }], windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }],
}, },
strategies: vec![StrategyEntry { strategies: vec![StrategyEntry {
r#ref: DocRef::ContentId(content_id_of(blueprint_canonical)), r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)),
axes: doc_axes, axes: doc_axes_from(inv.axes)?,
}], }],
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) }, process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
risk: vec![RiskRegime::Vol { length: stop_length, k: stop_k }], risk: risk_from(inv.stop),
seed, seed: mc.seed,
presentation: Presentation { presentation: Presentation {
persist_taps: vec![], persist_taps: vec![],
emit: vec!["family_table".to_string()], emit: vec!["family_table".to_string()],
@@ -604,38 +565,19 @@ pub(crate) fn register_generated_mc(
/// Run one dissolved `mc` invocation end-to-end: register the generated documents, /// Run one dissolved `mc` invocation end-to-end: register the generated documents,
/// run through the one campaign path, then reprint the single `mc_r_bootstrap` grade /// run through the one campaign path, then reprint the single `mc_r_bootstrap` grade
/// line from the recorded terminal `PooledOos` bootstrap. The stdout line is /// line from the recorded terminal `PooledOos` bootstrap. The stdout line is
/// byte-identical to the inline path (the committed exact-grade anchor is the gate). /// byte-identical to the retired welded path (the committed exact-grade anchor is
/// mc prints ONLY the one summary line — no per-window member lines (unlike /// the gate). mc prints ONLY the one summary line — no per-window member lines
/// walkforward): the monte_carlo stage is a terminal annotator, not a family. /// (unlike walkforward): the monte_carlo stage is a terminal annotator, not a family.
#[allow(clippy::too_many_arguments)]
pub(crate) fn run_mc_sugar( pub(crate) fn run_mc_sugar(
fast: &[i64], inv: &SugarInvocation,
slow: &[i64],
stop_length: i64,
stop_k: f64,
metric: &str, metric: &str,
in_sample_ms: u64, w: WfWindows,
out_of_sample_ms: u64, mc: McKnobs,
step_ms: u64,
resamples: u32,
block_len: u32,
seed: u64,
name: &str,
symbol: &str,
from_ms: i64,
to_ms: i64,
blueprint_canonical: &str,
env: &crate::project::Env, env: &crate::project::Env,
) -> Result<(), String> { ) -> Result<(), String> {
let generated = translate_mc( let generated = translate_mc(inv, metric, w, mc)?;
fast, slow, stop_length, stop_k, metric, in_sample_ms, out_of_sample_ms, step_ms, let probe_params = probe_params_from(inv.axes);
resamples, block_len, seed, name, symbol, from_ms, to_ms, blueprint_canonical, validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?;
)?;
let probe_params: Vec<(String, Scalar)> = vec![
("fast.length".to_string(), Scalar::i64(fast[0])),
("slow.length".to_string(), Scalar::i64(slow[0])),
];
validate_before_register(&generated.process, &generated.campaign, blueprint_canonical, &probe_params, env)?;
let reg = env.registry(); let reg = env.registry();
let (_process_id, campaign_id) = register_generated_mc(&reg, &generated)?; let (_process_id, campaign_id) = register_generated_mc(&reg, &generated)?;
let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?; let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?;
@@ -693,6 +635,19 @@ mod tests {
] ]
} }
fn wf_axes() -> Vec<(String, Vec<Scalar>)> {
vec![
("fast.length".to_string(), vec![Scalar::i64(3), Scalar::i64(5)]),
("slow.length".to_string(), vec![Scalar::i64(12), Scalar::i64(20)]),
]
}
const WF_W: WfWindows = WfWindows {
in_sample_ms: 7_776_000_000,
out_of_sample_ms: 2_592_000_000,
step_ms: 2_592_000_000,
};
#[test] #[test]
fn translate_sweep_is_deterministic_and_names_flow() { fn translate_sweep_is_deterministic_and_names_flow() {
let ax = axes(); let ax = axes();
@@ -860,18 +815,10 @@ mod tests {
#[test] #[test]
fn translate_walkforward_is_deterministic_and_carries_the_regime() { fn translate_walkforward_is_deterministic_and_carries_the_regime() {
let a = translate_walkforward( let ax = wf_axes();
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", let i = inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
7_776_000_000, 2_592_000_000, 2_592_000_000, let a = translate_walkforward(&i, "sqn_normalized", WF_W).unwrap();
"walkforward", "GER40", 100, 200, "{\"bp\":1}", let b = translate_walkforward(&i, "sqn_normalized", WF_W).unwrap();
)
.unwrap();
let b = translate_walkforward(
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized",
7_776_000_000, 2_592_000_000, 2_592_000_000,
"walkforward", "GER40", 100, 200, "{\"bp\":1}",
)
.unwrap();
assert_eq!( assert_eq!(
content_id_of(&campaign_to_json(&a.campaign)), content_id_of(&campaign_to_json(&a.campaign)),
content_id_of(&campaign_to_json(&b.campaign)), content_id_of(&campaign_to_json(&b.campaign)),
@@ -918,24 +865,10 @@ mod tests {
#[test] #[test]
fn translate_walkforward_diverging_regimes_do_not_collide_content_ids() { fn translate_walkforward_diverging_regimes_do_not_collide_content_ids() {
let base = translate_walkforward( let ax = wf_axes();
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", let base = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
7_776_000_000, 2_592_000_000, 2_592_000_000, let different_k = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
"walkforward", "GER40", 100, 200, "{\"bp\":1}", let different_length = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
)
.unwrap();
let different_k = translate_walkforward(
&[3, 5], &[12, 20], 14, 3.0, "sqn_normalized",
7_776_000_000, 2_592_000_000, 2_592_000_000,
"walkforward", "GER40", 100, 200, "{\"bp\":1}",
)
.unwrap();
let different_length = translate_walkforward(
&[3, 5], &[12, 20], 20, 2.0, "sqn_normalized",
7_776_000_000, 2_592_000_000, 2_592_000_000,
"walkforward", "GER40", 100, 200, "{\"bp\":1}",
)
.unwrap();
let base_id = content_id_of(&campaign_to_json(&base.campaign)); let base_id = content_id_of(&campaign_to_json(&base.campaign));
let k_id = content_id_of(&campaign_to_json(&different_k.campaign)); let k_id = content_id_of(&campaign_to_json(&different_k.campaign));
let length_id = content_id_of(&campaign_to_json(&different_length.campaign)); let length_id = content_id_of(&campaign_to_json(&different_length.campaign));
@@ -955,10 +888,11 @@ mod tests {
)); ));
std::fs::create_dir_all(&dir).unwrap(); std::fs::create_dir_all(&dir).unwrap();
let reg = aura_registry::Registry::open(dir.join("runs.jsonl")); let reg = aura_registry::Registry::open(dir.join("runs.jsonl"));
let ax = wf_axes();
let generated = translate_walkforward( let generated = translate_walkforward(
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", &inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"),
7_776_000_000, 2_592_000_000, 2_592_000_000, "sqn_normalized",
"walkforward", "GER40", 100, 200, "{\"bp\":1}", WF_W,
) )
.unwrap(); .unwrap();
let (process_id, campaign_id) = register_generated_wf(&reg, &generated).unwrap(); let (process_id, campaign_id) = register_generated_wf(&reg, &generated).unwrap();
@@ -975,20 +909,10 @@ mod tests {
#[test] #[test]
fn translate_mc_is_deterministic_and_carries_the_regime_and_seed() { fn translate_mc_is_deterministic_and_carries_the_regime_and_seed() {
let a = translate_mc( let ax = wf_axes();
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", let i = inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
7_776_000_000, 2_592_000_000, 2_592_000_000, let a = translate_mc(&i, "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap();
1000, 5, 42, let b = translate_mc(&i, "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap();
"mc", "GER40", 100, 200, "{\"bp\":1}",
)
.unwrap();
let b = translate_mc(
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized",
7_776_000_000, 2_592_000_000, 2_592_000_000,
1000, 5, 42,
"mc", "GER40", 100, 200, "{\"bp\":1}",
)
.unwrap();
assert_eq!( assert_eq!(
content_id_of(&campaign_to_json(&a.campaign)), content_id_of(&campaign_to_json(&a.campaign)),
content_id_of(&campaign_to_json(&b.campaign)), content_id_of(&campaign_to_json(&b.campaign)),
@@ -1036,27 +960,10 @@ mod tests {
#[test] #[test]
fn translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids() { fn translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids() {
let base = translate_mc( let ax = wf_axes();
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", let base = translate_mc(&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap();
7_776_000_000, 2_592_000_000, 2_592_000_000, let different_seed = translate_mc(&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 7 }).unwrap();
1000, 5, 42, let different_k = translate_mc(&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap();
"mc", "GER40", 100, 200, "{\"bp\":1}",
)
.unwrap();
let different_seed = translate_mc(
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized",
7_776_000_000, 2_592_000_000, 2_592_000_000,
1000, 5, 7,
"mc", "GER40", 100, 200, "{\"bp\":1}",
)
.unwrap();
let different_k = translate_mc(
&[3, 5], &[12, 20], 14, 3.0, "sqn_normalized",
7_776_000_000, 2_592_000_000, 2_592_000_000,
1000, 5, 42,
"mc", "GER40", 100, 200, "{\"bp\":1}",
)
.unwrap();
let base_id = content_id_of(&campaign_to_json(&base.campaign)); let base_id = content_id_of(&campaign_to_json(&base.campaign));
let seed_id = content_id_of(&campaign_to_json(&different_seed.campaign)); let seed_id = content_id_of(&campaign_to_json(&different_seed.campaign));
let k_id = content_id_of(&campaign_to_json(&different_k.campaign)); let k_id = content_id_of(&campaign_to_json(&different_k.campaign));
@@ -1073,11 +980,12 @@ mod tests {
)); ));
std::fs::create_dir_all(&dir).unwrap(); std::fs::create_dir_all(&dir).unwrap();
let reg = aura_registry::Registry::open(dir.join("runs.jsonl")); let reg = aura_registry::Registry::open(dir.join("runs.jsonl"));
let ax = wf_axes();
let generated = translate_mc( let generated = translate_mc(
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized", &inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"),
7_776_000_000, 2_592_000_000, 2_592_000_000, "sqn_normalized",
1000, 5, 42, WF_W,
"mc", "GER40", 100, 200, "{\"bp\":1}", McKnobs { resamples: 1000, block_len: 5, seed: 42 },
) )
.unwrap(); .unwrap();
let (process_id, campaign_id) = register_generated_mc(&reg, &generated).unwrap(); let (process_id, campaign_id) = register_generated_mc(&reg, &generated).unwrap();
+276 -50
View File
@@ -1048,12 +1048,9 @@ fn mc_rejects_real_flag_with_usage_exit_2() {
.current_dir(&dir) .current_dir(&dir)
.output() .output()
.expect("spawn aura mc --real"); .expect("spawn aura mc --real");
assert_eq!(out.status.code(), Some(2), "mc --real must exit 2; status: {:?}", out.status); assert_eq!(out.status.code(), Some(2), "mc --real without a blueprint is a usage error");
assert!(out.stdout.is_empty(), "mc --real must not emit a run on stdout: {:?}", out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr); let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage"), "mc --real stderr must carry usage: {stderr:?}"); assert!(stderr.contains("Usage: aura mc"), "usage refusal: {stderr}");
// the exclusion is total: no `runs/` registry write either.
assert!(!dir.join("runs").exists(), "mc --real must not start a real run");
let _ = std::fs::remove_dir_all(&dir); 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 /// 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 /// `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. /// 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 /// An unknown `--strategy` name reaches sweep's closure; the bare verb reaches
/// it is the smallest trigger for each. /// walkforward's.
#[test] #[test]
fn builtin_branch_usage_lines_lead_with_the_house_style_prefix() { 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 dir = temp_cwd("builtin_usage_prefix");
let out = Command::new(BIN) let out = Command::new(BIN)
.args(args) .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 /// 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 /// (`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 /// for a single `"Usage"` occurrence — a regression that fixed only the first
/// alternative (leaving `… | mc --strategy r-sma …` unprefixed) would still /// alternative (leaving the second `| aura mc <blueprint.json> --real …`
/// pass it. `--seeds` with no blueprint file is the built-in branch's own /// 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 /// grammar violation, reaching this exact literal. (#159 cut 4: the first
/// alternative's literal is now the blueprint-mode grammar, not a bare /// alternative's literal is now the blueprint-mode grammar, not a bare
/// `[--name` — `mc` is blueprint-first.) /// `[--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:?}" "first alternative must name the program: {stderr:?}"
); );
assert!( assert!(
stderr.contains("| aura mc --strategy r-sma"), stderr.contains("| aura mc <blueprint.json> --real"),
"second alternative must ALSO name the program: {stderr:?}" "second alternative must ALSO name the program: {stderr:?}"
); );
let _ = std::fs::remove_dir_all(&dir); 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}")); .unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status); assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr); let stderr = String::from_utf8_lossy(&out.stderr);
let want = format!("aura: Usage: aura {verb} "); if verb == "sweep" {
assert!( let want = format!("aura: Usage: aura {verb} ");
stderr.starts_with(&want), assert!(
"`aura {args:?}` must fall into the generic usage error, not a special-cased \ stderr.starts_with(&want),
message: {stderr:?}" "`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); assert!(out.stdout.is_empty(), "no family summary on the rejected path: {:?}", out.stdout);
let _ = std::fs::remove_dir_all(&cwd); 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}")); .unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status); assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr); let stderr = String::from_utf8_lossy(&out.stderr);
let want = format!("aura: Usage: aura {verb} "); if verb == "sweep" {
assert!(stderr.starts_with(&want), "generic usage, not special-cased: {stderr:?}"); 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); assert!(out.stdout.is_empty(), "no summary on the rejected path: {:?}", out.stdout);
let _ = std::fs::remove_dir_all(&cwd); let _ = std::fs::remove_dir_all(&cwd);
} }
@@ -1272,12 +1284,19 @@ fn sweep_and_walkforward_reject_retired_pip_tokens_as_unrecognized() {
out.status out.status
); );
let stderr = String::from_utf8_lossy(&out.stderr); let stderr = String::from_utf8_lossy(&out.stderr);
let want = format!("aura: Usage: aura {verb} "); if verb == "sweep" {
assert!( let want = format!("aura: Usage: aura {verb} ");
stderr.starts_with(&want), assert!(
"`aura {args:?}` must fall into the generic usage error, not a \ stderr.starts_with(&want),
special-cased message: {stderr:?}" "`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); assert!(out.stdout.is_empty(), "no summary on the rejected path: {:?}", out.stdout);
let _ = std::fs::remove_dir_all(&cwd); 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 FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000"; const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("walkforward-exact-grade"); 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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"walkforward", "--strategy", "r-sma", "--real", "GER40", "walkforward", &fixture, "--real", "GER40",
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0", "--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, "--from", FROM_MS, "--to", TO_MS,
]) ])
.current_dir(&cwd) .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}"); 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 /// 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. /// multi-value --stop-length/--stop-k is refused (exit 2) before any run.
#[test] #[test]
fn walkforward_dissolved_refuses_a_multi_value_stop() { fn walkforward_dissolved_refuses_a_multi_value_stop() {
let cwd = temp_cwd("walkforward-multi-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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"walkforward", "--strategy", "r-sma", "--real", "GER40", "walkforward", &fixture, "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0", "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14,20", "--stop-k", "2.0",
]) ])
.current_dir(&cwd) .current_dir(&cwd)
.output() .output()
@@ -1558,10 +1636,12 @@ fn walkforward_dissolved_refuses_a_multi_value_stop() {
#[test] #[test]
fn walkforward_dissolved_refuses_select_plateau() { fn walkforward_dissolved_refuses_select_plateau() {
let cwd = temp_cwd("walkforward-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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"walkforward", "--strategy", "r-sma", "--real", "GER40", "walkforward", &fixture, "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0", "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--select", "plateau:worst", "--select", "plateau:worst",
]) ])
.current_dir(&cwd) .current_dir(&cwd)
@@ -1577,14 +1657,18 @@ fn walkforward_dissolved_refuses_select_plateau() {
#[test] #[test]
fn walkforward_dissolved_refuses_missing_knobs() { fn walkforward_dissolved_refuses_missing_knobs() {
let cwd = temp_cwd("walkforward-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")) 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) .current_dir(&cwd)
.output() .output()
.expect("spawn aura"); .expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "missing --stop-length/--stop-k is a usage refusal"); assert_eq!(out.status.code(), Some(2), "missing --stop-length/--stop-k is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr); 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 /// An empty `--real ""` still satisfies clap's `Option::is_some()` dispatch guard, so
@@ -1593,10 +1677,12 @@ fn walkforward_dissolved_refuses_missing_knobs() {
#[test] #[test]
fn walkforward_dissolved_refuses_an_empty_real_symbol() { fn walkforward_dissolved_refuses_an_empty_real_symbol() {
let cwd = temp_cwd("walkforward-empty-real"); 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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"walkforward", "--strategy", "r-sma", "--real", "", "walkforward", &fixture, "--real", "",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0", "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
]) ])
.current_dir(&cwd) .current_dir(&cwd)
.output() .output()
@@ -1606,16 +1692,51 @@ fn walkforward_dissolved_refuses_an_empty_real_symbol() {
assert!(stderr.contains("dissolves only over --real"), "empty-real refusal: {stderr}"); 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 /// 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 /// 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. /// swallowed by `select_is_plateau`'s plateau-only check and fall through to argmax.
#[test] #[test]
fn walkforward_dissolved_refuses_an_unknown_select_token() { fn walkforward_dissolved_refuses_an_unknown_select_token() {
let cwd = temp_cwd("walkforward-bad-select"); 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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"walkforward", "--strategy", "r-sma", "--real", "GER40", "walkforward", &fixture, "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0", "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--select", "bogus", "--select", "bogus",
]) ])
.current_dir(&cwd) .current_dir(&cwd)
@@ -1633,10 +1754,12 @@ fn walkforward_dissolved_refuses_an_unknown_select_token() {
#[test] #[test]
fn walkforward_dissolved_refuses_name_and_trace_together() { fn walkforward_dissolved_refuses_name_and_trace_together() {
let cwd = temp_cwd("walkforward-name-and-trace"); 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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"walkforward", "--strategy", "r-sma", "--real", "GER40", "walkforward", &fixture, "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0", "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--name", "a", "--trace", "b", "--name", "a", "--trace", "b",
]) ])
.current_dir(&cwd) .current_dir(&cwd)
@@ -1662,10 +1785,12 @@ fn walkforward_dissolves_through_the_campaign_path() {
const FROM_MS: &str = "1735689600000"; const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000"; const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("walkforward-dissolves"); 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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"walkforward", "--strategy", "r-sma", "--real", "GER40", "walkforward", &fixture, "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0", "--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, "--from", FROM_MS, "--to", TO_MS,
]) ])
.current_dir(&cwd) .current_dir(&cwd)
@@ -3035,7 +3160,7 @@ fn dual_grammar_stray_positional_is_a_usage_error_not_swallowed() {
for argv in [ for argv in [
&["run", "bogus"][..], &["run", "bogus"][..],
&["sweep", "bogus", "--name", "x"][..], &["sweep", "bogus", "--name", "x"][..],
&["walkforward", "bogus", "--strategy", "sma"][..], &["walkforward", "bogus"][..],
&["mc", "bogus"][..], &["mc", "bogus"][..],
] { ] {
let out = Command::new(BIN).args(argv).output().expect("spawn aura <sub> <stray-positional>"); 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 FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000"; const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("mc-r-bootstrap-exact-grade"); 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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"mc", "--strategy", "r-sma", "--real", "GER40", "mc", &fixture, "--real", "GER40",
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0", "--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", "--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS, "--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}"); 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 /// 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 /// path — a successful run durably auto-registers exactly one generated process
/// document, one generated campaign document (carrying the constant "mc" name and the /// 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 FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000"; const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("mc-dissolves"); 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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"mc", "--strategy", "r-sma", "--real", "GER40", "mc", &fixture, "--real", "GER40",
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0", "--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", "--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS, "--from", FROM_MS, "--to", TO_MS,
]) ])
@@ -3267,10 +3455,12 @@ fn mc_dissolves_through_the_campaign_path() {
#[test] #[test]
fn mc_dissolved_refuses_a_multi_value_stop() { fn mc_dissolved_refuses_a_multi_value_stop() {
let cwd = temp_cwd("mc-multi-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")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"mc", "--strategy", "r-sma", "--real", "GER40", "mc", &fixture, "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0", "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14,20", "--stop-k", "2.0",
]) ])
.current_dir(&cwd) .current_dir(&cwd)
.output() .output()
@@ -3280,6 +3470,40 @@ fn mc_dissolved_refuses_a_multi_value_stop() {
assert!(stderr.contains("single risk regime"), "stop-regime refusal: {stderr}"); 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` /// Property: the one load-bearing decision new to the mc dissolution — `translate_mc`
/// maps `campaign.seed` to the mc `--seed` (unlike `translate_walkforward`, which /// maps `campaign.seed` to the mc `--seed` (unlike `translate_walkforward`, which
/// hardcodes `seed: 0`) — actually flows through the executor to the emitted grade, /// 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() { fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() {
const FROM_MS: &str = "1735689600000"; const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000"; const TO_MS: &str = "1767225599000";
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let run = |seed: &str| { let run = |seed: &str| {
let cwd = temp_cwd(&format!("mc-seed-{seed}")); let cwd = temp_cwd(&format!("mc-seed-{seed}"));
std::process::Command::new(env!("CARGO_BIN_EXE_aura")) std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([ .args([
"mc", "--strategy", "r-sma", "--real", "GER40", "mc", &fixture, "--real", "GER40",
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0", "--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, "--block-len", "5", "--resamples", "1000", "--seed", seed,
"--from", FROM_MS, "--to", TO_MS, "--from", FROM_MS, "--to", TO_MS,
]) ])