# DQN Hyperopt Improvements Design

## Problem Statement

The current DQN hyperparameter optimization has three critical failures:

1. **"Do Nothing" convergence (57% of trials)**: 8/14 trials converge to objective=44.86 (penalty value), producing 0 trades. PSO with 20 particles in 45D space can't explore effectively. Cliff penalties (-10.0 entropy, 1000.0 completion) give zero gradient signal.

2. **Catastrophic leverage (29% of trials)**: 4/14 trials produce >100% max drawdown. `max_position_absolute` range [4.0, 8.0] allows 8x leverage on $100K. No drawdown circuit breaker during backtest evaluation.

3. **Memory growth (844-1435 MB/trial)**: No explicit CUDA cache clearing between trials. `drop()` + 100ms sleep is insufficient for Candle's CUDA allocator.

## Approach: A+B (Surgical Fixes + Bayesian Optimizer)

### Part A: Objective Function & Environment Fixes

#### A1. Smooth Penalties (replace cliffs)

**Current**: Binary cliff penalties destroy PSO gradient signal.
```
entropy < 0.5  →  -10.0  (cliff)
entropy >= 0.5 →    0.0  (cliff)
completion < min_epochs → 1000.0 (dominates everything)
```

**Proposed**: Smooth, gradient-friendly penalties.
```
diversity_penalty = -5.0 * max(0, 1.0 - entropy/1.0)²   # Quadratic, smooth at boundary
completion_penalty = 50.0 * max(0, 1.0 - epochs/min_epochs)  # Linear scale, not 1000x
```

**Files**: `crates/ml/src/hyperopt/adapters/dqn.rs` — `calculate_diversity_penalty()` (line 1840), `calculate_completion_penalty()` (line 2198)

#### A2. Position Limit Enforcement

**Current**: `max_position_absolute` range [4.0, 8.0] with no drawdown guard.

**Proposed**:
- Narrow search range to [1.0, 4.0] (max 4x leverage)
- Add drawdown circuit breaker in backtest: if drawdown > 20%, force-close all positions and halt trading for remainder of episode
- Wire into `execute_action_internal` in portfolio_tracker.rs

**Files**: `crates/ml/src/hyperopt/adapters/dqn.rs` (search bounds line 476), `crates/ml/src/dqn/portfolio_tracker.rs` (execute_action_internal line 232)

#### A3. CUDA Memory Cleanup

**Current**: `drop()` + 100ms sleep. No CUDA synchronize or cache clear.

**Proposed**: After dropping trainer/metrics, call CUDA synchronize via Candle's backend, then force a GC pass. Add memory budget check — if usage exceeds 80% of available, trigger aggressive cleanup.

**Files**: `crates/ml/src/hyperopt/adapters/dqn.rs` (cleanup block line 3076)

#### A4. Search Space Reduction (45D → ~25D)

Fix 20 parameters to validated defaults. Keep the parameters that genuinely affect trading performance in the search space.

**Keep in search (25D)**:
- Base: learning_rate, batch_size, gamma, buffer_size, hold_penalty_weight, max_position_absolute, huber_delta, entropy_coefficient, transaction_cost_multiplier (9)
- PER: per_alpha, per_beta_start (2)
- Rainbow: v_min, v_max, noisy_sigma_init, dueling_hidden_dim, n_steps, num_atoms (6)
- Risk: kelly_fractional, kelly_max_fraction, volatility_window (3)
- Training: weight_decay, curiosity_weight, tau, hidden_dim_base (4)
- Exploration: noisy_epsilon_floor (1)

**Fix to defaults (20D)**:
- minimum_profit_factor → 1.5 (midpoint)
- kelly_min_trades → 20 (validated)
- ensemble_size → 5, beta_variance → 0.5, beta_disagreement → 0.5, beta_entropy → 0.2, variance_cap → 1.0 (ensemble defaults)
- warmup_ratio → 0.0 (hyperopt trials too short)
- td_error_clamp_max → 10.0, batch_diversity_cooldown → 50.0 (validated)
- lr_decay_type → 0 (constant for short trials)
- sharpe_weight → 0.0 (already in objective)
- gae_lambda → 0.95 (standard)
- noisy_sigma_initial → 0.5, noisy_sigma_final → 0.3 (validated)
- norm_type → 1 (RMSNorm), activation_type → 1 (LeakyReLU)
- num_quantiles → 64, qr_kappa → 1.0 (QR-DQN defaults)
- count_bonus_coefficient → 0.1 (validated)

**Files**: `crates/ml/src/hyperopt/adapters/dqn.rs` — `continuous_bounds()` (line 469), `from_continuous()` (line 549)

### Part B: Replace PSO with Bayesian Optimization (TPE)

#### B1. TPE Optimizer Implementation

Replace `ArgminOptimizer` (PSO) with a Tree-Parzen Estimator:

1. **Surrogate model**: Maintain two kernel density estimates (KDEs) — one for parameters from "good" trials (top γ=25%), one for "bad" trials
2. **Acquisition function**: Expected Improvement (EI) = l(x)/g(x) where l(x) is the good KDE and g(x) is the bad KDE
3. **Sampling**: Draw candidates from l(x), score by EI, pick best
4. **Initial exploration**: Keep LHS (Latin Hypercube Sampling) for first 5 trials

The `egobox` crate (already in the workspace) provides EGO (GP-based BO) as an alternative, but TPE scales better to 25D. We'll implement a lightweight TPE using the `statrs` crate for KDE.

**Architecture**:
```
ArgminOptimizer (PSO, 20 particles, 50 iters/restart)
       ↓ replace with
TpeOptimizer {
    n_initial: 5,          // LHS warmup
    max_trials: 30,        // Same budget
    gamma: 0.25,           // Top 25% = "good"
    n_candidates: 100,     // EI candidates per trial
    bandwidth: "silverman" // KDE bandwidth selection
}
```

**Files**:
- New: `crates/ml/src/hyperopt/tpe.rs` (~300 lines)
- Modify: `crates/ml/src/hyperopt/optimizer.rs` (add TPE variant)
- Modify: `crates/ml/src/hyperopt/mod.rs` (export)
- Modify: `crates/ml/src/hyperopt/campaign.rs` (wire TPE)
- Modify CLI: `bin/fxt/src/commands/train/hyperopt.rs` (--optimizer=tpe flag)

#### B2. Trial History & Warm-Starting

TPE benefits from trial history across runs. Add JSON-based trial persistence:
- Save all (params, objective) pairs to `hyperopt_dir/trial_history.json`
- On restart, load history to seed TPE's KDE
- This makes interrupted runs resume intelligently instead of starting from scratch

**Files**: `crates/ml/src/hyperopt/tpe.rs` (persistence methods)

## Non-Goals

- Changing the reward function or backtest environment (separate concern)
- Multi-GPU support (single L40S is the target)
- Changing the DQN architecture itself
- Modifying the FactoredAction space

## Expected Impact

| Metric | Current (PSO 45D) | Target (TPE 25D + fixes) |
|--------|-------------------|--------------------------|
| "Do nothing" rate | 57% (8/14) | <15% |
| Catastrophic DD rate | 29% (4/14) | <5% |
| Memory per trial | 844-1435 MB | <200 MB delta |
| Best Sharpe found | 1.73 (1 trial) | >2.0 consistently |
| Trials to find signal | ~7 | ~3-5 |

## Testing Strategy

- Unit tests for TPE KDE, EI acquisition, smooth penalties
- Integration test: 5-trial micro-hyperopt on synthetic data
- Regression: ensure existing PSO path still works (--optimizer=pso flag)
