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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user