diff --git a/README.md b/README.md index 2c9c077..8511c2f 100644 --- a/README.md +++ b/README.md @@ -44,16 +44,18 @@ Invoke it as `aura …` (examples below use the plain name). ## Running & orchestrating a loaded blueprint These verbs all take the **blueprint file as their first positional argument** -and drive a family of runs over it. (`walkforward`, `mc`, and `generalize` also -accept a legacy built-in form selected by `--strategy` instead of a `.json` -path, for the engine's own bundled example strategies — see `--help`.) +and drive a family of runs over it. `walkforward`, `mc`, and `generalize` share +`sweep`'s generic grammar — the blueprint positional plus `--real` and a +repeatable `--axis =` (`generalize` takes `--real `, at +least two instruments, with a single value per axis) — see `--help`. | Command | Purpose | |---|---| | `aura run ` | Run one backtest of a **closed** blueprint and print its report. An open (free-knob) blueprint is refused with a clean error — bind it, or use `sweep`. | | `aura sweep --list-axes` | **Discover** the blueprint's open, sweepable knobs. Prints each as `:`. Run this first to learn the axis names. | | `aura sweep --axis = [--axis …]` | Run a **grid family** over the named axes and persist it. | -| `aura mc --seeds ` | Run a **Monte-Carlo family** of `n` seeded realizations. Wants a **closed** blueprint (the inverse of sweep). | +| `aura mc --seeds ` | Run a **synthetic Monte-Carlo family** of `n` seeded realizations. Wants a **closed** blueprint (the inverse of sweep). | +| `aura mc --real --axis = [--axis …]` | Run a **Monte-Carlo R-bootstrap campaign** over recorded data — driven through the same generated-campaign pipeline as `sweep`; `--block-len`/`--resamples`/`--seed` tune the bootstrap. | | `aura walkforward --axis = [--axis …]` | Run an **in-sample-refit walk-forward family** — rolling optimize + out-of-sample test across windows, stitched into one verdict + parameter stability. `--select` chooses the per-window objective. | | `aura runs families` | List every persisted family (id, kind, member count). | | `aura runs family [rank ]` | List one family's members, optionally ranked best-first by an R metric (e.g. `sqn_normalized`, `expectancy_r`). | diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index f8970ef..0ba38ce 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -2509,6 +2509,44 @@ fn validate_and_register_axes( Ok((canonical, raw_axes)) } +/// Exit-map for a `validate_and_register_axes` failure — the stderr line and +/// exit code every campaign dispatcher (sweep/generalize/walkforward/mc) +/// preserves: an unknown axis is a usage error (exit 2, echoed before the +/// archive is touched), a registry write failure a runtime error (exit 1). +fn exit_axis_register_error(e: AxisRegisterError) -> ! { + match e { + AxisRegisterError::UnknownAxis(m) => { + eprintln!("aura: {m}"); + std::process::exit(2) + } + AxisRegisterError::Registry(m) => { + eprintln!("aura: {m}"); + std::process::exit(1) + } + } +} + +/// The shared campaign window in Unix-ms, clipped to the archive. `full_window` +/// probes the ARCHIVE's actual first/last bar in range (never the literal ms +/// request) and returns aura's native epoch-ns `Timestamp` (the ms->ns crossing +/// happens once, at the ingest seam — C3); the campaign document's `Window` +/// field is Unix-ms (same currency as `--from`/`--to` and every existing +/// campaign fixture, e.g. `campaign_doc_json` in research_docs.rs). Convert +/// back through the seam's own `aura_ingest::epoch_ns_to_unix_ms` at this one +/// seam-crossing (never reimplement the division inline) so the executor's +/// `unix_ms_to_epoch_ns` re-normalizes exactly once downstream, not twice. +/// Shared by the campaign dispatchers — sweep/walkforward/mc always clip; +/// generalize reaches here only through its no-explicit-window fallback (an +/// explicit `--from`+`--to` pair passes through unclipped there BY DESIGN). +fn campaign_window_ms(choice: DataChoice, env: &project::Env) -> (i64, i64) { + let source = DataSource::from_choice(choice, env); + let (from_ts, to_ts) = source.full_window(env); + ( + aura_ingest::epoch_ns_to_unix_ms(from_ts), + aura_ingest::epoch_ns_to_unix_ms(to_ts), + ) +} + /// `aura run`: the loaded-blueprint branch (an existing `.json` first-positional) or /// the built-in harness-kind dispatch. fn dispatch_run(a: RunCmd, env: &project::Env) { @@ -2638,33 +2676,17 @@ fn dispatch_generalize(a: GeneralizeCmd, env: &project::Env) { std::process::exit(2); } } - let (canonical, raw_axes) = - validate_and_register_axes(&doc, &axes, env).unwrap_or_else(|e| match e { - AxisRegisterError::UnknownAxis(m) => { - eprintln!("aura: {m}"); - std::process::exit(2); - } - AxisRegisterError::Registry(m) => { - eprintln!("aura: {m}"); - std::process::exit(1); - } - }); + let (canonical, raw_axes) = validate_and_register_axes(&doc, &axes, env) + .unwrap_or_else(|e| exit_axis_register_error(e)); // Window: the explicit --from/--to when both present (byte-identical to the // retired welded path, verified by the exact-grade anchor); otherwise the // first symbol's full archive window as the single shared campaign window. let (from_ms, to_ms) = match (from, to) { (Some(f), Some(t)) => (f, t), - _ => { - let source = DataSource::from_choice( - DataChoice::Real { symbol: symbols[0].clone(), from_ms: from, to_ms: to }, - env, - ); - let (from_ts, to_ts) = source.full_window(env); - ( - aura_ingest::epoch_ns_to_unix_ms(from_ts), - aura_ingest::epoch_ns_to_unix_ms(to_ts), - ) - } + _ => campaign_window_ms( + DataChoice::Real { symbol: symbols[0].clone(), from_ms: from, to_ms: to }, + env, + ), }; let inv = verb_sugar::SugarInvocation { axes: &raw_axes, @@ -2796,34 +2818,11 @@ fn dispatch_sweep(a: SweepCmd, env: &project::Env) { // 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); - } - }); + .unwrap_or_else(|e| exit_axis_register_error(e)); let symbol = symbol.clone(); - let source = DataSource::from_choice(data, env); - let (from_ts, to_ts) = source.full_window(env); - // `full_window` probes the ARCHIVE's actual first/last bar - // and returns aura's native epoch-ns `Timestamp` (the - // ms->ns crossing happens once, at the ingest seam — C3); - // the campaign document's `Window` field is Unix-ms (same - // currency as `--from`/`--to` and every existing campaign - // fixture, e.g. `campaign_doc_json` in research_docs.rs). - // Convert back through the seam's own - // `aura_ingest::epoch_ns_to_unix_ms` at this one - // seam-crossing (never reimplement the division inline) so - // the executor's `unix_ms_to_epoch_ns` re-normalizes - // exactly once downstream, not twice. - let (from_ms, to_ms) = ( - aura_ingest::epoch_ns_to_unix_ms(from_ts), - aura_ingest::epoch_ns_to_unix_ms(to_ts), - ); + // Archive-clipped Unix-ms window; the ns->ms seam-crossing + // rationale lives on `campaign_window_ms`. + let (from_ms, to_ms) = campaign_window_ms(data, env); let inv = verb_sugar::SugarInvocation { axes: &raw_axes, name: name.clone(), @@ -2900,16 +2899,7 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) { // 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); - } - }); + .unwrap_or_else(|e| exit_axis_register_error(e)); // Unlike `dispatch_generalize` (a single run per instrument, insensitive // to a day's edge shift), `blueprint_walkforward_family` sources its span // from `DataSource::wf_full_span`, which — for Real — ALWAYS clips @@ -2918,15 +2908,10 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &project::Env) { // 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( + let (from_ms, to_ms) = campaign_window_ms( 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(); let inv = verb_sugar::SugarInvocation { axes: &raw_axes, @@ -3017,31 +3002,17 @@ fn dispatch_mc(a: McCmd, env: &project::Env) { // 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); - } - }); + .unwrap_or_else(|e| exit_axis_register_error(e)); // `blueprint_walkforward_family` sources its span from // `DataSource::wf_full_span`, which for Real ALWAYS clips `--from`/`--to` // to the archive's actual first/last bar in range: a holiday/weekend edge // 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 pooled OOS series diverge from the committed exact-grade anchor. - let source = DataSource::from_choice( + let (from_ms, to_ms) = campaign_window_ms( 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(); let inv = verb_sugar::SugarInvocation { axes: &raw_axes, diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 45c6365..4073d1a 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -1036,9 +1036,11 @@ fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() { /// The synthetic seed-resweep `mc` is undefined over real bars (one realization -> /// identical members), so a bare `aura mc --real EURUSD` — with no R candidate — is /// REJECTED (usage on stderr, exit 2) at the binary boundary. The real path is -/// reachable ONLY via `--strategy r-sma` (the R-bootstrap over the pooled OOS R -/// series, `mc_strategy_r_sma_prints_a_bootstrap_line_deterministically`); a -/// `--real` without it stays a usage error. NOT gated — the refusal precedes any +/// reachable ONLY via the blueprint positional + `--axis` campaign form (the +/// R-bootstrap over the pooled OOS R series, +/// `mc_r_bootstrap_real_e2e_pins_the_exact_current_grade`; spelled +/// `--strategy r-sma` before the #220 argv migration); a `--real` without a +/// blueprint stays a usage error. NOT gated — the refusal precedes any /// data access, so it is CI-safe on every machine. #[test] fn mc_rejects_real_flag_with_usage_exit_2() { @@ -1770,14 +1772,16 @@ fn walkforward_dissolved_refuses_name_and_trace_together() { 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 +/// Property (#210 T3, dispatch split): a successful real-data walkforward +/// (`aura walkforward --real --axis …`; spelled `--strategy r-sma +/// --real` before the #220 argv migration) 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 +/// `dispatch_walkforward`'s real-data path 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] @@ -3234,19 +3238,22 @@ fn exit_codes_partition_usage_two_from_runtime_one() { } /// Characterization pin (byte-identity anchor for the mc R-bootstrap dissolution, -/// #210, the last verb). The current `aura mc --strategy r-sma --real` runs the -/// inline rolling walk-forward, pools the per-window OOS trade-R series, and -/// r-bootstraps E[R] (`mc_r_bootstrap_report`). The dissolution reroutes this -/// through the campaign `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` -/// process, whose terminal monte_carlo stage does the identical +/// #210, the last verb; argv since migrated to the generic form by #220). +/// `aura mc --real --axis …` pools the per-window OOS trade-R +/// series and r-bootstraps E[R] through the campaign +/// `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` +/// process, whose terminal monte_carlo stage does the /// `StageBootstrap::PooledOos(r_bootstrap(...))` seeded from the campaign seed -- so -/// the campaign seed must carry the mc `--seed`. The wf winners are +/// the campaign seed carries the mc `--seed`. The wf winners are /// deflation-seed-independent (argmax), so the pooled series, and thus the /// bootstrap, is stable across that seed remap (the walkforward anchor already /// proved winner seed-independence). The multi-point grid makes the per-window -/// winner selection non-degenerate. This pins the EXACT current bootstrap grade of -/// a fixed 2025 GER40 invocation; after the dissolution the same command must -/// reproduce these bytes (the acceptance gate). Gated on the GER40 archive; skips +/// winner selection non-degenerate. The pinned bootstrap grade of this fixed 2025 +/// GER40 invocation ORIGINATES from the pre-#210 inline rolling walk-forward +/// (`mc_r_bootstrap_report`) under the retired welded `aura mc --strategy r-sma +/// --real` grammar, and survived both the campaign dissolution and the #220 argv +/// migration byte-identically (the acceptance gate then, the regression anchor +/// now). Gated on the GER40 archive; skips /// cleanly on a data refusal. #[test] fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() { @@ -3350,7 +3357,9 @@ fn mc_dissolves_a_non_r_sma_blueprint() { ); } -/// Property: `aura mc --strategy r-sma --real` is now thin sugar over the one campaign +/// Property: real-data `aura mc --real --axis …` (the #210 dissolution +/// of the retired welded `--strategy r-sma` form, argv since migrated by #220) is +/// thin sugar over the one campaign /// path — a successful run durably auto-registers exactly one generated process /// document, one generated campaign document (carrying the constant "mc" name and the /// stop as a non-empty single risk regime), and one campaign-run record, and emits the diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index a16f6dc..fafca8a 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -2374,12 +2374,19 @@ the project layout and docs-by-role are open (#192 context). (` --real`), while `aura sweep --strategy r-sma --real` stays the inline built-in path (its member lines carry no instrument/topology_hash/selection stamp and register no campaign document) — the built-in `--strategy` demo surface is #159's hard-wired-harness - retirement target, not the dissolution's; for generalize/walkforward/mc the dissolved form - is `--strategy r-sma --real`. [HISTORY — #159 (cuts 1b-4) has since landed - (post-2026-07-07): the retirement target has been hit — the built-in - `--strategy` sweep/demo surface is retired and the inline `--strategy r-sma - --real` path no longer exists; the surviving form is `aura - --axis …` over examples/r_*.json.] Milestone closed 2026-07-07 on a green end-to-end fieldtest + retirement target, not the dissolution's; for generalize/walkforward/mc the dissolved + form is now the same generic grammar as sweep — an arbitrary blueprint positional plus + `--real` and repeatable `--axis =` (#220). [HISTORY — superseded in + two steps. #159 (cuts 1b-4, post-2026-07-07) retired the built-in `--strategy` + sweep/demo surface: the inline `aura sweep --strategy r-sma --real` path no longer + exists. #220 then removed the verbs' weld — at milestone close the dissolved form of + generalize/walkforward/mc was still the welded `--strategy r-sma --real`; #220 made + the three verbs blueprint-generic (arbitrary blueprint + arbitrary `--axis`; mc keeps + the synthetic `--seeds` family unchanged beside the new `--real --axis` campaign mode; + generalize takes `--real `, >=2, with one value per axis), deleted the + welded `--strategy` grammar (clap rejects it, exit 2), dissolved `RGrid`, and unified + the verbs on the #214 invocation struct. The surviving form everywhere is `aura + --real … --axis …` over examples/r_*.json.] Milestone closed 2026-07-07 on a green end-to-end fieldtest (0 bugs; behaviour preservation, campaign-substrate reach-through, and the risk-regime axis all confirmed); residual findings are discoverability/ergonomics follow-ups (#216 risk-axis discoverability, #217 verb knob asymmetry, #218 no-project store litter). diff --git a/docs/glossary.md b/docs/glossary.md index 2c216a6..deba6ea 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -301,7 +301,7 @@ The **optional** documented pre-trade-gate seam in the execution chain (`stop-ru ### walk-forward **Avoid:** — -An orchestration axis: rolling in-sample optimize + out-of-sample test across moving windows, stitched into one out-of-sample verdict plus parameter stability. As with `sweep`, every open knob named by `--list-axes` is **required** on a loaded blueprint — a subset is refused with the missing knob named; pin one with a single-value axis, there is no default. The `aura walkforward --strategy r-sma --real` CLI verb is now thin sugar over the `campaign document` path — translated to a generated campaign (`std::sweep → std::walk_forward`) run through the one executor (#210). +An orchestration axis: rolling in-sample optimize + out-of-sample test across moving windows, stitched into one out-of-sample verdict plus parameter stability. As with `sweep`, every open knob named by `--list-axes` is **required** on a loaded blueprint — a subset is refused with the missing knob named; pin one with a single-value axis, there is no default. The `aura walkforward --real --axis = …` CLI verb is thin sugar over the `campaign document` path — translated to a generated campaign (`std::sweep → std::walk_forward`) run through the one executor (#210; blueprint-generic over arbitrary blueprints and axes since #220). ### World **Avoid:** —