Files
Aura/docs/specs/mc-dissolution.md
T
Brummel 764dc2adbf 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
2026-07-06 23:45:50 +02:00

248 lines
13 KiB
Markdown

# 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(&reg, &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.