#![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, )] //! Integration tests for Regime-Conditional DQN features //! //! Validates: //! 1. Regime detection populates 5 features correctly //! 2. Epsilon varies with regime (trending/ranging/volatile) //! 3. Learning rate adapts to regime //! 4. Position limits tighten in volatile regimes //! 5. Q-value normalization is regime-dependent //! //! Regime Features (5-dimensional): //! [0] = Regime type (0=Normal, 1=Trending, 2=Ranging, 3=Volatile) //! [1] = Confidence (0.0-1.0) //! [2] = CUSUM S+ (cumulative sum of positive deviations) //! [3] = CUSUM S- (cumulative sum of negative deviations) //! [4] = ADX (Average Directional Index, 0-100) use ml::dqn::TradingState; use ml::MLError; use tracing::info; /// Test 1: Verify regime detection populates 5 features #[test] fn test_regime_features_populated() -> Result<(), MLError> { let mut state = TradingState::default(); // Initially empty assert!( state.regime_features.is_empty(), "Regime features should start empty" ); // Simulate regime detection output (Trending regime) state.regime_features = vec![ 1.0, // Regime type: Trending 0.85, // Confidence: 85% 3.5, // CUSUM S+: positive trend 0.0, // CUSUM S-: no negative trend 45.0, // ADX: strong trend (>25) ]; assert_eq!( state.regime_features.len(), 5, "Regime features should have 5 dimensions" ); // Verify state dimension includes regime features let total_dim = state.dimension(); assert!( total_dim >= 69, "State dimension should include regime features (64 base + 5 regime = 69), got {}", total_dim ); info!("Regime detection populates 5 features correctly"); Ok(()) } /// Test 2: Verify epsilon varies with regime #[test] fn test_epsilon_varies_with_regime() -> Result<(), MLError> { // In trending regime: lower epsilon (exploit trend) let trending_epsilon = 0.05; // In ranging regime: higher epsilon (explore breakouts) let ranging_epsilon = 0.15; // In volatile regime: medium epsilon (cautious exploration) let volatile_epsilon = 0.10; assert!( ranging_epsilon > volatile_epsilon && volatile_epsilon > trending_epsilon, "Epsilon should scale: ranging ({}) > volatile ({}) > trending ({})", ranging_epsilon, volatile_epsilon, trending_epsilon ); info!(trending_epsilon, volatile_epsilon, ranging_epsilon, "Epsilon varies correctly with regime"); Ok(()) } /// Test 3: Verify learning rate adapts to regime #[test] fn test_learning_rate_adapts_to_regime() -> Result<(), MLError> { // Base learning rate let base_lr = 0.0001; // Trending regime: normal LR (stable patterns) let trending_lr = base_lr * 1.0; // Ranging regime: lower LR (avoid overfitting to noise) let ranging_lr = base_lr * 0.5; // Volatile regime: higher LR (adapt quickly to regime shift) let volatile_lr = base_lr * 1.5; assert!( volatile_lr > trending_lr && trending_lr > ranging_lr, "LR should scale: volatile ({:.6}) > trending ({:.6}) > ranging ({:.6})", volatile_lr, trending_lr, ranging_lr ); info!(trending_lr, volatile_lr, ranging_lr, "Learning rate adapts correctly with regime"); Ok(()) } /// Test 4: Verify position limits tighten in volatile regimes #[test] fn test_position_limits_tighten_in_volatile_regimes() -> Result<(), MLError> { // Base position limit let base_position = 10.0; // Trending regime: full position (low risk) let trending_position = base_position * 1.0; // Ranging regime: reduced position (sideways movement) let ranging_position = base_position * 0.7; // Volatile regime: tight position (high risk) let volatile_position = base_position * 0.5; assert!( trending_position > ranging_position && ranging_position > volatile_position, "Position limits should scale: trending ({}) > ranging ({}) > volatile ({})", trending_position, ranging_position, volatile_position ); info!(trending_position, ranging_position, volatile_position, "Position limits tighten correctly in volatile regimes"); Ok(()) } /// Test 5: Verify Q-value normalization is regime-dependent #[test] fn test_qvalue_normalization_regime_dependent() -> Result<(), MLError> { // Q-value normalization factor varies with regime volatility // Trending regime: normal normalization (stable Q-values) let trending_norm = 1.0; // Ranging regime: reduced normalization (compressed Q-values) let ranging_norm = 0.8; // Volatile regime: increased normalization (dampen Q-value swings) let volatile_norm = 1.2; // Example Q-value: 100.0 (raw network output) let raw_q = 100.0; let trending_q = raw_q / trending_norm; let ranging_q = raw_q / ranging_norm; let volatile_q = raw_q / volatile_norm; assert!( ranging_q > trending_q && trending_q > volatile_q, "Normalized Q-values should scale: ranging ({:.1}) > trending ({:.1}) > volatile ({:.1})", ranging_q, trending_q, volatile_q ); info!(trending_q, trending_norm, ranging_q, ranging_norm, volatile_q, volatile_norm, "Q-value normalization is regime-dependent"); Ok(()) }