# Agent 10: Kelly Criterion Warmup Signal Leakage Fix **Date**: 2025-11-27 **Agent**: Agent 10 - Anti-Overfitting Features **Task**: Fix Kelly criterion warmup signal leakage in risk integration --- ## Issue Identified: Hardcoded Position Threshold Signal Leakage ### Location: `/home/jgrusewski/Work/foxhunt/ml/src/risk/kelly_position_sizing_service.rs` **Line 553**: Hardcoded concentration penalty factor ```rust let concentration_penalty = if portfolio_concentration > 0.5 { 0.8 // Reduce by 20% for high concentration } else { 1.0 }; ``` ### Root Cause Analysis #### 1. **Hardcoded Threshold Problem** - The `0.8` (80%) penalty is hardcoded and independent of Kelly calculations - This creates a **fixed signal** that the model can memorize during training - The threshold doesn't respect Kelly's warmup period or statistical confidence #### 2. **Kelly Warmup Period Not Considered** From `/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs`: - **Minimum sample size**: 10 trades required (line 139) - **Optimal sample size**: 20 trades for `use_kelly = true` (line 212) - **Confidence calculation**: Based on sample size and win rate (lines 247-262) #### 3. **Signal Leakage Mechanism** The current code applies the penalty **before** Kelly has sufficient data: ```rust // Current flow (WRONG): 1. Portfolio concentration check → 0.8 penalty applied immediately 2. Kelly calculation → May not have warmup data yet 3. DQN training → Learns the 0.8 threshold as a fixed pattern ``` This allows the model to: - **Memorize** the 0.8 penalty threshold - **Exploit** concentration patterns that haven't been validated by Kelly - **Overtrain** on fixed rules rather than market dynamics --- ## Solution: Kelly-Aware Dynamic Concentration Penalty ### Implementation Plan #### 1. **Add Warmup Period Awareness** ```rust /// Apply concentration limits with Kelly warmup awareness fn apply_concentration_limits( &self, fraction: f64, current_allocation: f64, portfolio_concentration: f64, kelly_confidence: f64, // NEW: Kelly's confidence level kelly_sample_size: usize, // NEW: Kelly's sample size ) -> Result { // Basic allocation limit let max_additional_allocation = self.config.max_single_asset_allocation - current_allocation; let concentration_adjusted = fraction.min(max_additional_allocation.max(0.0)); // FIXED: Dynamic concentration penalty based on Kelly warmup let concentration_penalty = if portfolio_concentration > 0.5 { // During warmup (< 20 trades), use more conservative penalty if kelly_sample_size < 20 { // Warmup period: More conservative, but scales with sample size let warmup_progress = kelly_sample_size as f64 / 20.0; // Start at 0.5 (50% penalty), scale to 0.8 as warmup completes 0.5 + (0.3 * warmup_progress) } else { // Post-warmup: Use Kelly-confidence-based penalty // High confidence (0.9) → 0.95 (5% penalty) // Medium confidence (0.7) → 0.85 (15% penalty) // Low confidence (0.5) → 0.75 (25% penalty) 0.75 + (kelly_confidence * 0.20) } } else { 1.0 // No penalty for low concentration }; Ok(concentration_adjusted * concentration_penalty) } ``` #### 2. **Thread Kelly Confidence Through Call Chain** Update `get_position_sizing` to pass Kelly metadata: ```rust // In get_position_sizing() method: let concentration_adjusted_fraction = self.apply_concentration_limits( adjusted_fraction, current_allocation, portfolio_concentration, kelly_recommendation.confidence, // NEW kelly_recommendation.sample_size, // NEW )?; ``` #### 3. **Configuration for Warmup Thresholds** Add to `KellyServiceConfig`: ```rust pub struct KellyServiceConfig { // ... existing fields ... /// Minimum sample size for full Kelly confidence pub kelly_warmup_sample_size: usize, // Default: 20 /// Minimum concentration penalty during warmup pub warmup_min_penalty: f64, // Default: 0.5 (50%) /// Maximum concentration penalty adjustment from confidence pub confidence_penalty_range: f64, // Default: 0.20 (20%) } impl Default for KellyServiceConfig { fn default() -> Self { Self { // ... existing defaults ... kelly_warmup_sample_size: 20, warmup_min_penalty: 0.5, confidence_penalty_range: 0.20, } } } ``` --- ## Anti-Leakage Properties ### Before Fix (Signal Leakage) ``` Sample Size | Confidence | Penalty | Problem ------------|------------|---------|--------------------------- 0 | 0.0 | 0.8 | Fixed penalty before data 5 | 0.4 | 0.8 | Fixed penalty during warmup 15 | 0.7 | 0.8 | Fixed penalty near warmup 25 | 0.85 | 0.8 | Fixed penalty post-warmup ``` **Result**: Model memorizes `0.8` as a magic number ### After Fix (No Leakage) ``` Sample Size | Confidence | Penalty | Rationale ------------|------------|---------|--------------------------- 0 | 0.0 | 0.5 | Conservative during zero data 5 | 0.4 | 0.575 | Warmup: 0.5 + (0.3 * 0.25) 15 | 0.7 | 0.725 | Warmup: 0.5 + (0.3 * 0.75) 25 | 0.85 | 0.92 | Post-warmup: 0.75 + (0.85 * 0.20) ``` **Result**: Penalty varies with statistical confidence, no fixed memorization --- ## Testing Requirements ### 1. **Unit Tests** ```rust #[tokio::test] async fn test_concentration_penalty_warmup_progression() { let service = create_test_service(); // Test warmup progression (0-20 trades) for sample_size in [0, 5, 10, 15, 20] { let penalty = service.apply_concentration_limits( 0.1, 0.05, 0.6, 0.7, // confidence sample_size // sample size ).unwrap(); // Verify penalty increases with sample size during warmup if sample_size < 20 { assert!(penalty >= 0.5); // Min warmup penalty assert!(penalty <= 0.8); // Max warmup penalty } } } #[tokio::test] async fn test_concentration_penalty_confidence_scaling() { let service = create_test_service(); // Test post-warmup confidence scaling for confidence in [0.5, 0.7, 0.85, 0.95] { let penalty = service.apply_concentration_limits( 0.1, 0.05, 0.6, confidence, 25 // Post-warmup ).unwrap(); // Verify penalty scales with confidence let expected = 0.75 + (confidence * 0.20); assert!((penalty - expected).abs() < 0.01); } } #[tokio::test] async fn test_no_hardcoded_thresholds() { // Verify no magic numbers in concentration penalty logic let service = create_test_service(); let penalties: Vec = (0..30) .map(|size| { service.apply_concentration_limits( 0.1, 0.05, 0.6, 0.7, size ).unwrap() }) .collect(); // All penalties should be unique (no repeated magic numbers) let unique_penalties: std::collections::HashSet<_> = penalties.iter().map(|p| (p * 1000.0) as i64).collect(); // Should have variation, not constant values assert!(unique_penalties.len() > 10); } ``` ### 2. **Integration Test** ```rust #[tokio::test] async fn test_kelly_warmup_prevents_signal_leakage() { let service = create_test_service(); // Simulate training scenario let mut recommendations = Vec::new(); for epoch in 0..50 { let sample_size = epoch / 2; // Gradual data accumulation let confidence = (sample_size as f64 / 20.0).min(1.0); let request = create_test_request(); let recommendation = service.get_position_sizing(&request).await.unwrap(); recommendations.push(( sample_size, recommendation.adjusted_position_fraction )); } // Verify: No repeated fractions during warmup let warmup_recs: Vec = recommendations.iter() .filter(|(size, _)| *size < 20) .map(|(_, frac)| frac) .cloned() .collect(); let unique_warmup = warmup_recs.iter() .map(|f| (f * 10000.0) as i64) .collect::>(); // Should have variety during warmup, not constant values assert!(unique_warmup.len() > warmup_recs.len() / 2); } ``` --- ## Temporal Safety Verification ### Causal Independence Test ```rust #[tokio::test] async fn test_concentration_penalty_temporal_safety() { let service = create_test_service(); // Verify penalty at time T doesn't depend on data from T+1 let penalty_t0 = service.apply_concentration_limits( 0.1, 0.05, 0.6, 0.7, 10 ).unwrap(); // Simulate "future" data accumulation // ... add more trades ... let penalty_t0_recomputed = service.apply_concentration_limits( 0.1, 0.05, 0.6, 0.7, 10 // Same inputs as before ).unwrap(); // Penalty should be identical (no look-ahead bias) assert_eq!(penalty_t0, penalty_t0_recomputed); } ``` --- ## Implementation Checklist - [ ] Update `apply_concentration_limits()` signature with Kelly parameters - [ ] Implement dynamic warmup-aware penalty calculation - [ ] Add configuration fields for warmup thresholds - [ ] Update call sites in `get_position_sizing()` - [ ] Write unit tests for warmup progression - [ ] Write unit tests for confidence scaling - [ ] Write integration test for signal leakage prevention - [ ] Write temporal safety verification test - [ ] Run `cargo test --package ml --lib risk` to verify - [ ] Run `cargo clippy` to check for new warnings - [ ] Update documentation in module docstring - [ ] Verify no hardcoded `0.8` remains in concentration logic --- ## Expected Impact ### Before Fix - **Signal Leakage**: DQN memorizes 0.8 penalty threshold - **Overfitting**: Model exploits hardcoded rules - **Poor Generalization**: Performance degrades on unseen data ### After Fix - **No Signal Leakage**: Penalty varies with statistical confidence - **Better Generalization**: Model learns market dynamics, not magic numbers - **Temporal Safety**: No look-ahead bias from future Kelly data - **Statistical Rigor**: Penalty respects Kelly's confidence and warmup period --- ## References 1. Kelly Criterion warmup requirements: `/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs:139-212` 2. Kelly confidence calculation: `/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs:247-262` 3. KellyConfig structure: `/home/jgrusewski/Work/foxhunt/config/src/structures.rs:118-138` 4. Current concentration penalty: `/home/jgrusewski/Work/foxhunt/ml/src/risk/kelly_position_sizing_service.rs:551-556` --- ## Next Steps 1. Implement the fix in `kelly_position_sizing_service.rs` 2. Add comprehensive test suite 3. Verify compilation with `cargo check --package ml` 4. Run full test suite with `cargo test --package ml --lib risk` 5. Document changes in module-level docstring 6. Coordinate with other agents on Kelly integration