A+B approach: smooth penalties + position limits + CUDA cleanup + search space reduction (45D→25D) + TPE optimizer replacing PSO. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6.4 KiB
DQN Hyperopt Improvements Design
Problem Statement
The current DQN hyperparameter optimization has three critical failures:
-
"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.
-
Catastrophic leverage (29% of trials): 4/14 trials produce >100% max drawdown.
max_position_absoluterange [4.0, 8.0] allows 8x leverage on $100K. No drawdown circuit breaker during backtest evaluation. -
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_internalin 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:
- Surrogate model: Maintain two kernel density estimates (KDEs) — one for parameters from "good" trials (top γ=25%), one for "bad" trials
- Acquisition function: Expected Improvement (EI) = l(x)/g(x) where l(x) is the good KDE and g(x) is the bad KDE
- Sampling: Draw candidates from l(x), score by EI, pick best
- 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)