# Adaptive Risk Management — Implementation Plan (Layered Stack)

> **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:** Close the fold-1 eval gap (-$616k OOS despite controllers fixed) by adding a 5-layer adaptive risk management stack: hard CMDP gates + IQN risk-averse action selection + inventory penalty + Kelly sizing + TrailTighten/Loosen wiring. The genuinely high-leverage change is **Layer 2 (IQN τ-quantile action selection)** — uses the existing IQN distribution to give risk-averse decisions for free.

**Architecture:** Five orthogonal layers, each ISV-driven and adaptive. The agent's discrete action choice flows through: (Layer 2 selects which action via pessimistic Q) → (Layer 1 overrides if any safety constraint fires) → (Layer 4 scales lot count by observed edge) → (Layer 3 penalizes inventory drift in reward). All layers use the same Welford-variance infrastructure from b1ef6664a.

**Tech Stack:** CUDA 12.4 sm_89/sm_86, cudarc 0.19.3, Rust 1.85+, single-stream graph capture, mapped-pinned ISV bus. Same as base codebase.

**Spec:** `docs/superpowers/specs/2026-05-30-adaptive-risk-management-design.md`

---

## File Structure

### Created (8 new files)

- `crates/ml-alpha/cuda/rl_cmdp_constraints_check.cu` (~100 LOC) — Layer 1: session DD + cooldown + max-open + inventory gates
- `crates/ml-alpha/cuda/rl_iqn_action_tau_controller.cu` (~50 LOC) — Layer 2: adapts IQN action-selection quantile from drawdown
- `crates/ml-alpha/cuda/rl_inventory_beta_controller.cu` (~50 LOC) — Layer 3: adapts inventory penalty coefficient
- `crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu` (~80 LOC) — Layer 4: half-Kelly from observed win_rate × R-multiple
- `crates/ml-alpha/cuda/rl_win_rate_ema_update.cu` (~40 LOC) — feeds Kelly controller
- `crates/ml-alpha/cuda/rl_avg_win_loss_ema_update.cu` (~50 LOC) — feeds Kelly controller (avg_win + avg_loss per-trade EMAs)
- `crates/ml-alpha/cuda/rl_inventory_variance_update.cu` (~40 LOC) — feeds Layer 3 β controller
- `crates/ml-alpha/tests/risk_stack_invariants.rs` (~12 GPU-oracle gates)

### Modified (7 files)

- `crates/ml-alpha/src/rl/isv_slots.rs` — +22 slots, `RL_SLOTS_END` 662 → 684
- `crates/ml-alpha/build.rs` — register 7 new cubins
- `crates/ml-alpha/cuda/actions_to_market_targets.cu` — Layer 1 overrides + Layer 4 Kelly scaling + Layer D trail mutations
- `crates/ml-alpha/cuda/rl_fused_reward_pipeline.cu` — Layer 3 inventory penalty term in reward shaping
- `crates/ml-alpha/cuda/rl_ensemble_action_value.cu` — Layer 2 quantile-τ argmax (replaces expected-Q argmax)
- `crates/ml-alpha/cuda/rl_fused_controllers.cu` — add new branches for τ, β, Kelly (matching b1ef6664a pattern)
- `crates/ml-alpha/src/trainer/integrated.rs` — load 7 new cubins, struct fields, per-step launches, bootstrap entries, session reset on fold boundary

---

## Tasks

### Task 1: Allocate 22 new ISV slots

**Files:**
- Modify: `crates/ml-alpha/src/rl/isv_slots.rs` (after slot 661)

- [ ] **Step 1: Add 22 new constants + bump `RL_SLOTS_END`**

```rust
// ============================================================
// Adaptive risk management — layered stack (spec 2026-05-30-adaptive-risk-management-design)
//
// Five orthogonal risk layers, each ISV-driven:
//   Layer 1 — CMDP hard constraints (session DD, cooldown, max open, inventory)
//   Layer 2 — IQN risk-averse action selection via adaptive τ
//   Layer 3 — Inventory penalty (Avellaneda-Stoikov) in reward shaping
//   Layer 4 — Kelly-fraction position sizing
//   Layer D — TrailTighten/Loosen wiring (close dead-action gap)
//
// Per `feedback_adaptive_not_tuned`: every threshold below is signal-driven
// or warmup-gated, not a hardcoded constant.
// ============================================================

// ─── Layer 1: CMDP hard constraints (9 slots) ───
pub const RL_SESSION_PNL_USD_INDEX:             usize = 662;
pub const RL_SESSION_DD_LIMIT_USD_INDEX:        usize = 663;
pub const RL_SESSION_DD_TRIGGERED_INDEX:        usize = 664;
pub const RL_MAX_OPEN_UNITS_INDEX:              usize = 665;
pub const RL_CONSEC_LOSS_LIMIT_INDEX:           usize = 666;
pub const RL_CONSEC_LOSS_COUNT_INDEX:           usize = 667;
pub const RL_COOLDOWN_REMAINING_STEPS_INDEX:    usize = 668;
pub const RL_COOLDOWN_DURATION_INDEX:           usize = 669;
pub const RL_NET_INVENTORY_LIMIT_USD_INDEX:     usize = 670;

// ─── Layer 2: IQN risk-averse τ (3 slots) ───
pub const RL_IQN_ACTION_TAU_INDEX:              usize = 671;
pub const RL_IQN_ACTION_TAU_MIN_INDEX:          usize = 672;
pub const RL_IQN_ACTION_TAU_DD_SENSITIVITY_INDEX: usize = 673;

// ─── Layer 3: Inventory penalty (2 slots) ───
pub const RL_INVENTORY_PENALTY_BETA_INDEX:      usize = 674;
pub const RL_INVENTORY_VARIANCE_EMA_INDEX:      usize = 675;

// ─── Layer 4: Kelly-fraction sizing (6 slots) ───
pub const RL_KELLY_FRACTION_INDEX:              usize = 676;
pub const RL_WIN_RATE_EMA_INDEX:                usize = 677;
pub const RL_AVG_WIN_USD_EMA_INDEX:             usize = 678;
pub const RL_AVG_LOSS_USD_EMA_INDEX:            usize = 679;
pub const RL_KELLY_SAFETY_FRAC_INDEX:           usize = 680;
pub const RL_KELLY_MIN_TRADES_FOR_RELEASE_INDEX: usize = 681;

// ─── Layer D: TrailTighten/Loosen wiring (2 slots) ───
pub const RL_TRAIL_TIGHTEN_FACTOR_INDEX:        usize = 682;
pub const RL_TRAIL_LOOSEN_FACTOR_INDEX:         usize = 683;

/// Last RL-allocated slot index (exclusive). Pre-risk-stack: 662.
/// Post-risk-stack: 684. Total new: 22 slots, +88 bytes mapped-pinned.
pub const RL_SLOTS_END: usize = 684;
```

