# Trade Plan Head 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:** Add a hierarchical plan head that outputs 6 trade parameters (target_bars, profit_target, stop_loss, scale_aggression, conviction, asymmetry) at entry, with auto-exit enforcement during the trade. Replaces per-bar coin-flip decisions with committed, risk-managed trade plans.

**Architecture:** Plan head MLP (h_s2→AH→6 params) fires at entry. Plan stored in portfolio state ps[23-29]. Direction locked during plan. Auto-exit on profit/stop/time/regime. PORTFOLIO_STRIDE grows 23→30. 4 new weight tensors at indices 82-85 (NUM_WEIGHT_TENSORS 82→86, assuming Phase 3 completes first at 82; if Phase 3 is not done yet, adjust indices accordingly).

**Tech Stack:** Rust 1.85, CUDA 12.4, experience_kernels.cu, gpu_dqn_trainer.rs

---

## Task 1: PORTFOLIO_STRIDE 23→30 + Plan Slot Allocation

Grow the portfolio state array to accommodate 7 plan slots. This must come FIRST because all other tasks depend on the wider stride.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/trade_stats_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/backtest_env_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs`

- [ ] **Step 1: Change PORTFOLIO_STRIDE in experience_kernels.cu**

Find line 58:
```cuda
#define PORTFOLIO_STRIDE 23
```
Change to:
```cuda
#define PORTFOLIO_STRIDE 30
```

Update the doc comment (lines 23-52) to add:
```
 *   [23] plan_target_bars  — 0=no plan, >0=active plan max hold bars
 *   [24] plan_profit_target — raw profit threshold %
 *   [25] plan_stop_loss     — raw stop loss threshold %
 *   [26] plan_scale_aggression — position ramp speed (0=cautious, 1=all-in)
 *   [27] plan_conviction    — position size fraction (Kelly proxy)
 *   [28] plan_asymmetry     — profit/stop ratio
 *   [29] counter_plan_q     — opposite direction Q-value at entry (N14)
```

- [ ] **Step 2: Change PORTFOLIO_STRIDE in trade_stats_kernel.cu**

Find:
```cuda
#define PORTFOLIO_STRIDE 23
```
Change to:
```cuda
#define PORTFOLIO_STRIDE 30
```

- [ ] **Step 3: Fix hardcoded `i * 23` in action select**

In `experience_kernels.cu`, find line ~774:
```cuda
        int ps_base = i * 23;  /* PORTFOLIO_STRIDE = 23 */
```
Change to:
```cuda
        int ps_base = i * PORTFOLIO_STRIDE;
```

- [ ] **Step 4: Update Rust buffer allocation**

In `gpu_experience_collector.rs`, find where `portfolio_states` buffer is allocated. It uses `n_episodes * PORTFOLIO_STRIDE_VALUE` or similar. Search for `* 23` in the allocation. Change to `* 30`.

Also search for any Rust constant like `const PORTFOLIO_STRIDE: usize = 23` and update to 30.

- [ ] **Step 5: Zero plan slots on episode reset**

In `experience_env_step`, find the hard reset block (around where `ps[15] = 0.0f` etc. are zeroed). Add:
```cuda
            ps[23] = 0.0f;  /* plan_target_bars */
            ps[24] = 0.0f;  /* plan_profit_target */
            ps[25] = 0.0f;  /* plan_stop_loss */
            ps[26] = 0.0f;  /* plan_scale_aggression */
            ps[27] = 0.0f;  /* plan_conviction */
            ps[28] = 0.0f;  /* plan_asymmetry */
            ps[29] = 0.0f;  /* counter_plan_q */
