feat(ml-backtesting): cold-start stopgap — max-confidence bytecode policy (Q1/Tier1)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -220,6 +220,9 @@ struct SimVariant {
|
||||
#[serde(default)] max_lots: Option<u16>,
|
||||
#[serde(default)] kelly_frac_floor: Option<f32>,
|
||||
#[serde(default)] sharpe_weight_floor: Option<f32>,
|
||||
/// 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<bool>,
|
||||
}
|
||||
|
||||
fn default_n_parallel() -> usize { 1 }
|
||||
@@ -407,6 +410,7 @@ fn resolve_sim_variants(base: &SweepBase) -> Vec<ml_backtesting::sim::ResolvedSi
|
||||
sharpe_weight_floor: v.sharpe_weight_floor.unwrap_or(base.sharpe_weight_floor),
|
||||
threshold: v.threshold,
|
||||
cost_per_lot_per_side: v.cost_per_lot_per_side,
|
||||
use_cold_start_stopgap: v.use_cold_start_stopgap.unwrap_or(false),
|
||||
}).collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -25,12 +25,15 @@ base:
|
||||
# dbd500ecf is the post-trunk-grows training checkpoint.
|
||||
checkpoint: /feature-cache/alpha-perception-runs/dbd500ecf/trunk_best_h6000.bin
|
||||
sim_variants:
|
||||
# Threshold-tuning pass: threshold=0.0 captures the full max_conviction
|
||||
# distribution into convictions.bin + conviction_percentiles.json.
|
||||
# Use the resulting p60-p95 values to populate the real deployability
|
||||
# sweep's threshold axis. Cost stays at 1-tick realistic anchor so the
|
||||
# observed Sharpe is net-of-cost too — useful as the no-gate floor.
|
||||
- { name: t0c1l200, threshold: 0.0, cost_per_lot_per_side: 0.125, latency_ns: 200000000 }
|
||||
# Q1 stopgap smoke: validates the cold-start dilution diagnosis by
|
||||
# uploading a max-confidence Strategy bytecode to bypass
|
||||
# decision_policy_default's linear-weighted-mean aggregator. Same
|
||||
# threshold/cost/latency as the previous threshold-tuning smoke for
|
||||
# apples-to-apples comparison: previous run produced n_trades=0; if
|
||||
# use_cold_start_stopgap=true now produces n_trades > 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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -22,6 +22,12 @@ pub struct BatchedSimConfig {
|
||||
// per fill in apply_fill_to_pos.
|
||||
pub threshold: Vec<f32>,
|
||||
pub cost_per_lot_per_side: Vec<f32>,
|
||||
/// 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<bool>,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user