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
|
||||
/// walk_forward in the pipeline (`sweep -> monte_carlo`), the terminal
|
||||
/// annotator's dual-input dispatch takes the `PerSurvivor` arm, not
|
||||
|
||||
Reference in New Issue
Block a user