//! PPO Stress Testing Framework Tests //! //! TDD Red Phase: Tests written BEFORE implementation. //! These tests will initially fail and guide the implementation. use ml::ppo::stress_testing::{ PPOStressTester, StressScenario, flash_crash_scenario, liquidity_crisis_scenario, }; use ml::hyperopt::adapters::PPOTrainer; use anyhow::Result; /// Helper to create a minimal PPO trainer for testing fn create_test_trainer() -> Result { // PPOTrainer requires a DBN data directory and episodes count // For stress testing, we use the test data directory // Tests run from the workspace root, so we need to go up one level from ml/ let data_dir = "../test_data"; let episodes = 100; PPOTrainer::new(data_dir, episodes) } #[test] fn test_flash_crash_scenario() -> Result<()> { // TDD Red Phase: This test will fail until implementation exists // Setup let trainer = create_test_trainer()?; let mut stress_tester = PPOStressTester::new(trainer); let scenario = flash_crash_scenario(); // Execute stress test let result = stress_tester.run_scenario(&scenario)?; // Validate flash crash scenario parameters assert_eq!(result.scenario_name, "Flash Crash"); assert_eq!(scenario.price_shock_pct, -10.0, "Flash crash should be -10% shock"); assert_eq!(scenario.volatility_multiplier, 3.0, "Flash crash should be 3x volatility"); assert_eq!(scenario.spread_multiplier, 10.0, "Flash crash should be 10x spreads"); assert_eq!(scenario.duration_steps, 300, "Flash crash should last 300 steps (5 min)"); // Validate robustness criteria assert!( result.max_drawdown <= scenario.max_drawdown_threshold, "Max drawdown {:.2}% exceeded threshold {:.2}%", result.max_drawdown, scenario.max_drawdown_threshold ); assert!( !result.bankruptcy, "Agent went bankrupt during flash crash (portfolio <= 0)" ); assert!( result.action_diversity >= scenario.min_action_diversity, "Action diversity {:.2}% below threshold {:.2}%", result.action_diversity, scenario.min_action_diversity ); println!("✅ Flash Crash Test: Drawdown={:.2}%, Diversity={:.2}%", result.max_drawdown, result.action_diversity); Ok(()) } #[test] fn test_liquidity_crisis_scenario() -> Result<()> { // TDD Red Phase: This test will fail until implementation exists // Setup let trainer = create_test_trainer()?; let mut stress_tester = PPOStressTester::new(trainer); let scenario = liquidity_crisis_scenario(); // Execute stress test let result = stress_tester.run_scenario(&scenario)?; // Validate liquidity crisis scenario parameters assert_eq!(result.scenario_name, "Liquidity Crisis"); assert_eq!(scenario.price_shock_pct, -2.0, "Liquidity crisis should be -2% shock"); assert_eq!(scenario.volatility_multiplier, 2.0, "Liquidity crisis should be 2x volatility"); assert_eq!(scenario.spread_multiplier, 50.0, "Liquidity crisis should be 50x spreads"); assert_eq!(scenario.duration_steps, 600, "Liquidity crisis should last 600 steps (10 min)"); // Validate agent survived extreme spread widening assert!( !result.bankruptcy, "Agent went bankrupt during liquidity crisis" ); assert!( result.max_drawdown <= scenario.max_drawdown_threshold, "Max drawdown {:.2}% exceeded threshold {:.2}%", result.max_drawdown, scenario.max_drawdown_threshold ); println!("✅ Liquidity Crisis Test: Drawdown={:.2}%, Final Portfolio={:.2}%", result.max_drawdown, result.final_portfolio_pct); Ok(()) } #[test] fn test_bankruptcy_detection() -> Result<()> { // TDD Red Phase: Test bankruptcy detection logic // Setup let trainer = create_test_trainer()?; let mut stress_tester = PPOStressTester::new(trainer); // Create extreme scenario designed to cause bankruptcy let extreme_scenario = StressScenario { name: "Extreme Bankruptcy Test".to_string(), price_shock_pct: -95.0, // 95% crash (unrealistic but tests edge case) volatility_multiplier: 10.0, spread_multiplier: 100.0, duration_steps: 100, max_drawdown_threshold: 100.0, // Accept any drawdown for this test min_action_diversity: 0.0, // No diversity requirement }; // Execute stress test let result = stress_tester.run_scenario(&extreme_scenario)?; // Validate bankruptcy was detected assert!( result.bankruptcy, "Bankruptcy should be detected when portfolio <= 0" ); assert!( !result.passed, "Test should FAIL when bankruptcy occurs" ); assert!( result.failure_reasons.iter().any(|r| r.contains("Bankruptcy")), "Failure reasons should mention bankruptcy: {:?}", result.failure_reasons ); println!("✅ Bankruptcy Detection Test: Correctly detected portfolio collapse"); Ok(()) } #[test] fn test_metrics_collection() -> Result<()> { // TDD Red Phase: Test metrics collection logic // Setup let trainer = create_test_trainer()?; let mut stress_tester = PPOStressTester::new(trainer); let scenario = flash_crash_scenario(); // Execute stress test let result = stress_tester.run_scenario(&scenario)?; // Validate all metrics are collected assert!( result.max_drawdown >= 0.0, "Max drawdown should be non-negative: {:.2}%", result.max_drawdown ); assert!( result.action_diversity >= 0.0 && result.action_diversity <= 100.0, "Action diversity should be 0-100%: {:.2}%", result.action_diversity ); assert!( result.total_trades > 0, "Should have executed trades during stress test" ); assert!( result.q_value_std >= 0.0, "Q-value standard deviation should be non-negative: {:.4}", result.q_value_std ); assert!( result.execution_time_ms > 0, "Execution time should be positive: {} ms", result.execution_time_ms ); // Validate result has scenario name assert_eq!( result.scenario_name, scenario.name, "Result should contain scenario name" ); println!("✅ Metrics Collection Test: All metrics tracked correctly"); println!(" - Max Drawdown: {:.2}%", result.max_drawdown); println!(" - Portfolio Change: {:.2}%", result.final_portfolio_pct); println!(" - Action Diversity: {:.2}%", result.action_diversity); println!(" - Total Trades: {}", result.total_trades); println!(" - Q-Value Std: {:.4}", result.q_value_std); println!(" - Execution Time: {} ms", result.execution_time_ms); Ok(()) } #[test] fn test_scenario_definitions() { // Validate all 8 predefined scenarios exist and have reasonable parameters let scenarios = vec![ flash_crash_scenario(), liquidity_crisis_scenario(), ml::ppo::stress_testing::vix_spike_scenario(), ml::ppo::stress_testing::trending_market_scenario(), ml::ppo::stress_testing::whipsaw_scenario(), ml::ppo::stress_testing::gap_risk_scenario(), ml::ppo::stress_testing::correlation_breakdown_scenario(), ml::ppo::stress_testing::multi_asset_stress_scenario(), ]; assert_eq!(scenarios.len(), 8, "Should have 8 predefined scenarios"); // Verify all scenarios have reasonable thresholds for scenario in scenarios { assert!( scenario.max_drawdown_threshold > 0.0, "Max drawdown threshold must be positive: {}", scenario.name ); assert!( scenario.min_action_diversity >= 0.0 && scenario.min_action_diversity <= 100.0, "Action diversity must be 0-100%: {}", scenario.name ); assert!( scenario.duration_steps > 0, "Duration must be positive: {}", scenario.name ); } }