Files
foxhunt/docs/codebase-cleanup/AGENT10_KELLY_WARMUP_FIX_REPORT.md
jgrusewski 2df1ea92e1 feat(ml): WAVE 29 DQN Codebase Cleanup & Refactoring Campaign
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>
2025-11-27 23:46:13 +01:00

343 lines
11 KiB
Markdown

# 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<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:
```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<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**
```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<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
```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