Files
foxhunt/docs/plans/2026-03-03-rl-agent-gaps-design.md
jgrusewski 44b38e8014 docs: RL agent gaps design — 7 features for training quality and production readiness
Position-aware state encoding, regime detection (feature-based + HMM),
slippage modeling, curriculum learning, multi-timeframe fusion (default on),
online learning with EWC, and A/B testing with Thompson Sampling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 02:23:38 +01:00

213 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |