fix(stage1-r): scale-robust tolerance for the R-record redundancy guard

The PositionManagement redundancy `debug_assert` (position_management.rs:229)
re-derives realized_r from the dense-record columns as
dir*(exit-entry)/|entry-stop| and compared it to the stored value with an
ABSOLUTE < 1e-9 tolerance. At tiny-pip price scale (EURUSD/GBPUSD entry ~1.1,
vol-stop distance ~1e-6) the denominator reconstruction `entry - stop` is a
catastrophic-cancellation subtraction that loses ~6 significant digits; with a
deep gap-through-stop realising a large |R|, the recomputed R drifts from the
(correct) stored realized_r by more than 1e-9 - so a debug build PANICS (exit
101) while a release build (assert compiled out) returns the correct R. GER40
(large pip) never trips it.

Found by the Stage-1 R milestone fieldtest: `aura run --harness stage1-r --real
EURUSD` crashed the headline "score real FX in R" use case on the default (debug)
build, and the debug/release disagreement made the R numbers' validity
unknowable from the public surface.

Root cause (via debug): the stored realized_r uses the cancellation-free FROZEN
latched_dist and is exact - only the guard's tolerance was wrong for small price
scales. Fix: a scale-robust relative tolerance `< 1e-9 * realized.abs().max(1.0)`
(floored at the historical 1e-9 for |R| <= 1, so no change for normal-scale runs).

RED-first: the hermetic synthetic-tiny-pip test
`tiny_pip_gap_stop_does_not_trip_the_redundancy_guard` reproduces the panic and is
pinned here. Verified end-to-end: `aura run --harness stage1-r --real EURUSD` now
returns the R block (exit 0) on a debug build. cargo test --workspace green;
clippy --workspace --all-targets -D warnings clean.

refs #117
This commit is contained in:
2026-06-24 12:30:08 +02:00
parent a6fc48daa7
commit 2ee0d74a8c
+48 -1
View File
@@ -226,10 +226,16 @@ impl Node for PositionManagement {
Cell::from_f64(unrealized),
Cell::from_f64(self.cum_realized_r),
];
// Scale-robust tolerance: the column re-derivation reconstructs the R-denominator
// via `d_entry - d_stop`, a catastrophic-cancellation subtraction at tiny-pip price
// scale (entry ~1.1, dist ~1e-6) that drifts from the cancellation-free frozen
// `latched_dist` `realized` was computed with. A relative tolerance (floored at the
// historical absolute 1e-9 for |R| <= 1) keeps the guard meaningful while letting the
// two build profiles agree on tiny-pip instruments — `realized` itself is exact.
debug_assert!(
!closed
|| (realized - d_dir as f64 * (d_exit - d_entry) / (d_entry - d_stop).abs()).abs()
< 1e-9
< 1e-9 * realized.abs().max(1.0)
);
Some(&self.out)
}
@@ -347,6 +353,47 @@ mod tests {
assert!(row[11].bool()); // reopened: open
assert_eq!(row[4].i64(), -1); // new direction short
}
// Property: the realized_r <-> dense-record-columns redundancy guard holds for a
// tiny-pip instrument (EURUSD-scale entry, a sub-pip stop distance). `eval` stores
// `realized` from the cancellation-free frozen `latched_dist`; the in-`eval`
// redundancy guard RE-derives R from the record columns as
// `direction * (exit - entry) / |entry - stop|`, reconstructing the R-denominator
// by the subtraction `entry - stop`. At a price scale ~1.1 with a stop distance
// ~1e-6, that subtraction loses ~6 significant digits relative to the magnitude of
// `entry` (~2e-16 ULP / 1e-6 dist), so the recomputed denominator drifts from the
// frozen `latched_dist`; with a deep gap-through-stop realising a large |R|, the
// recomputed R diverges from the (correct) stored `realized` by more than the
// guard's ABSOLUTE 1e-9 tolerance, panicking a debug build while a release build
// (assert compiled out) returns the correct R. The stored `realized` is correct;
// the guard must use a scale-robust (relative) tolerance so the two build profiles
// agree. A no-op for large-pip instruments (GER40), where the cancellation is
// negligible.
#[test]
fn tiny_pip_gap_stop_does_not_trip_the_redundancy_guard() {
let entry = 1.10000_f64; // EURUSD-scale price
let dist = 0.000001_f64; // sub-pip vol-stop distance (a quiet M1 bar)
let mut n = PositionManagement::new();
let mut c = cols();
let _ = step(&mut n, &mut c, 1.0, entry, dist); // open long @1.10000, stop @1.099999
// A fast gap straight through the stop: price 1.09990, ~100e-6 below entry ->
// a ~-100R loss (stop-outs are NOT capped at -1R; the honest tail). In a debug
// build the current absolute-tolerance guard PANICS here.
let row = step(&mut n, &mut c, 1.0, 1.09990_f64, dist);
assert!(row[0].bool(), "closed_this_cycle");
assert!(row[3].bool(), "was_stopped");
// The stored realized_r and the record-column re-derivation must agree within a
// tolerance robust to the tiny price scale (relative, not absolute 1e-9).
let realized = row[1].f64();
let (dir, d_entry, d_stop, d_exit) =
(row[4].i64() as f64, row[6].f64(), row[7].f64(), row[8].f64());
let recomputed = dir * (d_exit - d_entry) / (d_entry - d_stop).abs();
assert!(
(realized - recomputed).abs() <= 1e-9 * realized.abs().max(1.0),
"realized_r ({realized}) must match the column re-derivation ({recomputed}) \
within a scale-robust tolerance",
);
}
// (4) Open at window end is carried on the last row: open=true, the unrealized R the
// post-run fold will force-close, and the direction for window-end synthesis.
#[test]