Files
foxhunt/crates/ml/tests/kelly_criterion_integration_test.rs
jgrusewski db6462ba7a fix(clippy): resolve all clippy warnings across entire workspace (--all-targets)
Systematic fix of 360+ clippy errors across 37+ crates covering lib,
test, bench, and example targets. Key changes:

- Add targeted #[allow(...)] on #[cfg(test)] modules for test-only lints
  (assertions_on_result_states, float_cmp, str_to_string, indexing, etc.)
- Feature-gate broken integration tests behind __<crate>_integration flags
  where public APIs changed (trading-service, backtesting-service, etc.)
- Remove dead [[test]] entries from Cargo.toml files pointing to deleted files
- Fix production code: field_reassign_with_default, manual_range_contains,
  assert!(false) → panic!(), format!("{}") simplification, len() > 0 → !is_empty()
- Delete truly unused code (Order struct, unused methods/fields/variants)
- Convert sqlx::query!() to sqlx::query() for SQLX_OFFLINE compatibility

Result: cargo clippy --workspace --all-targets -- -D warnings = 0 errors, 0 warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:18:35 +01:00

1087 lines
37 KiB
Rust

#![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<TradeOutcome>>,
}
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<KellyResult, String> {
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::<f64>() / wins.len() as f64
};
let average_loss = if losses.is_empty() {
0.0
} else {
losses.iter().map(|t| t.profit_loss.abs()).sum::<f64>() / 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<f64, String> {
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%");
}