Major Changes: - Migrated from 3-action TradingAction to 45-action FactoredAction - 45 actions: 5 exposure × 3 order types × 3 urgency levels - Absolute exposure model (target positions -1.0 to +1.0) - Transaction cost differentiation (Market 0.15%, LimitMaker 0.05%, IoC 0.10%) - Fixed action diversity threshold (1.11% → 0.5% for 45-action space) Bug Fixes: - Bug #15: Incomplete FactoredAction integration (code existed but unused) - Bug #16: Runtime crash in action diversity checking (hardcoded 3-action match) Code Changes (13 files, ~464 lines): - ml/src/dqn/action_space.rs: Core FactoredAction + 4 helper methods - ml/src/trainers/dqn.rs: Action diversity refactored (3→45 dynamic) - ml/src/dqn/reward.rs: calculate_reward() signature updated - ml/src/dqn/portfolio_tracker.rs: execute_action() absolute exposure - ml/src/dqn/dqn.rs: WorkingDQN action selection migrated - ml/tests/*.rs: 9 test files updated with FactoredAction assertions Test Results: - 1-epoch smoke test: 100% action diversity (45/45 actions, 80.2s) - 10-epoch production: 87.8% readiness (79/90 scorecard, 14.0 min) - Loss convergence: 96.9% reduction (119K → 3.6K) - Action diversity: 100% → 44% (healthy specialization) - Checkpoint reliability: 12/12 files saved (100%) - DQN tests: 195/195 passing (100%) - ML baseline: 1,514/1,515 passing (99.93%) Production Status: ✅ CERTIFIED (87.8% readiness) Go/No-Go: ✅ GO FOR 100-EPOCH PRODUCTION TRAINING 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
284 lines
14 KiB
Markdown
284 lines
14 KiB
Markdown
# DQN Factored Actions - Debug Flowchart
|
||
|
||
## Current Broken Flow (Only 3 Actions Used)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ cargo build --release │
|
||
│ (NO --features factored-actions flag) │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Compilation: #[cfg(not(feature = "factored-actions"))] │
|
||
│ ▶ NUM_ACTIONS = 3 (line 232) │
|
||
│ ▶ num_actions: 3 in WorkingDQNConfig (line 619) │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ DQNTrainer::new() initialization (line 570) │
|
||
│ ▶ Creates WorkingDQNConfig with num_actions=3 │
|
||
│ ▶ factored_network = None (line 728) │
|
||
│ ▶ use_factored_actions = false (line 731) │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ WorkingDQN::new(config) - main/target networks │
|
||
│ ▶ Q-network output layer: num_actions=3 │
|
||
│ ▶ Sequential network: ... → 64 → [3 Q-values] │
|
||
│ ▶ Target network: ... → 64 → [3 Q-values] │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Training Loop - Action Selection │
|
||
│ │
|
||
│ State Vector (128 dims) │
|
||
│ ▼ │
|
||
│ Q-network forward() │
|
||
│ ▼ │
|
||
│ [Q_BUY, Q_SELL, Q_HOLD] ◄── Only 3 Q-values! │
|
||
│ ▼ │
|
||
│ argmax() → action_idx ∈ {0, 1, 2} │
|
||
│ ▼ │
|
||
│ TradingAction::from_int(action_idx) │
|
||
│ ├─ 0 → Buy │
|
||
│ ├─ 1 → Sell │
|
||
│ └─ 2 → Hold │
|
||
│ │
|
||
│ RESULT: Only 3 actions selectable! ❌ │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
Training logs show 3 actions only
|
||
(BUY%, SELL%, HOLD%) ✗
|
||
```
|
||
|
||
---
|
||
|
||
## Expected Correct Flow (With All 45 Actions)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ cargo build --release --features factored-actions │
|
||
│ (WITH feature flag) │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Compilation: #[cfg(feature = "factored-actions")] │
|
||
│ ▶ NUM_ACTIONS = 45 (line 230) │
|
||
│ ▶ num_actions: 45 in WorkingDQNConfig (line 617) │
|
||
│ ▶ Import FactoredQNetwork (line 28) │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ CLI: train_dqn --use-factored-actions (NEW FLAG) │
|
||
│ ▶ Flag parsed and passed to DQNTrainer │
|
||
│ ▶ use_factored_actions = true in trainer │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ DQNTrainer::new() initialization (FIXED) │
|
||
│ ▶ Creates WorkingDQNConfig with num_actions=45 │
|
||
│ ▶ Creates FactoredQNetwork (128 → 64 → 5,3,3 heads) │
|
||
│ ▶ factored_network = Some(FactoredQNetwork) │
|
||
│ ▶ use_factored_actions = true │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ FactoredQNetwork initialization │
|
||
│ ▶ shared_encoder: 128 → 64 │
|
||
│ ▶ exposure_head: 64 → 5 (Short100, Short50, Flat, ... │
|
||
│ ▶ order_head: 64 → 3 (Market, LimitMaker, IoC) │
|
||
│ ▶ urgency_head: 64 → 3 (Patient, Normal, Aggressive) │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Training Loop - Factored Action Selection (FIXED) │
|
||
│ │
|
||
│ State Vector (128 dims) │
|
||
│ ▼ │
|
||
│ FactoredQNetwork.forward() │
|
||
│ ▼ │
|
||
│ Q_exposure: [5 values] exp_idx ∈ {0,1,2,3,4} │
|
||
│ Q_order: [3 values] → ord_idx ∈ {0,1,2} │
|
||
│ Q_urgency: [3 values] urg_idx ∈ {0,1,2} │
|
||
│ ▼ │
|
||
│ compute_joint_q() broadcasts + sums │
|
||
│ → [batch, 5, 3, 3] → flatten → [batch, 45] │
|
||
│ ▼ │
|
||
│ argmax() → action_idx ∈ {0, 1, 2, ..., 44} ✅ │
|
||
│ ▼ │
|
||
│ FactoredAction::from_index(action_idx) │
|
||
│ → (exposure, order, urgency) tuple │
|
||
│ → All 45 combinations available! │
|
||
│ │
|
||
│ RESULT: All 45 actions selectable! ✅ │
|
||
└─────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
Training logs show 45 actions distributed
|
||
(Top 10 actions with proper factorization) ✓
|
||
```
|
||
|
||
---
|
||
|
||
## The Critical Gap: FactoredQNetwork Creation
|
||
|
||
```
|
||
┌─ CODE AT LINE 728 ─┐
|
||
│ #[cfg(feature = "factored-actions")]
|
||
│ factored_network: None, ◄── ALWAYS NONE!
|
||
│
|
||
│ #[cfg(feature = "factored-actions")]
|
||
│ use_factored_actions: false, ◄── ALWAYS FALSE!
|
||
└────────────────────┘
|
||
|
||
The infrastructure exists but is never activated!
|
||
|
||
WHAT SHOULD HAPPEN:
|
||
┌─────────────────────────────────────────┐
|
||
│ if cli_flag_use_factored_actions { │
|
||
│ let net = FactoredQNetwork::new( │
|
||
│ 128, // state_dim │
|
||
│ &device // GPU/CPU │
|
||
│ )?; │
|
||
│ self.factored_network = Some(net); │
|
||
│ self.use_factored_actions = true; │
|
||
│ } │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Action Selection Code Path Analysis
|
||
|
||
### Current (Broken) - Always 3 Actions
|
||
|
||
```rust
|
||
// In WorkingDQN.select_action()
|
||
let state_tensor = Tensor::from_vec(..., (1, 128), device)?;
|
||
|
||
// Main network forward
|
||
let q_values = self.q_network.forward(&state_tensor)?;
|
||
// q_values shape: [1, 3] ◄── ONLY 3!
|
||
|
||
// Epsilon-greedy
|
||
if rng.gen::<f64>() < epsilon {
|
||
// Random action
|
||
action_idx = rng.gen_range(0..3); // 0, 1, or 2
|
||
} else {
|
||
// Greedy action
|
||
action_idx = q_values.argmax(1)?; // Returns 0, 1, or 2
|
||
}
|
||
|
||
// Convert to enum
|
||
let action = TradingAction::from_int(action_idx as u8)?;
|
||
// Only 3 variants: Buy(0), Sell(1), Hold(2)
|
||
```
|
||
|
||
### Expected (Fixed) - 45 Actions
|
||
|
||
```rust
|
||
// In DQNTrainer with --use-factored-actions flag
|
||
if self.use_factored_actions {
|
||
// Use FactoredQNetwork
|
||
let state_tensor = Tensor::from_vec(..., (1, 128), device)?;
|
||
|
||
let action = self.factored_network
|
||
.as_ref()
|
||
.unwrap()
|
||
.select_epsilon_greedy(&state_tensor, epsilon)?;
|
||
// ▶ Inside select_epsilon_greedy():
|
||
// - Generate 3 random indices: exp (0-4), ord (0-2), urg (0-2)
|
||
// - Combine: idx = exp*9 + ord*3 + urg
|
||
// - Result: 0-44 (all 45 combinations)
|
||
|
||
// Use action: FactoredAction { exposure, order, urgency }
|
||
} else {
|
||
// Use standard 3-action selection (current)
|
||
let action = standard_dqn_select_action();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Why Only 3 Actions Shows Up in Logs
|
||
|
||
```
|
||
Config Initialization (Trainer):
|
||
├─ Compile without --features factored-actions
|
||
│ └─ num_actions = 3
|
||
│
|
||
└─ Create Q-network with output_dim = 3
|
||
└─ Sequential { ... Linear(64 → 3) }
|
||
└─ Forward returns [1, 3] tensor
|
||
└─ argmax on 3 values returns 0, 1, or 2
|
||
└─ Only 3 actions selectable!
|
||
└─ Training logs show BUY%, SELL%, HOLD%
|
||
```
|
||
|
||
---
|
||
|
||
## Compile-Time Feature Flag Impact
|
||
|
||
### Without `--features factored-actions`
|
||
|
||
```rust
|
||
// ml/src/trainers/dqn.rs line 228-232
|
||
#[cfg(feature = "factored-actions")]
|
||
const NUM_ACTIONS: usize = 45;
|
||
#[cfg(not(feature = "factored-actions"))] ◄── THIS BRANCH TAKEN
|
||
const NUM_ACTIONS: usize = 3;
|
||
|
||
// ml/src/trainers/dqn.rs line 615-619
|
||
#[cfg(feature = "factored-actions")]
|
||
num_actions: 45,
|
||
#[cfg(not(feature = "factored-actions"))] ◄── THIS BRANCH TAKEN
|
||
num_actions: 3,
|
||
```
|
||
|
||
### With `--features factored-actions`
|
||
|
||
```rust
|
||
// ml/src/trainers/dqn.rs line 228-232
|
||
#[cfg(feature = "factored-actions")] ◄── THIS BRANCH TAKEN
|
||
const NUM_ACTIONS: usize = 45;
|
||
#[cfg(not(feature = "factored-actions"))]
|
||
const NUM_ACTIONS: usize = 3;
|
||
|
||
// ml/src/trainers/dqn.rs line 615-619
|
||
#[cfg(feature = "factored-actions")] ◄── THIS BRANCH TAKEN
|
||
num_actions: 45,
|
||
#[cfg(not(feature = "factored-actions"))]
|
||
num_actions: 3,
|
||
|
||
// BUT STILL BROKEN BECAUSE:
|
||
#[cfg(feature = "factored-actions")]
|
||
factored_network: None, ◄── STILL NEVER CREATED!
|
||
use_factored_actions: false, ◄── STILL ALWAYS FALSE!
|
||
```
|
||
|
||
---
|
||
|
||
## Summary: Why Only 3 Out of 45
|
||
|
||
| Component | Status | Issue |
|
||
|-----------|--------|-------|
|
||
| **Action Space Definition** | ✅ Correct | All 45 combinations defined (exposure × order × urgency) |
|
||
| **FactoredQNetwork** | ✅ Correct | 5-head architecture properly implemented |
|
||
| **Compile-Time Feature Flag** | ⚠️ Works but depends on `--features` | `NUM_ACTIONS` set correctly IF flag enabled |
|
||
| **Trainer Initialization** | ❌ BROKEN | Sets `num_actions` to 3 by default, never enables factored network |
|
||
| **Action Selection Logic** | ❌ BROKEN | Still uses TradingAction (3 variants) instead of FactoredAction |
|
||
| **CLI Flag** | ❌ MISSING | `--use-factored-actions` doesn't exist, can't enable at runtime |
|
||
| **Training Monitoring** | ❌ BROKEN | Assumes 3 actions, won't track 45 properly |
|
||
|
||
**Result**: Only 3 actions selectable, regardless of feature flag compilation.
|