BREAKING CHANGES: - Removed orphaned dqn.rs monolithic trainer (4,975 lines) - Removed orphaned dqn_ensemble.rs module (816 lines) - Removed orphaned tft.rs and tft_complete_int8_integration_test.rs - TFT trainer split into modular directory structure DQN Module Refactoring: - Split trainers/dqn.rs into modular structure (config.rs, statistics.rs, trainer.rs) - Fixed hyperopt 39D search space (continuous params only) - Boolean flags (use_dueling, use_double_dqn, use_per, use_noisy_nets) are now FIXED architectural decisions - use_distributional defaults to false (Candle BUG #36 - scatter_add gradient issues) Clean Module Structure: - ml/src/trainers/dqn/ directory with proper mod.rs exports - ml/src/trainers/tft/ directory with config.rs, types.rs, model.rs, trainer.rs, tests.rs - All P0 features validated: TD-error clamping, batch diversity, LR scheduler, priority staleness Documentation: - Added comprehensive docs in docs/codebase-cleanup/ - ADR-001 for DQN refactoring decisions - Rainbow DQN component matrix and quick reference guides Build Status: Compiles with zero errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
11 KiB
11 KiB
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
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:
// 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
/// 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<f64> {
// 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:
// 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:
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
#[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<f64> = (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
#[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<f64> = 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::<std::collections::HashSet<_>>();
// Should have variety during warmup, not constant values
assert!(unique_warmup.len() > warmup_recs.len() / 2);
}
Temporal Safety Verification
Causal Independence Test
#[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 riskto verify - Run
cargo clippyto check for new warnings - Update documentation in module docstring
- Verify no hardcoded
0.8remains 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
- Kelly Criterion warmup requirements:
/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs:139-212 - Kelly confidence calculation:
/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs:247-262 - KellyConfig structure:
/home/jgrusewski/Work/foxhunt/config/src/structures.rs:118-138 - Current concentration penalty:
/home/jgrusewski/Work/foxhunt/ml/src/risk/kelly_position_sizing_service.rs:551-556
Next Steps
- Implement the fix in
kelly_position_sizing_service.rs - Add comprehensive test suite
- Verify compilation with
cargo check --package ml - Run full test suite with
cargo test --package ml --lib risk - Document changes in module-level docstring
- Coordinate with other agents on Kelly integration