docs(specs,plans): mc dissolution — spec + plan (#210)
The FOURTH and LAST verb dissolution. `aura mc --strategy r-sma --real`
dissolves into thin sugar over the one campaign executor — a generated
[std::sweep(argmax), std::walk_forward, std::monte_carlo] campaign
document whose terminal monte_carlo stage reproduces the current
`mc_r_bootstrap` grade byte-for-byte (the committed anchor 3fc491a gates
it). Pure sugar, no executor change; mirrors the walkforward dissolution.
The one new load-bearing decision is the seed mapping: the campaign
carries the mc `--seed` (the wf winners are argmax hence
deflation-seed-independent, proven by the shipped walkforward anchor).
Fork C retention as walkforward: the built-in arm splits, run_mc_r_bootstrap
/ run_mc stay inline for the fenced synthetic/non-real paths (until #159).
Spec grounding-check PASS, auto-signed on #210 (2026-07-06).
refs #210
This commit is contained in:
@@ -0,0 +1,664 @@
|
||||
# mc R-bootstrap dissolution — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/mc-dissolution.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to
|
||||
> run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Dissolve `aura mc --strategy r-sma --real` into thin sugar over the one
|
||||
campaign executor — the FOURTH and LAST verb dissolution (#210) — reproducing the
|
||||
`{"mc_r_bootstrap":{…}}` line byte-for-byte via a generated
|
||||
`[std::sweep(argmax), std::walk_forward, std::monte_carlo]` campaign document.
|
||||
|
||||
**Architecture:** Pure sugar, NO executor change (`aura-campaign`/`aura-engine`/
|
||||
`aura-research` diff-free). Mirrors the shipped walkforward dissolution verbatim
|
||||
(commits `96dc783..18060cb`): a new `verb_sugar.rs` trio (`GeneratedMc`,
|
||||
`translate_mc`, `register_generated_mc`, `run_mc_sugar`) + a `dispatch_mc`
|
||||
built-in-arm split gated on `--strategy r-sma && --real`. The single new
|
||||
load-bearing detail is the seed mapping: `translate_mc` sets `campaign.seed = the
|
||||
mc --seed` (the wf winners are argmax hence seed-independent, proven by the shipped
|
||||
walkforward anchor). The campaign `std::monte_carlo` stage already produces
|
||||
`StageBootstrap::PooledOos(RBootstrap)` after a `std::walk_forward` stage; the sugar
|
||||
reads it from `run.outcome.record.cells[].stages[].bootstrap` and renders the
|
||||
existing `mc_r_bootstrap_json`. Everything the dissolution predicate does not match
|
||||
stays inline, fenced until #159 (Fork C retention — `run_mc_r_bootstrap` / `run_mc`
|
||||
NOT deleted).
|
||||
|
||||
**Tech Stack:** `crates/aura-cli/src/verb_sugar.rs`, `crates/aura-cli/src/main.rs`,
|
||||
`crates/aura-cli/tests/cli_run.rs`. Reuses `run_campaign_returning`,
|
||||
`validate_before_register`, `axis_from_values`, `mc_r_bootstrap_json`, `sma_signal`,
|
||||
`blueprint_to_json`, `topology_hash`, `wf_ms_sizes`, `parse_csv_list`,
|
||||
`WINNER_SELECTION_METRIC`.
|
||||
|
||||
---
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/aura-cli/src/verb_sugar.rs` — add the mc translator/runner trio
|
||||
(`GeneratedMc`, `translate_mc`, `register_generated_mc`, `run_mc_sugar`) after
|
||||
`run_walkforward_sugar` (ends `:503`), plus their unit tests in the existing
|
||||
`#[cfg(test)] mod tests` (after `:794`). No new imports (the block at `:13-17`
|
||||
already carries `StageBlock`/`WfMode`/`SelectRule`/`RiskRegime`/`SweepSelection`/
|
||||
`Axis`/`FORMAT_VERSION`/…; `aura_registry::StageBootstrap` is fully qualified).
|
||||
- Modify: `crates/aura-cli/src/main.rs` — add `mc_args_from` (after
|
||||
`walkforward_args_from`, `:4265`); insert the r-sma-real sugar branch at the top of
|
||||
`dispatch_mc`'s `None` arm (after the blueprint/seeds guard `:4949`, before the
|
||||
`r_path` computation `:4952`). The existing arm body (`:4950-4998`) stays verbatim
|
||||
as the fenced fall-through.
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs` — `mc_dissolves_through_the_campaign_path`
|
||||
(dissolution proof) + `mc_dissolved_refuses_a_multi_value_stop` (Fork A refusal),
|
||||
after the mc anchor (`:5280`).
|
||||
- **Already present (do NOT recreate):** the acceptance anchor
|
||||
`mc_r_bootstrap_real_e2e_pins_the_exact_current_grade` (`cli_run.rs:5242`, committed
|
||||
`3fc491a`). It must stay green byte-for-byte through the dissolution.
|
||||
- **No executor change.** `aura-campaign`/`aura-engine`/`aura-research` are diff-free.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: The mc sugar trio + dispatch split (fully wired)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/verb_sugar.rs`
|
||||
- Modify: `crates/aura-cli/src/main.rs`
|
||||
- Test: `crates/aura-cli/src/verb_sugar.rs` (unit tests in the existing `mod tests`)
|
||||
|
||||
This task ships all production code AND wires the dispatch in one unit, so there is
|
||||
no dead-code window (the whole chain is reachable from `main` the moment it lands).
|
||||
|
||||
- [ ] **Step 1: Add `GeneratedMc` + `translate_mc` to `verb_sugar.rs`**
|
||||
|
||||
Insert immediately after `run_walkforward_sugar` (after line 503, before the
|
||||
`#[cfg(test)]` on line 505):
|
||||
|
||||
```rust
|
||||
/// The two generated documents of one dissolved `mc` invocation.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct GeneratedMc {
|
||||
pub process: ProcessDoc,
|
||||
pub campaign: CampaignDoc,
|
||||
}
|
||||
|
||||
/// Translate one `aura mc --strategy r-sma --real` invocation into its two generated
|
||||
/// documents: a `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` process
|
||||
/// (the sweep enumerates the IS-refit survivor grid for the wf stage; the terminal
|
||||
/// monte_carlo stage pools the per-window OOS trade-R series and r-bootstraps E[R])
|
||||
/// and a campaign running the fast/slow grid over one instrument under a single risk
|
||||
/// regime that carries the stop. Unlike `translate_walkforward` (which hardcodes
|
||||
/// `seed: 0`), `translate_mc` sets `campaign.seed = seed` (the mc `--seed`): the wf
|
||||
/// winners are argmax hence deflation-seed-independent (the shipped walkforward
|
||||
/// anchor proved this), so remapping the campaign seed leaves the pooled series
|
||||
/// unchanged while making the terminal `r_bootstrap` at that seed reproduce the
|
||||
/// inline bootstrap. The roller sizes (`in_sample_ms`/`out_of_sample_ms`/`step_ms`)
|
||||
/// and `blueprint_canonical` come from the caller, exactly as for walkforward.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn translate_mc(
|
||||
fast: &[i64],
|
||||
slow: &[i64],
|
||||
stop_length: i64,
|
||||
stop_k: f64,
|
||||
metric: &str,
|
||||
in_sample_ms: u64,
|
||||
out_of_sample_ms: u64,
|
||||
step_ms: u64,
|
||||
resamples: u32,
|
||||
block_len: u32,
|
||||
seed: u64,
|
||||
name: &str,
|
||||
symbol: &str,
|
||||
from_ms: i64,
|
||||
to_ms: i64,
|
||||
blueprint_canonical: &str,
|
||||
) -> Result<GeneratedMc, String> {
|
||||
let process = ProcessDoc {
|
||||
format_version: FORMAT_VERSION,
|
||||
kind: DocKind::Process,
|
||||
name: "mc".to_string(),
|
||||
description: None,
|
||||
pipeline: vec![
|
||||
StageBlock::Sweep {
|
||||
selection: Some(SweepSelection {
|
||||
metric: metric.to_string(),
|
||||
select: SelectRule::Argmax,
|
||||
deflate: false,
|
||||
}),
|
||||
},
|
||||
StageBlock::WalkForward {
|
||||
in_sample_ms,
|
||||
out_of_sample_ms,
|
||||
step_ms,
|
||||
mode: WfMode::Rolling,
|
||||
metric: metric.to_string(),
|
||||
select: SelectRule::Argmax,
|
||||
},
|
||||
StageBlock::MonteCarlo { resamples, block_len },
|
||||
],
|
||||
};
|
||||
let mut doc_axes: BTreeMap<String, Axis> = BTreeMap::new();
|
||||
doc_axes.insert(
|
||||
"fast.length".to_string(),
|
||||
axis_from_values("fast.length", &fast.iter().map(|&v| Scalar::i64(v)).collect::<Vec<_>>())?,
|
||||
);
|
||||
doc_axes.insert(
|
||||
"slow.length".to_string(),
|
||||
axis_from_values("slow.length", &slow.iter().map(|&v| Scalar::i64(v)).collect::<Vec<_>>())?,
|
||||
);
|
||||
let campaign = CampaignDoc {
|
||||
format_version: FORMAT_VERSION,
|
||||
kind: DocKind::Campaign,
|
||||
name: name.to_string(),
|
||||
description: None,
|
||||
data: DataSection {
|
||||
instruments: vec![symbol.to_string()],
|
||||
windows: vec![Window { from_ms, to_ms }],
|
||||
},
|
||||
strategies: vec![StrategyEntry {
|
||||
r#ref: DocRef::ContentId(content_id_of(blueprint_canonical)),
|
||||
axes: doc_axes,
|
||||
}],
|
||||
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
||||
risk: vec![RiskRegime::Vol { length: stop_length, k: stop_k }],
|
||||
seed,
|
||||
presentation: Presentation {
|
||||
persist_taps: vec![],
|
||||
emit: vec!["family_table".to_string()],
|
||||
},
|
||||
};
|
||||
Ok(GeneratedMc { process, campaign })
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `register_generated_mc` + `run_mc_sugar` to `verb_sugar.rs`**
|
||||
|
||||
Insert immediately after `translate_mc`:
|
||||
|
||||
```rust
|
||||
/// Auto-register both generated mc documents (content-addressed, idempotent).
|
||||
/// Returns `(process_id, campaign_id)`.
|
||||
pub(crate) fn register_generated_mc(
|
||||
reg: &aura_registry::Registry,
|
||||
generated: &GeneratedMc,
|
||||
) -> Result<(String, String), String> {
|
||||
let process_id =
|
||||
reg.put_process(&process_to_json(&generated.process)).map_err(|e| e.to_string())?;
|
||||
let campaign_id =
|
||||
reg.put_campaign(&campaign_to_json(&generated.campaign)).map_err(|e| e.to_string())?;
|
||||
Ok((process_id, campaign_id))
|
||||
}
|
||||
|
||||
/// Run one dissolved `mc` invocation end-to-end: register the generated documents,
|
||||
/// run through the one campaign path, then reprint the single `mc_r_bootstrap` grade
|
||||
/// line from the recorded terminal `PooledOos` bootstrap. The stdout line is
|
||||
/// byte-identical to the inline path (the committed exact-grade anchor is the gate).
|
||||
/// mc prints ONLY the one summary line — no per-window member lines (unlike
|
||||
/// walkforward): the monte_carlo stage is a terminal annotator, not a family.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn run_mc_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,
|
||||
resamples: u32,
|
||||
block_len: u32,
|
||||
seed: u64,
|
||||
name: &str,
|
||||
symbol: &str,
|
||||
from_ms: i64,
|
||||
to_ms: i64,
|
||||
blueprint_canonical: &str,
|
||||
env: &crate::project::Env,
|
||||
) -> Result<(), String> {
|
||||
let generated = translate_mc(
|
||||
fast, slow, stop_length, stop_k, metric, in_sample_ms, out_of_sample_ms, step_ms,
|
||||
resamples, block_len, seed, name, symbol, from_ms, to_ms, blueprint_canonical,
|
||||
)?;
|
||||
let probe_params: Vec<(String, Scalar)> = vec![
|
||||
("fast.length".to_string(), Scalar::i64(fast[0])),
|
||||
("slow.length".to_string(), Scalar::i64(slow[0])),
|
||||
];
|
||||
validate_before_register(&generated.process, &generated.campaign, blueprint_canonical, &probe_params, env)?;
|
||||
let reg = env.registry();
|
||||
let (_process_id, campaign_id) = register_generated_mc(®, &generated)?;
|
||||
let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?;
|
||||
|
||||
// The single cell's terminal monte_carlo StageRealization carries the pooled-OOS
|
||||
// bootstrap (record path: outcome.record.cells[].stages[].bootstrap).
|
||||
let boot = run
|
||||
.outcome
|
||||
.record
|
||||
.cells
|
||||
.iter()
|
||||
.flat_map(|c| c.stages.iter())
|
||||
.find_map(|s| match &s.bootstrap {
|
||||
Some(aura_registry::StageBootstrap::PooledOos(b)) => Some(b),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or("mc produced no pooled-OOS bootstrap")?;
|
||||
println!("{}", crate::mc_r_bootstrap_json(boot));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the `translate_mc` unit tests to `verb_sugar.rs`**
|
||||
|
||||
Insert inside `#[cfg(test)] mod tests`, immediately after
|
||||
`register_generated_wf_stores_documents_the_campaign_can_actually_resolve` (after
|
||||
line 794, before the closing `}` of the module on line 795):
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn translate_mc_is_deterministic_and_carries_the_regime_and_seed() {
|
||||
let a = translate_mc(
|
||||
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized",
|
||||
7_776_000_000, 2_592_000_000, 2_592_000_000,
|
||||
1000, 5, 42,
|
||||
"mc", "GER40", 100, 200, "{\"bp\":1}",
|
||||
)
|
||||
.unwrap();
|
||||
let b = translate_mc(
|
||||
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized",
|
||||
7_776_000_000, 2_592_000_000, 2_592_000_000,
|
||||
1000, 5, 42,
|
||||
"mc", "GER40", 100, 200, "{\"bp\":1}",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
content_id_of(&campaign_to_json(&a.campaign)),
|
||||
content_id_of(&campaign_to_json(&b.campaign)),
|
||||
"identical invocations must generate identical campaign ids"
|
||||
);
|
||||
// pipeline: sweep(argmax) then walk_forward(argmax, rolling) then monte_carlo
|
||||
assert_eq!(
|
||||
a.process.pipeline,
|
||||
vec![
|
||||
StageBlock::Sweep {
|
||||
selection: Some(SweepSelection {
|
||||
metric: "sqn_normalized".to_string(),
|
||||
select: SelectRule::Argmax,
|
||||
deflate: false,
|
||||
})
|
||||
},
|
||||
StageBlock::WalkForward {
|
||||
in_sample_ms: 7_776_000_000,
|
||||
out_of_sample_ms: 2_592_000_000,
|
||||
step_ms: 2_592_000_000,
|
||||
mode: WfMode::Rolling,
|
||||
metric: "sqn_normalized".to_string(),
|
||||
select: SelectRule::Argmax,
|
||||
},
|
||||
StageBlock::MonteCarlo { resamples: 1000, block_len: 5 },
|
||||
]
|
||||
);
|
||||
assert_eq!(a.campaign.seed, 42, "the campaign seed carries the mc --seed");
|
||||
assert_eq!(a.campaign.risk, vec![RiskRegime::Vol { length: 14, k: 2.0 }]);
|
||||
assert_eq!(a.campaign.data.instruments, vec!["GER40".to_string()]);
|
||||
assert_eq!(a.campaign.strategies.len(), 1);
|
||||
assert_eq!(
|
||||
a.campaign.strategies[0].axes["fast.length"].values,
|
||||
vec![Scalar::i64(3), Scalar::i64(5)]
|
||||
);
|
||||
assert_eq!(
|
||||
a.campaign.strategies[0].axes["slow.length"].values,
|
||||
vec![Scalar::i64(12), Scalar::i64(20)]
|
||||
);
|
||||
assert_eq!(a.process.name, "mc");
|
||||
assert!(aura_research::validate_campaign(&a.campaign).is_empty());
|
||||
assert!(aura_research::validate_process(&a.process).is_empty());
|
||||
assert!(aura_campaign::preflight(&a.process, &a.campaign).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids() {
|
||||
let base = translate_mc(
|
||||
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized",
|
||||
7_776_000_000, 2_592_000_000, 2_592_000_000,
|
||||
1000, 5, 42,
|
||||
"mc", "GER40", 100, 200, "{\"bp\":1}",
|
||||
)
|
||||
.unwrap();
|
||||
let different_seed = translate_mc(
|
||||
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized",
|
||||
7_776_000_000, 2_592_000_000, 2_592_000_000,
|
||||
1000, 5, 7,
|
||||
"mc", "GER40", 100, 200, "{\"bp\":1}",
|
||||
)
|
||||
.unwrap();
|
||||
let different_k = translate_mc(
|
||||
&[3, 5], &[12, 20], 14, 3.0, "sqn_normalized",
|
||||
7_776_000_000, 2_592_000_000, 2_592_000_000,
|
||||
1000, 5, 42,
|
||||
"mc", "GER40", 100, 200, "{\"bp\":1}",
|
||||
)
|
||||
.unwrap();
|
||||
let base_id = content_id_of(&campaign_to_json(&base.campaign));
|
||||
let seed_id = content_id_of(&campaign_to_json(&different_seed.campaign));
|
||||
let k_id = content_id_of(&campaign_to_json(&different_k.campaign));
|
||||
assert_ne!(base_id, seed_id, "a different mc seed must not collide onto the same campaign id");
|
||||
assert_ne!(base_id, k_id, "a different stop_k must not collide onto the same campaign id");
|
||||
assert_ne!(seed_id, k_id, "the two diverging documents must not collide with each other");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_generated_mc_stores_documents_the_campaign_can_actually_resolve() {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"aura-verb-sugar-mc-registry-test-{}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let reg = aura_registry::Registry::open(dir.join("runs.jsonl"));
|
||||
let generated = translate_mc(
|
||||
&[3, 5], &[12, 20], 14, 2.0, "sqn_normalized",
|
||||
7_776_000_000, 2_592_000_000, 2_592_000_000,
|
||||
1000, 5, 42,
|
||||
"mc", "GER40", 100, 200, "{\"bp\":1}",
|
||||
)
|
||||
.unwrap();
|
||||
let (process_id, campaign_id) = register_generated_mc(®, &generated).unwrap();
|
||||
let stored_campaign = reg.get_campaign(&campaign_id).unwrap().expect("campaign stored");
|
||||
assert_eq!(stored_campaign, campaign_to_json(&generated.campaign));
|
||||
let DocRef::ContentId(ref_id) = &generated.campaign.process.r#ref else {
|
||||
panic!("mc's process ref is a content id");
|
||||
};
|
||||
assert_eq!(ref_id, &process_id, "the stored process id must be the campaign's own ref");
|
||||
let stored_process = reg.get_process(ref_id).unwrap().expect("process stored");
|
||||
assert_eq!(stored_process, process_to_json(&generated.process));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify the verb_sugar unit tests pass**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura translate_mc`
|
||||
Expected: PASS — `translate_mc_is_deterministic_and_carries_the_regime_and_seed` and
|
||||
`translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids` run and pass.
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura register_generated_mc`
|
||||
Expected: PASS — `register_generated_mc_stores_documents_the_campaign_can_actually_resolve`
|
||||
runs and passes.
|
||||
|
||||
- [ ] **Step 5: Add `mc_args_from` to `main.rs`**
|
||||
|
||||
Insert immediately after `walkforward_args_from` (after line 4265, before
|
||||
`generalize_args_from`'s doc comment on line 4267):
|
||||
|
||||
```rust
|
||||
/// Convert `McCmd` into the resolved argument shape the mc sugar consumes (the
|
||||
/// dissolved `--strategy r-sma --real` branch). Single instrument, multi-value
|
||||
/// fast/slow (the IS-refit grid), single-value stop (Fork A: the stop is a risk
|
||||
/// regime, not a swept axis); all four grid knobs required. `--block-len`/
|
||||
/// `--resamples`/`--seed` default to `1`/`1000`/`1` — byte-identical to the inline
|
||||
/// `McArgs::RealR` defaults (`main.rs:4981-4983`), so an omitted flag produces the
|
||||
/// same document either way. The usize->u32 conversion for `block_len`/`resamples`
|
||||
/// lands HERE, at the argv boundary where the CLI's `usize` meets the document's
|
||||
/// `u32` vocabulary (`StageBlock::MonteCarlo`), keeping every downstream signature
|
||||
/// in the document's native type. `--name`/`--trace` are rejected: the inline mc
|
||||
/// R-path rejects them too (`main.rs:4964`), so the sugar preserves that (the
|
||||
/// R-bootstrap records without a family name — the campaign name is a constant
|
||||
/// "mc").
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn mc_args_from(
|
||||
a: &McCmd,
|
||||
) -> Result<(String, String, Vec<i64>, Vec<i64>, i64, f64, u32, u32, u64, Option<i64>, Option<i64>), String> {
|
||||
let symbol = match a.real.as_deref() {
|
||||
None | Some("") => return Err("mc --strategy r-sma dissolves only over --real <SYMBOL>".to_string()),
|
||||
Some(s) => s.to_string(),
|
||||
};
|
||||
if a.name.is_some() || a.trace.is_some() {
|
||||
return Err("mc --strategy r-sma: --name/--trace are not accepted (the R-bootstrap records without a family name)".to_string());
|
||||
}
|
||||
let knobs = || "mc requires --fast --slow --stop-length --stop-k (fast/slow may be CSV grids; the stop is single-value)".to_string();
|
||||
let fast: Vec<i64> = parse_csv_list(a.fast.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
|
||||
let slow: Vec<i64> = parse_csv_list(a.slow.as_deref().ok_or_else(knobs)?).map_err(|()| knobs())?;
|
||||
let 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("mc: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string());
|
||||
}
|
||||
let block_len = a.block_len.map(|v| v as u32).unwrap_or(1);
|
||||
let resamples = a.resamples.map(|v| v as u32).unwrap_or(1000);
|
||||
let seed = a.seed.unwrap_or(1);
|
||||
Ok(("mc".to_string(), symbol, fast, slow, stop_length[0], stop_k[0], block_len, resamples, seed, a.from, a.to))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Insert the dissolution split into `dispatch_mc`'s `None` arm**
|
||||
|
||||
In `main.rs`, the `None` arm currently reads (lines 4944-4949):
|
||||
|
||||
```rust
|
||||
None => {
|
||||
let usage = || "Usage: aura mc [--name <n>|--trace <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);
|
||||
}
|
||||
// The R-bootstrap path is selected by any r-sma knob; otherwise the
|
||||
```
|
||||
|
||||
Insert the sugar branch between the guard's closing `}` (line 4949) and the
|
||||
`// The R-bootstrap path is selected…` comment (line 4950), so the arm becomes:
|
||||
|
||||
```rust
|
||||
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| {
|
||||
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);
|
||||
});
|
||||
// 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: 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(
|
||||
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_mc_sugar(
|
||||
&fast, &slow, stop_length, stop_k, WINNER_SELECTION_METRIC,
|
||||
is_ms, oos_ms, step_ms, resamples, block_len, seed, &name, &symbol, from_ms, to_ms,
|
||||
&canonical, env,
|
||||
)
|
||||
.unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// The R-bootstrap path is selected by any r-sma knob; otherwise the
|
||||
```
|
||||
|
||||
Leave the entire existing arm body below (`let r_path = …` through the `match mc_args`
|
||||
block, lines 4950-4998) UNTOUCHED — it is the fenced fall-through for synthetic-r-sma,
|
||||
the seed-resweep, and everything else.
|
||||
|
||||
- [ ] **Step 7: Build gate + the acceptance anchor stays green**
|
||||
|
||||
Run: `cargo build --workspace`
|
||||
Expected: clean build, 0 errors. (`run_mc_sugar` is now reachable from `main` via the
|
||||
dispatch — no dead code.)
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run mc_r_bootstrap_real_e2e_pins_the_exact_current_grade`
|
||||
Expected: PASS (or `skip: no local GER40 data` if the archive is absent). If it runs,
|
||||
the `mc_r_bootstrap` line is byte-identical (block_len=5, n_resamples=1000,
|
||||
n_trades=20681, e_r.mean=-0.0025753095301594307, e_r.p50=-0.0023049902245975227,
|
||||
prob_le_zero=0.558). This is the equivalence gate — a failure here means the
|
||||
dissolution changed the grade and the task is not done.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Integration coverage — dissolution proof + Fork-A refusal
|
||||
|
||||
**Files:**
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs`
|
||||
|
||||
- [ ] **Step 1: Add the dissolution-proof and refusal tests**
|
||||
|
||||
Insert after the mc anchor test (after line 5280, at the end of the file's mc block):
|
||||
|
||||
```rust
|
||||
/// Property: `aura mc --strategy r-sma --real` is now thin sugar over the one campaign
|
||||
/// path — a successful run durably auto-registers exactly one generated process
|
||||
/// document, one generated campaign document (carrying the constant "mc" name and the
|
||||
/// stop as a non-empty single risk regime), and one campaign-run record, and emits the
|
||||
/// single `mc_r_bootstrap` grade line. The pipeline's leading argmax sweep and its
|
||||
/// walk_forward stage each persist one family; the terminal monte_carlo stage is an
|
||||
/// annotator and adds NO family. This is the observable proof the inline path is gone
|
||||
/// and the dissolution runs through the executor. Gated on the GER40 2025 archive;
|
||||
/// skips cleanly on a data refusal.
|
||||
#[test]
|
||||
fn mc_dissolves_through_the_campaign_path() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let cwd = temp_cwd("mc-dissolves");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"--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);
|
||||
|
||||
// the single grade line is emitted (and nothing else on stdout).
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
assert_eq!(stdout.lines().count(), 1, "mc prints exactly one grade line: {stdout:?}");
|
||||
assert!(
|
||||
stdout.lines().next().is_some_and(|l| l.starts_with("{\"mc_r_bootstrap\":")),
|
||||
"the one line is the mc_r_bootstrap grade: {stdout}"
|
||||
);
|
||||
|
||||
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\":\"mc\""),
|
||||
"the generated campaign document carries the constant mc name: {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}"
|
||||
);
|
||||
assert_eq!(
|
||||
fams_out.lines().filter(|l| l.contains("\"kind\":\"MonteCarlo\"")).count(),
|
||||
0,
|
||||
"the terminal monte_carlo stage is an annotator, not a family: {fams_out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Fork A: the stop is a single risk regime on the dissolved mc path — a multi-value
|
||||
/// `--stop-length`/`--stop-k` is a usage refusal (exit 2), not a swept axis. Mirrors
|
||||
/// `walkforward_dissolved_refuses_a_multi_value_stop`. NOT gated — the refusal is pure
|
||||
/// argument parsing, before any data access.
|
||||
#[test]
|
||||
fn mc_dissolved_refuses_a_multi_value_stop() {
|
||||
let cwd = temp_cwd("mc-multi-stop");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"mc", "--strategy", "r-sma", "--real", "GER40",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0",
|
||||
])
|
||||
.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}");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the new integration tests pass**
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run mc_dissolves_through_the_campaign_path`
|
||||
Expected: PASS (or `skip: no local GER40 data`).
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run mc_dissolved_refuses_a_multi_value_stop`
|
||||
Expected: PASS (exit 2, stderr carries "single risk regime").
|
||||
|
||||
- [ ] **Step 3: Full-suite + clippy gate**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — 0 failures. In particular the three synthetic-r-sma tests
|
||||
(`mc_strategy_r_sma_prints_a_bootstrap_line_deterministically`,
|
||||
`mc_r_sma_block_len_is_observable_and_changes_the_distribution`,
|
||||
`mc_r_sma_oversized_block_len_clamps_and_collapses`) stay green: they use no `--real`,
|
||||
so they never enter the sugar branch (`real.is_some()` is false) and run the fenced
|
||||
inline path unchanged. `mc_rejects_real_flag_with_usage_exit_2` also stays green
|
||||
(`--real` without `--strategy r-sma` falls through to the inline exit-2 refusal, no
|
||||
registry write).
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: clean, 0 warnings.
|
||||
@@ -0,0 +1,247 @@
|
||||
# mc R-bootstrap dissolution — Design Spec
|
||||
|
||||
**Date:** 2026-07-06
|
||||
**Status:** Draft — awaiting user spec review
|
||||
**Authors:** orchestrator + Claude
|
||||
|
||||
> Milestone #210, the FOURTH and LAST verb dissolution. Reference issue: Gitea
|
||||
> Brummel/Aura #210. Mirrors the just-shipped walkforward dissolution; the only new
|
||||
> load-bearing decision is the seed mapping (recorded as a #210 comment,
|
||||
> 2026-07-06). Scope reading: decision 2 / Reading A (Fork C retention).
|
||||
|
||||
## Goal
|
||||
|
||||
Dissolve `aura mc --strategy r-sma --real`'s inline execution into thin sugar over
|
||||
the one campaign executor (`aura_campaign::execute`), completing the verb set
|
||||
(sweep, generalize, walkforward, mc). The `dispatch_mc` built-in arm gains an
|
||||
r-sma-real branch that runs through a generated, content-addressed campaign
|
||||
document with a `[std::sweep(argmax), std::walk_forward, std::monte_carlo]` process,
|
||||
reproducing the `{"mc_r_bootstrap":{…}}` line byte-for-byte. The committed anchor
|
||||
`mc_r_bootstrap_real_e2e_pins_the_exact_current_grade` (3fc491a) is the acceptance
|
||||
gate.
|
||||
|
||||
`run_mc_r_bootstrap`, `run_mc`, and their helpers are NOT deleted (Fork C, as
|
||||
walkforward): the built-in arm splits, and everything the dissolution predicate
|
||||
does not match stays inline, fenced until #159 — the synthetic-r-sma path
|
||||
(`--strategy r-sma` without `--real`), the synthetic seed-resweep (`aura mc`), and
|
||||
the blueprint mc path.
|
||||
|
||||
## Architecture
|
||||
|
||||
Pure sugar — NO executor change (`aura-campaign`/`aura-engine`/`aura-research`
|
||||
diff-free). The campaign monte_carlo stage already does the identical work:
|
||||
`crates/aura-campaign/src/exec.rs:400-446`, `StageBlock::MonteCarlo` after a
|
||||
`std::walk_forward` stage produces `StageBootstrap::PooledOos(r_bootstrap(
|
||||
&pooled_trade_rs(&fam.reports), resamples, block_len, seed))`, seeded from the
|
||||
campaign seed, landing in `StageRealization.bootstrap`
|
||||
(`crates/aura-registry/src/lineage.rs:130`). It is reachable in-memory at
|
||||
`run.outcome.record.cells[0].stages[].bootstrap`. So the sugar reads that
|
||||
`PooledOos(RBootstrap)` and renders the existing `mc_r_bootstrap_json(&RBootstrap)`.
|
||||
mc prints ONLY the one summary line (no per-window member lines, unlike walkforward).
|
||||
|
||||
The inline mc R-bootstrap is `run_mc_r_bootstrap` → `mc_r_bootstrap_report` →
|
||||
`walkforward_family` → `pooled_oos_trade_rs` → `r_bootstrap` → `mc_r_bootstrap_json`
|
||||
(main.rs ~2106-2133). The dissolution reroutes the same rolling walk-forward +
|
||||
pooled-OOS bootstrap through the campaign path.
|
||||
|
||||
**The seed mapping (the one new load-bearing detail).** The inline path uses TWO
|
||||
seeds: a fixed `DEFLATION_SEED` for the per-window wf winner selection, and the mc
|
||||
`--seed` for the terminal `r_bootstrap`. The campaign path has ONE `campaign.seed`,
|
||||
used for both. This reconciles because the wf winners are **argmax** — the deflation
|
||||
seed only annotates the recorded selection provenance, never changes the pick — so
|
||||
the winners, and the pooled OOS series, are **seed-independent**. The shipped
|
||||
walkforward anchor already proved this (the campaign path at `campaign.seed = 0`
|
||||
reproduced the inline `DEFLATION_SEED` winners byte-for-byte). So `translate_mc`
|
||||
sets `campaign.seed = the mc --seed`: the pooled series is unchanged (n_trades =
|
||||
20681, matching the walkforward multi-grid anchor — mc pools the same series) and
|
||||
the `r_bootstrap` at that seed reproduces the inline bootstrap. `block_len` /
|
||||
`resamples` map to `StageBlock::MonteCarlo{resamples, block_len}`.
|
||||
|
||||
Fork A (stop): a single `RiskRegime::Vol{stop_length, stop_k}` (multi-value stop
|
||||
refused); fast/slow are the multi-value IS-refit grid.
|
||||
|
||||
## Concrete code shapes
|
||||
|
||||
### The user-facing program (unchanged surface, dissolved execution)
|
||||
|
||||
```console
|
||||
$ aura mc --strategy r-sma --real GER40 \
|
||||
--from 1735689600000 --to 1767225599000 \
|
||||
--fast 3,5 --slow 12,20 --stop-length 14 --stop-k 2.0 \
|
||||
--block-len 5 --resamples 1000 --seed 42
|
||||
{"mc_r_bootstrap":{"block_len":5,"e_r":{"mean":-0.0025753095301594307,…},
|
||||
"n_resamples":1000,"n_trades":20681,"prob_le_zero":0.558}}
|
||||
```
|
||||
|
||||
Byte-identical to today's inline output (the acceptance gate). The audience — a
|
||||
researcher stress-testing a candidate's E[R] distribution — runs the identical
|
||||
command; the execution becomes a durable, content-addressed campaign document (C18,
|
||||
reproducible-from-manifest). It reintroduces no look-ahead or determinism failure:
|
||||
the same deterministic engine `r_bootstrap` over the same pooled OOS series (C1),
|
||||
no new streamed non-scalar (invariant 4). All mc-specific logic stays in the CLI
|
||||
translator/reader (invariant 10: no verb-special engine logic); the World runs it
|
||||
through the one campaign path (invariant 12).
|
||||
|
||||
### Sugar translator — mirror of `translate_walkforward` (verb_sugar.rs), + the monte_carlo stage
|
||||
|
||||
```rust
|
||||
// crates/aura-cli/src/verb_sugar.rs — new, mirror of translate_walkforward
|
||||
pub(crate) struct GeneratedMc { pub process: ProcessDoc, pub campaign: CampaignDoc }
|
||||
|
||||
#[allow(clippy::too_many_arguments)] // #214: bundle deferred until after this last verb
|
||||
pub(crate) fn translate_mc(
|
||||
fast: &[i64], slow: &[i64],
|
||||
stop_length: i64, stop_k: f64,
|
||||
metric: &str, // "sqn_normalized" (the wf objective)
|
||||
in_sample_ms: u64, out_of_sample_ms: u64, step_ms: u64,
|
||||
resamples: u32, block_len: u32, seed: u64, // seed = the mc --seed
|
||||
name: &str, symbol: &str, from_ms: i64, to_ms: i64,
|
||||
blueprint_canonical: &str,
|
||||
) -> Result<GeneratedMc, String> {
|
||||
let process = ProcessDoc {
|
||||
format_version: FORMAT_VERSION, kind: DocKind::Process,
|
||||
name: "mc".to_string(), description: None,
|
||||
pipeline: vec![
|
||||
StageBlock::Sweep { selection: Some(SweepSelection {
|
||||
metric: metric.to_string(), select: SelectRule::Argmax, deflate: false }) },
|
||||
StageBlock::WalkForward {
|
||||
in_sample_ms, out_of_sample_ms, step_ms,
|
||||
mode: WfMode::Rolling, metric: metric.to_string(), select: SelectRule::Argmax },
|
||||
StageBlock::MonteCarlo { resamples, block_len },
|
||||
],
|
||||
};
|
||||
// …doc_axes for fast.length / slow.length exactly as translate_walkforward…
|
||||
let campaign = CampaignDoc {
|
||||
// …instruments: [symbol], windows: [{from_ms,to_ms}], one StrategyEntry with the axes…
|
||||
risk: vec![RiskRegime::Vol { length: stop_length, k: stop_k }],
|
||||
seed, // <-- the mc --seed (the wf winners are seed-independent)
|
||||
// …presentation: persist_taps [], emit ["family_table"]…
|
||||
// (process/data/strategies fields identical in shape to translate_walkforward)
|
||||
};
|
||||
Ok(GeneratedMc { process, campaign })
|
||||
}
|
||||
```
|
||||
|
||||
`register_generated_mc` is the verbatim `register_generated_wf` shape (put_process +
|
||||
put_campaign).
|
||||
|
||||
### Sugar runner — read the bootstrap from the record, print one line
|
||||
|
||||
```rust
|
||||
// crates/aura-cli/src/verb_sugar.rs — new, mirror of run_walkforward_sugar
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn run_mc_sugar(/* translate_mc args + env */) -> Result<(), String> {
|
||||
let generated = translate_mc(/* … */)?;
|
||||
let probe_params = 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 (_p, campaign_id) = register_generated_mc(®, &generated)?;
|
||||
let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?;
|
||||
|
||||
// The single cell's monte_carlo StageRealization carries the pooled-OOS bootstrap.
|
||||
let boot = run.outcome.record.cells.iter()
|
||||
.flat_map(|c| c.stages.iter())
|
||||
.find_map(|s| match &s.bootstrap {
|
||||
Some(aura_registry::StageBootstrap::PooledOos(b)) => Some(b),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or("mc produced no pooled-OOS bootstrap")?;
|
||||
println!("{}", crate::mc_r_bootstrap_json(boot));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Dispatch split — mirror of the walkforward split; `run_mc_r_bootstrap`/`run_mc` STAY
|
||||
|
||||
```rust
|
||||
// crates/aura-cli/src/main.rs — dispatch_mc, the None (built-in) arm
|
||||
None => {
|
||||
// …existing usage / blueprint+seeds guards, unchanged…
|
||||
if a.strategy.as_deref() == Some("r-sma") && a.real.is_some() {
|
||||
// r-sma --real: the dissolved sugar path.
|
||||
let (name, symbol, fast, slow, stop_length, stop_k, block_len, resamples, seed, from, to) =
|
||||
mc_args_from(&a) /* unwrap_or_else: eprintln + exit 2 */;
|
||||
let signal = sma_signal(None, None);
|
||||
let canonical = blueprint_to_json(&signal).expect("a bare sma_signal serializes");
|
||||
let reg = env.registry();
|
||||
reg.put_blueprint(&topology_hash(&signal), &canonical) /* exit 1 on err */;
|
||||
let (from_ms, to_ms) = resolve_window(&symbol, from, to, env); // full_window unconditionally
|
||||
let (is_ms, oos_ms, step_ms) = wf_ms_sizes();
|
||||
verb_sugar::run_mc_sugar(&fast, &slow, stop_length, stop_k, "sqn_normalized",
|
||||
is_ms, oos_ms, step_ms, resamples, block_len, seed, &name, &symbol, from_ms, to_ms, &canonical, env)
|
||||
/* unwrap_or_else: eprintln + exit 1 */;
|
||||
return;
|
||||
}
|
||||
// …EXISTING fenced body, verbatim: the r_path / McArgs::RealR / McArgs::Synthetic
|
||||
// logic → run_mc_r_bootstrap / run_mc…
|
||||
}
|
||||
```
|
||||
|
||||
`mc_args_from` mirrors `walkforward_args_from` plus `block_len` (default 1),
|
||||
`resamples` (default 1000), `seed` (default 1) — single-value stop (multi-value
|
||||
refused, Fork A), fast/slow CSV grids, `--real` required. `run_mc_r_bootstrap`,
|
||||
`run_mc`, `mc_r_bootstrap_report`, `mc_r_bootstrap_json`, `pooled_oos_trade_rs` are
|
||||
all RETAINED (the fenced paths + `mc_r_bootstrap_json` is reused by the sugar).
|
||||
|
||||
## Components
|
||||
|
||||
- **`crates/aura-cli/src/verb_sugar.rs`** — `GeneratedMc`, `translate_mc`,
|
||||
`register_generated_mc`, `run_mc_sugar`. Reuses `axis_from_values`,
|
||||
`validate_before_register`, `run_campaign_returning`.
|
||||
- **`crates/aura-cli/src/main.rs`** — `mc_args_from`, the `dispatch_mc` built-in-arm
|
||||
split (add the r-sma-real sugar branch; retain the existing body). Reuses
|
||||
`wf_ms_sizes`, `resolve_window` (the walkforward helper if it was extracted, else
|
||||
the same inline match), `sma_signal`, `mc_r_bootstrap_json`.
|
||||
- **No executor change.**
|
||||
|
||||
## Data flow
|
||||
|
||||
argv → `mc_args_from` → `dispatch_mc` synthesizes + stores the `sma_signal`
|
||||
blueprint, resolves the window, ms roller sizes → `run_mc_sugar` → `translate_mc`
|
||||
builds `[sweep(argmax), walk_forward, monte_carlo]` + a campaign (fast/slow axes,
|
||||
one instrument, one Vol regime, seed = mc_seed) → `validate_before_register` →
|
||||
`register_generated_mc` → `run_campaign_returning` (sweep → wf refit per window →
|
||||
monte_carlo pools the OOS R + bootstraps at the campaign seed) → the sugar reads
|
||||
`StageBootstrap::PooledOos(RBootstrap)` from the record's stages and prints
|
||||
`mc_r_bootstrap_json`.
|
||||
|
||||
## Error handling
|
||||
|
||||
- Non-`r-sma`, missing knobs, empty `--real`: the fenced arm's / `mc_args_from`'s
|
||||
refusals (exit 2), reusing message strings where shared.
|
||||
- Multi-value `--stop-length`/`--stop-k`: exit 2 (Fork A), same message shape as
|
||||
walkforward.
|
||||
- Referential faults: `validate_before_register` refuses before any store write.
|
||||
- Data refusal (no archive): the member runner's `no_real_data` path (exit 1).
|
||||
|
||||
## Testing strategy
|
||||
|
||||
- **The committed anchor** `mc_r_bootstrap_real_e2e_pins_the_exact_current_grade`
|
||||
(3fc491a) is the equivalence gate: it must stay green through the dissolution
|
||||
(block_len, n_resamples, n_trades, e_r.mean, e_r.p50, prob_le_zero all exact).
|
||||
- **`translate_mc` unit tests** (mirror the walkforward translator tests):
|
||||
deterministic content ids; the pipeline is `[sweep(argmax), walk_forward,
|
||||
monte_carlo]` with the right roller/mc fields + seed; the regime carries the stop;
|
||||
diverging seeds/stops do not collide content ids; `register_generated_mc` resolves.
|
||||
- **Dissolution-proof e2e** (mirror `walkforward_dissolves_through_the_campaign_path`):
|
||||
a successful real run auto-registers one process + one campaign + one campaign-run,
|
||||
and the output is the single `mc_r_bootstrap` line.
|
||||
- **Front-end refusal tests**: multi-value stop (exit 2), non-r-sma real handled by
|
||||
the fenced path (still works).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. `aura mc --strategy r-sma --real <SYM> …` runs through the campaign path; the
|
||||
anchor stays green (the `mc_r_bootstrap` line byte-identical).
|
||||
2. `--strategy r-sma --real` no longer flows through `run_mc_r_bootstrap`; it runs
|
||||
the sugar path. `run_mc_r_bootstrap` / `run_mc` are RETAINED for the fenced
|
||||
branches.
|
||||
3. The fenced branches are behaviour-unchanged (Reading A): synthetic-r-sma
|
||||
(`--strategy r-sma` no `--real`), the synthetic seed-resweep (`aura mc`), and the
|
||||
blueprint mc path all run inline exactly as before.
|
||||
4. Multi-value `--stop-length`/`--stop-k` is refused (Fork A).
|
||||
5. No executor change (`aura-campaign`/`aura-engine`/`aura-research` diff-free).
|
||||
6. Full workspace suite green; `clippy --all-targets -D warnings` clean.
|
||||
Reference in New Issue
Block a user