feat(0081): cost-model graph cycle 1 — ConstantCost node + net-R seam

Lands the first cycle of the "Cost-model graph (in R)" milestone: cost is
now a composable in-graph node, not a post-run scalar (C10).

- ConstantCost (aura-std): a flat round-trip cost per trade, emitted in R
  as a 3-field record {cost_in_r, cum_cost_in_r, open_cost_in_r} isomorphic
  to PositionManagement's R-triple. R-pure: cost_in_r = cost_per_trade /
  |entry-stop|; notional cancels, no equity held.
- summarize_r folds the cost stream into net_expectancy_r, replacing its
  scalar round_trip_cost (one home for cost, no double-count). Positional
  co-temporal join; an empty stream is the gross-R baseline.
- net_r_equity tap (stage1_r_graph): LinComb(4) over [cum_realized_r,
  unrealized_r, -cum_cost_in_r, -open_cost_in_r] -> Recorder, a sibling of
  r_equity; --cost-per-trade on the run path wires the node + tap + fold.
- A no-cost run is byte-identical: no net_r_equity tap, net == gross, the
  C18 golden unchanged.

Tests: exact-subsumption (the node-stream net == mean(r_i - cost_i) over
ALL trades incl. the window-end open one), the in-graph net_r_equity tap +
net < gross E2E, the no-cost golden held. Full suite green; clippy clean.

Implement-time decisions ratified (recorded on #148): the cost stream is
recorded 3-wide (the node's full output; summarize_r reads col 0/2, col 1
feeds the in-graph net curve) — resolving a 2-wide/3-wide inconsistency in
the plan's Step 3; Trade drops its `latched` field (cost now arrives in R),
so r_col ENTRY_PRICE/STOP_PRICE become #[allow(dead_code)] layout contract.

Scoped to the run path (non-reduce); sweep/walkforward/mc pass None — cost
on the reduce-mode sweep path and OOS-pooling cost are a deferred follow-up.

Known pre-existing concern (not this cycle): aura-std lib.rs module doc
still names "realistic broker profiles", retired by the C10 rework.

refs #148
This commit is contained in:
2026-06-28 14:41:45 +02:00
parent 725545cbde
commit 3fe4684190
8 changed files with 539 additions and 89 deletions
+26
View File
@@ -1620,6 +1620,32 @@ fn stage1_r_trace_persists_r_equity_and_charts_it() {
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (Task 3, the run-path cost seam): `aura run --harness stage1-r
/// --cost-per-trade <c>` charges a flat round-trip cost in R end to end — the run
/// persists a fourth `net_r_equity` tap beside r_equity and the report's
/// `net_expectancy_r` is STRICTLY below the gross `expectancy_r`. Pins the whole
/// flag→ConstantCost node→net_r_equity tap→summarize_r net fold loop (a no-cost run
/// leaves net == gross, the held golden); a regression that dropped the cost node or
/// left the net fold reading an empty stream would collapse net back onto gross here.
#[test]
fn stage1_r_cost_run_persists_net_r_equity_and_charges_cost() {
let dir = temp_cwd("stage1-r-cost");
let run = Command::new(BIN)
.current_dir(&dir)
.args(["run", "--harness", "stage1-r", "--cost-per-trade", "2", "--trace", "c1"])
.output()
.unwrap();
assert!(run.status.success(), "exit: {:?}; stderr: {}", run.status,
String::from_utf8_lossy(&run.stderr));
assert!(dir.join("runs/traces/c1/net_r_equity.json").exists(), "net_r_equity trace persisted");
let s = String::from_utf8(run.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
let gross = v["metrics"]["r"]["expectancy_r"].as_f64().unwrap();
let net = v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap();
assert!(net < gross, "cost charged: net {net} < gross {gross}");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (the spec's dual-yardstick headline): `aura run --harness stage1-r` scores
/// ONE signal in BOTH yardsticks at once — the single emitted `RunReport` carries the pip
/// `metrics.total_pips` (off the SimBroker branch) AND the nested `r` block (off the