diff --git a/crates/ml-backtesting/cuda/lob_state.cuh b/crates/ml-backtesting/cuda/lob_state.cuh index de2b33fd7..426bddefa 100644 --- a/crates/ml-backtesting/cuda/lob_state.cuh +++ b/crates/ml-backtesting/cuda/lob_state.cuh @@ -6,7 +6,9 @@ #ifndef LOB_STATE_CUH #define LOB_STATE_CUH -#define N_HORIZONS 5 +// Must stay in sync with crates/ml-alpha/src/heads.rs::N_HORIZONS and +// crates/ml-backtesting/src/lob/mod.rs::N_HORIZONS. +#define N_HORIZONS 3 #define MAX_LIMITS 32 #define MAX_STOPS 16 diff --git a/crates/ml-backtesting/src/lob/mod.rs b/crates/ml-backtesting/src/lob/mod.rs index d578b7c45..e0b3ca15b 100644 --- a/crates/ml-backtesting/src/lob/mod.rs +++ b/crates/ml-backtesting/src/lob/mod.rs @@ -2,7 +2,8 @@ //! See spec §2 (CUDA data layout) and §5b (state ownership). pub const BOOK_LEVELS: usize = 10; -pub const N_HORIZONS: usize = 5; +/// Re-export of `ml_alpha::heads::N_HORIZONS` — single source of truth. +pub const N_HORIZONS: usize = ml_alpha::heads::N_HORIZONS; pub const MAX_LIMITS: usize = 32; pub const MAX_STOPS: usize = 16; /// Bytes per LimitSlot — matches cuda/lob_state.cuh::LimitSlot. diff --git a/crates/ml-backtesting/src/policy/mod.rs b/crates/ml-backtesting/src/policy/mod.rs index 771f0e7ec..188e57c78 100644 --- a/crates/ml-backtesting/src/policy/mod.rs +++ b/crates/ml-backtesting/src/policy/mod.rs @@ -14,8 +14,8 @@ pub use sizing::{IsvKellyStateHost, SizingPolicyId}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -/// Mirrors `crates/ml-alpha/src/heads.rs::N_HORIZONS`. -pub const N_HORIZONS: usize = 5; +/// Re-export of `ml_alpha::heads::N_HORIZONS` — single source of truth. +pub const N_HORIZONS: usize = ml_alpha::heads::N_HORIZONS; /// One horizon-anchored leaf strategy. See spec §5 (per-horizon ISV-Kelly) + §6. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -194,11 +194,11 @@ mod tests { use super::*; #[test] - fn default_strategy_has_5_horizon_leaves() { + fn default_strategy_has_one_leaf_per_horizon() { let s = Strategy::default_for(3); match s { Strategy::Ensemble { children, aggregator } => { - assert_eq!(children.len(), 5); + assert_eq!(children.len(), N_HORIZONS); assert!(matches!( aggregator, EnsembleAggregator::WeightedByRealizedSharpe @@ -218,18 +218,19 @@ mod tests { } #[test] - fn default_strategy_flattens_to_5_emits_plus_agg_plus_write() { + fn default_strategy_flattens_to_emits_plus_agg_plus_write() { let s = Strategy::default_for(3); let p = s.flatten(); - assert_eq!(p.instructions.len(), 7); - for i in 0..5 { + // N_HORIZONS EmitPerHorizonSize + 1 AggWeightedSharpe + 1 WriteOrder. + assert_eq!(p.instructions.len(), N_HORIZONS + 2); + for i in 0..N_HORIZONS { assert_eq!(p.instructions[i].op, OpCode::EmitPerHorizonSize as u8); assert_eq!(p.instructions[i].arg0, i as u8); assert_eq!(p.instructions[i].arg1, 3); } - assert_eq!(p.instructions[5].op, OpCode::AggWeightedSharpe as u8); - assert_eq!(p.instructions[5].arg0, 5); - assert_eq!(p.instructions[6].op, OpCode::WriteOrder as u8); + assert_eq!(p.instructions[N_HORIZONS].op, OpCode::AggWeightedSharpe as u8); + assert_eq!(p.instructions[N_HORIZONS].arg0, N_HORIZONS as u8); + assert_eq!(p.instructions[N_HORIZONS + 1].op, OpCode::WriteOrder as u8); } #[test] diff --git a/crates/ml-backtesting/src/sim/mod.rs b/crates/ml-backtesting/src/sim/mod.rs index c9ac32f84..d322b564c 100644 --- a/crates/ml-backtesting/src/sim/mod.rs +++ b/crates/ml-backtesting/src/sim/mod.rs @@ -1748,7 +1748,7 @@ impl LobSimCuda { pub fn write_isv_kelly( &mut self, backtest_idx: usize, - states: &[crate::policy::IsvKellyStateHost; 5], + states: &[crate::policy::IsvKellyStateHost; N_HORIZONS], ) -> Result<()> { anyhow::ensure!( backtest_idx < self.n_backtests, @@ -1770,7 +1770,7 @@ impl LobSimCuda { pub fn read_isv_kelly( &self, backtest_idx: usize, - ) -> Result<[crate::policy::IsvKellyStateHost; 5]> { + ) -> Result<[crate::policy::IsvKellyStateHost; N_HORIZONS]> { anyhow::ensure!( backtest_idx < self.n_backtests, "backtest_idx {} >= n_backtests {}", @@ -1781,7 +1781,7 @@ impl LobSimCuda { self.stream.memcpy_dtoh(&self.isv_kelly_d, raw.as_mut_slice())?; let off = backtest_idx * N_HORIZONS * ISV_KELLY_STATE_BYTES; let slice = &raw[off..off + N_HORIZONS * ISV_KELLY_STATE_BYTES]; - let mut out = [crate::policy::IsvKellyStateHost::default(); 5]; + let mut out = [crate::policy::IsvKellyStateHost::default(); N_HORIZONS]; let bytes_out: &mut [u8] = bytemuck::cast_slice_mut(&mut out); bytes_out.copy_from_slice(slice); Ok(out) diff --git a/crates/ml-backtesting/tests/decision_floor_coldstart.rs b/crates/ml-backtesting/tests/decision_floor_coldstart.rs index 80355e67a..51c0e1bbe 100644 --- a/crates/ml-backtesting/tests/decision_floor_coldstart.rs +++ b/crates/ml-backtesting/tests/decision_floor_coldstart.rs @@ -64,7 +64,7 @@ fn cold_start_sentinel_state_still_fires_a_trade() -> Result<()> { // Do NOT seed isv_kelly — leave at zeros (alloc_zeros' sentinel). // Strong directional alpha across all horizons → conviction-driven // sig_mag = 0.6 for every horizon, dir = +1. - sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?; + sim.broadcast_alpha(&[0.8; N_HORIZONS])?; sim.step_decision_with_latency(0, &cfg_uniform(1, 0.20, 0.10))?; let (side, size) = sim.read_market_target(0)?; assert_eq!(side, 0, "cold-start with p_h=0.8 must produce a long; got side={side}"); @@ -96,7 +96,7 @@ fn post_first_loss_state_does_not_lock_out_further_trades() -> Result<()> { // Seed isv_kelly_d with the exact state pattern the smoke produced: // one closed-loss trade, large realised_return_var. Pre-fix, the // variance-derived cap collapses to ~0. - let post_loss: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { + let post_loss: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 10.18, // magnitude of the lone loss return win_rate_ema: 0.0, @@ -105,7 +105,7 @@ fn post_first_loss_state_does_not_lock_out_further_trades() -> Result<()> { recent_sharpe: -1.0, // very negative — would have starved the weight side too }); sim.write_isv_kelly(0, &post_loss)?; - sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?; + sim.broadcast_alpha(&[0.8; N_HORIZONS])?; sim.step_decision_with_latency(0, &cfg_uniform(1, 0.20, 0.10))?; let (side, size) = sim.read_market_target(0)?; assert_eq!(side, 0, "post-loss state must still fire a long with strong alpha (got side={side})"); @@ -151,7 +151,7 @@ fn cold_start_stopgap_bytecode_vm_fires_a_trade() -> Result<()> { let prog = max_conf.flatten(); sim.upload_program(0, &prog)?; - sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?; + sim.broadcast_alpha(&[0.8; N_HORIZONS])?; sim.step_decision_with_latency(0, &cfg_uniform(1, 0.20, 0.10))?; let (side, size) = sim.read_market_target(0)?; eprintln!("stopgap cold-start single-step: side={side} size={size}"); diff --git a/crates/ml-backtesting/tests/fixtures/decision_alpha_buy_close.json b/crates/ml-backtesting/tests/fixtures/decision_alpha_buy_close.json index d6e12772b..ba3f2646b 100644 --- a/crates/ml-backtesting/tests/fixtures/decision_alpha_buy_close.json +++ b/crates/ml-backtesting/tests/fixtures/decision_alpha_buy_close.json @@ -3,8 +3,6 @@ "n_backtests": 1, "warm_start_isv_kelly": [ [ - { "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 }, - { "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 }, { "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 }, { "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 }, { "pnl_ema_win": 2.0, "pnl_ema_loss": 0.5, "win_rate_ema": 0.8, "n_trades_seen": 50, "realised_return_var": 0.25, "recent_sharpe": 1.0 } @@ -20,7 +18,7 @@ }, { "type": "decision", - "alpha_probs": [0.5, 0.5, 0.5, 0.5, 0.9], + "alpha_probs": [0.5, 0.5, 0.9], "target_annual_vol_units": 50.0, "annualisation_factor": 1.0, "max_lots": 5, @@ -35,7 +33,7 @@ }, { "type": "decision", - "alpha_probs": [0.5, 0.5, 0.5, 0.5, 0.1], + "alpha_probs": [0.5, 0.5, 0.1], "target_annual_vol_units": 50.0, "annualisation_factor": 1.0, "max_lots": 5, @@ -51,8 +49,6 @@ "expected_min_trade_records": 1, "expected_isv_kelly_after": [ [ - { "n_trades_seen_min": 0, "n_trades_seen_max": 0 }, - { "n_trades_seen_min": 0, "n_trades_seen_max": 0 }, { "n_trades_seen_min": 0, "n_trades_seen_max": 0 }, { "n_trades_seen_min": 0, "n_trades_seen_max": 0 }, { "n_trades_seen_min": 51, "n_trades_seen_max": 51 } diff --git a/crates/ml-backtesting/tests/fixtures/decision_program_h4_only.json b/crates/ml-backtesting/tests/fixtures/decision_program_h4_only.json index ba00e1fe0..143851539 100644 --- a/crates/ml-backtesting/tests/fixtures/decision_program_h4_only.json +++ b/crates/ml-backtesting/tests/fixtures/decision_program_h4_only.json @@ -1,11 +1,9 @@ { "name": "decision_program_h4_only", "n_backtests": 1, - "comment": "Uploads a custom Strategy bytecode program (single Leaf at horizon 4) that bypasses the hardcoded WeightedByRealizedSharpe default. Should produce equivalent end-state when h4 is the only weighted horizon.", + "comment": "Uploads a custom Strategy bytecode program (single Leaf at horizon N_HORIZONS-1) that bypasses the hardcoded WeightedByRealizedSharpe default. Should produce equivalent end-state when only the last horizon is weighted.", "warm_start_isv_kelly": [ [ - { "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 }, - { "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 }, { "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 }, { "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 }, { "pnl_ema_win": 2.0, "pnl_ema_loss": 0.5, "win_rate_ema": 0.8, "n_trades_seen": 50, "realised_return_var": 0.25, "recent_sharpe": 1.0 } @@ -22,7 +20,7 @@ }, { "type": "decision", - "alpha_probs": [0.5, 0.5, 0.5, 0.5, 0.9], + "alpha_probs": [0.5, 0.5, 0.9], "target_annual_vol_units": 50.0, "annualisation_factor": 1.0, "max_lots": 5, diff --git a/crates/ml-backtesting/tests/lob_sim_fixtures.rs b/crates/ml-backtesting/tests/lob_sim_fixtures.rs index 4ec9249f3..76c49f884 100644 --- a/crates/ml-backtesting/tests/lob_sim_fixtures.rs +++ b/crates/ml-backtesting/tests/lob_sim_fixtures.rs @@ -5,7 +5,7 @@ use anyhow::{Context, Result}; use ml_backtesting::lob::BOOK_LEVELS; -use ml_backtesting::policy::IsvKellyStateHost; +use ml_backtesting::policy::{IsvKellyStateHost, N_HORIZONS}; use ml_backtesting::sim::LobSimCuda; use ml_core::device::MlDevice; use serde::Deserialize; @@ -28,7 +28,7 @@ enum FixtureEvent { ts_ns: u64, }, Decision { - alpha_probs: [f32; 5], + alpha_probs: [f32; N_HORIZONS], target_annual_vol_units: f32, annualisation_factor: f32, max_lots: u16, @@ -152,11 +152,11 @@ struct Fixture { #[serde(default)] expected_trade_records: Vec, #[serde(default)] - warm_start_isv_kelly: Vec<[IsvKellyFixture; 5]>, + warm_start_isv_kelly: Vec<[IsvKellyFixture; N_HORIZONS]>, #[serde(default)] expected_min_trade_records: usize, #[serde(default)] - expected_isv_kelly_after: Vec<[ExpectedIsvKelly; 5]>, + expected_isv_kelly_after: Vec<[ExpectedIsvKelly; N_HORIZONS]>, #[serde(default)] expected_limit_slot_states: Vec, #[serde(default)] @@ -202,11 +202,12 @@ fn run_book_fixture(path: &Path) -> Result<()> { .map_err(|e| anyhow::anyhow!("cuda device: {e}"))?; let mut sim = LobSimCuda::new(fx.n_backtests, &dev)?; - // Upload a single-leaf h4 Strategy program for backtest 0 if requested. + // Upload a single-leaf longest-horizon Strategy program for backtest 0 + // if requested. Horizon idx = N_HORIZONS-1 (longest available horizon). if fx.upload_h4_leaf_program { use ml_backtesting::policy::{SizingPolicyId, Strategy, StrategyConfig}; let leaf = Strategy::Leaf(StrategyConfig { - horizon_idx: 4, + horizon_idx: (N_HORIZONS - 1) as u8, sizing_policy: SizingPolicyId::IsvKelly, max_concurrent_lots: 5, }); @@ -216,13 +217,8 @@ fn run_book_fixture(path: &Path) -> Result<()> { // Apply ISV-Kelly warm-start if provided (one row per backtest). for (b, states) in fx.warm_start_isv_kelly.iter().enumerate() { - let host_states: [IsvKellyStateHost; 5] = [ - states[0].to_host(), - states[1].to_host(), - states[2].to_host(), - states[3].to_host(), - states[4].to_host(), - ]; + let host_states: [IsvKellyStateHost; N_HORIZONS] = + std::array::from_fn(|h| states[h].to_host()); sim.write_isv_kelly(b, &host_states)?; } @@ -345,7 +341,7 @@ fn run_book_fixture(path: &Path) -> Result<()> { if !fx.expected_isv_kelly_after.is_empty() { for (b, expected) in fx.expected_isv_kelly_after.iter().enumerate() { let got = sim.read_isv_kelly(b)?; - for h in 0..5 { + for h in 0..N_HORIZONS { let n = got[h].n_trades_seen; assert!( n >= expected[h].n_trades_seen_min && n <= expected[h].n_trades_seen_max, diff --git a/crates/ml-backtesting/tests/lob_sim_integrated_fuzz.rs b/crates/ml-backtesting/tests/lob_sim_integrated_fuzz.rs index 88885b7dc..d44ddc97a 100644 --- a/crates/ml-backtesting/tests/lob_sim_integrated_fuzz.rs +++ b/crates/ml-backtesting/tests/lob_sim_integrated_fuzz.rs @@ -13,7 +13,7 @@ //! resting-order pressure + trade-flow simulation. use anyhow::Result; -use ml_backtesting::policy::IsvKellyStateHost; +use ml_backtesting::policy::{IsvKellyStateHost, N_HORIZONS}; use ml_backtesting::sim::LobSimCuda; use ml_core::device::MlDevice; use rand::{Rng, SeedableRng}; @@ -37,12 +37,12 @@ fn make_initial_book() -> ([f32; BOOK_LEVELS], [f32; BOOK_LEVELS], [f32; BOOK_LE (bid_px, bid_sz, ask_px, ask_sz) } -fn random_warm_start_isv(rng: &mut ChaCha8Rng) -> [IsvKellyStateHost; 5] { - let mut out: [IsvKellyStateHost; 5] = Default::default(); - for h in 0..5 { +fn random_warm_start_isv(rng: &mut ChaCha8Rng) -> [IsvKellyStateHost; N_HORIZONS] { + let mut out: [IsvKellyStateHost; N_HORIZONS] = Default::default(); + for h in 0..N_HORIZONS { // Make at least one horizon credibly profitable so the decision // kernel actually opens positions; others get random warm-starts. - let positive_h = h == 4; + let positive_h = h == N_HORIZONS - 1; let win_rate: f32 = if positive_h { 0.7 } else { rng.gen_range(0.3..0.6) }; let pnl_win: f32 = if positive_h { 2.0 } else { rng.gen_range(0.5..1.5) }; let pnl_loss: f32 = rng.gen_range(0.3..1.0); @@ -113,8 +113,8 @@ fn run_integrated_fuzz(n_backtests: usize, n_events: usize, seed: u64) -> Result // Decide every 4th event. if event_idx % 4 == 0 { - let mut probs = [0.5_f32; 5]; - for h in 0..5 { + let mut probs = [0.5_f32; N_HORIZONS]; + for h in 0..N_HORIZONS { probs[h] = rng.gen_range(0.1..0.9); } sim.broadcast_alpha(&probs)?; diff --git a/crates/ml-backtesting/tests/parallel_sim_correctness.rs b/crates/ml-backtesting/tests/parallel_sim_correctness.rs index cd0891c31..ef089602b 100644 --- a/crates/ml-backtesting/tests/parallel_sim_correctness.rs +++ b/crates/ml-backtesting/tests/parallel_sim_correctness.rs @@ -8,6 +8,7 @@ //! kernel where the read_first_inflight_arrival_ts helper exists. use anyhow::Result; +use ml_backtesting::policy::N_HORIZONS; use ml_backtesting::sim::{BatchedSimConfig, LobSimCuda, UniformSimParams}; use ml_core::device::MlDevice; @@ -22,7 +23,7 @@ fn parallel_sim_equivalence_with_uniform_config() -> Result<()> { } }; let mut sim = LobSimCuda::new(8, &dev)?; - sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?; + sim.broadcast_alpha(&[0.8; N_HORIZONS])?; let cfg = BatchedSimConfig::from_uniform( 8, &UniformSimParams { diff --git a/crates/ml-backtesting/tests/ring3_replay.rs b/crates/ml-backtesting/tests/ring3_replay.rs index 1a329e487..1c5b35097 100644 --- a/crates/ml-backtesting/tests/ring3_replay.rs +++ b/crates/ml-backtesting/tests/ring3_replay.rs @@ -44,7 +44,7 @@ fn try_loader() -> Option { files, predecoded_dir: mbp10.clone(), seq_len: 1, - horizons: [30, 100, 300, 1000, 6000], + horizons: ml_alpha::heads::HORIZONS, n_max_sequences: 0, seed: 0xCAFE_F00D, inference_only: true, diff --git a/crates/ml-backtesting/tests/stop_controller.rs b/crates/ml-backtesting/tests/stop_controller.rs index 140558e88..f5b379ced 100644 --- a/crates/ml-backtesting/tests/stop_controller.rs +++ b/crates/ml-backtesting/tests/stop_controller.rs @@ -92,7 +92,7 @@ fn stop_check_skipped_when_flat() -> Result<()> { // 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.broadcast_alpha(&[0.8; N_HORIZONS])?; sim.step_decision_with_latency(0, &cfg_default(1))?; let (side, size) = sim.read_market_target(0)?; @@ -118,7 +118,7 @@ fn sl_fires_when_unrealized_breaks_distance() -> Result<()> { // 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 { + let seeded: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 2.0, win_rate_ema: 0.5, @@ -135,7 +135,7 @@ fn sl_fires_when_unrealized_breaks_distance() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(0, &cfg_default(1))?; let mut ts: u64 = 1_000_000; for _ in 0..3 { @@ -149,7 +149,7 @@ fn sl_fires_when_unrealized_breaks_distance() -> Result<()> { // 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; let (side, size) = sim.read_market_target(0)?; @@ -169,7 +169,7 @@ fn trail_arms_then_fires() -> Result<()> { // 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 { + let cold_start: [IsvKellyStateHost; N_HORIZONS] = 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, @@ -185,7 +185,7 @@ fn trail_arms_then_fires() -> Result<()> { sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Open long. - sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; + sim.broadcast_alpha(&[0.95; N_HORIZONS])?; 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; } @@ -193,7 +193,7 @@ fn trail_arms_then_fires() -> Result<()> { // 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 { + let trail_state: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 1.5, pnl_ema_loss: 10.0, win_rate_ema: 0.5, @@ -207,7 +207,7 @@ fn trail_arms_then_fires() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; ts += 1_000_000; } @@ -217,7 +217,7 @@ fn trail_arms_then_fires() -> Result<()> { // 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; let (side, size) = sim.read_market_target(0)?; @@ -244,7 +244,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> { // 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 { + let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 10.0, win_rate_ema: 0.0, @@ -259,7 +259,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; 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; } @@ -274,7 +274,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> { // 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 { + let mut seeded: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 100.0, pnl_ema_loss: 3.0, win_rate_ema: 0.5, @@ -299,7 +299,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; let (side_at_25, _) = sim.read_market_target(0)?; assert_ne!(side_at_25, 3, @@ -317,7 +317,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; 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}"); @@ -358,7 +358,7 @@ fn bytecode_and_default_kernel_agree_on_stops() -> Result<()> { // 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 { + let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 10.0, win_rate_ema: 0.0, @@ -374,7 +374,7 @@ fn bytecode_and_default_kernel_agree_on_stops() -> Result<()> { sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Open long. - sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; + sim.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(0, &cfg_default(1))?; let mut ts: u64 = 1_000_000; for _ in 0..3 { @@ -391,7 +391,7 @@ fn bytecode_and_default_kernel_agree_on_stops() -> Result<()> { // 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 { + let triggered: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 10.0, pnl_ema_loss: 2.0, win_rate_ema: 0.5, @@ -407,7 +407,7 @@ fn bytecode_and_default_kernel_agree_on_stops() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; sim.read_market_target(0) } @@ -444,7 +444,7 @@ fn position_target_not_additive() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg)?; sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; @@ -487,7 +487,7 @@ fn position_target_not_additive_with_latency() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_lat)?; sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; @@ -508,7 +508,7 @@ fn trail_hwm_reset_on_close() -> Result<()> { }; let mut sim = LobSimCuda::new(1, &dev)?; - let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { + let cold_start: [IsvKellyStateHost; N_HORIZONS] = 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, @@ -521,14 +521,14 @@ fn trail_hwm_reset_on_close() -> Result<()> { sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?; // Open long. - sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; + sim.broadcast_alpha(&[0.95; N_HORIZONS])?; 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 { + let trail_state: [IsvKellyStateHost; N_HORIZONS] = 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, @@ -539,7 +539,7 @@ fn trail_hwm_reset_on_close() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; @@ -551,7 +551,7 @@ fn trail_hwm_reset_on_close() -> Result<()> { // 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.broadcast_alpha(&[0.95; N_HORIZONS])?; 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"); @@ -593,7 +593,7 @@ fn max_hold_forces_close() -> Result<()> { 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 { + let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost { pnl_ema_win: 0.0, pnl_ema_loss: 100.0, win_rate_ema: 0.0, @@ -626,7 +626,7 @@ fn max_hold_forces_close() -> Result<()> { // 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.broadcast_alpha(&[0.95; N_HORIZONS])?; 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"); @@ -693,7 +693,7 @@ fn book_nan_inf_prices_dont_corrupt_realized_pnl() -> Result<()> { // 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])?; + sim.broadcast_alpha(&[0.95; N_HORIZONS])?; let mut ts: u64 = 1_000_000; sim.step_decision_with_latency(ts, &cfg_default(1))?; for _ in 0..5 { @@ -730,7 +730,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> { }; let mut sim = LobSimCuda::new(1, &dev)?; - let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { + let cold_start: [IsvKellyStateHost; N_HORIZONS] = 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, @@ -760,7 +760,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> { // 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; @@ -771,7 +771,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> { // 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; for _ in 0..5 { sim.step_resting_orders(ts, 0.0)?; @@ -795,7 +795,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> { // 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; @@ -806,7 +806,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> { // Force close trade 2. ts += 200_000_000; - sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; + sim.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_max_hold)?; for _ in 0..5 { sim.step_resting_orders(ts, 0.0)?; @@ -850,7 +850,7 @@ fn trade_vol_floor_prevents_sub_cost_stops() -> Result<()> { // 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 { + let cold_start: [IsvKellyStateHost; N_HORIZONS] = 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, @@ -886,7 +886,7 @@ fn trade_vol_floor_prevents_sub_cost_stops() -> Result<()> { delta_floor: 0.0, }); - sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?; + sim.broadcast_alpha(&[0.95; N_HORIZONS])?; 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; } @@ -903,7 +903,7 @@ fn trade_vol_floor_prevents_sub_cost_stops() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_cost)?; let (side_no_fire, _) = sim.read_market_target(0)?; assert_ne!(side_no_fire, 3, @@ -915,7 +915,7 @@ fn trade_vol_floor_prevents_sub_cost_stops() -> Result<()> { 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_cost)?; let (side_fire, size_fire) = sim.read_market_target(0)?; assert_eq!(side_fire, 3, @@ -940,7 +940,7 @@ fn session_gap_force_closes_open_positions() -> Result<()> { }; let mut sim = LobSimCuda::new(1, &dev)?; - let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { + let cold_start: [IsvKellyStateHost; N_HORIZONS] = 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, @@ -954,7 +954,7 @@ fn session_gap_force_closes_open_positions() -> Result<()> { // 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.broadcast_alpha(&[0.95; N_HORIZONS])?; sim.step_decision_with_latency(ts, &cfg_default(1))?; for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; @@ -1231,15 +1231,14 @@ fn multi_horizon_conviction_cancels_on_disagreement() -> Result<()> { delta_floor: 0.0, }); - // Two horizons bullish (0.7), two bearish (0.3), one neutral (0.5). - // Magnitudes: [0.4, 0.4, 0.4, 0.4, 0.0]. Directions: [+1, -1, +1, -1, +1]. + // One horizon bullish (0.7), one bearish (0.3), one neutral (0.5). + // Magnitudes: [0.4, 0.4, 0.0]. Directions: [+1, -1, +1]. // At cold start (ISV all zero, cost=1.0), every weight_h is // eps_edge / (0 + cost²) = 0.01 — equal. Weighted signed sum: - // 0.4*0.01*(+1) + 0.4*0.01*(-1) + 0.4*0.01*(+1) + 0.4*0.01*(-1) + 0 - // = 0 + // 0.4*0.01*(+1) + 0.4*0.01*(-1) + 0 = 0 // conviction_signed = 0 / total_abs_weight = 0 // → conv_ema = 0 → target_lots = 0 → side ∈ {2, 3}. - let mixed: [f32; N_HORIZONS] = [0.7, 0.3, 0.7, 0.3, 0.5]; + let mixed: [f32; N_HORIZONS] = [0.7, 0.3, 0.5]; sim.broadcast_alpha(&mixed)?; sim.step_decision_with_latency(1_000_000_000u64, &cfg)?; let (side, size) = sim.read_market_target(0)?; diff --git a/crates/ml-backtesting/tests/threshold_and_cost.rs b/crates/ml-backtesting/tests/threshold_and_cost.rs index dd9224f71..37322f265 100644 --- a/crates/ml-backtesting/tests/threshold_and_cost.rs +++ b/crates/ml-backtesting/tests/threshold_and_cost.rs @@ -7,6 +7,7 @@ //! contract in isolation. use anyhow::Result; +use ml_backtesting::policy::N_HORIZONS; use ml_backtesting::sim::{BatchedSimConfig, LobSimCuda, UniformSimParams}; use ml_core::device::MlDevice; @@ -39,7 +40,7 @@ fn threshold_gate_skips_low_conviction() -> Result<()> { // identical across horizons. With uniform alpha=0.51 across all 5 // horizons (all bullish), magnitude_h = 0.02 and conviction_signed = // 0.02 — well below threshold = 0.10. Kernel writes side=2 (no-op). - sim.broadcast_alpha(&[0.51, 0.51, 0.51, 0.51, 0.51])?; + sim.broadcast_alpha(&[0.51; N_HORIZONS])?; sim.step_decision_with_latency(0, &cfg_with_threshold(1, 0.10, 1.0))?; let (side, size) = sim.read_market_target(0)?; assert_eq!(side, 2, "side should be noop under threshold gate; got side={side}"); @@ -58,7 +59,7 @@ fn threshold_gate_allows_high_conviction() -> Result<()> { // CRT.1 C1.2: uniform alpha=0.8 across horizons → magnitude_h = 0.6, // direction_h = +1 for all → conviction_signed = 0.6, above threshold // = 0.10. target_lots = round(1 * 0.6 * 5) = 3. - sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?; + sim.broadcast_alpha(&[0.8; N_HORIZONS])?; sim.step_decision_with_latency(0, &cfg_with_threshold(1, 0.10, 1.0))?; let (side, size) = sim.read_market_target(0)?; assert_eq!(side, 0, "side should be buy with strong alpha; got side={side}"); @@ -76,7 +77,7 @@ fn threshold_zero_is_passthrough() -> Result<()> { Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); } }; let mut sim = LobSimCuda::new(1, &dev)?; - sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?; + sim.broadcast_alpha(&[0.8; N_HORIZONS])?; sim.step_decision_with_latency(0, &cfg_with_threshold(1, 0.0, 1.0))?; let (side, size) = sim.read_market_target(0)?; assert_eq!(side, 0, "threshold=0 with strong alpha should pass through; got side={side}"); diff --git a/crates/ml-backtesting/tests/trainer_parity.rs b/crates/ml-backtesting/tests/trainer_parity.rs index 074686bd3..2b5468993 100644 --- a/crates/ml-backtesting/tests/trainer_parity.rs +++ b/crates/ml-backtesting/tests/trainer_parity.rs @@ -31,7 +31,7 @@ fn try_loader(inference_only: bool) -> Option { files, predecoded_dir: mbp10.clone(), seq_len: 1, - horizons: [30, 100, 300, 1000, 6000], + horizons: ml_alpha::heads::HORIZONS, n_max_sequences: 1, seed: 0xCAFEF00D, inference_only,