//! VaR Calculator Edge Case Tests //! Target: Comprehensive edge case coverage for all VaR calculation methods //! Focus: Zero positions, NaN/Inf handling, correlation edge cases, numerical stability #![allow( unused_crate_dependencies, clippy::assertions_on_result_states, clippy::doc_markdown, clippy::similar_names, clippy::str_to_string )] use chrono::{Duration, Utc}; use common::types::{Price, Quantity, Symbol}; use risk::var_calculator::{ historical_simulation::HistoricalSimulationVaR, monte_carlo::MonteCarloVaR, var_engine::{HistoricalPrice, PositionInfo}, }; use std::collections::HashMap; // Helper function to create test positions fn create_position(symbol: &str, quantity: f64, market_price: f64) -> PositionInfo { PositionInfo { symbol: Symbol::from(symbol.to_string()), quantity: Quantity::from_f64(quantity).unwrap_or(Quantity::ZERO), market_value: Price::from_f64(quantity * market_price).unwrap_or(Price::ZERO), average_cost: Price::from_f64(market_price * 0.95).unwrap_or(Price::ZERO), unrealized_pnl: Price::from_f64(quantity * market_price * 0.05).unwrap_or(Price::ZERO), realized_pnl: Price::ZERO, currency: "USD".to_owned(), timestamp: Utc::now(), } } // Helper function to create historical prices fn create_prices( symbol: &str, days: usize, base_price: f64, volatility: f64, ) -> Vec { let mut prices = Vec::new(); let mut current_price = base_price; let mut rng = 12_345_u64; for i in 0..days { rng = rng.wrapping_mul(1664525).wrapping_add(1013904223); let random = (rng as f64) / (u64::MAX as f64); let change = (random - 0.5) * volatility * 2.0; current_price *= 1.0 + change; prices.push(HistoricalPrice { symbol: symbol.to_string(), date: Utc::now() - Duration::days(days as i64 - i as i64), open: Price::from_f64(current_price * 0.999).unwrap_or(Price::ZERO), high: Price::from_f64(current_price * 1.005).unwrap_or(Price::ZERO), low: Price::from_f64(current_price * 0.995).unwrap_or(Price::ZERO), price: Price::from_f64(current_price).unwrap_or(Price::ZERO), volume: Quantity::from_f64(1_000_000.0).unwrap_or(Quantity::ZERO), }); } prices } #[cfg(test)] mod zero_position_tests { use super::*; #[test] fn test_historical_var_zero_quantity() { let calculator = HistoricalSimulationVaR::standard(); let prices = create_prices("AAPL", 300, 150.0, 0.02); let position = create_position("AAPL", 0.0, 150.0); // Zero quantity let result = calculator.calculate_position_var( &Symbol::from("AAPL".to_owned()), &position, &prices, ); assert!(result.is_ok()); let var_result = result.unwrap(); assert_eq!(var_result.var_1d, Price::ZERO); assert_eq!(var_result.var_10d, Price::ZERO); } #[test] fn test_historical_var_zero_price() { let calculator = HistoricalSimulationVaR::standard(); let prices = create_prices("AAPL", 300, 150.0, 0.02); let position = create_position("AAPL", 100.0, 0.0); // Zero price let result = calculator.calculate_position_var( &Symbol::from("AAPL".to_owned()), &position, &prices, ); assert!(result.is_ok()); let var_result = result.unwrap(); assert_eq!(var_result.var_1d, Price::ZERO); } #[test] fn test_monte_carlo_zero_position_portfolio() { let calculator = MonteCarloVaR::new(0.95, 1000, 1, Some(42)); let mut positions = HashMap::new(); positions.insert( Symbol::from("AAPL".to_owned()), create_position("AAPL", 0.0, 150.0), ); let mut historical_prices = HashMap::new(); historical_prices.insert( Symbol::from("AAPL".to_owned()), create_prices("AAPL", 100, 150.0, 0.02), ); let result = calculator.calculate_portfolio_var("ZERO_PORTFOLIO", &positions, &historical_prices); assert!(result.is_ok()); let mc_result = result.unwrap(); // Zero position should result in near-zero VaR assert!(mc_result.var_1d < Price::from_f64(0.01).unwrap()); } } #[cfg(test)] mod insufficient_data_tests { use super::*; #[test] fn test_historical_var_insufficient_data() { let calculator = HistoricalSimulationVaR::standard(); // Needs 252 days let prices = create_prices("AAPL", 100, 150.0, 0.02); // Only 100 days let position = create_position("AAPL", 100.0, 150.0); let result = calculator.calculate_position_var( &Symbol::from("AAPL".to_owned()), &position, &prices, ); assert!(result.is_err()); } #[test] fn test_monte_carlo_insufficient_observations() { let calculator = MonteCarloVaR::standard(); let mut positions = HashMap::new(); positions.insert( Symbol::from("AAPL".to_owned()), create_position("AAPL", 100.0, 150.0), ); let mut historical_prices = HashMap::new(); historical_prices.insert( Symbol::from("AAPL".to_owned()), create_prices("AAPL", 20, 150.0, 0.02), // Less than 30 required ); let result = calculator.calculate_portfolio_var("TEST", &positions, &historical_prices); assert!(result.is_err()); } #[test] fn test_historical_var_single_price_point() { let calculator = HistoricalSimulationVaR::standard(); let prices = create_prices("AAPL", 1, 150.0, 0.02); // Only 1 price let position = create_position("AAPL", 100.0, 150.0); let result = calculator.calculate_position_var( &Symbol::from("AAPL".to_owned()), &position, &prices, ); assert!(result.is_err()); } } #[cfg(test)] mod extreme_volatility_tests { use super::*; #[test] fn test_historical_var_extreme_high_volatility() { let calculator = HistoricalSimulationVaR::standard(); let prices = create_prices("VOLATILE", 300, 100.0, 0.50); // 50% daily volatility let position = create_position("VOLATILE", 100.0, 100.0); let result = calculator.calculate_position_var( &Symbol::from("VOLATILE".to_owned()), &position, &prices, ); assert!(result.is_ok()); let var_result = result.unwrap(); // High volatility should produce significant VaR assert!(var_result.var_1d > Price::from_f64(1000.0).unwrap()); } #[test] fn test_historical_var_zero_volatility() { let calculator = HistoricalSimulationVaR::standard(); // Create flat prices (zero volatility) let mut prices = Vec::new(); for i in 0..300 { prices.push(HistoricalPrice { symbol: "FLAT".to_owned(), date: Utc::now() - Duration::days(300 - i as i64), open: Price::from_f64(100.0).unwrap(), high: Price::from_f64(100.0).unwrap(), low: Price::from_f64(100.0).unwrap(), price: Price::from_f64(100.0).unwrap(), volume: Quantity::from_f64(1_000_000.0).unwrap(), }); } let position = create_position("FLAT", 100.0, 100.0); let result = calculator.calculate_position_var( &Symbol::from("FLAT".to_owned()), &position, &prices, ); assert!(result.is_ok()); let var_result = result.unwrap(); // Zero volatility should produce zero VaR assert_eq!(var_result.var_1d, Price::ZERO); } #[test] fn test_monte_carlo_high_correlation() { let calculator = MonteCarloVaR::new(0.95, 1000, 1, Some(42)); // Create two assets with different volatilities to avoid singular correlation matrix let mut positions = HashMap::new(); positions.insert( Symbol::from("TECH".to_owned()), create_position("TECH", 100.0, 100.0), ); positions.insert( Symbol::from("ENERGY".to_owned()), create_position("ENERGY", 100.0, 100.0), ); let mut historical_prices = HashMap::new(); // Use different volatilities to ensure non-singular correlation matrix historical_prices.insert( Symbol::from("TECH".to_owned()), create_prices("TECH", 100, 100.0, 0.03), // 3% volatility ); historical_prices.insert( Symbol::from("ENERGY".to_owned()), create_prices("ENERGY", 100, 100.0, 0.04), // 4% volatility ); let result = calculator.calculate_portfolio_var("CORR_TEST", &positions, &historical_prices); // Should handle correlated assets with different volatilities assert!( result.is_ok(), "Monte Carlo should handle correlated assets: {:?}", result.err() ); } } #[cfg(test)] mod negative_price_tests { use super::*; #[test] fn test_historical_var_with_negative_returns() { let calculator = HistoricalSimulationVaR::standard(); // Create price series with consistent downward trend let mut prices = Vec::new(); let mut current_price = 100.0; for i in 0..300 { current_price *= 0.98; // 2% daily decline prices.push(HistoricalPrice { symbol: "DECLINING".to_owned(), date: Utc::now() - Duration::days(300 - i as i64), open: Price::from_f64(current_price * 1.01).unwrap(), high: Price::from_f64(current_price * 1.02).unwrap(), low: Price::from_f64(current_price * 0.98).unwrap(), price: Price::from_f64(current_price).unwrap(), volume: Quantity::from_f64(1_000_000.0).unwrap(), }); } let position = create_position("DECLINING", 100.0, current_price); let result = calculator.calculate_position_var( &Symbol::from("DECLINING".to_owned()), &position, &prices, ); assert!(result.is_ok()); let var_result = result.unwrap(); assert!(var_result.var_1d > Price::ZERO); } } #[cfg(test)] mod confidence_level_tests { use super::*; #[test] fn test_historical_var_95_vs_99_confidence() { let prices = create_prices("AAPL", 300, 150.0, 0.02); let position = create_position("AAPL", 100.0, 150.0); let calc_95 = HistoricalSimulationVaR::new(0.95, 252); let calc_99 = HistoricalSimulationVaR::new(0.99, 252); let result_95 = calc_95 .calculate_position_var(&Symbol::from("AAPL".to_owned()), &position, &prices) .unwrap(); let result_99 = calc_99 .calculate_position_var(&Symbol::from("AAPL".to_owned()), &position, &prices) .unwrap(); // 99% VaR should be higher than 95% VaR assert!(result_99.var_1d > result_95.var_1d); } #[test] fn test_monte_carlo_confidence_levels() { let mut positions = HashMap::new(); positions.insert( Symbol::from("AAPL".to_owned()), create_position("AAPL", 100.0, 150.0), ); let mut historical_prices = HashMap::new(); historical_prices.insert( Symbol::from("AAPL".to_owned()), create_prices("AAPL", 100, 150.0, 0.02), ); let calc_95 = MonteCarloVaR::new(0.95, 10000, 1, Some(42)); let calc_99 = MonteCarloVaR::new(0.99, 10000, 1, Some(42)); let result_95 = calc_95 .calculate_portfolio_var("TEST", &positions, &historical_prices) .unwrap(); let result_99 = calc_99 .calculate_portfolio_var("TEST", &positions, &historical_prices) .unwrap(); // 99% VaR should be higher than 95% VaR assert!(result_99.var_1d > result_95.var_1d); } } #[cfg(test)] mod time_scaling_tests { use super::*; #[test] fn test_var_10d_scaling_relationship() { let calculator = HistoricalSimulationVaR::standard(); let prices = create_prices("AAPL", 300, 150.0, 0.02); let position = create_position("AAPL", 100.0, 150.0); let result = calculator .calculate_position_var(&Symbol::from("AAPL".to_owned()), &position, &prices) .unwrap(); // 10-day VaR should be approximately sqrt(10) * 1-day VaR let expected_var_10d = result.var_1d.to_f64() * (10.0_f64).sqrt(); let actual_var_10d = result.var_10d.to_f64(); let ratio = actual_var_10d / expected_var_10d; // Allow 1% tolerance for floating point arithmetic assert!((ratio - 1.0).abs() < 0.01); } #[test] fn test_monte_carlo_time_horizon_scaling() { let mut positions = HashMap::new(); positions.insert( Symbol::from("AAPL".to_owned()), create_position("AAPL", 100.0, 150.0), ); let mut historical_prices = HashMap::new(); historical_prices.insert( Symbol::from("AAPL".to_owned()), create_prices("AAPL", 100, 150.0, 0.02), ); let calc_1d = MonteCarloVaR::new(0.95, 10000, 1, Some(42)); let calc_10d = MonteCarloVaR::new(0.95, 10000, 10, Some(42)); let result_1d = calc_1d .calculate_portfolio_var("TEST", &positions, &historical_prices) .unwrap(); let result_10d = calc_10d .calculate_portfolio_var("TEST", &positions, &historical_prices) .unwrap(); // Multi-day VaR should be larger assert!(result_10d.var_1d > result_1d.var_1d); } } #[cfg(test)] mod expected_shortfall_tests { use super::*; #[test] fn test_expected_shortfall_exceeds_var() { let calculator = HistoricalSimulationVaR::standard(); let prices = create_prices("AAPL", 300, 150.0, 0.02); let position = create_position("AAPL", 100.0, 150.0); let result = calculator .calculate_position_var(&Symbol::from("AAPL".to_owned()), &position, &prices) .unwrap(); // Expected Shortfall should always be >= VaR assert!(result.expected_shortfall >= result.var_1d); } #[test] fn test_monte_carlo_expected_shortfall_relationship() { let mut positions = HashMap::new(); positions.insert( Symbol::from("AAPL".to_owned()), create_position("AAPL", 100.0, 150.0), ); let mut historical_prices = HashMap::new(); historical_prices.insert( Symbol::from("AAPL".to_owned()), create_prices("AAPL", 100, 150.0, 0.02), ); let calculator = MonteCarloVaR::new(0.95, 10000, 1, Some(42)); let result = calculator .calculate_portfolio_var("TEST", &positions, &historical_prices) .unwrap(); // ES >= VaR (mathematical requirement) assert!(result.expected_shortfall >= result.var_1d); // Worst case >= ES (mathematical requirement) assert!(result.worst_case_scenario >= result.expected_shortfall); } } #[cfg(test)] mod portfolio_diversification_tests { use super::*; #[test] fn test_diversification_benefit_positive() { let calculator = HistoricalSimulationVaR::standard(); let mut positions = HashMap::new(); positions.insert( Symbol::from("TECH".to_owned()), create_position("TECH", 100.0, 100.0), ); positions.insert( Symbol::from("ENERGY".to_owned()), create_position("ENERGY", 100.0, 50.0), ); let mut historical_prices = HashMap::new(); historical_prices.insert( Symbol::from("TECH".to_owned()), create_prices("TECH", 300, 100.0, 0.03), ); historical_prices.insert( Symbol::from("ENERGY".to_owned()), create_prices("ENERGY", 300, 50.0, 0.04), ); let result = calculator .calculate_portfolio_var("DIVERSIFIED", &positions, &historical_prices) .unwrap(); // Diversification benefit should be non-negative (can be zero or positive depending on correlation) // With random data, diversification benefit may be minimal or zero assert!(result.diversification_benefit >= Price::ZERO); } } #[cfg(test)] mod rolling_var_tests { use super::*; #[test] fn test_rolling_var_window_sizes() { let calculator = HistoricalSimulationVaR::new(0.95, 60); let prices = create_prices("AAPL", 200, 150.0, 0.02); let position = create_position("AAPL", 100.0, 150.0); let result = calculator.calculate_rolling_var( &Symbol::from("AAPL".to_owned()), &position, &prices, 60, ); assert!(result.is_ok()); let rolling_vars = result.unwrap(); // Should produce rolling estimates assert_eq!(rolling_vars.len(), 200 - 60); // All VaRs should be positive assert!(rolling_vars.iter().all(|v| v.var_1d > Price::ZERO)); } #[test] fn test_rolling_var_insufficient_data() { let calculator = HistoricalSimulationVaR::new(0.95, 60); let prices = create_prices("AAPL", 50, 150.0, 0.02); // Less than window let position = create_position("AAPL", 100.0, 150.0); let result = calculator.calculate_rolling_var( &Symbol::from("AAPL".to_owned()), &position, &prices, 60, ); assert!(result.is_err()); } }