//! Stop-controller spec §9.1 unit tests. //! See docs/superpowers/specs/2026-05-19-isv-driven-stop-controller-design.md. use anyhow::Result; use ml_backtesting::policy::{ EnsembleAggregator, Strategy, StrategyConfig, SizingPolicyId, N_HORIZONS, }; use ml_backtesting::sim::{BatchedSimConfig, LobSimCuda, UniformSimParams}; use ml_core::device::MlDevice; fn level_book(mid: f32, tick: f32) -> ([f32; 10], [f32; 10], [f32; 10], [f32; 10]) { let mut bid_px = [0.0; 10]; let mut bid_sz = [0.0; 10]; let mut ask_px = [0.0; 10]; let mut ask_sz = [0.0; 10]; for k in 0..10 { bid_px[k] = mid - tick * (k as f32 + 1.0); ask_px[k] = mid + tick * (k as f32 + 1.0); bid_sz[k] = 20.0; ask_sz[k] = 20.0; } (bid_px, bid_sz, ask_px, ask_sz) } #[test] #[ignore = "requires CUDA"] fn atr_ema_bootstrap_first_observation() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Event 0: snapshot at mid=5500. After: prev_mid=5500, atr_mid_ema=0. let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; assert_eq!(sim.read_atr_mid_ema(0)?, 0.0, "ATR is 0 after first snapshot"); let prev0 = sim.read_prev_mid(0)?; assert!((prev0 - 5500.0).abs() < 1e-4, "prev_mid bootstrapped to 5500, got {prev0}"); // Event 1: snapshot at mid=5500.5. Δ=0.5. After: atr_mid_ema=0.5 (replace, not blend). let (bp, bs, ap, az) = level_book(5500.5, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let atr1 = sim.read_atr_mid_ema(0)?; assert!((atr1 - 0.5).abs() < 1e-4, "ATR after 2nd snapshot equals |Δmid|=0.5, got {atr1}"); // Events 2..200: jitter book. ATR stays positive and finite, bounded by max Δ. let mut mid = 5500.5_f32; let mut max_delta: f32 = 0.5; for i in 0..198 { let drift = if i % 3 == 0 { 0.25 } else if i % 3 == 1 { -0.25 } else { 0.0 }; mid += drift; max_delta = max_delta.max(drift.abs()); let (bp, bs, ap, az) = level_book(mid, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; } let atr_final = sim.read_atr_mid_ema(0)?; assert!(atr_final > 0.0 && atr_final.is_finite(), "ATR positive + finite after 200 snapshots, got {atr_final}"); assert!(atr_final <= max_delta + 1e-3, "ATR bounded by max observed Δ={max_delta}, got {atr_final}"); Ok(()) } fn cfg_default(n: usize) -> BatchedSimConfig { BatchedSimConfig::from_uniform(n, &UniformSimParams { target_annual_vol_units: 50.0, annualisation_factor: 825.0, max_lots: 5, latency_ns: 0, kelly_frac_floor: 0.20, sharpe_weight_floor: 0.10, threshold: 0.0, cost_per_lot_per_side: 0.0, max_hold_ns: 0, min_reasonable_px: 0.0, max_reasonable_px: f32::INFINITY, }) } #[test] #[ignore = "requires CUDA"] fn stop_check_skipped_when_flat() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; // Position is flat (just allocated, zeros). Stop check must not fire; // alpha decides the action. sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?; sim.step_decision_with_latency(0, &cfg_default(1))?; let (side, size) = sim.read_market_target(0)?; // Strong bullish alpha + flat position → entry side=0 size>=1. // Not (3, 0) (force-flat) and not unchanged from the initial alloc-zero state. assert_eq!(side, 0, "with flat position, stop must not write side=3; got side={side}"); assert!(size >= 1, "entry should fire under p=0.8 + flat (size={size})"); Ok(()) } use ml_backtesting::policy::IsvKellyStateHost; #[test] #[ignore = "requires CUDA"] fn sl_fires_when_unrealized_breaks_distance() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Seed isv: 5 horizons with pnl_ema_loss=2.0 → sl_distance=2.0. // pnl_ema_win=0.0 forces cold-start branch in decision kernel (kelly_frac → // floor=0.20, cap_lots → max_lots=5). With alpha=0.95 this gives // lots=round(0.9*0.20*5)=round(0.9)=1 — position opens. let seeded: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 2.0, win_rate_ema: 0.5, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.5, }); sim.write_isv_kelly(0, &seeded)?; // Two snapshots so atr_mid_ema becomes nonzero (small). let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Open a long position via strong-alpha + step_decision + manual fill. sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(0, &cfg_default(1))?; let mut ts: u64 = 1_000_000; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; } let pos_open = sim.read_pos(0)?; assert!(pos_open.position_lots > 0, "test setup: long position should open, got {}", pos_open.position_lots); // Drive mid down by 3.0 (>= sl_distance=2.0). Stop must fire force-flat. let (bp3, bs3, ap3, az3) = level_book(5497.0, 0.25); sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; let (side, size) = sim.read_market_target(0)?; assert_eq!(side, 3, "SL must fire with force-flat (3, 0); got side={side}"); assert_eq!(size, 0, "force-flat encoding has abs_sz=0; got size={size}"); Ok(()) } #[test] #[ignore = "requires CUDA"] fn trail_arms_then_fires() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Cold-start ISV (forces kelly_frac_floor + max_lots cap) so position opens. // pnl_ema_loss large enough that SL won't fire during the +5pt walk. let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 10.0, // big SL so trail fires first win_rate_ema: 0.0, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.0, }); sim.write_isv_kelly(0, &cold_start)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Open long. sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(0, &cfg_default(1))?; let mut ts: u64 = 1_000_000; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; } assert!(sim.read_pos(0)?.position_lots > 0, "setup: long opened"); // After opening, switch ISV to one with pnl_ema_win=1.5 (trail_distance target). // The trail check reads this fresh state on subsequent decision steps. let trail_state: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 1.5, pnl_ema_loss: 10.0, win_rate_ema: 0.5, n_trades_seen: 20, realised_return_var: 0.5, recent_sharpe: 0.5, }); sim.write_isv_kelly(0, &trail_state)?; // Walk mid up to ratchet trail HWM past trail_distance=1.5. for &m in &[5501.0_f32, 5502.0, 5503.0, 5504.0, 5505.0] { let (b, bs, a, asz) = level_book(m, 0.25); sim.apply_snapshot(&b, &bs, &a, &asz)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; ts += 1_000_000; } let hwm_armed = sim.read_trail_hwm(0)?; assert!(hwm_armed > 1.5, "HWM should ratchet past trail_distance=1.5, got {hwm_armed}"); // Drop mid by trail_distance+ε from peak to fire trail. let (b, bs, a, asz) = level_book(5502.0, 0.25); // drop from peak 5505 to 5502 = 3.0 sim.apply_snapshot(&b, &bs, &a, &asz)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; let (side, size) = sim.read_market_target(0)?; assert_eq!(side, 3, "trail must fire force-flat; got side={side}"); assert_eq!(size, 0); Ok(()) } #[test] #[ignore = "requires CUDA"] fn multi_horizon_mask_averages_emas() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Cold-start to open. After opening, overwrite ISV so averaging arithmetic // yields mean_sl = 3.0 across all open horizons in the mask: // h0.pnl_ema_loss=2.0, h1=4.0, h2=h3=h4=3.0 → mean = (2+4+3*3)/5 = 3.0 // attribution_mask = 0x1F (all 5 bits) because sharpe_weight_floor=0.10 // makes every horizon weight > 1e-9 in decision_policy_default. // Snapshots placed relative to vwap_entry to avoid ask-spread sensitivity. // Bit-pick-first (h0=2.0) would fire at Δ_entry=2.5 → test catches it. // Bit-pick-max (h1=4.0) would not fire at Δ_entry=4.5 → test catches it. // Correct mean (3.0) → no fire at Δ_entry=2.5, fire at Δ_entry=4.5. let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 10.0, win_rate_ema: 0.0, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.0, }); sim.write_isv_kelly(0, &cold_start)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(0, &cfg_default(1))?; let mut ts: u64 = 1_000_000; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; } let pos_open = sim.read_pos(0)?; assert!(pos_open.position_lots > 0, "setup: long opens, got {}", pos_open.position_lots); // Read the actual VWAP entry price so snapshot mids are placed relative to it, // making the test robust to ask-spread and tick size. let entry_px = pos_open.vwap_entry; assert!(entry_px > 0.0, "vwap_entry must be set after fill, got {entry_px}"); // Seed multi-horizon ISV state. pnl_ema_win=100.0 keeps trail_distance huge // so the trail never fires during the test. // See setup comment above: mean=3.0, regression discriminators at Δ=2.5/4.5. let mut seeded: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 100.0, pnl_ema_loss: 3.0, win_rate_ema: 0.5, n_trades_seen: 20, realised_return_var: 0.5, recent_sharpe: 0.5, }); seeded[0].pnl_ema_loss = 2.0; seeded[1].pnl_ema_loss = 4.0; // h2, h3, h4 remain 3.0. Mean = (2+4+3+3+3)/5 = 3.0. sim.write_isv_kelly(0, &seeded)?; // Snapshots placed so bid[0] = entry_px - delta_from_entry (for a long): // unrealized_pl_per_lot = bid[0] - entry_px = -delta_from_entry // sl_distance = fmaxf(mean_ema_loss=3.0, atr). ATR after these moderate // price moves stays < 3.0 (the move is ~2.5 from the open level), so // sl_distance == 3.0. (If ATR somehow > 3.0, sl_distance > 3.0 and the // no-fire leg is still safe; only the fire leg could be affected but // we use Δ=4.5 which gives 1.5× headroom above the 3.0 floor.) // // No-fire: Δ_from_entry = 2.5 — bid[0] = entry_px - 2.5 → mid = entry_px - 2.25 let mid_no_fire = entry_px - 2.25; let (bp3, bs3, ap3, az3) = level_book(mid_no_fire, 0.25); sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; let (side_at_25, _) = sim.read_market_target(0)?; assert_ne!(side_at_25, 3, "Δ=2.5 < mean_sl=3.0 must not fire under correct averaging; got side={side_at_25}"); ts += 1_000_000; let atr_at_check = sim.read_atr_mid_ema(0)?; assert!( atr_at_check < 3.0, "test precondition: ATR must stay below mean_sl=3.0 to make sl_distance = max(ema_loss=3.0, atr) = 3.0; got atr={atr_at_check}" ); // Fire: Δ_from_entry = 4.5 (> mean_sl=3.0 and > any reasonable ATR floor) // bid[0] = entry_px - 4.5 → mid = entry_px - 4.25 let mid_fire = entry_px - 4.25; let (bp4, bs4, ap4, az4) = level_book(mid_fire, 0.25); sim.apply_snapshot(&bp4, &bs4, &ap4, &az4)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; let (side_at_45, size_at_45) = sim.read_market_target(0)?; assert_eq!(side_at_45, 3, "Δ=4.5 > mean_sl=3.0 must fire force-flat; got side={side_at_45}"); assert_eq!(size_at_45, 0); Ok(()) } #[test] #[ignore = "requires CUDA"] fn bytecode_and_default_kernel_agree_on_stops() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; // Helper: drive one scenario using either the default kernel (use_program=false) // or the bytecode VM kernel (use_program=true). Returns the market_target after // a stop condition is triggered. fn drive_scenario(use_program: bool, dev: &MlDevice) -> Result<(i32, i32)> { let mut sim = LobSimCuda::new(1, dev)?; if use_program { // Upload an Ensemble(MaxConfidence) program so program_lens[0] != 0 // and the program kernel handles this backtest. let prog = Strategy::Ensemble { children: (0..N_HORIZONS as u8) .map(|h| Strategy::Leaf(StrategyConfig { horizon_idx: h, sizing_policy: SizingPolicyId::IsvKelly, max_concurrent_lots: 5, })) .collect(), aggregator: EnsembleAggregator::MaxConfidence, }; sim.upload_program(0, &prog.flatten())?; } // Cold-start ISV: pnl_ema_win=0.0, n_trades_seen=0, realised_return_var=0.0 // forces kelly_frac_floor + max_lots path so position opens. // pnl_ema_loss=10.0 keeps SL far enough that it won't fire during open setup. let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 10.0, win_rate_ema: 0.0, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.0, }); sim.write_isv_kelly(0, &cold_start)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Open long. sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(0, &cfg_default(1))?; let mut ts: u64 = 1_000_000; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; } let pos_open = sim.read_pos(0)?; assert!( pos_open.position_lots > 0, "setup ({} kernel): long must open, got {}", if use_program { "program" } else { "default" }, pos_open.position_lots ); // Set SL trigger setup: pnl_ema_loss=2.0 → sl_distance=2.0 (assuming ATR < 2.0). // pnl_ema_win=10.0 keeps trail_distance=10.0 so trail won't fire before SL. let triggered: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 10.0, pnl_ema_loss: 2.0, win_rate_ema: 0.5, n_trades_seen: 20, realised_return_var: 0.5, recent_sharpe: 0.5, }); sim.write_isv_kelly(0, &triggered)?; // Drive mid down well past sl_distance=2.0 relative to vwap_entry. // trigger_mid = entry - 4.0 → unrealized = bid[0] - entry = (mid - tick) - entry // = (entry - 4.0 - 0.25) - entry = -4.25 <= -2.0 → SL fires. let trigger_mid = pos_open.vwap_entry - 4.0; let (bp3, bs3, ap3, az3) = level_book(trigger_mid, 0.25); sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; sim.read_market_target(0) } let mt_default = drive_scenario(false, &dev)?; let mt_program = drive_scenario(true, &dev)?; assert_eq!( mt_default, mt_program, "default kernel and program kernel disagree on stop output: default={mt_default:?} program={mt_program:?}" ); assert_eq!( mt_default.0, 3, "both kernels must fire stop force-flat (side=3); got side={}", mt_default.0 ); assert_eq!(mt_default.1, 0, "force-flat encoding has abs_sz=0; got size={}", mt_default.1); Ok(()) } #[test] #[ignore = "requires CUDA"] fn position_target_not_additive() -> Result<()> { // latency=0: target (0, n) repeated must NOT accumulate the position // past n. Cap is max_lots=5. let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let cfg = cfg_default(1); let mut ts: u64 = 0; for _ in 0..10 { sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg)?; sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; } let pos = sim.read_pos(0)?; assert!(pos.position_lots.abs() <= 5, "|position_lots| must respect max_lots=5 with target semantics; got {}", pos.position_lots); Ok(()) } #[test] #[ignore = "requires CUDA"] fn position_target_not_additive_with_latency() -> Result<()> { // 200ms latency: in-flight orders must count toward effective_position // so re-submission of the same target produces delta=0 and no new order. let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let cfg_lat = BatchedSimConfig::from_uniform(1, &UniformSimParams { target_annual_vol_units: 50.0, annualisation_factor: 825.0, max_lots: 5, latency_ns: 200_000_000, kelly_frac_floor: 0.20, sharpe_weight_floor: 0.10, threshold: 0.0, cost_per_lot_per_side: 0.0, max_hold_ns: 0, min_reasonable_px: 0.0, max_reasonable_px: f32::INFINITY, }); let mut ts: u64 = 0; // 500 events of persistent target — most should produce delta=0. for _ in 0..500 { sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_lat)?; sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; } let pos = sim.read_pos(0)?; assert!(pos.position_lots.abs() <= 5, "|position_lots| must stay <= max_lots=5 under 200ms latency with in-flight summation; got {}", pos.position_lots); Ok(()) } #[test] #[ignore = "requires CUDA"] fn alpha_noop_side_2_preserved() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Seed ISV with large pnl_ema_loss (= 10.0) so sl_distance stays far enough // that the spread (0.25) and small price moves never fire the SL during the // open-position setup phase. Same pattern as other open-setup tests. let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 10.0, win_rate_ema: 0.0, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.0, }); sim.write_isv_kelly(0, &cold_start)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; let mut ts: u64 = 0; // Open long via strong alpha. for _ in 0..10 { sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; } let pos_open = sim.read_pos(0)?.position_lots; assert!(pos_open > 0, "setup: long opens with strong alpha, got {}", pos_open); // Weak alpha that produces sig_mag * 0.20 * cap_lots < 0.5, rounding lots=0 // → market_target = (2, 0). The pos must stay unchanged. // Compute: sig_mag = |0.55-0.5|*2 = 0.1; ss = 0.1*0.20*5 = 0.1; truncf(0.1+0.5)=0. for _ in 0..20 { sim.broadcast_alpha(&[0.55, 0.55, 0.55, 0.55, 0.55])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; } let pos_after = sim.read_pos(0)?.position_lots; assert_eq!(pos_after, pos_open, "side=2 must preserve position; opened={} after 20 weak-alpha events={}", pos_open, pos_after); Ok(()) } #[test] #[ignore = "requires CUDA"] fn trail_hwm_reset_on_close() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 10.0, win_rate_ema: 0.0, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.0, }); sim.write_isv_kelly(0, &cold_start)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Open long. sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; let mut ts: u64 = 1_000_000; sim.step_decision_with_latency(ts, &cfg_default(1))?; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } assert!(sim.read_pos(0)?.position_lots > 0, "setup: long opens"); // Switch ISV to trail-test state (pnl_ema_win=1.5 → trail_distance=1.5). let trail_state: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 1.5, pnl_ema_loss: 100.0, // big SL → only trail can fire win_rate_ema: 0.5, n_trades_seen: 20, realised_return_var: 0.5, recent_sharpe: 0.5, }); sim.write_isv_kelly(0, &trail_state)?; // Ratchet HWM up. for &m in &[5501.0_f32, 5502.0, 5503.0, 5504.0, 5505.0] { let (b, bs, a, asz) = level_book(m, 0.25); sim.apply_snapshot(&b, &bs, &a, &asz)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } let hwm_armed = sim.read_trail_hwm(0)?; assert!(hwm_armed > 1.5, "HWM should ratchet past trail_distance=1.5, got {hwm_armed}"); // Fire trail — drop mid past trail_distance from peak. let (b, bs, a, asz) = level_book(5502.0, 0.25); sim.apply_snapshot(&b, &bs, &a, &asz)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; let (s_fire, _) = sim.read_market_target(0)?; assert_eq!(s_fire, 3, "trail must fire force-flat"); // Drain so position flattens AND pnl_track runs. for _ in 0..10 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } let pos_closed = sim.read_pos(0)?; assert_eq!(pos_closed.position_lots, 0, "position must flatten after trail fire; got {}", pos_closed.position_lots); let hwm_after_close = sim.read_trail_hwm(0)?; assert_eq!(hwm_after_close, 0.0, "pnl_track_step must reset trail_hwm to 0 on close; got {hwm_after_close}"); Ok(()) } /// Validates that max_hold_ns fires a force-flat when the elapsed hold /// time exceeds the configured limit, and does NOT fire before that limit. /// /// Test geometry: /// - max_hold_ns = 100ms = 100_000_000ns /// - Open long at t=1ms via strong alpha /// - At entry_ts + 50ms: still under limit → must NOT fire (side != 3) /// - At entry_ts + 150ms: past limit → must fire force-flat (side=3, size=0) #[test] #[ignore = "requires CUDA"] fn max_hold_forces_close() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Cold-start ISV: large pnl_ema_loss so SL never fires during the test. let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 100.0, win_rate_ema: 0.0, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.0, }); sim.write_isv_kelly(0, &cold_start)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Config with max_hold_ns = 100ms. let cfg_max_hold = BatchedSimConfig::from_uniform(1, &UniformSimParams { target_annual_vol_units: 50.0, annualisation_factor: 825.0, max_lots: 5, latency_ns: 0, kelly_frac_floor: 0.20, sharpe_weight_floor: 0.10, threshold: 0.0, cost_per_lot_per_side: 0.0, max_hold_ns: 100_000_000, // 100ms min_reasonable_px: 0.0, max_reasonable_px: f32::INFINITY, }); // Open long at t=1ms via strong alpha. let mut ts: u64 = 1_000_000; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } assert!(sim.read_pos(0)?.position_lots > 0, "setup: long opens"); let entry_ts = ts; // At entry_ts + 50ms — still under 100ms hold. Book stable so SL/trail don't fire. ts = entry_ts + 50_000_000; let (bp3, bs3, ap3, az3) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; let (side_pre, _) = sim.read_market_target(0)?; assert_ne!(side_pre, 3, "elapsed 50ms < max_hold=100ms: must NOT fire; got side={side_pre}"); // At entry_ts + 150ms — past max_hold. ts = entry_ts + 150_000_000; let (bp4, bs4, ap4, az4) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp4, &bs4, &ap4, &az4)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; let (side_post, size_post) = sim.read_market_target(0)?; assert_eq!(side_post, 3, "elapsed 150ms > max_hold=100ms: must fire force-flat; got side={side_post}"); assert_eq!(size_post, 0); Ok(()) } /// Validates that NaN or Inf prices in an MBP-10 snapshot do not propagate /// into realized_pnl via the book walk → fill pipeline. /// /// Real MBP-10 data at session boundaries or gap-fills can contain NaN or Inf /// in level prices/sizes. apply_snapshot_kernel must sanitize them so the walk /// helpers never accumulate NaN/Inf cost, which would corrupt avg_px and /// subsequently realized_pnl, emitting the ±21,474,836 sentinel. #[test] #[ignore = "requires CUDA"] fn book_nan_inf_prices_dont_corrupt_realized_pnl() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Build a book with NaN at one level and Inf at another (real MBP-10 // data at session boundaries can contain these). let mut bid_px = [0.0_f32; 10]; let mut bid_sz = [0.0_f32; 10]; let mut ask_px = [0.0_f32; 10]; let mut ask_sz = [0.0_f32; 10]; for k in 0..10 { bid_px[k] = 5500.0 - 0.25 * (k as f32 + 1.0); ask_px[k] = 5500.0 + 0.25 * (k as f32 + 1.0); bid_sz[k] = 20.0; ask_sz[k] = 20.0; } // Inject corruption at level 3 (NaN price) and level 5 (Inf size). ask_px[3] = f32::NAN; bid_sz[5] = f32::INFINITY; // First snapshot bootstraps ATR + book. sim.apply_snapshot(&bid_px, &bid_sz, &ask_px, &ask_sz)?; // Second snapshot: same book; ATR converges; sanitization tested again. sim.apply_snapshot(&bid_px, &bid_sz, &ask_px, &ask_sz)?; // Open long via strong alpha. The fill must NOT propagate NaN/Inf into // pos.realized_pnl, even though the book has NaN at ask[3] and Inf at bid[5]. sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; let mut ts: u64 = 1_000_000; sim.step_decision_with_latency(ts, &cfg_default(1))?; for _ in 0..5 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } let pos = sim.read_pos(0)?; assert!( pos.realized_pnl.is_finite(), "realized_pnl must stay finite even with NaN/Inf in book; got {}", pos.realized_pnl ); // Position may or may not have opened (depending on which levels were used). // The invariant is finiteness, not a specific position state. Ok(()) } /// Validates that pnl_track resets scratch on close so that: /// (a) flat events after a close do NOT emit duplicate close records, and /// (b) the next open transition is captured correctly (exactly 1 record per trade). /// /// Without the scratch reset: prev_size stays = prior entry_size, so every flat /// event fires the close branch again (duplicates). The next open (0→1) sees /// prev=1, now=1 → neither branch fires → open record never captured → second /// close uses stale realized_at_open from trade 1. #[test] #[ignore = "requires CUDA"] fn pnl_track_resets_scratch_on_close() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 100.0, // huge SL so it doesn't fire win_rate_ema: 0.0, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.0, }); sim.write_isv_kelly(0, &cold_start)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Config with max_hold_ns = 100ms to force-close without needing SL/trail. let cfg_max_hold = BatchedSimConfig::from_uniform(1, &UniformSimParams { target_annual_vol_units: 50.0, annualisation_factor: 825.0, max_lots: 5, latency_ns: 0, kelly_frac_floor: 0.20, sharpe_weight_floor: 0.10, threshold: 0.0, cost_per_lot_per_side: 0.0, max_hold_ns: 100_000_000, // 100ms min_reasonable_px: 0.0, max_reasonable_px: f32::INFINITY, }); // Trade 1: open long, force-flatten via max_hold. let mut ts: u64 = 1_000_000; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } assert!(sim.read_pos(0)?.position_lots > 0, "trade 1: long opens"); // Force close trade 1 via max_hold: jump ts by 200ms. ts += 200_000_000; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; for _ in 0..5 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } assert_eq!(sim.read_pos(0)?.position_lots, 0, "trade 1: closed via max_hold"); let trades_after_t1 = sim.read_total_trade_count()?; assert_eq!(trades_after_t1, 1, "trade 1: exactly ONE record emitted (no duplicates); got {trades_after_t1}"); // Several flat events — pnl_track must NOT emit duplicate close records. for _ in 0..5 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } let trades_after_flat = sim.read_total_trade_count()?; assert_eq!(trades_after_flat, 1, "flat events: no duplicate close records; got {trades_after_flat}"); // Trade 2: re-open + close via max_hold. ts += 10_000_000; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } assert!(sim.read_pos(0)?.position_lots > 0, "trade 2: long opens"); // Force close trade 2. ts += 200_000_000; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; for _ in 0..5 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } assert_eq!(sim.read_pos(0)?.position_lots, 0, "trade 2: closed via max_hold"); let trades_after_t2 = sim.read_total_trade_count()?; assert_eq!(trades_after_t2, 2, "trade 2: exactly TWO records total (1 per trade, no missed open); got {trades_after_t2}"); Ok(()) } /// Validates that the trade-vol floor prevents sub-cost stop fires. /// /// Per pearl_trade_level_vol_for_stop_distance.md: at cold-start (var=0), /// trade_vol = sqrt(max(0, cost²)) = cost = 0.125. With ema_loss=0 and /// ATR decayed to ≈ 0.003 before entry, sl_distance = max(0, atr, 0.125) = 0.125. /// /// Test geometry (long position, tick=0.25, cost=0.125): /// entry fills at ask[0] = mid_fill + tick /// unrealized = bid[0] - entry = (mid_new - tick) - entry /// /// Boundary straddling trade_vol = 0.125: /// no-fire (u = -0.08): mid_new = entry + tick - 0.08 = entry + 0.17 /// |u| = 0.08 < trade_vol = 0.125 → NOT fire ✓ /// fire (u = -0.20): mid_new = entry + tick - 0.20 = entry + 0.05 /// |u| = 0.20 > trade_vol = 0.125 → fires ✓ /// /// ATR convergence: 25 identical snapshots (delta=0) decay ATR to ≈ 0.003 /// before opening. Both discriminating snaps produce ATR well below 0.125, /// so trade_vol is always the binding constraint in this cold-start scenario. #[test] #[ignore = "requires CUDA"] fn trade_vol_floor_prevents_sub_cost_stops() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Zero ema_loss: only ATR and cost floor govern sl_distance. // Large pnl_ema_win to keep trail_distance enormous, preventing trail fires. let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 100.0, pnl_ema_loss: 0.0, win_rate_ema: 0.0, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.0, }); sim.write_isv_kelly(0, &cold_start)?; // Bootstrap ATR: two different mids to seed first-observation atr_mid_ema. let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Converge ATR to ≈ 0 via 25 identical snapshots at 5500.1 (delta=0). // After 25 steps: atr ≈ 0.6^25 * 0.1 ≈ 0.003 — tiny baseline. for _ in 0..25 { let (bp, bs, ap, az) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; } // Open long with cost=0.125 (trade_vol floor at cold-start = cost = 0.125). let cfg_cost = BatchedSimConfig::from_uniform(1, &UniformSimParams { target_annual_vol_units: 50.0, annualisation_factor: 825.0, max_lots: 5, latency_ns: 0, kelly_frac_floor: 0.20, sharpe_weight_floor: 0.10, threshold: 0.0, cost_per_lot_per_side: 0.125, max_hold_ns: 0, min_reasonable_px: 0.0, max_reasonable_px: f32::INFINITY, }); sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(0, &cfg_cost)?; let mut ts: u64 = 1_000_000; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; } let pos_open = sim.read_pos(0)?; assert!(pos_open.position_lots > 0, "setup: long opens (got {})", pos_open.position_lots); let entry = pos_open.vwap_entry; // entry ≈ 5500.1 + tick(0.25) = 5500.35 // No-fire leg: unrealized = -0.08 (< trade_vol = cost = 0.125). // trade_vol = sqrt(max(var=0, cost²=0.015625)) = 0.125 // sl_distance = max(0, ATR≈0.003, 0.125) = 0.125; 0.08 < 0.125 → NOT fire ✓ // bid[0] = entry - 0.08 → mid = entry - 0.08 + tick(0.25) = entry + 0.17 let mid_no_fire = entry + 0.17; let (bp3, bs3, ap3, az3) = level_book(mid_no_fire, 0.25); sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?; // ATR update: delta ≈ |(entry+0.17) - 5500.1| — pumped slightly but still << 0.125 ✓ sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_cost)?; let (side_no_fire, _) = sim.read_market_target(0)?; assert_ne!(side_no_fire, 3, "unrealized=-0.08 < trade_vol=0.125: SL must NOT fire; got side={side_no_fire}"); ts += 1_000_000; // Fire leg: unrealized = -0.20 (> trade_vol = 0.125 → fires). // bid[0] = entry - 0.20 → mid = entry - 0.20 + tick(0.25) = entry + 0.05 let mid_fire = entry + 0.05; let (bp4, bs4, ap4, az4) = level_book(mid_fire, 0.25); sim.apply_snapshot(&bp4, &bs4, &ap4, &az4)?; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_cost)?; let (side_fire, size_fire) = sim.read_market_target(0)?; assert_eq!(side_fire, 3, "unrealized=-0.20 > trade_vol=0.125: SL must fire force-flat; got side={side_fire}"); assert_eq!(size_fire, 0); Ok(()) } /// Validates that a timestamp gap > 1 hour (session boundary / weekend halt) /// force-closes any open position before the next event is processed. /// /// max_hold_ns can't fire during a halt because no events advance current_ts. /// The session-gap check in resting_orders_step detects the jump and zeroes /// position_lots directly; pnl_track_step's existing close branch then emits /// exactly one TradeRecord. #[test] #[ignore = "requires CUDA"] fn session_gap_force_closes_open_positions() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 100.0, // huge SL — won't fire win_rate_ema: 0.0, n_trades_seen: 0, realised_return_var: 0.0, recent_sharpe: 0.0, }); sim.write_isv_kelly(0, &cold_start)?; let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Open long at t=1ms. let mut ts: u64 = 1_000_000; sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; } assert!(sim.read_pos(0)?.position_lots > 0, "setup: long opens"); let trades_before_gap = sim.read_total_trade_count()?; // Advance ts by 2 hours (> 1 hour SESSION_GAP_NS). Call step_resting_orders // with the new ts — session-gap force-close fires, then pnl_track detects // the prev!=0 && now==0 close transition and emits a TradeRecord. ts += 2 * 3_600_000_000_000u64; sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; assert_eq!( sim.read_pos(0)?.position_lots, 0, "session gap > 1h must force-close the position" ); let trades_after_gap = sim.read_total_trade_count()?; assert_eq!( trades_after_gap, trades_before_gap + 1, "session-gap close emits exactly one TradeRecord; got {} -> {}", trades_before_gap, trades_after_gap ); Ok(()) } /// Validates that a corrupt top-of-book snapshot is skipped entirely and the /// per-backtest `snapshots_skipped` counter is incremented. /// /// Bug C-b (smoke v74v4 2026-05-20): per-level sanitization (Task 15) zeros each /// unhealthy level individually. When ALL 10 levels are invalid (session-boundary /// event), the book becomes uniformly zero. apply_fill_to_pos then reads /// bid_px[0]=0/ask_px[0]=0 → vwap_entry=0 → trade record entry_px=0 (zero sentinel). /// /// Fix: pre-validate top-of-book in apply_snapshot_kernel. If bid_px[0]/ask_px[0] /// are non-finite or zero/negative, skip the entire snapshot — book, prev_mid, /// and atr_mid_ema remain unchanged. #[test] #[ignore = "requires CUDA"] fn corrupt_top_of_book_skips_snapshot_and_increments_counter() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Apply a valid book first to seed prev_mid and atr_mid_ema. let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; // Apply a second valid snapshot to advance ATR from 0. let (bp2, bs2, ap2, az2) = level_book(5500.5, 0.25); sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; let atr_before = sim.read_atr_mid_ema(0)?; let prev_mid_before = sim.read_prev_mid(0)?; // Sanity: atr_before must be nonzero after two valid snapshots with Δ=0.5. assert!(atr_before > 0.0, "ATR should be nonzero after two valid snapshots, got {atr_before}"); // Case 1: top-of-book bid_px[0] = NaN. Whole snapshot must be skipped. let mut bp_bad = bp2; bp_bad[0] = f32::NAN; sim.apply_snapshot(&bp_bad, &bs2, &ap2, &az2)?; let skipped = sim.read_snapshots_skipped()?; assert_eq!(skipped[0], 1, "corrupt top-of-book (NaN bid_px[0]) should increment skip counter to 1; got {}", skipped[0]); let atr_after_nan = sim.read_atr_mid_ema(0)?; let prev_mid_after_nan = sim.read_prev_mid(0)?; assert_eq!(atr_after_nan, atr_before, "ATR must not change on corrupt snapshot; before={atr_before} after={atr_after_nan}"); assert_eq!(prev_mid_after_nan, prev_mid_before, "prev_mid must not change on corrupt snapshot; before={prev_mid_before} after={prev_mid_after_nan}"); // Case 2: top-of-book bid_sz[0] = 0. Must also skip. let mut bs_bad = bs2; bs_bad[0] = 0.0; sim.apply_snapshot(&bp2, &bs_bad, &ap2, &az2)?; let skipped_2 = sim.read_snapshots_skipped()?; assert_eq!(skipped_2[0], 2, "zero top-of-book bid_sz[0] should increment skip counter to 2; got {}", skipped_2[0]); let atr_after_zero_sz = sim.read_atr_mid_ema(0)?; assert_eq!(atr_after_zero_sz, atr_before, "ATR must not change on zero-size corrupt snapshot; got {atr_after_zero_sz}"); // Case 3: valid snapshot after two corrupt skips must update ATR normally. let (bp3, bs3, ap3, az3) = level_book(5500.5, 0.25); sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?; let skipped_3 = sim.read_snapshots_skipped()?; assert_eq!(skipped_3[0], 2, "valid snapshot must not increment skip counter; got {}", skipped_3[0]); let atr_after_valid = sim.read_atr_mid_ema(0)?; // mid didn't move (same 5500.5 as prev), so delta=0, atr stays at atr_before. // The assertion here is that it is still finite and the state is reachable. assert!(atr_after_valid.is_finite(), "ATR must remain finite after valid snapshot following skips; got {atr_after_valid}"); // Case 4: valid snapshot with a different mid forces an ATR update, proving // the pipeline is unblocked after the skips. let (bp4, bs4, ap4, az4) = level_book(5501.5, 0.25); sim.apply_snapshot(&bp4, &bs4, &ap4, &az4)?; let atr_after_move = sim.read_atr_mid_ema(0)?; assert_ne!(atr_after_move, atr_before, "valid snapshot with Δmid=1.0 must update ATR; before={atr_before} after={atr_after_move}"); Ok(()) } /// Validates that all 6 NaN instrumentation counters (S1.20) are zero after /// LobSimCuda::new() with no kernel launches. This confirms the device buffers /// are correctly zero-initialized and the accessor compiles + works. #[test] #[ignore = "requires CUDA"] fn nan_counters_initialize_to_zero() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let sim = LobSimCuda::new(1, &dev)?; let counters = sim.read_nan_counters()?; assert_eq!(counters.nan_avg_px[0], 0, "nan_avg_px must be zero at init"); assert_eq!(counters.nan_realised[0], 0, "nan_realised must be zero at init"); assert_eq!(counters.nan_realized_pnl[0], 0, "nan_realized_pnl must be zero at init"); assert_eq!(counters.zero_vwap_at_open[0], 0, "zero_vwap_at_open must be zero at init"); assert_eq!(counters.saturated_vwap_at_open[0], 0, "saturated_vwap_at_open must be zero at init"); assert_eq!(counters.defensive_exit_clamp[0], 0, "defensive_exit_clamp must be zero at init"); Ok(()) } /// Validates that the parameterized price-range filter rejects real-data outliers. /// /// Audit (fxt-data-audit on ES.FUT_2024-Q1) found: /// bid_min = -$4.85 (negative bid) /// bid_p1 = $64.15 (sub-$100, far below ES range) /// ask_max = $53,012 (10x above any plausible ES price) /// /// The old hardcoded `< 1e8` threshold ($100M) never triggered on any of these. /// This test pins the corrected behavior: upload min=1000/max=20000, then verify /// that sub-range, super-range, and negative-bid snapshots all increment /// snapshots_skipped and leave prev_mid unchanged; valid ES snapshots pass through. #[test] #[ignore = "requires CUDA"] fn price_range_rejection_skips_snapshot() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; // Configure a tight ES-like range. sim.upload_price_range(&[1000.0], &[20000.0])?; // Valid snapshot at ES range — should pass and be applied. let (bp, bs, ap, az) = level_book(5500.0, 0.25); sim.apply_snapshot(&bp, &bs, &ap, &az)?; let skipped_after_valid = sim.read_snapshots_skipped()?; assert_eq!(skipped_after_valid[0], 0, "valid snapshot must not skip"); let prev_mid_after_valid = sim.read_prev_mid(0)?; assert!( (prev_mid_after_valid - 5500.0).abs() < 1e-4, "valid snapshot must set prev_mid to mid; got {prev_mid_after_valid}" ); // Sub-range top-of-book (e.g. $50) — should be skipped. let (bp_low, bs_low, ap_low, az_low) = level_book(50.0, 0.25); sim.apply_snapshot(&bp_low, &bs_low, &ap_low, &az_low)?; let skipped_after_low = sim.read_snapshots_skipped()?; assert_eq!(skipped_after_low[0], 1, "sub-range snapshot must skip"); let prev_mid_after_low = sim.read_prev_mid(0)?; assert!( (prev_mid_after_low - prev_mid_after_valid).abs() < 1e-4, "skipped snapshot must not mutate prev_mid; got {prev_mid_after_low}" ); // Super-range top-of-book ($50,000) — should be skipped. let (bp_high, bs_high, ap_high, az_high) = level_book(50000.0, 0.25); sim.apply_snapshot(&bp_high, &bs_high, &ap_high, &az_high)?; let skipped_after_high = sim.read_snapshots_skipped()?; assert_eq!(skipped_after_high[0], 2, "super-range snapshot must skip"); // Negative bid (bid[0] = -5.0) — should be skipped. let (mut bp_neg, bs_neg, ap_neg, az_neg) = level_book(5500.0, 0.25); bp_neg[0] = -5.0; sim.apply_snapshot(&bp_neg, &bs_neg, &ap_neg, &az_neg)?; let skipped_after_neg = sim.read_snapshots_skipped()?; assert_eq!(skipped_after_neg[0], 3, "negative bid must skip"); Ok(()) } /// S1.21: All per-vwap-write-site v2 counters initialize to zero at /// LobSimCuda construction; last_bad_path initializes to zero (no bad /// write observed yet). #[test] #[ignore = "requires CUDA"] fn nan_counters_v2_initialize_to_zero() -> Result<()> { let dev = match MlDevice::cuda(0) { Ok(d) => d, Err(e) => { eprintln!("skipping: cuda unavailable ({e})"); return Ok(()); } }; let sim = LobSimCuda::new(1, &dev)?; let c = sim.read_nan_counters_v2()?; assert_eq!(c.vwap_zero_open_flat[0], 0, "vwap_zero_open_flat must initialize to 0"); assert_eq!(c.vwap_huge_open_flat[0], 0, "vwap_huge_open_flat must initialize to 0"); assert_eq!(c.vwap_zero_scale_in[0], 0, "vwap_zero_scale_in must initialize to 0"); assert_eq!(c.vwap_huge_scale_in[0], 0, "vwap_huge_scale_in must initialize to 0"); assert_eq!(c.vwap_zero_flip[0], 0, "vwap_zero_flip must initialize to 0"); assert_eq!(c.vwap_huge_flip[0], 0, "vwap_huge_flip must initialize to 0"); assert_eq!(c.vwap_session_gap_was_bad[0], 0, "vwap_session_gap_was_bad must initialize to 0"); assert_eq!(c.last_bad_path[0], 0, "last_bad_path must initialize to 0 (no bad write)"); Ok(()) }