Files
foxhunt/ml/tests/hyperopt_multi_objective_test.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

279 lines
10 KiB
Rust

//! Multi-Objective Hyperopt Tests
//!
//! Validates WAVE 11 multi-objective composite risk metrics implementation:
//! - Composite score calculation (Sortino 40%, Calmar 30%, Sharpe 20%, Omega 10%)
//! - CVaR tail risk penalty (10x if CVaR < -5%)
//! - Objective function integration
//! - Edge cases (zero metrics, extreme values, no trades)
use approx::assert_relative_eq;
/// Test composite score calculation with balanced metrics
#[test]
fn test_composite_score_balanced() {
// Simulate balanced performance metrics
let sortino = 1.5;
let calmar = 2.0;
let sharpe = 1.0;
let omega = 1.8;
// Calculate composite score (weights: 40%, 30%, 20%, 10%)
let composite_score = 0.4 * sortino + 0.3 * calmar + 0.2 * sharpe + 0.1 * omega;
// Expected: 0.4*1.5 + 0.3*2.0 + 0.2*1.0 + 0.1*1.8 = 0.6 + 0.6 + 0.2 + 0.18 = 1.58
assert_relative_eq!(composite_score, 1.58, epsilon = 0.001);
}
/// Test composite score with elite Sortino (highest weight 40%)
#[test]
fn test_composite_score_elite_sortino() {
// Elite Sortino should dominate composite score
let sortino = 3.0; // Excellent
let calmar = 1.5;
let sharpe = 0.8;
let omega = 1.2;
let composite_score = 0.4 * sortino + 0.3 * calmar + 0.2 * sharpe + 0.1 * omega;
// Expected: 0.4*3.0 + 0.3*1.5 + 0.2*0.8 + 0.1*1.2 = 1.2 + 0.45 + 0.16 + 0.12 = 1.93
assert_relative_eq!(composite_score, 1.93, epsilon = 0.001);
// Elite Sortino (3.0) should produce significantly higher composite than baseline (1.5)
let baseline_composite = 0.4 * 1.5 + 0.3 * calmar + 0.2 * sharpe + 0.1 * omega;
assert!(composite_score > baseline_composite + 0.5);
}
/// Test CVaR penalty triggers at -5% threshold
#[test]
fn test_cvar_penalty_threshold() {
// CVaR = -4% (acceptable, no penalty)
let cvar_acceptable = -0.04;
let penalty_acceptable = if cvar_acceptable < -0.05 { 10.0 } else { 0.0 };
assert_relative_eq!(penalty_acceptable, 0.0, epsilon = 0.001);
// CVaR = -5% (exactly at threshold, no penalty)
let cvar_threshold = -0.05;
let penalty_threshold = if cvar_threshold < -0.05 { 10.0 } else { 0.0 };
assert_relative_eq!(penalty_threshold, 0.0, epsilon = 0.001);
// CVaR = -5.1% (exceeds threshold, 10x penalty)
let cvar_excessive = -0.051;
let penalty_excessive = if cvar_excessive < -0.05 { 10.0 } else { 0.0 };
assert_relative_eq!(penalty_excessive, 10.0, epsilon = 0.001);
// CVaR = -10% (severe tail risk, 10x penalty)
let cvar_severe = -0.10;
let penalty_severe = if cvar_severe < -0.05 { 10.0 } else { 0.0 };
assert_relative_eq!(penalty_severe, 10.0, epsilon = 0.001);
}
/// Test objective function integration (minimize composite)
#[test]
fn test_objective_function_integration() {
// Scenario 1: Good performance, no tail risk
let sortino_1 = 2.0;
let calmar_1 = 2.5;
let sharpe_1 = 1.2;
let omega_1 = 1.6;
let cvar_1 = -0.03; // Acceptable tail risk
let composite_1 = 0.4 * sortino_1 + 0.3 * calmar_1 + 0.2 * sharpe_1 + 0.1 * omega_1;
let penalty_1 = if cvar_1 < -0.05 { 10.0 } else { 0.0 };
// Objective = -0.60 * composite + penalty (minimize)
let objective_1 = -0.60 * composite_1 + penalty_1;
// Expected: composite = 0.8 + 0.75 + 0.24 + 0.16 = 1.95, penalty = 0.0
// Objective = -0.60 * 1.95 + 0.0 = -1.17
assert_relative_eq!(composite_1, 1.95, epsilon = 0.001);
assert_relative_eq!(penalty_1, 0.0, epsilon = 0.001);
assert_relative_eq!(objective_1, -1.17, epsilon = 0.01);
// Scenario 2: Poor performance, severe tail risk
let sortino_2 = 0.5;
let calmar_2 = 0.8;
let sharpe_2 = 0.3;
let omega_2 = 0.9;
let cvar_2 = -0.08; // Severe tail risk (>5%)
let composite_2 = 0.4 * sortino_2 + 0.3 * calmar_2 + 0.2 * sharpe_2 + 0.1 * omega_2;
let penalty_2 = if cvar_2 < -0.05 { 10.0 } else { 0.0 };
let objective_2 = -0.60 * composite_2 + penalty_2;
// Expected: composite = 0.2 + 0.24 + 0.06 + 0.09 = 0.59, penalty = 10.0
// Objective = -0.60 * 0.59 + 10.0 = -0.354 + 10.0 = 9.646
assert_relative_eq!(composite_2, 0.59, epsilon = 0.001);
assert_relative_eq!(penalty_2, 10.0, epsilon = 0.001);
assert_relative_eq!(objective_2, 9.646, epsilon = 0.01);
// Scenario 1 (good performance, no tail risk) should have MUCH lower objective (better)
assert!(objective_1 < objective_2 - 10.0);
}
/// Test edge case: Zero metrics (no trades scenario)
#[test]
fn test_zero_metrics_edge_case() {
let sortino = 0.0;
let calmar = 0.0;
let sharpe = 0.0;
let omega = 0.0;
let cvar = 0.0; // No tail risk if no trades
let composite_score = 0.4 * sortino + 0.3 * calmar + 0.2 * sharpe + 0.1 * omega;
let penalty = if cvar < -0.05 { 10.0 } else { 0.0 };
let objective = -0.60 * composite_score + penalty;
assert_relative_eq!(composite_score, 0.0, epsilon = 0.001);
assert_relative_eq!(penalty, 0.0, epsilon = 0.001);
assert_relative_eq!(objective, 0.0, epsilon = 0.001);
}
/// Test edge case: Negative metrics (poor performance)
#[test]
fn test_negative_metrics() {
// Negative Sortino/Calmar/Sharpe indicate losses exceed gains
let sortino = -0.5;
let calmar = -0.3;
let sharpe = -0.2;
let omega = 0.5; // Omega can still be positive (losses less than gains)
let cvar = -0.02; // Acceptable tail risk
let composite_score = 0.4 * sortino + 0.3 * calmar + 0.2 * sharpe + 0.1 * omega;
let penalty = if cvar < -0.05 { 10.0 } else { 0.0 };
let objective = -0.60 * composite_score + penalty;
// Expected: composite = -0.2 + -0.09 + -0.04 + 0.05 = -0.28
// Objective = -0.60 * (-0.28) + 0.0 = 0.168
assert_relative_eq!(composite_score, -0.28, epsilon = 0.001);
assert_relative_eq!(penalty, 0.0, epsilon = 0.001);
assert_relative_eq!(objective, 0.168, epsilon = 0.001);
}
/// Test Sortino weight dominance (40% vs 30% Calmar)
#[test]
fn test_sortino_weight_dominance() {
// Scenario A: High Sortino, low Calmar
let sortino_a = 2.5;
let calmar_a = 1.0;
let sharpe_a = 1.0;
let omega_a = 1.5;
let composite_a = 0.4 * sortino_a + 0.3 * calmar_a + 0.2 * sharpe_a + 0.1 * omega_a;
// Scenario B: Low Sortino, high Calmar
let sortino_b = 1.0;
let calmar_b = 2.5;
let sharpe_b = 1.0;
let omega_b = 1.5;
let composite_b = 0.4 * sortino_b + 0.3 * calmar_b + 0.2 * sharpe_b + 0.1 * omega_b;
// Expected A: 0.4*2.5 + 0.3*1.0 + 0.2*1.0 + 0.1*1.5 = 1.0 + 0.3 + 0.2 + 0.15 = 1.65
// Expected B: 0.4*1.0 + 0.3*2.5 + 0.2*1.0 + 0.1*1.5 = 0.4 + 0.75 + 0.2 + 0.15 = 1.50
assert_relative_eq!(composite_a, 1.65, epsilon = 0.001);
assert_relative_eq!(composite_b, 1.50, epsilon = 0.001);
// High Sortino (40% weight) should dominate over high Calmar (30% weight)
assert!(composite_a > composite_b);
}
/// Test CVaR penalty impact on objective
#[test]
fn test_cvar_penalty_impact() {
let sortino = 1.5;
let calmar = 2.0;
let sharpe = 1.0;
let omega = 1.5;
let composite = 0.4 * sortino + 0.3 * calmar + 0.2 * sharpe + 0.1 * omega;
// Scenario 1: Acceptable tail risk (CVaR = -3%)
let cvar_ok = -0.03;
let penalty_ok = if cvar_ok < -0.05 { 10.0 } else { 0.0 };
let objective_ok = -0.60 * composite + penalty_ok;
// Scenario 2: Excessive tail risk (CVaR = -8%)
let cvar_bad = -0.08;
let penalty_bad = if cvar_bad < -0.05 { 10.0 } else { 0.0 };
let objective_bad = -0.60 * composite + penalty_bad;
// 10x penalty should massively increase objective (worse)
assert_relative_eq!(penalty_ok, 0.0, epsilon = 0.001);
assert_relative_eq!(penalty_bad, 10.0, epsilon = 0.001);
assert!(objective_bad > objective_ok + 9.0); // Penalty should dominate
}
/// Test extreme Omega ratio (upside dominance)
#[test]
fn test_extreme_omega_ratio() {
// Omega ratio > 3.0 indicates strong upside dominance
let sortino = 1.5;
let calmar = 2.0;
let sharpe = 1.0;
let omega = 4.0; // Extreme upside
let composite = 0.4 * sortino + 0.3 * calmar + 0.2 * sharpe + 0.1 * omega;
// Expected: 0.4*1.5 + 0.3*2.0 + 0.2*1.0 + 0.1*4.0 = 0.6 + 0.6 + 0.2 + 0.4 = 1.8
assert_relative_eq!(composite, 1.8, epsilon = 0.001);
// Compare to baseline Omega = 1.5
let composite_baseline = 0.4 * sortino + 0.3 * calmar + 0.2 * sharpe + 0.1 * 1.5;
assert_relative_eq!(composite_baseline, 1.55, epsilon = 0.001);
// Extreme Omega should improve composite by 0.25 (10% weight * 2.5 delta)
assert_relative_eq!(composite - composite_baseline, 0.25, epsilon = 0.001);
}
/// Test complete objective function (all components)
#[test]
fn test_complete_objective_function() {
// Simulate realistic hyperopt trial metrics
let sortino = 1.8;
let calmar = 2.2;
let sharpe = 1.1;
let omega = 1.6;
let cvar = -0.04; // Acceptable tail risk
let buy_pct = 35.0;
let sell_pct = 25.0;
let hold_pct = 40.0;
let gradient_norm = 5.0; // Low gradient norm (stable)
let q_value_std = 10.0; // Low Q-value volatility (stable)
// Component 1: Composite score (60% weight)
let composite_score = 0.4 * sortino + 0.3 * calmar + 0.2 * sharpe + 0.1 * omega;
let cvar_penalty = if cvar < -0.05 { 10.0 } else { 0.0 };
// Component 2: HFT activity score (25% weight)
// Simplified activity score: reward BUY+SELL ratio
let buy_sell_ratio = (buy_pct + sell_pct) / (hold_pct + 1e-6);
let hft_activity = 2.0 * (buy_sell_ratio / 3.0).min(1.0); // Cap at 3:1 ratio
// Component 3: Stability penalty (15% weight)
// Simplified: penalize high gradient norm and Q-value std
let stability_penalty = if gradient_norm > 50.0 || q_value_std > 100.0 {
5.0
} else {
0.0
};
// Final objective (minimize)
let objective = -0.60 * composite_score + cvar_penalty + -0.25 * hft_activity + 0.15 * stability_penalty;
// Expected values:
// composite_score = 0.4*1.8 + 0.3*2.2 + 0.2*1.1 + 0.1*1.6 = 0.72 + 0.66 + 0.22 + 0.16 = 1.76
// cvar_penalty = 0.0
// hft_activity = 2.0 * ((60/40)/3.0).min(1.0) = 2.0 * 0.5 = 1.0
// stability_penalty = 0.0
// objective = -0.60*1.76 + 0.0 + -0.25*1.0 + 0.15*0.0 = -1.056 + -0.25 = -1.306
assert_relative_eq!(composite_score, 1.76, epsilon = 0.001);
assert_relative_eq!(cvar_penalty, 0.0, epsilon = 0.001);
assert_relative_eq!(hft_activity, 1.0, epsilon = 0.001);
assert_relative_eq!(stability_penalty, 0.0, epsilon = 0.001);
assert_relative_eq!(objective, -1.306, epsilon = 0.01);
}