# Hive Findings Fixes — All Issues Resolution 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:** Fix ALL findings from the 5-agent hive analysis: 2 critical, 4 high, 6 medium issues. Unify the portfolio simulation model between training and backtest kernels.

**Architecture:** Quick wins first (1-line fixes), then the deep refactor (unified cash/MtM model in trade_physics.cuh). Each task is independent except Task 7 (unification) which depends on Tasks 1-6.

**Tech Stack:** CUDA C (.cu/.cuh), Rust (cudarc), build.rs precompilation

---

## File Structure

| File | Responsibility |
|------|---------------|
| `trade_physics.cuh` | Shared trade physics — add unified trade execution function |
| `experience_kernels.cu` | Training env_step — call shared trade execution |
| `backtest_env_kernel.cu` | Backtest env_step — call shared trade execution |
| `expected_q_kernel.cu` | Fix dueling centering |
| `backtest_metrics_kernel.cu` | Fix Calmar mean, CVaR off-by-one |
| `dqn.rs` (hyperopt adapter) | Fix CVaR cap, dd_threshold, entropy logging, capital floor penalty |
| `financials.rs` | Fix Sortino denominator |

---

### Task 1: Quick wins — 1-line fixes in dqn.rs

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

- [ ] **Step 1: Reduce CVaR penalty cap from 3.0 to 1.0**

Find `* 100.0).min(3.0)` in `extract_objective` and change to `.min(1.0)`.

This prevents CVaR from dominating the composite score (composite max = 0.60, CVaR was capped at 3.0 = 5x the composite).

- [ ] **Step 2: Fix dd_threshold default mismatch**

In `from_continuous()`, find `dd_threshold: 0.01` and change to `dd_threshold: 0.02` to match `Default::default()`.

- [ ] **Step 3: Fix action entropy logging — use 9 actions not 3**

In `extract_objective`, find `(3.0_f64).log2()` and change to `(9.0_f64).log2()`. Also update the action_distribution to use the 9 exposure actions from backtest metrics when available, not the 3-bucket BUY/SELL/HOLD approximation.

- [ ] **Step 4: Compile check**

Run: `SQLX_OFFLINE=true cargo check -p ml`

---

### Task 2: Capital floor penalty — training kernel

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

- [ ] **Step 1: Increase capital floor penalty from -1.0 to -10.0**

Find `reward = -1.0f;` in the capital floor block (near `check_capital_floor`) and change to `reward = -10.0f;`.

This matches the tanh reward range [-10, +10] and gives C51 a proper distributional signal for catastrophic loss.

- [ ] **Step 2: Add pre-trade capital floor check**

Before the action decode block (after reading portfolio state), add:

```c
/* Pre-trade capital floor: don't execute trades on a blown account.
 * Matches backtest_env_kernel which checks floor at both pre-trade and post-trade. */
if (check_capital_floor(ps[2], peak_equity)) {
    position = 0.0f;
    cash = ps[2];
    reward = -10.0f;
    out_rewards[out_off] = reward;
    out_dones[out_off] = 1.0f;
    /* Write flat portfolio state */
    ps[0] = 0.0f;   /* position */
    ps[1] = cash;
    ps[2] = cash;    /* portfolio_value */
    current_timesteps[i] = 0;  /* reset episode */
    return;
}
```

- [ ] **Step 3: Add tx cost to capital floor liquidation**

In the post-trade capital floor block, before `position = 0.0f`, add:

```c
float exit_cost = compute_tx_cost(position, raw_close, tx_cost_multiplier, 0.0f,
                                  max_position, 0, -1.0f);
cash -= exit_cost;
```

Do the same in `backtest_env_kernel.cu`'s two capital floor blocks.

- [ ] **Step 4: Compile check**

Run: `SQLX_OFFLINE=true cargo check -p ml`

---

### Task 3: Fix expected_q dueling centering

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

- [ ] **Step 1: Replace scalar mean with per-atom mean**

Find the block (around lines 41-49):
```c
float mean_adv_sum = 0.0f;
for (int aa = 0; aa < bd; aa++) {
    for (int j = 0; j < num_atoms; j++) {
        mean_adv_sum += adv_aa[j];
    }
}
float mean_adv_per_atom = mean_adv_sum / (float)(bd * num_atoms);
```

