Files
foxhunt/ml/tests/kelly_position_sizing_integration.rs
jgrusewski c645e6222d Wave 11: Rainbow DQN integration + 23/23 tests passing
CRITICAL FINDINGS from 3-trial validation:
- 85,120 gradient clipping warnings (81.6% of logs) - REGRESSION
- Rainbow features DISABLED: use_dueling=false, use_distributional=false, use_noisy_nets=false
- Negative Q-values confirmed: HOLD -1000 to -3250
- Performance: Sharpe 0.29 (target 0.77)

Changes:
- Fixed N-Step compilation (7/7 tests passing)
- Fixed Distributional compilation (6/6 tests passing)
- Fixed Dueling CUDA errors (10/10 tests passing)
- Added TDD validation for state_dim=225
- Total: 23/23 Wave 11 tests passing (100%)

Issues requiring investigation:
1. Why are Dueling/Distributional/Noisy disabled in hyperopt?
2. Why gradient explosion despite previous fixes?
3. Test coverage gaps - unit tests pass but integration fails

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 13:53:59 +01:00

173 lines
5.0 KiB
Rust

// Kelly Criterion Position Sizing Integration Test
//
// Validates that Kelly fraction is correctly applied to position sizing
// in the backtesting pipeline (PortfolioTracker → EvaluationEngine → Hyperopt).
use ml::evaluation::engine::{Action, EvaluationEngine};
use ml::evaluation::metrics::OHLCVBar;
#[test]
fn test_kelly_scales_pnl_correctly() {
// Test 1: Full Kelly (1.0) - baseline
let mut engine_full = EvaluationEngine::new_with_kelly(10_000.0, 1.0);
// Open long position at $100
let bar1 = OHLCVBar {
timestamp: 0,
open: 100.0,
high: 100.0,
low: 100.0,
close: 100.0,
volume: 0.0,
};
engine_full.process_bar(0, &bar1, Action::Buy);
// Close at $110 (10% gain)
let bar2 = OHLCVBar {
timestamp: 1,
open: 110.0,
high: 110.0,
low: 110.0,
close: 110.0,
volume: 0.0,
};
engine_full.process_bar(1, &bar2, Action::Sell);
// Full Kelly: PnL = 110 - 100 = $10
assert_eq!(engine_full.trades.len(), 1);
assert!((engine_full.trades[0].pnl - 10.0).abs() < 0.01);
// Test 2: Half Kelly (0.5) - reduced position
let mut engine_half = EvaluationEngine::new_with_kelly(10_000.0, 0.5);
engine_half.process_bar(0, &bar1, Action::Buy);
engine_half.process_bar(1, &bar2, Action::Sell);
// Half Kelly: PnL = (110 - 100) * 0.5 = $5
assert_eq!(engine_half.trades.len(), 1);
assert!((engine_half.trades[0].pnl - 5.0).abs() < 0.01);
// Test 3: Quarter Kelly (0.25) - conservative
let mut engine_quarter = EvaluationEngine::new_with_kelly(10_000.0, 0.25);
engine_quarter.process_bar(0, &bar1, Action::Buy);
engine_quarter.process_bar(1, &bar2, Action::Sell);
// Quarter Kelly: PnL = (110 - 100) * 0.25 = $2.50
assert_eq!(engine_quarter.trades.len(), 1);
assert!((engine_quarter.trades[0].pnl - 2.5).abs() < 0.01);
}
#[test]
fn test_kelly_scales_losses_correctly() {
// Test Kelly scaling on losing trades
// Full Kelly (1.0)
let mut engine_full = EvaluationEngine::new_with_kelly(10_000.0, 1.0);
let bar1 = OHLCVBar {
timestamp: 0,
open: 100.0,
high: 100.0,
low: 100.0,
close: 100.0,
volume: 0.0,
};
engine_full.process_bar(0, &bar1, Action::Buy);
// Price drops to $90 (-10% loss)
let bar2 = OHLCVBar {
timestamp: 1,
open: 90.0,
high: 90.0,
low: 90.0,
close: 90.0,
volume: 0.0,
};
engine_full.process_bar(1, &bar2, Action::Sell);
// Full Kelly: Loss = 90 - 100 = -$10
assert_eq!(engine_full.trades.len(), 1);
assert!((engine_full.trades[0].pnl - (-10.0)).abs() < 0.01);
// Half Kelly (0.5) - reduced loss
let mut engine_half = EvaluationEngine::new_with_kelly(10_000.0, 0.5);
engine_half.process_bar(0, &bar1, Action::Buy);
engine_half.process_bar(1, &bar2, Action::Sell);
// Half Kelly: Loss = (90 - 100) * 0.5 = -$5
assert_eq!(engine_half.trades.len(), 1);
assert!((engine_half.trades[0].pnl - (-5.0)).abs() < 0.01);
}
#[test]
fn test_kelly_with_short_positions() {
// Test Kelly scaling on short positions
// Full Kelly (1.0)
let mut engine_full = EvaluationEngine::new_with_kelly(10_000.0, 1.0);
// Short at $100
let bar1 = OHLCVBar {
timestamp: 0,
open: 100.0,
high: 100.0,
low: 100.0,
close: 100.0,
volume: 0.0,
};
engine_full.process_bar(0, &bar1, Action::Sell);
// Cover at $90 (10% profit for short)
let bar2 = OHLCVBar {
timestamp: 1,
open: 90.0,
high: 90.0,
low: 90.0,
close: 90.0,
volume: 0.0,
};
engine_full.process_bar(1, &bar2, Action::Buy);
// Full Kelly: PnL = 100 - 90 = $10 (profit from price drop)
assert_eq!(engine_full.trades.len(), 1);
assert!((engine_full.trades[0].pnl - 10.0).abs() < 0.01);
// Half Kelly (0.5)
let mut engine_half = EvaluationEngine::new_with_kelly(10_000.0, 0.5);
engine_half.process_bar(0, &bar1, Action::Sell);
engine_half.process_bar(1, &bar2, Action::Buy);
// Half Kelly: PnL = (100 - 90) * 0.5 = $5
assert_eq!(engine_half.trades.len(), 1);
assert!((engine_half.trades[0].pnl - 5.0).abs() < 0.01);
}
#[test]
fn test_default_new_uses_full_kelly() {
// Verify that EvaluationEngine::new() defaults to Kelly=1.0
let mut engine_default = EvaluationEngine::new(10_000.0);
let bar1 = OHLCVBar {
timestamp: 0,
open: 100.0,
high: 100.0,
low: 100.0,
close: 100.0,
volume: 0.0,
};
engine_default.process_bar(0, &bar1, Action::Buy);
let bar2 = OHLCVBar {
timestamp: 1,
open: 110.0,
high: 110.0,
low: 110.0,
close: 110.0,
volume: 0.0,
};
engine_default.process_bar(1, &bar2, Action::Sell);
// Should behave like full Kelly (no scaling)
assert_eq!(engine_default.trades.len(), 1);
assert!((engine_default.trades[0].pnl - 10.0).abs() < 0.01);
}