diff --git a/crates/ml-backtesting/src/sim/mod.rs b/crates/ml-backtesting/src/sim/mod.rs index 28db6b867..f0480ed82 100644 --- a/crates/ml-backtesting/src/sim/mod.rs +++ b/crates/ml-backtesting/src/sim/mod.rs @@ -103,6 +103,11 @@ pub struct LobSimCuda { threshold_d: CudaSlice, // [n_backtests] cost_per_lot_per_side_d: CudaSlice, // [n_backtests] total_fees_per_b_d: CudaSlice, // [n_backtests] — accumulator + + // Stop-controller state slots (ISV-driven stop controller). + prev_mid_d: CudaSlice, // [n_backtests] — previous-event mid, sentinel 0 = first-obs bootstrap + atr_mid_ema_d: CudaSlice, // [n_backtests] — Wiener-α=0.4 EMA on |Δmid|; floor for §5 SL/trail + trail_hwm_d: CudaSlice, // [n_backtests] — per-position unrealized-P&L-per-lot high-water-mark } impl LobSimCuda { @@ -256,6 +261,16 @@ impl LobSimCuda { let total_fees_per_b_d = stream.alloc_zeros::(n_backtests) .context("alloc total_fees_per_b_d")?; + let prev_mid_d = stream + .alloc_zeros::(n_backtests) + .context("alloc prev_mid_d")?; + let atr_mid_ema_d = stream + .alloc_zeros::(n_backtests) + .context("alloc atr_mid_ema_d")?; + let trail_hwm_d = stream + .alloc_zeros::(n_backtests) + .context("alloc trail_hwm_d")?; + Ok(Self { n_backtests, stream, @@ -305,6 +320,9 @@ impl LobSimCuda { threshold_d, cost_per_lot_per_side_d, total_fees_per_b_d, + prev_mid_d, + atr_mid_ema_d, + trail_hwm_d, }) } @@ -401,6 +419,45 @@ impl LobSimCuda { Ok(heads.iter().map(|&h| h as u64).sum()) } + /// Read `atr_mid_ema_d[backtest_idx]`. Test-only accessor. + pub fn read_atr_mid_ema(&self, backtest_idx: usize) -> Result { + anyhow::ensure!( + backtest_idx < self.n_backtests, + "backtest_idx {} >= n_backtests {}", + backtest_idx, + self.n_backtests + ); + let mut buf = vec![0.0f32; self.n_backtests]; + self.stream.memcpy_dtoh(&self.atr_mid_ema_d, buf.as_mut_slice())?; + Ok(buf[backtest_idx]) + } + + /// Read `prev_mid_d[backtest_idx]`. Test-only accessor. + pub fn read_prev_mid(&self, backtest_idx: usize) -> Result { + anyhow::ensure!( + backtest_idx < self.n_backtests, + "backtest_idx {} >= n_backtests {}", + backtest_idx, + self.n_backtests + ); + let mut buf = vec![0.0f32; self.n_backtests]; + self.stream.memcpy_dtoh(&self.prev_mid_d, buf.as_mut_slice())?; + Ok(buf[backtest_idx]) + } + + /// Read `trail_hwm_d[backtest_idx]`. Test-only accessor. + pub fn read_trail_hwm(&self, backtest_idx: usize) -> Result { + anyhow::ensure!( + backtest_idx < self.n_backtests, + "backtest_idx {} >= n_backtests {}", + backtest_idx, + self.n_backtests + ); + let mut buf = vec![0.0f32; self.n_backtests]; + self.stream.memcpy_dtoh(&self.trail_hwm_d, buf.as_mut_slice())?; + Ok(buf[backtest_idx]) + } + /// Apply one MBP-10 snapshot's top-10 levels to every backtest's book. /// v1 inputs are single-snapshot broadcast (same market data to all /// parallel backtests — see spec §7 inference scope, policy sweeps only).