Replace the inner combined logit computation with per-atom centering:
```c
/* Per-atom advantage centering: mean_a(A[a,j]) for each atom j independently.
 * Matches the dueling formulation in c51_loss_kernel.cu and mse_loss_kernel.cu. */
for (int j = 0; j < num_atoms; j++) {
    float a_mean = 0.0f;
    for (int aa = 0; aa < bd; aa++) {
        const float* adv_aa = b_logits + (long long)i * total_actions * num_atoms
                             + (long long)(adv_offset + aa) * num_atoms;
        a_mean += adv_aa[j];
    }
    a_mean /= (float)bd;
    float combined = val[j] + adv[j] - a_mean;
    /* ... rest of log_softmax computation uses combined per-atom ... */
}
```

The key change: move the mean computation INSIDE the atom loop so each atom j gets its own mean across actions, not a single scalar mean across all atoms and actions.

- [ ] **Step 2: Rebuild cubins and test**

Run: `cargo clean -p ml --profile release-test && FOXHUNT_TEST_DATA=test_data/futures-baseline SQLX_OFFLINE=true cargo test -p ml --lib --profile release-test -- smoke_tests::training_stability::test_gpu_collector_auto_initializes --ignored --nocapture`

---

### Task 4: Fix financial metrics

**Files:**
- Modify: `crates/ml/src/trainers/dqn/financials.rs`
- Modify: `crates/ml/src/cuda_pipeline/backtest_metrics_kernel.cu`

- [ ] **Step 1: Fix Sortino denominator in financials.rs**

Find the Sortino computation and change the denominator from `downside.len()` to `n` (total number of returns), matching the kernel and textbook definition (Sortino & Price 1994).

- [ ] **Step 2: Fix Calmar to use full-window mean**

In `backtest_metrics_kernel.cu`, the Calmar computation (around line 393-397) re-sums returns from the sorted array. Replace with the already-computed `mean` from the parallel reduction:

```c
float calmar = (max_dd > 0.001f)
    ? (mean * annualization_factor * annualization_factor) / max_dd
    : 0.0f;
calmar = fmaxf(-100.0f, fminf(100.0f, calmar));
```

This eliminates sampling error for windows > 4096 bars.

- [ ] **Step 3: Fix CVaR off-by-one**

Change `int cvar_count = (var_idx > 0) ? var_idx : 1;` to `int cvar_count = var_idx + 1;` to include the VaR observation itself in the Expected Shortfall average.

- [ ] **Step 4: Compile check**

---

### Task 5: Fix trailing stop divergence

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/trade_physics.cuh`
- Modify: `crates/ml/src/cuda_pipeline/backtest_env_kernel.cu`

- [ ] **Step 1: Add vol/trend scaling params to check_trailing_stop**

Update the shared function signature to accept `vol_scale` and `trend_scale`:

```c
__device__ __forceinline__ int check_trailing_stop(
    float hold_time, int min_hold_bars,
    float peak_equity, float prev_equity,
    float current_trade_return,
    float base_trail_distance,
    float vol_scale,       /* 1.0 = no scaling, >1 = widen in volatile markets */
    float trend_scale      /* 1.0 = no scaling, >1 = widen in trending markets */
) {
    if (hold_time < (float)min_hold_bars || peak_equity <= 1.0f) return 0;
    float trail_distance = base_trail_distance * vol_scale * trend_scale;
    float peak_return = (peak_equity - prev_equity) / fmaxf(prev_equity, 1.0f);
    if (peak_return > trail_distance) {
        if (current_trade_return < peak_return - trail_distance) return 1;
    }
    return 0;
}
```

- [ ] **Step 2: Update backtest kernel to pass vol/trend = 1.0**

The backtest doesn't have ADX/CUSUM features, so pass `1.0f, 1.0f` for default scaling.

- [ ] **Step 3: Update training kernel to call shared function**

Replace the inline trailing stop (lines ~755-770) with `check_trailing_stop()`, passing the computed `vol_scale` and `trend_scale` from ADX/CUSUM features.

- [ ] **Step 4: Compile check**

---

### Task 6: Stale docstring cleanup

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

- [ ] **Step 1: Update reward v5 → v6 references**

Find all comments referencing "reward v5" and update to "reward v6: Sparse Trade-Completion Only".

- [ ] **Step 2: Fix metrics kernel sqrt(252) comment**

Change "sqrt(252) for daily" to "sqrt(bars_per_day * 252), e.g. sqrt(98280) for 1-min bars".

---

### Task 7: Unify portfolio simulation model (CRITICAL)

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/trade_physics.cuh`
- Modify: `crates/ml/src/cuda_pipeline/backtest_env_kernel.cu`

