#![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 Portfolio Tracker Tests (TDD) //! //! These tests validate the PortfolioTracker integration for PPO. //! Tests written FIRST (TDD Red Phase) before implementation. use ml::ppo::portfolio_tracker::PortfolioTracker; #[test] fn test_portfolio_tracker_initialization() { // Test initial state (cash, position, pnl) let tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); assert_eq!(tracker.cash_balance(), 10_000.0, "Initial cash should equal initial capital"); assert_eq!(tracker.current_position(), 0.0, "Initial position should be 0 (flat)"); assert_eq!(tracker.realized_pnl(), 0.0, "Initial realized P&L should be 0"); assert_eq!(tracker.unrealized_pnl(100.0), 0.0, "Initial unrealized P&L should be 0"); assert_eq!(tracker.total_value(100.0), 10_000.0, "Initial portfolio value should equal cash"); } #[test] fn test_portfolio_tracker_buy_action() { // Test BUY action updates position and cash let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); // Execute PPO-style action: action index 0 = BUY tracker.execute_ppo_action(0, 100.0, 10.0); assert_eq!(tracker.current_position(), 10.0, "Position should be 10 contracts after BUY"); assert_eq!(tracker.cash_balance(), 9_000.0, "Cash should decrease by position cost (10 * 100)"); assert_eq!(tracker.average_entry_price(), 100.0, "Entry price should be recorded"); } #[test] fn test_portfolio_tracker_sell_action() { // Test SELL action updates position and cash let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); // Execute PPO-style action: action index 1 = SELL tracker.execute_ppo_action(1, 100.0, 10.0); assert_eq!(tracker.current_position(), -10.0, "Position should be -10 contracts after SELL (short)"); assert_eq!(tracker.cash_balance(), 11_000.0, "Cash should increase by short proceeds (10 * 100)"); assert_eq!(tracker.average_entry_price(), 100.0, "Entry price should be recorded"); } #[test] fn test_portfolio_tracker_cash_reserve() { // Test cash reserve enforcement let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 20.0); // 20% cash reserve // Try to buy with insufficient cash (reserve enforcement) // At price 100, max affordable = (10000 - 2000) / 100 = 80 contracts // Request 100 contracts -> should be reduced to 80 tracker.execute_ppo_action(0, 100.0, 100.0); let cash = tracker.cash_balance(); let portfolio_value = tracker.total_value(100.0); let reserve_required = portfolio_value * 0.20; assert!( cash >= reserve_required, "Cash reserve should be enforced: cash=${:.2}, reserve=${:.2}", cash, reserve_required ); } #[test] fn test_portfolio_tracker_pnl_calculation() { // Test P&L calculation accuracy let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); // Open long position: Buy 10 contracts at $100 tracker.execute_ppo_action(0, 100.0, 10.0); assert_eq!(tracker.cash_balance(), 9_000.0, "Cash after buy"); // Price rises to $110 -> +$100 unrealized profit let unrealized = tracker.unrealized_pnl(110.0); assert_eq!(unrealized, 100.0, "Unrealized P&L should be +$100 (10 contracts * $10 gain)"); // Total portfolio value = cash + position_value let total_value = tracker.total_value(110.0); assert_eq!(total_value, 10_100.0, "Portfolio value = 9000 (cash) + 1100 (position value)"); // Close position: Sell 10 contracts at $110 tracker.execute_ppo_action(1, 110.0, 10.0); // After closing, realized P&L should be +$100 assert_eq!(tracker.current_position(), 0.0, "Position should be closed"); assert_eq!(tracker.realized_pnl(), 100.0, "Realized P&L should be +$100"); } #[test] fn test_portfolio_tracker_hold_action() { // Test HOLD action (no change) let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); let initial_cash = tracker.cash_balance(); let initial_position = tracker.current_position(); // Execute PPO-style action: action index 2 = HOLD tracker.execute_ppo_action(2, 100.0, 10.0); assert_eq!(tracker.cash_balance(), initial_cash, "Cash should not change after HOLD"); assert_eq!(tracker.current_position(), initial_position, "Position should not change after HOLD"); } #[test] fn test_portfolio_tracker_reset() { // Test reset functionality let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); // Execute trades tracker.execute_ppo_action(0, 100.0, 10.0); // Reset tracker.reset(); assert_eq!(tracker.cash_balance(), 10_000.0, "Cash should reset to initial capital"); assert_eq!(tracker.current_position(), 0.0, "Position should reset to 0"); assert_eq!(tracker.average_entry_price(), 0.0, "Entry price should reset to 0"); } #[test] fn test_portfolio_tracker_short_position_pnl() { // Test short position P&L calculation let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); // Open short position: Sell 10 contracts at $100 tracker.execute_ppo_action(1, 100.0, 10.0); assert_eq!(tracker.current_position(), -10.0, "Position should be -10 (short)"); assert_eq!(tracker.cash_balance(), 11_000.0, "Cash after short sale"); // Price falls to $90 -> +$100 unrealized profit (profitable for short) let unrealized = tracker.unrealized_pnl(90.0); assert_eq!(unrealized, 100.0, "Unrealized P&L should be +$100 (10 contracts * $10 gain from price drop)"); // Total portfolio value = cash + position_value (position_value is negative for shorts) let total_value = tracker.total_value(90.0); assert_eq!(total_value, 10_100.0, "Portfolio value = 11000 (cash) - 900 (short liability)"); } #[test] fn test_portfolio_tracker_get_portfolio_features() { // Test portfolio features retrieval let tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); let features = tracker.get_portfolio_features(100.0); // Features: [normalized_value, normalized_position, spread] assert_eq!(features[0], 1.0, "Normalized portfolio value should be 1.0 (initial capital)"); assert_eq!(features[1], 0.0, "Normalized position should be 0.0 (no position)"); assert_eq!(features[2], 0.0001, "Spread should be 0.0001"); } // ========== WAVE 2 AGENT 4: POSITION REVERSAL TESTS (TDD RED PHASE) ========== #[test] fn test_position_reversal_long_to_short() { // Test position reversal from long to short let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); // Step 1: Open long position: Buy 20 contracts at $100 tracker.execute_ppo_action(0, 100.0, 20.0); assert_eq!(tracker.current_position(), 20.0, "Should have long position of 20 contracts"); assert_eq!(tracker.cash_balance(), 8_000.0, "Cash = 10000 - (20 * 100)"); // Step 2: Reversal - Sell 40 contracts at $110 (close 20 long + open 20 short) // Phase 1: Close 20 long at $110 -> cash += 20 * 110 = +2200 // Phase 2: Open 20 short at $110 -> cash += 20 * 110 = +2200 tracker.execute_ppo_action(1, 110.0, 40.0); // Expected: Position = -20 (short), Cash = 8000 + 2200 + 2200 = 12400 assert_eq!(tracker.current_position(), -20.0, "Should have short position of -20 contracts after reversal"); assert_eq!(tracker.cash_balance(), 12_400.0, "Cash = 8000 + 2200 (close long) + 2200 (open short)"); } #[test] fn test_position_reversal_short_to_long() { // Test position reversal from short to long let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); // Step 1: Open short position: Sell 20 contracts at $100 tracker.execute_ppo_action(1, 100.0, 20.0); assert_eq!(tracker.current_position(), -20.0, "Should have short position of -20 contracts"); assert_eq!(tracker.cash_balance(), 12_000.0, "Cash = 10000 + (20 * 100)"); // Step 2: Reversal - Buy 40 contracts at $90 (close 20 short + open 20 long) // Phase 1: Close 20 short at $90 -> cash -= 20 * 90 = -1800 // Phase 2: Open 20 long at $90 -> cash -= 20 * 90 = -1800 tracker.execute_ppo_action(0, 90.0, 40.0); // Expected: Position = 20 (long), Cash = 12000 - 1800 - 1800 = 8400 assert_eq!(tracker.current_position(), 20.0, "Should have long position of 20 contracts after reversal"); assert_eq!(tracker.cash_balance(), 8_400.0, "Cash = 12000 - 1800 (close short) - 1800 (open long)"); } #[test] fn test_partial_reversal_insufficient_cash() { // Test partial reversal when insufficient cash for full flip let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 20.0); // 20% cash reserve // Step 1: Open long position: Buy 50 contracts at $100 // Max affordable = (10000 - 2000 reserve) / 100 = 80 contracts tracker.execute_ppo_action(0, 100.0, 50.0); assert_eq!(tracker.current_position(), 50.0, "Should have long position of 50 contracts"); // Step 2: Attempt reversal - Sell 100 contracts at $110 (close 50 long + open 50 short) // Phase 1: Close 50 long at $110 -> cash += 50 * 110 = +5500 // Phase 2: Try to open 50 short at $110 -> may be partial due to cash reserve tracker.execute_ppo_action(1, 110.0, 100.0); // After reversal: // - Position should be negative (short) but may be less than -50 due to cash constraint // - Cash reserve should still be enforced let final_position = tracker.current_position(); let final_cash = tracker.cash_balance(); let portfolio_value = tracker.total_value(110.0); let reserve_required = portfolio_value * 0.20; assert!(final_position < 0.0, "Should have short position after reversal (negative)"); assert!(final_cash >= reserve_required, "Cash reserve should be enforced: cash=${:.2}, reserve=${:.2}", final_cash, reserve_required); }