- Remove unnecessary unsqueeze(1) in FlowPolicy log_prob and entropy (shapes should be [batch_size], not [batch_size, 1]) - Update flow_policy tests to expect correct [batch_size] shape - Simplify kelly position sizing test with helper function - Clean up example files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
124 lines
6.0 KiB
Rust
124 lines
6.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::OHLCVBarF32;
|
|
|
|
/// Compute expected net PnL after transaction costs (0.15% per side).
|
|
/// This mirrors EvaluationEngine::close_position().
|
|
fn expected_net_pnl(entry: f32, exit: f32, kelly: f64, is_long: bool) -> f32 {
|
|
let base_pnl = if is_long { exit - entry } else { entry - exit };
|
|
let gross = base_pnl * kelly as f32;
|
|
let fee: f32 = 0.0015;
|
|
let entry_cost = entry * kelly as f32 * fee;
|
|
let exit_cost = exit * kelly as f32 * fee;
|
|
gross - entry_cost - exit_cost
|
|
}
|
|
|
|
#[test]
|
|
fn test_kelly_scales_pnl_correctly() {
|
|
let bar1 = OHLCVBarF32 { timestamp: 0, open: 100.0, high: 100.0, low: 100.0, close: 100.0, volume: 0.0 };
|
|
let bar2 = OHLCVBarF32 { timestamp: 1, open: 110.0, high: 110.0, low: 110.0, close: 110.0, volume: 0.0 };
|
|
|
|
// Test 1: Full Kelly (1.0) - baseline
|
|
let mut engine_full = EvaluationEngine::new_with_kelly(10_000.0, 1.0);
|
|
engine_full.process_bar(0, &bar1, Action::Buy);
|
|
engine_full.process_bar(1, &bar2, Action::Sell);
|
|
assert_eq!(engine_full.trades.len(), 1);
|
|
let expected = expected_net_pnl(100.0, 110.0, 1.0, true);
|
|
assert!((engine_full.trades[0].pnl - expected).abs() < 0.01,
|
|
"Full Kelly: expected {:.4}, got {:.4}", expected, engine_full.trades[0].pnl);
|
|
|
|
// 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);
|
|
assert_eq!(engine_half.trades.len(), 1);
|
|
let expected = expected_net_pnl(100.0, 110.0, 0.5, true);
|
|
assert!((engine_half.trades[0].pnl - expected).abs() < 0.01,
|
|
"Half Kelly: expected {:.4}, got {:.4}", expected, engine_half.trades[0].pnl);
|
|
|
|
// 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);
|
|
assert_eq!(engine_quarter.trades.len(), 1);
|
|
let expected = expected_net_pnl(100.0, 110.0, 0.25, true);
|
|
assert!((engine_quarter.trades[0].pnl - expected).abs() < 0.01,
|
|
"Quarter Kelly: expected {:.4}, got {:.4}", expected, engine_quarter.trades[0].pnl);
|
|
|
|
// Verify proportional scaling: half Kelly PnL ~= full Kelly PnL * 0.5
|
|
let ratio = engine_half.trades[0].pnl / engine_full.trades[0].pnl;
|
|
assert!((ratio - 0.5).abs() < 0.01, "Half/Full ratio should be ~0.5, got {:.4}", ratio);
|
|
}
|
|
|
|
#[test]
|
|
fn test_kelly_scales_losses_correctly() {
|
|
let bar1 = OHLCVBarF32 { timestamp: 0, open: 100.0, high: 100.0, low: 100.0, close: 100.0, volume: 0.0 };
|
|
let bar2 = OHLCVBarF32 { timestamp: 1, open: 90.0, high: 90.0, low: 90.0, close: 90.0, volume: 0.0 };
|
|
|
|
// Full Kelly (1.0) - loss
|
|
let mut engine_full = EvaluationEngine::new_with_kelly(10_000.0, 1.0);
|
|
engine_full.process_bar(0, &bar1, Action::Buy);
|
|
engine_full.process_bar(1, &bar2, Action::Sell);
|
|
assert_eq!(engine_full.trades.len(), 1);
|
|
let expected = expected_net_pnl(100.0, 90.0, 1.0, true);
|
|
assert!((engine_full.trades[0].pnl - expected).abs() < 0.01,
|
|
"Full Kelly loss: expected {:.4}, got {:.4}", expected, engine_full.trades[0].pnl);
|
|
|
|
// 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);
|
|
assert_eq!(engine_half.trades.len(), 1);
|
|
let expected = expected_net_pnl(100.0, 90.0, 0.5, true);
|
|
assert!((engine_half.trades[0].pnl - expected).abs() < 0.01,
|
|
"Half Kelly loss: expected {:.4}, got {:.4}", expected, engine_half.trades[0].pnl);
|
|
}
|
|
|
|
#[test]
|
|
fn test_kelly_with_short_positions() {
|
|
let bar1 = OHLCVBarF32 { timestamp: 0, open: 100.0, high: 100.0, low: 100.0, close: 100.0, volume: 0.0 };
|
|
let bar2 = OHLCVBarF32 { timestamp: 1, open: 90.0, high: 90.0, low: 90.0, close: 90.0, volume: 0.0 };
|
|
|
|
// Full Kelly (1.0) - short profit
|
|
let mut engine_full = EvaluationEngine::new_with_kelly(10_000.0, 1.0);
|
|
engine_full.process_bar(0, &bar1, Action::Sell);
|
|
engine_full.process_bar(1, &bar2, Action::Buy);
|
|
assert_eq!(engine_full.trades.len(), 1);
|
|
let expected = expected_net_pnl(100.0, 90.0, 1.0, false);
|
|
assert!((engine_full.trades[0].pnl - expected).abs() < 0.01,
|
|
"Full Kelly short: expected {:.4}, got {:.4}", expected, engine_full.trades[0].pnl);
|
|
|
|
// 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);
|
|
assert_eq!(engine_half.trades.len(), 1);
|
|
let expected = expected_net_pnl(100.0, 90.0, 0.5, false);
|
|
assert!((engine_half.trades[0].pnl - expected).abs() < 0.01,
|
|
"Half Kelly short: expected {:.4}, got {:.4}", expected, engine_half.trades[0].pnl);
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_new_uses_full_kelly() {
|
|
let bar1 = OHLCVBarF32 { timestamp: 0, open: 100.0, high: 100.0, low: 100.0, close: 100.0, volume: 0.0 };
|
|
let bar2 = OHLCVBarF32 { timestamp: 1, open: 110.0, high: 110.0, low: 110.0, close: 110.0, volume: 0.0 };
|
|
|
|
// Verify that EvaluationEngine::new() defaults to Kelly=1.0
|
|
let mut engine_default = EvaluationEngine::new(10_000.0);
|
|
engine_default.process_bar(0, &bar1, Action::Buy);
|
|
engine_default.process_bar(1, &bar2, Action::Sell);
|
|
assert_eq!(engine_default.trades.len(), 1);
|
|
|
|
// Compare with explicit Kelly=1.0
|
|
let mut engine_explicit = EvaluationEngine::new_with_kelly(10_000.0, 1.0);
|
|
engine_explicit.process_bar(0, &bar1, Action::Buy);
|
|
engine_explicit.process_bar(1, &bar2, Action::Sell);
|
|
|
|
assert!((engine_default.trades[0].pnl - engine_explicit.trades[0].pnl).abs() < 0.001,
|
|
"Default should match explicit Kelly=1.0");
|
|
}
|