diff --git a/bin/fxt-backtest/src/main.rs b/bin/fxt-backtest/src/main.rs index 5499c6ae7..ed93cdb4c 100644 --- a/bin/fxt-backtest/src/main.rs +++ b/bin/fxt-backtest/src/main.rs @@ -220,6 +220,9 @@ struct SimVariant { #[serde(default)] max_lots: Option, #[serde(default)] kelly_frac_floor: Option, #[serde(default)] sharpe_weight_floor: Option, + /// Q1 stopgap (Tier 1 only): when true, the harness uploads a + /// max-confidence Strategy bytecode program for this variant's backtest. + #[serde(default)] use_cold_start_stopgap: Option, } fn default_n_parallel() -> usize { 1 } @@ -407,6 +410,7 @@ fn resolve_sim_variants(base: &SweepBase) -> Vec 100, the + # diagnosis is confirmed and Q2 lands the proper kernel fix. + - { name: t0c1l200_stopgap, threshold: 0.0, cost_per_lot_per_side: 0.125, + latency_ns: 200000000, use_cold_start_stopgap: true } cells: - { name: smoke } diff --git a/crates/ml-backtesting/src/harness.rs b/crates/ml-backtesting/src/harness.rs index d2f87efd6..ec2e01c30 100644 --- a/crates/ml-backtesting/src/harness.rs +++ b/crates/ml-backtesting/src/harness.rs @@ -149,16 +149,11 @@ impl BacktestHarness { let mut sim = LobSimCuda::new(cfg.n_parallel, dev)?; - // Upload per-cell bytecode programs if the caller provided any. - // Empty Vec means every cell uses the hardcoded default policy. - for (b, strat) in cfg.strategies.iter().enumerate() { - let prog = strat.flatten(); - sim.upload_program(b, &prog)?; - } - // P6: if sim_config_override is provided (sweep runner grid-pack // flow), use it directly; otherwise build a uniform broadcast from // scalar cfg fields (smoke + fixture flows). + // Built BEFORE strategy upload so the Q1 stopgap branch can read + // sim_config.use_cold_start_stopgap[b]. let sim_config = match cfg.sim_config_override.clone() { Some(bc) => { anyhow::ensure!( @@ -179,10 +174,47 @@ impl BacktestHarness { sharpe_weight_floor: cfg.sharpe_weight_floor, threshold: cfg.threshold, cost_per_lot_per_side: cfg.cost_per_lot_per_side, + use_cold_start_stopgap: false, // Q1 stopgap defaults off in scalar-cfg path }, ), }; + // Q1 cold-start stopgap: when sim_config.use_cold_start_stopgap[b] + // is true, upload a max-confidence Strategy for that backtest. + // Routes through decision_policy_program (existing bytecode VM + // with OP_AGG_MAX_CONFIDENCE), bypassing the linear-weighted-mean + // dilution in decision_policy_default. Q2's kernel CBSW makes + // this redundant; the whole branch + field is deleted there. + let any_stopgap = sim_config.use_cold_start_stopgap.iter().any(|&b| b); + if any_stopgap { + let max_conf = crate::policy::Strategy::Ensemble { + children: (0..crate::policy::N_HORIZONS as u8) + .map(|h| crate::policy::Strategy::Leaf(crate::policy::StrategyConfig { + horizon_idx: h, + sizing_policy: crate::policy::SizingPolicyId::IsvKelly, + sl_tp_rules: crate::policy::StopRules::default(), + max_concurrent_lots: cfg.max_lots, + })) + .collect(), + aggregator: crate::policy::EnsembleAggregator::MaxConfidence, + }; + let prog = max_conf.flatten(); + for (b, &flag) in sim_config.use_cold_start_stopgap.iter().enumerate() { + if flag { + sim.upload_program(b, &prog)?; + } + } + } + + // Caller-provided strategies (legacy path) — only for backtests + // NOT already set up by the Q1 stopgap above. + for (b, strat) in cfg.strategies.iter().enumerate() { + if !sim_config.use_cold_start_stopgap.get(b).copied().unwrap_or(false) { + let prog = strat.flatten(); + sim.upload_program(b, &prog)?; + } + } + let pnl_curves = (0..cfg.n_parallel).map(|_| Vec::with_capacity(1024)).collect(); Ok(Self { cfg, diff --git a/crates/ml-backtesting/src/sim/batched_config.rs b/crates/ml-backtesting/src/sim/batched_config.rs index fced0f219..0990c3761 100644 --- a/crates/ml-backtesting/src/sim/batched_config.rs +++ b/crates/ml-backtesting/src/sim/batched_config.rs @@ -22,6 +22,12 @@ pub struct BatchedSimConfig { // per fill in apply_fill_to_pos. pub threshold: Vec, pub cost_per_lot_per_side: Vec, + /// Q1 stopgap (deleted in Q2 kernel CBSW): when true for backtest b, + /// the harness uploads a max-confidence Strategy bytecode program to + /// that backtest, bypassing the linear-weighted-mean dilution in + /// decision_policy_default. Validates the cold-start dilution + /// diagnosis. Default false; Q2's kernel CBSW makes this redundant. + pub use_cold_start_stopgap: Vec, } /// P6: one fully-resolved sim variant for the sweep runner's grid-pack @@ -38,6 +44,8 @@ pub struct ResolvedSimVariant { pub sharpe_weight_floor: f32, pub threshold: f32, pub cost_per_lot_per_side: f32, + /// Q1 stopgap (Tier 1 only — deleted in Q2). See BatchedSimConfig. + pub use_cold_start_stopgap: bool, } /// Convenience scalar input for `from_uniform`. Mirror of the historical @@ -52,6 +60,8 @@ pub struct UniformSimParams { pub sharpe_weight_floor: f32, pub threshold: f32, pub cost_per_lot_per_side: f32, + /// Q1 stopgap (Tier 1 only — deleted in Q2). See BatchedSimConfig. + pub use_cold_start_stopgap: bool, } impl BatchedSimConfig { @@ -69,6 +79,7 @@ impl BatchedSimConfig { sharpe_weight_floor: vec![p.sharpe_weight_floor; n], threshold: vec![p.threshold; n], cost_per_lot_per_side: vec![p.cost_per_lot_per_side; n], + use_cold_start_stopgap: vec![p.use_cold_start_stopgap; n], } } @@ -85,6 +96,7 @@ impl BatchedSimConfig { sharpe_weight_floor: variants.iter().map(|v| v.sharpe_weight_floor).collect(), threshold: variants.iter().map(|v| v.threshold).collect(), cost_per_lot_per_side: variants.iter().map(|v| v.cost_per_lot_per_side).collect(), + use_cold_start_stopgap: variants.iter().map(|v| v.use_cold_start_stopgap).collect(), } } @@ -98,6 +110,7 @@ impl BatchedSimConfig { ("sharpe_weight_floor", self.sharpe_weight_floor.len()), ("threshold", self.threshold.len()), ("cost_per_lot_per_side", self.cost_per_lot_per_side.len()), + ("use_cold_start_stopgap", self.use_cold_start_stopgap.len()), ]; for (name, l) in lens { if l != n { diff --git a/crates/ml-backtesting/tests/decision_floor_coldstart.rs b/crates/ml-backtesting/tests/decision_floor_coldstart.rs index eef07396f..3299798d0 100644 --- a/crates/ml-backtesting/tests/decision_floor_coldstart.rs +++ b/crates/ml-backtesting/tests/decision_floor_coldstart.rs @@ -28,6 +28,7 @@ fn cfg_uniform(n: usize, kelly: f32, sharpe: f32) -> BatchedSimConfig { sharpe_weight_floor: sharpe, threshold: 0.0, cost_per_lot_per_side: 0.0, + use_cold_start_stopgap: false, }) } diff --git a/crates/ml-backtesting/tests/lob_sim_fixtures.rs b/crates/ml-backtesting/tests/lob_sim_fixtures.rs index 00a60bfb6..b6828c756 100644 --- a/crates/ml-backtesting/tests/lob_sim_fixtures.rs +++ b/crates/ml-backtesting/tests/lob_sim_fixtures.rs @@ -259,6 +259,7 @@ fn run_book_fixture(path: &Path) -> Result<()> { sharpe_weight_floor: 0.10, threshold: 0.0, cost_per_lot_per_side: 0.0, + use_cold_start_stopgap: false, }, ); sim.step_decision(*ts_ns, &sim_cfg)?; diff --git a/crates/ml-backtesting/tests/lob_sim_integrated_fuzz.rs b/crates/ml-backtesting/tests/lob_sim_integrated_fuzz.rs index d3a1e94d5..c0b4ddbf4 100644 --- a/crates/ml-backtesting/tests/lob_sim_integrated_fuzz.rs +++ b/crates/ml-backtesting/tests/lob_sim_integrated_fuzz.rs @@ -131,6 +131,7 @@ fn run_integrated_fuzz(n_backtests: usize, n_events: usize, seed: u64) -> Result sharpe_weight_floor: 0.10, threshold: 0.0, cost_per_lot_per_side: 0.0, + use_cold_start_stopgap: false, }, ); sim.step_decision_with_latency(ts_ns, &sim_cfg)?; diff --git a/crates/ml-backtesting/tests/parallel_sim_correctness.rs b/crates/ml-backtesting/tests/parallel_sim_correctness.rs index 727fa9cd1..b7317636f 100644 --- a/crates/ml-backtesting/tests/parallel_sim_correctness.rs +++ b/crates/ml-backtesting/tests/parallel_sim_correctness.rs @@ -34,6 +34,7 @@ fn parallel_sim_equivalence_with_uniform_config() -> Result<()> { sharpe_weight_floor: 0.10, threshold: 0.0, cost_per_lot_per_side: 0.0, + use_cold_start_stopgap: false, }, ); sim.step_decision_with_latency(0, &cfg)?; diff --git a/crates/ml-backtesting/tests/threshold_and_cost.rs b/crates/ml-backtesting/tests/threshold_and_cost.rs index 88d41f4b6..085fa6dc0 100644 --- a/crates/ml-backtesting/tests/threshold_and_cost.rs +++ b/crates/ml-backtesting/tests/threshold_and_cost.rs @@ -20,6 +20,7 @@ fn cfg_with_threshold(n: usize, threshold: f32, cost: f32) -> BatchedSimConfig { sharpe_weight_floor: 0.10, threshold, cost_per_lot_per_side: cost, + use_cold_start_stopgap: false, }) }