This is the deepest fix. The training kernel uses a notional cash model and marks to `raw_next`. The backtest uses entry-price-reset and marks to `close`. They must match.

**Design decision:** Adopt the training kernel's model (notional cash + mark-to-next) because:
1. It's more realistic (cash reflects actual trade cost, not entry-price-reset)
2. It handles partial fills correctly (L50→L100 doesn't reset entry_price)
3. MtM to next bar is standard for futures (daily settlement)

The backtest kernel needs access to the NEXT bar's close price for MtM. Currently it only has the current bar's OHLC. The evaluator's step loop processes bars sequentially, so we can pass `next_close` as an additional price.

- [ ] **Step 1: Add `execute_trade` to trade_physics.cuh**

```c
/* Unified trade execution — single source of truth for both kernels.
 * Handles: delta computation, tx cost, cash update, position update.
 * Returns: tx_cost charged (for logging). */
__device__ __forceinline__ float execute_trade(
    float* position,      /* in/out: current position */
    float* cash,          /* in/out: cash balance */
    float target_position,
    float close_price,
    float tx_cost_bps,
    float spread_cost,
    float max_position,
    int   order_type_idx,
    float spread_scale_override
) {
    float delta = target_position - *position;
    if (fabsf(delta) <= 0.001f || close_price <= 0.0f) return 0.0f;
    float cost = compute_tx_cost(delta, close_price, tx_cost_bps, spread_cost,
                                 max_position, order_type_idx, spread_scale_override);
    *cash -= delta * close_price;  /* notional: buy costs cash, sell adds */
    *cash -= cost;
    *position = target_position;
    return cost;
}

/* Mark-to-market: compute portfolio value from cash + unrealized.
 * Uses next_close for consistent forward-looking MtM (futures settlement). */
__device__ __forceinline__ float mark_to_market(
    float cash, float position, float next_close
) {
    return cash + position * next_close;
}
```

- [ ] **Step 2: Refactor backtest_env_kernel.cu to use unified model**

Replace the entry-price-reset trade execution with `execute_trade()`. Remove `entry_price` from the portfolio state — it's no longer needed (notional model tracks P&L through cash directly).

Add `next_close` access: the evaluator's step loop at step `t` can pass `prices[(w * max_len + t + 1) * 4 + 3]` as the next close. For the last bar, use current close.

Replace the mark-to-market with `mark_to_market(cash, position, next_close)`.

The portfolio state layout changes:
```
[0] value        → mark_to_market(cash, position, next_close)
[1] position     → unchanged
[2] cash         → notional cash (no entry_price needed)
[3] entry_price  → REMOVED (repurpose for peak_trade_return for trailing stop)
[4] max_equity   → unchanged
[5] hold_time    → unchanged
[6] cum_return   → unchanged
[7] step_count   → unchanged
```

- [ ] **Step 3: Update backtest_env_kernel signature**

Add `next_prices` buffer or pass `max_len` so the kernel can read the next bar's close:

```c
float next_close = (current_step + 1 < wlen)
    ? prices[(w * max_len + current_step + 1) * 4 + 3]
    : close;  /* last bar: use current close */
```

- [ ] **Step 4: Update GpuBacktestEvaluator launch site**

No new buffers needed — the kernel already has access to all prices. The `next_close` read is within the existing `prices_buf` bounds.

- [ ] **Step 5: Update backtest_metrics_kernel.cu if needed**

The metrics kernel reads `step_returns` which are computed by env_step. With the unified model, step_ret = `(new_value - old_value) / old_value` where new_value uses `mark_to_market(cash, position, next_close)`. This is now consistent with training.

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

```bash
# Unit tests
SQLX_OFFLINE=true cargo test -p ml --lib -- hyperopt::adapters::dqn::tests --nocapture

# Smoke test
FOXHUNT_TEST_DATA=test_data/futures-baseline SQLX_OFFLINE=true cargo test -p ml --lib --profile release-test -- smoke_tests::training_stability::test_gpu_collector_auto_initializes --ignored --nocapture

# Hyperopt integration
FOXHUNT_TEST_DATA=test_data/futures-baseline SQLX_OFFLINE=true cargo test -p ml --lib --profile release-test -- hyperopt::campaign::tests::test_local_hyperopt --ignored --nocapture
```

---

## Validation

Success criteria:
- All unit tests pass (25+)
- Smoke test: Sharpe > 0, trades > 0
- Hyperopt: 2 trials complete, max_dd < 30%, finite metrics
- No SIGSEGV
- Training and backtest produce consistent P&L for the same actions on the same data
