// Bug #15: Portfolio Reset Per Epoch // // Root Cause: ml/src/trainers/dqn.rs:1052 resets portfolio to $100k at START of every epoch // Impact: Constant rewards (~0.004 ± 0.0001), zero learning signal, no compounding // // Expected Behavior: Portfolio should compound across entire training run // - Epoch 1: $100k → $105k (reward = 0.05) // - Epoch 2: $105k → $110k (reward = 0.05, compounded on higher base) // - Epoch 100: $500k → $550k (reward = 0.50, 10x learning signal!) use anyhow::Result; use ml::trainers::dqn::{DQNHyperparameters, DQNTrainer}; /// Helper: Create minimal DQN trainer for testing fn create_test_trainer() -> Result { let mut hyperparams = DQNHyperparameters::conservative(); // Override for fast test hyperparams.epochs = 3; hyperparams.batch_size = 32; hyperparams.buffer_size = 10000; hyperparams.min_replay_size = 100; hyperparams.checkpoint_frequency = 1; hyperparams.min_epochs_before_stopping = 1000; hyperparams.early_stopping_enabled = false; DQNTrainer::new(hyperparams) } /// Helper: Calculate variance of a Vec fn calculate_variance(values: &[f32]) -> f32 { if values.len() < 2 { return 0.0; } let mean = values.iter().sum::() / values.len() as f32; let variance = values.iter() .map(|v| { let diff = v - mean; diff * diff }) .sum::() / values.len() as f32; variance } #[test] fn test_portfolio_reset_removed_from_training_loop() { // This test verifies that portfolio.reset() is NOT called in the training loop // We check this by examining the source code directly let source_code = include_str!("../src/trainers/dqn.rs"); // Count occurrences of ACTUAL calls (not comments with "REMOVED:") // We look for lines that execute reset, not document it let reset_count = source_code .lines() .filter(|line| { let trimmed = line.trim(); // Only count if: // 1. Contains "portfolio_tracker.reset()" // 2. NOT a comment (doesn't start with //) // 3. NOT inside a multiline comment trimmed.contains("portfolio_tracker.reset()") && !trimmed.starts_with("//") && !trimmed.starts_with("*") && !trimmed.contains("REMOVED:") }) .count(); // We expect ZERO occurrences of actual reset calls assert_eq!(reset_count, 0, "Found {} ACTUAL calls to portfolio_tracker.reset() in dqn.rs. \ Bug #15 fix requires removing reset from training loop (line ~1052). \ Portfolio should compound across all epochs, not reset per epoch. \ Note: Comments with 'REMOVED:' are OK and expected.", reset_count); println!("✅ Test PASSED: No portfolio_tracker.reset() calls found in training loop"); println!(" Portfolio will compound across all epochs as expected"); } #[test] fn test_portfolio_compounding_explanation() { // This test documents the expected behavior after Bug #15 fix println!("\n=== Bug #15 Fix: Portfolio Compounding ==="); println!(""); println!("BEFORE (Bug #15 - BROKEN):"); println!(" Epoch 1: $100k → $105k (reward = +0.05)"); println!(" Epoch 2: $100k → $104k (reward = +0.04) ← RESET TO $100k!"); println!(" Epoch 3: $100k → $106k (reward = +0.06) ← RESET TO $100k!"); println!(" Result: Constant rewards ~0.004 ± 0.0001 (ZERO learning signal)"); println!(""); println!("AFTER (Bug #15 - FIXED):"); println!(" Epoch 1: $100k → $105k (reward = +0.05)"); println!(" Epoch 2: $105k → $110k (reward = +0.05, 5% on higher base)"); println!(" Epoch 3: $110k → $116k (reward = +0.06, compounds further)"); println!(" Result: Increasing rewards with variance (STRONG learning signal)"); println!(""); println!("KEY INSIGHT:"); println!(" By removing portfolio_tracker.reset() from line 1052,"); println!(" the portfolio compounds across ALL epochs, creating"); println!(" increasing reward variance that guides DQN learning."); println!(""); // This test always passes - it's documentation assert!(true, "Portfolio compounding documented"); }