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

13 KiB

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_bootstrapmc_r_bootstrap_reportwalkforward_familypooled_oos_trade_rsr_bootstrapmc_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)

$ 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

// 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

// 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

// 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.rsGeneratedMc, translate_mc, register_generated_mc, run_mc_sugar. Reuses axis_from_values, validate_before_register, run_campaign_returning.
  • crates/aura-cli/src/main.rsmc_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_fromdispatch_mc synthesizes + stores the sma_signal blueprint, resolves the window, ms roller sizes → run_mc_sugartranslate_mc builds [sweep(argmax), walk_forward, monte_carlo] + a campaign (fast/slow axes, one instrument, one Vol regime, seed = mc_seed) → validate_before_registerregister_generated_mcrun_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.