feat(ml-backtesting): add stop-controller state slots + accessors

Three CudaSlice<f32> per-backtest slots (prev_mid_d, atr_mid_ema_d,
trail_hwm_d) plus test-only accessors. Foundation for the
ISV-driven stop controller; no behavioral change until subsequent
tasks wire them into kernels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-19 23:41:29 +02:00
parent 091707cae7
commit 5b5292aecd

View File

@@ -103,6 +103,11 @@ pub struct LobSimCuda {
threshold_d: CudaSlice<f32>, // [n_backtests]
cost_per_lot_per_side_d: CudaSlice<f32>, // [n_backtests]
total_fees_per_b_d: CudaSlice<f32>, // [n_backtests] — accumulator
// Stop-controller state slots (ISV-driven stop controller).
prev_mid_d: CudaSlice<f32>, // [n_backtests] — previous-event mid, sentinel 0 = first-obs bootstrap
atr_mid_ema_d: CudaSlice<f32>, // [n_backtests] — Wiener-α=0.4 EMA on |Δmid|; floor for §5 SL/trail
trail_hwm_d: CudaSlice<f32>, // [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::<f32>(n_backtests)
.context("alloc total_fees_per_b_d")?;
let prev_mid_d = stream
.alloc_zeros::<f32>(n_backtests)
.context("alloc prev_mid_d")?;
let atr_mid_ema_d = stream
.alloc_zeros::<f32>(n_backtests)
.context("alloc atr_mid_ema_d")?;
let trail_hwm_d = stream
.alloc_zeros::<f32>(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<f32> {
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<f32> {
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<f32> {
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).