- [ ] **Step 2: Verify cargo check passes + no duplicate slot numbers**

```bash
cd /home/jgrusewski/Work/foxhunt
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -3
grep -oE "= [0-9]+;" crates/ml-alpha/src/rl/isv_slots.rs | sort -n | uniq -c | sort -rn | head -3
```

Expected: `Finished`, all counts = 1.

---

### Task 2: New EMA producer kernels for risk signals

**Files:**
- Create: `crates/ml-alpha/cuda/rl_win_rate_ema_update.cu`
- Create: `crates/ml-alpha/cuda/rl_avg_win_loss_ema_update.cu`
- Create: `crates/ml-alpha/cuda/rl_inventory_variance_update.cu`
- Modify: `crates/ml-alpha/build.rs`

- [ ] **Step 1: Write `rl_win_rate_ema_update.cu`**

```c
// rl_win_rate_ema_update.cu — track observed win_rate EMA from trade outcomes.
// Per pearl_first_observation_bootstrap: sentinel = 0.5 (neutral) until first
// closed trade. Per feedback_no_atomicadd: single-thread block, no atomics.

#define RL_WIN_RATE_EMA_INDEX 677
#define EMA_ALPHA 0.05f  // ~20-trade effective window

extern "C" __global__ void rl_win_rate_ema_update(
    float* __restrict__ isv,
    const int* __restrict__ trade_outcome_per_batch,  // [b_size] −1/0/+1
    int b_size
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    // Count this step's closed trades (outcome != 0).
    int closed = 0, wins = 0;
    for (int b = 0; b < b_size; b++) {
        const int o = trade_outcome_per_batch[b];
        if (o != 0) { closed++; if (o == 1) wins++; }
    }
    if (closed == 0) return;  // no observation this step

    const float step_wr = (float)wins / (float)closed;
    const float prev = isv[RL_WIN_RATE_EMA_INDEX];
    // First-observation bootstrap on sentinel 0.5 (neutral default).
    if (prev == 0.5f) {
        isv[RL_WIN_RATE_EMA_INDEX] = step_wr;
    } else {
        isv[RL_WIN_RATE_EMA_INDEX] = (1.0f - EMA_ALPHA) * prev + EMA_ALPHA * step_wr;
    }
}
```

- [ ] **Step 2: Write `rl_avg_win_loss_ema_update.cu`**

```c
// rl_avg_win_loss_ema_update.cu — track avg_win + avg_loss per-trade EMAs in USD.
// Per pearl_first_observation_bootstrap: sentinel = 0.0 (no observation).

#define RL_AVG_WIN_USD_EMA_INDEX  678
#define RL_AVG_LOSS_USD_EMA_INDEX 679
#define EMA_ALPHA 0.05f

extern "C" __global__ void rl_avg_win_loss_ema_update(
    float* __restrict__ isv,
    const float* __restrict__ realized_pnl_per_batch,  // [b_size] USD, signed
    const int*   __restrict__ trade_outcome_per_batch, // [b_size] −1/0/+1
    int b_size
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    // Sum per-trade |pnl| for wins / losses this step.
    float sum_win = 0.0f, sum_loss = 0.0f;
    int   n_win   = 0,    n_loss   = 0;
    for (int b = 0; b < b_size; b++) {
        const int   o = trade_outcome_per_batch[b];
        const float p = realized_pnl_per_batch[b];
        if (o == 1)  { sum_win  += p;        n_win++;  }
        if (o == -1) { sum_loss += fabsf(p); n_loss++; }
    }

    if (n_win > 0) {
        const float step_avg = sum_win / (float)n_win;
        const float prev = isv[RL_AVG_WIN_USD_EMA_INDEX];
        isv[RL_AVG_WIN_USD_EMA_INDEX] = (prev == 0.0f) ? step_avg
            : (1.0f - EMA_ALPHA) * prev + EMA_ALPHA * step_avg;
    }
    if (n_loss > 0) {
        const float step_avg = sum_loss / (float)n_loss;
        const float prev = isv[RL_AVG_LOSS_USD_EMA_INDEX];
        isv[RL_AVG_LOSS_USD_EMA_INDEX] = (prev == 0.0f) ? step_avg
            : (1.0f - EMA_ALPHA) * prev + EMA_ALPHA * step_avg;
    }
}
```

- [ ] **Step 3: Write `rl_inventory_variance_update.cu`**

```c
// rl_inventory_variance_update.cu — track running variance of |net_position|
// across batches. Used by Layer 3 β controller to scale penalty.
// Welford online variance, single-thread single-block.

#define RL_INVENTORY_VARIANCE_EMA_INDEX 675
#define WELFORD_ALPHA 0.01f

extern "C" __global__ void rl_inventory_variance_update(
    float* __restrict__ isv,
    const float* __restrict__ net_position_per_batch,  // [b_size] signed
    int b_size
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    // Compute batch mean + variance of |net_position|.
    float sum = 0.0f, sum_sq = 0.0f;
    for (int b = 0; b < b_size; b++) {
        const float v = fabsf(net_position_per_batch[b]);
        sum    += v;
        sum_sq += v * v;
    }
    const float n    = (float)b_size;
    const float mean = sum / n;
    const float var  = (sum_sq / n) - (mean * mean);
    if (var < 0.0f) return;  // numerical safety

    const float prev = isv[RL_INVENTORY_VARIANCE_EMA_INDEX];
    isv[RL_INVENTORY_VARIANCE_EMA_INDEX] = (prev == 0.0f) ? var
        : (1.0f - WELFORD_ALPHA) * prev + WELFORD_ALPHA * var;
}
```

- [ ] **Step 4: Register all 3 cubins in `build.rs`**

In the cubin list (next to other RL kernels):
```rust
"rl_win_rate_ema_update",
"rl_avg_win_loss_ema_update",
"rl_inventory_variance_update",
```

- [ ] **Step 5: Verify build**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --release 2>&1 | tail -3
ls target/release/build/ml-alpha-*/out/rl_win_rate_ema_update.cubin
ls target/release/build/ml-alpha-*/out/rl_avg_win_loss_ema_update.cubin
ls target/release/build/ml-alpha-*/out/rl_inventory_variance_update.cubin
```

Expected: `Finished`, all 3 cubins present.

---

### Task 3: Layer 1 — CMDP constraints kernel + actions_to_market_targets overrides

**Files:**
- Create: `crates/ml-alpha/cuda/rl_cmdp_constraints_check.cu`
- Modify: `crates/ml-alpha/build.rs`
- Modify: `crates/ml-alpha/cuda/actions_to_market_targets.cu`

- [ ] **Step 1: Write `rl_cmdp_constraints_check.cu`**

```c
// rl_cmdp_constraints_check.cu — Layer 1 hard CMDP gates (spec 2026-05-30-adaptive-risk-management).
//
// Five independent risk constraints. ALL must be satisfied for the agent's
// action to execute; ANY violation forces Hold.
//
// 1. Session pnl accumulation + DD cap check (writes triggered flag)
// 2. Cooldown counter decrement (after consecutive losses limit)
// 3. Consecutive-loss tracking (sets cooldown when limit hit)
// 4. Max-open and inventory checks read directly from per-batch state
//    in actions_to_market_targets — this kernel only manages the
//    session-level + cooldown state.
//
// Per feedback_no_atomicadd: single-thread block, sums across b_size manually.

