fix(campaign): thread the risk regime through trace persistence
The persist-side C1 drift alarm re-ran every nominee under the hardcoded
default vol-stop because CellRealization dropped the regime at the record
boundary — a non-default risk regime + persist taps false-failed C1 and
refused to persist a correct trace; two regimes over the same cell would
also have collided in one trace dir.
Fix at the record boundary: CellRealization carries regime + regime_ordinal
(serde-default + skip-serializing keeps pre-existing stored records
byte-stable), populated where the cell is realized; the regime->StopRule
binding is single-sourced in stop_rule_for_regime, shared by the member
runner and the persist re-run so the two sites cannot drift apart again;
campaign_cell_key appends -r{regime_ordinal} for non-default regimes
(default keys unchanged, no stored dir renamed).
closes #219, closes #212
This commit is contained in:
@@ -459,6 +459,8 @@ fn run_cell(
|
||||
strategy: cell.strategy_id.clone(),
|
||||
instrument: cell.instrument.clone(),
|
||||
window_ms: cell.window_ms,
|
||||
regime: cell.regime,
|
||||
regime_ordinal: cell.regime_ordinal,
|
||||
stages,
|
||||
},
|
||||
))
|
||||
|
||||
@@ -43,6 +43,23 @@ pub(crate) fn is_content_id(s: &str) -> bool {
|
||||
s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
|
||||
}
|
||||
|
||||
/// The one regime->StopRule binding, shared by `CliMemberRunner::run_member`
|
||||
/// and `persist_campaign_traces`'s drift-alarm re-run (#219): `None` is the
|
||||
/// default vol-stop, `Some(RiskRegime::Vol { .. })` binds that regime's own
|
||||
/// params. Single-sourced so the persist-side re-run structurally cannot
|
||||
/// diverge from the run-side binding again (the #219 divergence class).
|
||||
fn stop_rule_for_regime(regime: Option<aura_research::RiskRegime>) -> aura_composites::StopRule {
|
||||
match regime {
|
||||
None => aura_composites::StopRule::Vol {
|
||||
length: crate::R_SMA_STOP_LENGTH,
|
||||
k: crate::R_SMA_STOP_K,
|
||||
},
|
||||
Some(aura_research::RiskRegime::Vol { length, k }) => {
|
||||
aura_composites::StopRule::Vol { length, k }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Phrase an [`ExecFault`] for stderr — Debug-leak-free, path-addressed
|
||||
/// (stage index + block id), the `doc_fault_prose`/`ref_fault_prose` register.
|
||||
pub(crate) fn exec_fault_prose(f: &ExecFault) -> String {
|
||||
@@ -268,15 +285,7 @@ impl MemberRunner for CliMemberRunner<'_> {
|
||||
// id, project provenance stamped inside the helper.
|
||||
let signal = blueprint_from_json(&cell.blueprint_json, &|t| self.env.resolve(t))
|
||||
.expect("stored blueprint passed the referential gate; reload is infallible");
|
||||
let stop = match cell.regime {
|
||||
None => aura_composites::StopRule::Vol {
|
||||
length: crate::R_SMA_STOP_LENGTH,
|
||||
k: crate::R_SMA_STOP_K,
|
||||
},
|
||||
Some(aura_research::RiskRegime::Vol { length, k }) => {
|
||||
aura_composites::StopRule::Vol { length, k }
|
||||
}
|
||||
};
|
||||
let stop = stop_rule_for_regime(cell.regime);
|
||||
let mut report = crate::run_blueprint_member(
|
||||
signal,
|
||||
&point,
|
||||
@@ -608,9 +617,22 @@ fn tap_channel(tap: &str) -> Option<TapChannel> {
|
||||
/// `traces/<trace_name>/` — the `member_key` discipline (content, never a
|
||||
/// runtime ordinal): strategy content-id prefix + instrument +
|
||||
/// doc-positional window ordinal, sanitized to one portable path component.
|
||||
fn campaign_cell_key(strategy: &str, instrument: &str, window_ordinal: usize) -> String {
|
||||
/// `regime_ordinal` (#219/#212) discriminates two risk regimes over the same
|
||||
/// (strategy, instrument, window) cell so their traces never collide: the
|
||||
/// default regime (ordinal 0) keeps the pre-#219 key unchanged (no stored
|
||||
/// trace dir is renamed by this change), a non-default regime appends
|
||||
/// `-r{ordinal}`.
|
||||
fn campaign_cell_key(
|
||||
strategy: &str,
|
||||
instrument: &str,
|
||||
window_ordinal: usize,
|
||||
regime_ordinal: usize,
|
||||
) -> String {
|
||||
let strategy8 = strategy.get(..8).unwrap_or(strategy);
|
||||
crate::sanitize_component(&format!("{strategy8}-{instrument}-w{window_ordinal}"))
|
||||
let base = format!("{strategy8}-{instrument}-w{window_ordinal}");
|
||||
let base =
|
||||
if regime_ordinal == 0 { base } else { format!("{base}-r{regime_ordinal}") };
|
||||
crate::sanitize_component(&base)
|
||||
}
|
||||
|
||||
/// Persist the requested `persist_taps` for every nominee cell under
|
||||
@@ -684,8 +706,12 @@ fn persist_campaign_traces(
|
||||
cell_rec.window_ms.0, cell_rec.window_ms.1
|
||||
)
|
||||
})?;
|
||||
let cell_key =
|
||||
campaign_cell_key(&cell_rec.strategy, &cell_rec.instrument, window_ordinal);
|
||||
let cell_key = campaign_cell_key(
|
||||
&cell_rec.strategy,
|
||||
&cell_rec.instrument,
|
||||
window_ordinal,
|
||||
cell_rec.regime_ordinal,
|
||||
);
|
||||
|
||||
// The cell's blueprint: the run's own index-aligned resolution, by
|
||||
// content id (exactly the bytes the executor's members ran).
|
||||
@@ -728,23 +754,17 @@ fn persist_campaign_traces(
|
||||
}
|
||||
let signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t))
|
||||
.expect("stored blueprint passed the referential gate; reload is infallible");
|
||||
// The cell's OWN regime (#219/#212), not the hardcoded default: the
|
||||
// recorded nominee ran under `cell_rec.regime`, bound through the same
|
||||
// `stop_rule_for_regime` helper `CliMemberRunner::run_member` uses, so
|
||||
// the two sites cannot drift apart again (the #219 divergence class)
|
||||
// and the re-run binds the same stop the C1 drift alarm below relies on.
|
||||
let stop = stop_rule_for_regime(cell_rec.regime);
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let (tx_r, rx_r) = mpsc::channel();
|
||||
let (tx_req, rx_req) = mpsc::channel();
|
||||
let mut h = crate::wrap_r(
|
||||
signal,
|
||||
tx_eq,
|
||||
tx_ex,
|
||||
tx_r,
|
||||
tx_req,
|
||||
aura_composites::StopRule::Vol {
|
||||
length: crate::R_SMA_STOP_LENGTH,
|
||||
k: crate::R_SMA_STOP_K,
|
||||
},
|
||||
false,
|
||||
None,
|
||||
)
|
||||
let mut h = crate::wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, false, None)
|
||||
.bootstrap_with_cells(&point)
|
||||
.expect("the nominee's point re-bootstraps (it already ran this realization)");
|
||||
let sources: Vec<Box<dyn aura_engine::Source>> = vec![Box::new(source)];
|
||||
@@ -957,7 +977,7 @@ mod tests {
|
||||
/// instrument + doc-positional window ordinal, one portable component.
|
||||
fn campaign_cell_key_is_content_derived() {
|
||||
let strategy = "bb34aa55".repeat(8); // a 64-hex content id
|
||||
assert_eq!(campaign_cell_key(&strategy, "GER40", 0), "bb34aa55-GER40-w0");
|
||||
assert_eq!(campaign_cell_key(&strategy, "GER40", 0, 0), "bb34aa55-GER40-w0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -966,7 +986,17 @@ mod tests {
|
||||
/// path or an invalid directory name.
|
||||
fn campaign_cell_key_sanitizes_non_portable_bytes() {
|
||||
let strategy = "0123abcd".repeat(8);
|
||||
assert_eq!(campaign_cell_key(&strategy, "GER/40", 3), "0123abcd-GER_40-w3");
|
||||
assert_eq!(campaign_cell_key(&strategy, "GER/40", 3, 0), "0123abcd-GER_40-w3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A non-default regime_ordinal (#219/#212) appends `-r{ordinal}` so two
|
||||
/// regimes over the same (strategy, instrument, window) cell land in
|
||||
/// distinct trace dirs; ordinal 0 (the default regime) is covered above
|
||||
/// and stays unsuffixed for pre-#219 key stability.
|
||||
fn campaign_cell_key_appends_regime_suffix_for_non_default_ordinal() {
|
||||
let strategy = "bb34aa55".repeat(8);
|
||||
assert_eq!(campaign_cell_key(&strategy, "GER40", 0, 2), "bb34aa55-GER40-w0-r2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1761,6 +1761,8 @@ mod tests {
|
||||
strategy: "3f9c".to_string(),
|
||||
instrument: "EURUSD".to_string(),
|
||||
window_ms: (1_136_073_600_000, 1_154_390_400_000),
|
||||
regime: None,
|
||||
regime_ordinal: 0,
|
||||
stages: vec![
|
||||
StageRealization {
|
||||
block: "std::sweep".to_string(),
|
||||
@@ -1825,6 +1827,8 @@ mod tests {
|
||||
strategy: "3f9c".to_string(),
|
||||
instrument: "EURUSD".to_string(),
|
||||
window_ms: (0, 1000),
|
||||
regime: None,
|
||||
regime_ordinal: 0,
|
||||
stages: vec![StageRealization {
|
||||
block: "std::monte_carlo".to_string(),
|
||||
family_id: None,
|
||||
@@ -1840,6 +1844,8 @@ mod tests {
|
||||
strategy: "3f9c".to_string(),
|
||||
instrument: "GER40".to_string(),
|
||||
window_ms: (0, 1000),
|
||||
regime: None,
|
||||
regime_ordinal: 0,
|
||||
stages: vec![StageRealization {
|
||||
block: "std::monte_carlo".to_string(),
|
||||
family_id: None,
|
||||
|
||||
@@ -108,6 +108,29 @@ pub struct CellRealization {
|
||||
/// Inclusive epoch-ms window.
|
||||
pub window_ms: (i64, i64),
|
||||
pub stages: Vec<StageRealization>,
|
||||
/// The cell's risk regime (#219/#212) — mirrors `CellSpec::regime`: `None`
|
||||
/// is the member runner's baked default stop, `Some` an explicit document
|
||||
/// regime. Threaded through so a downstream re-run (the trace-persist C1
|
||||
/// drift alarm) binds the SAME stop the cell recorded under, instead of
|
||||
/// hardcoding the default. `#[serde(default, skip_serializing_if =
|
||||
/// "Option::is_none")]` keeps pre-#219 stored records byte-identical on
|
||||
/// round-trip (they predate the risk axis, so their implicit regime was
|
||||
/// always the default / absent).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub regime: Option<aura_research::RiskRegime>,
|
||||
/// The regime's ordinal in the resolved regime list (0 for the default) —
|
||||
/// mirrors `CellSpec::regime_ordinal`; discriminates trace dirs for two
|
||||
/// regimes over the same (strategy, instrument, window) cell.
|
||||
/// `#[serde(default, skip_serializing_if = "is_zero_ordinal")]` for the
|
||||
/// same pre-#219 byte-identical round-trip reason.
|
||||
#[serde(default, skip_serializing_if = "is_zero_ordinal")]
|
||||
pub regime_ordinal: usize,
|
||||
}
|
||||
|
||||
/// `CellRealization::regime_ordinal`'s serialize-skip predicate: the default
|
||||
/// regime (ordinal 0) is omitted, keeping pre-#219 stored lines byte-stable.
|
||||
fn is_zero_ordinal(n: &usize) -> bool {
|
||||
*n == 0
|
||||
}
|
||||
|
||||
/// One realized pipeline stage. `family_id` is set for family-producing stages
|
||||
|
||||
@@ -52,6 +52,8 @@ fn campaign_run(campaign: &str) -> CampaignRunRecord {
|
||||
strategy: "strat-id".to_string(),
|
||||
instrument: "EURUSD".to_string(),
|
||||
window_ms: (0, 1000),
|
||||
regime: None,
|
||||
regime_ordinal: 0,
|
||||
stages: vec![StageRealization {
|
||||
block: "std::sweep".to_string(),
|
||||
family_id: Some(format!("{campaign}-0-EURUSD-w0-s0-0")),
|
||||
|
||||
Reference in New Issue
Block a user