```

- [ ] **Step 6: Check backtest_env_kernel.cu**

Search for `PORTFOLIO_STRIDE` or `* 23` in backtest kernels. Update any hardcoded values. The backtest kernel may have its own stride — check `backtest_env_kernel.cu` carefully. If it uses a different stride (e.g., 8 for the simpler portfolio sim), leave it alone.

- [ ] **Step 7: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 8: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add -A && git commit -m "feat(plan): PORTFOLIO_STRIDE 23→30 — 7 plan slots ps[23-29]

Plan slots: target_bars, profit_target, stop_loss, scale_aggression,
conviction, asymmetry, counter_plan_q. Zeroed on episode reset.
Fixed hardcoded i*23 → i*PORTFOLIO_STRIDE in action select.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```

---

## Task 2: Plan Head Kernel + Weight Tensors

Add `trade_plan_forward` kernel and 4 new weight tensors.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Write `trade_plan_forward` kernel**

Add to end of `experience_kernels.cu`:

```cuda
/* ================================================================== */
/* Kernel: trade_plan_forward — plan head MLP → 6 plan parameters     */
/* ================================================================== */
extern "C" __global__ void trade_plan_forward(
    const float* __restrict__ h_s2,        /* [B, SH2] */
    const float* __restrict__ w_plan_fc,   /* [AH, SH2] */
    const float* __restrict__ b_plan_fc,   /* [AH] */
    const float* __restrict__ w_plan_out,  /* [6, AH] */
    const float* __restrict__ b_plan_out,  /* [6] */
    float* __restrict__ plan_params,       /* [B, 6] output */
    int B, int SH2, int AH
) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= B) return;

    const float* h = h_s2 + (long long)i * SH2;
    float hidden[128]; /* AH <= 128 */

    for (int j = 0; j < AH; j++) {
        float val = b_plan_fc[j];
        for (int k = 0; k < SH2; k++)
            val += w_plan_fc[(long long)j * SH2 + k] * h[k];
        hidden[j] = fmaxf(val, 0.0f); /* ReLU */
    }

    float* out = plan_params + i * 6;
    for (int p = 0; p < 6; p++) {
        float val = b_plan_out[p];
        for (int j = 0; j < AH; j++)
            val += w_plan_out[(long long)p * AH + j] * hidden[j];
        out[p] = val;
    }

    /* Activations */
    out[0] = 3.0f + 22.0f / (1.0f + expf(-out[0]));        /* target_bars [3, 25] */
    out[1] = 0.0005f + 0.0145f / (1.0f + expf(-out[1]));   /* profit_target [0.05%, 1.5%] */
    out[2] = 0.0002f + 0.0048f / (1.0f + expf(-out[2]));   /* stop_loss [0.02%, 0.5%] */
    out[3] = 1.0f / (1.0f + expf(-out[3]));                 /* scale_aggression [0, 1] */
    out[4] = 1.0f / (1.0f + expf(-out[4]));                 /* conviction [0, 1] */
    out[5] = 0.5f + 2.5f / (1.0f + expf(-out[5]));         /* asymmetry [0.5, 3.0] */
}
```

- [ ] **Step 2: Add weight tensors to compute_param_sizes**

In `gpu_dqn_trainer.rs`, determine the current NUM_WEIGHT_TENSORS value. Add 4 after the last index:

```rust
        // ── Trade Plan Head ──
        cfg.adv_h * cfg.shared_h2,      // [N] w_plan_fc [AH, SH2]
        cfg.adv_h,                        // [N+1] b_plan_fc [AH]
        6 * cfg.adv_h,                   // [N+2] w_plan_out [6, AH]
        6,                                // [N+3] b_plan_out [6]
```

Where N is the first available index. Update NUM_WEIGHT_TENSORS += 4.

- [ ] **Step 3: Add fan_dims for Xavier init**

```rust
        (cfg.adv_h, cfg.shared_h2),  // w_plan_fc
        (1, cfg.adv_h),              // b_plan_fc
        (6, cfg.adv_h),              // w_plan_out
        (1, 6),                       // b_plan_out
```

- [ ] **Step 4: Add plan_params_buf + kernel field**

Struct fields:
```rust
    plan_params_buf: CudaSlice<f32>,     // [B, 6]
    trade_plan_fwd_kernel: CudaFunction,
```