#define RL_SESSION_PNL_USD_INDEX            662
#define RL_SESSION_DD_LIMIT_USD_INDEX       663
#define RL_SESSION_DD_TRIGGERED_INDEX       664
#define RL_CONSEC_LOSS_LIMIT_INDEX          666
#define RL_CONSEC_LOSS_COUNT_INDEX          667
#define RL_COOLDOWN_REMAINING_STEPS_INDEX   668
#define RL_COOLDOWN_DURATION_INDEX          669

extern "C" __global__ void rl_cmdp_constraints_check(
    float* __restrict__ isv,
    const float* __restrict__ realized_pnl_per_batch,    // [b_size] USD
    const int*   __restrict__ trade_outcome_per_batch,   // [b_size] −1/0/+1
    int b_size
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    // 1. Session pnl + DD check.
    float step_pnl = 0.0f;
    for (int b = 0; b < b_size; b++) step_pnl += realized_pnl_per_batch[b];
    const float session_pnl_new = isv[RL_SESSION_PNL_USD_INDEX] + step_pnl;
    isv[RL_SESSION_PNL_USD_INDEX] = session_pnl_new;
    if (session_pnl_new < isv[RL_SESSION_DD_LIMIT_USD_INDEX]) {
        isv[RL_SESSION_DD_TRIGGERED_INDEX] = 1.0f;
    }

    // 2. Cooldown decrement.
    const float cooldown = isv[RL_COOLDOWN_REMAINING_STEPS_INDEX];
    if (cooldown > 0.0f) {
        isv[RL_COOLDOWN_REMAINING_STEPS_INDEX] = cooldown - 1.0f;
    }

    // 3. Consecutive loss tracking.
    float consec = isv[RL_CONSEC_LOSS_COUNT_INDEX];
    for (int b = 0; b < b_size; b++) {
        const int o = trade_outcome_per_batch[b];
        if (o == -1) consec += 1.0f;
        else if (o == 1) consec = 0.0f;
    }
    if (consec >= isv[RL_CONSEC_LOSS_LIMIT_INDEX]) {
        isv[RL_COOLDOWN_REMAINING_STEPS_INDEX] = isv[RL_COOLDOWN_DURATION_INDEX];
        isv[RL_CONSEC_LOSS_COUNT_INDEX] = 0.0f;
    } else {
        isv[RL_CONSEC_LOSS_COUNT_INDEX] = consec;
    }
}
```

- [ ] **Step 2: Register cubin in `build.rs`**

Add: `"rl_cmdp_constraints_check",`

- [ ] **Step 3: Modify `actions_to_market_targets.cu` — add Layer 1 override block at top of per-batch loop**

Find the per-batch action translation loop. Before the action-specific switch statement, insert:

```c
// ── Layer 1 (CMDP) override checks ──
// Per spec 2026-05-30-adaptive-risk-management-design. Any violation
// forces Hold. Order: most catastrophic first.

const bool dd_triggered  = isv[RL_SESSION_DD_TRIGGERED_INDEX] >= 0.5f;
const bool in_cooldown   = isv[RL_COOLDOWN_REMAINING_STEPS_INDEX] > 0.0f;
if (dd_triggered || in_cooldown) {
    actions[b] = ACTION_HOLD;
    target_lots[b] = 0;
    continue;  // skip rest of action translation for this batch
}

// Max-open units check (per-batch dynamic count).
int active_units = 0;
for (int u = 0; u < MAX_UNITS; u++) {
    if (unit_active_d[b * MAX_UNITS + u]) active_units++;
}
const int max_open = (int)isv[RL_MAX_OPEN_UNITS_INDEX];
if (active_units >= max_open && is_opening_action(actions[b])) {
    actions[b] = ACTION_HOLD;
    // continue translating since Hold has no further side-effects
}

// Inventory cap check.
const float net_pos_usd = compute_net_position_usd(b, unit_active_d, unit_lots_d, current_mid);
const float inv_limit   = isv[RL_NET_INVENTORY_LIMIT_USD_INDEX];
if (fabsf(net_pos_usd) > inv_limit && would_increase_exposure(actions[b], net_pos_usd)) {
    actions[b] = ACTION_HOLD;
}
```

Where `is_opening_action(a)` returns true for {Long, Short, ...} and false for {Hold, Flat, HalfFlat, Trail*}. Define as a `__device__` helper near the top of the file.

- [ ] **Step 4: Verify build + integrated_trainer_smoke still passes**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --release 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored 2>&1 | tail -3
```

Expected: `Finished` + smoke passes (controllers' new ISV slots are sentinel 0 so overrides don't fire).

---

### Task 4: Layer 2 — IQN risk-averse action selection

**Files:**
- Create: `crates/ml-alpha/cuda/rl_iqn_action_tau_controller.cu`
- Modify: `crates/ml-alpha/build.rs`
- Modify: `crates/ml-alpha/cuda/rl_ensemble_action_value.cu` (or wherever IQN action selection happens — verify location during Step 0)

- [ ] **Step 0: Locate the IQN action selection site**

```bash
cd /home/jgrusewski/Work/foxhunt
grep -rn "expected_q\|argmax.*Q\|compute_q\|RL_IQN_ENSEMBLE_ALPHA" crates/ml-alpha/cuda/rl_ensemble_action_value.cu | head -10
grep -rn "iqn.*action\|iqn_head.*forward\|sample_tau" crates/ml-alpha/src/trainer/integrated.rs | head -10
```

Identify the kernel that computes per-action expected Q from IQN quantiles. Most likely `rl_ensemble_action_value.cu`.

- [ ] **Step 1: Write `rl_iqn_action_tau_controller.cu`**

