3 fixes: (1) Delete GpuActionSelector + branching_action_select (zero callers, hardcoded 5 actions vs production 9), (2) Add hold enforcement to backtest env_step kernel (same Layer 2 as training), (3) Dynamic trade insufficiency threshold = max(window_bars / (min_hold * 20), 5). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
199 lines
7.0 KiB
Markdown
199 lines
7.0 KiB
Markdown
# 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)
|
|
```
|