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:
@@ -875,11 +875,11 @@ fn select_winner(
|
||||
/// (window order, then within-window trade order). Windows with no `r` block
|
||||
/// contribute nothing. The single home of the pooling-in-roll-order semantics —
|
||||
/// both the walk-forward `oos_r` summary and the `mc` R-bootstrap reduce this.
|
||||
fn pooled_oos_trade_rs(result: &WalkForwardResult) -> Vec<f64> {
|
||||
fn pooled_oos_net_trade_rs(result: &WalkForwardResult) -> Vec<f64> {
|
||||
result
|
||||
.windows
|
||||
.iter()
|
||||
.flat_map(|w| w.run.oos_report.metrics.r.as_ref().map(|r| r.trade_rs.clone()).unwrap_or_default())
|
||||
.flat_map(|w| w.run.oos_report.metrics.r.as_ref().map(|r| r.net_trade_rs.clone()).unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -888,14 +888,14 @@ fn pooled_oos_trade_rs(result: &WalkForwardResult) -> Vec<f64> {
|
||||
/// (C14).
|
||||
fn walkforward_summary_json(result: &WalkForwardResult) -> String {
|
||||
let total = result.stitched_oos_equity.last().map(|&(_, v)| v).unwrap_or(0.0);
|
||||
let pooled_rs = pooled_oos_trade_rs(result);
|
||||
let pooled_rs = pooled_oos_net_trade_rs(result);
|
||||
let mut obj = serde_json::json!({
|
||||
"windows": result.windows.len(),
|
||||
"stitched_total_pips": total,
|
||||
"param_stability": param_stability(result),
|
||||
});
|
||||
if result.windows.iter().any(|w| w.run.oos_report.metrics.r.is_some()) {
|
||||
// RMetrics serializes its scalar fields (trade_rs is serde-skipped, so the
|
||||
// RMetrics serializes its scalar fields (net_trade_rs is serde-skipped, so the
|
||||
// oos_r block is the clean R-metric summary of the pooled series).
|
||||
obj["oos_r"] = serde_json::to_value(r_metrics_from_rs(&pooled_rs))
|
||||
.expect("RMetrics serializes");
|
||||
@@ -914,7 +914,7 @@ fn walkforward_summary_json(result: &WalkForwardResult) -> String {
|
||||
/// (e.g. `sma_signal.fast.length`) by the strategy's own param space, while
|
||||
/// `stop_length`/`stop_k` ride the risk regime unwrapped, so an exact-name match
|
||||
/// would miss the wrapped ones) through the same `MetricStats::from_values`; the
|
||||
/// `oos_r` block pools the per-window `trade_rs` through `r_metrics_from_rs`.
|
||||
/// `oos_r` block pools the per-window `net_trade_rs` through `r_metrics_from_rs`.
|
||||
/// Canonical JSON (C14).
|
||||
///
|
||||
/// `axes` carries the invocation's raw axis names in argv order, followed by the
|
||||
@@ -954,7 +954,7 @@ fn walkforward_summary_json_from_reports(reports: &[RunReport], axes: &[String])
|
||||
.collect();
|
||||
let pooled_rs: Vec<f64> = reports
|
||||
.iter()
|
||||
.flat_map(|r| r.metrics.r.as_ref().map(|m| m.trade_rs.clone()).unwrap_or_default())
|
||||
.flat_map(|r| r.metrics.r.as_ref().map(|m| m.net_trade_rs.clone()).unwrap_or_default())
|
||||
.collect();
|
||||
let mut obj = serde_json::json!({
|
||||
"windows": reports.len(),
|
||||
|
||||
Reference in New Issue
Block a user