```c
// rl_iqn_action_tau_controller.cu — Layer 2: adapt IQN action-selection quantile τ
// from observed session drawdown. Normal trading: τ=0.5 (median ≈ expected-Q-like).
// In drawdown: τ → MIN (pessimistic, downside-aware).

#define RL_IQN_ACTION_TAU_INDEX                  671
#define RL_IQN_ACTION_TAU_MIN_INDEX              672
#define RL_IQN_ACTION_TAU_DD_SENSITIVITY_INDEX   673
#define RL_SESSION_PNL_USD_INDEX                 662
#define RL_SESSION_MAX_PNL_USD_INDEX             684  // see Step 2 (new slot if not yet added)

// NOTE: starting capital reference for normalizing drawdown. If not in ISV,
// use a default $35k constant — but per `feedback_adaptive_not_tuned` add to ISV.
#define DEFAULT_STARTING_CAPITAL_USD 35000.0f

extern "C" __global__ void rl_iqn_action_tau_controller(
    float* __restrict__ isv
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    const float session_pnl = isv[RL_SESSION_PNL_USD_INDEX];
    float max_pnl = isv[RL_SESSION_MAX_PNL_USD_INDEX];
    if (session_pnl > max_pnl) {
        max_pnl = session_pnl;
        isv[RL_SESSION_MAX_PNL_USD_INDEX] = max_pnl;
    }

    const float drawdown_usd  = fmaxf(0.0f, max_pnl - session_pnl);
    const float drawdown_frac = drawdown_usd / DEFAULT_STARTING_CAPITAL_USD;

    const float tau_min     = isv[RL_IQN_ACTION_TAU_MIN_INDEX];
    const float sensitivity = isv[RL_IQN_ACTION_TAU_DD_SENSITIVITY_INDEX];
    const float tau_target  = fmaxf(tau_min, 0.5f - sensitivity * drawdown_frac);
    isv[RL_IQN_ACTION_TAU_INDEX] = tau_target;
}
```

NOTE: If `RL_SESSION_MAX_PNL_USD_INDEX` isn't yet allocated, add it to Task 1 (becomes slot 684, `RL_SLOTS_END` 684 → 685). Same for `RL_STARTING_CAPITAL_USD_INDEX` if we want it ISV-driven (recommended; bump to 686). Update Task 1 to include both.

- [ ] **Step 2: Register cubin in `build.rs`**

Add: `"rl_iqn_action_tau_controller",`

- [ ] **Step 3: Modify the IQN ensemble action-value kernel to use adaptive τ**

In `rl_ensemble_action_value.cu`, find where Q values per action are computed. The IQN side currently uses `expected_Q = E_τ[Q(s, a, τ)]`. Replace with quantile-specific Q:

```c
// BEFORE (illustrative — actual code structure may differ):
float exp_q_iqn[N_ACTIONS];
for (int a = 0; a < N_ACTIONS; a++) {
    float sum = 0.0f;
    for (int t = 0; t < N_TAU; t++) sum += iqn_q[a * N_TAU + t];
    exp_q_iqn[a] = sum / (float)N_TAU;
}

// AFTER:
const float tau_a = isv[RL_IQN_ACTION_TAU_INDEX];
float pess_q_iqn[N_ACTIONS];
for (int a = 0; a < N_ACTIONS; a++) {
    // Find the τ-th quantile (linear interpolation across sampled τ values).
    // tau_samples[t] is monotonic in t (sorted), and tau_a ∈ [0, 1].
    const float target_idx = tau_a * (float)(N_TAU - 1);
    const int   t_lo = (int)floorf(target_idx);
    const int   t_hi = min(t_lo + 1, N_TAU - 1);
    const float frac = target_idx - (float)t_lo;
    pess_q_iqn[a] = (1.0f - frac) * iqn_q[a * N_TAU + t_lo] + frac * iqn_q[a * N_TAU + t_hi];
}

// Existing ensemble blend with C51 side stays the same:
const float alpha = isv[RL_IQN_ENSEMBLE_ALPHA_INDEX];
float ensemble_q[N_ACTIONS];
for (int a = 0; a < N_ACTIONS; a++) {
    ensemble_q[a] = alpha * c51_expected[a] + (1.0f - alpha) * pess_q_iqn[a];
}
// argmax(ensemble_q) → selected_action
```

NOTE: This assumes `iqn_q` is sorted ascending in τ. If not, must sort first OR sample at the specific τ via the IQN forward kernel. Check kernel architecture during implementation.

- [ ] **Step 4: Verify build + smoke**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --release 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored 2>&1 | tail -3
```

Expected: clean + smoke passes (τ=0.5 default = same as expected-Q behavior approximately).

---

### Task 5: Layer 3 — Inventory penalty controller + reward shaping

**Files:**
- Create: `crates/ml-alpha/cuda/rl_inventory_beta_controller.cu`
- Modify: `crates/ml-alpha/build.rs`
- Modify: `crates/ml-alpha/cuda/rl_fused_reward_pipeline.cu`

- [ ] **Step 1: Write `rl_inventory_beta_controller.cu`**

```c
// rl_inventory_beta_controller.cu — Layer 3: adapt inventory penalty coefficient β.
//
// Target: penalty ≈ 1% of typical absolute reward when inventory is at ~2σ.
// β = 0.01 × E[|reward|] / (2 × σ_inventory).
// Wiener blend with floor 0.4 per pearl_wiener_alpha_floor_for_nonstationary.
// Bootstrap on sentinel 0.0 (β = 0 until variance EMA accumulates real signal).

#define RL_INVENTORY_PENALTY_BETA_INDEX  674
#define RL_INVENTORY_VARIANCE_EMA_INDEX  675
#define RL_REWARD_MAGNITUDE_EMA_INDEX    614  // from b1ef6664a
#define RL_WIENER_ALPHA_FLOOR_INDEX      659  // from b1ef6664a

extern "C" __global__ void rl_inventory_beta_controller(
    float* __restrict__ isv
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    const float reward_mag = isv[RL_REWARD_MAGNITUDE_EMA_INDEX];
    const float inv_var    = isv[RL_INVENTORY_VARIANCE_EMA_INDEX];
    if (inv_var <= 0.0f || reward_mag <= 0.0f) return;  // dead-signal hold

    const float inv_std = sqrtf(inv_var);
    const float beta_target = 0.01f * reward_mag / (2.0f * inv_std);

    const float prev = isv[RL_INVENTORY_PENALTY_BETA_INDEX];
    const float a    = isv[RL_WIENER_ALPHA_FLOOR_INDEX];  // typically 0.4
    if (prev == 0.0f) {
        isv[RL_INVENTORY_PENALTY_BETA_INDEX] = beta_target;
    } else {
        isv[RL_INVENTORY_PENALTY_BETA_INDEX] = (1.0f - a) * prev + a * beta_target;
    }
}
```

- [ ] **Step 2: Register cubin**

Add to `build.rs`: `"rl_inventory_beta_controller",`

- [ ] **Step 3: Modify `rl_fused_reward_pipeline.cu` — apply inventory penalty**

Find the reward computation per batch slot. After raw reward, before clamps:

```c
// Layer 3 (spec 2026-05-30): inventory penalty.
// reward_shaped = reward_raw − β × |net_position|
const float net_pos     = net_position_per_batch[b];  // signed contracts
const float inv_beta    = isv[RL_INVENTORY_PENALTY_BETA_INDEX];
const float inv_penalty = inv_beta * fabsf(net_pos);
shaped_reward = shaped_reward - inv_penalty;
```

- [ ] **Step 4: Build + smoke**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --release 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored 2>&1 | tail -3
```

---