Allocate: `stream.alloc_zeros::<f32>(b * 6)`. Load kernel from experience_kernels cubin.

- [ ] **Step 5: Add launch method**

```rust
    pub(crate) fn launch_trade_plan_forward(&self, batch_size: usize) -> Result<(), MLError> {
        let param_sizes = compute_param_sizes(&self.config);
        let plan_fc_idx = /* first plan tensor index */;
        let w_fc = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, plan_fc_idx);
        let b_fc = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, plan_fc_idx + 1);
        let w_out = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, plan_fc_idx + 2);
        let b_out = self.ptrs.params_ptr + padded_byte_offset(&param_sizes, plan_fc_idx + 3);
        let blocks = ((batch_size as u32 + 255) / 256).max(1);
        let b_i32 = batch_size as i32;
        let sh2 = self.config.shared_h2 as i32;
        let ah = self.config.adv_h as i32;

        unsafe {
            self.stream.launch_builder(&self.trade_plan_fwd_kernel)
                .arg(&self.save_h_s2)
                .arg(&w_fc).arg(&b_fc)
                .arg(&w_out).arg(&b_out)
                .arg(&self.plan_params_buf)
                .arg(&b_i32).arg(&sh2).arg(&ah)
                .launch(LaunchConfig {
                    grid_dim: (blocks, 1, 1),
                    block_dim: (256, 1, 1),
                    shared_mem_bytes: 0,
                })
                .map_err(|e| MLError::ModelError(format!("trade_plan_forward: {e}")))?;
        }
        Ok(())
    }
```

- [ ] **Step 6: Wire into reduce_current_q_stats**

After `launch_recursive_confidence_forward()` and before `risk_budget_forward()`:
```rust
        self.launch_trade_plan_forward(batch_size)?;
```

- [ ] **Step 7: Expose plan_params_buf to experience collector**

Add accessor:
```rust
    pub fn plan_params_buf_ptr(&self) -> u64 { self.plan_params_buf.raw_ptr() }
```

And passthrough in `fused_training.rs`.

- [ ] **Step 8: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 9: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs crates/ml/src/cuda_pipeline/experience_kernels.cu crates/ml/src/trainers/dqn/fused_training.rs && git commit -m "feat(plan): trade_plan_forward kernel + 4 weight tensors

Plan MLP: h_s2 → hidden[AH] → plan_params[B, 6].
6 outputs: target_bars[3-25], profit_target[0.05-1.5%],
stop_loss[0.02-0.5%], scale_aggression[0-1], conviction[0-1],
asymmetry[0.5-3.0]. NUM_WEIGHT_TENSORS grows by 4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```

---

## Task 3: Plan Activation + Enforcement in env_step

Wire plan parameters into the experience env_step kernel for activation at entry and enforcement during trades.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs`

- [ ] **Step 1: Add plan_params_ptr to env_step kernel signature**

After `isv_signals_ptr`:
```cuda
    const float* __restrict__ plan_params_ptr    /* [N, 6] trade plan parameters from plan head */
```

- [ ] **Step 2: Add plan activation logic**

In `experience_env_step`, after the position update and hold enforcement, add plan activation when transitioning Flat→Positioned:

```cuda
    /* ── Plan activation: Flat → Positioned ── */
    int was_flat = (fabsf(pre_trade_position) < 0.001f);
    int now_positioned = (fabsf(position) > 0.001f);

    if (was_flat && now_positioned && plan_params_ptr != NULL) {
        const float* pp = plan_params_ptr + i * 6;
        ps[23] = pp[0];  /* target_bars */
        ps[24] = pp[1];  /* profit_target */
        ps[25] = pp[2];  /* stop_loss */
        ps[26] = pp[3];  /* scale_aggression */
        ps[27] = pp[4];  /* conviction */
        ps[28] = pp[5];  /* asymmetry */
        /* Apply conviction to position size */
        position *= fmaxf(ps[27], 0.1f);
    }
```

