test(cli): archive-gated e2e — a costed campaign nets members and reproduces (#234 task 7)
The closing proof: a GER40 campaign with a constant+vol_slippage cost block runs to exit 0; every member's net_expectancy_r < expectancy_r (costs bite), the same doc without the cost block reproduces today's exact gross numbers (golden parity), net_r_equity.json persists on request, and the costed family reproduces bit-identically. One plan literal corrected in place: the member-line filter exact-matches the line shape so the campaign_run summary record (which nests a family_id) no longer false-matches. Verified: e2e green on the real archive, full workspace suite green, clippy -D warnings clean. closes #234
This commit is contained in:
@@ -2822,6 +2822,246 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #234 (archive-gated): the net-R campaign path end to end. The SAME document
|
||||||
|
/// runs twice — without and with a `cost` block (the only textual difference).
|
||||||
|
/// Cost only ADDS: every member's gross field family is identical across the
|
||||||
|
/// two runs and net == gross without cost; with cost, every member's
|
||||||
|
/// net_expectancy_r sits strictly below its gross expectancy_r. The costed run
|
||||||
|
/// persists `net_r_equity` when requested (no skip notice), while the
|
||||||
|
/// cost-less run skips it with the remedy notice; and the costed family
|
||||||
|
/// reproduces bit-identically (components re-derived from the member
|
||||||
|
/// manifests). Skips on a data-less host like its siblings.
|
||||||
|
#[test]
|
||||||
|
fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
|
||||||
|
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("costnet.process.json")),
|
||||||
|
ScratchPath::File(dir.join("costless.campaign.json")),
|
||||||
|
ScratchPath::File(dir.join("costnet.campaign.json")),
|
||||||
|
]);
|
||||||
|
let bp_id = seed_blueprint(dir, "campaign-run-costnet-seed");
|
||||||
|
let proc_id = register_process_doc(dir, "costnet.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||||||
|
let base = campaign_doc_json(
|
||||||
|
&bp_id,
|
||||||
|
&proc_id,
|
||||||
|
(1725148800000, 1727740799999),
|
||||||
|
"\"net_r_equity\", \"equity\"",
|
||||||
|
"\"family_table\"",
|
||||||
|
);
|
||||||
|
// The same document, plus a cost block — the ONLY difference. 2.0 price
|
||||||
|
// units per close on GER40 (index ~18500, M1 vol-stop distances of a few
|
||||||
|
// points) yields a clearly visible per-trade cost-in-R.
|
||||||
|
let with_cost = base.replacen(
|
||||||
|
"\"seed\": 7,",
|
||||||
|
"\"seed\": 7,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 2.0 } }, { \"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, "costless.campaign.json", &base);
|
||||||
|
write_doc(dir, "costnet.campaign.json", &with_cost);
|
||||||
|
|
||||||
|
// Per-member (axis-params key -> (expectancy_r, net_expectancy_r)),
|
||||||
|
// parsed from the emitted family_table member lines. Cost stamps are
|
||||||
|
// excluded from the key so the two runs' members align.
|
||||||
|
let members = |out: &str| -> Vec<(String, f64, f64)> {
|
||||||
|
out.lines()
|
||||||
|
// A member line starts with the family_id key itself; the always-on
|
||||||
|
// final `{"campaign_run":...}` summary line also embeds a nested
|
||||||
|
// "family_id" (per selection stage) and must NOT be swept in here.
|
||||||
|
.filter(|l| l.starts_with("{\"family_id\":"))
|
||||||
|
.map(|l| {
|
||||||
|
let v: serde_json::Value = serde_json::from_str(l).expect("member line parses");
|
||||||
|
let key = v["report"]["manifest"]["params"]
|
||||||
|
.as_array()
|
||||||
|
.expect("params array")
|
||||||
|
.iter()
|
||||||
|
.filter(|p| !p[0].as_str().unwrap_or_default().starts_with("cost["))
|
||||||
|
.map(|p| p.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
|
let r = &v["report"]["metrics"]["r"];
|
||||||
|
(
|
||||||
|
key,
|
||||||
|
r["expectancy_r"].as_f64().expect("expectancy_r"),
|
||||||
|
r["net_expectancy_r"].as_f64().expect("net_expectancy_r"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// (a) the cost-less baseline: net == gross, and net_r_equity skips with
|
||||||
|
// the remedy notice while equity still persists.
|
||||||
|
let (out_gross, code_gross) = run_code_in(dir, &["campaign", "run", "costless.campaign.json"]);
|
||||||
|
if code_gross == Some(1)
|
||||||
|
&& (out_gross.contains("no recorded geometry") || out_gross.contains("no data for instrument"))
|
||||||
|
{
|
||||||
|
eprintln!("skip: no local GER40 data for the cost e2e");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert_eq!(code_gross, Some(0), "cost-less baseline run: {out_gross}");
|
||||||
|
assert!(
|
||||||
|
out_gross.contains(
|
||||||
|
"aura: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||||
|
),
|
||||||
|
"the cost-less run skips the net tap with the remedy: {out_gross}"
|
||||||
|
);
|
||||||
|
let gross_members = members(&out_gross);
|
||||||
|
assert_eq!(gross_members.len(), 4, "2x2 axes -> four member lines: {out_gross}");
|
||||||
|
for (key, e, ne) in &gross_members {
|
||||||
|
assert_eq!(e, ne, "cost-less member {key} must have net == gross");
|
||||||
|
}
|
||||||
|
|
||||||
|
// (b) the costed run: gross identical member-for-member, net strictly
|
||||||
|
// below gross, no skip notice, net_r_equity persisted on disk.
|
||||||
|
let (out_net, code_net) = run_code_in(dir, &["campaign", "run", "costnet.campaign.json"]);
|
||||||
|
assert_eq!(code_net, Some(0), "costed run: {out_net}");
|
||||||
|
assert!(
|
||||||
|
!out_net.contains("skipped"),
|
||||||
|
"a costed run must not skip any requested tap: {out_net}"
|
||||||
|
);
|
||||||
|
let net_members = members(&out_net);
|
||||||
|
assert_eq!(net_members.len(), 4, "the same four members: {out_net}");
|
||||||
|
for (key, e, ne) in &net_members {
|
||||||
|
let (_, gross_e, _) = gross_members
|
||||||
|
.iter()
|
||||||
|
.find(|(gk, _, _)| gk == key)
|
||||||
|
.unwrap_or_else(|| panic!("costed member {key} has no cost-less twin"));
|
||||||
|
assert_eq!(e, gross_e, "cost only ADDS: member {key} gross must be identical");
|
||||||
|
assert!(ne < e, "member {key}: net {ne} must sit strictly below gross {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let record_line = out_net
|
||||||
|
.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("net_r_equity.json").is_file(),
|
||||||
|
"the net curve persists at {}",
|
||||||
|
cell_dir.display()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cell_dir.join("equity.json").is_file(),
|
||||||
|
"the sibling tap still persists: {}",
|
||||||
|
cell_dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// (c) the costed family reproduces bit-identically — the components are
|
||||||
|
// re-derived from the member manifests (#233 pattern, cost edition).
|
||||||
|
let family_id = out_net
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.starts_with("{\"family_id\":"))
|
||||||
|
.map(|l| {
|
||||||
|
serde_json::from_str::<serde_json::Value>(l).expect("member line parses")["family_id"]
|
||||||
|
.as_str()
|
||||||
|
.expect("family_id is a string")
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.expect("a costed member line carries the family id");
|
||||||
|
let (rep_out, rep_code) = run_code_in(dir, &["reproduce", &family_id]);
|
||||||
|
assert_eq!(rep_code, Some(0), "reproduce: {rep_out}");
|
||||||
|
assert!(
|
||||||
|
rep_out.contains("reproduced 4/4 members bit-identically"),
|
||||||
|
"the costed family re-derives: {rep_out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Property (#234): a `carry` cost component — the third and structurally
|
||||||
|
/// distinct cost leg (a per-held-cycle accrual charged while a position stays
|
||||||
|
/// open, via `CarryCost`'s own extra input, unlike `constant`'s flat per-close
|
||||||
|
/// charge or `vol_slippage`'s volatility-proxy-scaled charge) — nets a real
|
||||||
|
/// campaign member through the SAME production path as its `constant`/
|
||||||
|
/// `vol_slippage` siblings (`campaign run` -> `CliMemberRunner` (net-mode) ->
|
||||||
|
/// `persist_campaign_traces`, non-reduce re-run binding a `CostLeg`). The
|
||||||
|
/// re-run must bind the identical carry model the nominee ran under, so it
|
||||||
|
/// must not false-fail the C1 drift alarm; the emitted member must stamp
|
||||||
|
/// `cost[0].carry_per_cycle`; and the accrual must actually move at least one
|
||||||
|
/// member's net off its gross (non-vacuity — a zero-effect charge would let
|
||||||
|
/// this pass even if the per-cycle accrual were never wired). Gated on the
|
||||||
|
/// local GER40 archive; skips on a data-less host.
|
||||||
|
#[test]
|
||||||
|
fn campaign_run_real_e2e_carry_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("carrycost-persist.process.json")),
|
||||||
|
ScratchPath::File(dir.join("carrycost-persist.campaign.json")),
|
||||||
|
]);
|
||||||
|
let bp_id = seed_blueprint(dir, "campaign-persist-carrycost-seed");
|
||||||
|
let proc_id =
|
||||||
|
register_process_doc(dir, "carrycost-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\": [ { \"carry\": { \"carry_per_cycle\": 0.001 } } ],",
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
|
||||||
|
write_doc(dir, "carrycost-persist.campaign.json", &with_cost);
|
||||||
|
let (out, code) = run_code_in(dir, &["campaign", "run", "carrycost-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 carry-cost-persist e2e");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!out.contains("C1 violation"),
|
||||||
|
"the carry 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 carry-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].carry_per_cycle")),
|
||||||
|
"the costed member stamps its carry 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 carry accrual must move net off gross \
|
||||||
|
(else the per-held-cycle charge never engaged): {out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Gated real-data e2e, the OTHER mc bootstrap input shape (#200 d1): with no
|
/// Gated real-data e2e, the OTHER mc bootstrap input shape (#200 d1): with no
|
||||||
/// walk_forward in the pipeline (`sweep -> monte_carlo`), the terminal
|
/// walk_forward in the pipeline (`sweep -> monte_carlo`), the terminal
|
||||||
/// annotator's dual-input dispatch takes the `PerSurvivor` arm, not
|
/// annotator's dual-input dispatch takes the `PerSurvivor` arm, not
|
||||||
|
|||||||
Reference in New Issue
Block a user