feat(analysis,campaign,registry,cli): bootstrap net R through the process pipeline

The OOS bootstrap conduit RMetrics.trade_rs becomes net_trade_rs and
carries the COST-NETTED per-trade R (r − cost_in_r, trade order). Every
conduit consumer is thereby net-when-costed: the monte-carlo pooled-OOS
and per-survivor bootstraps, the walk-forward oos_r pooling, and the
deflation null-max — which previously compared a net observed statistic
against a gross-resampled null whenever a costed campaign selected on
net_expectancy_r. An uncosted run is bit-identical (empty cost stream
⇒ cost 0.0 per trade), so every existing golden pin stays green.

Design: one conduit, no knob — an explicit net: knob would let a costed
campaign silently produce a gross headline again (the exact misreading
of the issue's evidence). Per-member RMetrics scalar fields stay gross;
net_expectancy_r keeps its meaning. Wire shape unchanged (serde(skip)).
The net series is materialized as a separate expression in summarize_r;
the pinned-float expressions (net_sum, the SQN pair, the lockstep
r_metrics_from_rs copies) keep their tokens verbatim. Fork decisions
and rationales: issue #259 comments.

New coverage, both hostless over the synthetic SYMA archive: a costed
campaign twin shifts the pooled-OOS bootstrap (sweep→gate→wf→mc) and
every per-survivor bootstrap (sweep→mc) below its gross sibling, with
the trade population unchanged; a unit test pins the conduit as the
cost-netted series with the empty-stream degeneracy. Existing conduit
tests renamed with the field.

Verified: full workspace suite green (71 result blocks), clippy clean,
zero bare trade_rs tokens remain, ledger conduit prose updated in place.

closes #259
This commit is contained in:
2026-07-13 21:21:41 +02:00
parent 1ccce9ad32
commit bb0b0aeac2
9 changed files with 283 additions and 79 deletions
+16 -16
View File
@@ -505,8 +505,8 @@ fn closed_neighbourhood(i: usize, axis_lens: &[usize]) -> Vec<usize> {
out
}
fn member_trade_rs(rep: &RunReport) -> &[f64] {
rep.metrics.r.as_ref().map(|r| r.trade_rs.as_slice()).unwrap_or(&[])
fn member_net_trade_rs(rep: &RunReport) -> &[f64] {
rep.metrics.r.as_ref().map(|r| r.net_trade_rs.as_slice()).unwrap_or(&[])
}
/// Recompute the selected R metric from a trade-R slice (reuses the engine's
@@ -522,13 +522,13 @@ fn member_metric_from_rs(rs: &[f64], m: Metric) -> f64 {
}
}
/// The centred best-of-K null-max distribution: each member's `trade_rs` is
/// The centred best-of-K null-max distribution: each member's `net_trade_rs` is
/// mean-subtracted (the no-edge null), then for each resample every member is
/// moving-block-resampled (in odometer order, from a per-iteration seed) and the
/// max recomputed metric across members is taken. Deterministic given `seed` (C1).
fn null_best_of_k(family: &SweepFamily, m: Metric, n_resamples: usize, block_len: usize, seed: u64) -> Vec<f64> {
let centred: Vec<Vec<f64>> = family.points.iter().map(|p| {
let rs = member_trade_rs(&p.report);
let rs = member_net_trade_rs(&p.report);
if rs.is_empty() { return Vec::new(); }
let mean = rs.iter().sum::<f64>() / rs.len() as f64;
rs.iter().map(|x| x - mean).collect()
@@ -657,8 +657,8 @@ pub fn optimize_deflated(
let null_max = null_best_of_k(family, m, n_resamples, block_len, seed);
// Keep only the finite best-of-K draws. The null is *not computable* in two
// degenerate cases: zero resamples (`null_max` empty), or no member carries
// `trade_rs` (every member's centred series is empty, so each iteration's
// best-of-K folds to `NEG_INFINITY`). The latter is reachable — `trade_rs`
// `net_trade_rs` (every member's centred series is empty, so each iteration's
// best-of-K folds to `NEG_INFINITY`). The latter is reachable — `net_trade_rs`
// is `#[serde(skip)]`, so a family loaded back from the registry has empty
// conduits even when its `r` block (hence `raw`) is finite. Filtering to
// finite values collapses both into one "no usable null" branch, instead of
@@ -847,7 +847,7 @@ mod tests {
sqn_normalized: sqn, // mirror sqn: only the rank-key wiring is under test here
net_expectancy_r,
conviction_terciles_r: [0.0, 0.0, 0.0],
trade_rs: Vec::new(),
net_trade_rs: Vec::new(),
});
rep
}
@@ -1174,16 +1174,16 @@ mod tests {
assert_eq!(ranked[0].metrics.total_pips, 3.0); // best-first within the family
}
fn member(total_pips: f64, trade_rs: Vec<f64>) -> SweepPoint {
fn member(total_pips: f64, net_trade_rs: Vec<f64>) -> SweepPoint {
// Every RMetrics field is derived from the R slice by `r_metrics_from_rs`
// (the same arithmetic production members carry), then the per-trade
// `trade_rs` conduit is restored: `r_metrics_from_rs` empties it
// (`trade_rs: Vec::new()`), whereas a production member is built by
// `net_trade_rs` conduit is restored: `r_metrics_from_rs` empties it
// (`net_trade_rs: Vec::new()`), whereas a production member is built by
// `summarize_r`, which RETAINS it — and `optimize_deflated`'s R arm
// resamples exactly that conduit, so a fixture without it cannot reach
// the R-arm path under test.
let mut r = aura_engine::r_metrics_from_rs(&trade_rs);
r.trade_rs = trade_rs;
let mut r = aura_engine::r_metrics_from_rs(&net_trade_rs);
r.net_trade_rs = net_trade_rs;
SweepPoint {
params: vec![],
report: RunReport {
@@ -1219,7 +1219,7 @@ mod tests {
#[test]
fn optimize_deflated_winner_is_byte_identical_to_optimize() {
let fam = fixture_family_with_r(); // helper: ≥3 members, varied sqn_normalized + trade_rs
let fam = fixture_family_with_r(); // helper: ≥3 members, varied sqn_normalized + net_trade_rs
for metric in ["total_pips", "sqn_normalized", "expectancy_r"] {
let plain = optimize(&fam, metric).unwrap();
let (defl, _) = optimize_deflated(&fam, metric, 200, 3, 7).unwrap();
@@ -1259,18 +1259,18 @@ mod tests {
assert_eq!(sel.overfit_probability, Some(1.0), "(0+1)/(0+1) Laplace floor");
}
/// Regression: a family whose members carry an `r` block but NO `trade_rs` —
/// Regression: a family whose members carry an `r` block but NO `net_trade_rs` —
/// the serde-skipped state of a family `load`ed back from the registry — must
/// NOT yield a `+inf` deflated score on the R arm with positive resamples. With
/// no member contributing a centred series, every best-of-K draw is
/// `NEG_INFINITY`; the finite-filter collapses this to the same degenerate floor
/// as zero-resamples (`deflated_score == raw`, `overfit_probability == 1.0`).
#[test]
fn optimize_deflated_no_trade_rs_floors_instead_of_infinity() {
fn optimize_deflated_no_net_trade_rs_floors_instead_of_infinity() {
let mut fam = fixture_family_with_r();
for p in &mut fam.points {
if let Some(r) = p.report.metrics.r.as_mut() {
r.trade_rs.clear(); // mimic a serde-loaded member (trade_rs is #[serde(skip)])
r.net_trade_rs.clear(); // mimic a serde-loaded member (net_trade_rs is #[serde(skip)])
}
}
let sel = optimize_deflated(&fam, "expectancy_r", 500, 3, 1).unwrap().1;