### Task 6: Layer 4 — Kelly-fraction sizing

**Files:**
- Create: `crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu`
- Modify: `crates/ml-alpha/build.rs`
- Modify: `crates/ml-alpha/cuda/actions_to_market_targets.cu`

- [ ] **Step 1: Write `rl_kelly_fraction_controller.cu`**

```c
// rl_kelly_fraction_controller.cu — Layer 4: half-Kelly position size from observed edge.
//
// f_kelly = (p × b − q) / b   where p = win_rate, q = 1−p, b = avg_win/avg_loss
// f       = clamp(safety × f_kelly, 0, 1)   where safety = 0.5 (Thorp half-Kelly)
// Warmup: until cumulative_dones ≥ MIN_TRADES_FOR_RELEASE, hold at bootstrap=1.0.

#define RL_KELLY_FRACTION_INDEX                676
#define RL_WIN_RATE_EMA_INDEX                  677
#define RL_AVG_WIN_USD_EMA_INDEX               678
#define RL_AVG_LOSS_USD_EMA_INDEX              679
#define RL_KELLY_SAFETY_FRAC_INDEX             680
#define RL_KELLY_MIN_TRADES_FOR_RELEASE_INDEX  681
#define RL_CUMULATIVE_DONES_INDEX              660  // from b1ef6664a

extern "C" __global__ void rl_kelly_fraction_controller(
    float* __restrict__ isv
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    // Warmup gate: hold at bootstrap=1.0 until enough closed trades.
    if (isv[RL_CUMULATIVE_DONES_INDEX] < isv[RL_KELLY_MIN_TRADES_FOR_RELEASE_INDEX]) {
        isv[RL_KELLY_FRACTION_INDEX] = 1.0f;
        return;
    }

    const float p        = isv[RL_WIN_RATE_EMA_INDEX];
    const float avg_win  = isv[RL_AVG_WIN_USD_EMA_INDEX];
    const float avg_loss = isv[RL_AVG_LOSS_USD_EMA_INDEX];

    // Dead-signal guard: need positive loss magnitude for R-multiple.
    if (avg_loss <= 0.0f) { isv[RL_KELLY_FRACTION_INDEX] = 1.0f; return; }

    const float b       = avg_win / avg_loss;
    const float q       = 1.0f - p;
    const float f_kelly = (p * b - q) / b;
    const float safety  = isv[RL_KELLY_SAFETY_FRAC_INDEX];  // typically 0.5
    float       f       = f_kelly * safety;
    f = fmaxf(0.0f, fminf(f, 1.0f));
    isv[RL_KELLY_FRACTION_INDEX] = f;
}
```

- [ ] **Step 2: Register cubin**

Add to `build.rs`: `"rl_kelly_fraction_controller",`

- [ ] **Step 3: Apply Kelly scaling in `actions_to_market_targets.cu`**

After Layer 1 overrides (Task 3) have been applied and before returning per-batch results:

```c
// Layer 4 (spec 2026-05-30): Kelly-fraction sizing.
// Apply AFTER Layer 1 overrides have set target_lots based on the action.
const float kelly = isv[RL_KELLY_FRACTION_INDEX];
target_lots[b] = (int)ceilf((float)target_lots[b] * kelly);
// If kelly = 0 (Kelly negative or warmup), target_lots = 0 → no commitment.
```

- [ ] **Step 4: Build + smoke**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --release 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored 2>&1 | tail -3
```

---

### Task 7: Layer D — TrailTighten/Loosen wiring

**Files:**
- Modify: `crates/ml-alpha/cuda/actions_to_market_targets.cu`

- [ ] **Step 1: Wire a7 (TrailTighten) and a8 (TrailLoosen) into the action switch**

Find the existing switch statement (likely already has `case ACTION_TRAIL_TIGHTEN:` as a stub). Replace stub with:

```c
case ACTION_TRAIL_TIGHTEN: {
    // Spec 2026-05-30 Layer D — close pearl_dead_trail_stop_actions_a7_a8 gap.
    const float factor = isv[RL_TRAIL_TIGHTEN_FACTOR_INDEX];  // bootstrap 0.9
    for (int u = 0; u < MAX_UNITS; u++) {
        if (unit_active_d[b * MAX_UNITS + u]) {
            unit_trail_distance_d[b * MAX_UNITS + u] *= factor;
        }
    }
    // No new market order — pure ISV state mutation.
    target_lots[b] = 0;
    break;
}
case ACTION_TRAIL_LOOSEN: {
    const float factor = isv[RL_TRAIL_LOOSEN_FACTOR_INDEX];  // bootstrap 1.1
    for (int u = 0; u < MAX_UNITS; u++) {
        if (unit_active_d[b * MAX_UNITS + u]) {
            unit_trail_distance_d[b * MAX_UNITS + u] *= factor;
        }
    }
    target_lots[b] = 0;
    break;
}
```

- [ ] **Step 2: Build + smoke**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --release 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored 2>&1 | tail -3
```

---

### Task 8: Fused kernel integration

**Files:**
- Modify: `crates/ml-alpha/cuda/rl_fused_controllers.cu`

- [ ] **Step 1: Add Layer 2 + Layer 3 + Layer 4 branches**

The fused kernel already has 10 controller branches. Append 3 more (matching the per-controller standalone logic from Tasks 4, 5, 6) at the end. Layer 1 stays a separate kernel because it needs per-step `dones` + `realized_pnl` arrays.

```c
// ═══════════════════════════════════════════════════════════════════
// 11. IQN action τ controller → ISV[671]  (Layer 2)
// ═══════════════════════════════════════════════════════════════════
{
    // [paste body of rl_iqn_action_tau_controller from Task 4 Step 1]
}

// ═══════════════════════════════════════════════════════════════════
// 12. Inventory β controller → ISV[674]  (Layer 3)
// ═══════════════════════════════════════════════════════════════════
{
    // [paste body of rl_inventory_beta_controller from Task 5 Step 1]
}

// ═══════════════════════════════════════════════════════════════════
// 13. Kelly fraction controller → ISV[676]  (Layer 4)
// ═══════════════════════════════════════════════════════════════════
{
    // [paste body of rl_kelly_fraction_controller from Task 6 Step 1]
}
```

- [ ] **Step 2: Build + smoke**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --release 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test signal_variance_kernel --release -- --ignored 2>&1 | tail -3
```

---

### Task 9: Trainer integration

**Files:**
- Modify: `crates/ml-alpha/src/trainer/integrated.rs`

- [ ] **Step 1: Load 7 new cubins, add struct fields, struct init**

Follow the exact pattern from b1ef6664a's `rl_signal_variance_update` integration. For each of:
- `rl_cmdp_constraints_check`
- `rl_iqn_action_tau_controller`
- `rl_inventory_beta_controller`
- `rl_kelly_fraction_controller`
- `rl_win_rate_ema_update`
- `rl_avg_win_loss_ema_update`
- `rl_inventory_variance_update`

Add:
```rust
const RL_<NAME>_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/rl_<name>.cubin"));
// + struct field _module + fn
// + load in new()
// + init in Ok(Self {...})
```

- [ ] **Step 2: Add per-step launches in the per-step pipeline**

Find the per-step block (search for `launch_rl_fused_controllers`). Insert NEW launches **between** the EMA producers and the controllers:

```rust
// After existing EMA producer launches:

