diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index bc392e7..bbf9749 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -3409,6 +3409,64 @@ mod tests { assert_eq!(*bias.last().unwrap(), 1.0, "the down-fade long must hold to the end: {bias:?}"); } + /// Property: the pip a run resolves and stamps into its manifest must be the + /// pip the in-graph `SimBroker` divides by — the resolved (real, per-instrument) + /// pip has to reach the graph, not just the manifest label. The `SimBroker` + /// integrates `exposure * (price - prev_price) / pip`, so the raw price-move + /// total `total_pips * pip` is pip-invariant: the SAME signal + prices run at + /// two different pips must agree on that product. If `pip` only decorates the + /// label and the graph always divides by the synthetic default, both runs + /// yield the identical `total_pips` and the invariant breaks (and real-data + /// pips are inflated by `1 / 0.0001 = 10^4`). + #[test] + fn run_blueprint_member_computes_pips_at_the_resolved_pip_not_a_hardwired_default() { + let env = project::Env::std(); + // A price series that drives the meanrev signal into a definite non-flat + // exposure (short an up-move, long a down-move), so broker equity != 0 — + // the same idiom as the fade test above. + let closes = [ + 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 130.0, 130.0, 130.0, 70.0, 70.0, 70.0, 70.0, + ]; + let make_source = || -> Vec> { + let prices: Vec<(Timestamp, Scalar)> = closes + .iter() + .enumerate() + .map(|(i, &c)| (Timestamp(i as i64), Scalar::f64(c))) + .collect(); + vec![Box::new(VecSource::new(prices))] + }; + let window = (Timestamp(0), Timestamp(closes.len() as i64 - 1)); + let stop = StopRule::Vol { length: 3, k: 2.0 }; + let space: Vec = vec![]; + // Two per-instrument pips, an order of magnitude apart and both distinct + // from the synthetic 0.0001 — i.e. the real-data condition. + let pip_a = 1.0_f64; + let pip_b = 0.1_f64; + let report_a = run_blueprint_member( + r_meanrev_signal(Some(3), Some(0.0)), &[], &space, make_source(), window, 0, pip_a, "topo", &env, stop, + ); + let report_b = run_blueprint_member( + r_meanrev_signal(Some(3), Some(0.0)), &[], &space, make_source(), window, 0, pip_b, "topo", &env, stop, + ); + // Guard: the run must have actually traded, else the invariant is vacuous. + assert!( + report_a.metrics.total_pips.abs() > 0.0, + "test needs a non-flat run to be meaningful: {:?}", + report_a.metrics + ); + // The pip-invariant raw price-move total must agree across the two pips. + let raw_a = report_a.metrics.total_pips * pip_a; + let raw_b = report_b.metrics.total_pips * pip_b; + assert!( + (raw_a - raw_b).abs() < 1e-9, + "total_pips must be computed at the resolved pip: pip={pip_a} gave total_pips={} \ + (raw price-move {raw_a}), pip={pip_b} gave total_pips={} (raw price-move {raw_b}) \ + — the resolved pip never reached the in-graph SimBroker", + report_a.metrics.total_pips, + report_b.metrics.total_pips, + ); + } + /// Loads the shipped closed r-sma example (fast=2, slow=4 bound) through the /// public `blueprint_from_json` path — the single call site so a fixture /// rename or vocabulary change is one edit, not fourteen.