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