// Layer 1 — CMDP state update (per-step, reads pnl + outcomes)
self.launch_rl_cmdp_constraints_check(
    &realized_pnl_per_batch_d,
    &trade_outcome_per_batch_d,
    b_size,
)?;

// New EMA producers (feed Kelly + inventory β controllers)
self.launch_rl_win_rate_ema_update(&trade_outcome_per_batch_d, b_size)?;
self.launch_rl_avg_win_loss_ema_update(
    &realized_pnl_per_batch_d,
    &trade_outcome_per_batch_d,
    b_size,
)?;
self.launch_rl_inventory_variance_update(&net_position_per_batch_d, b_size)?;

// Layer 2/3/4 controllers — already integrated into fused kernel (Task 8),
// so no separate launches needed. The fused kernel handles them.

// Existing fused controllers launch continues as-is.
self.launch_rl_fused_controllers(b_size)?;
```

Each `launch_*` helper follows the same pattern as `launch_rl_signal_variance_update` from b1ef6664a.

- [ ] **Step 3: Bootstrap all 22 new ISV slots in `with_controllers_bootstrapped`**

Add entries to the `isv_constants` array (currently 147 entries from b1ef6664a). New entries:

```rust
// Layer 1
(RL_SESSION_PNL_USD_INDEX,            0.0),       // session boundary reset
(RL_SESSION_DD_LIMIT_USD_INDEX,    -3500.0),       // 10% of $35k starting capital
(RL_SESSION_DD_TRIGGERED_INDEX,        0.0),
(RL_MAX_OPEN_UNITS_INDEX,              4.0),       // = current MAX_UNITS
(RL_CONSEC_LOSS_LIMIT_INDEX,          10.0),
(RL_CONSEC_LOSS_COUNT_INDEX,           0.0),
(RL_COOLDOWN_REMAINING_STEPS_INDEX,    0.0),
(RL_COOLDOWN_DURATION_INDEX,         500.0),
(RL_NET_INVENTORY_LIMIT_USD_INDEX, 10000.0),
// Layer 2
(RL_IQN_ACTION_TAU_INDEX,              0.5),       // median = ~current behavior
(RL_IQN_ACTION_TAU_MIN_INDEX,          0.1),
(RL_IQN_ACTION_TAU_DD_SENSITIVITY_INDEX, 5.0),
// Layer 3
(RL_INVENTORY_PENALTY_BETA_INDEX,      0.0),       // sentinel
(RL_INVENTORY_VARIANCE_EMA_INDEX,      0.0),       // sentinel
// Layer 4
(RL_KELLY_FRACTION_INDEX,              1.0),       // full size pre-warmup
(RL_WIN_RATE_EMA_INDEX,                0.5),       // neutral sentinel
(RL_AVG_WIN_USD_EMA_INDEX,             0.0),       // sentinel
(RL_AVG_LOSS_USD_EMA_INDEX,            0.0),       // sentinel
(RL_KELLY_SAFETY_FRAC_INDEX,           0.5),       // half-Kelly Thorp default
(RL_KELLY_MIN_TRADES_FOR_RELEASE_INDEX, 1000.0),
// Layer D
(RL_TRAIL_TIGHTEN_FACTOR_INDEX,        0.9),
(RL_TRAIL_LOOSEN_FACTOR_INDEX,         1.1),
```

Bump array size to 169.

- [ ] **Step 4: Add session-boundary reset on train/eval phase transition**

Find the trainer's fold/eval transition (look for `n_eval_steps` or `eval phase` markers in `integrated.rs`). At the transition, reset Layer 1 session-level state:

```rust
// Session boundary — reset Layer 1 session state for fresh eval phase.
self.isv_mapped.write_record(RL_SESSION_PNL_USD_INDEX,         0.0);
self.isv_mapped.write_record(RL_SESSION_DD_TRIGGERED_INDEX,    0.0);
self.isv_mapped.write_record(RL_CONSEC_LOSS_COUNT_INDEX,       0.0);
self.isv_mapped.write_record(RL_COOLDOWN_REMAINING_STEPS_INDEX, 0.0);
// Also reset Kelly + inventory EMAs if we want eval to start clean (debatable)
// — leave them for now to test "policy learned during train, applied in eval"
```

- [ ] **Step 5: Build + smoke**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --release 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test controller_adaptive_floors --release -- --ignored 2>&1 | tail -3
SQLX_OFFLINE=true cargo test -p ml-alpha --test signal_variance_kernel --release -- --ignored 2>&1 | tail -3
```

Expected: all pass.

---

### Task 10: GPU-oracle invariant tests

**Files:**
- Create: `crates/ml-alpha/tests/risk_stack_invariants.rs`

- [ ] **Step 1: Write 12 GPU-oracle tests**

Follow the pattern from `controller_adaptive_floors.rs`. For brevity, the full test bodies are templated below — instantiate each gate using `IntegratedTrainer::new` + ISV writes + `launch_rl_fused_controllers` + ISV reads.

```rust
//! Risk-stack invariants (spec 2026-05-30-adaptive-risk-management-design).
//!
//! 12 GPU-oracle gates — one per layer behavior + integration tests.
//!
//! Run with:
//!   cargo test -p ml-alpha --test risk_stack_invariants --release -- --ignored --nocapture

use ml_alpha::rl::isv_slots::*;
use ml_alpha::trainer::integrated::{IntegratedTrainer, IntegratedTrainerConfig};
use ml_alpha::trainer::perception::PerceptionTrainerConfig;
use ml_core::device::MlDevice;

// [build_trainer helper — copy from controller_adaptive_floors.rs]

#[test] #[ignore]
fn g1_cmdp_dd_breaker_triggers_at_limit() { /* ... */ }

#[test] #[ignore]
fn g2_cmdp_cooldown_after_consec_losses() { /* ... */ }

#[test] #[ignore]
fn g3_cmdp_max_open_forces_hold_on_opening_action() { /* ... */ }

#[test] #[ignore]
fn g4_cmdp_inventory_cap_blocks_one_sided_expansion() { /* ... */ }

#[test] #[ignore]
fn g5_iqn_tau_adapts_to_drawdown() { /* ... */ }

#[test] #[ignore]
fn g6_iqn_tau_at_peak_returns_0_5() { /* ... */ }

#[test] #[ignore]
fn g7_inventory_beta_adapts_from_variance() { /* ... */ }

#[test] #[ignore]
fn g8_kelly_at_known_edge_returns_0_15() {
    // win_rate=0.55, avg_win=1.8, avg_loss=1.0, trades=2000 → f = 0.5 × 0.30 = 0.15
}

#[test] #[ignore]
fn g9_kelly_at_negative_edge_returns_0() {
    // win_rate=0.21, avg_win=1.8, avg_loss=1.0, trades=2000 → f = 0
}

#[test] #[ignore]
fn g10_kelly_held_at_bootstrap_during_warmup() {
    // cumulative_dones=500 (< 1000) → f = 1.0 regardless of input
}

#[test] #[ignore]
fn g11_trail_tighten_reduces_distance() {
    // Pre-set unit_trail_distance=100, agent picks a7 → distance becomes 90
}

#[test] #[ignore]
fn g12_dd_breaker_overrides_kelly() {
    // Set kelly=1.0 AND dd_triggered=1 → target_lots=0
}
```

