# Anti-Calibration Fix Plan — Aux Supervision + Regime Gate (HORIZONS unchanged)

> **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:** Lift trade Win Rate from current ~22% toward the 55%+ target by combining (1) aux supervision on cost-aware D-style asymmetric labels (E3 design with user's D-label choice) and (2) regime-conditional decision gate (per `pearl_snapshot_alpha_is_regime_conditional` — empirically validated). **HORIZONS = [10, 100, 1000] stays as-is** (the Smoke-1 validated set with auc_h1000=0.7137). Horizon rebase to [100,300,1000] is dropped; the K=10 noise issue identified by E1 is addressed via the regime gate, which masks out low-spread regimes where K=10 hurts most.

**Architecture:** Two independent additive layers:
- **Layer B — Aux supervision**: separate aux trunk (smaller CfC, single bucket) sharing the encoder, supervised on D-style asymmetric loss-aversion labels per (long, short) direction. Asymmetric stop-grad. Separate Adam group. Decision policy gates trade entry on aux signal.
- **Layer C — Regime gate**: regime classification (spread quintile from ISV-driven trailing percentiles) at decision time; trade only in alpha-dense quintiles identified by Layer C1's NumPy investigation.

**Tech Stack:** Rust + CUDA. Build via `SQLX_OFFLINE=true cargo check`.

---

## Current state (start of plan)

Branch: `ml-alpha-phase-a` HEAD `6d772a5d6`. Working tree has 5 uncommitted files from an aborted horizon-rebase attempt that should be reverted before starting:
```
M crates/ml-alpha/examples/alpha_train.rs                   (log field renames)
M crates/ml-alpha/src/gpu_log.rs                            (JSON field renames)
M crates/ml-alpha/tests/gpu_log_ring_invariants.rs          (test assertion rename)
M crates/ml-alpha/tests/smoothness_lambda_controller_invariants.rs (comments)
M crates/ml-backtesting/src/harness.rs                      (log labels)
```
These were applied assuming HORIZONS=[100,300,1000]; with HORIZONS staying at [10,100,1000] they must be reverted to keep log/field names consistent with actual horizon values.

**First action** (before any new code): `git checkout HEAD -- <those 5 files>` to restore consistency.

## Layer ordering — sequential, attribution-clean

Run Layer B first end-to-end (commit + Smoke 1 + Smoke 2 with gate OFF + gate ON). Then run Layer C end-to-end. Sequential ordering means each layer's WR contribution is attributable; bundling makes diagnosis hard.

## Strategic context

Per investigations completed this session:
- **E1 finding (root cause of WR drop)**: K=10 anchor produces noise-injection at inference time because labels are heavily tie-masked (sub-tick moves dropped). Layer C (regime gate) is expected to mitigate by routing K=10's noisy outputs out of the alpha-dense regimes.
- **E2 finding (trajectory output design)**: Candidate C (Huber log-return) was rejected — D3 falsified the smoothness argument; calibration/class-imbalance arguments remain but require BCE→Huber kernel surgery for unclear lift. Deferred.
- **E3 finding (aux supervision)**: A+B paired labels recommended; user chose D-style asymmetric loss-aversion labels `(direction × ΔP - cost) - 1.5 × |MaxDD_against|` per (long, short). Stronger alignment with trading objective.
- **WR analysis**: 55% target achievable but requires multiple stacked interventions. Layer B alone likely lifts WR to 40-50%; Layer C is essential to bridge to 55%+. Per `pearl_snapshot_alpha_is_regime_conditional`, alpha concentrates in ~20% of book states (spread-Q4 = 75% accuracy).

## Sub-decisions resolved

| # | Decision | Choice | Source |
|---|---|---|---|
| 1 | HORIZONS | [10, 100, 1000] (unchanged) | User revert; Layer C compensates for K=10 noise |
| 2 | Aux label form | D (asymmetric: `ΔP - 1.5×|MaxDD| - cost`) per (long, short) | User |
| 3 | Aux trunk hidden | 64 (vs main trunk 128) | E3 design default; revisit if starves |
| 4 | Stop-grad lifting | Conditional (lift if aux CE < 0.4 + dir_acc > 0.85 within 200 steps) | E3 |
| 5 | Cost model | Constant 2-tick round-trip (no slippage in v1) | E3 |
| 6 | MaxDD multiplier | 1.5× | E3 |
| 7 | Aux gate in policy | Disabled in v1 wire-up; enabled in v2 commit | feedback_smoke_validation_before_structural_priors |
| 8 | Layer C regime feature | Spread quintile (per pearl_snapshot_alpha_is_regime_conditional) | Empirical from data |
| 9 | Layer C alpha-dense regime(s) | TBD — Layer C1's NumPy investigation decides | C1 |
| 10 | Layer C cost-vs-alpha tradeoff at Q4 | Verified by C1 net-of-cost calculation | C1 |

---

## Task breakdown

### Pre-flight: revert inconsistent uncommitted state

- [ ] **Step 1**: `git checkout HEAD -- crates/ml-alpha/examples/alpha_train.rs crates/ml-alpha/src/gpu_log.rs crates/ml-alpha/tests/gpu_log_ring_invariants.rs crates/ml-alpha/tests/smoothness_lambda_controller_invariants.rs crates/ml-backtesting/src/harness.rs`
- [ ] **Step 2**: Verify `git status --short` empty.
- [ ] **Step 3**: Verify `cargo check --workspace --all-targets` clean.

### Layer B — Aux supervision with D-labels

See the prior plan file `2026-05-22-horizon-rebase-n3-100-300-1000-and-aux-d-labels.md.bak` for B1-B8 task details; copy here without modification (HORIZONS=[10,100,1000] is the only context shift, all aux infrastructure scope unchanged). Tasks summary:

- **B1**: Generate D-style labels `(direction × ΔP - cost) - 1.5 × |MaxDD_against|` per (long, short) with monotonic-deque forward MaxDD scan (`crates/ml-alpha/src/multi_horizon_labels.rs`)
- **B2**: Loader integration — extend `LabeledSequence` + `LoadedFile` with outcome labels
- **B3**: Aux trunk (64-hidden CfC, single bucket) — `crates/ml-alpha/src/cfc/aux_trunk.rs` + `cuda/aux_trunk.cu` (clone of cfc_step kernel with smaller hidden)
- **B4**: Aux heads (linear regression per long/short) + Huber loss — `cuda/aux_heads.cu` + `cuda/aux_loss.cu`
- **B5**: Trainer wiring — parallel aux trunk, separate Adam group, asymmetric stop-grad at encoder, conditional lift after gate G1
- **B6**: Two-stage smoke validation — aux gate OFF first (validate training wiring), then aux gate ON
- **B7**: Decision policy aux gate — `crates/ml-backtesting/cuda/decision_policy.cu` gate `target_lots = 0 if predicted_outcome < threshold_aux × σ_local`
- **B8**: Layer-B pearls (k10 noise, D-label rationale, aux-trunk pattern, two-stage smoke)

**Layer B exit gate**: Argo Smoke 2 with aux gate ON shows WR at conv [0.0-0.6] ≥ 30% AND not monotonically anti-calibrated (Spearman ρ > -0.3 across conv buckets).

If Layer B exit gate fails (WR <30% or still strongly anti-cal), STOP and diagnose before Layer C — adding more interventions to a broken base wastes compute.

**Estimated Layer B cost**: 60 hours implementation + 4 hours Argo cycles.

### Layer C — Regime gate

### Task C1: Regime data analysis (NumPy)

**Cost**: ~2 hours. No GPU, no model training.

- [ ] **Step 1: Compute per-regime cost-adjusted PnL on existing fxcache**

Reuse the existing decoded `/tmp/acf_analysis/features.npz` (from prior C1+C3 investigations). For one fold (~600k ES MBP-10 snapshots):
- Compute spread at each snapshot: `spread = ask_px[0] - bid_px[0]` (already a feature)
- Quintile-bin all snapshots by spread (Q1=lowest, Q5=highest)
- For each (quintile, horizon-K), compute the per-snapshot label `delta = price[t+K] - price[t]` and the cost-adjusted profit IF we had taken the directionally-correct trade: `pnl_per_snapshot = |delta| - cost` (cost = 2×tick = 0.5)
- Aggregate: mean `pnl_per_snapshot` per quintile per horizon

- [ ] **Step 2: Identify alpha-dense regime(s)**

Output a 5×3 table of mean cost-adjusted PnL per (spread-quintile, horizon). Identify which quintiles have POSITIVE expected cost-adjusted PnL.

Expected per `pearl_snapshot_alpha_is_regime_conditional`: Q4 (high spread) has best alpha but biggest cost. Q1 (low spread) has lowest cost but possibly weakest alpha. Middle quintiles likely sub-zero.

- [ ] **Step 3: Decide gate set**

If Q4 dominates: gate to Q4 only (~20% of trades, expected highest WR).
If Q1 and Q4 both positive: gate to extremes (Q1 ∪ Q4, ~40% of trades).
If middle quintiles dominate alpha (unlikely but possible): gate to alpha-rich middle.

Output: `allowed_quintiles: HashSet<u32>`.

### Task C2: Regime gate implementation

**Cost**: ~6 hours.

**Files**:
- Modify: `crates/ml-backtesting/cuda/decision_policy.cu` (add regime computation + gate)
- Modify: `crates/ml-backtesting/src/sim/mod.rs` (allocate regime state buffers)
- Modify: `crates/ml-backtesting/src/policy/mod.rs` (config plumbing for regime quintile set)
- Add: regime quintile thresholds via ISV-driven trailing-window percentiles (NOT hardcoded — `feedback_isv_for_adaptive_bounds`)

- [ ] **Step 1: Compute regime at decision time**

Add per-batch trailing-window stats for spread:
```cuda
// Per-batch rolling spread quintile via ISV: maintain spread_p20/p40/p60/p80
// as Wiener-α EMAs of trailing-window quantile estimates (one update per event)
const float s = book.ask_px[0] - book.bid_px[0];
const int q = (s < spread_p20[b]) ? 0
            : (s < spread_p40[b]) ? 1
            : (s < spread_p60[b]) ? 2
            : (s < spread_p80[b]) ? 3 : 4;
```

P-square quantile estimator (single-pass, O(1) per event, well-known algorithm) is the right structure. Alternative: maintain a small reservoir of last-N spread values and recompute quantiles every K events.

- [ ] **Step 2: Gate logic**

```cuda
if (!quintile_in_allowed_set(q, allowed_quintile_mask)) {
    target_lots = 0;
    return;
}
```

`allowed_quintile_mask: u8` bitmask (5 bits, 1 per quintile). Configured at backtest config load.

- [ ] **Step 3: ISV diagnostic counters**

Add per-quintile trade-attempt counter (diagnostic): `regime_trade_attempts[5]`, `regime_trades_executed[5]`. Logged at end of run for sanity check that the gate is actually firing as designed.

### Task C3: Smoke validation

**Cost**: ~1 hour wallclock (Argo).

- [ ] **Step 1: Dispatch Smoke 2 with Layer B + C active**

`./scripts/argo-lob-sweep.sh --grid config/ml/sweep_smoke_perhoriz_cfc.yaml --branch ml-alpha-phase-a`

- [ ] **Step 2: Pull outcome_by_entry_conv + regime counters**

Verify:
- **Primary gate**: WR at conv [0.0-0.6] ≥ 50% (the 55% target needs some breathing room for production noise)
- **Secondary**: trade count drops 60-80% from Layer B alone (expected — most trades filtered out)
- **Calibration**: `outcome_by_entry_conv` mean PnL across populated conv buckets monotonically improving with conviction (or at least non-negative)

If WR doesn't lift to 50%+, diagnose: maybe the regime gate isn't selecting the right quintile(s), maybe cost model is wrong, maybe aux supervision needs more training.

### Task C4: Pearls

- [ ] **Step 1: Write 2 new pearls**

- `pearl_regime_gate_essential_for_efficient_markets.md` — empirical validation: alpha concentrates in narrow regime band, trading equally across all regimes is a unforced WR loss
- `pearl_cost_adjusted_per_regime_pnl_decides_gate_set.md` — methodology pearl: deciding which regime to allow trades in is a per-(quintile, horizon) cost-adjusted expected-PnL computation, not a heuristic

- [ ] **Step 2: Update MEMORY.md**

---

## Validation summary

| Gate | Layer | Pass condition |
|---|---|---|
| Library compile | All | `cargo check --workspace --all-targets` clean |
| Library tests | All | `cargo test -p ml-alpha --lib` 33 passed |
| Local GPU smoke | B5 | perception_overfit 4/4 pass (both BCE + aux losses → 0) |
| Argo Smoke 1 (Layer B wiring) | B6 | aux Huber < baseline (predicts mean); BCE not regressed |
| Argo Smoke 2 (Layer B gate OFF) | B6 | Anti-calibration ≥ as good as current Layer A baseline (no regression from aux wiring) |
| Argo Smoke 3 (Layer B gate ON) | B7 | WR at conv [0.0-0.6] ≥ 30%; not monotonically anti-calibrated |
| **Argo Smoke 4 (Layer C active)** | **C3** | **WR at conv [0.0-0.6] ≥ 50%** — the 55% target ceiling |

---

## Risks

| Risk | Likelihood | Mitigation |
|---|---|---|
| Layer B WR < 30% (aux supervision doesn't carry signal) | Medium | Layer B exit gate stops Layer C. Diagnose: maybe D-label cost is wrong, maybe MaxDD multiplier too aggressive. |
| Aux trunk plateaus at ln(K) (encoder starvation) | Medium | Conditional stop-grad lift mechanism. If still stuck, increase aux hidden to 96 or 128. |
| Aux gate causes Hold-collapse | Medium | D-label preserves direction info (paired long/short); threshold ISV-driven prevents stuck-closed gate. Verify trade count post-Smoke 3 (>50% of Layer B baseline). |
| Layer C Q4-only gate has cost > alpha (spread eats the move) | Medium | C1's NumPy investigation falsifies this BEFORE committing to gate set. If Q4 negative net-of-cost, fall back to Q1 (low cost, weak alpha) or middle (per data). |
| P-square quantile estimator unstable / slow to converge | Low | Alternative: reservoir-based recompute every K events (slower per-event but more stable). Decide in Task C2. |
| Cost model (constant 2-tick) under-states real cost | Medium | If anti-cal persists with constant cost, escalate to ISV-driven slip estimate per `pearl_isv_for_adaptive_bounds`. |
| Forward MaxDD scan O(N·K_max) is slow (K=1000) | Low | Monotonic deque amortized O(N). Profile on local CPU; if >5 min per 1M snapshots, switch to segment tree O(N log K). |
| Adam-summed loss balance no-op | High if implemented wrong | SEPARATE Adam group per trunk. Verify by checking optim.rs has truly disjoint Adam state. |
| Aux-only smoke shows no improvement (E3's anti-cal hypothesis wrong) | Medium | Honest diagnosis: maybe `pearl_separate_aux_trunk_when_shared_starves` lifts the stop-grad and we get back to a shared-trunk failure mode. Consider Candidate C (E2) trajectory output as next pivot. |

---

## Estimated combined timeline

- Pre-flight revert: 5 min
- Layer B implementation: ~60 hours
- Layer B smokes (3 Argo cycles): ~3 hours
- Layer B exit gate decision point: 30 min (does WR justify Layer C?)
- Layer C1 NumPy analysis: ~2 hours
- Layer C2 implementation: ~6 hours
- Layer C3 smoke: ~1 hour wallclock
- Pearls + commit: ~1 hour
- **Total: ~73 hours over ~2 weeks**

## Execution

After this plan is reviewed: dispatch subagent-driven-development via Skill tool. Layer B is independent of Layer C — STRONGLY RECOMMENDED to complete Layer B through Task B6 (validate aux wiring) before starting B7 (gate). And complete Layer B through Task B7 (smoke) before starting Layer C.
