feat(stage1-r): rename the exposure_sign_flips metric to bias_sign_flips (#126)

Complete the typed-field half of the exposure->bias rename - the one the cycle-0065
close audit flagged as aging worst (a serde-stable on-disk key).

- RunMetrics.exposure_sign_flips -> bias_sign_flips with
  #[serde(alias = "exposure_sign_flips")], so legacy runs.jsonl still deserialises
  (pinned by the legacy-read tests, which keep the old key); new output serialises
  bias_sign_flips (the byte-pin tests updated to the new key).
- The registry rank metric: Metric::ExposureSignFlips -> BiasSignFlips, the rank
  string accepts BOTH "bias_sign_flips" and "exposure_sign_flips" (CLI back-compat),
  the error/listing updated; the rank tests exercise both names.
- The MC aggregate field renamed too (McAggregate is hand-rendered to JSON, not
  serde-derived - no alias needed).

Scope held to the typed metric. The strategy-output PARAM NAMESPACE - the
.named("exposure") Bias instance, its exposure.scale knob path (Composite::param_space
derives the knob from the node name), and the exposure_scale manifest label - is a
coherent ~30-site, sweep-pinned surface (the implement loop correctly flagged renaming
the knob as an unscoped expansion touching walkforward + the cli_run byte-pins). Kept
uniformly as "exposure" and deferred to a follow-on so the cluster renames as one
consistent unit. SimBroker's pre-reframe exposure concept and the on-disk "exposure"
tap label stay by the #117 fork decision.

cargo test --workspace green; clippy --workspace --all-targets -D warnings clean; a
live `aura run --harness stage1-r` emits "bias_sign_flips".

closes #126
refs #117
This commit is contained in:
2026-06-24 13:11:46 +02:00
parent 34e8bbc803
commit c6c1d3bb10
9 changed files with 59 additions and 39 deletions
+5 -5
View File
@@ -1459,7 +1459,7 @@ fn mc_aggregate_json(agg: &McAggregate) -> String {
"mc_aggregate": {
"total_pips": agg.total_pips,
"max_drawdown": agg.max_drawdown,
"exposure_sign_flips": agg.exposure_sign_flips,
"bias_sign_flips": agg.bias_sign_flips,
}
})
.to_string()
@@ -2470,7 +2470,7 @@ mod tests {
for line in &lines {
assert!(line.contains(r#""total_pips":"#), "missing total_pips: {line}");
assert!(line.contains(r#""max_drawdown":"#), "missing max_drawdown: {line}");
assert!(line.contains(r#""exposure_sign_flips":"#), "missing flips: {line}");
assert!(line.contains(r#""bias_sign_flips":"#), "missing flips: {line}");
assert!(line.ends_with('}'), "line not closed: {line}");
}
}
@@ -2557,7 +2557,7 @@ mod tests {
// after warm-up the EMA-of-EMA histogram crosses zero, so the strategy
// reverses exposure at least once — a genuinely non-trivial trace.
assert!(
r1.metrics.exposure_sign_flips >= 1,
r1.metrics.bias_sign_flips >= 1,
"macd trace should flip exposure: {:?}",
r1.metrics
);
@@ -2724,8 +2724,8 @@ mod tests {
assert_eq!(r1.to_json(), r2.to_json());
let m = &r1.metrics;
// exactly one exposure sign flip in the demo trace (rises then reverses).
assert_eq!(m.exposure_sign_flips, 1);
// exactly one bias sign flip in the demo trace (rises then reverses).
assert_eq!(m.bias_sign_flips, 1);
// a non-trivial, populated trace: a real drawdown.
assert!(m.max_drawdown > 0.0);
// hand-computed magnitudes for the chosen stream (float tolerance; the