feat(cli): dissolve aura walkforward --strategy r-sma --real (#210 T3-T4)
The dispatch split lands: `dispatch_walkforward`'s built-in arm now routes
`--strategy r-sma --real` through the one campaign path (`run_walkforward_sugar`
-> generated campaign doc -> executor -> summary reprint), reproducing the
`{"walkforward":{…}}` line byte-for-byte. Everything else — synthetic (any
strategy) and non-r-sma real — stays on the inline `run_walkforward`, fenced
until #159 (Reading A). The committed anchor
`walkforward_real_e2e_pins_the_exact_current_grade` passes: the dissolved path
reproduces windows=9, stitched_total_pips, oos_r, and the param_stability means
exactly. `--select plateau:*` is refused (exit 2 -> #215, Fork B); a multi-value
stop is refused (Fork A).
Two plan guesses were wrong and the anchor + real-data e2e caught both; the fixes
are documented inline:
- Window resolution: the plan mirrored generalize's `(--from,--to) -> (f,t)`
shortcut, but the inline `wf_full_span` ALWAYS clips `--from`/`--to` to the
archive's actual first/last bar (`probe_window`), even when both flags are
explicit — a holiday/weekend edge shifts every IS/OOS window's calendar
placement (stitched_total_pips diverged by ~5.16M, param_stability means intact,
confirming only window boundaries moved). The dissolved path now calls
`source.full_window(env)` unconditionally, matching the inline behaviour;
verified byte-for-byte against the still-fenced inline path.
- Reducer axis match: the campaign path records the blueprint axes wrapped
(`sma_signal.fast.length`) while the stop rides unwrapped via the risk regime, so
the reducer's exact-name match panicked on real data. Fixed with the existing
`campaign_run::raw_matches_wrapped` (#203) wrap-convention helper.
Tests: the dissolution-proof e2e `walkforward_dissolves_through_the_campaign_path`
(one process + one campaign + one campaign-run auto-registered, windows=9) plus
front-end refusal coverage (multi-value stop, `--select plateau`, missing knobs,
empty `--real`, unknown `--select`, `--name`/`--trace` exclusivity). Full workspace
suite green; `clippy --all-targets -D warnings` clean.
refs #210
This commit is contained in:
+126
-7
@@ -67,6 +67,12 @@ const WF_REAL_IS_NS: i64 = 90 * WF_DAY_NS;
|
||||
const WF_REAL_OOS_NS: i64 = 30 * WF_DAY_NS;
|
||||
const WF_REAL_STEP_NS: i64 = 30 * WF_DAY_NS;
|
||||
|
||||
/// The winner-selection objective for walk-forward's per-window IS refit — the
|
||||
/// deflation-aware SQN variant (#144 default). Named once so the three call sites
|
||||
/// (the inline r-sma roller, the inline blueprint roller, and the dissolved
|
||||
/// campaign sugar's bridge) cannot drift apart on the token.
|
||||
const WINNER_SELECTION_METRIC: &str = "sqn_normalized";
|
||||
|
||||
/// Fixed RNG seed for the trials-deflation reality-check bootstrap in walk-forward
|
||||
/// winner selection. Recorded on each winner's manifest (so `overfit_probability`
|
||||
/// is reproducible by re-run); a CLI flag for it is a deferred refinement. The
|
||||
@@ -1758,7 +1764,7 @@ fn walkforward_family(
|
||||
let space = r_sma_space();
|
||||
walk_forward(roller, space, |w: WindowBounds| {
|
||||
let (is_family, lattice) = r_sma_sweep_over(w.is.0, w.is.1, data, grid, env);
|
||||
let (best, selection) = match select_winner(&is_family, "sqn_normalized", select, lattice.as_deref()) {
|
||||
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, lattice.as_deref()) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
|
||||
};
|
||||
@@ -1882,16 +1888,18 @@ fn walkforward_summary_json(result: &WalkForwardResult) -> String {
|
||||
/// `stitched_total_pips` = the per-window `total_pips` summed left-to-right in roll
|
||||
/// order (the engine `stitch` folds each segment's final cumulative value, and a
|
||||
/// window's OOS segment ends at its `total_pips`); `param_stability` reduces each
|
||||
/// r-sma IS-refit axis over the per-window chosen params (read by exact name from
|
||||
/// each report's prefix-stripped `manifest.params`) through the same
|
||||
/// `MetricStats::from_values`; the `oos_r` block pools the per-window `trade_rs`
|
||||
/// r-sma IS-refit axis over the per-window chosen params (read from each report's
|
||||
/// `manifest.params` via [`campaign_run::raw_matches_wrapped`] — the blueprint axes
|
||||
/// `fast.length`/`slow.length` are recorded wrapped as `sma_signal.fast.length` etc.
|
||||
/// by the strategy's own param space, while `stop_length`/`stop_k` ride the risk
|
||||
/// regime unwrapped, so an exact-name match would miss the wrapped pair) through the
|
||||
/// same `MetricStats::from_values`; the `oos_r` block pools the per-window `trade_rs`
|
||||
/// through `r_metrics_from_rs`. Canonical JSON (C14).
|
||||
///
|
||||
/// The axis list + order mirror what `r_sma_space` yields (fast, slow, stop_length,
|
||||
/// stop_k) — the same order + values the inline `param_stability` reduces. A wrong
|
||||
/// order or axis would fail the committed exact-grade anchor loudly (it pins
|
||||
/// `param_stability[0].mean` = the fast-MA refit mean), never silently.
|
||||
#[allow(dead_code)]
|
||||
fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String {
|
||||
let total: f64 = reports.iter().map(|r| r.metrics.total_pips).sum();
|
||||
// The four r-sma axis names, in `r_sma_space` order. Coercion to `f64` is
|
||||
@@ -1910,7 +1918,7 @@ fn walkforward_summary_json_from_reports(reports: &[RunReport]) -> String {
|
||||
.manifest
|
||||
.params
|
||||
.iter()
|
||||
.find(|(name, _)| name == axis)
|
||||
.find(|(name, _)| campaign_run::raw_matches_wrapped(axis, name))
|
||||
.expect("each r-sma walk-forward window records its chosen axis");
|
||||
match v {
|
||||
Scalar::I64(i) => *i as f64,
|
||||
@@ -3141,7 +3149,7 @@ fn blueprint_walkforward_family(
|
||||
walk_forward(roller, space.clone(), |w: WindowBounds| {
|
||||
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env)
|
||||
.expect("axes validated in the dispatch-boundary pre-flight");
|
||||
let (best, selection) = match select_winner(&is_family, "sqn_normalized", select, Some(&lattice)) {
|
||||
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
|
||||
};
|
||||
@@ -4201,6 +4209,61 @@ fn run_args_from(a: &RunCmd) -> Result<RunArgs, String> {
|
||||
Ok(RunArgs { harness, data, trace: a.trace.clone(), cost, slip_vol_mult, carry_per_cycle })
|
||||
}
|
||||
|
||||
/// The real walk-forward roller sizes in ms (the campaign wf stage works in ms:
|
||||
/// span `cell.window_ms`, sizes the `StageBlock` `_ms` fields). `WF_REAL_*_NS` are
|
||||
/// nanoseconds; divide by 1e6. The ms sizes over the doc window reproduce the same
|
||||
/// calendar windows the inline ns roller produces (anchor-gated).
|
||||
fn wf_ms_sizes() -> (u64, u64, u64) {
|
||||
(
|
||||
(WF_REAL_IS_NS / 1_000_000) as u64,
|
||||
(WF_REAL_OOS_NS / 1_000_000) as u64,
|
||||
(WF_REAL_STEP_NS / 1_000_000) as u64,
|
||||
)
|
||||
}
|
||||
|
||||
/// True iff `--select` names a plateau mode (refused on the dissolved path, Fork B,
|
||||
/// until #215 ships the wf-stage plateau relaxation). Reuses `parse_select` so the
|
||||
/// vocabulary stays single-sourced.
|
||||
fn select_is_plateau(select: Option<&str>) -> bool {
|
||||
matches!(select.map(parse_select), Some(Ok(Selection::Plateau(_))))
|
||||
}
|
||||
|
||||
/// Convert `WalkforwardCmd` into the resolved argument shape the walkforward sugar
|
||||
/// consumes (the dissolved `--strategy r-sma --real` branch). Single instrument,
|
||||
/// multi-value fast/slow (the IS-refit grid), single-value stop (Fork A: the stop is
|
||||
/// a risk regime, not a swept axis); all four knobs required. `parse_csv_list`
|
||||
/// (`:1586`) is generic over `T: FromStr`, so the same helper parses the i64 grids
|
||||
/// and the f64 stop; the local `knobs` closure (captureless → `Copy`, mirroring
|
||||
/// `generalize_args_from`'s idiom) is reused across the four `ok_or_else` calls.
|
||||
/// `--name`/`--trace` are mutually exclusive here too, matching the flag's own
|
||||
/// documented contract and the inline path's `name_persist` refusal.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn walkforward_args_from(
|
||||
a: &WalkforwardCmd,
|
||||
) -> Result<(String, String, Vec<i64>, Vec<i64>, i64, f64, Option<i64>, Option<i64>), String> {
|
||||
let symbol = match a.real.as_deref() {
|
||||
None | Some("") => return Err("walkforward --strategy r-sma dissolves only over --real <SYMBOL>".to_string()),
|
||||
Some(s) => s.to_string(),
|
||||
};
|
||||
let knobs = || "walkforward requires --fast --slow --stop-length --stop-k (fast/slow may be CSV grids; the stop is single-value)".to_string();
|
||||
let fast: Vec<i64> = parse_csv_list(a.fast.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
|
||||
let slow: Vec<i64> = parse_csv_list(a.slow.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
|
||||
let stop_length: Vec<i64> = parse_csv_list(a.stop_length.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
|
||||
let stop_k: Vec<f64> = parse_csv_list(a.stop_k.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
|
||||
if stop_length.len() != 1 || stop_k.len() != 1 {
|
||||
return Err("walkforward: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string());
|
||||
}
|
||||
let name = match (a.name.as_deref(), a.trace.as_deref()) {
|
||||
(Some(_), Some(_)) => {
|
||||
return Err("walkforward: --name and --trace are mutually exclusive".to_string());
|
||||
}
|
||||
(Some(n), None) => n.to_string(),
|
||||
(None, Some(t)) => t.to_string(),
|
||||
(None, None) => "walkforward".to_string(),
|
||||
};
|
||||
Ok((name, symbol, fast, slow, stop_length[0], stop_k[0], a.from, a.to))
|
||||
}
|
||||
|
||||
/// Convert `GeneralizeCmd` into the resolved argument shape the generalize sugar
|
||||
/// consumes. The candidate is a single cell (clap already types `--fast`/etc. as
|
||||
/// one `i64`/`f64`), all four knobs required, `--real` a `>=2`-distinct comma
|
||||
@@ -4749,6 +4812,62 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) {
|
||||
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()
|
||||
&& parse_select(s).is_err()
|
||||
{
|
||||
eprintln!("aura: {}", usage());
|
||||
std::process::exit(2);
|
||||
}
|
||||
if select_is_plateau(a.select.as_deref()) {
|
||||
eprintln!("aura: --select plateau is not yet available on the campaign path; see #215");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let (name, symbol, fast, slow, stop_length, stop_k, from, to) =
|
||||
walkforward_args_from(&a).unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
let signal = sma_signal(None, None);
|
||||
let canonical = blueprint_to_json(&signal).expect("a bare sma_signal serializes");
|
||||
let reg = env.registry();
|
||||
let topo = topology_hash(&signal);
|
||||
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
// Unlike `dispatch_generalize` (a single run per instrument, insensitive
|
||||
// to a day's edge shift), the inline `walkforward_family` sources its span
|
||||
// from `DataSource::wf_full_span`, which — for Real — ALWAYS clips
|
||||
// `--from`/`--to` to the archive's actual first/last bar in range (never the
|
||||
// literal ms request): a holiday/weekend edge shifts every IS/OOS window's
|
||||
// calendar placement, so the roller must clip identically here too, even
|
||||
// when both flags are given, or the per-window winners and OOS pips diverge
|
||||
// from the committed exact-grade anchor.
|
||||
let source = DataSource::from_choice(
|
||||
DataChoice::Real { symbol: symbol.clone(), from_ms: from, to_ms: to },
|
||||
env,
|
||||
);
|
||||
let (from_ts, to_ts) = source.full_window(env);
|
||||
let (from_ms, to_ms) = (
|
||||
aura_ingest::epoch_ns_to_unix_ms(from_ts),
|
||||
aura_ingest::epoch_ns_to_unix_ms(to_ts),
|
||||
);
|
||||
let (is_ms, oos_ms, step_ms) = wf_ms_sizes();
|
||||
verb_sugar::run_walkforward_sugar(
|
||||
&fast, &slow, stop_length, stop_k, WINNER_SELECTION_METRIC,
|
||||
is_ms, oos_ms, step_ms, &name, &symbol, from_ms, to_ms, &canonical, env,
|
||||
)
|
||||
.unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
let strategy = strategy_from(a.strategy.as_deref(), &usage).unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
|
||||
@@ -352,7 +352,6 @@ pub(crate) fn run_generalize_sugar(
|
||||
}
|
||||
|
||||
/// The two generated documents of one dissolved `walkforward` invocation.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct GeneratedWalkforward {
|
||||
pub process: ProcessDoc,
|
||||
@@ -368,7 +367,6 @@ pub(crate) struct GeneratedWalkforward {
|
||||
/// (ms). `blueprint_canonical` is the bare `sma_signal` blueprint already stored by
|
||||
/// topology hash; its content id is the strategy ref.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn translate_walkforward(
|
||||
fast: &[i64],
|
||||
slow: &[i64],
|
||||
@@ -442,7 +440,6 @@ pub(crate) fn translate_walkforward(
|
||||
|
||||
/// Auto-register both generated walkforward documents (content-addressed,
|
||||
/// idempotent). Returns `(process_id, campaign_id)`.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn register_generated_wf(
|
||||
reg: &aura_registry::Registry,
|
||||
generated: &GeneratedWalkforward,
|
||||
@@ -454,6 +451,57 @@ pub(crate) fn register_generated_wf(
|
||||
Ok((process_id, campaign_id))
|
||||
}
|
||||
|
||||
/// Run one dissolved `walkforward` invocation end-to-end: register the generated
|
||||
/// documents, run through the one campaign path, then reprint the per-window member
|
||||
/// lines + the summary line from the recorded `WalkForward` family. The stdout
|
||||
/// summary line is byte-identical to the inline path (the committed exact-grade
|
||||
/// anchor is the gate).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn run_walkforward_sugar(
|
||||
fast: &[i64],
|
||||
slow: &[i64],
|
||||
stop_length: i64,
|
||||
stop_k: f64,
|
||||
metric: &str,
|
||||
in_sample_ms: u64,
|
||||
out_of_sample_ms: u64,
|
||||
step_ms: u64,
|
||||
name: &str,
|
||||
symbol: &str,
|
||||
from_ms: i64,
|
||||
to_ms: i64,
|
||||
blueprint_canonical: &str,
|
||||
env: &crate::project::Env,
|
||||
) -> Result<(), String> {
|
||||
let generated = translate_walkforward(
|
||||
fast, slow, stop_length, stop_k, metric, in_sample_ms, out_of_sample_ms, step_ms,
|
||||
name, symbol, from_ms, to_ms, blueprint_canonical,
|
||||
)?;
|
||||
let probe_params: Vec<(String, Scalar)> = vec![
|
||||
("fast.length".to_string(), Scalar::i64(fast[0])),
|
||||
("slow.length".to_string(), Scalar::i64(slow[0])),
|
||||
];
|
||||
validate_before_register(&generated.process, &generated.campaign, blueprint_canonical, &probe_params, env)?;
|
||||
let reg = env.registry();
|
||||
let (_process_id, campaign_id) = register_generated_wf(®, &generated)?;
|
||||
let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?;
|
||||
|
||||
// The single cell's walk_forward StageFamily.
|
||||
let wf = run
|
||||
.outcome
|
||||
.cells
|
||||
.iter()
|
||||
.flat_map(|c| c.families.iter())
|
||||
.find(|f| f.block == "std::walk_forward")
|
||||
.ok_or("walkforward produced no walk_forward family")?;
|
||||
|
||||
for report in &wf.reports {
|
||||
println!("{}", crate::family_member_line(&wf.family_id, report));
|
||||
}
|
||||
println!("{}", crate::walkforward_summary_json_from_reports(&wf.reports));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -3695,6 +3695,202 @@ 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}");
|
||||
}
|
||||
|
||||
/// Fork A: the dissolved walkforward binds the stop as a single risk regime, so a
|
||||
/// multi-value --stop-length/--stop-k is refused (exit 2) before any run.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_a_multi_value_stop() {
|
||||
let cwd = temp_cwd("walkforward-multi-stop");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "multi-value stop is a usage refusal");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("single risk regime"), "stop-regime refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// Fork B: --select plateau is refused on the dissolved path (exit 2) with a
|
||||
/// pointer to #215 until the wf-stage plateau relaxation ships.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_select_plateau() {
|
||||
let cwd = temp_cwd("walkforward-plateau");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"--select", "plateau:worst",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "plateau is refused on the dissolved path");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("#215"), "forward pointer to the plateau follow-up: {stderr}");
|
||||
}
|
||||
|
||||
/// `walkforward_args_from` requires all four grid knobs on the dissolved r-sma-real
|
||||
/// path (unlike the inline path, which defaults an absent flag via `RGrid::default()`)
|
||||
/// — a missing knob is a usage refusal (exit 2), not a silent default.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_missing_knobs() {
|
||||
let cwd = temp_cwd("walkforward-missing-knobs");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["walkforward", "--strategy", "r-sma", "--real", "GER40", "--fast", "3", "--slow", "12"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "missing --stop-length/--stop-k is a usage refusal");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("requires --fast --slow --stop-length --stop-k"), "missing-knob refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// An empty `--real ""` still satisfies clap's `Option::is_some()` dispatch guard, so
|
||||
/// `walkforward_args_from`'s own empty-string check is what refuses it (exit 2) —
|
||||
/// distinct from omitting `--real` entirely, which never enters the dissolved branch.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_an_empty_real_symbol() {
|
||||
let cwd = temp_cwd("walkforward-empty-real");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "empty --real is a usage refusal");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("dissolves only over --real"), "empty-real refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// Front-end parity: an unknown `--select` token is refused (exit 2) on the
|
||||
/// dissolved path exactly as it is on the inline path — it must not be silently
|
||||
/// swallowed by `select_is_plateau`'s plateau-only check and fall through to argmax.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_an_unknown_select_token() {
|
||||
let cwd = temp_cwd("walkforward-bad-select");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"--select", "bogus",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "unknown --select token is a usage refusal");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("Usage: aura walkforward"), "unknown-select refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// `--trace`'s own help text ("mutually exclusive with --name") is a contract the
|
||||
/// inline path already enforces via `name_persist`; the dissolved r-sma-real path
|
||||
/// must refuse the same combination (exit 2) rather than silently letting --name
|
||||
/// win over --trace.
|
||||
#[test]
|
||||
fn walkforward_dissolved_refuses_name_and_trace_together() {
|
||||
let cwd = temp_cwd("walkforward-name-and-trace");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"--name", "a", "--trace", "b",
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "--name and --trace together is a usage refusal");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("mutually exclusive"), "name/trace refusal: {stderr}");
|
||||
}
|
||||
|
||||
/// Property (#210 T3, dispatch split): a successful `aura walkforward --strategy
|
||||
/// r-sma --real` durably auto-registers exactly one generated process document, one
|
||||
/// generated campaign document (carrying the `--name` handle and the stop as a
|
||||
/// non-empty single risk regime), and one campaign-run record — mirroring the
|
||||
/// generalize dissolution's audit trail. The persisted family set is exactly one
|
||||
/// `Sweep` family (the pipeline's leading `std::sweep(argmax)` stage) plus one
|
||||
/// `WalkForward` family (the per-window OOS reports) — the observable proof that
|
||||
/// `dispatch_walkforward`'s r-sma-real branch now runs through the one campaign
|
||||
/// executor rather than the deleted inline roller. Gated on the shared GER40
|
||||
/// archive; skips cleanly on a data refusal.
|
||||
#[test]
|
||||
fn walkforward_dissolves_through_the_campaign_path() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("walkforward-dissolves");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"--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 count = |sub: &str| {
|
||||
std::fs::read_dir(cwd.join("runs").join(sub))
|
||||
.map(|d| d.count())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
assert_eq!(count("processes"), 1, "one generated process document registered");
|
||||
assert_eq!(count("campaigns"), 1, "one generated campaign document registered");
|
||||
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
|
||||
.expect("campaign_runs.jsonl exists after a sugar run");
|
||||
assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded");
|
||||
|
||||
let campaigns_dir = cwd.join("runs").join("campaigns");
|
||||
let campaign_doc_path = std::fs::read_dir(&campaigns_dir)
|
||||
.expect("campaigns dir exists")
|
||||
.next()
|
||||
.expect("exactly one campaign document")
|
||||
.expect("readable dir entry")
|
||||
.path();
|
||||
let campaign_doc_json = std::fs::read_to_string(&campaign_doc_path).expect("read campaign doc");
|
||||
assert!(
|
||||
campaign_doc_json.contains("\"name\":\"walkforward\""),
|
||||
"the generated campaign document carries the --name handle: {campaign_doc_json}"
|
||||
);
|
||||
assert!(
|
||||
campaign_doc_json.contains("\"risk\":[")
|
||||
&& campaign_doc_json.contains("\"length\":14")
|
||||
&& campaign_doc_json.contains("\"k\":2.0"),
|
||||
"the stop rides a non-empty risk regime: {campaign_doc_json}"
|
||||
);
|
||||
|
||||
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["runs", "families"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn families");
|
||||
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
|
||||
assert_eq!(
|
||||
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
|
||||
1,
|
||||
"one Sweep family from the pipeline's leading argmax stage: {fams_out}"
|
||||
);
|
||||
assert_eq!(
|
||||
fams_out.lines().filter(|l| l.contains("\"kind\":\"WalkForward\"")).count(),
|
||||
1,
|
||||
"one WalkForward family holding the per-window OOS reports: {fams_out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property: `aura generalize` is now thin sugar over the one campaign path — a
|
||||
/// successful run durably auto-registers exactly one generated process document,
|
||||
/// one generated campaign document (carrying the `--name` handle and the stop as
|
||||
|
||||
Reference in New Issue
Block a user