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>
343 lines
11 KiB
Markdown
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
|