#![allow( clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_markdown, clippy::empty_line_after_doc_comments, clippy::field_reassign_with_default, clippy::get_unwrap, clippy::identity_op, clippy::inconsistent_digit_grouping, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_underscore_must_use, clippy::manual_div_ceil, clippy::manual_let_else, clippy::manual_range_contains, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::non_ascii_literal, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::too_many_lines, clippy::unnecessary_wraps, clippy::unseparated_literal_suffix, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, clippy::else_if_without_else, clippy::expect_used, clippy::missing_const_for_fn, clippy::similar_names, clippy::type_complexity, clippy::collapsible_else_if, clippy::doc_lazy_continuation, clippy::items_after_test_module, clippy::map_clone, clippy::multiple_unsafe_ops_per_block, clippy::unwrap_or_default, clippy::assign_op_pattern, clippy::needless_borrow, clippy::println_empty_string, clippy::unnecessary_cast, clippy::used_underscore_binding, clippy::create_dir, clippy::implicit_saturating_sub, clippy::exit, clippy::expect_fun_call, clippy::too_many_arguments, clippy::unnecessary_map_or, clippy::unwrap_used, dead_code, unused_imports, unused_variables, clippy::cloned_ref_to_slice_refs, clippy::neg_multiply, clippy::while_let_loop, clippy::bool_assert_comparison, clippy::excessive_precision, clippy::trivially_copy_pass_by_ref, clippy::op_ref, clippy::redundant_closure, clippy::unnecessary_lazy_evaluations, clippy::if_then_some_else_none, clippy::unnecessary_to_owned, clippy::single_component_path_imports, )] //! TDD Tests for Kelly Criterion Position Sizing Integration with 45-Action DQN //! //! This test suite validates Kelly optimal position sizing across: //! 1. Kelly fraction calculation (raw and adjusted) //! 2. Position sizing based on capital, win rate, edge, and confidence //! 3. Integration with 45-action DQN action space //! 4. Risk management constraints (min/max kelly, fractional kelly) //! 5. Confidence thresholds and sample size requirements //! 6. Multi-symbol and multi-strategy tracking //! //! Test Coverage: //! - Basic Kelly calculations: 4 tests //! - Position sizing: 3 tests //! - Risk constraints: 3 tests //! - Confidence and sample size: 2 tests //! - Integration with DQN: 2 tests //! - Edge cases: 2 tests //! Total: 16 TDD tests use std::collections::HashMap; // Mock Kelly Criterion structures for testing #[derive(Debug, Clone)] pub struct KellyConfig { pub enabled: bool, pub confidence_threshold: f64, pub fractional_kelly: f64, // Fractional Kelly (e.g., 0.5 for half-Kelly) pub min_kelly_fraction: f64, pub max_kelly_fraction: f64, pub default_position_fraction: f64, pub lookback_periods: usize, } impl Default for KellyConfig { fn default() -> Self { Self { enabled: true, confidence_threshold: 0.5, fractional_kelly: 1.0, // Full Kelly by default min_kelly_fraction: 0.0, max_kelly_fraction: 0.25, // Never risk more than 25% of capital default_position_fraction: 0.05, // 5% default lookback_periods: 100, } } } #[derive(Debug, Clone)] pub struct TradeOutcome { pub symbol: String, pub strategy_id: String, pub profit_loss: f64, pub win: bool, } #[derive(Debug, Clone, Default)] pub struct KellyResult { pub raw_kelly_fraction: f64, pub adjusted_kelly_fraction: f64, pub confidence: f64, pub win_rate: f64, pub average_win: f64, pub average_loss: f64, pub sample_size: usize, pub use_kelly: bool, pub position_fraction: f64, } // Kelly Calculator Implementation pub struct KellyCalculator { config: KellyConfig, trade_history: HashMap<(String, String), Vec>, } impl KellyCalculator { pub fn new(config: KellyConfig) -> Self { Self { config, trade_history: HashMap::new(), } } pub fn add_trade_outcome(&mut self, outcome: TradeOutcome) { let key = (outcome.symbol.clone(), outcome.strategy_id.clone()); self.trade_history.entry(key).or_insert_with(Vec::new).push(outcome); } pub fn calculate_kelly_fraction( &self, symbol: &str, strategy_id: &str, ) -> Result { let key = (symbol.to_string(), strategy_id.to_string()); let trades = self.trade_history.get(&key).cloned().unwrap_or_default(); // Require minimum sample size if trades.len() < 10 { return Err(format!( "Insufficient trade history: {} trades (minimum 10 required)", trades.len() )); } // Calculate statistics let total_trades = trades.len(); let wins: Vec<_> = trades.iter().filter(|t| t.win).collect(); let losses: Vec<_> = trades.iter().filter(|t| !t.win).collect(); let win_rate = wins.len() as f64 / total_trades as f64; let loss_rate = losses.len() as f64 / total_trades as f64; let average_win = if wins.is_empty() { 0.0 } else { wins.iter().map(|t| t.profit_loss).sum::() / wins.len() as f64 }; let average_loss = if losses.is_empty() { 0.0 } else { losses.iter().map(|t| t.profit_loss.abs()).sum::() / losses.len() as f64 }; // Kelly formula: f* = (bp - q) / b // where b = average_win / average_loss (odds ratio) // p = win_rate // q = loss_rate let raw_kelly = if average_loss > 0.0 && average_win > 0.0 && win_rate > 0.0 { let b = average_win / average_loss; let p = win_rate; let q = loss_rate; let kelly = (b * p - q) / b; if kelly > 0.0 { kelly } else { 0.0 } } else { 0.0 }; // Calculate confidence based on sample size and win rate let confidence = self.calculate_confidence(total_trades, win_rate); // Determine if Kelly sizing should be used let use_kelly = self.config.enabled && confidence >= self.config.confidence_threshold && raw_kelly > 0.0 && total_trades >= 20; // Apply fractional Kelly and caps let adjusted_kelly = if use_kelly { let fractional = raw_kelly * self.config.fractional_kelly; fractional .max(self.config.min_kelly_fraction) .min(self.config.max_kelly_fraction) } else { self.config.default_position_fraction }; Ok(KellyResult { raw_kelly_fraction: raw_kelly, adjusted_kelly_fraction: adjusted_kelly, confidence, win_rate, average_win, average_loss, sample_size: total_trades, use_kelly, position_fraction: adjusted_kelly, }) } fn calculate_confidence(&self, sample_size: usize, win_rate: f64) -> f64 { // Sample size confidence (larger samples = higher confidence) let size_confidence = (sample_size as f64 / 100.0).min(1.0); // Win rate confidence (avoid extreme win rates which may be overfitting) let rate_confidence = if (0.3..=0.7).contains(&win_rate) { 1.0 // Reasonable win rates } else if (0.2..=0.8).contains(&win_rate) { 0.8 // Slightly extreme but acceptable } else { 0.5 // Very extreme win rates - lower confidence }; // Combined confidence (size_confidence * rate_confidence).min(1.0) } pub fn get_position_size( &self, symbol: &str, strategy_id: &str, capital: f64, entry_price: f64, ) -> Result { if entry_price <= 0.0 { return Err("Entry price must be positive".to_string()); } let kelly_result = self.calculate_kelly_fraction(symbol, strategy_id)?; let position_value = capital * kelly_result.position_fraction; let shares = position_value / entry_price; Ok(shares) } } // ============================================================================ // TEST 1: Kelly Calculator Initialization // ============================================================================ #[test] fn test_kelly_calculator_initialization() { let config = KellyConfig::default(); let calculator = KellyCalculator::new(config.clone()); // Verify config loaded correctly assert!(calculator.config.enabled); assert_eq!(calculator.config.fractional_kelly, 1.0); assert_eq!(calculator.config.max_kelly_fraction, 0.25); assert_eq!(calculator.config.default_position_fraction, 0.05); assert_eq!(calculator.config.min_kelly_fraction, 0.0); // Verify empty trade history let result = calculator.calculate_kelly_fraction("ES", "dqn_strategy"); assert!( result.is_err(), "Should fail with no trade history" ); } // ============================================================================ // TEST 2: High Edge Position (60% Win, 1.5 Ratio) → Kelly with caps // ============================================================================ #[test] fn test_kelly_full_position_high_edge() { // Use larger sample for sufficient confidence let mut calculator = KellyCalculator::new(KellyConfig::default()); // Create high-edge scenario: 60% win rate, wins are 50% larger than losses // 100 trades = 60 wins, 40 losses → confidence = 1.0 * 1.0 = 1.0 ✓ for _ in 0..60 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: 150.0, // Wins: $150 win: true, }); } for _ in 0..40 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: -100.0, // Losses: $100 win: false, }); } let result = calculator .calculate_kelly_fraction("ES", "dqn_strategy") .expect("Kelly calculation should succeed"); // Verify statistics assert_eq!(result.sample_size, 100, "Should have 100 trades"); assert_eq!(result.win_rate, 0.6, "Win rate should be 60%"); assert_eq!(result.average_win, 150.0, "Average win should be 150"); assert_eq!(result.average_loss, 100.0, "Average loss should be 100"); // Kelly formula: f* = (bp - q) / b // b = 150/100 = 1.5 // p = 0.6, q = 0.4 // f* = (1.5 * 0.6 - 0.4) / 1.5 = (0.9 - 0.4) / 1.5 ≈ 0.333 assert!(result.raw_kelly_fraction > 0.3 && result.raw_kelly_fraction < 0.35, "Raw Kelly should be ~0.333, got {}", result.raw_kelly_fraction); // With full Kelly (1.0) and cap at 0.25, adjusted should be 0.25 assert_eq!( result.adjusted_kelly_fraction, 0.25, "Adjusted Kelly should be capped at 0.25 (max_kelly_fraction)" ); // Should use Kelly sizing assert!(result.use_kelly, "Should use Kelly sizing"); assert!(result.confidence >= 0.5, "Confidence should meet threshold"); } // ============================================================================ // TEST 3: Medium Edge Position (55% Win, 1.2 Ratio) → 50% Kelly // ============================================================================ #[test] fn test_kelly_half_position_medium_edge() { let mut config = KellyConfig::default(); config.fractional_kelly = 0.5; // Use half-Kelly for safety let mut calculator = KellyCalculator::new(config); // Medium edge: 55% win rate, wins are 20% larger than losses // 100 trades = 55 wins, 45 losses → confidence = 1.0 * 1.0 = 1.0 ✓ for _ in 0..55 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "conservative_strategy".to_string(), profit_loss: 120.0, // Wins: $120 win: true, }); } for _ in 0..45 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "conservative_strategy".to_string(), profit_loss: -100.0, // Losses: $100 win: false, }); } let result = calculator .calculate_kelly_fraction("ES", "conservative_strategy") .expect("Kelly calculation should succeed"); // Verify statistics assert_eq!(result.sample_size, 100, "Should have 100 trades"); assert_eq!(result.win_rate, 0.55, "Win rate should be 55%"); // Kelly formula: b = 120/100 = 1.2 // f* = (1.2 * 0.55 - 0.45) / 1.2 = (0.66 - 0.45) / 1.2 = 0.21 / 1.2 ≈ 0.175 let expected_raw_kelly = (1.2 * 0.55 - 0.45) / 1.2; assert!( (result.raw_kelly_fraction - expected_raw_kelly).abs() < 0.01, "Raw Kelly mismatch: expected {}, got {}", expected_raw_kelly, result.raw_kelly_fraction ); // Half-Kelly: 0.175 * 0.5 ≈ 0.0875 let expected_adjusted = expected_raw_kelly * 0.5; assert!( (result.adjusted_kelly_fraction - expected_adjusted).abs() < 0.01, "Adjusted Kelly should be ~{} (half of {}), got {}", expected_adjusted, result.raw_kelly_fraction, result.adjusted_kelly_fraction ); // Should use Kelly sizing with sufficient sample size assert!(result.use_kelly, "Should use Kelly sizing"); } // ============================================================================ // TEST 4: Zero Position for Negative Edge (Losing Strategy) // ============================================================================ #[test] fn test_kelly_zero_position_negative_edge() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // Losing strategy: 40% win rate (60% losses) // 100 trades = 40 wins, 60 losses → confidence = 1.0 * 0.8 = 0.8 ✓ for _ in 0..40 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "bad_strategy".to_string(), profit_loss: 100.0, win: true, }); } for _ in 0..60 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "bad_strategy".to_string(), profit_loss: -150.0, // Losses larger than wins win: false, }); } let result = calculator .calculate_kelly_fraction("ES", "bad_strategy") .expect("Kelly calculation should succeed"); // Verify losing strategy assert_eq!(result.win_rate, 0.4, "Win rate should be 40%"); assert!(result.raw_kelly_fraction <= 0.0, "Raw Kelly should be 0 or negative for losing strategy"); // Position should default to config.default_position_fraction (5%) assert_eq!( result.adjusted_kelly_fraction, 0.05, "Should use default position size (5%) for losing strategy" ); // Should NOT use Kelly sizing for negative edge assert!(!result.use_kelly, "Should not use Kelly sizing for negative edge"); } // ============================================================================ // TEST 5: Fractional Kelly Conservative (0.25x Kelly) // ============================================================================ #[test] fn test_kelly_fractional_conservative() { let mut config = KellyConfig::default(); config.fractional_kelly = 0.25; // Ultra-conservative: 1/4 Kelly let mut calculator = KellyCalculator::new(config); // Profitable strategy: 65% win rate // 100 trades = 65 wins, 35 losses → confidence = 1.0 * 1.0 = 1.0 ✓ for _ in 0..65 { calculator.add_trade_outcome(TradeOutcome { symbol: "NQ".to_string(), strategy_id: "aggressive_strategy".to_string(), profit_loss: 200.0, win: true, }); } for _ in 0..35 { calculator.add_trade_outcome(TradeOutcome { symbol: "NQ".to_string(), strategy_id: "aggressive_strategy".to_string(), profit_loss: -100.0, win: false, }); } let result = calculator .calculate_kelly_fraction("NQ", "aggressive_strategy") .expect("Kelly calculation should succeed"); // Verify strategy assert_eq!(result.win_rate, 0.65, "Win rate should be 65%"); // Kelly formula: b = 200/100 = 2.0 // f* = (2.0 * 0.65 - 0.35) / 2.0 = (1.3 - 0.35) / 2.0 = 0.95 / 2.0 = 0.475 let expected_raw = (2.0 * 0.65 - 0.35) / 2.0; // Fractional: 0.475 * 0.25 = 0.11875 (not capped at max 0.25) let expected_fractional = expected_raw * 0.25; assert!( (result.adjusted_kelly_fraction - expected_fractional).abs() < 0.01, "Adjusted Kelly should be ~{}, got {}", expected_fractional, result.adjusted_kelly_fraction ); } // ============================================================================ // TEST 6: Position Size Calculation with Capital and Entry Price // ============================================================================ #[test] fn test_kelly_position_size_calculation() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // Add trade history with large sample for _ in 0..66 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: 100.0, win: true, }); } for _ in 0..34 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: -75.0, win: false, }); } // Calculate position size with $100k capital, $5000 entry price let position_size = calculator .get_position_size("ES", "dqn_strategy", 100_000.0, 5000.0) .expect("Position size calculation should succeed"); // Verify calculation: kelly_result.position_fraction * capital / entry_price let kelly_result = calculator .calculate_kelly_fraction("ES", "dqn_strategy") .expect("Kelly calculation should succeed"); let expected_position_value = 100_000.0 * kelly_result.position_fraction; let expected_shares = expected_position_value / 5000.0; assert_eq!( position_size, expected_shares, "Position size should match calculated value" ); // Verify position size is reasonable (between 0 and capital) assert!(position_size > 0.0, "Position size should be positive"); assert!(position_size < 100_000.0 / 5000.0, "Position size should be less than total capital"); } // ============================================================================ // TEST 7: Minimum Sample Size Requirement (10 Trades) // ============================================================================ #[test] fn test_kelly_minimum_sample_size() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // Add only 9 trades (below minimum of 10) for _ in 0..9 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: 100.0, win: true, }); } let result = calculator.calculate_kelly_fraction("ES", "dqn_strategy"); assert!( result.is_err(), "Should fail with insufficient sample size (< 10 trades)" ); if let Err(msg) = result { assert!( msg.contains("Insufficient trade history"), "Error message should mention insufficient history" ); } } // ============================================================================ // TEST 8: Confidence Threshold Enforcement (Small Sample) // ============================================================================ #[test] fn test_kelly_confidence_threshold() { let mut config = KellyConfig::default(); config.confidence_threshold = 0.8; // High confidence required let mut calculator = KellyCalculator::new(config); // Add small sample size with normal win rate // 20 trades = 10 wins, 10 losses → confidence = (20/100) * 1.0 = 0.2 < 0.8 ✗ for _ in 0..10 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: 100.0, win: true, }); } for _ in 0..10 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: -100.0, win: false, }); } let result = calculator .calculate_kelly_fraction("ES", "dqn_strategy") .expect("Kelly calculation should succeed"); // With only 20 trades, confidence = (20/100) * 1.0 = 0.2 < 0.8 threshold assert!( result.confidence < 0.8, "Confidence should be below 0.8 threshold with small sample" ); // Should NOT use Kelly sizing due to confidence threshold assert!(!result.use_kelly, "Should not use Kelly sizing below confidence threshold"); // Should fall back to default position assert_eq!( result.adjusted_kelly_fraction, 0.05, "Should use default position (5%) when confidence is too low" ); } // ============================================================================ // TEST 9: Multiple Strategies Tracking // ============================================================================ #[test] fn test_kelly_multiple_strategies_tracking() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // Strategy 1: DQN (high performance) // 100 trades = 62.5 wins, 37.5 losses → 62.5/100 = 0.625 → confidence = 1.0 * 1.0 = 1.0 ✓ for _ in 0..63 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: 150.0, win: true, }); } for _ in 0..37 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: -100.0, win: false, }); } // Strategy 2: PPO (moderate performance) // 100 trades = 55 wins, 45 losses → confidence = 1.0 * 1.0 = 1.0 ✓ for _ in 0..55 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "ppo_strategy".to_string(), profit_loss: 120.0, win: true, }); } for _ in 0..45 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "ppo_strategy".to_string(), profit_loss: -90.0, win: false, }); } let dqn_result = calculator .calculate_kelly_fraction("ES", "dqn_strategy") .expect("DQN Kelly calculation should succeed"); let ppo_result = calculator .calculate_kelly_fraction("ES", "ppo_strategy") .expect("PPO Kelly calculation should succeed"); // DQN should have higher edge (63% win vs 55% win) assert!(dqn_result.win_rate > ppo_result.win_rate, "DQN win rate should be higher"); // DQN should recommend larger position assert!( dqn_result.adjusted_kelly_fraction > ppo_result.adjusted_kelly_fraction, "DQN should recommend larger position than PPO" ); } // ============================================================================ // TEST 10: Multiple Symbols Tracking // ============================================================================ #[test] fn test_kelly_multiple_symbols_tracking() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // ES Futures: Lower win rate // 100 trades = 40 wins, 60 losses → confidence = 1.0 * 0.8 = 0.8 ✓ for _ in 0..40 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: 200.0, win: true, }); } for _ in 0..60 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: -100.0, win: false, }); } // NQ Futures: Higher win rate // 100 trades = 60 wins, 40 losses → confidence = 1.0 * 1.0 = 1.0 ✓ for _ in 0..60 { calculator.add_trade_outcome(TradeOutcome { symbol: "NQ".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: 150.0, win: true, }); } for _ in 0..40 { calculator.add_trade_outcome(TradeOutcome { symbol: "NQ".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: -100.0, win: false, }); } let es_result = calculator .calculate_kelly_fraction("ES", "dqn_strategy") .expect("ES Kelly calculation should succeed"); let nq_result = calculator .calculate_kelly_fraction("NQ", "dqn_strategy") .expect("NQ Kelly calculation should succeed"); // NQ has higher win rate (60% vs 40%) assert_eq!(es_result.win_rate, 0.4, "ES win rate should be 40%"); assert_eq!(nq_result.win_rate, 0.6, "NQ win rate should be 60%"); // NQ should recommend larger position due to higher edge assert!( nq_result.adjusted_kelly_fraction > es_result.adjusted_kelly_fraction, "NQ should recommend larger position than ES" ); } // ============================================================================ // TEST 11: Kelly Fraction Caps (Min and Max) // ============================================================================ #[test] fn test_kelly_fraction_caps() { let mut config = KellyConfig::default(); config.min_kelly_fraction = 0.02; // Minimum 2% config.max_kelly_fraction = 0.15; // Maximum 15% let mut calculator = KellyCalculator::new(config); // Create extremely profitable scenario // 100 trades = 87.5 wins, 12.5 losses (87 wins, 13 losses) // confidence = 1.0 * 0.5 = 0.5 ✓ for _ in 0..87 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: 500.0, win: true, }); } for _ in 0..13 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_strategy".to_string(), profit_loss: -100.0, win: false, }); } let result = calculator .calculate_kelly_fraction("ES", "dqn_strategy") .expect("Kelly calculation should succeed"); // Raw Kelly would be very high with 87% win rate assert!(result.raw_kelly_fraction > 0.5, "Raw Kelly should be very high (>50%)"); // But adjusted should be capped at 15% assert_eq!( result.adjusted_kelly_fraction, 0.15, "Adjusted Kelly should be capped at max_kelly_fraction (15%)" ); } // ============================================================================ // TEST 12: Zero Profit Edge Case (Break-Even Strategy) // ============================================================================ #[test] fn test_kelly_zero_profit_edge_case() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // Break-even scenario: 50% win, equal wins and losses // 100 trades = 50 wins, 50 losses → confidence = 1.0 * 1.0 = 1.0 ✓ for _ in 0..50 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "breakeven_strategy".to_string(), profit_loss: 100.0, win: true, }); } for _ in 0..50 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "breakeven_strategy".to_string(), profit_loss: -100.0, win: false, }); } let result = calculator .calculate_kelly_fraction("ES", "breakeven_strategy") .expect("Kelly calculation should succeed"); // With 50% win rate and equal odds, Kelly should be 0 assert_eq!(result.raw_kelly_fraction, 0.0, "Kelly should be 0 for break-even strategy"); // Should use default position assert_eq!(result.adjusted_kelly_fraction, 0.05, "Should use default position (5%)"); } // ============================================================================ // TEST 13: Extreme Win Rate (95% Win) - Positive Edge despite extreme rate // ============================================================================ #[test] fn test_kelly_extreme_win_rate_low_confidence() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // Extreme 95% win rate with edge (wins > losses) // 100 trades = 95 wins, 5 losses // confidence = 1.0 * 0.5 = 0.5 ✓ for _ in 0..95 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "overfitted_strategy".to_string(), profit_loss: 100.0, // Wins are $100 win: true, }); } for _ in 0..5 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "overfitted_strategy".to_string(), profit_loss: -100.0, // Losses are $100 win: false, }); } let result = calculator .calculate_kelly_fraction("ES", "overfitted_strategy") .expect("Kelly calculation should succeed"); // Extreme win rate should reduce confidence assert_eq!(result.win_rate, 0.95, "Win rate should be 95%"); assert_eq!( result.confidence, 0.5, "Confidence should be 0.5 for extreme win rates (1.0 * 0.5)" ); // Kelly = (1.0 * 0.95 - 0.05) / 1.0 = 0.9 (very profitable despite equal odds!) // With confidence >= 0.5 threshold and size >= 20, Kelly sizing is used // Capped at max_kelly_fraction = 0.25 assert!(result.use_kelly, "Should use Kelly sizing with high win rate"); assert!(result.raw_kelly_fraction > 0.0, "Raw Kelly should be positive"); assert_eq!(result.adjusted_kelly_fraction, 0.25, "Should cap at max_kelly_fraction (25%)"); } // ============================================================================ // TEST 14: DQN 45-Action Integration - Position Size from Action Index // ============================================================================ #[test] fn test_kelly_dqn_45action_position_scaling() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // Add trade history for DQN strategy // 100 trades = 62.5 wins, 37.5 losses (62 wins, 38 losses) for _ in 0..62 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_45action".to_string(), profit_loss: 150.0, win: true, }); } for _ in 0..38 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "dqn_45action".to_string(), profit_loss: -100.0, win: false, }); } let result = calculator .calculate_kelly_fraction("ES", "dqn_45action") .expect("Kelly calculation should succeed"); assert!(result.use_kelly, "Should use Kelly sizing"); assert!(result.adjusted_kelly_fraction > 0.0, "Position fraction should be positive"); // Test position scaling with different entry prices let capital = 100_000.0; let es_price = 5000.0; let nq_price = 20_000.0; let es_shares = calculator .get_position_size("ES", "dqn_45action", capital, es_price) .expect("Position calculation should succeed"); let nq_shares = calculator .get_position_size("ES", "dqn_45action", capital, nq_price) .expect("Position calculation should succeed"); // Same Kelly fraction but different entry prices should scale appropriately assert!((es_shares / nq_shares - nq_price / es_price).abs() < 0.01, "Position scaling should be inversely proportional to entry price"); } // ============================================================================ // TEST 15: Kelly Position Sizing with Action Masking Constraints // ============================================================================ #[test] fn test_kelly_with_action_masking_constraints() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // Add winning strategy trades // 100 trades = 60 wins, 40 losses → confidence = 1.0 * 1.0 = 1.0 ✓ for _ in 0..60 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "masked_dqn".to_string(), profit_loss: 120.0, win: true, }); } for _ in 0..40 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "masked_dqn".to_string(), profit_loss: -80.0, win: false, }); } let _kelly_result = calculator .calculate_kelly_fraction("ES", "masked_dqn") .expect("Kelly calculation should succeed"); // Verify Kelly sizing gives actionable position let capital = 50_000.0; let entry_price = 5000.0; let position_size = calculator .get_position_size("ES", "masked_dqn", capital, entry_price) .expect("Position size should be calculated"); // Position should be within reasonable bounds for action masking assert!(position_size > 0.0, "Position size should be positive"); assert!(position_size < (capital / entry_price), "Position size should be less than total capital"); // Kelly position should be meaningful but not reckless let position_value = position_size * entry_price; let position_percent = position_value / capital; assert!( position_percent <= 0.25, "Kelly position should never exceed 25% of capital (max_kelly_fraction), got {}%", position_percent * 100.0 ); } // ============================================================================ // TEST 16: Rapid Adaptation to Losing Period // ============================================================================ #[test] fn test_kelly_rapid_adaptation_losing_period() { let mut calculator = KellyCalculator::new(KellyConfig::default()); // Initial winning period: 70% win rate // 20 trades = 14 wins, 6 losses → confidence = (20/100) * 1.0 = 0.2 < 0.5 ✗ for _ in 0..14 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "adaptive_dqn".to_string(), profit_loss: 100.0, win: true, }); } for _ in 0..6 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "adaptive_dqn".to_string(), profit_loss: -100.0, win: false, }); } let initial_result = calculator .calculate_kelly_fraction("ES", "adaptive_dqn") .expect("Initial Kelly calculation should succeed"); assert_eq!(initial_result.win_rate, 0.7, "Initial win rate should be 70%"); let initial_position = initial_result.adjusted_kelly_fraction; // Market downturn: Add 80 more losing trades // Total: 100 trades = 24 wins, 76 losses // confidence = 1.0 * 0.5 = 0.5 ✓ for _ in 0..10 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "adaptive_dqn".to_string(), profit_loss: 100.0, win: true, }); } for _ in 0..70 { calculator.add_trade_outcome(TradeOutcome { symbol: "ES".to_string(), strategy_id: "adaptive_dqn".to_string(), profit_loss: -150.0, // Larger losses win: false, }); } let adapted_result = calculator .calculate_kelly_fraction("ES", "adaptive_dqn") .expect("Adapted Kelly calculation should succeed"); // Win rate should drop significantly assert_eq!(adapted_result.win_rate, 0.24, "Win rate should drop to 24%"); // Position should reduce or stay at default due to losing strategy assert!( adapted_result.adjusted_kelly_fraction <= initial_position, "Position should reduce or stay same during losing period" ); // With 24% win rate (below 50%), should use default or smaller position assert!(!adapted_result.use_kelly, "Should not use Kelly when win rate drops below 50%"); }