feat(campaign): expand the campaign matrix over risk regimes (#210 T3)
The regime becomes the fourth structural coordinate of the campaign matrix, a
kept-separate axis (a peer of window, not aggregated-over like instrument):
- aura-campaign: CellSpec gains `regime` + `regime_ordinal`; the executor
resolves the regime list (absent/empty risk -> one default cell, ordinal 0),
runs a cell per (strategy, instrument, window, regime), and keys the nominee
map by the 3-tuple including the regime ordinal — so generalize aggregates
across instruments WITHIN a regime, never across regimes. Family names gain a
`-r{regime_ordinal}` discriminator so two regimes' families cannot collide.
- aura-registry: `CampaignGeneralization` gains `regime_ordinal` (with
`#[serde(default)]`, so pre-feature stored records still parse — pinned by a
new backward-compat test).
The existing single-regime execute tests shift their family ids by `-r0`; a new
test pins that two regimes yield distinct `-r0`/`-r1` families. `CellSpec.regime`
is write-only in this slice — its sole reader is the deferred member runner
(the next slice) — a held quality nit, seam-prep as the plan prescribes. Full
workspace suite green.
refs #210
This commit is contained in:
@@ -1852,6 +1852,7 @@ mod tests {
|
||||
generalizations: vec![CampaignGeneralization {
|
||||
strategy_ordinal: 0,
|
||||
window_ordinal: 0,
|
||||
regime_ordinal: 0,
|
||||
generalization: Some(Generalization {
|
||||
selection_metric: "net_expectancy_r".to_string(),
|
||||
n_instruments: 2,
|
||||
@@ -1906,6 +1907,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#210 T3, C14/C23 widening convention): a stored
|
||||
/// `campaign_runs.jsonl` line whose `generalizations` entries predate the
|
||||
/// risk-regime axis — carrying `strategy_ordinal`/`window_ordinal`/
|
||||
/// `winners`/`missing` but no `regime_ordinal` key at all — still parses,
|
||||
/// with `regime_ordinal` defaulting to 0 (`#[serde(default)]` on
|
||||
/// `CampaignGeneralization::regime_ordinal`). Without that default, every
|
||||
/// campaign-run store written before this iteration would fail to load
|
||||
/// the instant it contained a recorded generalization.
|
||||
#[test]
|
||||
fn campaign_run_generalization_without_regime_ordinal_still_parses() {
|
||||
let path = temp_family_dir("campaign_run_pre_regime_ordinal");
|
||||
let store = path.with_file_name("campaign_runs.jsonl");
|
||||
let line = r#"{"campaign":"cafe","process":"beef","run":0,"seed":7,"cells":[{"strategy":"3f9c","instrument":"EURUSD","window_ms":[0,1000],"stages":[{"block":"std::gate","survivor_ordinals":[0,2]}]}],"generalizations":[{"strategy_ordinal":0,"window_ordinal":0,"winners":[["EURUSD",[["fast.length",{"I64":3}]]]],"missing":["GER40"]}]}"#;
|
||||
fs::write(&store, format!("{line}\n")).expect("write pre-regime-ordinal line");
|
||||
let reg = Registry::open(&path);
|
||||
let loaded = reg.load_campaign_runs().expect("pre-regime-ordinal generalization parses");
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(loaded[0].generalizations.len(), 1);
|
||||
assert_eq!(
|
||||
loaded[0].generalizations[0].regime_ordinal, 0,
|
||||
"a missing regime_ordinal key defaults to 0, not a parse failure"
|
||||
);
|
||||
assert_eq!(loaded[0].generalizations[0].strategy_ordinal, 0);
|
||||
assert_eq!(loaded[0].generalizations[0].missing, vec!["GER40".to_string()]);
|
||||
}
|
||||
|
||||
/// The 0109 name composition (#201 d5 / the spec's seam note): a `Some`
|
||||
/// trace_name on the input record — the executor's claim sentinel,
|
||||
/// content ignored — is replaced on the STORED line with the derived
|
||||
|
||||
@@ -158,6 +158,11 @@ pub enum StageBootstrap {
|
||||
pub struct CampaignGeneralization {
|
||||
pub strategy_ordinal: usize,
|
||||
pub window_ordinal: usize,
|
||||
/// The regime ordinal (0 for the default regime) — the third structural
|
||||
/// coordinate, so a generalize grade is unambiguous per (strategy, window,
|
||||
/// regime). `#[serde(default)]` keeps pre-feature stored records parsing.
|
||||
#[serde(default)]
|
||||
pub regime_ordinal: usize,
|
||||
/// None when fewer than 2 instrument cells produced a winner (gate
|
||||
/// truncation) — the shortfall is recorded, never computed around.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
||||
Reference in New Issue
Block a user