#![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 16N Integration Tests: P&L Realism //! //! Property tests ensuring P&L values remain realistic throughout training, //! preventing catastrophic explosions like the Wave 16M $640 trillion bug. //! //! # Test Coverage //! - P&L values within realistic bounds (<$10,000 for validation) //! - Return percentages reasonable (±10% max for short validation runs) //! - P&L matches position changes (accounting validation) //! - Transaction costs correctly reduce net P&L //! - P&L accumulation is consistent across epochs //! //! # Regression Prevention //! - Detects P&L explosions (e.g., $640T from preprocessed prices) //! - Validates accounting consistency (position changes → P&L) //! - Ensures transaction costs are applied correctly #![allow(unused_crate_dependencies)] use anyhow::Result; use ml::dqn::action_space::{ExposureLevel, FactoredAction, OrderType, Urgency}; use ml::dqn::portfolio_tracker::PortfolioTracker; use tracing::{info, warn}; // ============================================================================ // Test 1: P&L in Realistic Range // ============================================================================ #[test] fn test_pnl_in_realistic_range() -> Result<()> { // For a 1-epoch validation run with $10,000 initial capital: // Expected P&L: <$10,000 (100% return is extreme but theoretically possible) // BUG DETECTOR: $640 trillion P&L indicates preprocessed price leak let initial_capital = 10_000.0; let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 1.0); // Simulate realistic trading sequence (ES.FUT) let price_sequence = vec![ 4500.0, // Initial price 4550.0, // +$50 (+1.1%) 4525.0, // -$25 (-0.5%) 4575.0, // +$50 (+1.1%) 4550.0, // -$25 (-0.5%) ]; for (i, &price) in price_sequence.iter().enumerate() { let exposure = match i % 3 { 0 => ExposureLevel::Long100, 1 => ExposureLevel::Flat, 2 => ExposureLevel::Short50, _ => ExposureLevel::Flat, }; tracker.execute_action( FactoredAction::new(exposure, OrderType::Market, Urgency::Normal), price, 100.0, ); // Check P&L at each step let pnl = tracker.unrealized_pnl(price); assert!( pnl.abs() < 10_000.0, "P&L out of realistic range at step {}: {:.2} (expected <$10K)", i, pnl ); } let final_pnl = tracker.unrealized_pnl(*price_sequence.last().unwrap()); info!(pnl = %format!("{:.2}", final_pnl), "P&L within realistic range (expected <$10,000)"); // CRITICAL: Detect $640T explosion assert!( final_pnl.abs() < 1_000_000_000_000.0, // $1 trillion threshold "CATASTROPHIC: P&L = ${:.2e} (likely preprocessed price leak)", final_pnl ); Ok(()) } // ============================================================================ // Test 2: P&L Return Percentage Reasonable // ============================================================================ #[test] fn test_pnl_return_percentage_reasonable() -> Result<()> { let initial_capital = 10_000.0; let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 1.0); // Simulate moderate trading (ES.FUT, 10 steps) let price_sequence = vec![ 4500.0, 4520.0, 4510.0, 4530.0, 4515.0, 4540.0, 4525.0, 4550.0, 4535.0, 4560.0, ]; for (i, &price) in price_sequence.iter().enumerate() { let exposure = if i % 2 == 0 { ExposureLevel::Long50 } else { ExposureLevel::Flat }; tracker.execute_action( FactoredAction::new(exposure, OrderType::Market, Urgency::Normal), price, 100.0, ); } let final_pnl = tracker.unrealized_pnl(*price_sequence.last().unwrap()); let return_pct = (final_pnl / initial_capital) * 100.0; // For validation data (limited time horizon), expect ±10% max assert!( return_pct.abs() < 10.0, "Return percentage too high: {:.2}% (expected ±10% for short validation)", return_pct ); info!(return_pct = %format!("{:.2}", return_pct), "Return percentage reasonable (expected +-10%)"); Ok(()) } // ============================================================================ // Test 3: P&L Matches Position Changes // ============================================================================ #[test] fn test_pnl_matches_position_changes() -> Result<()> { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); // Test case: Long 100 contracts, price moves from $4500 → $4550 (+$50) // Expected P&L: 100 contracts × $50 = $5,000 let entry_price = 4500.0; let exit_price = 4550.0; let position_size = 100.0; // Open long position tracker.execute_action( FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), entry_price, position_size, ); // Close position tracker.execute_action( FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal), exit_price, position_size, ); let actual_pnl = tracker.unrealized_pnl(exit_price); let expected_pnl = position_size * (exit_price - entry_price); // Allow 1% tolerance for rounding let tolerance = expected_pnl.abs() * 0.01; assert!( (actual_pnl - expected_pnl).abs() < tolerance, "P&L mismatch: actual={:.2}, expected={:.2} (position × price_change)", actual_pnl, expected_pnl ); info!(actual_pnl = %format!("{:.2}", actual_pnl), expected_pnl = %format!("{:.2}", expected_pnl), "P&L matches position changes"); Ok(()) } // ============================================================================ // Test 4: Transaction Costs Reduce P&L // ============================================================================ #[test] fn test_transaction_costs_reduce_pnl() -> Result<()> { // This test verifies that transaction costs are applied correctly // Expected: Net P&L < Gross P&L (after costs) let initial_capital = 10_000.0; let spread = 0.0001; // 1 basis point let mut tracker = PortfolioTracker::new(initial_capital, spread, 1.0); // Execute profitable trade let entry_price = 4500.0; let exit_price = 4550.0; // +$50 per contract let position_size = 100.0; // Open position tracker.execute_action( FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), entry_price, position_size, ); // Close position tracker.execute_action( FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal), exit_price, position_size, ); // Calculate gross P&L (before costs) let gross_pnl = position_size * (exit_price - entry_price); // Calculate net P&L (after costs, from realized_pnl) let net_pnl = tracker.realized_pnl(); // Transaction costs reduce P&L assert!( net_pnl < gross_pnl, "Net P&L ({:.2}) should be less than gross P&L ({:.2}) due to transaction costs", net_pnl, gross_pnl ); let cost = gross_pnl - net_pnl; info!(gross_pnl = %format!("{:.2}", gross_pnl), net_pnl = %format!("{:.2}", net_pnl), cost = %format!("{:.2}", cost), "Transaction costs applied"); Ok(()) } // ============================================================================ // Test 5: P&L Accumulation Correct // ============================================================================ #[test] fn test_pnl_accumulation_correct() -> Result<()> { // Simulate 5 epochs of trading, verify cumulative P&L consistency let initial_capital = 10_000.0; let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 1.0); let mut epoch_pnls = Vec::new(); // 5 epochs, 10 steps each for epoch in 0..5 { let base_price = 4500.0 + (epoch as f32 * 10.0); // Slight upward trend for step in 0..10 { let price = base_price + (step as f32 * 2.0); let exposure = match step % 3 { 0 => ExposureLevel::Long50, 1 => ExposureLevel::Flat, 2 => ExposureLevel::Short50, _ => ExposureLevel::Flat, }; tracker.execute_action( FactoredAction::new(exposure, OrderType::Market, Urgency::Normal), price, 100.0, ); } // Record epoch P&L let epoch_pnl = tracker.unrealized_pnl(base_price + 18.0); epoch_pnls.push(epoch_pnl); // Check for sudden jumps (>100% change) if epoch > 0 { let pnl_change = (epoch_pnl - epoch_pnls[epoch - 1]).abs(); let pnl_change_pct = if epoch_pnls[epoch - 1].abs() > 0.01 { (pnl_change / epoch_pnls[epoch - 1].abs()) * 100.0 } else { 0.0 }; assert!( pnl_change_pct < 100.0, "Sudden P&L jump at epoch {}: {:.2}% (expected <100%)", epoch, pnl_change_pct ); } } info!(epoch_count = epoch_pnls.len(), "P&L accumulation consistent across epochs (no sudden jumps >100%)"); info!(?epoch_pnls, "Epoch P&Ls"); Ok(()) } // ============================================================================ // Test 6: Long Position P&L Calculation // ============================================================================ #[test] fn test_long_position_pnl_calculation() -> Result<()> { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); // Open long position at $4500 tracker.execute_action( FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), 4500.0, 100.0, ); // Test 1: Price rises to $4600 (+$100 per contract) let pnl_up = tracker.unrealized_pnl(4600.0); let expected_up = 100.0 * (4600.0 - 4500.0); // +$10,000 assert!( (pnl_up - expected_up).abs() < 1.0, "Long position P&L (up): {:.2}, expected {:.2}", pnl_up, expected_up ); // Test 2: Price falls to $4400 (-$100 per contract) let pnl_down = tracker.unrealized_pnl(4400.0); let expected_down = 100.0 * (4400.0 - 4500.0); // -$10,000 assert!( (pnl_down - expected_down).abs() < 1.0, "Long position P&L (down): {:.2}, expected {:.2}", pnl_down, expected_down ); info!(pnl_up = %format!("{:.2}", pnl_up), pnl_down = %format!("{:.2}", pnl_down), "Long position P&L"); Ok(()) } // ============================================================================ // Test 7: Short Position P&L Calculation // ============================================================================ #[test] fn test_short_position_pnl_calculation() -> Result<()> { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); // Open short position at $4500 tracker.execute_action( FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal), 4500.0, 100.0, ); // Test 1: Price falls to $4400 (+$100 profit per contract for short) let pnl_down = tracker.unrealized_pnl(4400.0); let expected_down = -100.0 * (4400.0 - 4500.0); // +$10,000 assert!( (pnl_down - expected_down).abs() < 1.0, "Short position P&L (down): {:.2}, expected {:.2}", pnl_down, expected_down ); // Test 2: Price rises to $4600 (-$100 loss per contract for short) let pnl_up = tracker.unrealized_pnl(4600.0); let expected_up = -100.0 * (4600.0 - 4500.0); // -$10,000 assert!( (pnl_up - expected_up).abs() < 1.0, "Short position P&L (up): {:.2}, expected {:.2}", pnl_up, expected_up ); info!(pnl_down = %format!("{:.2}", pnl_down), pnl_up = %format!("{:.2}", pnl_up), "Short position P&L"); Ok(()) } // ============================================================================ // Test 8: P&L Explosion Detector (Regression Prevention) // ============================================================================ #[test] fn test_pnl_explosion_detector() -> Result<()> { // This test specifically targets the Wave 16M $640 trillion bug // Root cause: Preprocessed z-scores (-3 to +3) used as prices let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); // Simulate bug scenario: z-score used as price let preprocessed_z_score = -2.5; // This should NEVER be a price tracker.execute_action( FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), preprocessed_z_score, 100.0, ); let pnl = tracker.unrealized_pnl(preprocessed_z_score); // BUG DETECTOR: If P&L > $1 billion, preprocessed prices are leaking if pnl.abs() > 1_000_000_000.0 { panic!( "❌ CATASTROPHIC P&L EXPLOSION DETECTED: ${:.2e}\n\ This indicates preprocessed prices (z-scores) are being used in P&L calculations.\n\ Root cause: Feature extraction using normalized prices instead of raw prices.", pnl ); } warn!("Bug detector active: Testing preprocessed price leak"); warn!("If this test fails, preprocessed prices are contaminating P&L"); Ok(()) }