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:
+260
-267
@@ -786,31 +786,29 @@ fn walkforward_summary_json(result: &WalkForwardResult) -> String {
|
||||
|
||||
/// The walk-forward summary line reconstructed from the recorded per-window OOS
|
||||
/// reports (the campaign path's `WalkForward` `StageFamily.reports`) rather than a
|
||||
/// live `WalkForwardResult`. Byte-identical to `walkforward_summary_json`:
|
||||
/// `stitched_total_pips` = the per-window `total_pips` summed left-to-right in roll
|
||||
/// order (the engine `stitch` folds each segment's final cumulative value, and a
|
||||
/// window's OOS segment ends at its `total_pips`); `param_stability` reduces each
|
||||
/// r-sma IS-refit axis over the per-window chosen params (read from each report's
|
||||
/// `manifest.params` via [`campaign_run::raw_matches_wrapped`] — the blueprint axes
|
||||
/// `fast.length`/`slow.length` are recorded wrapped as `sma_signal.fast.length` etc.
|
||||
/// by the strategy's own param space, while `stop_length`/`stop_k` ride the risk
|
||||
/// regime unwrapped, so an exact-name match would miss the wrapped pair) through the
|
||||
/// same `MetricStats::from_values`; the `oos_r` block pools the per-window `trade_rs`
|
||||
/// through `r_metrics_from_rs`. Canonical JSON (C14).
|
||||
/// live `WalkForwardResult`. `stitched_total_pips` = the per-window `total_pips`
|
||||
/// summed left-to-right in roll order (the engine `stitch` folds each segment's
|
||||
/// final cumulative value, and a window's OOS segment ends at its `total_pips`);
|
||||
/// `param_stability` reduces each IS-refit axis in `axes` over the per-window
|
||||
/// chosen params (read from each report's `manifest.params` via
|
||||
/// [`campaign_run::raw_matches_wrapped`] — blueprint axes are recorded wrapped
|
||||
/// (e.g. `sma_signal.fast.length`) by the strategy's own param space, while
|
||||
/// `stop_length`/`stop_k` ride the risk regime unwrapped, so an exact-name match
|
||||
/// would miss the wrapped ones) through the same `MetricStats::from_values`; the
|
||||
/// `oos_r` block pools the per-window `trade_rs` through `r_metrics_from_rs`.
|
||||
/// Canonical JSON (C14).
|
||||
///
|
||||
/// The axis list + order mirror what `r_sma_space` yields (fast, slow, stop_length,
|
||||
/// stop_k) — the same order + values the inline `param_stability` reduces. A wrong
|
||||
/// order or axis would fail the committed exact-grade anchor loudly (it pins
|
||||
/// `param_stability[0].mean` = the fast-MA refit mean), never silently.
|
||||
fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String {
|
||||
/// `axes` carries the invocation's raw axis names in argv order, followed by the
|
||||
/// stop columns when a regime is bound (#220 — no axis name is hardcoded here).
|
||||
/// Order matters: the committed exact-grade anchor pins
|
||||
/// `param_stability[0].mean` = the first axis's refit mean.
|
||||
fn walkforward_summary_json_from_reports(reports: &[RunReport], axes: &[String]) -> String {
|
||||
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
|
||||
// decided per-value at runtime by the `Scalar` variant match below (i64 axes
|
||||
// cast value-as-f64, the f64 axis passes through), exactly as
|
||||
// `param_stability`'s per-`ScalarKind` coerce does — this list carries no
|
||||
// per-axis type flag.
|
||||
const AXES: [&str; 4] = ["fast.length", "slow.length", "stop_length", "stop_k"];
|
||||
let param_stability: Vec<aura_engine::MetricStats> = AXES
|
||||
// Coercion to `f64` is decided per-value at runtime by the `Scalar` variant
|
||||
// match below (i64 axes cast value-as-f64, the f64 axis passes through),
|
||||
// exactly as `param_stability`'s per-`ScalarKind` coerce does — the axis
|
||||
// list carries no per-axis type flag.
|
||||
let param_stability: Vec<aura_engine::MetricStats> = axes
|
||||
.iter()
|
||||
.map(|axis| {
|
||||
let vals: Vec<f64> = reports
|
||||
@@ -821,7 +819,7 @@ fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String {
|
||||
.params
|
||||
.iter()
|
||||
.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 {
|
||||
Scalar::I64(i) => *i as f64,
|
||||
Scalar::F64(f) => *f,
|
||||
@@ -2187,11 +2185,8 @@ struct SweepCmd {
|
||||
|
||||
#[derive(Args)]
|
||||
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>,
|
||||
/// 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.
|
||||
#[arg(long)]
|
||||
real: Option<String>,
|
||||
@@ -2207,16 +2202,10 @@ struct WalkforwardCmd {
|
||||
/// Family name that also persists each OOS window's taps (mutually exclusive with --name).
|
||||
#[arg(long)]
|
||||
trace: Option<String>,
|
||||
/// Fast-MA IS-refit grid axis (comma-separated values).
|
||||
#[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).
|
||||
/// Campaign-path stop length (single value; --real mode).
|
||||
#[arg(long)]
|
||||
stop_length: Option<String>,
|
||||
/// Stop-k IS-refit grid axis (comma-separated values).
|
||||
/// Campaign-path stop-k multiple (single value; --real mode).
|
||||
#[arg(long)]
|
||||
stop_k: Option<String>,
|
||||
/// In-sample winner selection: argmax | plateau:mean | plateau:worst (default argmax).
|
||||
@@ -2231,10 +2220,8 @@ struct WalkforwardCmd {
|
||||
struct McCmd {
|
||||
/// A loaded blueprint (.json); omit for the built-in grammar.
|
||||
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 for the R-bootstrap (recorded data); requires --strategy r-sma.
|
||||
/// Real instrument symbol for the R-bootstrap campaign path (recorded data); omit
|
||||
/// for the synthetic seed family.
|
||||
#[arg(long)]
|
||||
real: Option<String>,
|
||||
/// 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).
|
||||
#[arg(long)]
|
||||
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)]
|
||||
fast: Option<String>,
|
||||
/// Slow-MA grid axis for the R-bootstrap (comma-separated values).
|
||||
#[arg(long)]
|
||||
slow: Option<String>,
|
||||
/// Stop-length grid axis for the R-bootstrap (comma-separated values).
|
||||
axis: Vec<String>,
|
||||
/// Campaign-path stop length (single value; --real mode).
|
||||
#[arg(long)]
|
||||
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)]
|
||||
stop_k: Option<String>,
|
||||
/// 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
|
||||
/// consumes (the dissolved `--strategy r-sma --real` branch). Single instrument,
|
||||
/// multi-value fast/slow (the IS-refit grid), single-value stop (Fork A: the stop is
|
||||
/// a risk regime, not a swept axis); all four knobs required. `parse_csv_list`
|
||||
/// (`:1586`) is generic over `T: FromStr`, so the same helper parses the i64 grids
|
||||
/// and the f64 stop; the local `knobs` closure (captureless → `Copy`, mirroring
|
||||
/// `generalize_args_from`'s idiom) is reused across the four `ok_or_else` calls.
|
||||
/// `--name`/`--trace` are mutually exclusive here too, matching the flag's own
|
||||
/// documented contract and the inline path's `name_persist` refusal.
|
||||
/// consumes (the dissolved `.json --real` branch). Single instrument, single-value
|
||||
/// stop (Fork A: the stop is a risk regime, not a swept axis); `parse_csv_list` is
|
||||
/// generic over `T: FromStr`, so the same helper parses the i64 length and the f64
|
||||
/// k; the local `knobs` closure (captureless → `Copy`, mirroring `mc_args_from`'s
|
||||
/// idiom) is reused across both `ok_or_else` calls. `--name`/`--trace` are
|
||||
/// mutually exclusive, matching the flag's own documented contract and the inline
|
||||
/// path's `name_persist` refusal; an omitted flag defaults the family name to
|
||||
/// "walkforward". The IS-refit `--axis` grid is parsed separately at the dispatch
|
||||
/// site, not here.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn walkforward_args_from(
|
||||
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() {
|
||||
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(),
|
||||
};
|
||||
let knobs = || "walkforward requires --fast --slow --stop-length --stop-k (fast/slow may be CSV grids; the stop is single-value)".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 knobs = || "walkforward requires --stop-length --stop-k (single-value; the stop is a risk regime)".to_string();
|
||||
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())?;
|
||||
if stop_length.len() != 1 || stop_k.len() != 1 {
|
||||
@@ -2354,35 +2337,30 @@ fn walkforward_args_from(
|
||||
(None, Some(t)) => t.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
|
||||
/// dissolved `--strategy r-sma --real` branch). Single instrument, multi-value
|
||||
/// fast/slow (the IS-refit grid), single-value stop (Fork A: the stop is a risk
|
||||
/// regime, not a swept axis); all four grid knobs required. `--block-len`/
|
||||
/// `--resamples`/`--seed` default to `1`/`1000`/`1` — the same defaults the retired
|
||||
/// real-R dispatch used, so an omitted flag produces the same document either way. The usize->u32 conversion for `block_len`/`resamples`
|
||||
/// lands HERE, at the argv boundary where the CLI's `usize` meets the document's
|
||||
/// `u32` vocabulary (`StageBlock::MonteCarlo`), keeping every downstream signature
|
||||
/// in the document's native type. `--name`/`--trace` are rejected: the inline 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").
|
||||
/// `--real` campaign branch). Single instrument, single-value stop (Fork A: the
|
||||
/// stop is a risk regime, not a swept axis). `--block-len`/`--resamples`/`--seed`
|
||||
/// default to `1`/`1000`/`1` — the same defaults the retired real-R dispatch
|
||||
/// used, so an omitted flag produces the same document either way. The
|
||||
/// usize->u32 conversion lands HERE, at the argv boundary where the CLI's
|
||||
/// `usize` meets the document's `u32` vocabulary (`StageBlock::MonteCarlo`).
|
||||
/// `--name`/`--trace` are rejected: the R-bootstrap records without a family
|
||||
/// name (the campaign name is a constant "mc").
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn mc_args_from(
|
||||
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() {
|
||||
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(),
|
||||
};
|
||||
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 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 knobs = || "mc requires --stop-length --stop-k (single-value; the stop is a risk regime)".to_string();
|
||||
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())?;
|
||||
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 resamples = a.resamples.map(|v| v as u32).unwrap_or(1000);
|
||||
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
|
||||
@@ -2483,6 +2461,54 @@ fn parse_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
|
||||
/// the built-in harness-kind dispatch.
|
||||
fn dispatch_run(a: RunCmd, env: &project::Env) {
|
||||
@@ -2612,28 +2638,17 @@ fn dispatch_generalize(a: GeneralizeCmd, env: &project::Env) {
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
// Validate every --axis name against the WRAPPED probe namespace before
|
||||
// stripping to the raw campaign namespace (the sweep sequence, #210 c0110).
|
||||
let space = blueprint_axis_probe(&doc, env).param_space();
|
||||
for (n, _) in &axes {
|
||||
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 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);
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
// Window: the explicit --from/--to when both present (byte-identical to the
|
||||
// retired welded path, verified by the exact-grade anchor); otherwise the
|
||||
// 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 {
|
||||
axes: &raw_axes,
|
||||
name,
|
||||
@@ -2783,27 +2792,20 @@ fn dispatch_sweep(a: SweepCmd, env: &project::Env) {
|
||||
// strategy ref resolves against this one write; a second
|
||||
// put under `content_id_of(&canonical)` would target the
|
||||
// identical key and is not needed.
|
||||
// FIX (#210 c0110 finding "wrapped-name refusal mangles the
|
||||
// axis"): validate every `--axis` name against the WRAPPED
|
||||
// probe namespace (`blueprint_axis_probe`/`--list-axes` —
|
||||
// the same names #203's strip later consumes) BEFORE
|
||||
// stripping/translating to the raw campaign namespace. A
|
||||
// raw-form or fat-fingered name is refused here, echoing
|
||||
// exactly what the user typed, instead of surfacing a
|
||||
// stripped-then-mangled fragment deep inside a member run.
|
||||
// Data-free (the probe only reloads the blueprint), so
|
||||
// this fires before the archive is touched.
|
||||
let space = blueprint_axis_probe(&doc, env).param_space();
|
||||
for (n, _) in &axes {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
let symbol = symbol.clone();
|
||||
let source = DataSource::from_choice(data, 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(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 {
|
||||
axes: &raw_axes,
|
||||
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
|
||||
/// `.json` first-positional) or the built-in `--strategy` walk-forward.
|
||||
/// `aura walkforward`: IS-refit walk-forward over a loaded blueprint — the
|
||||
/// 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) {
|
||||
// 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) {
|
||||
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| {
|
||||
eprintln!("aura: {path}: {e}");
|
||||
std::process::exit(2);
|
||||
@@ -2888,16 +2869,6 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
|
||||
eprintln!("aura: {path}: {msg}");
|
||||
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| {
|
||||
eprintln!("aura: {m}");
|
||||
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");
|
||||
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 => {
|
||||
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 a.real.is_some() {
|
||||
// The campaign path (#220): the real-archive execution routes
|
||||
// through the one campaign executor, over the user's own
|
||||
// blueprint and axes (formerly the welded r-sma branch).
|
||||
if let Some(s) = a.select.as_deref()
|
||||
&& 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");
|
||||
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| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
let canonical = include_str!("../examples/r_sma_open.json");
|
||||
let reg = env.registry();
|
||||
let topo = content_id(canonical);
|
||||
reg.put_blueprint(&topo, canonical).unwrap_or_else(|e| {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
// Unlike `dispatch_generalize` (a single run per instrument, insensitive
|
||||
// to a day's edge shift), `blueprint_walkforward_family` sources its span
|
||||
// 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),
|
||||
);
|
||||
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(
|
||||
&fast, &slow, stop_length, stop_k, WINNER_SELECTION_METRIC,
|
||||
is_ms, oos_ms, step_ms, &name, &symbol, from_ms, to_ms, canonical, env,
|
||||
&inv,
|
||||
WINNER_SELECTION_METRIC,
|
||||
verb_sugar::WfWindows {
|
||||
in_sample_ms: is_ms,
|
||||
out_of_sample_ms: oos_ms,
|
||||
step_ms,
|
||||
},
|
||||
env,
|
||||
)
|
||||
.unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
@@ -2977,18 +2953,36 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
|
||||
});
|
||||
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());
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `aura mc`: loaded-blueprint Monte-Carlo (an existing `.json` first-positional), or
|
||||
/// the built-in synthetic seed-resweep / r-sma R-bootstrap split.
|
||||
/// `aura mc`: Monte-Carlo over a loaded blueprint — the synthetic seed family
|
||||
/// (`--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) {
|
||||
// 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) {
|
||||
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| {
|
||||
eprintln!("aura: {path}: {e}");
|
||||
std::process::exit(2);
|
||||
@@ -2997,57 +2991,42 @@ fn dispatch_mc(a: McCmd, env: &project::Env) {
|
||||
eprintln!("aura: {path}: {msg}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
// A built-in-only flag with a blueprint file is not in this grammar
|
||||
// (MC over a loaded blueprint is synthetic-only this cycle).
|
||||
if a.strategy.is_some()
|
||||
|| a.real.is_some()
|
||||
|| a.from.is_some()
|
||||
|| a.to.is_some()
|
||||
|| a.fast.is_some()
|
||||
|| 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}");
|
||||
if a.real.is_some() {
|
||||
// The campaign path (#220): blueprint + --real + --axis routes
|
||||
// the R-bootstrap pipeline through the one campaign executor.
|
||||
// The two mc modes stay disjoint: --seeds belongs to the
|
||||
// synthetic seed family only.
|
||||
if a.seeds.is_some() {
|
||||
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 => {
|
||||
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) =
|
||||
let (name, symbol, stop_length, stop_k, block_len, resamples, seed, from, to) =
|
||||
mc_args_from(&a).unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
let canonical = include_str!("../examples/r_sma_open.json");
|
||||
let reg = env.registry();
|
||||
let topo = content_id(canonical);
|
||||
reg.put_blueprint(&topo, canonical).unwrap_or_else(|e| {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
let axes = parse_axes(&a.axis, &usage).unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
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
|
||||
// `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
|
||||
@@ -3064,10 +3043,25 @@ fn dispatch_mc(a: McCmd, env: &project::Env) {
|
||||
aura_ingest::epoch_ns_to_unix_ms(to_ts),
|
||||
);
|
||||
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(
|
||||
&fast, &slow, stop_length, stop_k, WINNER_SELECTION_METRIC,
|
||||
is_ms, oos_ms, step_ms, resamples, block_len, seed, &name, &symbol, from_ms, to_ms,
|
||||
canonical, env,
|
||||
&inv,
|
||||
WINNER_SELECTION_METRIC,
|
||||
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| {
|
||||
eprintln!("aura: {m}");
|
||||
@@ -3075,24 +3069,32 @@ fn dispatch_mc(a: McCmd, env: &project::Env) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Any r-sma-only knob reaching here (past the dissolved sugar path above)
|
||||
// is a usage error now — the inline R-bootstrap execution retired with
|
||||
// `run_r_sma` (#159); the sole real-R execution is the sugar path above.
|
||||
let r_path = a.strategy.is_some()
|
||||
|| a.real.is_some()
|
||||
|| a.from.is_some()
|
||||
// Synthetic seed family (unchanged, #220 non-goal): closed
|
||||
// blueprint, --seeds only; every campaign-mode flag is rejected.
|
||||
if a.from.is_some()
|
||||
|| a.to.is_some()
|
||||
|| a.fast.is_some()
|
||||
|| 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();
|
||||
if r_path {
|
||||
|| a.seed.is_some()
|
||||
|| a.trace.is_some()
|
||||
|| !a.axis.is_empty()
|
||||
{
|
||||
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);
|
||||
}
|
||||
};
|
||||
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());
|
||||
std::process::exit(2);
|
||||
}
|
||||
@@ -4141,14 +4143,12 @@ mod tests {
|
||||
fn bare_mc_cmd() -> McCmd {
|
||||
McCmd {
|
||||
blueprint: None,
|
||||
strategy: None,
|
||||
real: None,
|
||||
from: None,
|
||||
to: None,
|
||||
name: None,
|
||||
trace: None,
|
||||
fast: None,
|
||||
slow: None,
|
||||
axis: Vec::new(),
|
||||
stop_length: None,
|
||||
stop_k: None,
|
||||
block_len: None,
|
||||
@@ -4186,15 +4186,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mc_args_from_refuses_missing_knobs() {
|
||||
let a = McCmd {
|
||||
real: Some("GER40".to_string()),
|
||||
fast: Some("3".to_string()),
|
||||
slow: Some("12".to_string()),
|
||||
..bare_mc_cmd()
|
||||
};
|
||||
let a = McCmd { real: Some("GER40".to_string()), ..bare_mc_cmd() };
|
||||
let err = mc_args_from(&a).unwrap_err();
|
||||
assert!(
|
||||
err.contains("requires --fast --slow --stop-length --stop-k"),
|
||||
err.contains("requires --stop-length --stop-k"),
|
||||
"missing-knob refusal: {err}"
|
||||
);
|
||||
}
|
||||
@@ -4203,8 +4198,6 @@ mod tests {
|
||||
fn mc_args_from_refuses_a_multi_value_stop() {
|
||||
let a = McCmd {
|
||||
real: Some("GER40".to_string()),
|
||||
fast: Some("3".to_string()),
|
||||
slow: Some("12".to_string()),
|
||||
stop_length: Some("14,20".to_string()),
|
||||
stop_k: Some("2.0".to_string()),
|
||||
..bare_mc_cmd()
|
||||
|
||||
+124
-216
@@ -26,10 +26,9 @@ pub(crate) struct GeneratedSweep {
|
||||
/// One dissolved-verb invocation's shared shape (#214/#220): arbitrary RAW
|
||||
/// axes (post `wrapped_to_raw_axis` strip), the family name, the instrument
|
||||
/// list, the shared window, the canonical blueprint, and the optional single
|
||||
/// Vol stop regime (`None` binds no regime — sweep's contract). Sweep and
|
||||
/// generalize route their dispatch args through this shape today;
|
||||
/// walkforward/mc still build their args directly via their own
|
||||
/// `--fast`/`--slow` builders, pending their own #220 dissolution.
|
||||
/// Vol stop regime (`None` binds no regime — sweep's contract). All four
|
||||
/// dissolved verbs (sweep, generalize, walkforward, mc) route their dispatch
|
||||
/// args through this shape.
|
||||
pub(crate) struct SugarInvocation<'a> {
|
||||
pub axes: &'a [(String, Vec<Scalar>)],
|
||||
pub name: String,
|
||||
@@ -48,6 +47,22 @@ pub(crate) struct VolStop {
|
||||
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
|
||||
/// bind-key (the sweep shape, adopted by all four verbs — #220).
|
||||
fn doc_axes_from(axes: &[(String, Vec<Scalar>)]) -> Result<BTreeMap<String, Axis>, String> {
|
||||
@@ -349,29 +364,17 @@ pub(crate) struct GeneratedWalkforward {
|
||||
pub campaign: CampaignDoc,
|
||||
}
|
||||
|
||||
/// Translate one `aura walkforward --strategy r-sma --real` invocation into its two
|
||||
/// generated documents: a `[std::sweep(argmax), std::walk_forward]` process (the
|
||||
/// sweep 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
|
||||
/// grid over one instrument under a single risk regime that carries the stop. The
|
||||
/// roller sizes (`in_sample_ms`/`out_of_sample_ms`/`step_ms`) come from the caller
|
||||
/// (ms). `blueprint_canonical` is the bare `sma_signal` blueprint already stored by
|
||||
/// topology hash; its content id is the strategy ref.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// Translate one `aura walkforward --real` invocation into its two generated
|
||||
/// documents: a `[std::sweep(argmax), std::walk_forward]` process (the sweep
|
||||
/// 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
|
||||
/// invocation's axis grid over one instrument under its single risk regime.
|
||||
/// The roller sizes come from `w` (ms). `inv.blueprint_canonical` is the user's
|
||||
/// blueprint already stored by topology hash; its content id is the strategy ref.
|
||||
pub(crate) fn translate_walkforward(
|
||||
fast: &[i64],
|
||||
slow: &[i64],
|
||||
stop_length: i64,
|
||||
stop_k: f64,
|
||||
inv: &SugarInvocation,
|
||||
metric: &str,
|
||||
in_sample_ms: u64,
|
||||
out_of_sample_ms: u64,
|
||||
step_ms: u64,
|
||||
name: &str,
|
||||
symbol: &str,
|
||||
from_ms: i64,
|
||||
to_ms: i64,
|
||||
blueprint_canonical: &str,
|
||||
w: WfWindows,
|
||||
) -> Result<GeneratedWalkforward, String> {
|
||||
let process = ProcessDoc {
|
||||
format_version: FORMAT_VERSION,
|
||||
@@ -387,39 +390,30 @@ pub(crate) fn translate_walkforward(
|
||||
}),
|
||||
},
|
||||
StageBlock::WalkForward {
|
||||
in_sample_ms,
|
||||
out_of_sample_ms,
|
||||
step_ms,
|
||||
in_sample_ms: w.in_sample_ms,
|
||||
out_of_sample_ms: w.out_of_sample_ms,
|
||||
step_ms: w.step_ms,
|
||||
mode: WfMode::Rolling,
|
||||
metric: metric.to_string(),
|
||||
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 {
|
||||
format_version: FORMAT_VERSION,
|
||||
kind: DocKind::Campaign,
|
||||
name: name.to_string(),
|
||||
name: inv.name.clone(),
|
||||
description: None,
|
||||
data: DataSection {
|
||||
instruments: vec![symbol.to_string()],
|
||||
windows: vec![Window { from_ms, to_ms }],
|
||||
instruments: inv.symbols.clone(),
|
||||
windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }],
|
||||
},
|
||||
strategies: vec![StrategyEntry {
|
||||
r#ref: DocRef::ContentId(content_id_of(blueprint_canonical)),
|
||||
axes: doc_axes,
|
||||
r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)),
|
||||
axes: doc_axes_from(inv.axes)?,
|
||||
}],
|
||||
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,
|
||||
presentation: Presentation {
|
||||
persist_taps: vec![],
|
||||
@@ -445,34 +439,17 @@ pub(crate) fn register_generated_wf(
|
||||
/// Run one dissolved `walkforward` invocation end-to-end: register the generated
|
||||
/// documents, run through the one campaign path, then reprint the per-window member
|
||||
/// lines + the summary line from the recorded `WalkForward` family. The stdout
|
||||
/// summary line is byte-identical to the inline path (the committed exact-grade
|
||||
/// anchor is the gate).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// summary line is byte-identical to the retired welded path (the committed
|
||||
/// exact-grade anchor is the gate).
|
||||
pub(crate) fn run_walkforward_sugar(
|
||||
fast: &[i64],
|
||||
slow: &[i64],
|
||||
stop_length: i64,
|
||||
stop_k: f64,
|
||||
inv: &SugarInvocation,
|
||||
metric: &str,
|
||||
in_sample_ms: u64,
|
||||
out_of_sample_ms: u64,
|
||||
step_ms: u64,
|
||||
name: &str,
|
||||
symbol: &str,
|
||||
from_ms: i64,
|
||||
to_ms: i64,
|
||||
blueprint_canonical: &str,
|
||||
w: WfWindows,
|
||||
env: &crate::project::Env,
|
||||
) -> Result<(), String> {
|
||||
let generated = translate_walkforward(
|
||||
fast, slow, stop_length, stop_k, metric, in_sample_ms, out_of_sample_ms, step_ms,
|
||||
name, symbol, from_ms, to_ms, blueprint_canonical,
|
||||
)?;
|
||||
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 generated = translate_walkforward(inv, metric, w)?;
|
||||
let probe_params = probe_params_from(inv.axes);
|
||||
validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?;
|
||||
let reg = env.registry();
|
||||
let (_process_id, campaign_id) = register_generated_wf(®, &generated)?;
|
||||
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 {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -500,36 +484,22 @@ pub(crate) struct GeneratedMc {
|
||||
pub campaign: CampaignDoc,
|
||||
}
|
||||
|
||||
/// Translate one `aura mc --strategy r-sma --real` invocation into its two generated
|
||||
/// documents: a `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` process
|
||||
/// (the 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]`)
|
||||
/// and a campaign running the fast/slow grid over one instrument under a single risk
|
||||
/// regime that carries the stop. Unlike `translate_walkforward` (which hardcodes
|
||||
/// `seed: 0`), `translate_mc` sets `campaign.seed = seed` (the mc `--seed`): the wf
|
||||
/// winners are argmax hence deflation-seed-independent (the shipped walkforward
|
||||
/// anchor proved this), so remapping the campaign seed leaves the pooled series
|
||||
/// unchanged while making the terminal `r_bootstrap` at that seed reproduce the
|
||||
/// inline bootstrap. The roller sizes (`in_sample_ms`/`out_of_sample_ms`/`step_ms`)
|
||||
/// and `blueprint_canonical` come from the caller, exactly as for walkforward.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// Translate one `aura mc --real` invocation into its two generated documents:
|
||||
/// a `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` process (the
|
||||
/// 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]`) and a campaign running the invocation's axis grid over one instrument
|
||||
/// under its single risk regime. Unlike `translate_walkforward` (which hardcodes
|
||||
/// `seed: 0`), `translate_mc` sets `campaign.seed = mc.seed` (the mc `--seed`):
|
||||
/// the wf winners are argmax hence deflation-seed-independent (the shipped
|
||||
/// walkforward anchor proved this), so remapping the campaign seed leaves the
|
||||
/// pooled series unchanged while making the terminal `r_bootstrap` at that seed
|
||||
/// reproduce the retired inline bootstrap.
|
||||
pub(crate) fn translate_mc(
|
||||
fast: &[i64],
|
||||
slow: &[i64],
|
||||
stop_length: i64,
|
||||
stop_k: f64,
|
||||
inv: &SugarInvocation,
|
||||
metric: &str,
|
||||
in_sample_ms: u64,
|
||||
out_of_sample_ms: u64,
|
||||
step_ms: u64,
|
||||
resamples: u32,
|
||||
block_len: u32,
|
||||
seed: u64,
|
||||
name: &str,
|
||||
symbol: &str,
|
||||
from_ms: i64,
|
||||
to_ms: i64,
|
||||
blueprint_canonical: &str,
|
||||
w: WfWindows,
|
||||
mc: McKnobs,
|
||||
) -> Result<GeneratedMc, String> {
|
||||
let process = ProcessDoc {
|
||||
format_version: FORMAT_VERSION,
|
||||
@@ -545,41 +515,32 @@ pub(crate) fn translate_mc(
|
||||
}),
|
||||
},
|
||||
StageBlock::WalkForward {
|
||||
in_sample_ms,
|
||||
out_of_sample_ms,
|
||||
step_ms,
|
||||
in_sample_ms: w.in_sample_ms,
|
||||
out_of_sample_ms: w.out_of_sample_ms,
|
||||
step_ms: w.step_ms,
|
||||
mode: WfMode::Rolling,
|
||||
metric: metric.to_string(),
|
||||
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 {
|
||||
format_version: FORMAT_VERSION,
|
||||
kind: DocKind::Campaign,
|
||||
name: name.to_string(),
|
||||
name: inv.name.clone(),
|
||||
description: None,
|
||||
data: DataSection {
|
||||
instruments: vec![symbol.to_string()],
|
||||
windows: vec![Window { from_ms, to_ms }],
|
||||
instruments: inv.symbols.clone(),
|
||||
windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }],
|
||||
},
|
||||
strategies: vec![StrategyEntry {
|
||||
r#ref: DocRef::ContentId(content_id_of(blueprint_canonical)),
|
||||
axes: doc_axes,
|
||||
r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)),
|
||||
axes: doc_axes_from(inv.axes)?,
|
||||
}],
|
||||
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
||||
risk: vec![RiskRegime::Vol { length: stop_length, k: stop_k }],
|
||||
seed,
|
||||
risk: risk_from(inv.stop),
|
||||
seed: mc.seed,
|
||||
presentation: Presentation {
|
||||
persist_taps: vec![],
|
||||
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 through the one campaign path, then reprint the single `mc_r_bootstrap` grade
|
||||
/// 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).
|
||||
/// mc prints ONLY the one summary line — no per-window member lines (unlike
|
||||
/// walkforward): the monte_carlo stage is a terminal annotator, not a family.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// byte-identical to the retired welded path (the committed exact-grade anchor is
|
||||
/// the gate). mc prints ONLY the one summary line — no per-window member lines
|
||||
/// (unlike walkforward): the monte_carlo stage is a terminal annotator, not a family.
|
||||
pub(crate) fn run_mc_sugar(
|
||||
fast: &[i64],
|
||||
slow: &[i64],
|
||||
stop_length: i64,
|
||||
stop_k: f64,
|
||||
inv: &SugarInvocation,
|
||||
metric: &str,
|
||||
in_sample_ms: u64,
|
||||
out_of_sample_ms: u64,
|
||||
step_ms: u64,
|
||||
resamples: u32,
|
||||
block_len: u32,
|
||||
seed: u64,
|
||||
name: &str,
|
||||
symbol: &str,
|
||||
from_ms: i64,
|
||||
to_ms: i64,
|
||||
blueprint_canonical: &str,
|
||||
w: WfWindows,
|
||||
mc: McKnobs,
|
||||
env: &crate::project::Env,
|
||||
) -> Result<(), String> {
|
||||
let generated = translate_mc(
|
||||
fast, slow, stop_length, stop_k, metric, in_sample_ms, out_of_sample_ms, step_ms,
|
||||
resamples, block_len, seed, name, symbol, from_ms, to_ms, blueprint_canonical,
|
||||
)?;
|
||||
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 generated = translate_mc(inv, metric, w, mc)?;
|
||||
let probe_params = probe_params_from(inv.axes);
|
||||
validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?;
|
||||
let reg = env.registry();
|
||||
let (_process_id, campaign_id) = register_generated_mc(®, &generated)?;
|
||||
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]
|
||||
fn translate_sweep_is_deterministic_and_names_flow() {
|
||||
let ax = axes();
|
||||
@@ -860,18 +815,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn translate_walkforward_is_deterministic_and_carries_the_regime() {
|
||||
let a = 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();
|
||||
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();
|
||||
let ax = wf_axes();
|
||||
let i = inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
|
||||
let a = translate_walkforward(&i, "sqn_normalized", WF_W).unwrap();
|
||||
let b = translate_walkforward(&i, "sqn_normalized", WF_W).unwrap();
|
||||
assert_eq!(
|
||||
content_id_of(&campaign_to_json(&a.campaign)),
|
||||
content_id_of(&campaign_to_json(&b.campaign)),
|
||||
@@ -918,24 +865,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn translate_walkforward_diverging_regimes_do_not_collide_content_ids() {
|
||||
let base = 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();
|
||||
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 ax = wf_axes();
|
||||
let base = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
|
||||
let different_k = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
|
||||
let different_length = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W).unwrap();
|
||||
let base_id = content_id_of(&campaign_to_json(&base.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));
|
||||
@@ -955,10 +888,11 @@ mod tests {
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let reg = aura_registry::Registry::open(dir.join("runs.jsonl"));
|
||||
let ax = wf_axes();
|
||||
let generated = 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}",
|
||||
&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"),
|
||||
"sqn_normalized",
|
||||
WF_W,
|
||||
)
|
||||
.unwrap();
|
||||
let (process_id, campaign_id) = register_generated_wf(®, &generated).unwrap();
|
||||
@@ -975,20 +909,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn translate_mc_is_deterministic_and_carries_the_regime_and_seed() {
|
||||
let a = 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();
|
||||
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();
|
||||
let ax = wf_axes();
|
||||
let i = inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
|
||||
let a = translate_mc(&i, "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap();
|
||||
let b = translate_mc(&i, "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap();
|
||||
assert_eq!(
|
||||
content_id_of(&campaign_to_json(&a.campaign)),
|
||||
content_id_of(&campaign_to_json(&b.campaign)),
|
||||
@@ -1036,27 +960,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids() {
|
||||
let base = 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();
|
||||
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 ax = wf_axes();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
let base_id = content_id_of(&campaign_to_json(&base.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));
|
||||
@@ -1073,11 +980,12 @@ mod tests {
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let reg = aura_registry::Registry::open(dir.join("runs.jsonl"));
|
||||
let ax = wf_axes();
|
||||
let generated = 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}",
|
||||
&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();
|
||||
let (process_id, campaign_id) = register_generated_mc(®, &generated).unwrap();
|
||||
|
||||
@@ -1048,12 +1048,9 @@ fn mc_rejects_real_flag_with_usage_exit_2() {
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura mc --real");
|
||||
assert_eq!(out.status.code(), Some(2), "mc --real must exit 2; status: {:?}", out.status);
|
||||
assert!(out.stdout.is_empty(), "mc --real must not emit a run on stdout: {:?}", out.stdout);
|
||||
assert_eq!(out.status.code(), Some(2), "mc --real without a blueprint is a usage error");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("Usage"), "mc --real stderr must carry usage: {stderr:?}");
|
||||
// the exclusion is total: no `runs/` registry write either.
|
||||
assert!(!dir.join("runs").exists(), "mc --real must not start a real run");
|
||||
assert!(stderr.contains("Usage: aura mc"), "usage refusal: {stderr}");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -1099,11 +1096,11 @@ fn family_window_flag_without_real_refuses_with_usage_exit_2() {
|
||||
/// regression that drops the prefix again would NOT be caught by the older
|
||||
/// `family_window_flag_without_real_refuses_with_usage_exit_2` pin above, which
|
||||
/// only checks for the verb token and `--real` — not the `Usage: aura` lead-in.
|
||||
/// An unknown `--strategy` name reaches this same closure on both commands, so
|
||||
/// it is the smallest trigger for each.
|
||||
/// An unknown `--strategy` name reaches sweep's closure; the bare verb reaches
|
||||
/// walkforward's.
|
||||
#[test]
|
||||
fn builtin_branch_usage_lines_lead_with_the_house_style_prefix() {
|
||||
for (args, verb) in [(&["sweep", "--strategy", "bogus"][..], "sweep"), (&["walkforward", "--strategy", "bogus"][..], "walkforward")] {
|
||||
for (args, verb) in [(&["sweep", "--strategy", "bogus"][..], "sweep"), (&["walkforward"][..], "walkforward")] {
|
||||
let dir = temp_cwd("builtin_usage_prefix");
|
||||
let out = Command::new(BIN)
|
||||
.args(args)
|
||||
@@ -1123,8 +1120,9 @@ fn builtin_branch_usage_lines_lead_with_the_house_style_prefix() {
|
||||
/// not just the first. This is the one lockstep-pinned site
|
||||
/// (`mc_rejects_real_flag_with_usage_exit_2` above), but that pin only checks
|
||||
/// for a single `"Usage"` occurrence — a regression that fixed only the first
|
||||
/// alternative (leaving `… | mc --strategy r-sma …` unprefixed) would still
|
||||
/// pass it. `--seeds` with no blueprint file is the built-in branch's own
|
||||
/// alternative (leaving the second `| aura mc <blueprint.json> --real …`
|
||||
/// alternative unprefixed) would still pass it. `--seeds` with no blueprint
|
||||
/// file is the built-in branch's own
|
||||
/// grammar violation, reaching this exact literal. (#159 cut 4: the first
|
||||
/// alternative's literal is now the blueprint-mode grammar, not a bare
|
||||
/// `[--name` — `mc` is blueprint-first.)
|
||||
@@ -1143,7 +1141,7 @@ fn mc_builtin_usage_names_the_program_in_both_alternatives() {
|
||||
"first alternative must name the program: {stderr:?}"
|
||||
);
|
||||
assert!(
|
||||
stderr.contains("| aura mc --strategy r-sma"),
|
||||
stderr.contains("| aura mc <blueprint.json> --real"),
|
||||
"second alternative must ALSO name the program: {stderr:?}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
@@ -1220,12 +1218,19 @@ fn sweep_and_walkforward_reject_retired_r_breakout_token_as_unrecognized() {
|
||||
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
|
||||
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(
|
||||
stderr.starts_with(&want),
|
||||
"`aura {args:?}` must fall into the generic usage error, not a special-cased \
|
||||
message: {stderr:?}"
|
||||
);
|
||||
if verb == "sweep" {
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(
|
||||
stderr.starts_with(&want),
|
||||
"`aura {args:?}` must fall into the generic usage error, not a special-cased \
|
||||
message: {stderr:?}"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
stderr.contains("unexpected argument '--strategy'"),
|
||||
"`aura {args:?}` must be a clap unknown-argument rejection: {stderr:?}"
|
||||
);
|
||||
}
|
||||
assert!(out.stdout.is_empty(), "no family summary on the rejected path: {:?}", out.stdout);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
@@ -1244,8 +1249,15 @@ fn sweep_and_walkforward_reject_retired_r_meanrev_token_as_unrecognized() {
|
||||
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
|
||||
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(stderr.starts_with(&want), "generic usage, not special-cased: {stderr:?}");
|
||||
if verb == "sweep" {
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(stderr.starts_with(&want), "generic usage, not special-cased: {stderr:?}");
|
||||
} else {
|
||||
assert!(
|
||||
stderr.contains("unexpected argument '--strategy'"),
|
||||
"`aura {args:?}` must be a clap unknown-argument rejection: {stderr:?}"
|
||||
);
|
||||
}
|
||||
assert!(out.stdout.is_empty(), "no summary on the rejected path: {:?}", out.stdout);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
@@ -1272,12 +1284,19 @@ fn sweep_and_walkforward_reject_retired_pip_tokens_as_unrecognized() {
|
||||
out.status
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(
|
||||
stderr.starts_with(&want),
|
||||
"`aura {args:?}` must fall into the generic usage error, not a \
|
||||
special-cased message: {stderr:?}"
|
||||
);
|
||||
if verb == "sweep" {
|
||||
let want = format!("aura: Usage: aura {verb} ");
|
||||
assert!(
|
||||
stderr.starts_with(&want),
|
||||
"`aura {args:?}` must fall into the generic usage error, not a \
|
||||
special-cased message: {stderr:?}"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
stderr.contains("unexpected argument '--strategy'"),
|
||||
"`aura {args:?}` must be a clap unknown-argument rejection: {stderr:?}"
|
||||
);
|
||||
}
|
||||
assert!(out.stdout.is_empty(), "no summary on the rejected path: {:?}", out.stdout);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
@@ -1495,10 +1514,12 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("walkforward-exact-grade");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -1535,15 +1556,72 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() {
|
||||
assert_eq!(ps[1]["mean"].as_f64(), Some(17.333333333333332), "slow-MA refit mean: {grade_line}");
|
||||
}
|
||||
|
||||
/// Property (#220 walkforward vertical): `aura walkforward --real` IS-refits ANY
|
||||
/// sweepable blueprint through the campaign path, not just the historical r-sma
|
||||
/// candidate — the whole point of dropping `--strategy r-sma`/`--fast`/`--slow`
|
||||
/// for a positional blueprint + generic `--axis`. Every other walkforward E2E
|
||||
/// test here still exercises `r_sma_open.json`; a regression that silently
|
||||
/// special-cased the refit's axis lookup back to a hardcoded `sma_signal.*`
|
||||
/// namespace (e.g. in `walkforward_summary_json_from_reports`'s per-axis
|
||||
/// `raw_matches_wrapped` search) would ship green there while breaking every
|
||||
/// other blueprint. This runs the unrelated r-breakout candidate
|
||||
/// (`channel_hi`/`channel_lo` axes, no `sma_signal` node in sight) end-to-end
|
||||
/// and asserts a well-formed multi-window summary whose `param_stability` has
|
||||
/// exactly one entry per bound axis (2 blueprint axes + the 2 stop columns).
|
||||
/// The window count (9) is roller/data-derived, not strategy-derived, so it is
|
||||
/// pinned exactly against the same GER40 2025 span the r-sma anchor uses.
|
||||
/// Gated on the shared GER40 archive; skips cleanly on a data refusal.
|
||||
#[test]
|
||||
fn walkforward_dissolves_a_non_r_sma_blueprint() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("walkforward-non-r-sma");
|
||||
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "r_breakout_signal.channel_hi.length=10,20",
|
||||
"--axis", "r_breakout_signal.channel_lo.length=10,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
if out.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40 data");
|
||||
return;
|
||||
}
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
let grade_line = stdout
|
||||
.lines()
|
||||
.find(|l| l.starts_with("{\"walkforward\":"))
|
||||
.unwrap_or_else(|| panic!("the walkforward summary line: {stdout}"));
|
||||
let v: serde_json::Value = serde_json::from_str(grade_line).expect("summary line parses as JSON");
|
||||
let wf = &v["walkforward"];
|
||||
assert_eq!(wf["windows"].as_u64(), Some(9), "window count (data/roller-derived): {grade_line}");
|
||||
assert!(wf["oos_r"]["n_trades"].as_u64().is_some(), "pooled OOS trade count present: {grade_line}");
|
||||
let ps = wf["param_stability"].as_array().expect("param_stability is an array");
|
||||
assert_eq!(ps.len(), 4, "one entry per bound axis (channel_hi, channel_lo, stop_length, stop_k): {grade_line}");
|
||||
}
|
||||
|
||||
/// Fork A: the dissolved walkforward binds the stop as a single risk regime, so a
|
||||
/// multi-value --stop-length/--stop-k is refused (exit 2) before any run.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_a_multi_value_stop() {
|
||||
let cwd = temp_cwd("walkforward-multi-stop");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14,20", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
@@ -1558,10 +1636,12 @@ fn walkforward_dissolved_refuses_a_multi_value_stop() {
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_select_plateau() {
|
||||
let cwd = temp_cwd("walkforward-plateau");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--select", "plateau:worst",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -1577,14 +1657,18 @@ fn walkforward_dissolved_refuses_select_plateau() {
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_missing_knobs() {
|
||||
let cwd = temp_cwd("walkforward-missing-knobs");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["walkforward", "--strategy", "r-sma", "--real", "GER40", "--fast", "3", "--slow", "12"])
|
||||
.args([
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "missing --stop-length/--stop-k is a usage refusal");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("requires --fast --slow --stop-length --stop-k"), "missing-knob refusal: {stderr}");
|
||||
assert!(stderr.contains("requires --stop-length --stop-k"), "missing-knob refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// An empty `--real ""` still satisfies clap's `Option::is_some()` dispatch guard, so
|
||||
@@ -1593,10 +1677,12 @@ fn walkforward_dissolved_refuses_missing_knobs() {
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_an_empty_real_symbol() {
|
||||
let cwd = temp_cwd("walkforward-empty-real");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
@@ -1606,16 +1692,51 @@ fn walkforward_dissolved_refuses_an_empty_real_symbol() {
|
||||
assert!(stderr.contains("dissolves only over --real"), "empty-real refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// Property: an `--axis` name that is not among the loaded blueprint's sweepable
|
||||
/// axes is refused before the raw-namespace strip or any archive access (exit 2),
|
||||
/// echoing exactly the name the user typed — the same WRAPPED-probe preflight the
|
||||
/// sibling verbs enforce
|
||||
/// (`aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access`,
|
||||
/// `generalize_refuses_an_unknown_axis_name`). Data-free: no archive/store access.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_an_unknown_axis_name() {
|
||||
let cwd = temp_cwd("walkforward-unknown-axis");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.nope=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "an unknown axis name must exit 2");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("axis \"sma_signal.nope\"") && stderr.contains("sweepable axes"),
|
||||
"must name the unresolved axis exactly as typed: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
out.stdout.is_empty(),
|
||||
"the refusal must not leak an aggregate to stdout, got: {:?}",
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
);
|
||||
assert!(!cwd.join("runs").exists(), "a refused axis name must not start a real run");
|
||||
}
|
||||
|
||||
/// Front-end parity: an unknown `--select` token is refused (exit 2) on the
|
||||
/// dissolved path exactly as it is on the inline path — it must not be silently
|
||||
/// swallowed by `select_is_plateau`'s plateau-only check and fall through to argmax.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_an_unknown_select_token() {
|
||||
let cwd = temp_cwd("walkforward-bad-select");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--select", "bogus",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -1633,10 +1754,12 @@ fn walkforward_dissolved_refuses_an_unknown_select_token() {
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_name_and_trace_together() {
|
||||
let cwd = temp_cwd("walkforward-name-and-trace");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--name", "a", "--trace", "b",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -1662,10 +1785,12 @@ fn walkforward_dissolves_through_the_campaign_path() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("walkforward-dissolves");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
@@ -3035,7 +3160,7 @@ fn dual_grammar_stray_positional_is_a_usage_error_not_swallowed() {
|
||||
for argv in [
|
||||
&["run", "bogus"][..],
|
||||
&["sweep", "bogus", "--name", "x"][..],
|
||||
&["walkforward", "bogus", "--strategy", "sma"][..],
|
||||
&["walkforward", "bogus"][..],
|
||||
&["mc", "bogus"][..],
|
||||
] {
|
||||
let out = Command::new(BIN).args(argv).output().expect("spawn aura <sub> <stray-positional>");
|
||||
@@ -3128,10 +3253,12 @@ fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("mc-r-bootstrap-exact-grade");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--block-len", "5", "--resamples", "1000", "--seed", "42",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
@@ -3164,6 +3291,65 @@ fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
|
||||
assert_eq!(mc["prob_le_zero"].as_f64(), Some(0.558), "P(E[R] <= 0): {grade_line}");
|
||||
}
|
||||
|
||||
/// Property (#220 mc vertical): `aura mc --real` R-bootstraps ANY sweepable
|
||||
/// blueprint through the campaign path, not just the historical r-sma candidate
|
||||
/// — the whole point of dropping `--strategy r-sma`/`--fast`/`--slow` for a
|
||||
/// positional blueprint + generic `--axis`. Every other mc E2E test here still
|
||||
/// exercises `r_sma_open.json`; a regression that silently special-cased the
|
||||
/// pipeline's axis lookup back to a hardcoded `sma_signal.*` namespace would
|
||||
/// ship green there while breaking every other blueprint. This runs the
|
||||
/// unrelated r-breakout candidate (`channel_hi`/`channel_lo` axes, no
|
||||
/// `sma_signal` node in sight) end-to-end and asserts a well-formed
|
||||
/// `mc_r_bootstrap` grade: the argv-echoed knobs (`block_len`/`n_resamples`)
|
||||
/// pin exactly, `n_trades`/`e_r.mean`/`prob_le_zero` are asserted present and
|
||||
/// well-typed (their exact values are strategy-specific, already the r-sma
|
||||
/// pin's job above). Gated on the shared GER40 archive; skips cleanly on a
|
||||
/// data refusal.
|
||||
#[test]
|
||||
fn mc_dissolves_a_non_r_sma_blueprint() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("mc-non-r-sma");
|
||||
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "r_breakout_signal.channel_hi.length=10,20",
|
||||
"--axis", "r_breakout_signal.channel_lo.length=10,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--block-len", "5", "--resamples", "1000", "--seed", "42",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
if out.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40 data");
|
||||
return;
|
||||
}
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
let grade_line = stdout
|
||||
.lines()
|
||||
.find(|l| l.starts_with("{\"mc_r_bootstrap\":"))
|
||||
.unwrap_or_else(|| panic!("the mc_r_bootstrap grade line: {stdout}"));
|
||||
let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON");
|
||||
let mc = &v["mc_r_bootstrap"];
|
||||
assert_eq!(mc["block_len"].as_u64(), Some(5), "block_len (argv echo): {grade_line}");
|
||||
assert_eq!(mc["n_resamples"].as_u64(), Some(1000), "n_resamples (argv echo): {grade_line}");
|
||||
assert!(mc["n_trades"].as_u64().is_some_and(|n| n > 0), "pooled OOS trade count present: {grade_line}");
|
||||
assert!(mc["e_r"]["mean"].as_f64().is_some_and(|m| m.is_finite()), "E[R] mean present + finite: {grade_line}");
|
||||
assert!(
|
||||
mc["prob_le_zero"].as_f64().is_some_and(|p| (0.0..=1.0).contains(&p)),
|
||||
"P(E[R] <= 0) present + in [0,1]: {grade_line}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property: `aura mc --strategy r-sma --real` is now thin sugar over the one campaign
|
||||
/// path — a successful run durably auto-registers exactly one generated process
|
||||
/// document, one generated campaign document (carrying the constant "mc" name and the
|
||||
@@ -3178,10 +3364,12 @@ fn mc_dissolves_through_the_campaign_path() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("mc-dissolves");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--block-len", "5", "--resamples", "1000", "--seed", "42",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
@@ -3267,10 +3455,12 @@ fn mc_dissolves_through_the_campaign_path() {
|
||||
#[test]
|
||||
fn mc_dissolved_refuses_a_multi_value_stop() {
|
||||
let cwd = temp_cwd("mc-multi-stop");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0",
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14,20", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
@@ -3280,6 +3470,40 @@ fn mc_dissolved_refuses_a_multi_value_stop() {
|
||||
assert!(stderr.contains("single risk regime"), "stop-regime refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// Property: an `--axis` name that is not among the loaded blueprint's sweepable
|
||||
/// axes is refused before the raw-namespace strip or any archive access (exit 2),
|
||||
/// echoing exactly the name the user typed — mc shares the identical WRAPPED-probe
|
||||
/// preflight `walkforward_dissolved_refuses_an_unknown_axis_name` pins for
|
||||
/// walkforward (both dispatchers now call the same `validate_and_register_axes`
|
||||
/// helper); this test pins mc's own copy so a future edit cannot silently break it
|
||||
/// while walkforward's stays green. Data-free: no archive/store access.
|
||||
#[test]
|
||||
fn mc_dissolved_refuses_an_unknown_axis_name() {
|
||||
let cwd = temp_cwd("mc-unknown-axis");
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.nope=3", "--axis", "sma_signal.slow.length=12",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "an unknown axis name must exit 2");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("axis \"sma_signal.nope\"") && stderr.contains("sweepable axes"),
|
||||
"must name the unresolved axis exactly as typed: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
out.stdout.is_empty(),
|
||||
"the refusal must not leak an aggregate to stdout, got: {:?}",
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
);
|
||||
assert!(!cwd.join("runs").exists(), "a refused axis name must not start a real run");
|
||||
}
|
||||
|
||||
/// Property: the one load-bearing decision new to the mc dissolution — `translate_mc`
|
||||
/// maps `campaign.seed` to the mc `--seed` (unlike `translate_walkforward`, which
|
||||
/// hardcodes `seed: 0`) — actually flows through the executor to the emitted grade,
|
||||
@@ -3297,12 +3521,14 @@ fn mc_dissolved_refuses_a_multi_value_stop() {
|
||||
fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let run = |seed: &str| {
|
||||
let cwd = temp_cwd(&format!("mc-seed-{seed}"));
|
||||
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"mc", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--block-len", "5", "--resamples", "1000", "--seed", seed,
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user