- [ ] **Step 3: Add plan enforcement logic**

After plan activation, add plan execution checks:

```cuda
    /* ── Plan enforcement: auto-exit conditions ── */
    int has_plan = (ps[23] > 0.5f);
    if (has_plan && fabsf(position) > 0.001f) {
        float pnl_pct = (raw_close - entry_price) / fmaxf(fabsf(entry_price), 1.0f)
                         * ((position > 0.0f) ? 1.0f : -1.0f);

        /* ISV-modulated thresholds (N16) */
        float stability = (isv_signals_ptr != NULL) ? isv_signals_ptr[11] : 1.0f;
        float eff_profit = ps[24] * ps[28] * (0.5f + 0.5f * stability);
        float eff_stop   = ps[25] * fmaxf(stability, 0.5f);

        int plan_exit = 0;
        if (pnl_pct >= eff_profit) plan_exit = 1;     /* profit target */
        if (pnl_pct <= -eff_stop) plan_exit = 1;      /* stop loss */
        if (hold_time >= ps[23]) plan_exit = 1;        /* time exit */
        if (stability < 0.3f) plan_exit = 1;           /* regime emergency */

        /* G9: Scale schedule — ramp position over first 3 bars */
        if (hold_time < 3.0f && !plan_exit) {
            float scale = ps[26] + (1.0f - ps[26]) * hold_time / 3.0f;
            position *= fminf(scale, 1.0f);
        }

        if (plan_exit) {
            position = 0.0f;
            for (int s = 23; s <= 29; s++) ps[s] = 0.0f;
        }
    }
```

Place this AFTER the existing hold enforcement and BEFORE the reward computation.

- [ ] **Step 4: Pass plan_params_ptr in Rust launch**

In `gpu_experience_collector.rs`, find the env_step launch. Add after `isv_signals_dev_ptr`:
```rust
                    .arg(&self.plan_params_dev_ptr)  // trade plan parameters
```

Add field `plan_params_dev_ptr: u64` to the collector struct, with setter:
```rust
    pub fn set_plan_params_ptr(&mut self, ptr: u64) { self.plan_params_dev_ptr = ptr; }
```

Wire from trainer → fused_ctx → collector in training_loop.rs (same pattern as isv_signals).

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

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 6: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add -A && git commit -m "feat(plan): plan activation + enforcement in env_step

Flat→Positioned copies plan_params to ps[23-29]. Auto-exit on
profit_target (ISV-modulated × asymmetry), stop_loss (ISV-modulated),
target_bars, regime_stability<0.3. Scale schedule via scale_aggression.
Conviction applied to position size (Kelly proxy).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```

---

## Task 4: Direction Lock in Action Select + Counter-Plan Q

Lock direction branch during active plan. Store opposite-direction Q for N14 comparison.

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/experience_kernels.cu`

- [ ] **Step 1: Add direction lock during active plan**

In `experience_action_select`, after the hold enforcement block (which uses `in_hold`), add plan direction lock:

```cuda
    /* Plan direction lock: during active plan, force current direction */
    int has_plan_active = 0;
    if (portfolio_states != NULL) {
        int ps_base = i * PORTFOLIO_STRIDE;
        has_plan_active = (portfolio_states[ps_base + 23] > 0.5f);
        if (has_plan_active) {
            float cur_pos = portfolio_states[ps_base + 0];
            if (cur_pos > 0.001f) dir_idx = 2;       /* Long locked */
            else if (cur_pos < -0.001f) dir_idx = 0;  /* Short locked */
            /* Magnitude: follows scale — let the plan enforcement in env_step handle it */
        }
    }
```

- [ ] **Step 2: Store counter-plan Q at entry**

In `experience_action_select`, when the model selects a direction (before plan lock), compute the opposite direction's Q-value:

