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:
@@ -407,7 +407,7 @@ fn run_cell(
|
||||
let bootstrap =
|
||||
match families.iter().rev().find(|f| f.block == "std::walk_forward") {
|
||||
Some(fam) => StageBootstrap::PooledOos(r_bootstrap(
|
||||
&pooled_trade_rs(&fam.reports),
|
||||
&pooled_net_trade_rs(&fam.reports),
|
||||
*resamples as usize,
|
||||
*block_len as usize,
|
||||
seed,
|
||||
@@ -423,7 +423,7 @@ fn run_cell(
|
||||
.metrics
|
||||
.r
|
||||
.as_ref()
|
||||
.map(|r| r.trade_rs.as_slice())
|
||||
.map(|r| r.net_trade_rs.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
(
|
||||
*ordinal,
|
||||
@@ -627,15 +627,15 @@ fn scalar_point(space: &[ParamSpec], point: &[Cell]) -> Vec<Scalar> {
|
||||
space.iter().zip(point).map(|(ps, c)| Scalar::from_cell(ps.kind, *c)).collect()
|
||||
}
|
||||
|
||||
/// The pooled walk-forward OOS trade-R series: the family reports' `trade_rs`
|
||||
/// The pooled walk-forward OOS trade-R series: the family reports' `net_trade_rs`
|
||||
/// concatenated in report order — which IS roll order (the wf stage builds its
|
||||
/// family via `walkforward_member_reports`, per-window OOS reports in roll
|
||||
/// order). Reports without an R block contribute nothing.
|
||||
fn pooled_trade_rs(reports: &[RunReport]) -> Vec<f64> {
|
||||
fn pooled_net_trade_rs(reports: &[RunReport]) -> Vec<f64> {
|
||||
let mut pooled = Vec::new();
|
||||
for report in reports {
|
||||
if let Some(r) = &report.metrics.r {
|
||||
pooled.extend_from_slice(&r.trade_rs);
|
||||
pooled.extend_from_slice(&r.net_trade_rs);
|
||||
}
|
||||
}
|
||||
pooled
|
||||
|
||||
@@ -365,7 +365,7 @@ mod tests {
|
||||
sqn: 13.0,
|
||||
net_expectancy_r: 14.0,
|
||||
conviction_terciles_r: [0.0; 3],
|
||||
trade_rs: Vec::new(),
|
||||
net_trade_rs: Vec::new(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ fn param_i64(params: &[(String, Scalar)], name: &str) -> i64 {
|
||||
|
||||
/// The deterministic 4-trade R series a planted report carries (matches
|
||||
/// `n_trades: 4`; non-constant so block resampling has structure). In-memory
|
||||
/// only: `trade_rs` is serde-skipped and excluded from `PartialEq`, so every
|
||||
/// only: `net_trade_rs` is serde-skipped and excluded from `PartialEq`, so every
|
||||
/// existing equality and round-trip assertion stays green.
|
||||
fn planted_trade_rs(net: f64) -> Vec<f64> {
|
||||
fn planted_net_trade_rs(net: f64) -> Vec<f64> {
|
||||
vec![net, -net, 2.0 * net, net]
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ fn planted_report(cell: &CellSpec, params: &[(String, Scalar)], window_ms: (i64,
|
||||
sqn_normalized: net,
|
||||
net_expectancy_r: net,
|
||||
conviction_terciles_r: [0.0, 0.0, 0.0],
|
||||
trade_rs: planted_trade_rs(net),
|
||||
net_trade_rs: planted_net_trade_rs(net),
|
||||
}),
|
||||
},
|
||||
}
|
||||
@@ -483,11 +483,11 @@ fn execute_mc_after_gate_bootstraps_each_survivor() {
|
||||
assert_eq!(mc.selection, None);
|
||||
|
||||
// each survivor's bootstrap == a hand-called r_bootstrap on its planted
|
||||
// trade_rs, seeded from the campaign doc (the deflation convention)
|
||||
// net_trade_rs, seeded from the campaign doc (the deflation convention)
|
||||
let net = |fast: i64, slow: i64| (fast * 10 + slow) as f64 / 100.0;
|
||||
let expected = StageBootstrap::PerSurvivor(vec![
|
||||
(2, r_bootstrap(&planted_trade_rs(net(3, 6)), 200, 2, 7)),
|
||||
(3, r_bootstrap(&planted_trade_rs(net(3, 9)), 200, 2, 7)),
|
||||
(2, r_bootstrap(&planted_net_trade_rs(net(3, 6)), 200, 2, 7)),
|
||||
(3, r_bootstrap(&planted_net_trade_rs(net(3, 9)), 200, 2, 7)),
|
||||
]);
|
||||
assert_eq!(mc.bootstrap, Some(expected));
|
||||
|
||||
@@ -513,15 +513,15 @@ fn execute_mc_after_wf_pools_the_oos_series() {
|
||||
assert_eq!(wf.block, "std::walk_forward");
|
||||
assert_eq!(wf.reports.len(), 2);
|
||||
|
||||
// pooled input = the wf family reports' trade_rs concatenated in report
|
||||
// pooled input = the wf family reports' net_trade_rs concatenated in report
|
||||
// order (roll order per walkforward_member_reports)
|
||||
let mut pooled: Vec<f64> = Vec::new();
|
||||
for report in &wf.reports {
|
||||
pooled.extend_from_slice(&report.metrics.r.as_ref().expect("planted R block").trade_rs);
|
||||
pooled.extend_from_slice(&report.metrics.r.as_ref().expect("planted R block").net_trade_rs);
|
||||
}
|
||||
let net = (3 * 10 + 9) as f64 / 100.0;
|
||||
let mut expected_pool = planted_trade_rs(net);
|
||||
expected_pool.extend(planted_trade_rs(net));
|
||||
let mut expected_pool = planted_net_trade_rs(net);
|
||||
expected_pool.extend(planted_net_trade_rs(net));
|
||||
assert_eq!(pooled, expected_pool);
|
||||
|
||||
let mc = &out.record.cells[0].stages[2];
|
||||
@@ -677,7 +677,7 @@ fn execute_generalize_only_after_sweep() {
|
||||
/// annotators compose in ONE pipeline without disturbing each other or the
|
||||
/// population stages' generalize-nominee. `std::monte_carlo` sits between
|
||||
/// `std::walk_forward` and `std::generalize` in the pipeline, yet its
|
||||
/// per-cell bootstrap still pools exactly the wf family's OOS `trade_rs`
|
||||
/// per-cell bootstrap still pools exactly the wf family's OOS `net_trade_rs`
|
||||
/// (unaffected by generalize running after it), and `std::generalize`'s
|
||||
/// campaign-scope nominee is still the wf stage's last-window winner
|
||||
/// (unaffected by mc having annotated the cell first). A regression that
|
||||
@@ -706,13 +706,13 @@ fn execute_mc_and_generalize_compose_in_one_pipeline() {
|
||||
assert_eq!(cell.stages[3].block, "std::monte_carlo");
|
||||
}
|
||||
|
||||
// mc's bootstrap pools the wf family's OOS trade_rs exactly as it does
|
||||
// mc's bootstrap pools the wf family's OOS net_trade_rs exactly as it does
|
||||
// with no generalize stage after it (execute_mc_after_wf_pools_the_oos_series)
|
||||
let wf = &out.cells[0].families[1];
|
||||
assert_eq!(wf.block, "std::walk_forward");
|
||||
let mut pooled: Vec<f64> = Vec::new();
|
||||
for report in &wf.reports {
|
||||
pooled.extend_from_slice(&report.metrics.r.as_ref().expect("planted R block").trade_rs);
|
||||
pooled.extend_from_slice(&report.metrics.r.as_ref().expect("planted R block").net_trade_rs);
|
||||
}
|
||||
let expected_mc = StageBootstrap::PooledOos(r_bootstrap(&pooled, 200, 2, 7));
|
||||
assert_eq!(out.record.cells[0].stages[3].bootstrap, Some(expected_mc));
|
||||
|
||||
@@ -125,7 +125,7 @@ fn rankable_metrics_are_all_per_member_resolvable() {
|
||||
sqn: 13.0,
|
||||
net_expectancy_r: 14.0,
|
||||
conviction_terciles_r: [0.0; 3],
|
||||
trade_rs: Vec::new(),
|
||||
net_trade_rs: Vec::new(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user