test(cli): RED — real-data member pips ignore the resolved sidecar pip

The in-graph SimBroker divides by SYNTHETIC_PIP_SIZE regardless of the
resolved instrument pip; the sidecar pip reaches only the manifest label.
Pins the pip-invariance property total_pips * pip == raw price move.

refs #232
This commit is contained in:
2026-07-10 17:30:29 +02:00
parent 3016bd6b11
commit eb5fc2145b
+58
View File
@@ -3409,6 +3409,64 @@ mod tests {
assert_eq!(*bias.last().unwrap(), 1.0, "the down-fade long must hold to the end: {bias:?}"); 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<Box<dyn aura_engine::Source>> {
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<ParamSpec> = 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 /// 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 /// public `blueprint_from_json` path — the single call site so a fixture
/// rename or vocabulary change is one edit, not fourteen. /// rename or vocabulary change is one edit, not fourteen.