From fef59395563dc1d5d718cb3ef4e64c62ee8b9ef6 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Tue, 19 May 2026 22:00:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(ml-backtesting):=20cold-start=20stopgap=20?= =?UTF-8?q?=E2=80=94=20max-confidence=20bytecode=20policy=20(Q1/Tier1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The threshold-tuning smoke at 81decf40f produced n_trades=0 despite 74.6% of decisions having max_conv ≥ 0.30 — the linear-weighted-mean aggregator in decision_policy_default is structurally dilution-bound at cold-start (per spec §1). Q1 stopgap: when sim_variants[i].use_cold_start_stopgap = true, the harness uploads a max-confidence Strategy bytecode program for that backtest, routing decisions through decision_policy_program with OP_AGG_MAX_CONFIDENCE. Existing kernel; zero CUDA changes. Field additions (atomically across BatchedSimConfig + UniformSimParams + ResolvedSimVariant + SweepBase.SimVariant) — every UniformSimParams literal migrated to include use_cold_start_stopgap: false (default). The sweep YAML's sim_variants entry sets it to true only for the validation run; production deployability uses Q2's kernel fix instead. Sweep YAML (config/ml/sweep_smoke.yaml) flipped to use_cold_start_stopgap=true at threshold=0.0, cost=0.125 — same anchor as the threshold-tuning smoke that produced n_trades=0, for direct comparison. This is a VALIDATION step. Cluster smoke at this commit MUST produce n_trades > 100 + finite metrics. Q2's kernel CBSW immediately follows and deletes this entire stopgap atomically (field, harness branch, YAML setting, every literal). Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/fxt-backtest/src/main.rs | 4 ++ config/ml/sweep_smoke.yaml | 15 +++--- crates/ml-backtesting/src/harness.rs | 46 ++++++++++++++++--- .../ml-backtesting/src/sim/batched_config.rs | 13 ++++++ .../tests/decision_floor_coldstart.rs | 1 + .../ml-backtesting/tests/lob_sim_fixtures.rs | 1 + .../tests/lob_sim_integrated_fuzz.rs | 1 + .../tests/parallel_sim_correctness.rs | 1 + .../tests/threshold_and_cost.rs | 1 + 9 files changed, 70 insertions(+), 13 deletions(-) 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, }) }