```cuda
    /* N14: Store counter-direction Q for plan comparison.
     * Only computed when model is flat (potential entry). */
    if (portfolio_states != NULL) {
        int ps_base_n14 = i * PORTFOLIO_STRIDE;
        float cur_pos_n14 = portfolio_states[ps_base_n14 + 0];
        if (fabsf(cur_pos_n14) < 0.001f) {
            /* Flat — compute opposite direction Q */
            int opp_dir = (dir_idx == 2) ? 0 : 2;  /* Long↔Short */
            float counter_q_val = q_per_action[opp_dir];  /* Q-value of opposite direction */
            portfolio_states[ps_base_n14 + 29] = counter_q_val;  /* store for N14 */
        }
    }
```

Note: `q_per_action` or `q_values` — check what variable name holds the per-action Q-values in the action select kernel. Read the code.

IMPORTANT: writing to `portfolio_states` in the action select kernel requires it to be non-const. Check if it's `const float*` — if so, this needs to move to env_step instead. If const, store the counter-Q in a separate buffer passed as output.

- [ ] **Step 3: Verify compilation**

```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```

- [ ] **Step 4: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add crates/ml/src/cuda_pipeline/experience_kernels.cu && git commit -m "feat(plan): direction lock during active plan + counter-plan Q (N14)

Direction branch locked to current position during active plan.
Prevents direction flips that would violate plan commitment.
Counter-direction Q-value stored at entry for N14 opportunity
cost comparison (exit early if opposite thesis is winning).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```

---

## Task 5: Smoke Test + Compute-Sanitizer

**Files:** None (verification only)

- [ ] **Step 1: Run smoke test**

```bash
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml --lib -- test_generalization_components_smoke --include-ignored --nocapture 2>&1 | tail -20
```

- [ ] **Step 2: Run compute-sanitizer**

```bash
SQLX_OFFLINE=true cargo test -p ml --lib --no-run 2>&1 | grep "Executable"
# Use the binary path:
FOXHUNT_TEST_DATA=test_data/futures-baseline compute-sanitizer --tool memcheck --print-limit 10 <binary> "test_generalization_components_smoke" --test-threads=1 --include-ignored 2>&1 | tail -5
```

**Target: 0 errors.**

- [ ] **Step 3: Full test suite**

```bash
SQLX_OFFLINE=true cargo test -p ml --lib 2>&1 | tail -5
```

**Target: 899+ passed, 0 failed.**

- [ ] **Step 4: Commit**

```bash
cd /home/jgrusewski/Work/foxhunt && git add -A && git commit -m "chore(plan): trade plan head verification — smoke test + compute-sanitizer 0 errors

Plan head complete: 6 learned params per trade, auto-exit enforcement,
direction lock, ISV-modulated thresholds, conviction sizing, pyramiding.
PORTFOLIO_STRIDE 23→30. compute-sanitizer: 0 errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```

---

## Self-Review

**Spec coverage:**
- ✅ Plan head MLP (6 params) — Task 2
- ✅ PORTFOLIO_STRIDE 23→30 — Task 1
- ✅ Plan activation (Flat→Positioned) — Task 3
- ✅ Plan enforcement (profit/stop/time/regime exit) — Task 3
- ✅ ISV-modulated thresholds (N16) — Task 3
- ✅ Scale schedule / pyramiding (G9) — Task 3
- ✅ Conviction sizing (P9) — Task 3
- ✅ Asymmetric R/R (G8) — Task 3
- ✅ Direction lock — Task 4
- ✅ Counter-plan Q (N14) — Task 4
- ✅ Weight tensors — Task 2
- ✅ compute-sanitizer — Task 5

**Not in this plan (future):**
- Plan backward kernel (gradients flow through main C51 loss naturally via Q-value at entry bar)
- Plan replay priority (N15) — requires replay buffer changes
- Learned termination function (G10) — future enhancement on top of hard thresholds

**Placeholder scan:** No TBD/TODO found. All code blocks complete.
