94cdbf90cc
The campaign walk_forward stage picked its per-window in-sample winner by optimize_deflated (argmax) unconditionally, dropping the stage's `select` field, and both the preflight and the `walkforward` verb's `--real` campaign sub-branch refused plateau:* with a forward-pointer here (the #210 decision-4 deferral; the synthetic non-`--real` sub-branch has accepted plateau since #173). Honour plateau selection when no gate precedes the wf stage. - Preflight: the plateau-in-wf refusal is narrowed to gate-preceded stages. With no preceding gate the survivors are the full sweep grid, so the parameter lattice (grid.axis_lens) is exact and optimize_plateau is sound; a gate filters survivors below the grid, breaking the lattice, so plateau after any gate stays refused. The refusal is static — document validity must be data-independent (a gate's filtering depends on the data) — and keeps the existing "a gated survivor subset has no parameter lattice" message, which now describes exactly the only case it fires. - Executor: run_walk_forward_stage threads the per-cell grid.axis_lens and picks each window's in-sample winner via select_sweep_winner — the same dispatch the sweep stage uses. (Argmax, deflate=true) is byte-identical to the old optimize_deflated call, so the argmax grade and content-id pins are unchanged; the plateau arms read axis_lens and ignore deflate. - CLI: the walkforward verb threads --select through translate_walkforward / run_walkforward_sugar (a walkforward-local param, not the shared four-verb SugarInvocation), mapping the parsed Selection to the research SelectRule via select_rule_of; the refusal and its select_is_plateau helper are deleted. Scope is walkforward-only — the mc verb's wf-stage select is untouched. The plateau:worst grade anchor (stitched_total_pips = -9683776.67) is distinct from the argmax anchor (-10398606.67), proving the plateau path genuinely changes winner selection rather than falling back to argmax. A gate-free plateau process validates clean while a gate-preceded one is refused (a new campaign-validate e2e). Argmax defaults stay byte-identical. closes #215
1006 lines
44 KiB
Rust
1006 lines
44 KiB
Rust
//! Verb sugar: the dissolved orchestration verbs' argv → generated campaign
|
|
//! documents (docs/specs/sweep-dissolution.md; #210 fork decisions).
|
|
//!
|
|
//! A dissolved verb invocation is translated into a selection-free process
|
|
//! document plus a campaign document expressing exactly the invocation's
|
|
//! intent, both auto-registered content-addressed (every ad-hoc invocation
|
|
//! becomes durable, diffable, reproducible intent), then run through the one
|
|
//! campaign executor in sugar presentation mode (member lines only).
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use aura_core::{Scalar, ScalarKind};
|
|
use aura_research::{
|
|
campaign_to_json, content_id_of, process_to_json, Axis, CampaignDoc, DataSection, DocKind,
|
|
DocRef, Presentation, ProcessDoc, ProcessRef, RiskRegime, SelectRule, StageBlock,
|
|
StrategyEntry, SweepSelection, WfMode, Window, FORMAT_VERSION,
|
|
};
|
|
|
|
/// The two generated documents of one dissolved-verb invocation.
|
|
#[derive(Debug)]
|
|
pub(crate) struct GeneratedSweep {
|
|
pub process: ProcessDoc,
|
|
pub campaign: CampaignDoc,
|
|
}
|
|
|
|
/// One dissolved-verb invocation's shared shape (#214/#220): arbitrary RAW
|
|
/// axes (post `wrapped_to_raw_axis` strip), the family name, the instrument
|
|
/// list, the shared window, the canonical blueprint, and the optional single
|
|
/// Vol stop regime (`None` binds no regime — sweep's contract). All four
|
|
/// dissolved verbs (sweep, generalize, walkforward, mc) route their dispatch
|
|
/// args through this shape.
|
|
pub(crate) struct SugarInvocation<'a> {
|
|
pub axes: &'a [(String, Vec<Scalar>)],
|
|
pub name: String,
|
|
/// Instruments: sweep/walkforward/mc carry exactly one; generalize >=2.
|
|
pub symbols: Vec<String>,
|
|
pub from_ms: i64,
|
|
pub to_ms: i64,
|
|
pub blueprint_canonical: &'a str,
|
|
pub stop: Option<VolStop>,
|
|
}
|
|
|
|
/// The single protective-stop regime a dissolved verb binds (`RiskRegime::Vol`).
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct VolStop {
|
|
pub length: i64,
|
|
pub k: f64,
|
|
}
|
|
|
|
/// The walk-forward roller sizes (ms) a wf/mc invocation carries.
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct WfWindows {
|
|
pub in_sample_ms: u64,
|
|
pub out_of_sample_ms: u64,
|
|
pub step_ms: u64,
|
|
}
|
|
|
|
/// The mc-specific bootstrap knobs.
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct McKnobs {
|
|
pub resamples: u32,
|
|
pub block_len: u32,
|
|
pub seed: u64,
|
|
}
|
|
|
|
/// The generic doc-axes map: every invocation axis under its arbitrary raw
|
|
/// bind-key (the sweep shape, adopted by all four verbs — #220).
|
|
fn doc_axes_from(axes: &[(String, Vec<Scalar>)]) -> Result<BTreeMap<String, Axis>, String> {
|
|
let mut doc_axes: BTreeMap<String, Axis> = BTreeMap::new();
|
|
for (axis_name, values) in axes {
|
|
doc_axes.insert(axis_name.clone(), axis_from_values(axis_name, values)?);
|
|
}
|
|
Ok(doc_axes)
|
|
}
|
|
|
|
/// The optional single Vol stop regime -> the campaign risk vector.
|
|
fn risk_from(stop: Option<VolStop>) -> Vec<RiskRegime> {
|
|
stop.map(|s| vec![RiskRegime::Vol { length: s.length, k: s.k }]).unwrap_or_default()
|
|
}
|
|
|
|
/// One probe param per axis (any one grid value — `bind_axes` checks NAME
|
|
/// coverage/uniqueness, never the bound value).
|
|
fn probe_params_from(axes: &[(String, Vec<Scalar>)]) -> Vec<(String, Scalar)> {
|
|
axes.iter().filter_map(|(n, vals)| vals.first().map(|v| (n.clone(), *v))).collect()
|
|
}
|
|
|
|
/// Map one verb axis (`--axis name=csv`, already scalar-parsed) to a
|
|
/// document `Axis`: the kind is the first value's kind; a mixed-kind CSV is
|
|
/// refused (the document axis declares its kind ONCE — #189).
|
|
fn axis_from_values(name: &str, values: &[Scalar]) -> Result<Axis, String> {
|
|
let kind = match values.first() {
|
|
Some(Scalar::I64(_)) => ScalarKind::I64,
|
|
Some(Scalar::F64(_)) => ScalarKind::F64,
|
|
Some(Scalar::Bool(_)) => ScalarKind::Bool,
|
|
Some(Scalar::Timestamp(_)) => ScalarKind::Timestamp,
|
|
None => return Err(format!("axis {name}: empty value list")),
|
|
};
|
|
let homogeneous = values.iter().all(|v| {
|
|
matches!(
|
|
(v, kind),
|
|
(Scalar::I64(_), ScalarKind::I64)
|
|
| (Scalar::F64(_), ScalarKind::F64)
|
|
| (Scalar::Bool(_), ScalarKind::Bool)
|
|
| (Scalar::Timestamp(_), ScalarKind::Timestamp)
|
|
)
|
|
});
|
|
if !homogeneous {
|
|
return Err(format!(
|
|
"axis {name}: mixed value kinds in one axis (an axis declares its kind once)"
|
|
));
|
|
}
|
|
Ok(Axis { kind, values: values.to_vec() })
|
|
}
|
|
|
|
/// Translate a real-data blueprint sweep invocation into its two generated
|
|
/// documents. `inv.blueprint_canonical` is the canonical blueprint JSON already
|
|
/// stored by topology hash; its content id is the strategy ref.
|
|
pub(crate) fn translate_sweep(inv: &SugarInvocation) -> Result<GeneratedSweep, String> {
|
|
let process = ProcessDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Process,
|
|
name: "sweep".to_string(),
|
|
description: None,
|
|
pipeline: vec![StageBlock::Sweep { selection: None }],
|
|
};
|
|
let campaign = CampaignDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Campaign,
|
|
name: inv.name.clone(),
|
|
description: None,
|
|
data: DataSection {
|
|
instruments: inv.symbols.clone(),
|
|
windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }],
|
|
},
|
|
strategies: vec![StrategyEntry {
|
|
r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)),
|
|
axes: doc_axes_from(inv.axes)?,
|
|
}],
|
|
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
|
// No stop regime is bound for a dissolved single-sweep member (the
|
|
// dispatch passes `stop: None`; absent risk == empty risk, per the
|
|
// established parity). The other verbs bind the single Vol regime
|
|
// through this same seam.
|
|
risk: risk_from(inv.stop),
|
|
// No stage of a selection-free single-sweep pipeline consumes the
|
|
// seed; a fixed zero keeps generated bytes deterministic so identical
|
|
// invocations dedupe onto identical content ids.
|
|
seed: 0,
|
|
presentation: Presentation {
|
|
persist_taps: vec![],
|
|
emit: vec!["family_table".to_string()],
|
|
},
|
|
};
|
|
Ok(GeneratedSweep { process, campaign })
|
|
}
|
|
|
|
/// Auto-register both generated documents (content-addressed, idempotent).
|
|
/// Returns `(process_id, campaign_id)`.
|
|
pub(crate) fn register_generated(
|
|
reg: &aura_registry::Registry,
|
|
generated: &GeneratedSweep,
|
|
) -> 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))
|
|
}
|
|
|
|
/// The two generated documents of one dissolved `generalize` invocation.
|
|
#[derive(Debug)]
|
|
pub(crate) struct GeneratedGeneralize {
|
|
pub process: ProcessDoc,
|
|
pub campaign: CampaignDoc,
|
|
}
|
|
|
|
/// Translate one `aura generalize` invocation into its two generated documents:
|
|
/// a **selection-bearing** process (`[std::sweep(argmax), std::generalize]` —
|
|
/// generalize needs a nominee, so the sweep stage is selection-bearing, unlike
|
|
/// the selection-free single-sweep translator) and a campaign running the one
|
|
/// fixed candidate (single-value raw axes) across all instruments under the
|
|
/// invocation's single risk regime. `inv.blueprint_canonical` is the user's
|
|
/// blueprint already stored by topology hash; its content id is the strategy ref.
|
|
pub(crate) fn translate_generalize(
|
|
inv: &SugarInvocation,
|
|
metric: &str,
|
|
) -> Result<GeneratedGeneralize, String> {
|
|
let process = ProcessDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Process,
|
|
name: "generalize".to_string(),
|
|
description: None,
|
|
pipeline: vec![
|
|
StageBlock::Sweep {
|
|
selection: Some(SweepSelection {
|
|
metric: metric.to_string(),
|
|
select: SelectRule::Argmax,
|
|
deflate: false,
|
|
}),
|
|
},
|
|
StageBlock::Generalize { metric: metric.to_string() },
|
|
],
|
|
};
|
|
let campaign = CampaignDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Campaign,
|
|
name: inv.name.clone(),
|
|
description: None,
|
|
data: DataSection {
|
|
instruments: inv.symbols.clone(),
|
|
windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }],
|
|
},
|
|
strategies: vec![StrategyEntry {
|
|
r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)),
|
|
axes: doc_axes_from(inv.axes)?,
|
|
}],
|
|
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
|
// The stop is a single structural risk regime; the member runner maps
|
|
// it back to StopRule::Vol at run time.
|
|
risk: risk_from(inv.stop),
|
|
seed: 0,
|
|
presentation: Presentation {
|
|
persist_taps: vec![],
|
|
emit: vec!["family_table".to_string()],
|
|
},
|
|
};
|
|
Ok(GeneratedGeneralize { process, campaign })
|
|
}
|
|
|
|
/// Auto-register both generated generalize documents (content-addressed,
|
|
/// idempotent). Returns `(process_id, campaign_id)`.
|
|
pub(crate) fn register_generated_g(
|
|
reg: &aura_registry::Registry,
|
|
generated: &GeneratedGeneralize,
|
|
) -> 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))
|
|
}
|
|
|
|
/// Validate a generated `(process, campaign)` pair BEFORE anything is
|
|
/// registered (#210 c0110 fieldtest, "store litter" finding): a referential
|
|
/// refusal must never leave a dead generated document in the content-
|
|
/// addressed store. Defensive doc-shape checks first (generated docs should
|
|
/// always pass this — the translators build them by construction), then the
|
|
/// executable-shape preflight, then the axis-binding check against the
|
|
/// blueprint's own (wrapped) open param space via the pure `bind_axes` seam
|
|
/// (campaign_run.rs) — the referential shape the dispatch-boundary name
|
|
/// preflight (main.rs) does not catch: a subset of axes that leaves an open
|
|
/// param unbound (probe P3). `probe_params` is any one grid value per axis
|
|
/// (checks NAME coverage/uniqueness, never the bound value). Shared by both
|
|
/// dissolved-verb sugars: one seam enforcing the store-litter invariant,
|
|
/// rather than two drift-prone copies.
|
|
fn validate_before_register(
|
|
process: &ProcessDoc,
|
|
campaign: &CampaignDoc,
|
|
blueprint_canonical: &str,
|
|
probe_params: &[(String, Scalar)],
|
|
env: &crate::project::Env,
|
|
) -> Result<(), String> {
|
|
let doc_faults = aura_research::validate_campaign(campaign);
|
|
if !doc_faults.is_empty() {
|
|
return Err(crate::research_docs::fault_block(
|
|
"generated campaign document invalid:",
|
|
doc_faults.iter().map(crate::research_docs::doc_fault_prose).collect(),
|
|
));
|
|
}
|
|
let process_faults = aura_research::validate_process(process);
|
|
if !process_faults.is_empty() {
|
|
return Err(crate::research_docs::fault_block(
|
|
"generated process document invalid:",
|
|
process_faults.iter().map(crate::research_docs::doc_fault_prose).collect(),
|
|
));
|
|
}
|
|
aura_campaign::preflight(process, campaign).map_err(|f| crate::campaign_run::exec_fault_prose(&f))?;
|
|
|
|
let space = crate::blueprint_axis_probe(blueprint_canonical, env).param_space();
|
|
let strategy_id = content_id_of(blueprint_canonical);
|
|
crate::campaign_run::bind_axes(&space, &strategy_id, probe_params)
|
|
.map(|_| ())
|
|
.map_err(|f| crate::campaign_run::exec_fault_prose(&aura_campaign::ExecFault::Member(f)))
|
|
}
|
|
|
|
/// Run one dissolved sweep invocation end-to-end: register the generated
|
|
/// documents, then execute through the one campaign path in sugar mode.
|
|
pub(crate) fn run_sweep_sugar(
|
|
inv: &SugarInvocation,
|
|
env: &crate::project::Env,
|
|
) -> Result<(), String> {
|
|
let generated = translate_sweep(inv)?;
|
|
let probe_params = probe_params_from(inv.axes);
|
|
validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?;
|
|
let reg = env.registry();
|
|
let (_process_id, campaign_id) = register_generated(®, &generated)?;
|
|
crate::campaign_run::run_campaign_by_id(&campaign_id, env, crate::campaign_run::RunPresentation::MemberLinesOnly)
|
|
}
|
|
|
|
/// Build the `CrossInstrument` family members from an executed generalize
|
|
/// campaign: each cell's nominee `RunReport`, in cell (instrument doc) order —
|
|
/// the same order + shape the inline `run_generalize` built its `members` in,
|
|
/// each already carrying `manifest.instrument`. Debug-asserts one member per
|
|
/// cell: generalize's pipeline has no gate stage, so every cell nominates —
|
|
/// a cell silently missing its nominee here would be a campaign-executor
|
|
/// regression, not a legitimate generalize outcome, and the filter_map below
|
|
/// must not swallow it unnoticed.
|
|
fn cross_instrument_members(
|
|
outcome: &aura_campaign::CampaignOutcome,
|
|
) -> Vec<aura_engine::RunReport> {
|
|
let members: Vec<aura_engine::RunReport> = outcome
|
|
.cells
|
|
.iter()
|
|
.filter_map(|c| c.nominee.as_ref().map(|(_, report)| report.clone()))
|
|
.collect();
|
|
debug_assert_eq!(
|
|
members.len(),
|
|
outcome.cells.len(),
|
|
"generalize has no gate stage — every cell must nominate"
|
|
);
|
|
members
|
|
}
|
|
|
|
/// Run one dissolved `generalize` invocation end-to-end: register the generated
|
|
/// documents, run through the one campaign path, then reprint today's exact
|
|
/// `{"generalize":{…}}` + `{"family_id":…}` lines from the recorded outcome and
|
|
/// persist the `CrossInstrument` family. The stdout is byte-identical to the
|
|
/// retired welded path (the committed exact-grade anchor is the gate).
|
|
pub(crate) fn run_generalize_sugar(
|
|
inv: &SugarInvocation,
|
|
metric: &str,
|
|
env: &crate::project::Env,
|
|
) -> Result<(), String> {
|
|
let generated = translate_generalize(inv, metric)?;
|
|
let probe_params = probe_params_from(inv.axes);
|
|
validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?;
|
|
let reg = env.registry();
|
|
let (_process_id, campaign_id) = register_generated_g(®, &generated)?;
|
|
let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?;
|
|
|
|
// Reprint the verb's aggregate line from the recorded cross-instrument
|
|
// grade — byte-identical to the retired welded path's `generalize_json(&agg)`.
|
|
let cg = run
|
|
.outcome
|
|
.record
|
|
.generalizations
|
|
.iter()
|
|
.find_map(|g| g.generalization.as_ref())
|
|
.ok_or("generalize produced no cross-instrument grade")?;
|
|
println!("{}", crate::generalize_json(cg));
|
|
|
|
let members = cross_instrument_members(&run.outcome);
|
|
let family_id = reg
|
|
.append_family(&inv.name, aura_registry::FamilyKind::CrossInstrument, &members)
|
|
.map_err(|e| e.to_string())?;
|
|
println!("{{\"family_id\":\"{family_id}\"}}");
|
|
Ok(())
|
|
}
|
|
|
|
/// The two generated documents of one dissolved `walkforward` invocation.
|
|
#[derive(Debug)]
|
|
pub(crate) struct GeneratedWalkforward {
|
|
pub process: ProcessDoc,
|
|
pub campaign: CampaignDoc,
|
|
}
|
|
|
|
/// Translate one `aura walkforward --real` invocation into its two generated
|
|
/// documents: a `[std::sweep(argmax), std::walk_forward]` process (the sweep
|
|
/// only enumerates the IS-refit survivor grid for the wf stage; its recorded
|
|
/// argmax selection is not part of the summary) and a campaign running the
|
|
/// invocation's axis grid over one instrument under its single risk regime.
|
|
/// The roller sizes come from `w` (ms). `inv.blueprint_canonical` is the user's
|
|
/// blueprint already stored by topology hash; its content id is the strategy ref.
|
|
pub(crate) fn translate_walkforward(
|
|
inv: &SugarInvocation,
|
|
metric: &str,
|
|
w: WfWindows,
|
|
select: SelectRule,
|
|
) -> Result<GeneratedWalkforward, String> {
|
|
let process = ProcessDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Process,
|
|
name: "walkforward".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: w.in_sample_ms,
|
|
out_of_sample_ms: w.out_of_sample_ms,
|
|
step_ms: w.step_ms,
|
|
mode: WfMode::Rolling,
|
|
metric: metric.to_string(),
|
|
select,
|
|
},
|
|
],
|
|
};
|
|
let campaign = CampaignDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Campaign,
|
|
name: inv.name.clone(),
|
|
description: None,
|
|
data: DataSection {
|
|
instruments: inv.symbols.clone(),
|
|
windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }],
|
|
},
|
|
strategies: vec![StrategyEntry {
|
|
r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)),
|
|
axes: doc_axes_from(inv.axes)?,
|
|
}],
|
|
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
|
risk: risk_from(inv.stop),
|
|
seed: 0,
|
|
presentation: Presentation {
|
|
persist_taps: vec![],
|
|
emit: vec!["family_table".to_string()],
|
|
},
|
|
};
|
|
Ok(GeneratedWalkforward { process, campaign })
|
|
}
|
|
|
|
/// Auto-register both generated walkforward documents (content-addressed,
|
|
/// idempotent). Returns `(process_id, campaign_id)`.
|
|
pub(crate) fn register_generated_wf(
|
|
reg: &aura_registry::Registry,
|
|
generated: &GeneratedWalkforward,
|
|
) -> 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 `walkforward` invocation end-to-end: register the generated
|
|
/// documents, run through the one campaign path, then reprint the per-window member
|
|
/// lines + the summary line from the recorded `WalkForward` family. The stdout
|
|
/// summary line is byte-identical to the retired welded path (the committed
|
|
/// exact-grade anchor is the gate).
|
|
pub(crate) fn run_walkforward_sugar(
|
|
inv: &SugarInvocation,
|
|
metric: &str,
|
|
w: WfWindows,
|
|
select: SelectRule,
|
|
env: &crate::project::Env,
|
|
) -> Result<(), String> {
|
|
let generated = translate_walkforward(inv, metric, w, select)?;
|
|
let probe_params = probe_params_from(inv.axes);
|
|
validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?;
|
|
let reg = env.registry();
|
|
let (_process_id, campaign_id) = register_generated_wf(®, &generated)?;
|
|
let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?;
|
|
|
|
// The single cell's walk_forward StageFamily.
|
|
let wf = run
|
|
.outcome
|
|
.cells
|
|
.iter()
|
|
.flat_map(|c| c.families.iter())
|
|
.find(|f| f.block == "std::walk_forward")
|
|
.ok_or("walkforward produced no walk_forward family")?;
|
|
|
|
for report in &wf.reports {
|
|
println!("{}", crate::family_member_line(&wf.family_id, report));
|
|
}
|
|
// Summary axes: the invocation's raw axis names in argv order, then the
|
|
// stop columns when a regime is bound (#220 — the renderer hardcodes none).
|
|
let mut summary_axes: Vec<String> = inv.axes.iter().map(|(n, _)| n.clone()).collect();
|
|
if inv.stop.is_some() {
|
|
summary_axes.push("stop_length".to_string());
|
|
summary_axes.push("stop_k".to_string());
|
|
}
|
|
println!("{}", crate::walkforward_summary_json_from_reports(&wf.reports, &summary_axes));
|
|
Ok(())
|
|
}
|
|
|
|
/// 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 --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 invocation's axis grid over one instrument
|
|
/// under its single risk regime. Unlike `translate_walkforward` (which hardcodes
|
|
/// `seed: 0`), `translate_mc` sets `campaign.seed = mc.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 retired inline bootstrap.
|
|
pub(crate) fn translate_mc(
|
|
inv: &SugarInvocation,
|
|
metric: &str,
|
|
w: WfWindows,
|
|
mc: McKnobs,
|
|
) -> 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: w.in_sample_ms,
|
|
out_of_sample_ms: w.out_of_sample_ms,
|
|
step_ms: w.step_ms,
|
|
mode: WfMode::Rolling,
|
|
metric: metric.to_string(),
|
|
select: SelectRule::Argmax,
|
|
},
|
|
StageBlock::MonteCarlo { resamples: mc.resamples, block_len: mc.block_len },
|
|
],
|
|
};
|
|
let campaign = CampaignDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Campaign,
|
|
name: inv.name.clone(),
|
|
description: None,
|
|
data: DataSection {
|
|
instruments: inv.symbols.clone(),
|
|
windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }],
|
|
},
|
|
strategies: vec![StrategyEntry {
|
|
r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)),
|
|
axes: doc_axes_from(inv.axes)?,
|
|
}],
|
|
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
|
risk: risk_from(inv.stop),
|
|
seed: mc.seed,
|
|
presentation: Presentation {
|
|
persist_taps: vec![],
|
|
emit: vec!["family_table".to_string()],
|
|
},
|
|
};
|
|
Ok(GeneratedMc { process, campaign })
|
|
}
|
|
|
|
/// 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 retired welded 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.
|
|
pub(crate) fn run_mc_sugar(
|
|
inv: &SugarInvocation,
|
|
metric: &str,
|
|
w: WfWindows,
|
|
mc: McKnobs,
|
|
env: &crate::project::Env,
|
|
) -> Result<(), String> {
|
|
let generated = translate_mc(inv, metric, w, mc)?;
|
|
let probe_params = probe_params_from(inv.axes);
|
|
validate_before_register(&generated.process, &generated.campaign, inv.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(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn axes() -> Vec<(String, Vec<Scalar>)> {
|
|
vec![
|
|
("fast.length".to_string(), vec![Scalar::I64(2), Scalar::I64(4)]),
|
|
("slow.length".to_string(), vec![Scalar::I64(8), Scalar::I64(16)]),
|
|
]
|
|
}
|
|
|
|
fn inv<'a>(
|
|
axes: &'a [(String, Vec<Scalar>)],
|
|
name: &str,
|
|
symbols: &[&str],
|
|
stop: Option<VolStop>,
|
|
bp: &'a str,
|
|
) -> SugarInvocation<'a> {
|
|
SugarInvocation {
|
|
axes,
|
|
name: name.to_string(),
|
|
symbols: symbols.iter().map(|s| s.to_string()).collect(),
|
|
from_ms: 100,
|
|
to_ms: 200,
|
|
blueprint_canonical: bp,
|
|
stop,
|
|
}
|
|
}
|
|
|
|
fn g_axes() -> Vec<(String, Vec<Scalar>)> {
|
|
vec![
|
|
("fast.length".to_string(), vec![Scalar::i64(3)]),
|
|
("slow.length".to_string(), vec![Scalar::i64(12)]),
|
|
]
|
|
}
|
|
|
|
fn wf_axes() -> Vec<(String, Vec<Scalar>)> {
|
|
vec![
|
|
("fast.length".to_string(), vec![Scalar::i64(3), Scalar::i64(5)]),
|
|
("slow.length".to_string(), vec![Scalar::i64(12), Scalar::i64(20)]),
|
|
]
|
|
}
|
|
|
|
const WF_W: WfWindows = WfWindows {
|
|
in_sample_ms: 7_776_000_000,
|
|
out_of_sample_ms: 2_592_000_000,
|
|
step_ms: 2_592_000_000,
|
|
};
|
|
|
|
#[test]
|
|
fn translate_sweep_is_deterministic_and_names_flow() {
|
|
let ax = axes();
|
|
let i = inv(&ax, "probe", &["GER40"], None, "{\"bp\":1}");
|
|
let a = translate_sweep(&i).unwrap();
|
|
let b = translate_sweep(&i).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"
|
|
);
|
|
assert_eq!(a.campaign.name, "probe");
|
|
assert_eq!(a.campaign.seed, 0);
|
|
assert_eq!(a.process.name, "sweep");
|
|
assert_eq!(a.process.pipeline, vec![StageBlock::Sweep { selection: None }]);
|
|
assert_eq!(a.campaign.data.instruments, vec!["GER40".to_string()]);
|
|
assert_eq!(a.campaign.data.windows, vec![Window { from_ms: 100, to_ms: 200 }]);
|
|
assert_eq!(a.campaign.presentation.emit, vec!["family_table".to_string()]);
|
|
assert!(a.campaign.presentation.persist_taps.is_empty());
|
|
// the process ref is the content id of the generated process bytes
|
|
assert_eq!(
|
|
a.campaign.process.r#ref,
|
|
DocRef::ContentId(content_id_of(&process_to_json(&a.process)))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn translate_sweep_refuses_a_mixed_kind_axis() {
|
|
let mixed = vec![("k".to_string(), vec![Scalar::I64(1), Scalar::F64(2.5)])];
|
|
let err = translate_sweep(&inv(&mixed, "n", &["GER40"], None, "{}")).unwrap_err();
|
|
assert!(err.contains("mixed value kinds"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn generated_campaign_validates_intrinsically_and_preflights() {
|
|
let ax = axes();
|
|
let g = translate_sweep(&inv(&ax, "probe", &["GER40"], None, "{\"bp\":1}")).unwrap();
|
|
assert!(aura_research::validate_campaign(&g.campaign).is_empty());
|
|
assert!(aura_research::validate_process(&g.process).is_empty());
|
|
assert!(aura_campaign::preflight(&g.process, &g.campaign).is_ok());
|
|
}
|
|
|
|
/// The one seam all four verbs share (#220): `stop: Some(VolStop)` binds the
|
|
/// single Vol regime; `stop: None` binds no regime (sweep's contract).
|
|
#[test]
|
|
fn sugar_invocation_stop_maps_to_the_vol_regime() {
|
|
let ax = axes();
|
|
let mut i = inv(&ax, "n", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
|
|
let with = translate_sweep(&i).unwrap();
|
|
assert_eq!(with.campaign.risk, vec![RiskRegime::Vol { length: 14, k: 2.0 }]);
|
|
i.stop = None;
|
|
let without = translate_sweep(&i).unwrap();
|
|
assert!(without.campaign.risk.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn translate_generalize_is_deterministic_and_carries_the_regime() {
|
|
let ax = g_axes();
|
|
let i = inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
|
|
let a = translate_generalize(&i, "expectancy_r").unwrap();
|
|
let b = translate_generalize(&i, "expectancy_r").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"
|
|
);
|
|
// A selection-bearing pipeline: sweep(argmax) then generalize.
|
|
assert_eq!(
|
|
a.process.pipeline,
|
|
vec![
|
|
StageBlock::Sweep {
|
|
selection: Some(SweepSelection {
|
|
metric: "expectancy_r".to_string(),
|
|
select: SelectRule::Argmax,
|
|
deflate: false,
|
|
})
|
|
},
|
|
StageBlock::Generalize { metric: "expectancy_r".to_string() },
|
|
]
|
|
);
|
|
// The stop rides a single risk regime (the structural axis).
|
|
assert_eq!(a.campaign.risk, vec![RiskRegime::Vol { length: 14, k: 2.0 }]);
|
|
// All instruments under one campaign; the candidate as single-value axes.
|
|
assert_eq!(
|
|
a.campaign.data.instruments,
|
|
vec!["GER40".to_string(), "USDJPY".to_string()]
|
|
);
|
|
assert_eq!(a.campaign.strategies.len(), 1);
|
|
assert!(a.campaign.strategies[0].axes.contains_key("fast.length"));
|
|
assert!(a.campaign.strategies[0].axes.contains_key("slow.length"));
|
|
assert_eq!(a.process.name, "generalize");
|
|
assert_eq!(a.campaign.name, "generalize");
|
|
// Intrinsically valid + preflights (like the sweep sibling).
|
|
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());
|
|
}
|
|
|
|
/// Content-addressing correctness for the risk-regime axis (#210 T1-T2):
|
|
/// two invocations that differ ONLY in the protective stop (`stop_length`/
|
|
/// `stop_k`) must generate campaigns with DIFFERENT content ids. The stop
|
|
/// is now carried through `CampaignDoc.risk` rather than as an opaque
|
|
/// runtime constant, so a hashing/serialization bug that dropped or
|
|
/// normalized-away the regime would silently collide two distinct
|
|
/// invocations onto one cached campaign — the wrong grade would be served
|
|
/// for one of them without any error. This pins that a regime change is
|
|
/// visible in the generated bytes, both against a same-length,
|
|
/// different-k pair and a different-length, same-k pair.
|
|
#[test]
|
|
fn translate_generalize_diverging_regimes_do_not_collide_content_ids() {
|
|
let ax = g_axes();
|
|
let base = translate_generalize(&inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "expectancy_r").unwrap();
|
|
let different_k = translate_generalize(&inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "expectancy_r").unwrap();
|
|
let different_length = translate_generalize(&inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "expectancy_r").unwrap();
|
|
let base_id = content_id_of(&campaign_to_json(&base.campaign));
|
|
let k_id = content_id_of(&campaign_to_json(&different_k.campaign));
|
|
let length_id = content_id_of(&campaign_to_json(&different_length.campaign));
|
|
assert_ne!(base_id, k_id, "a different stop_k must not collide onto the same campaign id");
|
|
assert_ne!(
|
|
base_id, length_id,
|
|
"a different stop_length must not collide onto the same campaign id"
|
|
);
|
|
assert_ne!(k_id, length_id, "the two diverging regimes must not collide with each other");
|
|
}
|
|
|
|
/// The translator's output is not merely intrinsically valid in isolation
|
|
/// (preflight/validate never touch a store) — `register_generated_g` must
|
|
/// actually persist documents the campaign can resolve through a real
|
|
/// `Registry`: the campaign's `process.ref` content id must fetch back the
|
|
/// EXACT process the translator produced, and the returned `campaign_id`
|
|
/// must fetch back the exact campaign. A drift between what gets hashed
|
|
/// into the ref and what gets stored (e.g. a canonicalization mismatch)
|
|
/// would pass every static check here yet leave the campaign permanently
|
|
/// unresolvable at real run time — this is the property that would catch
|
|
/// that silently, before the dispatch rewire ever exercises the path live.
|
|
#[test]
|
|
fn register_generated_g_stores_documents_the_campaign_can_actually_resolve() {
|
|
let dir = std::env::temp_dir().join(format!(
|
|
"aura-verb-sugar-generalize-registry-test-{}",
|
|
std::process::id()
|
|
));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let reg = aura_registry::Registry::open(dir.join("runs.jsonl"));
|
|
|
|
let ax = g_axes();
|
|
let generated = translate_generalize(
|
|
&inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"),
|
|
"expectancy_r",
|
|
)
|
|
.unwrap();
|
|
let (process_id, campaign_id) = register_generated_g(®, &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!("generalize'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);
|
|
}
|
|
|
|
#[test]
|
|
fn translate_walkforward_is_deterministic_and_carries_the_regime() {
|
|
let ax = wf_axes();
|
|
let i = inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
|
|
let a = translate_walkforward(&i, "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
|
|
let b = translate_walkforward(&i, "sqn_normalized", WF_W, SelectRule::Argmax).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)
|
|
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,
|
|
},
|
|
]
|
|
);
|
|
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);
|
|
// multi-value IS-refit grid axes
|
|
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, "walkforward");
|
|
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_walkforward_diverging_regimes_do_not_collide_content_ids() {
|
|
let ax = wf_axes();
|
|
let base = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
|
|
let different_k = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
|
|
let different_length = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap();
|
|
let base_id = content_id_of(&campaign_to_json(&base.campaign));
|
|
let k_id = content_id_of(&campaign_to_json(&different_k.campaign));
|
|
let length_id = content_id_of(&campaign_to_json(&different_length.campaign));
|
|
assert_ne!(base_id, k_id, "a different stop_k must not collide onto the same campaign id");
|
|
assert_ne!(
|
|
base_id, length_id,
|
|
"a different stop_length must not collide onto the same campaign id"
|
|
);
|
|
assert_ne!(k_id, length_id, "the two diverging regimes must not collide with each other");
|
|
}
|
|
|
|
#[test]
|
|
fn register_generated_wf_stores_documents_the_campaign_can_actually_resolve() {
|
|
let dir = std::env::temp_dir().join(format!(
|
|
"aura-verb-sugar-walkforward-registry-test-{}",
|
|
std::process::id()
|
|
));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let reg = aura_registry::Registry::open(dir.join("runs.jsonl"));
|
|
let ax = wf_axes();
|
|
let generated = translate_walkforward(
|
|
&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"),
|
|
"sqn_normalized",
|
|
WF_W,
|
|
SelectRule::Argmax,
|
|
)
|
|
.unwrap();
|
|
let (process_id, campaign_id) = register_generated_wf(®, &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!("walkforward'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);
|
|
}
|
|
|
|
#[test]
|
|
fn translate_mc_is_deterministic_and_carries_the_regime_and_seed() {
|
|
let ax = wf_axes();
|
|
let i = inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}");
|
|
let a = translate_mc(&i, "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap();
|
|
let b = translate_mc(&i, "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).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 ax = wf_axes();
|
|
let base = translate_mc(&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap();
|
|
let different_seed = translate_mc(&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 7 }).unwrap();
|
|
let different_k = translate_mc(&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).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 ax = wf_axes();
|
|
let generated = translate_mc(
|
|
&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"),
|
|
"sqn_normalized",
|
|
WF_W,
|
|
McKnobs { resamples: 1000, block_len: 5, seed: 42 },
|
|
)
|
|
.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);
|
|
}
|
|
}
|