- [ ] **Step 2: Run all gates**

```bash
SQLX_OFFLINE=true cargo test -p ml-alpha --test risk_stack_invariants --release -- --ignored --nocapture 2>&1 | tail -20
```

Expected: 12 passed.

---

### Task 11: Local 1k smoke + compute-sanitizer

- [ ] **Step 1: 1k-step smoke at b=128**

```bash
cd /home/jgrusewski/Work/foxhunt
SMOKE_OUT=/tmp/risk-stack-smoke
TEST_DATA=/home/jgrusewski/Work/foxhunt/test_data/futures-baseline
rm -rf "$SMOKE_OUT" && mkdir -p "$SMOKE_OUT/predecoded"
SQLX_OFFLINE=true cargo build -p ml-alpha --release --example alpha_rl_train 2>&1 | tail -2
./target/release/examples/alpha_rl_train \
  --mbp10-data-dir "$TEST_DATA/ES.FUT" \
  --predecoded-dir "$SMOKE_OUT/predecoded" \
  --out "$SMOKE_OUT" \
  --n-steps 1000 --n-backtests 128 --seq-len 32 \
  --per-capacity 1024 --seed 16962 --instrument-mode all \
  --n-folds 1 --fold-idx 0 --n-eval-steps 0 \
  --diag-jsonl "$SMOKE_OUT/diag.jsonl"
```

Expected: completes 1000/1000 steps clean, no NaN.

- [ ] **Step 2: Inspect risk-stack ISVs in diag**

```bash
tail -1 $SMOKE_OUT/diag.jsonl | tr "," "\n" | \
  grep -E "kelly_fraction|iqn_action_tau|inventory_penalty|session_pnl|session_dd_triggered"
```

Expected: kelly_fraction ∈ [0, 1], iqn_action_tau ∈ [0.1, 0.5], inventory_penalty_beta > 0, session_pnl tracked, session_dd_triggered = 0 (unlikely to hit at b=128 1k).

- [ ] **Step 3: compute-sanitizer memcheck**

```bash
SANITIZER_OUT=/tmp/risk-stack-cs
rm -rf "$SANITIZER_OUT" && mkdir -p "$SANITIZER_OUT/predecoded"
compute-sanitizer --tool memcheck --launch-timeout 600 --error-exitcode 1 \
  ./target/release/examples/alpha_rl_train \
  --mbp10-data-dir "$TEST_DATA/ES.FUT" \
  --predecoded-dir "$SANITIZER_OUT/predecoded" \
  --out "$SANITIZER_OUT" \
  --n-steps 5 --n-backtests 128 --seq-len 32 \
  --per-capacity 1024 --seed 16962 --instrument-mode all \
  --n-folds 1 --fold-idx 0 --n-eval-steps 0 \
  2>&1 | grep "ERROR SUMMARY"
```

Expected: `ERROR SUMMARY: 0 errors`.

---

### Task 12: Atomic commit + push

- [ ] **Step 1: Stage all changes**

```bash
cd /home/jgrusewski/Work/foxhunt
git add crates/ml-alpha/cuda/rl_cmdp_constraints_check.cu \
        crates/ml-alpha/cuda/rl_iqn_action_tau_controller.cu \
        crates/ml-alpha/cuda/rl_inventory_beta_controller.cu \
        crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu \
        crates/ml-alpha/cuda/rl_win_rate_ema_update.cu \
        crates/ml-alpha/cuda/rl_avg_win_loss_ema_update.cu \
        crates/ml-alpha/cuda/rl_inventory_variance_update.cu \
        crates/ml-alpha/cuda/actions_to_market_targets.cu \
        crates/ml-alpha/cuda/rl_fused_reward_pipeline.cu \
        crates/ml-alpha/cuda/rl_ensemble_action_value.cu \
        crates/ml-alpha/cuda/rl_fused_controllers.cu \
        crates/ml-alpha/src/rl/isv_slots.rs \
        crates/ml-alpha/src/trainer/integrated.rs \
        crates/ml-alpha/tests/risk_stack_invariants.rs \
        crates/ml-alpha/build.rs \
        docs/superpowers/specs/2026-05-30-adaptive-risk-management-design.md \
        docs/superpowers/plans/2026-05-30-adaptive-risk-management-plan.md
```

- [ ] **Step 2: Commit with comprehensive message**

```bash
git commit -m "$(cat <<'EOF'
feat(rl): adaptive risk management — 5-layer stack

Closes the train-eval gap surfaced by the b1ef6664a adaptive-controller-
floor refactor (fold 1 eval -$616k despite train -$1.05M = 68% better
than OLD). Diagnosis from fold 1 eval JSONL: agent has good per-trade
R-multiple (1.8) but win_rate collapses 0.55 (train) → 0.21 (eval) →
Kelly fraction NEGATIVE. The agent trades 1024 batches at no edge
because position commitment was never adaptive.

Five-layer architectural completion:

  Layer 1 — Hard CMDP constraints (rl_cmdp_constraints_check.cu)
    Session DD cap (-$3.5k = 10% of $35k starting capital).
    Cool-down after 10 consecutive losses (500-step lockout).
    Max-open positions per batch (= MAX_UNITS).
    Net inventory cap (USD).
    Override actions to Hold on any violation.

  Layer 2 — IQN risk-averse action selection (rl_iqn_action_tau_controller.cu
    + rl_ensemble_action_value.cu modification)
    Reads adaptive τ from session drawdown signal.
    Action selection: argmax(Q(s, a, τ_action)) instead of argmax(E_τ[Q]).
    Normal trading: τ=0.5 (median, ≈ expected-Q behavior).
    Under drawdown: τ → MIN=0.1 (downside-aware decisions).
    HIGHEST-LEVERAGE LAYER — uses existing IQN distribution for free.

  Layer 3 — Inventory penalty (rl_inventory_beta_controller.cu + reward shaping)
    β-scaled penalty on |net_position| in fused reward pipeline.
    β adaptive from observed inventory variance vs reward magnitude.
    Targets ~1% reward reduction at 2σ inventory.

  Layer 4 — Kelly-fraction sizing (rl_kelly_fraction_controller.cu)
    Half-Kelly from observed win_rate × R-multiple EMAs.
    Multiplies lot count in actions_to_market_targets.
    Warmup: held at bootstrap=1.0 until 1000 closed trades accumulate.

  Layer D — TrailTighten/Loosen wiring (actions_to_market_targets.cu)
    Closes pearl_dead_trail_stop_actions_a7_a8 — 10% policy mass was
    wasted on no-ops. Now mutates unit_trail_distance_d by ISV-driven
    factors (bootstrap 0.9 tighten / 1.1 loosen).

22 new ISV slots, RL_SLOTS_END 662 → 684. 7 new kernels, 6 modified files.

Spec: docs/superpowers/specs/2026-05-30-adaptive-risk-management-design.md
Plan: docs/superpowers/plans/2026-05-30-adaptive-risk-management-plan.md

Validation:
  - cargo build --release: clean
  - cargo build --tests: clean
  - 12 GPU-oracle invariant tests (G1-G12 in risk_stack_invariants.rs): pass
  - integrated_trainer_smoke: passes
  - 1k local smoke b=128: completes_clean, no NaN, all 5 layers' ISVs
    populated and tracking sensibly
  - compute-sanitizer memcheck b=128 5 steps: 0 errors

Cluster validation (G13 fold 1 re-run + G14 ksll2 regression) submitted
as follow-up runs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```

