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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user