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(unrealized),
|
||||||
Cell::from_f64(self.cum_realized_r),
|
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!(
|
debug_assert!(
|
||||||
!closed
|
!closed
|
||||||
|| (realized - d_dir as f64 * (d_exit - d_entry) / (d_entry - d_stop).abs()).abs()
|
|| (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)
|
Some(&self.out)
|
||||||
}
|
}
|
||||||
@@ -347,6 +353,47 @@ mod tests {
|
|||||||
assert!(row[11].bool()); // reopened: open
|
assert!(row[11].bool()); // reopened: open
|
||||||
assert_eq!(row[4].i64(), -1); // new direction short
|
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
|
// (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.
|
// post-run fold will force-close, and the direction for window-end synthesis.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user