#![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, )] //! Test suite for configurable initial capital feature //! //! Validates that initial capital can be configured via CLI and properly //! scales position sizes, portfolio values, and cash balances across //! different account sizes ($1K to $1M+). use ml::dqn::portfolio_tracker::PortfolioTracker; /// Helper function to create a PortfolioTracker with specified capital fn create_tracker_with_capital(capital: f32) -> PortfolioTracker { PortfolioTracker::new( capital, 0.0001, // avg_spread: 1 basis point (standard) 0.0, // cash_reserve_percent: 0% (backward compatible, no reserve requirement) ) } #[test] fn test_small_capital_10k() { let capital = 10_000.0; let tracker = create_tracker_with_capital(capital); let price = 5_600.0; // Typical ES price // Calculate expected max position (capital / price) let expected_max_position = capital / price; // 1.78 contracts // Verify initialization assert_eq!( tracker.cash_balance(), capital, "Cash balance should equal initial capital" ); assert_eq!( tracker.total_value(price), capital, "Portfolio value should equal initial capital before any trades" ); assert_eq!( tracker.current_position(), 0.0, "Position should be flat initially" ); // Verify position scaling (approximate due to floating point) assert!( (expected_max_position - 1.78).abs() < 0.01, "Max position should be ~1.78 contracts for $10K at $5,600" ); } #[test] fn test_standard_capital_100k() { let capital = 100_000.0; let tracker = create_tracker_with_capital(capital); let price = 5_600.0; // Calculate expected max position let expected_max_position = capital / price; // 17.85 contracts // Verify initialization assert_eq!(tracker.cash_balance(), capital); assert_eq!(tracker.total_value(price), capital); assert_eq!(tracker.current_position(), 0.0); // Verify position scaling (baseline behavior) assert!( (expected_max_position - 17.85).abs() < 0.01, "Max position should be ~17.85 contracts for $100K at $5,600" ); } #[test] fn test_large_capital_500k() { let capital = 500_000.0; let tracker = create_tracker_with_capital(capital); let price = 5_600.0; // Calculate expected max position (5x standard) let expected_max_position = capital / price; // 89.28 contracts // Verify initialization assert_eq!(tracker.cash_balance(), capital); assert_eq!(tracker.total_value(price), capital); assert_eq!(tracker.current_position(), 0.0); // Verify linear scaling (5x capital = 5x positions) let standard_max = 100_000.0 / price; assert!( (expected_max_position / standard_max - 5.0).abs() < 0.01, "Position capacity should scale linearly with capital (5x)" ); } #[test] fn test_institutional_capital_1m() { let capital = 1_000_000.0; let tracker = create_tracker_with_capital(capital); let price = 5_600.0; // Calculate expected max position (10x standard) let expected_max_position = capital / price; // 178.57 contracts // Verify initialization assert_eq!(tracker.cash_balance(), capital); assert_eq!(tracker.total_value(price), capital); assert_eq!(tracker.current_position(), 0.0); // Verify linear scaling (10x capital = 10x positions) let standard_max = 100_000.0 / price; assert!( (expected_max_position / standard_max - 10.0).abs() < 0.01, "Position capacity should scale linearly with capital (10x)" ); // Stress test: Verify large portfolio value calculations don't overflow let large_value = tracker.total_value(price); assert!( large_value.is_finite(), "Large portfolio values should not overflow" ); assert!(large_value > 0.0, "Portfolio value should be positive"); } #[test] fn test_minimum_capital_1k() { let capital = 1_000.0; let tracker = create_tracker_with_capital(capital); let price = 5_600.0; // Calculate expected max position (very small) let expected_max_position = capital / price; // 0.178 contracts // Verify initialization assert_eq!(tracker.cash_balance(), capital); assert_eq!(tracker.total_value(price), capital); assert_eq!(tracker.current_position(), 0.0); // Verify fractional position handling assert!( expected_max_position < 1.0, "Minimum capital should result in fractional position capacity" ); assert!( (expected_max_position - 0.178).abs() < 0.01, "Max position should be ~0.178 contracts for $1K at $5,600" ); // Edge case: Verify position limits are enforced (MAX_POSITION_CONTRACTS=1.0) // This is enforced in execute_action(), not in max_position calculation } #[test] fn test_portfolio_value_initialization() { let test_capitals = vec![1_000.0, 10_000.0, 100_000.0, 500_000.0, 1_000_000.0]; for capital in test_capitals { let tracker = create_tracker_with_capital(capital); let price = 5_600.0; // Portfolio value should equal initial capital before any trades assert_eq!( tracker.total_value(price), capital, "Portfolio value should equal initial capital of ${:.0}", capital ); // Normalized value should be 1.0 (portfolio_value / initial_capital) let raw_features = tracker.get_raw_portfolio_features(price); let portfolio_value = raw_features[0]; assert_eq!( portfolio_value, capital, "Raw portfolio value should equal initial capital" ); // Normalized features: [normalized_value, normalized_position, spread] let normalized_features = tracker.get_portfolio_features(price); let normalized_value = normalized_features[0]; assert!( (normalized_value - 1.0).abs() < 0.0001, "Normalized portfolio value should be 1.0 (no P&L yet)" ); } } #[test] fn test_cash_balance_initialization() { let test_capitals = vec![1_000.0, 10_000.0, 100_000.0, 500_000.0, 1_000_000.0]; for capital in test_capitals { let tracker = create_tracker_with_capital(capital); // Cash balance should equal initial capital assert_eq!( tracker.cash_balance(), capital, "Cash balance should equal initial capital of ${:.0}", capital ); // After reset, cash should be restored to initial capital let mut tracker_mut = tracker.clone(); tracker_mut.reset(); assert_eq!( tracker_mut.cash_balance(), capital, "Cash balance should reset to initial capital of ${:.0}", capital ); } } #[test] fn test_position_scaling_accuracy() { // Test that position scaling formula (capital / price) is accurate let test_cases = vec![ (1_000.0, 5_600.0, 0.178), // $1K at $5,600 = 0.178 contracts (10_000.0, 5_600.0, 1.785), // $10K at $5,600 = 1.785 contracts (100_000.0, 5_600.0, 17.857), // $100K at $5,600 = 17.857 contracts (500_000.0, 5_600.0, 89.285), // $500K at $5,600 = 89.285 contracts (1_000_000.0, 5_600.0, 178.571), // $1M at $5,600 = 178.571 contracts ]; for (capital, price, expected_max) in test_cases { let tracker = create_tracker_with_capital(capital); let calculated_max = capital / price; assert!( (calculated_max - expected_max).abs() < 0.001, "Max position for ${:.0} at ${:.0} should be {:.3} contracts, got {:.3}", capital, price, expected_max, calculated_max ); // Verify PortfolioTracker uses this formula internally let raw_features = tracker.get_raw_portfolio_features(price); let cash_balance = raw_features[0]; // Portfolio value = cash (no position) assert_eq!(cash_balance, capital, "Raw portfolio features should show correct cash balance"); } }