//! Critical Risk Management Validation Tests //! //! These tests validate the core risk management components for production readiness, //! focusing on the critical components identified in the analysis: //! - VaR calculations (Historical, Parametric, Monte Carlo, Hybrid) //! - Kelly criterion sizing //! - Position limits and concentration risk //! - Atomic kill switch functionality //! - Stress testing scenarios //! - Regulatory compliance (MiFID II, Basel III, Dodd-Frank) use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use tokio::time::timeout; use foxhunt_core::types::prelude::*; use risk::prelude::*; use risk::{ ComplianceEngine, HistoricalPrice, KillSwitchScope, OrderInfo, OrderSide, OrderType, Portfolio, PositionInfo, Symbol, TimeInForce, }; /// Test data constants for reproducible testing const TEST_SYMBOL: &str = "EURUSD"; const TEST_ACCOUNT_ID: &str = "test_account_001"; const TEST_PORTFOLIO_ID: &str = "test_portfolio_001"; const STRESS_TEST_TIMEOUT: Duration = Duration::from_secs(30); #[tokio::test] async fn test_var_calculation_comprehensive() { let var_engine = create_test_var_engine().await; let test_positions = create_test_positions(); let historical_prices = create_test_historical_data(); // Test all VaR methodologies let result = var_engine .calculate_comprehensive_var(TEST_PORTFOLIO_ID, &test_positions, &historical_prices) .await .expect("VaR calculation should succeed"); // Validate all VaR methods produce reasonable results assert!( result.historical_var > Decimal::ZERO, "Historical VaR must be positive" ); assert!( result.parametric_var > Decimal::ZERO, "Parametric VaR must be positive" ); assert!( result.monte_carlo_var > Decimal::ZERO, "Monte Carlo VaR must be positive" ); assert!( result.hybrid_var > Decimal::ZERO, "Hybrid VaR must be positive" ); // VaR should be reasonable (between 0.1% and 10% of portfolio value) let portfolio_value = calculate_portfolio_value(&test_positions); let var_ratio = result.hybrid_var / portfolio_value; assert!( var_ratio >= Decimal::from_str("0.001").unwrap(), "VaR too low - may be miscalculated" ); assert!( var_ratio <= Decimal::from_str("0.10").unwrap(), "VaR too high - may indicate error" ); // Hybrid VaR should be within reasonable bounds of other methods let max_var = [ result.historical_var, result.parametric_var, result.monte_carlo_var, ] .iter() .max() .unwrap(); let min_var = [ result.historical_var, result.parametric_var, result.monte_carlo_var, ] .iter() .min() .unwrap(); assert!( result.hybrid_var >= *min_var, "Hybrid VaR below minimum component" ); assert!( result.hybrid_var <= *max_var, "Hybrid VaR above maximum component" ); } #[tokio::test] async fn test_kelly_criterion_sizing() { let kelly_sizer = create_test_kelly_sizer().await; let symbol = Symbol::from_str(TEST_SYMBOL).unwrap(); // Test with profitable strategy parameters let result = kelly_sizer .calculate_kelly_fraction(&symbol, "profitable_strategy") .await .expect("Kelly calculation should succeed"); // Kelly fraction should be reasonable for profitable strategy assert!( result.kelly_fraction > Decimal::ZERO, "Kelly fraction should be positive for profitable strategy" ); assert!( result.kelly_fraction <= Decimal::ONE, "Kelly fraction should not exceed 100%" ); // Fractional Kelly should be applied (typically 25% of full Kelly) assert!( result.recommended_fraction < result.kelly_fraction, "Recommended should be less than full Kelly" ); assert!( result.recommended_fraction >= result.kelly_fraction * Decimal::from_str("0.1").unwrap(), "Recommended fraction too conservative" ); // Test with losing strategy parameters let losing_result = kelly_sizer .calculate_kelly_fraction(&symbol, "losing_strategy") .await .expect("Kelly calculation should succeed for losing strategy"); assert!( losing_result.kelly_fraction <= Decimal::ZERO, "Kelly fraction should be zero or negative for losing strategy" ); assert!( losing_result.recommended_fraction == Decimal::ZERO, "No position recommended for losing strategy" ); } #[tokio::test] async fn test_position_limits_enforcement() { let risk_engine = create_test_risk_engine().await; // Test normal position within limits let normal_order = create_test_order(Decimal::from_str("10000").unwrap()); // $10k position let result = risk_engine .check_pre_trade_risk(&normal_order, TEST_ACCOUNT_ID) .await .expect("Risk check should succeed"); assert!(result.approved, "Normal position should be approved"); assert!( result.risk_warnings.is_empty(), "No warnings for normal position" ); // Test position exceeding single instrument limit let large_order = create_test_order(Decimal::from_str("1000000").unwrap()); // $1M position let large_result = risk_engine .check_pre_trade_risk(&large_order, TEST_ACCOUNT_ID) .await .expect("Risk check should succeed"); assert!(!large_result.approved, "Large position should be rejected"); assert!( large_result .risk_warnings .iter() .any(|w| w.contains("position limit")), "Should warn about position limits" ); // Test concentration risk (too much in single instrument) let concentration_order = create_concentration_test_order(); let conc_result = risk_engine .check_pre_trade_risk(&concentration_order, TEST_ACCOUNT_ID) .await .expect("Risk check should succeed"); assert!( !conc_result.approved, "Concentrated position should be rejected" ); assert!( conc_result .risk_warnings .iter() .any(|w| w.contains("concentration")), "Should warn about concentration risk" ); } #[tokio::test] async fn test_atomic_kill_switch_functionality() { let kill_switch = create_test_kill_switch().await; // Initially trading should be allowed assert!( kill_switch.is_trading_allowed(&KillSwitchScope::Global), "Trading should initially be allowed" ); assert!( kill_switch.is_trading_allowed(&KillSwitchScope::Account(TEST_ACCOUNT_ID.to_string())), "Account trading should initially be allowed" ); // Test global halt kill_switch .emergency_halt(KillSwitchScope::Global, "Test global halt") .await .expect("Global halt should succeed"); assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Global), "Global halt should prevent all trading" ); assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Account(TEST_ACCOUNT_ID.to_string())), "Global halt should prevent account trading" ); // Test kill switch performance (must be sub-microsecond) use std::time::Instant; let start = Instant::now(); for _ in 0..1000 { kill_switch.is_trading_allowed(&KillSwitchScope::Global); } let elapsed = start.elapsed(); let avg_nanos = elapsed.as_nanos() / 1000; assert!( avg_nanos < 1000, "Kill switch check must be sub-microsecond, got {}ns", avg_nanos ); // Test recovery kill_switch .resume_trading(KillSwitchScope::Global, "Test recovery") .await .expect("Trading resume should succeed"); assert!( kill_switch.is_trading_allowed(&KillSwitchScope::Global), "Trading should resume after recovery" ); } #[tokio::test] async fn test_stress_testing_scenarios() { let stress_tester = create_test_stress_tester().await; let test_portfolio = create_test_portfolio(); // Test 2008 Financial Crisis scenario let crisis_result = timeout( STRESS_TEST_TIMEOUT, stress_tester.run_stress_test("2008_crisis", &test_portfolio), ) .await .expect("Stress test should not timeout") .expect("2008 crisis stress test should succeed"); assert!( crisis_result.portfolio_loss > Decimal::ZERO, "Crisis scenario should show losses" ); assert!( crisis_result.max_drawdown > Decimal::ZERO, "Should calculate max drawdown" ); assert!( crisis_result.var_breach_probability > Decimal::ZERO, "Should show VaR breach probability" ); // Stress test loss should be significant but not total portfolio destruction let loss_ratio = crisis_result.portfolio_loss / test_portfolio.total_value; assert!( loss_ratio > Decimal::from_str("0.05").unwrap(), "Crisis should cause >5% loss" ); assert!( loss_ratio < Decimal::from_str("0.90").unwrap(), "Crisis should not destroy >90% of portfolio" ); // Test COVID-19 Flash Crash scenario let covid_result = timeout( STRESS_TEST_TIMEOUT, stress_tester.run_stress_test("covid_crash", &test_portfolio), ) .await .expect("COVID stress test should not timeout") .expect("COVID stress test should succeed"); assert!( covid_result.portfolio_loss > Decimal::ZERO, "COVID scenario should show losses" ); // Test Flash Crash scenario (high-frequency event) let flash_result = timeout( STRESS_TEST_TIMEOUT, stress_tester.run_stress_test("flash_crash", &test_portfolio), ) .await .expect("Flash crash test should not timeout") .expect("Flash crash test should succeed"); assert!( flash_result.portfolio_loss > Decimal::ZERO, "Flash crash should show losses" ); assert!( flash_result.time_to_recovery.is_some(), "Should estimate recovery time" ); } #[tokio::test] async fn test_regulatory_compliance() { let compliance_engine = create_test_compliance_engine().await; // Test MiFID II compliance let mifid_result = compliance_engine .validate_mifid_ii_compliance(TEST_ACCOUNT_ID) .await .expect("MiFID II validation should succeed"); assert!(mifid_result.is_compliant, "Should be MiFID II compliant"); assert!( mifid_result.best_execution_documented, "Best execution must be documented" ); assert!( mifid_result.client_categorization_valid, "Client categorization must be valid" ); // Test Basel III compliance let basel_result = compliance_engine .validate_basel_iii_compliance(TEST_PORTFOLIO_ID) .await .expect("Basel III validation should succeed"); assert!(basel_result.is_compliant, "Should be Basel III compliant"); assert!( basel_result.capital_adequacy_ratio > Decimal::from_str("0.08").unwrap(), "Capital adequacy ratio must exceed 8%" ); assert!( basel_result.leverage_ratio > Decimal::from_str("0.03").unwrap(), "Leverage ratio must exceed 3%" ); // Test Dodd-Frank compliance let dodd_frank_result = compliance_engine .validate_dodd_frank_compliance(TEST_ACCOUNT_ID) .await .expect("Dodd-Frank validation should succeed"); assert!( dodd_frank_result.is_compliant, "Should be Dodd-Frank compliant" ); assert!( dodd_frank_result.volcker_rule_compliant, "Must comply with Volcker rule" ); assert!( dodd_frank_result.swap_reporting_compliant, "Swap reporting must be compliant" ); } #[tokio::test] async fn test_circuit_breaker_conditions() { let risk_engine = create_test_risk_engine().await; // Simulate 2% daily loss to trigger circuit breaker let loss_order = create_loss_triggering_order(); let result = risk_engine .check_pre_trade_risk(&loss_order, TEST_ACCOUNT_ID) .await .expect("Risk check should succeed"); assert!( !result.approved, "Order triggering 2% loss should be rejected" ); assert!( result .risk_warnings .iter() .any(|w| w.contains("circuit breaker")), "Should trigger circuit breaker warning" ); // Verify kill switch is activated for account let kill_switch = risk_engine.get_kill_switch(); assert!( !kill_switch.is_trading_allowed(&KillSwitchScope::Account(TEST_ACCOUNT_ID.to_string())), "Circuit breaker should halt account trading" ); } #[tokio::test] async fn test_performance_requirements() { let risk_engine = create_test_risk_engine().await; let test_order = create_test_order(Decimal::from_str("10000").unwrap()); // Test that risk checks meet HFT latency requirements use std::time::Instant; let start = Instant::now(); // Run 1000 risk checks to get average latency for _ in 0..1000 { let _result = risk_engine .check_pre_trade_risk(&test_order, TEST_ACCOUNT_ID) .await .expect("Risk check should succeed"); } let elapsed = start.elapsed(); let avg_micros = elapsed.as_micros() / 1000; // Risk checks should be sub-50μs for HFT requirements // Note: This validates the performance concern identified in the analysis if avg_micros > 50 { eprintln!( "WARNING: Risk check latency {}μs exceeds 50μs HFT target", avg_micros ); eprintln!("This confirms the performance concern identified in the analysis"); eprintln!("VaR calculations are the likely bottleneck - consider caching or approximation"); } // At minimum, should be under 1ms for any production use assert!( avg_micros < 1000, "Risk check latency {}μs exceeds 1ms maximum", avg_micros ); } // Helper functions for test setup async fn create_test_var_engine() -> RealVaREngine { RealVaREngine::new() .await .expect("VaR engine creation should succeed") } async fn create_test_kelly_sizer() -> KellySizer { KellySizer::new() .await .expect("Kelly sizer creation should succeed") } async fn create_test_risk_engine() -> RiskEngine { RiskEngine::new() .await .expect("Risk engine creation should succeed") } async fn create_test_kill_switch() -> AtomicKillSwitch { AtomicKillSwitch::new() .await .expect("Kill switch creation should succeed") } async fn create_test_stress_tester() -> StressTester { StressTester::new() .await .expect("Stress tester creation should succeed") } async fn create_test_compliance_engine() -> ComplianceEngine { ComplianceEngine::new() .await .expect("Compliance engine creation should succeed") } fn create_test_positions() -> HashMap { let mut positions = HashMap::new(); positions.insert( Symbol::from_str(TEST_SYMBOL).unwrap(), PositionInfo { quantity: Decimal::from_str("100000").unwrap(), avg_price: Decimal::from_str("1.1050").unwrap(), market_value: Decimal::from_str("110500").unwrap(), unrealized_pnl: Decimal::from_str("500").unwrap(), }, ); positions } fn create_test_historical_data() -> HashMap> { let mut data = HashMap::new(); let symbol = Symbol::from_str(TEST_SYMBOL).unwrap(); // Create 30 days of synthetic price data with some volatility let mut prices = Vec::new(); let base_price = 1.1050; for i in 0..30 { prices.push(HistoricalPrice { symbol: symbol.clone(), price: Decimal::from_f64( base_price + (i as f64 * 0.001) + (i as f64 % 5) * 0.0005 - 0.001, ) .unwrap(), timestamp: chrono::Utc::now() - chrono::Duration::days(30 - i), volume: Decimal::from_str("1000000").unwrap(), }); } data.insert(symbol, prices); data } fn create_test_order(size: Decimal) -> OrderInfo { OrderInfo { symbol: Symbol::from_str(TEST_SYMBOL).unwrap(), side: OrderSide::Buy, quantity: size / Decimal::from_str("1.1050").unwrap(), // Convert to units order_type: OrderType::Market, price: None, time_in_force: TimeInForce::IOC, } } fn create_concentration_test_order() -> OrderInfo { // Create order that would exceed concentration limits (>20% of portfolio) OrderInfo { symbol: Symbol::from_str(TEST_SYMBOL).unwrap(), side: OrderSide::Buy, quantity: Decimal::from_str("500000").unwrap(), // Large position order_type: OrderType::Market, price: None, time_in_force: TimeInForce::IOC, } } fn create_loss_triggering_order() -> OrderInfo { // Create order that would trigger 2% daily loss circuit breaker OrderInfo { symbol: Symbol::from_str(TEST_SYMBOL).unwrap(), side: OrderSide::Sell, quantity: Decimal::from_str("200000").unwrap(), // Position that would cause 2%+ loss order_type: OrderType::Market, price: Some(Decimal::from_str("1.0800").unwrap()), // Below current market time_in_force: TimeInForce::IOC, } } fn create_test_portfolio() -> Portfolio { Portfolio { id: TEST_PORTFOLIO_ID.to_string(), total_value: Decimal::from_str("1000000").unwrap(), // $1M portfolio positions: create_test_positions(), cash_balance: Decimal::from_str("100000").unwrap(), unrealized_pnl: Decimal::from_str("2500").unwrap(), daily_pnl: Decimal::from_str("1200").unwrap(), } } fn calculate_portfolio_value(positions: &HashMap) -> Decimal { positions.values().map(|p| p.market_value).sum() }