# Evaluation Cleanup — 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:** Remove dead 5-action `branching_action_select` path, add hold enforcement to backtest evaluator, make trade penalty threshold dynamic.

**Architecture:** Delete `gpu_action_selector.rs` + `epsilon_greedy_kernel.cu` (zero callers). Add `min_hold_bars` override to `backtest_env_step` kernel. Dynamic trade penalty threshold = `max(window_bars / (min_hold_bars * 20), 5)`.

**Tech Stack:** CUDA kernel (C), Rust, cudarc 0.19.3.

---

## Task 1: Remove dead code — GpuActionSelector + branching_action_select

**Files:**
- Delete: `crates/ml/src/cuda_pipeline/gpu_action_selector.rs`
- Delete: `crates/ml/src/cuda_pipeline/epsilon_greedy_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/mod.rs` — remove module declaration
- Modify: `crates/ml/src/trainers/dqn/trainer/action.rs` — remove `select_actions_batch()`
- Modify: `crates/ml/src/trainers/dqn/trainer/mod.rs` — remove `GpuActionSelector` field + imports

- [ ] **Step 1: Remove module declaration from mod.rs**

In `crates/ml/src/cuda_pipeline/mod.rs`, find and remove:
```rust
pub mod gpu_action_selector;
```

- [ ] **Step 2: Remove GpuActionSelector from DQNTrainer**

In `trainer/mod.rs`, remove the `GpuActionSelector` field and any `use` imports referencing it. Search for `gpu_action_selector` and `GpuActionSelector`.

- [ ] **Step 3: Remove `select_actions_batch()` from action.rs**

In `trainer/action.rs`, remove the entire `select_actions_batch` method and any helper functions only used by it. If the file becomes empty/trivial, consider removing it.

- [ ] **Step 4: Delete the files**

```bash
rm crates/ml/src/cuda_pipeline/gpu_action_selector.rs
rm crates/ml/src/cuda_pipeline/epsilon_greedy_kernel.cu
```

- [ ] **Step 5: Fix any remaining references**

```bash
grep -rn "gpu_action_selector\|GpuActionSelector\|branching_action_select\|epsilon_greedy_kernel\|select_actions_batch" crates/ml/src/ --include="*.rs"
```

Remove any remaining references (comments, imports, doc references).

- [ ] **Step 6: Build + test**

Run: `SQLX_OFFLINE=true cargo check -p ml --lib`
Run: `SQLX_OFFLINE=true cargo test -p ml --lib`
Expected: compiles, all tests pass (some tests that used GpuActionSelector may need removing)

- [ ] **Step 7: Commit**

```bash
git commit -m "refactor: remove dead GpuActionSelector + branching_action_select (5-action)

Zero callers in training or evaluation. The backtest evaluator uses
evaluate_dqn_graphed() with cuBLAS + experience_action_select (9-action).
Removes train/eval action space mismatch (was 5 vs 9 actions)."
```

---

## Task 2: Add hold enforcement to backtest env_step kernel

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/backtest_env_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs`

- [ ] **Step 1: Add `min_hold_bars` parameter to `backtest_env_step` kernel**

In `backtest_env_kernel.cu`, add `int min_hold_bars` as the last parameter to the kernel signature.

- [ ] **Step 2: Add hold enforcement logic**

After the position update and sign computation, add the same Layer 2 hold enforcement as the training kernel:

```cuda
// Hold enforcement: block exits and reversals during minimum hold period
int wants_exit = (prev_sign != 0 && curr_sign == 0);
int wants_reversal = (prev_sign != 0 && curr_sign != 0 && prev_sign != curr_sign);
if (hold_time > 0.0f && hold_time < (float)min_hold_bars
    && (wants_exit || wants_reversal)
    && step < window_len - 1) {
    // Override: keep previous position
    position = prev_position;
    // Don't update portfolio for this bar
}
```

Adapt the variable names to match the backtest kernel's conventions (it may use different names than the training kernel).

- [ ] **Step 3: Pass `min_hold_bars` from Rust**

In `gpu_backtest_evaluator.rs`, add `min_hold_bars: i32` to `GpuBacktestConfig`. Pass it to the kernel launch. Default: 5.

Wire from the hyperopt adapter: when creating the evaluator config, pass `hyperparams.min_hold_bars`.

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

Run: `SQLX_OFFLINE=true cargo check -p ml --lib`
Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- backtest`
Expected: compiles, backtest tests pass

- [ ] **Step 5: Commit**

```bash
git commit -m "feat: hold enforcement in backtest_env_step — fixes train/eval mismatch

Backtest evaluator now enforces min_hold_bars during evaluation,
matching the training kernel's Layer 2 hold guard. The model is
evaluated under the same rules it learned under."
```

---

## Task 3: Dynamic trade insufficiency threshold

**Files:**
- Modify: `crates/ml/src/hyperopt/adapters/dqn.rs`

- [ ] **Step 1: Find `calculate_trade_insufficiency_penalty` usage**

Search for the function call in `extract_objective()`. Currently:
```rust
let trade_penalty = calculate_trade_insufficiency_penalty(backtest.total_trades);
```

- [ ] **Step 2: Make threshold dynamic**

Replace with:
```rust
// Dynamic threshold: adapts to window size and min_hold_bars
// With min_hold=5 and 10K window: max_trades=2000, threshold=max(100, 5)=100
// With min_hold=20 and 10K window: max_trades=500, threshold=max(25, 5)=25
let backtest_window_bars = 10_000_usize; // MAX_WINDOW_BARS from evaluator
let min_hold = params.min_hold_bars.max(1);
let max_possible_trades = backtest_window_bars / min_hold;
let dynamic_threshold = (max_possible_trades / 20).max(5);
let trade_penalty = calculate_trade_insufficiency_penalty_dynamic(
    backtest.total_trades,
    dynamic_threshold,
);
```

- [ ] **Step 3: Create `calculate_trade_insufficiency_penalty_dynamic()`**

Add a new function (or modify the existing one) that accepts a threshold parameter:

```rust
fn calculate_trade_insufficiency_penalty_dynamic(total_trades: usize, min_threshold: usize) -> f64 {
    if total_trades < min_threshold {
        // Smooth ramp from 10.0 (zero trades) to 0.0 (at threshold)
        let ratio = total_trades as f64 / min_threshold as f64;
        10.0 * (1.0 - ratio)
    } else {
        0.0
    }
}
```

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

Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- objective`
Expected: objective tests pass

- [ ] **Step 5: Commit**

```bash
git commit -m "feat: dynamic trade insufficiency threshold — adapts to min_hold_bars

Threshold = max(window_bars / (min_hold * 20), 5). With min_hold=5 and
10K window, model needs 100+ trades to avoid penalty (was fixed at 10).
Prevents degenerate objective when hold enforcement reduces trade count."
```

---

## Task 4: Integration test

- [ ] **Step 1:** `SQLX_OFFLINE=true cargo test -p ml --lib` — 887+ pass
- [ ] **Step 2:** `SQLX_OFFLINE=true cargo test -p ml-dqn --lib` — 359 pass
- [ ] **Step 3:** `SQLX_OFFLINE=true cargo check --workspace` — clean
- [ ] **Step 4:** Push to main

---

## Execution Order

```
Task 1 (delete dead code) → Task 2 (backtest hold enforcement) → Task 3 (dynamic threshold) → Task 4 (integration)
```