- [ ] **Step 3: Push to origin**

```bash
git push origin ml-alpha-adaptive-controller-floors 2>&1 | tail -3
git rev-parse HEAD
```

Expected: pushed, new commit SHA returned.

---

### Task 13: Cluster validation (G13 fold 1 + G14 ksll2)

- [ ] **Step 1: Submit fold 1 walk-forward at new SHA**

```bash
cd /home/jgrusewski/Work/foxhunt
NEW_SHA=$(git rev-parse HEAD)
./scripts/argo-alpha-rl.sh \
  --sha "$NEW_SHA" \
  --branch ml-alpha-adaptive-controller-floors \
  --gpu-pool ci-training-l40s \
  --n-steps 20000 --seq-len 32 --n-backtests 1024 --per-capacity 32768 \
  --seed 16962 --instrument-mode all \
  --n-folds 3 --fold-idx 1 --n-eval-steps 2000 \
  --skip-push-check
```

- [ ] **Step 2: Submit ksll2 regression in parallel** (after fold 1 starts)

Wait for fold 1 to be Running. Submit ksll2 to same L40S pool — it will queue. Or submit to H100 if needed (note PVC RWO constraint from `pearl_h100_pvc_rwo_blocks_parallel_gpu_workloads`).

```bash
./scripts/argo-alpha-rl.sh \
  --sha "$NEW_SHA" \
  --branch ml-alpha-adaptive-controller-floors \
  --gpu-pool ci-training-l40s \
  --n-steps 20000 --seq-len 32 --n-backtests 1024 --per-capacity 32768 \
  --seed 16962 --instrument-mode all \
  --n-folds 1 --fold-idx 0 --n-eval-steps 0 \
  --skip-push-check
```

- [ ] **Step 3: Wait + collect results**

Monitor via the Monitor tool. After both terminal:

Extract fold 1 eval summary from PVC reader:
```bash
# pvc-reader pod pattern as in prior sessions
kubectl -n foxhunt exec pvc-reader -- cat /feature-cache/alpha-rl-runs/$NEW_SHA/fold1/eval_summary.json
```

- [ ] **Step 4: G13 verdict**

Pass conditions (3+ of 6 = PASS):
- eval pnl > -$200k (vs today's -$616k) — **PRIMARY**
- profit_factor > 0.90 (vs 0.83)
- max_drawdown_pct < 15% (vs 18.7%)
- Kelly fraction trajectory: ends < 1.0 if observed edge negative
- IQN τ trajectory: ends < 0.5 if drawdown occurred
- Inventory β > 0 in late training (Layer 3 active)

- [ ] **Step 5: G14 verdict**

ksll2 regression: final pnl ≥ +$13M (within 20% of today's +$15.81M).

- [ ] **Step 6: Update spec with cluster results + write pearls**

Save outcome to:
- `project_adaptive_risk_management_session_<date>.md`
- Update `MEMORY.md` index
- Decision: merge to main now (if both gates pass) OR iterate (if G13 marginal/fails)

---

## Self-review

### Coverage check
- Spec scope (5 layers) → 1 task per layer (Tasks 3-7), shared infrastructure tasks (1, 2, 8, 9), validation (10-13) ✓
- Every controller branch in spec has a kernel in Task 2/3/4/5/6 ✓
- Every ISV slot in spec is allocated in Task 1 ✓
- Spec's validation gates G1-G14 all mapped to Tasks 10-13 ✓

### Placeholder scan
- Task 4 Step 0 explicitly delegates "verify kernel location" because the IQN action-selection site requires reading the actual codebase. Acceptable — it's a precondition step, not a hidden TODO.
- Task 10 templates the 12 tests rather than fully writing each. Acceptable per writing-plans for tests that follow the same pattern; the templates name the inputs + expected output for each.

### Type consistency
- All ISV slot constants used in kernels (Tasks 2-7) match the definitions in Task 1 ✓
- `actions_to_market_targets.cu` modifications (Tasks 3, 6, 7) all reference consistent slot names ✓

### Atomic commit discipline
- Tasks 1-11 build progressively without committing ✓
- Task 12 is the single commit ✓
- Per `feedback_no_partial_refactor` — same pattern as the controller-floor refactor ✓

### Known open items deferred
- Multi-seed cluster validation (deferred to post-G13 if marginal)
- Regime detection / meta-policy (deferred per research caveat — not production-validated)
- Confidence-to-size scaling (was Approach C, dropped — Layer 2's quantile selection achieves the intent without the multiplicative complexity)

---

## Execution

Use **subagent-driven-development** (recommended) — Tasks 3-7 are each well-scoped per-layer changes that map naturally to one subagent per task. Two-stage review (spec compliance + code quality) after each. Tasks 9 (trainer integration) and 10 (tests) are higher-context, may dispatch a single subagent for both. Task 12 (commit) and Task 13 (cluster) execute inline by the orchestrator.

Estimated cumulative wall-time:
- Tasks 1-2: ~30 min (slot allocation + 3 EMA kernels)
- Tasks 3-7: ~3 hours (5 layers, ~30-40 min each via subagents)
- Task 8 (fused integration): ~30 min
- Task 9 (trainer integration): ~45 min — most complex, may warrant subagent
- Tasks 10-11: ~1 hour (tests + smoke)
- Task 12: ~10 min (commit)
- Task 13: ~2-3 hours (cluster validation, two 20k runs sequential on L40S)

Total: ~8 hours of focused work + cluster wait time.
