feat(cli): campaign cost models reach the member run and both re-run sides (#234 tasks 4-5)

cost_nodes_for (beside stop_rule_for_regime) is the one CostSpec ->
bound-builder binding — every component fully bound, so the wrapped
param space stays cost-invariant. run_blueprint_member joins the cost
rows into summarize_r (member metrics genuinely net) and stamps the
cost components into the manifest params; CliMemberRunner threads the
doc's cost. The re-run sides re-derive the model from the manifest:
reproduce via cost_specs_from_params (the #233 stop pattern — a costed
family reproduces bit-identically) and persist_campaign_traces binds
the same model so the C1 drift alarm compares like with like.
Hand-computed pin: a constant cost model yields
net_expectancy_r == expectancy_r - cost_per_trade/|entry-stop| on a
synthetic member.

Verified: full workspace suite green, clippy -D warnings clean; in-loop
spec + quality gates passed per task.

refs #234
This commit is contained in:
2026-07-11 10:41:16 +02:00
parent a1afc2fd02
commit bb1f6c8bdd
3 changed files with 576 additions and 16 deletions
+178
View File
@@ -2359,6 +2359,184 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() {
);
}
/// Property (#234 Task 5, quality-review Minor finding): a costed campaign's
/// trace-persist re-run must not false-fail the C1 drift alarm, exercised
/// through the REAL production path — `campaign run` -> `run_campaign_returning`
/// -> `CliMemberRunner` (net-mode member run) -> `persist_campaign_traces`
/// (non-reduce re-run binding a `CostLeg` via `cost_nodes_for`) — not the
/// hand-mirrored arithmetic pinned by
/// `persist_side_nonreduce_rerun_matches_reduce_mode_net_metrics_under_cost` in
/// `main.rs`'s unit tests. Mirrors the sibling non-default-regime e2e above
/// (same #219-class divergence risk, cost edition): a costed campaign
/// document, `persist_taps` non-empty so the re-run actually executes, must
/// exit 0 with its nominee's cost stamp on disk and no C1 violation. Gated on
/// the local GER40 archive; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() {
let _fixture = project_lock();
let dir = built_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir.clone()),
ScratchPath::File(dir.join("cost-persist.process.json")),
ScratchPath::File(dir.join("cost-persist.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-persist-cost-seed");
let proc_id = register_process_doc(dir, "cost-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
(1725148800000, 1727740799999),
"\"equity\", \"r_equity\"",
"\"family_table\"",
);
let with_cost = base.replacen(
"\"seed\": 7,",
"\"seed\": 7,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 0.0005 } } ],",
1,
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "cost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(dir, &["campaign", "run", "cost-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
{
eprintln!("skip: no local GER40 data for the campaign cost-persist e2e");
return;
}
assert!(
!out.contains("C1 violation"),
"the costed persist-side re-run must bind the SAME cost model the nominee ran \
under, not false-fail C1: {out}"
);
assert_eq!(code, Some(0), "a deterministic costed campaign persists cleanly: {out}");
// Non-vacuity: every emitted member actually stamped the cost component
// it ran under (else the C1 comparison above never engaged a non-empty
// CostLeg and this test would pass for the wrong reason).
let member_lines: Vec<&str> =
out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}");
for line in &member_lines {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
let params = v["report"]["manifest"]["params"]
.as_array()
.expect("manifest.params is an array");
assert!(
params.iter().any(|p| p[0].as_str() == Some("cost[0].cost_per_trade")),
"the costed member stamps its cost component: {line}"
);
}
// The nominee's taps actually landed under the costed re-run.
let record_line = out
.lines()
.find(|l| l.starts_with("{\"campaign_run\":"))
.expect("the always-on final campaign_run line");
let v: serde_json::Value =
serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
let trace_name = v["campaign_run"]["trace_name"]
.as_str()
.expect("persist_taps non-empty => the record carries trace_name");
let cell_key = format!("{}-GER40-w0", &bp_id[..8]);
let cell_dir = runs_dir.join("traces").join(trace_name).join(&cell_key);
assert!(
cell_dir.join("r_equity.json").is_file(),
"the costed nominee's r_equity tap persists at {}",
cell_dir.display()
);
}
/// Property (#234): a `vol_slippage` cost component — the structurally
/// distinct leg from `constant` (it reactivates the vol-proxy RollingMax/
/// RollingMin/Sub chain feeding a `VolSlippageCost` node, not a flat per-close
/// charge) — nets a real campaign member through the SAME production path as
/// its `constant` sibling
/// (`campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm`):
/// `campaign run` -> `CliMemberRunner` (net-mode) -> `persist_campaign_traces`
/// (non-reduce re-run binding a `CostLeg`). The re-run must bind the identical
/// vol-slippage model the nominee ran under, so it must not false-fail the C1
/// drift alarm; the emitted member must stamp `cost[0].slip_vol_mult`; and the
/// vol-driven charge must actually move net off gross (non-vacuity — a
/// zero-effect charge would let this pass even if the vol proxy were never
/// wired). Gated on the local GER40 archive; skips on a data-less host.
#[test]
fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() {
let _fixture = project_lock();
let dir = built_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir.clone()),
ScratchPath::File(dir.join("volcost-persist.process.json")),
ScratchPath::File(dir.join("volcost-persist.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-persist-volcost-seed");
let proc_id =
register_process_doc(dir, "volcost-persist.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
(1725148800000, 1727740799999),
"\"r_equity\"",
"\"family_table\"",
);
let with_cost = base.replacen(
"\"seed\": 7,",
"\"seed\": 7,\n \"cost\": [ { \"vol_slippage\": { \"slip_vol_mult\": 0.5 } } ],",
1,
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(dir, "volcost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(dir, &["campaign", "run", "volcost-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(1)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
{
eprintln!("skip: no local GER40 data for the campaign vol-slippage-persist e2e");
return;
}
assert!(
!out.contains("C1 violation"),
"the vol-slippage persist-side re-run must bind the SAME cost model the nominee \
ran under, not false-fail C1: {out}"
);
assert_eq!(code, Some(0), "a deterministic vol-slippage-costed campaign persists cleanly: {out}");
let member_lines: Vec<&str> =
out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}");
let mut any_net_moved = false;
for line in &member_lines {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
let params = v["report"]["manifest"]["params"]
.as_array()
.expect("manifest.params is an array");
assert!(
params.iter().any(|p| p[0].as_str() == Some("cost[0].slip_vol_mult")),
"the costed member stamps its vol_slippage component: {line}"
);
if let Some(r) = v["report"]["metrics"]["r"].as_object() {
let gross = r["expectancy_r"].as_f64().expect("expectancy_r is a number");
let net = r["net_expectancy_r"].as_f64().expect("net_expectancy_r is a number");
if gross != net {
any_net_moved = true;
}
}
}
assert!(
any_net_moved,
"at least one member's vol-slippage charge must move net off gross \
(else the vol proxy never engaged): {out}"
);
}
/// RED (#212 — the trace-dir collision facet of the same #219 record gap): two
/// risk regimes over the SAME (strategy, instrument, window) cell must persist
/// their traces into DISTINCT directories. `campaign_cell_key` names dirs