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