2.9 KiB
Hyperopt Degenerate Trial Fix
Problem
7/9 DQN hyperopt trials produce degenerate policies (all-buy or all-hold) that generate 0-1 trades in backtest evaluation. All degenerates receive identical objective=1.25, creating a flat plateau that gives PSO zero gradient signal.
Root cause chain:
- 45-action factored space collapses to 3 legacy actions (Buy/Sell/Hold) at eval time
- EvaluationEngine treats consecutive same-direction actions as no-ops
- Objective function can't distinguish between failure modes (all-buy vs all-hold vs insufficient trades)
Solution: Three-Part Fix
Part 1: Exposure-Aware Evaluation Engine
Add process_bar_factored(&FactoredAction) to EvaluationEngine that tracks continuous position exposure (-1.0 to +1.0):
- Long100→Long50 generates a partial close trade (delta=-0.5)
- Long50→Short100 generates a reversal trade (delta=-1.5)
- Long100→Long100 = no trade (delta=0)
- Transaction cost uses FactoredAction's order type (Market=0.15%, Limit=0.05%, IoC=0.10%)
- Position sizing:
effective_size = target_exposure * kelly_fraction(multiplicative)
File: crates/ml/src/evaluation/engine.rs (~50 lines)
Part 2: Graduated Trade Insufficiency Penalty
Replace flat 1.25 plateau with smooth, differentiated penalties in extract_objective():
min_expected_trades = total_bars / 500 (~448 for 224K bars)
penalty = match total_trades {
0 => 10.0, // Model does nothing
1..10 => 5.0 + 5.0 * (1.0 - trades/10.0), // Near-degenerate
10..min => 2.0 * (1.0 - trades/min_expected), // Insufficient activity
_ => 0.0, // Rely on Sharpe/Sortino
}
Additive on objective (higher=worse for PSO minimization). Each failure mode gets a unique value → PSO gets gradient signal.
File: crates/ml/src/hyperopt/adapters/dqn.rs (~25 lines in extract_objective())
Part 3: Wire Factored Eval into Hyperopt Backtest
Replace legacy action collapse in backtest loop:
// Before: factored → legacy → Action::Buy/Sell/Hold → engine.process_bar()
// After: factored → engine.process_bar_factored()
File: crates/ml/src/hyperopt/adapters/dqn.rs (~3 lines in backtest loop)
Expected Impact
- Degenerate trials now produce different objectives (10.0 vs 5.0 vs 2.0 vs 1.25) → PSO can navigate
- Factored eval generates more trades from partial position changes → richer performance metrics
- Models that learn Long100/Long50 switching get credit instead of being penalized
- Combined: PSO should find viable configs in 5-8 initial trials instead of relying on luck
Files Changed
| File | Change | ~Lines |
|---|---|---|
| evaluation/engine.rs | process_bar_factored() | 50 |
| hyperopt/adapters/dqn.rs | extract_objective() penalty | 25 |
| hyperopt/adapters/dqn.rs | Backtest loop wiring | 3 |
| Tests | Unit tests | 80 |
| Total | ~160 |