#![allow( clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_markdown, clippy::empty_line_after_doc_comments, clippy::field_reassign_with_default, clippy::get_unwrap, clippy::identity_op, clippy::inconsistent_digit_grouping, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_underscore_must_use, clippy::manual_div_ceil, clippy::manual_let_else, clippy::manual_range_contains, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::non_ascii_literal, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::too_many_lines, clippy::unnecessary_wraps, clippy::unseparated_literal_suffix, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, clippy::else_if_without_else, clippy::expect_used, clippy::missing_const_for_fn, clippy::similar_names, clippy::type_complexity, clippy::collapsible_else_if, clippy::doc_lazy_continuation, clippy::items_after_test_module, clippy::map_clone, clippy::multiple_unsafe_ops_per_block, clippy::unwrap_or_default, clippy::assign_op_pattern, clippy::needless_borrow, clippy::println_empty_string, clippy::unnecessary_cast, clippy::used_underscore_binding, clippy::create_dir, clippy::implicit_saturating_sub, clippy::exit, clippy::expect_fun_call, clippy::too_many_arguments, clippy::unnecessary_map_or, clippy::unwrap_used, dead_code, unused_imports, unused_variables, clippy::cloned_ref_to_slice_refs, clippy::neg_multiply, clippy::while_let_loop, clippy::bool_assert_comparison, clippy::excessive_precision, clippy::trivially_copy_pass_by_ref, clippy::op_ref, clippy::redundant_closure, clippy::unnecessary_lazy_evaluations, clippy::if_then_some_else_none, clippy::unnecessary_to_owned, clippy::single_component_path_imports, )] //! 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; use tracing::{info, warn}; /// 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 ); info!(max_drawdown = %format!("{:.2}", result.max_drawdown), action_diversity = %format!("{:.2}", result.action_diversity), "Flash Crash Test passed"); 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 ); info!(max_drawdown = %format!("{:.2}", result.max_drawdown), final_portfolio_pct = %format!("{:.2}", result.final_portfolio_pct), "Liquidity Crisis Test passed"); 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 ); info!("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" ); info!("Metrics Collection Test: All metrics tracked correctly"); info!(max_drawdown = %format!("{:.2}", result.max_drawdown), "Max Drawdown (%)"); info!(final_portfolio_pct = %format!("{:.2}", result.final_portfolio_pct), "Portfolio Change (%)"); info!(action_diversity = %format!("{:.2}", result.action_diversity), "Action Diversity (%)"); info!(total_trades = result.total_trades, "Total Trades"); info!(q_value_std = %format!("{:.4}", result.q_value_std), "Q-Value Std"); info!(execution_time_ms = result.execution_time_ms, "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 ); } }