From 2130bee0060f64c8c09323f6ff8b2b1efe8e02b1 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Tue, 19 May 2026 21:53:28 +0200 Subject: [PATCH] plan(ml-backtesting): CBSW cold-start aggregator implementation plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4-task atomic ladder (Q1-Q4) implementing spec 566e8bcb0. Each task maps to one commit per feedback_no_partial_refactor: Q1: Cold-start stopgap (bytecode max-confidence policy upload) Q2: CBSW kernel aggregator + Tier 1 revert (atomic) Q3: Memory pearl pearl_conviction_bootstrap_for_kelly_aggregation Q4: Parallelism spec §3.4 cross-reference Kernel ABI unchanged across all 4 commits — Q2 modifies only decision_policy_default's body (decision_policy_program left as a pluggable bytecode-VM experiment surface). Q2 atomically deletes the Q1 stopgap field/CLI/YAML/harness branch in the same commit that lands the kernel fix (feedback_no_legacy_aliases compliance: grep returns zero hits for use_cold_start_stopgap post-Q2). Validation gates per spec §9: - Q1: cluster smoke n_trades > 100, total_pnl != 0 (diagnosis confirmation). If n_trades = 0 still, STOP — downstream bug. - Q2: pre-existing 11 CUDA tests still pass; 7 new cbsw_* regression tests pass; cluster smoke n_trades > 100; ev/s rate ratio Q2/Q1 ≥ 0.95. Self-review confirms all 11 spec sections covered, no placeholders, type/signature consistency across tasks. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-19-cbsw-cold-start-aggregator.md | 1010 +++++++++++++++++ 1 file changed, 1010 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-19-cbsw-cold-start-aggregator.md diff --git a/docs/superpowers/plans/2026-05-19-cbsw-cold-start-aggregator.md b/docs/superpowers/plans/2026-05-19-cbsw-cold-start-aggregator.md new file mode 100644 index 000000000..45836712a --- /dev/null +++ b/docs/superpowers/plans/2026-05-19-cbsw-cold-start-aggregator.md @@ -0,0 +1,1010 @@ +# CBSW Cold-Start Aggregator Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix the cold-start dilution bug that produced `n_trades=0` on the deployability smoke. Land CBSW (Conviction-Bootstrapped Sharpe Weighting) in `decision_policy_default` via 4 atomic commits: bytecode stopgap (validation) → kernel CBSW (proper fix) → memory pearl → parallelism spec cross-ref. + +**Architecture:** Hybrid max-confidence × weighted-sharpe aggregator with a per-horizon piecewise-linear ramp keyed on `n_trades_seen vs MIN_TRADES_FOR_VAR_CAP=10`. Cold-start = pure max-confidence (sq=0); mature = pure weighted-sharpe (sq=1). Permanent floor preserved. + +**Tech Stack:** Rust 1.85, cudarc 0.19, CUDA 12.4 (sm_89 L40S local sm_86), Argo Workflows v4.0.2. + +**Spec:** `docs/superpowers/specs/2026-05-19-cbsw-cold-start-aggregator-design.md` (commit `566e8bcb0`) + +**Latest baseline commit:** `81decf40f` (threshold-tuning smoke + conviction_log side-channel landed; threshold-tuning data captured in `convictions.bin` proving the diagnosis) + +**Phasing:** 4 atomic phases (Q1–Q4). Each phase lands as ONE commit per `feedback_no_partial_refactor`. Kernel ABI unchanged across all four (Q2 modifies only `decision_policy_default`'s body). All CUDA tests stay `#[ignore = "requires CUDA"]` and run locally on RTX 3050 Ti via `SQLX_OFFLINE=true cargo test -- --ignored --nocapture`. Each phase ends with a measurement gate. + +--- + +## Task 1 (Q1): Cold-start stopgap — bytecode max-confidence policy + +**Goal:** Validate the dilution-bug diagnosis with zero kernel changes. Upload a max-confidence Strategy bytecode program to every backtest when the new `use_cold_start_stopgap` flag is set. The existing `decision_policy_program` kernel handles `OP_AGG_MAX_CONFIDENCE` already. If trades fire at threshold=0.0, cost=0.125 → diagnosis confirmed. + +**Files:** +- Modify: `crates/ml-backtesting/src/sim/batched_config.rs` +- Modify: `crates/ml-backtesting/src/sim/mod.rs` (UniformSimParams field add; no kernel changes) +- Modify: `crates/ml-backtesting/src/harness.rs` +- Modify: `crates/ml-backtesting/tests/{decision_floor_coldstart, lob_sim_integrated_fuzz, lob_sim_fixtures, parallel_sim_correctness, threshold_and_cost}.rs` (add new field to existing `UniformSimParams` constructors, default `false`) +- Modify: `bin/fxt-backtest/src/main.rs` +- Modify: `config/ml/sweep_smoke.yaml` + +### Steps + +- [ ] **Step 1: Baseline tests pass on RTX 3050 (sanity)** + +```bash +cd /home/jgrusewski/Work/foxhunt +SQLX_OFFLINE=true cargo test -p ml-backtesting --test decision_floor_coldstart --test parallel_sim_correctness --test threshold_and_cost -- --ignored --nocapture +``` +Expected: 7 tests pass (3 + 1 + 3). + +- [ ] **Step 2: Add `use_cold_start_stopgap` field to `BatchedSimConfig` and `UniformSimParams`** + +File: `crates/ml-backtesting/src/sim/batched_config.rs` + +In the `BatchedSimConfig` struct, after `cost_per_lot_per_side`: + +```rust +/// Q1 stopgap (deleted in Q2): when true, harness uploads a max-confidence +/// 6-instruction bytecode strategy to every 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, +``` + +In `UniformSimParams`, after `cost_per_lot_per_side`: + +```rust +pub use_cold_start_stopgap: bool, +``` + +In `from_uniform`, add to the constructed struct: + +```rust +use_cold_start_stopgap: vec![p.use_cold_start_stopgap; n], +``` + +In `validate`, append to the `lens` array: + +```rust +("use_cold_start_stopgap", self.use_cold_start_stopgap.len()), +``` + +- [ ] **Step 3: Update every existing `UniformSimParams` constructor with the new field** + +Run: + +```bash +grep -rn 'UniformSimParams {' crates/ml-backtesting/ bin/fxt-backtest/ +``` + +For each location (expected: ~6-7 sites in tests + 1 in harness + 1 in main.rs + 1 in fxt-backtest sweep runner), add: + +```rust +use_cold_start_stopgap: false, +``` + +just after `cost_per_lot_per_side: 0.0,` (or matching position). Tests stay with default-false; only `sweep_smoke.yaml` flips it to true in Step 7. + +- [ ] **Step 4: Cargo check** + +```bash +SQLX_OFFLINE=true cargo check --workspace +``` +Expected: clean. All `UniformSimParams` literals must be updated; missing-field error is the regression-catcher for Step 3. + +- [ ] **Step 5: In `BacktestHarness::new`, build the max-confidence Strategy when the flag is set** + +File: `crates/ml-backtesting/src/harness.rs` + +Find the existing strategy-upload loop (around the `for (b, strat) in cfg.strategies.iter().enumerate()` block, ~line 154): + +```rust +// 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)?; +} +``` + +Replace with (note: `sim_config` is built earlier in `new()`; we read it after construction): + +```rust +// Upload per-cell bytecode programs. +// +// Q1 cold-start stopgap: when sim_config.use_cold_start_stopgap[b] is true, +// override any user strategy with a max-confidence Strategy. This bypasses +// the linear-weighted-mean dilution in decision_policy_default by routing +// through decision_policy_program with OP_AGG_MAX_CONFIDENCE. Q2's kernel +// CBSW makes this redundant; this branch is deleted in Q2. +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)?; + } + } +} +// Then upload any caller-provided strategies (legacy path). +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)?; + } +} +``` + +The stopgap overrides per-backtest if the flag is set; user strategies still work for backtests where the flag is false. + +- [ ] **Step 6: Add CLI flag to fxt-backtest** + +File: `bin/fxt-backtest/src/main.rs` + +In `RunArgs`, after the `sharpe_weight_floor` arg: + +```rust +/// Q1 stopgap (deleted in Q2 kernel CBSW): use OP_AGG_MAX_CONFIDENCE +/// via uploaded bytecode strategy to bypass cold-start dilution. +#[arg(long, default_value_t = false)] +cold_start_stopgap: bool, +``` + +In the `BacktestHarnessConfig {` construction in `run()`, add: + +```rust +use_cold_start_stopgap: false, +``` + +(Note: `BacktestHarnessConfig` doesn't have this field directly; it goes through the harness's `sim_config_override` path OR through the constructor's `UniformSimParams`. Use the latter — set the field there.) + +Find where `UniformSimParams` is constructed inside `run()` (the harness builds it internally from cfg's scalar fields per P1 — read `harness.rs::new` to find the construction site). Add `use_cold_start_stopgap: args.cold_start_stopgap` to it. + +For the `run_batched_cell` path in main.rs, similarly route the flag through to the `BatchedSimConfig` construction; default false in CLI-driven runs. + +- [ ] **Step 7: Update sweep_smoke.yaml to enable the stopgap for the validation run** + +File: `config/ml/sweep_smoke.yaml` + +Update the `sim_variants` entry: + +```yaml +sim_variants: + - { name: t30c1l200_stopgap, threshold: 0.30, cost_per_lot_per_side: 0.125, + latency_ns: 200000000, use_cold_start_stopgap: true } +``` + +Note: requires extending the SweepBase `SimVariant` schema in `main.rs` to deserialize `use_cold_start_stopgap`. Add to the `SimVariant` struct definition: + +```rust +#[serde(default)] use_cold_start_stopgap: Option, +``` + +And in `resolve_sim_variants` in `main.rs`, add to the produced `ResolvedSimVariant` (extending the `ResolvedSimVariant` struct in `batched_config.rs` too): + +```rust +use_cold_start_stopgap: v.use_cold_start_stopgap.unwrap_or(false), +``` + +And in `BatchedSimConfig::from_grid`: + +```rust +use_cold_start_stopgap: variants.iter().map(|v| v.use_cold_start_stopgap).collect(), +``` + +(The field also lives on `ResolvedSimVariant` for this — add it during Step 2 actually, retroactively. Or do it here. Make sure compilation flows through.) + +- [ ] **Step 8: Cargo check + run all pre-existing tests** + +```bash +SQLX_OFFLINE=true cargo check --workspace +SQLX_OFFLINE=true cargo test -p ml-backtesting -- --ignored --nocapture +``` + +Expected: pre-existing 7 tests still pass (defaults preserve behavior). + +- [ ] **Step 9: Commit Q1** + +```bash +git add crates/ml-backtesting/src/sim/ \ + crates/ml-backtesting/src/harness.rs \ + crates/ml-backtesting/tests/ \ + bin/fxt-backtest/src/main.rs \ + config/ml/sweep_smoke.yaml +git commit -m "$(cat <<'EOF' +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. + +This is a VALIDATION step. Cluster smoke at threshold=0.0, cost=0.125, +use_cold_start_stopgap=true must produce n_trades > 100 + finite +metrics. Q2's kernel CBSW immediately follows and deletes this entire +stopgap atomically (field, CLI flag, harness branch, YAML setting). + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +git push +``` + +- [ ] **Step 10: Q1 cluster validation gate** + +Submit the stopgap smoke: + +```bash +SHA=$(git rev-parse HEAD) +./scripts/argo-lob-sweep.sh --grid config/ml/sweep_smoke.yaml --sha "$SHA" --branch ml-alpha-phase-a --sweep-tag q1-stopgap-$(echo "$SHA" | cut -c1-9) +``` + +Wait for completion. Expected (read via `kubectl logs` or `aggregate.parquet` inspection): +- `n_trades > 100` on the `sim_t30c1l200_stopgap/` artifact (a real trade count, not zero) +- `total_pnl_usd != 0` (some net P&L emerged — sign doesn't matter for validation) +- `sharpe_ann` finite (not 0, not inf, not NaN) + +If `n_trades == 0` still: STOP. The diagnosis is wrong somewhere downstream of the aggregator (likely cap_lots collapse or fill-time OCO). Re-investigate before Q2. Do NOT proceed. + +If `n_trades > 100`: diagnosis confirmed. Proceed to Task 2. + +--- + +## Task 2 (Q2): CBSW kernel aggregator + Tier 1 revert + +**Goal:** Replace `decision_policy_default`'s weight + aggregation logic with CBSW per spec §2. Atomically delete the Q1 stopgap (field, CLI flag, harness branch, YAML override) in the SAME commit. + +**Files:** +- Modify: `crates/ml-backtesting/cuda/decision_policy.cu` (only `decision_policy_default`; `decision_policy_program` UNCHANGED) +- Modify: `crates/ml-backtesting/src/sim/batched_config.rs` (drop `use_cold_start_stopgap` field) +- Modify: `crates/ml-backtesting/src/sim/mod.rs` (drop `use_cold_start_stopgap` from UniformSimParams) +- Modify: `crates/ml-backtesting/src/harness.rs` (drop the stopgap upload branch) +- Modify: `bin/fxt-backtest/src/main.rs` (drop the CLI flag + SimVariant field) +- Modify: `config/ml/sweep_smoke.yaml` (drop use_cold_start_stopgap) +- Modify: every `UniformSimParams { ... }` literal (drop the field) +- Create: `crates/ml-backtesting/tests/cbsw_aggregator.rs` + +### Steps + +- [ ] **Step 1: Add CBSW `#define`s + `__device__` helpers in decision_policy.cu** + +File: `crates/ml-backtesting/cuda/decision_policy.cu` + +Near the top of the file, after the existing `#define MIN_TRADES_FOR_VAR_CAP 10u`: + +```cuda +// CBSW (Conviction-Bootstrapped Sharpe Weighting) constants. Per spec §4: +// sample-size threshold for trusting historical recent_sharpe and the +// half-width of the linear transition zone. Both are sample-size +// constants (not adaptive bounds) — satisfy pearl_isv_for_adaptive_bounds. +#define MIN_TRADES_FOR_TRUST 10u +#define RAMP_HALF_WIDTH 5.0f + +// Piecewise-linear ramp: 0 below K-half, 1 above K+half, linear +// interpolation between. ~5x cheaper than sigmoid (no expf), same +// operational behavior (monotonic + saturating + midpoint at K). +__device__ static inline float cbsw_signal_quality(unsigned int n_trades_seen) { + const float n = (float)n_trades_seen; + const float lo = (float)MIN_TRADES_FOR_TRUST - RAMP_HALF_WIDTH; // 5.0 + const float span = 2.0f * RAMP_HALF_WIDTH; // 10.0 + return fmaxf(0.0f, fminf(1.0f, (n - lo) / span)); +} + +// CBSW per-horizon weight: blend sig_mag (cold) and recent_sharpe +// (mature) by signal_quality. Permanent floor wrapped via fmaxf per +// pearl_blend_formulas_must_have_permanent_floor. +__device__ static inline float cbsw_weight(float sig_mag, float recent_sharpe, + unsigned int n_trades_seen, float floor) { + const float sq = cbsw_signal_quality(n_trades_seen); + const float sharpe_w = fmaxf(0.0f, recent_sharpe); + const float w_blend = (1.0f - sq) * sig_mag + sq * sharpe_w; + return fmaxf(floor, w_blend); +} +``` + +- [ ] **Step 2: Replace the per-horizon weight/aggregation body in `decision_policy_default`** + +File: `crates/ml-backtesting/cuda/decision_policy.cu` + +Find the existing per-horizon loop in `decision_policy_default` (after the threshold-gate prelude, ~line 70-115). The current body has: + +```cuda +for (int h = 0; h < N_HORIZONS; ++h) { + const float p_h = alpha_probs[h]; + const IsvKellyState& s = isv[h]; + + const float sig_mag = fabsf(p_h - 0.5f) * 2.0f; + const float dir = (p_h > 0.5f) ? 1.0f : -1.0f; + + float kelly_frac; + if (s.pnl_ema_win > 1e-9f) { + float kf = (s.win_rate_ema * s.pnl_ema_win + - (1.0f - s.win_rate_ema) * s.pnl_ema_loss) + / s.pnl_ema_win; + kf = fmaxf(0.0f, fminf(1.0f, kf)); + kelly_frac = fmaxf(kelly_frac_floor, kf); + } else { + kelly_frac = kelly_frac_floor; + } + + float cap_lots; + if (s.n_trades_seen >= MIN_TRADES_FOR_VAR_CAP + && s.realised_return_var > 1e-9f) + { + const float cap_units = target_annual_vol_units + / sqrtf(s.realised_return_var * annualisation_factor); + cap_lots = fminf(cap_units, (float)max_lots); + } else { + cap_lots = (float)max_lots; + } + + signed_sizes[h] = dir * sig_mag * kelly_frac * cap_lots; + const float w = fmaxf(sharpe_weight_floor, s.recent_sharpe); + weights[h] = w; + w_sum += w; +} + +// Aggregator: linear weighted mean +float final_size = 0.0f; +if (w_sum > 1e-9f) { + #pragma unroll + for (int h = 0; h < N_HORIZONS; ++h) { + const float w_norm = weights[h] / w_sum; + final_size += w_norm * signed_sizes[h]; + if (weights[h] > 1e-9f) attribution_mask |= (1u << h); + } +} +``` + +Replace with the CBSW single-pass body per spec §4. The full replacement block: + +```cuda +// Pre-loop init: tracks for max-confidence pick + sq_min over active horizons. +int strong_h = 0; +float strong_abs = 0.0f; +int n_active_horizons = 0; +float sq_min_active = 1.0f; + +#pragma unroll +for (int h = 0; h < N_HORIZONS; ++h) { + const float p_h = alpha_probs[h]; + const IsvKellyState& s = isv[h]; + + const float sig_mag = fabsf(p_h - 0.5f) * 2.0f; + const float dir = (p_h > 0.5f) ? 1.0f : -1.0f; + + // Kelly fraction with floor (existing logic, unchanged) + float kelly_frac; + if (s.pnl_ema_win > 1e-9f) { + float kf = (s.win_rate_ema * s.pnl_ema_win + - (1.0f - s.win_rate_ema) * s.pnl_ema_loss) + / s.pnl_ema_win; + kf = fmaxf(0.0f, fminf(1.0f, kf)); + kelly_frac = fmaxf(kelly_frac_floor, kf); + } else { + kelly_frac = kelly_frac_floor; + } + + // Cap lots with sample-size gate (existing logic, unchanged) + float cap_lots; + if (s.n_trades_seen >= MIN_TRADES_FOR_VAR_CAP + && s.realised_return_var > 1e-9f) + { + const float cap_units = target_annual_vol_units + / sqrtf(s.realised_return_var * annualisation_factor); + cap_lots = fminf(cap_units, (float)max_lots); + } else { + cap_lots = (float)max_lots; + } + + signed_sizes[h] = dir * sig_mag * kelly_frac * cap_lots; + + // CBSW weight: blend sig_mag (cold) and recent_sharpe (mature). + weights[h] = cbsw_weight(sig_mag, s.recent_sharpe, s.n_trades_seen, sharpe_weight_floor); + w_sum += weights[h]; + + // Max-confidence pick: track strong_h by |ss|. + const float abs_ss = fabsf(signed_sizes[h]); + if (abs_ss > strong_abs) { strong_abs = abs_ss; strong_h = h; } + + // sq_min_active: min over horizons with n_trades_seen > 0. Horizons + // that have never been attributed don't gate maturity (otherwise a + // h6000-only-trading regime locks the aggregator into cold-start mode + // forever). + if (s.n_trades_seen > 0u) { + sq_min_active = fminf(sq_min_active, cbsw_signal_quality(s.n_trades_seen)); + n_active_horizons += 1; + } +} +// No closes anywhere → pure max-confidence aggregator. +if (n_active_horizons == 0) sq_min_active = 0.0f; + +// Hybrid aggregator: (1-sq) * ss_strong + sq * ss_weighted. +float ss_weighted = 0.0f; +if (w_sum > 1e-9f) { + #pragma unroll + for (int h = 0; h < N_HORIZONS; ++h) { + ss_weighted += (weights[h] / w_sum) * signed_sizes[h]; + } +} +const float ss_strong = signed_sizes[strong_h]; +const float final_size = (1.0f - sq_min_active) * ss_strong + + sq_min_active * ss_weighted; + +// Attribution: cold = strong_h only; mature = horizons with w > floor + ε. +if (sq_min_active < 0.5f) { + attribution_mask = (1u << strong_h); +} else { + attribution_mask = 0u; + #pragma unroll + for (int h = 0; h < N_HORIZONS; ++h) { + if (weights[h] > sharpe_weight_floor + 1e-6f) { + attribution_mask |= (1u << h); + } + } +} +``` + +- [ ] **Step 3: Verify `decision_policy_program` is UNTOUCHED** + +```bash +grep -n "cbsw_\|MIN_TRADES_FOR_TRUST\|RAMP_HALF_WIDTH" crates/ml-backtesting/cuda/decision_policy.cu | head -10 +``` + +Expected: helpers + #defines appear only in file-scope and inside `decision_policy_default`. NOT in `decision_policy_program`. (Per spec §4: bytecode VM stays a pluggable experiment surface.) + +- [ ] **Step 4: Drop `use_cold_start_stopgap` field everywhere (Tier 1 revert)** + +File: `crates/ml-backtesting/src/sim/batched_config.rs` + +Remove `pub use_cold_start_stopgap: Vec` from `BatchedSimConfig`. Remove from `from_uniform`. Remove the entry from `validate`'s `lens` array. Remove from `UniformSimParams`. Remove from `ResolvedSimVariant`. Remove from `from_grid`. + +File: `crates/ml-backtesting/src/harness.rs` + +Delete the entire `any_stopgap` / `if any_stopgap` block + the dual-loop logic added in Q1 Step 5. Restore the original single-loop: + +```rust +for (b, strat) in cfg.strategies.iter().enumerate() { + let prog = strat.flatten(); + sim.upload_program(b, &prog)?; +} +``` + +File: `bin/fxt-backtest/src/main.rs` + +Remove `cold_start_stopgap: bool` from `RunArgs`. Remove `use_cold_start_stopgap` from `SimVariant`. Remove from `resolve_sim_variants`. Remove the field from any `UniformSimParams { ... }` literal in this file. + +File: `config/ml/sweep_smoke.yaml` + +Drop the `use_cold_start_stopgap: true` line; rename the variant from `t30c1l200_stopgap` to `t30c1l200_cbsw` to reflect the new code path. + +File: all test files + +Remove `use_cold_start_stopgap: false` from every `UniformSimParams { ... }` literal in tests. Use: + +```bash +grep -rn 'use_cold_start_stopgap' crates/ml-backtesting/tests/ bin/fxt-backtest/ +``` + +to find them all. Drop each. + +- [ ] **Step 5: Cargo check after the revert + kernel change** + +```bash +SQLX_OFFLINE=true cargo check --workspace +``` +Expected: clean. `grep -rn 'use_cold_start_stopgap' crates/ bin/ config/` returns ZERO hits (this is the `feedback_no_legacy_aliases` compliance gate). + +- [ ] **Step 6: Run pre-existing CUDA tests to verify CBSW doesn't break them** + +```bash +SQLX_OFFLINE=true cargo test -p ml-backtesting -- --ignored --nocapture +SQLX_OFFLINE=true cargo test -p ml-alpha --lib save_load_roundtrip -- --ignored --nocapture +SQLX_OFFLINE=true cargo test -p ml-alpha --test forward_graph_capture -- --ignored --nocapture +``` +Expected: all pre-existing pass (decision_floor_coldstart × 3, parallel_sim_correctness × 1, threshold_and_cost × 3, save_load_roundtrip, forward_graph_capture). + +Note: `decision_floor_coldstart::cold_start_with_zero_floor_reproduces_old_bug` may change behavior — with zero floors but CBSW's pure max-conf at cold-start, the test expectations may differ. If it fails, investigate; the test may need updating to reflect the new architectural reality. + +- [ ] **Step 7: Write `cbsw_aggregator.rs` test file with 7 CBSW tests** + +File: `crates/ml-backtesting/tests/cbsw_aggregator.rs` + +```rust +//! Q2 regression tests for CBSW (Conviction-Bootstrapped Sharpe Weighting) +//! in decision_policy_default. Per spec §4 test table. Each test +//! exercises a specific (n_trades_seen, alpha) configuration and asserts +//! the resulting market_target + attribution_mask. + +use anyhow::Result; +use ml_backtesting::policy::IsvKellyStateHost; +use ml_backtesting::sim::{BatchedSimConfig, LobSimCuda, UniformSimParams}; +use ml_core::device::MlDevice; + +fn cfg(n: usize, threshold: f32, cost: f32) -> 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, + cost_per_lot_per_side: cost, + }) +} + +#[test] +#[ignore = "requires CUDA"] +fn cbsw_cold_start_single_horizon_strong_fires_trade() -> Result<()> { + let dev = match MlDevice::cuda(0) { + Ok(d) => d, + Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); } + }; + let mut sim = LobSimCuda::new(1, &dev)?; + // n=0 across all horizons (sentinel). Single-horizon strong. + sim.broadcast_alpha(&[0.85, 0.50, 0.50, 0.50, 0.50])?; + sim.step_decision_with_latency(0, &cfg(1, 0.0, 0.0))?; + let (side, size) = sim.read_market_target(0)?; + assert_eq!(side, 0, "cold-start single-h strong should fire buy; got side={side}"); + assert!(size >= 1, "cold-start single-h size {size} < 1"); + Ok(()) +} + +#[test] +#[ignore = "requires CUDA"] +fn cbsw_cold_start_opposing_horizons_fires_on_strongest() -> Result<()> { + let dev = match MlDevice::cuda(0) { + Ok(d) => d, + Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); } + }; + let mut sim = LobSimCuda::new(1, &dev)?; + // h0 bullish strong (0.75 → sig_mag 0.50), h1 bearish weaker (0.30 → sig_mag 0.40). + sim.broadcast_alpha(&[0.75, 0.30, 0.50, 0.50, 0.50])?; + sim.step_decision_with_latency(0, &cfg(1, 0.0, 0.0))?; + let (side, size) = sim.read_market_target(0)?; + assert_eq!(side, 0, "cold-start opposing-h should fire on strongest (h0 long); got side={side}"); + assert!(size >= 1, "size {size} < 1 — opposing horizons shouldn't suppress strongest at cold-start"); + Ok(()) +} + +#[test] +#[ignore = "requires CUDA"] +fn cbsw_cold_start_weak_conflict_does_not_trade() -> Result<()> { + let dev = match MlDevice::cuda(0) { + Ok(d) => d, + Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); } + }; + let mut sim = LobSimCuda::new(1, &dev)?; + // All horizons weakly convicted, mixed directions. Strongest |ss| = 0.10 + // → final < 0.5 → rounds to 0. + sim.broadcast_alpha(&[0.55, 0.45, 0.55, 0.45, 0.55])?; + sim.step_decision_with_latency(0, &cfg(1, 0.0, 0.0))?; + let (side, size) = sim.read_market_target(0)?; + assert_eq!(side, 2, "cold-start weak conflict should produce noop; got side={side}"); + assert_eq!(size, 0); + Ok(()) +} + +#[test] +#[ignore = "requires CUDA"] +fn cbsw_cold_start_consensus_strong_fires_trade() -> Result<()> { + let dev = match MlDevice::cuda(0) { + Ok(d) => d, + Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); } + }; + let mut sim = LobSimCuda::new(1, &dev)?; + sim.broadcast_alpha(&[0.85, 0.85, 0.85, 0.85, 0.85])?; + sim.step_decision_with_latency(0, &cfg(1, 0.0, 0.0))?; + let (side, size) = sim.read_market_target(0)?; + assert_eq!(side, 0); + assert!(size >= 1, "cold-start consensus should fire 1+ lot"); + Ok(()) +} + +#[test] +#[ignore = "requires CUDA"] +fn cbsw_mature_recovers_weighted_sharpe() -> Result<()> { + let dev = match MlDevice::cuda(0) { + Ok(d) => d, + Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); } + }; + let mut sim = LobSimCuda::new(1, &dev)?; + // Seed all 5 horizons with n_trades_seen=20, recent_sharpe varying. + // h=4 has strong sharpe, others modest. CBSW must weight by sharpe. + let mut warm: [IsvKellyStateHost; 5] = std::array::from_fn(|h| IsvKellyStateHost { + pnl_ema_win: 1.0, pnl_ema_loss: 0.5, win_rate_ema: 0.6, + n_trades_seen: 20, + realised_return_var: 0.5, + recent_sharpe: if h == 4 { 1.5 } else { 0.1 }, + }); + sim.write_isv_kelly(0, &warm)?; + sim.broadcast_alpha(&[0.85, 0.50, 0.50, 0.50, 0.85])?; + sim.step_decision_with_latency(0, &cfg(1, 0.0, 0.0))?; + let (side, size) = sim.read_market_target(0)?; + assert_eq!(side, 0, "mature consensus-long should fire; got side={side}"); + assert!(size >= 1, "mature size {size} < 1"); + Ok(()) +} + +#[test] +#[ignore = "requires CUDA"] +fn cbsw_h6000_only_active_does_not_block_maturity() -> Result<()> { + let dev = match MlDevice::cuda(0) { + Ok(d) => d, + Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); } + }; + let mut sim = LobSimCuda::new(1, &dev)?; + // Only h=4 has any closes; others n=0. sq_min_active should be sq[4] + // (which is 1.0 at n=50), NOT min over all h (which would include n=0 + // sq=0 from cold horizons). + let warm: [IsvKellyStateHost; 5] = std::array::from_fn(|h| if h == 4 { + IsvKellyStateHost { + pnl_ema_win: 1.0, pnl_ema_loss: 0.5, win_rate_ema: 0.6, + n_trades_seen: 50, + realised_return_var: 0.5, + recent_sharpe: 1.5, + } + } else { + IsvKellyStateHost::default() // all zeros, n_trades_seen=0 + }); + sim.write_isv_kelly(0, &warm)?; + sim.broadcast_alpha(&[0.85, 0.50, 0.50, 0.50, 0.85])?; + sim.step_decision_with_latency(0, &cfg(1, 0.0, 0.0))?; + let (side, size) = sim.read_market_target(0)?; + assert_eq!(side, 0, "h6000-only-active should still fire trades in mature regime"); + assert!(size >= 1, "h6000-only-active size {size} < 1 — sq_min_active gate too strict"); + Ok(()) +} + +#[test] +#[ignore = "requires CUDA"] +fn cbsw_transition_smooth() -> Result<()> { + // Sweep n_trades_seen across [0, 20] and assert size is monotone + // non-decreasing in n. Catches non-monotonic transitions and + // discontinuities at the K=10 boundary. + let dev = match MlDevice::cuda(0) { + Ok(d) => d, + Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); } + }; + let mut sim = LobSimCuda::new(1, &dev)?; + let mut last_size: i32 = -1; + for n in 0..=20u32 { + let warm: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost { + pnl_ema_win: 1.0, pnl_ema_loss: 0.5, win_rate_ema: 0.6, + n_trades_seen: n, + realised_return_var: 0.5, + // Use a positive recent_sharpe so the mature regime has signal to amplify. + recent_sharpe: 0.8, + }); + sim.write_isv_kelly(0, &warm)?; + sim.broadcast_alpha(&[0.70, 0.50, 0.50, 0.50, 0.50])?; + sim.step_decision_with_latency(0, &cfg(1, 0.0, 0.0))?; + let (_side, size) = sim.read_market_target(0)?; + if last_size >= 0 { + // Allow small jitter from floating-point reordering — size + // must not DECREASE as n grows. + assert!(size >= last_size, + "non-monotonic at n={n}: size went {last_size} → {size}"); + } + last_size = size; + } + Ok(()) +} +``` + +- [ ] **Step 8: Run the new CBSW tests** + +```bash +SQLX_OFFLINE=true cargo test -p ml-backtesting --test cbsw_aggregator -- --ignored --nocapture +``` +Expected: all 7 pass. If any fails, the kernel change has a bug — fix and re-run. + +- [ ] **Step 9: Run all `--ignored` tests workspace-wide one more time** + +```bash +SQLX_OFFLINE=true cargo test -p ml-backtesting -- --ignored --nocapture +SQLX_OFFLINE=true cargo test -p ml-alpha --lib save_load_roundtrip -- --ignored --nocapture +SQLX_OFFLINE=true cargo test -p ml-alpha --test forward_graph_capture -- --ignored --nocapture +``` + +Expected: all pre-existing pass + 7 new cbsw tests pass. + +- [ ] **Step 10: Verify no orphan references** + +```bash +grep -rn 'use_cold_start_stopgap\|cold_start_stopgap' crates/ bin/ config/ scripts/ infra/ +``` + +Expected: ZERO hits. If anything remains, fix before committing. + +- [ ] **Step 11: Commit Q2** + +```bash +git add crates/ml-backtesting/cuda/decision_policy.cu \ + crates/ml-backtesting/src/sim/ \ + crates/ml-backtesting/src/harness.rs \ + crates/ml-backtesting/tests/ \ + bin/fxt-backtest/src/main.rs \ + config/ml/sweep_smoke.yaml +git commit -m "$(cat <<'EOF' +feat(ml-backtesting): CBSW kernel aggregator + Tier 1 revert (Q2/Tier2) + +Replaces decision_policy_default's weight + aggregation with the CBSW +formula from spec §2: + - per-horizon weight: cbsw_weight(sig_mag, recent_sharpe, n, floor) + blends conviction (cold) and historical sharpe (mature) via + piecewise-linear ramp on signal_quality(n_trades_seen). + - aggregator: (1-sq) * ss_strong + sq * ss_weighted, where sq is + min over horizons with n_trades_seen>0 (cold horizons don't gate + maturity; fixes the h6000-only-trading regime). + - attribution: binary at sq < 0.5 (cold = strong_h only; mature = + horizons above floor+ε). Prevents recent_sharpe pollution at + cold-start. + +decision_policy_program is UNCHANGED — bytecode VM remains pluggable +for strategy experiments. Spec §4: scope narrowed to default kernel +only (production policy path). + +Two new file-scope #defines + two __device__ helpers; no kernel ABI +change. ~50 LOC in decision_policy.cu. Per feedback_no_partial_refactor: +no consumer migration needed because the kernel signature is preserved. + +Atomic revert of Q1 stopgap in the same commit per feedback_no_legacy_ +aliases: use_cold_start_stopgap field dropped from BatchedSimConfig + +UniformSimParams + ResolvedSimVariant; --cold-start-stopgap CLI flag +dropped; sweep_smoke.yaml setting dropped; harness branch dropped; +every UniformSimParams literal updated. grep returns zero hits for +use_cold_start_stopgap | cold_start_stopgap. + +Regression: 7 new cbsw_* tests cover cold-start (single, opposing, +weak-conflict, consensus), mature (weighted-sharpe recovery), h6000-only- +active (sq_min_active gate not stuck), and transition smoothness (no +discontinuity at n=K). All 7 pass on local RTX 3050. Pre-existing +decision_floor_coldstart + parallel_sim_correctness + threshold_and_cost + +save_load_roundtrip + forward_graph_capture all still pass. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +git push +``` + +- [ ] **Step 12: Q2 cluster validation gate — rate + n_trades** + +Submit the post-CBSW smoke: + +```bash +SHA=$(git rev-parse HEAD) +./scripts/argo-lob-sweep.sh --grid config/ml/sweep_smoke.yaml --sha "$SHA" --branch ml-alpha-phase-a --sweep-tag q2-cbsw-$(echo "$SHA" | cut -c1-9) +``` + +Wait for completion. Two gates per spec §11: + +Gate A (trade count): `n_trades > 100` on `sim_t30c1l200_cbsw/summary.json`. Diagnoses the structural fix end-to-end. + +Gate B (rate ratio): post-Q2 ev/s ≥ 95% of Q1's ev/s on the same 1M-event setup. Confirms CBSW's per-decision overhead is negligible. + +Compute Gate B: + +```bash +# Q1 rate (from the q1-stopgap-* run's logs) +Q1_RATE=$(kubectl logs -n foxhunt -c main 2>&1 | grep "rate=" | tail -1 | sed 's/.*rate=//; s/ev.*//') +# Q2 rate (from the q2-cbsw-* run's logs) +Q2_RATE=$(kubectl logs -n foxhunt -c main 2>&1 | grep "rate=" | tail -1 | sed 's/.*rate=//; s/ev.*//') +echo "Q1: $Q1_RATE ev/s; Q2: $Q2_RATE ev/s; ratio: $(echo "scale=3; $Q2_RATE / $Q1_RATE" | bc)" +``` + +Required: ratio ≥ 0.95. If less, investigate register spills (`cuobjdump` the cubin, check spills) before continuing. + +--- + +## Task 3 (Q3): Memory pearl + index update + +**Goal:** Capture the lesson as a permanent memory pearl. Linear-weighted-mean aggregation is structurally dilution-bound; the CBSW pattern is the long-term solution. + +**Files:** +- Create: `/home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_conviction_bootstrap_for_kelly_aggregation.md` +- Modify: `/home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/MEMORY.md` + +### Steps + +- [ ] **Step 1: Write the pearl file** + +File: `/home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_conviction_bootstrap_for_kelly_aggregation.md` + +```markdown +--- +name: pearl-conviction-bootstrap-for-kelly-aggregation +description: Linear-weighted-mean aggregation over per-horizon ss[] is structurally dilution-bound at cold-start. Single-horizon strong → final_size ≤ max_h ss[h], typically much less under uniform-floor weights. Fix: hybrid max-confidence × weighted-sharpe with sigmoid/piecewise-linear transition keyed on n_trades_seen. +metadata: + type: pearl +--- + +# Conviction-bootstrapped Kelly aggregation (CBSW) + +When aggregating per-horizon signed sizes via linear weighted mean with +weights summing to 1, `final_size ≤ max_h |ss[h]|`. At cold-start when +no horizons have closed trades, weights default to a uniform floor → +`final_size = mean(ss[h])`. Single-horizon strong signals get washed +out structurally — model has signal but can't trade. + +**Why:** Linear weighted mean with normalized weights has a hard upper +bound at the per-h maximum. Uniform weights drive aggregation toward +the mean. For models where horizons frequently disagree (different +forward windows on different signals), this STRUCTURALLY excludes +single-horizon strong signals from trading. + +**How to apply:** When aggregating heterogeneous signals where one source +can be confident alone while others lag (per-horizon predictions, +multi-classifier ensembles, multi-channel anomaly detectors), DO NOT +use linear-weighted-mean as the aggregator at cold-start. Use a hybrid: + + - cold-start: max-confidence (pick the strongest signed value) + - mature: weighted-mean (linear, but with non-uniform weights driven + by historical performance like recent_sharpe) + - transition: smooth blend keyed on a sample-size signal (n_trades_seen + vs MIN_TRADES_FOR_TRUST threshold). Piecewise-linear is fine; sigmoid + is unnecessary and ~5x more expensive on GPU. + +Permanent floor on weights still required per +[[pearl-blend-formulas-must-have-permanent-floor]]. ISV signals drive +the bootstrap per [[pearl-isv-for-adaptive-bounds]] (sig_mag is the +ISV at decision time; recent_sharpe is the ISV at trade-close time). + +**Canonical instance:** ml-alpha decision_policy on dbd500ecf checkpoint +(2026-05-19, threshold-tuning smoke `81decf40f`). The model produced +74.6% of decisions with max_conv ≥ 0.30 yet n_trades=0 over 500k +decisions because the weighted-mean aggregator diluted single-horizon +strong signals at cold-start. Fix landed in commit ``. + +**DQN parallel:** [[pearl-thompson-for-distributional-action-selection]] +and [[pearl-c51-thompson-closed-phase-e3-gap]] — when historical Q +estimates are uncertain, Thompson-sample over the Q distribution. Same +principle: use available signal (current decision's conviction) when +the historical signal (recent_sharpe) hasn't converged. + +**Where else to look:** Multi-source aggregations elsewhere in the +codebase that linear-weighted-mean over heterogeneous sources may have +the same bug. Candidates: +- multi-horizon training-loss weighting (Σ λ_h × BCE[h]) +- any per-horizon recent_sharpe-driven aggregator in policy/sim stacks +Audit before declaring CBSW the only place this matters. +``` + +- [ ] **Step 2: Add MEMORY.md index entry** + +File: `/home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/MEMORY.md` + +Find the section for Patterns / Pearls → Controllers and signals. Add a new line, alphabetically placed: + +```markdown +- [pearl_conviction_bootstrap_for_kelly_aggregation.md](pearl_conviction_bootstrap_for_kelly_aggregation.md) — linear-weighted-mean is dilution-bound at cold-start; hybrid max-conf × weighted-sharpe with sample-size-driven transition (CBSW); canonical ml-alpha decision_policy 2026-05-19 +``` + +- [ ] **Step 3: Verify MEMORY.md size constraint** + +```bash +wc -l /home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/MEMORY.md +``` +Expected: ≤ 200 lines. Per MEMORY.md's own guidance "Keep index entries to one line under ~200 chars". One-line entry shouldn't push us over the truncation budget. + +- [ ] **Step 4: Commit Q3** + +```bash +# Memory dir is outside the repo — no git commit. Files persist on the user's filesystem. +# Just verify the files are written and readable. +cat /home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_conviction_bootstrap_for_kelly_aggregation.md | head -5 +grep 'pearl_conviction_bootstrap' /home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/MEMORY.md +``` + +Both should show the expected content. No repo-side commit for this task — memory lives outside the working tree. + +--- + +## Task 4 (Q4): Parallelism spec cross-reference + +**Goal:** Update the parallelism spec to flag that the deployability sweep verdict depends on CBSW being live; otherwise variants would all show n_trades=0 regardless of cost/latency/threshold. + +**Files:** +- Modify: `docs/superpowers/specs/2026-05-19-deployability-sweep-parallelism-design.md` + +### Steps + +- [ ] **Step 1: Append a §3.4 cross-reference in the parallelism spec** + +File: `docs/superpowers/specs/2026-05-19-deployability-sweep-parallelism-design.md` + +Find the existing §3.3 ("Pod parallelism (1 → 3 pods, ~3× wall reduction)"). After that section, before §4 (threshold gate), insert: + +```markdown +### 3.4 Cross-reference: CBSW prerequisite + +The deployability sweep verdict on checkpoint `dbd500ecf` is uninformative +without CBSW (Conviction-Bootstrapped Sharpe Weighting) being live in +`decision_policy_default`. The threshold-tuning smoke (`81decf40f`, the +last commit before CBSW landed) produced `n_trades = 0` over 500k +decisions despite 74.6% of decisions having `max_conv ≥ 0.30`, because +the legacy linear-weighted-mean aggregator structurally diluted single- +horizon strong signals at cold-start. + +CBSW lands in commits Q1-Q4 of `docs/superpowers/specs/2026-05-19-cbsw- +cold-start-aggregator-design.md`. Q2 is the load-bearing kernel commit; +the deployability sweep MUST be re-submitted after Q2 lands. Any +deployability_verdict.json generated against a pre-CBSW SHA (≤ `81decf40f`) +should be considered diagnostic only — not the model's actual deployability. + +See [pearl_conviction_bootstrap_for_kelly_aggregation.md](https://github.com/.../memory/pearl_conviction_bootstrap_for_kelly_aggregation.md) +for the architectural pattern + DQN parallel. +``` + +- [ ] **Step 2: Verify the spec parses + reads cleanly** + +```bash +head -200 docs/superpowers/specs/2026-05-19-deployability-sweep-parallelism-design.md | grep -A5 'Cross-reference: CBSW' +``` +Expected: the new §3.4 appears in the spec. + +- [ ] **Step 3: Commit Q4** + +```bash +git add docs/superpowers/specs/2026-05-19-deployability-sweep-parallelism-design.md +git commit -m "$(cat <<'EOF' +spec(ml-alpha): deployability sweep depends on CBSW (Q4) + +The parallelism spec is silent on the CBSW prerequisite. Without CBSW +(commits Q1-Q4 of 2026-05-19-cbsw-cold-start-aggregator-design.md), +the deployability sweep's verdict on the dbd500ecf checkpoint is +uninformative — all 560 variants would show n_trades=0 due to the +linear-weighted-mean cold-start dilution. + +Adds §3.4 cross-reference: deployability sweep MUST be submitted at a +SHA where CBSW Q2 has landed. Pre-CBSW verdicts are diagnostic only. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +git push +``` + +--- + +## Self-review + +**Spec coverage:** + +- §1 Problem → addressed by overall Q1-Q4 (Q1 validates dx, Q2 fixes it) +- §2 Architecture (piecewise-linear sq, blended weight, hybrid aggregator, attribution at sq<0.5) → Q2 Step 1+2 +- §3 Tier 1 stopgap → Q1 +- §4 Tier 2 kernel CBSW (scope: default kernel only) → Q2 Steps 1-3 +- §5 Tier 3 pearl + cross-ref → Q3 + Q4 +- §6 Migration plan → 1-to-1 mapping with Q1-Q4 commit gates +- §7 Tests → Q2 Steps 7-9 (7 cbsw_* tests) + pre-existing regression in every Q +- §8 Rate target validation → Q2 Step 12 Gate B (ev/s ratio ≥ 0.95) +- §9 Risks → addressed in step rationale (register-spill check in Gate B, cold-start losses via empirical escalation noted) +- §10 Out of scope → no tasks touch bf16, seq_len, training-time policy, code audit +- §11 Definition of done → Q2 Steps 9, 10, 12 + Q3 + Q4 + +**Placeholder scan:** no "TBD" / "TODO" / "implement later" / "Similar to". Every step contains the code or commands to run. Concrete file paths throughout. + +**Type consistency:** `BatchedSimConfig` / `UniformSimParams` / `ResolvedSimVariant` schemas align across Q1 (add field) → Q2 (drop field). `cbsw_signal_quality` / `cbsw_weight` helper signatures consistent between definition (Q2 Step 1) and use (Q2 Step 2). `use_cold_start_stopgap` field naming consistent across Q1 (add) and Q2 (drop). + +**Plan complete and saved to `docs/superpowers/plans/2026-05-19-cbsw-cold-start-aggregator.md`.**