Files
foxhunt/ELITE_REWARD_INTEGRATION_STATUS.md
jgrusewski 00ef9e2866 Wave 15: Complete FactoredAction migration to 45-action system
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>
2025-11-11 23:27:02 +01:00

15 KiB

Elite Reward Coordinator Integration - Status Report

Date: 2025-11-08 Agent: Integration Agent (Step 2 - Wiring Coordinator into DQN Trainer) Status: PHASE 1 COMPLETE | PHASES 2-5 READY | BLOCKER: curiosity.rs compilation errors


Executive Summary

PHASE 1 COMPLETE: CLI flag --use-elite-reward successfully added to train_dqn.rs with backward compatibility (default: false).

CRITICAL BLOCKER: Compilation fails due to 2 errors in ml/src/dqn/curiosity.rs (parallel agent's code):

  1. Line 143: Adam does not implement candle_nn::Optimizer trait
  2. Line 199: next_state_embedding moved value used after move

PHASES 2-5 READY: Comprehensive integration plan complete, waiting for curiosity.rs fixes.


Phase 1: CLI Flag Addition (COMPLETE)

Files Modified

/home/jgrusewski/Work/foxhunt/ml/examples/train_dqn.rs

Change 1: Added CLI flag (lines 183-186)

/// Enable elite multi-component reward system (experimental)
/// Default: false (uses legacy RewardFunction for backward compatibility)
#[arg(long, default_value = "false")]
use_elite_reward: bool,

Change 2: Added reward system logging (lines 241-246)

// Log reward system configuration
if opts.use_elite_reward {
    info!("  • Reward system: Elite (multi-component: extrinsic + intrinsic + entropy + curiosity + ensemble)");
} else {
    info!("  • Reward system: Legacy (portfolio tracking + diversity penalty)");
}

Change 3: Added TODO for trainer creation (lines 444-446)

// Create DQN trainer
// TODO: Once reward_coordinator.rs is created, update this to:
// let mut trainer = DQNTrainer::new(hyperparams, opts.use_elite_reward).context("Failed to create DQN trainer")?;
let mut trainer = DQNTrainer::new(hyperparams).context("Failed to create DQN trainer")?;

Validation

  • CLI help: cargo run -p ml --example train_dqn -- --help (SUCCESS - flag visible)
  • Compilation: BLOCKED (curiosity.rs errors)
  • Backward compatibility: PENDING (blocked by compilation)

EliteRewardCoordinator API Analysis

File: /home/jgrusewski/Work/foxhunt/ml/src/dqn/reward_coordinator.rs

Status: Created by parallel agent, API confirmed.

Constructor

pub fn new(device: Device) -> Result<Self, Box<dyn std::error::Error>>

Default Weights:

  • Extrinsic: 0.40 (P&L focus)
  • Intrinsic: 0.25 (action diversity)
  • Entropy: 0.15 (policy diversity)
  • Curiosity: 0.10 (exploration)
  • Ensemble: 0.10 (multi-model consensus)

Reward Calculation

pub fn calculate_total_reward(
    &mut self,
    position: &Position,
    entry_price: f64,
    exit_price: f64,
    action: TradingAction,
    portfolio_value: f64,
    max_drawdown: f64,
    state: &Tensor,
    next_state: &Tensor,
    q_values: &Tensor,
    episode_step: u64,
    ensemble_votes: Vec<usize>,
) -> Result<f64, Box<dyn std::error::Error>>

Episode Reset

pub fn reset_episode(&mut self)

Missing Feature: No get_last_reward_components() method for component logging.

  • Impact: Cannot log individual component values (extrinsic, intrinsic, entropy, curiosity, ensemble)
  • Workaround: Log only total reward, or modify coordinator to track component values
  • Recommendation: Add last_components: [f64; 5] field and getter method

Critical Blockers

Blocker 1: curiosity.rs Compilation Errors

File: ml/src/dqn/curiosity.rs

Error 1: Optimizer Trait (Line 143)

error[E0277]: the trait bound `Adam: candle_nn::Optimizer` is not satisfied
   --> ml/src/dqn/curiosity.rs:143:29
    |
143 |             Optimizer::step(optimizer, &gradients)
    |             --------------- ^^^^^^^^^ the trait `candle_nn::Optimizer` is not implemented for `Adam`

Root Cause: candle_optimisers::Adam (3rd party) vs candle_nn::Optimizer trait mismatch.

Fix: Replace with candle_nn::AdamW or implement trait wrapper.

Error 2: Moved Value (Line 199)

error[E0382]: borrow of moved value: `next_state_embedding`
   --> ml/src/dqn/curiosity.rs:213:55
    |
199 |         let diff = (predicted_next_state - next_state_embedding)
    |                                            -------------------- value moved here
...
213 |         self.forward_model.train_step(state, action, &next_state_embedding.clone())?;
    |                                                       ^^^^^^^^^^^^^^^^^^^^ value borrowed here after move

Root Cause: next_state_embedding consumed in line 199, then borrowed in line 213.

Fix: Clone before line 199:

let next_state_embedding_clone = next_state_embedding.clone();
let diff = (predicted_next_state - next_state_embedding_clone)

Owner: Parallel agent (reward system creator)


Remaining Work (Phases 2-5)

Phase 2: Trainer Field Additions (20 min)

File: ml/src/trainers/dqn.rs

Modifications:

  1. Add import:
use crate::dqn::reward_coordinator::EliteRewardCoordinator;
  1. Update struct (around line 50-70):
pub struct DQNTrainer {
    // ... existing fields
    elite_coordinator: Option<EliteRewardCoordinator>,
    episode_step: usize,
    max_drawdown: f32,
}
  1. Update constructor signature (around line 430):
pub fn new(hyperparams: DQNHyperparameters, use_elite_reward: bool) -> Result<Self>
  1. Initialize fields:
let elite_coordinator = if use_elite_reward {
    Some(EliteRewardCoordinator::new(device.clone())?)
} else {
    None
};

Ok(Self {
    // ... existing fields
    elite_coordinator,
    episode_step: 0,
    max_drawdown: 0.0,
})

Blockers: None (reward_coordinator.rs exists)


Phase 3: Reward Calculation Integration (30 min)

File: ml/src/trainers/dqn.rs

Location 1: Training Loop (around line 789-790):

Current Code:

let reward_decimal = self.reward_fn.calculate_reward(
    action, state, &next_state, &recent_actions_vec
)?;
let reward = reward_decimal.to_string().parse::<f32>().unwrap_or(0.0);

Replacement:

let reward = if let Some(ref mut coordinator) = self.elite_coordinator {
    // Elite reward system
    let q_values_vec = self.get_q_values(state).await?;
    let q_values_tensor = Tensor::new(&q_values_vec[..], &self.device)?
        .reshape(&[1, 3])?; // [batch=1, num_actions=3]

    coordinator.calculate_total_reward(
        &position,
        entry_price,
        exit_price,
        action,
        portfolio_value,
        self.max_drawdown as f64,
        state_tensor,      // TODO: Convert TradingState to Tensor
        next_state_tensor,  // TODO: Convert TradingState to Tensor
        &q_values_tensor,
        self.episode_step as u64,
        vec![],  // ensemble_votes (disabled for now)
    )? as f32
} else {
    // Legacy reward (backward compatibility)
    let reward_decimal = self.reward_fn.calculate_reward(
        action, state, &next_state, &recent_actions_vec
    )?;
    reward_decimal.to_string().parse::<f32>().unwrap_or(0.0)
};

// Increment episode step for intrinsic rewards
if self.elite_coordinator.is_some() {
    self.episode_step += 1;
}

Location 2: Evaluation Loop (around line 566-569):

  • Similar replacement as Location 1
  • IMPORTANT: Do NOT increment episode_step during evaluation (no exploration rewards)

Challenges:

  1. TradingState to Tensor conversion: Need state.to_tensor(&device) method
  2. Position tracking: Need to extract entry_price, exit_price from episode history
  3. Portfolio value tracking: Need to calculate current portfolio value
  4. Max drawdown tracking: Need to update self.max_drawdown during training

Phase 4: Component Logging (20 min)

File: ml/src/trainers/dqn.rs

Location: Per-epoch logging (after line 880)

Limitation: EliteRewardCoordinator does NOT provide get_last_reward_components() method.

Options:

Option A: Modify coordinator to track components (RECOMMENDED)

// In reward_coordinator.rs
pub struct EliteRewardCoordinator {
    // ... existing fields
    last_components: [f64; 5],  // [extrinsic, intrinsic, entropy, curiosity, ensemble]
}

pub fn get_last_reward_components(&self) -> [f64; 5] {
    self.last_components
}

Option B: Log only total reward (NO COMPONENT BREAKDOWN)

if self.elite_coordinator.is_some() {
    info!("Epoch {} Elite Reward System: ACTIVE (component breakdown unavailable)", epoch + 1);
}

Option C: Calculate components separately (INEFFICIENT)

  • Requires calling each module individually
  • Doubles computation cost
  • Not recommended

Action Diversity Logging (READY):

// Log action diversity (existing monitor.action_counts)
let total_actions = monitor.action_counts.iter().sum::<usize>() as f64;
if total_actions > 0.0 {
    info!(
        "Epoch {} Action Diversity - BUY: {:.1}%, SELL: {:.1}%, HOLD: {:.1}%",
        epoch + 1,
        100.0 * monitor.action_counts[0] as f64 / total_actions,
        100.0 * monitor.action_counts[1] as f64 / total_actions,
        100.0 * monitor.action_counts[2] as f64 / total_actions
    );
}

Phase 5: Testing & Validation (25 min)

Test 1: Compilation (BLOCKED)

cargo build -p ml --example train_dqn --release --features cuda

Expected: Clean build, no errors Status: BLOCKED by curiosity.rs errors

Test 2: Backward Compatibility (PENDING)

cargo test -p ml --lib dqn --no-fail-fast

Expected: 147/147 tests pass (default flag = false, legacy reward) Status: PENDING (blocked by compilation)

Test 3: Elite Reward Smoke Test (PENDING)

cargo run -p ml --example train_dqn --release --features cuda -- --use-elite-reward --epochs 2

Expected: No crashes, reward logging visible Status: PENDING (blocked by compilation)

Test 4: Clippy Warnings (PENDING)

cargo clippy -p ml --example train_dqn -- -D warnings
cargo clippy -p ml --lib --no-deps -- -D warnings

Expected: ≤2 warnings (current threshold) Status: PENDING (blocked by compilation)


Integration Challenges

Challenge 1: TradingState to Tensor Conversion

Problem: EliteRewardCoordinator expects &Tensor for state/next_state, but DQN trainer uses TradingState struct.

Current: TradingState has to_vector() method returning Vec<f64>.

Solution: Add helper method to DQNTrainer:

fn state_to_tensor(&self, state: &TradingState) -> Result<Tensor> {
    let state_vec = state.to_vector();
    Tensor::new(&state_vec[..], &self.device)?
        .reshape(&[1, state_vec.len()])  // [batch=1, num_features]
}

Challenge 2: Episode Step Reset

Problem: episode_step counter must reset at episode boundaries.

Current: DQN training loop does NOT have explicit episode boundaries (continuous training).

Solutions:

Option A: Reset every epoch (simple, but inaccurate)

self.episode_step = 0;  // At epoch start

Option B: Reset on terminal states (accurate, requires state tracking)

if is_terminal_state {
    self.episode_step = 0;
}

Option C: Ignore resets (acceptable for continuous training)

  • Intrinsic rewards use episode_step % 1000 for decay
  • No functional impact if step counter keeps incrementing

Recommendation: Option C (simplest, no behavior change)


Challenge 3: Max Drawdown Tracking

Problem: Elite reward requires max_drawdown parameter, but DQN trainer doesn't track it.

Current: PortfolioTracker exists (Bug #2 fix, Wave B), but max_drawdown not exposed.

Solution: Add max_drawdown tracking to DQNTrainer:

// In training loop
let current_portfolio_value = portfolio_tracker.get_value();
if current_portfolio_value < initial_portfolio_value {
    let drawdown = (initial_portfolio_value - current_portfolio_value) / initial_portfolio_value;
    self.max_drawdown = self.max_drawdown.max(drawdown as f32);
}

Assumption: PortfolioTracker provides get_value() method (needs verification).


Challenge 4: Ensemble Votes

Problem: Elite reward expects ensemble_votes: Vec<usize>, but DQN is a single model.

Solution: Disable ensemble component by passing empty vector:

coordinator.calculate_total_reward(
    // ... other params
    vec![],  // ensemble_votes (disabled)
)

Impact: Ensemble component returns 0.0, effective weight distribution becomes:

  • Extrinsic: 0.444 (0.40 / 0.90)
  • Intrinsic: 0.278 (0.25 / 0.90)
  • Entropy: 0.167 (0.15 / 0.90)
  • Curiosity: 0.111 (0.10 / 0.90)

Recommendation: Accept this limitation (ensemble is optional feature).


Success Criteria

  • CLI flag --use-elite-reward added to train_dqn.rs
  • Reward system logging added
  • EliteRewardCoordinator API documented
  • Compilation errors fixed (BLOCKER: parallel agent)
  • DQNTrainer::new() signature updated with use_elite_reward parameter
  • elite_coordinator, episode_step, max_drawdown fields added to DQNTrainer
  • Reward calculation replaced in training loop (conditional logic)
  • Reward calculation replaced in evaluation loop (conditional logic)
  • Action diversity logging added (per-epoch)
  • Component logging added (or documented as limitation)
  • Compilation test passes (147 DQN tests + clean build)
  • Backward compatibility test passes (147/147 tests with default flag)
  • Elite reward smoke test passes (2 epochs, no crashes)
  • Clippy warnings ≤2 (threshold maintained)

Recommendations

Immediate Actions (Parallel Agent)

  1. Fix curiosity.rs Line 143: Replace Adam with candle_nn::AdamW or implement trait wrapper
  2. Fix curiosity.rs Line 199: Clone next_state_embedding before subtraction
  3. Add get_last_reward_components(): Expose component values for logging

Next Steps (Integration Agent)

  1. Wait for compilation fix: Monitor curiosity.rs changes
  2. Implement Phase 2: Add trainer fields (20 min)
  3. Implement Phase 3: Replace reward calculations (30 min)
  4. Implement Phase 4: Add logging (20 min)
  5. Implement Phase 5: Run full test suite (25 min)

Total Estimated Time: 95 minutes (excluding blocker resolution)


Files Modified

Completed

  • ml/examples/train_dqn.rs (+17 lines: CLI flag, logging, TODO)

Pending

  • ml/src/trainers/dqn.rs (Phases 2-4: struct fields, reward calculation, logging)
  • ml/src/dqn/curiosity.rs (BLOCKER: compilation fixes, owned by parallel agent)
  • ml/src/dqn/reward_coordinator.rs (OPTIONAL: add get_last_reward_components())

Appendix: Comprehensive Plan

See planning tool output (8 steps) for complete phase breakdown:

  1. Step 1: Scope Analysis
  2. Step 2: Code Analysis
  3. Step 3: Implementation Breakdown (5 phases)
  4. Step 4: Risk Analysis & Mitigation
  5. Step 5: Detailed Plan - Phase 1 (CLI Flag)
  6. Step 6: Detailed Plan - Phases 2-3 (Trainer Integration)
  7. Step 7: Detailed Plan - Phases 4-5 (Logging & Testing)
  8. Step 8: Final Summary & Execution Readiness

Continuation ID: 98d46d7b-41fc-484f-a25b-c732954ab473


END OF REPORT