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
+13 -8
View File
@@ -100,12 +100,12 @@ impl Registry {
enum Metric {
TotalPips,
MaxDrawdown,
ExposureSignFlips,
BiasSignFlips,
}
/// The per-metric **best-first ordering** of two reports: `Less` means `a` is
/// better than `b`. "Best" is fixed by each metric's meaning: `total_pips`
/// higher-is-better; `max_drawdown` and `exposure_sign_flips` lower-is-better.
/// higher-is-better; `max_drawdown` and `bias_sign_flips` lower-is-better.
/// `f64` keys use `partial_cmp` (total, since metrics are finite by
/// construction). An unknown metric name is a `RegistryError::UnknownMetric`.
///
@@ -119,7 +119,8 @@ fn metric_cmp(
let metric = match metric {
"total_pips" => Metric::TotalPips,
"max_drawdown" => Metric::MaxDrawdown,
"exposure_sign_flips" => Metric::ExposureSignFlips,
// accept the new name AND the pre-rename one (CLI back-compat).
"bias_sign_flips" | "exposure_sign_flips" => Metric::BiasSignFlips,
other => return Err(RegistryError::UnknownMetric(other.to_string())),
};
Ok(move |a: &RunReport, b: &RunReport| match metric {
@@ -127,8 +128,8 @@ fn metric_cmp(
Metric::TotalPips => b.metrics.total_pips.partial_cmp(&a.metrics.total_pips).unwrap(),
// lower-is-better -> better when smaller
Metric::MaxDrawdown => a.metrics.max_drawdown.partial_cmp(&b.metrics.max_drawdown).unwrap(),
Metric::ExposureSignFlips => {
a.metrics.exposure_sign_flips.cmp(&b.metrics.exposure_sign_flips)
Metric::BiasSignFlips => {
a.metrics.bias_sign_flips.cmp(&b.metrics.bias_sign_flips)
}
})
}
@@ -182,7 +183,7 @@ impl fmt::Display for RegistryError {
}
RegistryError::UnknownMetric(m) => write!(
f,
"unknown metric '{m}' (known: total_pips, max_drawdown, exposure_sign_flips)"
"unknown metric '{m}' (known: total_pips, max_drawdown, bias_sign_flips)"
),
}
}
@@ -211,7 +212,7 @@ mod tests {
seed: 0,
broker: "b".to_string(),
},
metrics: RunMetrics { total_pips, max_drawdown, exposure_sign_flips: flips, r: None },
metrics: RunMetrics { total_pips, max_drawdown, bias_sign_flips: flips, r: None },
}
}
@@ -293,8 +294,12 @@ mod tests {
assert_eq!(by_pips[0].metrics.total_pips, 3.0); // higher-is-better -> desc
let by_dd = rank_by(reports.clone(), "max_drawdown").expect("rank dd");
assert_eq!(by_dd[0].metrics.max_drawdown, 0.1); // lower-is-better -> asc
// the legacy rank-string still resolves (the dual-accept alias).
let by_flips = rank_by(reports.clone(), "exposure_sign_flips").expect("rank flips");
assert_eq!(by_flips[0].metrics.exposure_sign_flips, 1); // lower-is-better -> asc
assert_eq!(by_flips[0].metrics.bias_sign_flips, 1); // lower-is-better -> asc
// the new rank-string ranks identically.
let by_flips_new = rank_by(reports.clone(), "bias_sign_flips").expect("rank flips (new name)");
assert_eq!(by_flips_new[0].metrics.bias_sign_flips, 1);
}
#[test]