# RL Agent Gaps — Design Document **Date:** 2026-03-03 **Status:** Approved **Approach:** Vertical slices (sequential, dependency-ordered) ## Overview Audit of DQN and PPO agents identified 7 gaps across training quality and production readiness. This design addresses all 7 in dependency order. **Implementation order:** 1. Position-aware state encoding (foundation) 2. Regime detection (feature-based + HMM) 3. Slippage modeling 4. Curriculum learning (uses regime detection) 5. Multi-timeframe fusion (default on) 6. Online learning (needs stable architecture) 7. A/B testing framework (deployment layer) --- ## 1. Position-Aware State Encoding **Problem:** Agent doesn't see its own position — learns constraints by trial-and-error. **Design:** Extend state vector with 4 features (51 → 55 dims): | Feature | Normalization | Range | |---------|--------------|-------| | `current_exposure` | Direct | [-1.0, 1.0] (Short100 to Long100) | | `unrealized_pnl` | Divide by EMA σ (100-bar) | Unbounded, typically [-5, 5] | | `bars_in_position` | log(1 + x) / log(1001) | [0, 1] | | `position_cost_basis` | Relative to current price, in bps | [-500, 500] typical | **Location:** `crates/ml/src/features/position_features.rs` **Impact:** All hidden dim configs, hyperopt bounds, walk-forward feature counts update from 51 → 55. --- ## 2. Regime Detection **Problem:** `detect_regime()` returns hardcoded "normal". RegimeConditionalDQN has no real input. ### Layer 1: Feature-Based Classifier (real-time, <1μs) Input: rolling 100-bar window of OHLCV. | Regime | Conditions | |--------|-----------| | Trending | ADX > 25 AND Hurst > 0.55 | | MeanReverting | Hurst < 0.45 AND vol < median | | Volatile | vol > 2σ above mean OR ADX < 15 | | Normal | Default | Returns existing `RegimeType` enum. ### Layer 2: HMM (offline, periodic refit) - 3-state HMM (Low-Vol / Trending / Crisis) on daily returns - Fitted during walk-forward setup, refit weekly - Provides transition matrix for persistence estimation - Validates feature-based classifier accuracy **Location:** `crates/ml/src/regime_detection/` (replace stub). `feature_classifier.rs` + `hmm.rs`. **Integration:** Wire into `RegimeConditionalDQN` head dispatch. --- ## 3. Slippage Modeling **Problem:** Fixed 15/5/10 bps costs don't reflect volume-dependent market impact. ### `SlippageModel` trait Two implementations: **`LinearImpactModel`** (new default): ``` slippage = spread/2 + sqrt_impact * sqrt(volume / ADV) ``` - `spread`: estimated from OHLCV as `0.5 * (high - low) / close * normalization` - `sqrt_impact`: per-instrument (ES ≈ 0.1, 6E ≈ 0.3 bps/√lot) - `ADV`: 20-day rolling average daily volume - Temporary impact decays: `temp_impact * exp(-t / decay_half_life)` **`FixedCostModel`** (backward compat): - Wraps existing 15/5/10 bps constants **Location:** `crates/ml/src/risk/slippage.rs` **Integration:** Replace fixed `cost_weight` in `dqn/reward.rs` with `slippage_model.estimate_cost(action, state)`. Same for PPO and evaluation engine. --- ## 4. Curriculum Learning **Problem:** 45-action space + all regimes from step 0 slows convergence. ### 3-Phase Schedule | Phase | Exposure Actions | Regimes | Gate | |-------|-----------------|---------|------| | 1: Basic | Flat, Long50, Short50 (27 actions) | Normal only | Sharpe > 0.5 for 3 folds | | 2: Full Position | All 5 exposures (45 actions) | Normal + Trending | Sharpe > 0.3 for 2 folds | | 3: All Regimes | All 45 | All | Final phase | **Mechanism:** - Action masking expanded per phase (reuse `get_valid_action_mask()`) - Regime filtering via Section 2 detection - Phase tracked in checkpoint metadata - Manual override: `--curriculum-phase 3` **Location:** `crates/ml/src/trainers/curriculum.rs` --- ## 5. Multi-Timeframe Feature Fusion (Default On) **Problem:** Single 1-minute resolution. No cross-timeframe context. ### Architecture ``` 1m bars ──→ [Encoder] ──→ emb_1m (64) ─┐ ├─ resample 5m ──→ [Encoder] ──→ emb_5m (64) ─┤ ├─ resample 15m ──→ [Encoder] ──→ emb_15m (64) ─┼─→ [Concat 256] ─→ [Linear 128] ─→ macro_state └─ resample 1h ──→ [Encoder] ──→ emb_1h (64) ─┘ ``` - Resample from 1m OHLCV on-the-fly (no extra data download) - Per-timeframe encoder: shared-weight LSTM - Fusion: concatenate → linear projection → 128-dim macro context - Combined state: position features (55) + macro context (128) = 183 input dims - **Default on** — no feature gate **Location:** `crates/ml/src/features/multi_timeframe.rs` --- ## 6. Online Learning (Per-Trade) **Problem:** Models freeze after walk-forward training. No adaptation to regime shifts. ### Core Mechanism - Rolling experience buffer: 10,000 transitions (configurable) - Mini-update every 100 trades: - DQN: sample mini-batch, TD loss, weight update - PPO: N-step trajectory, advantages, single epoch - **EWC regularization** prevents catastrophic forgetting: - Fisher Information Matrix (diagonal approx) computed after initial training - `online_loss = task_loss + λ * Σ F_i * (θ_i - θ*_i)²` - λ default: 1000.0, Fisher recomputed every 10,000 steps ### Safety Rails | Rail | Config | Default | |------|--------|---------| | Gradient clipping | `max_grad_norm` | 1.0 | | Learning rate | `online_lr_multiplier` | 0.1× base LR | | Checkpoint rollback | Sharpe degrades > 20% over 500 trades | Auto-rollback | | Kill switch | Rolling Sharpe < -1.0 | Freeze + alert | **Location:** `crates/ml/src/trainers/online_learning.rs` **Integration:** Called from `trading_service` after trade execution. --- ## 7. A/B Testing Framework **Problem:** No safe mechanism to deploy and compare new models. ### Architecture Two slots: **Champion** (production) and **Challenger** (candidate). **Traffic allocation via Thompson Sampling:** - Each model: Beta(α, β) distribution on win rate - Sample from both; higher sample gets the trade - Initial: Champion Beta(10, 1), Challenger Beta(1, 1) **Shadow mode:** Challenger runs inference but never executes (100% Champion). ### Metrics per model - Rolling Sharpe (500 trades) - Win rate, mean PnL/trade - Max drawdown - Action diversity (entropy) ### Promotion criteria (all required) 1. Challenger Sharpe > Champion Sharpe (Bayesian P > 0.95) 2. Challenger max DD ≤ 1.2 × Champion max DD 3. Minimum 500 evaluated trades 4. Manual approval via existing `ApproveModel` gRPC **Location:** `crates/ml/src/deployment/ab_testing.rs` **Integration:** `trading_service` ensemble infrastructure + `ApproveModel`/`RejectModel` handlers. --- ## Summary | # | Feature | New Files | Impact | |---|---------|-----------|--------| | 1 | Position-aware state | `features/position_features.rs` | State 51→55 dims | | 2 | Regime detection | `regime_detection/{feature_classifier,hmm}.rs` | Replace stub | | 3 | Slippage model | `risk/slippage.rs` | Reward computation | | 4 | Curriculum learning | `trainers/curriculum.rs` | Trainer loops | | 5 | Multi-timeframe fusion | `features/multi_timeframe.rs` | State 55→183 dims (default on) | | 6 | Online learning | `trainers/online_learning.rs` | Inference hot path | | 7 | A/B testing | `deployment/ab_testing.rs` | Trading service |