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>
173 lines
5.0 KiB
Rust
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);
|
|
}
|