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

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 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