#![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, )] /// WAVE 16S-P1: Price Validation Test Suite /// /// Tests the production-grade price validation logic that prevents catastrophic /// portfolio values from corrupted data (e.g., the -$1.93B bug from $1.11 price). use ml::dqn::action_space::{ExposureLevel, FactoredAction, OrderType, Urgency}; use ml::dqn::portfolio_tracker::PortfolioTracker; #[test] fn test_price_validation_rejects_nan() { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // Attempt to execute with NaN price tracker.execute_action(action, f32::NAN, 100.0); // Portfolio should be unchanged (action rejected) assert_eq!(tracker.current_position(), 0.0, "NaN price should be rejected"); assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should be unchanged"); } #[test] fn test_price_validation_rejects_infinity() { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // Attempt to execute with Inf price tracker.execute_action(action, f32::INFINITY, 100.0); // Portfolio should be unchanged (action rejected) assert_eq!(tracker.current_position(), 0.0, "Infinity price should be rejected"); assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should be unchanged"); } #[test] fn test_price_validation_rejects_zero() { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // Attempt to execute with zero price tracker.execute_action(action, 0.0, 100.0); // Portfolio should be unchanged (action rejected) assert_eq!(tracker.current_position(), 0.0, "Zero price should be rejected"); assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should be unchanged"); } #[test] fn test_price_validation_rejects_negative() { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // Attempt to execute with negative price tracker.execute_action(action, -100.0, 100.0); // Portfolio should be unchanged (action rejected) assert_eq!(tracker.current_position(), 0.0, "Negative price should be rejected"); assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should be unchanged"); } #[test] fn test_price_validation_rejects_corrupted_price_1_11() { // This is the exact bug that caused -$1.93B portfolio value let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // Attempt to execute with corrupted price $1.11 (instead of ~$5000 for ES) tracker.execute_action(action, 1.11, 100.0); // Portfolio should be unchanged (action rejected) assert_eq!(tracker.current_position(), 0.0, "Price $1.11 should be rejected (< $1000 ES min)"); assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should be unchanged"); } #[test] fn test_price_validation_rejects_below_es_min() { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // Attempt to execute with price below ES minimum ($1000) tracker.execute_action(action, 999.99, 100.0); // Portfolio should be unchanged (action rejected) assert_eq!(tracker.current_position(), 0.0, "Price $999.99 should be rejected (< $1000 ES min)"); assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should be unchanged"); } #[test] fn test_price_validation_rejects_above_es_max() { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // Attempt to execute with price above ES maximum ($10,000) tracker.execute_action(action, 10_000.01, 100.0); // Portfolio should be unchanged (action rejected) assert_eq!(tracker.current_position(), 0.0, "Price $10,000.01 should be rejected (> $10K ES max)"); assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should be unchanged"); } #[test] fn test_price_validation_accepts_valid_es_price() { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal); // Execute with valid ES price (~$5000) tracker.execute_action(action, 5000.0, 100.0); // Portfolio should be updated (action accepted) // Long50 = 0.5 exposure, so position = 0.5 * 100 = 50 contracts assert_eq!(tracker.current_position(), 50.0, "Valid price $5000 should be accepted"); // Cash should be reduced by position value minus transaction costs // 50 contracts * $5000 = $250,000 notional, but position is clamped to ±200 max // target = 0.5 * 100 = 50 contracts (within ±200 limit) // Cash change: 50 * 5000 = 250,000 (but we only have $10K capital) // This will show up as negative cash (leveraged position) assert!(tracker.cash_balance() < 10_000.0, "Cash should be reduced after buying"); } #[test] fn test_price_validation_accepts_boundary_prices() { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal); // Test exact minimum boundary ($1000.00) tracker.execute_action(action, 1000.0, 100.0); assert_eq!(tracker.current_position(), 0.0, "Min boundary $1000 should be accepted (Flat position)"); tracker.reset(); // Test exact maximum boundary ($10,000.00) tracker.execute_action(action, 10_000.0, 100.0); assert_eq!(tracker.current_position(), 0.0, "Max boundary $10,000 should be accepted (Flat position)"); } #[test] fn test_price_validation_continuity_warning() { // This test verifies that large price jumps (>50%) are logged but allowed let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal); // First action: establish baseline price tracker.execute_action(action, 5000.0, 100.0); // Second action: price jumps 60% (from $5000 to $8000) // This should trigger WARNING but still be accepted (legitimate flash crashes can happen) tracker.execute_action(action, 8000.0, 100.0); // Portfolio should be updated (action accepted despite warning) // Flat position, so position should still be 0 assert_eq!(tracker.current_position(), 0.0, "Large price jump should be allowed with warning"); } #[test] fn test_price_validation_multiple_rejections() { // Verify that multiple rejected actions don't corrupt portfolio state let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // Attempt multiple invalid prices tracker.execute_action(action, f32::NAN, 100.0); tracker.execute_action(action, -50.0, 100.0); tracker.execute_action(action, 0.5, 100.0); tracker.execute_action(action, 15_000.0, 100.0); // Portfolio should remain pristine assert_eq!(tracker.current_position(), 0.0, "Multiple rejections should leave portfolio unchanged"); assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should be unchanged after multiple rejections"); // Now execute a valid action to verify tracker still works tracker.execute_action(action, 5000.0, 100.0); assert_eq!(tracker.current_position(), 100.0, "Valid action should work after rejections"); }