#![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, )] //! Comprehensive Unit Tests for Transition Probability Features (Wave D Phase 3, Agent D15) //! //! This test suite validates transition probability features (indices 216-220, 5 features): //! 1. **Stability P(i→i)** (216): Probability of staying in current regime //! 2. **Most Likely Next Regime** (217): Index of regime with highest transition probability //! 3. **Shannon Entropy** (218): H = -Σ P(i→j) log₂ P(i→j), uncertainty measure //! 4. **Expected Duration** (219): E[T] = 1 / (1 - P[i][i]), bars until transition //! 5. **Change Probability** (220): 1 - P(i→i), probability of regime change //! //! ## Test Coverage (15 tests across 5 categories) //! - ✅ Stability tests (3): P(i→i) calculation, deterministic transitions, random transitions //! - ✅ Most likely next tests (3): argmax calculation, tie breaking, index encoding //! - ✅ Entropy tests (3): bounds [0, log₂N], deterministic (entropy=0), uniform (max entropy) //! - ✅ Expected duration tests (3): duration calculation, integration with TransitionMatrix, edge cases //! - ✅ Change probability tests (3): complement of stability, bounds [0, 1], deterministic vs random //! //! ## TDD Methodology //! Tests written to validate full implementation of TransitionProbabilityFeatures. use ml::ensemble::MarketRegime; use ml::regime::transition_probability_features::TransitionProbabilityFeatures; // ==================== CATEGORY 1: STABILITY TESTS (3 tests) ==================== #[test] fn test_stability_self_transition_probability() { // Test: Stability feature correctly tracks P(i→i) for current regime let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Create a strongly persistent sequence: Bull → Bull → Bull for _ in 0..10 { features.update(MarketRegime::Bull); } let result = features.compute_features(); let stability = result[0]; // After 10 self-transitions, stability should be very high (>0.8) assert!( stability > 0.8 && stability <= 1.0, "Stability for persistent regime should be >0.8, got {}", stability ); } #[test] fn test_stability_deterministic_transitions() { // Test: Deterministic self-transitions yield stability ≈ 1.0 let regimes = vec![MarketRegime::Sideways]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.1, 1); // Only one regime: all transitions are self-transitions for _ in 0..20 { features.update(MarketRegime::Sideways); } let result = features.compute_features(); let stability = result[0]; // With only self-transitions, stability should approach 1.0 assert!( stability > 0.95, "Deterministic self-transitions should yield stability >0.95, got {}", stability ); } #[test] fn test_stability_random_transitions() { // Test: Random transitions between regimes yield lower stability let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Alternate between regimes (low persistence) let sequence = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, ]; for regime in sequence { features.update(regime); } let result = features.compute_features(); let stability = result[0]; // With frequent transitions, stability should be lower (<0.6) assert!( stability < 0.6, "Random transitions should yield stability <0.6, got {}", stability ); } // ==================== CATEGORY 2: MOST LIKELY NEXT REGIME TESTS (3 tests) ==================== #[test] fn test_most_likely_next_argmax_calculation() { // Test: Most likely next regime correctly identifies highest transition probability let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Create pattern: Bull → Sideways (repeated) for _ in 0..10 { features.update(MarketRegime::Bull); features.update(MarketRegime::Sideways); } // Ensure current regime is Bull features.update(MarketRegime::Bull); let result = features.compute_features(); let most_likely_idx = result[1] as usize; // Most likely next regime from Bull should be Sideways (index 2) assert_eq!( most_likely_idx, 2, "Most likely next regime after Bull should be Sideways (index 2), got {}", most_likely_idx ); } #[test] fn test_most_likely_next_tie_breaking() { // Test: Tie breaking when multiple regimes have equal probability let regimes = vec![MarketRegime::Bull, MarketRegime::Bear]; let features = TransitionProbabilityFeatures::new(regimes, 0.1, 10); // With no updates, both transitions have equal probability (uniform initialization) let result = features.compute_features(); let most_likely_idx = result[1] as usize; // Should return the first matching index (0 or 1) assert!( most_likely_idx < 2, "Most likely index should be valid (0-1), got {}", most_likely_idx ); } #[test] fn test_most_likely_next_index_encoding() { // Test: Index encoding correctly maps regime to 0-based index let regimes = vec![ MarketRegime::Bull, // Index 0 MarketRegime::Bear, // Index 1 MarketRegime::Sideways, // Index 2 ]; let features = TransitionProbabilityFeatures::new(regimes, 0.1, 1); let result = features.compute_features(); let most_likely_idx = result[1]; // Index should be in valid range [0, 2] assert!( most_likely_idx >= 0.0 && most_likely_idx <= 2.0, "Most likely index should be in [0, 2], got {}", most_likely_idx ); } // ==================== CATEGORY 3: ENTROPY TESTS (3 tests) ==================== #[test] fn test_entropy_bounds() { // Test: Shannon entropy stays within bounds [0, log₂(N)] let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::HighVolatility, ]; let mut features = TransitionProbabilityFeatures::new(regimes.clone(), 0.2, 1); // Create diverse transition pattern let sequence = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::HighVolatility, MarketRegime::Bull, ]; for regime in sequence { features.update(regime); } let result = features.compute_features(); let entropy = result[2]; let max_entropy = (regimes.len() as f64).log2(); assert!( entropy >= 0.0 && entropy <= max_entropy, "Entropy should be in [0, {:.4}], got {:.4}", max_entropy, entropy ); } #[test] fn test_entropy_deterministic_zero() { // Test: Deterministic transitions (single outcome) yield entropy ≈ 0 let regimes = vec![MarketRegime::Sideways]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.1, 1); // Only one regime: deterministic transitions for _ in 0..20 { features.update(MarketRegime::Sideways); } let result = features.compute_features(); let entropy = result[2]; // Deterministic case: entropy should be near zero assert!( entropy < 0.1, "Deterministic transitions should yield low entropy (<0.1), got {:.4}", entropy ); } #[test] fn test_entropy_uniform_maximum() { // Test: Uniform distribution over regimes yields maximum entropy let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::HighVolatility, ]; // With min_obs=100, insufficient data forces uniform Laplace smoothing let features = TransitionProbabilityFeatures::new(regimes.clone(), 0.1, 100); let result = features.compute_features(); let entropy = result[2]; let max_entropy = (regimes.len() as f64).log2(); // Uniform distribution should yield near-maximum entropy assert!( (entropy - max_entropy).abs() < 0.5, "Uniform distribution should yield entropy ≈ {:.4}, got {:.4}", max_entropy, entropy ); } // ==================== CATEGORY 4: EXPECTED DURATION TESTS (3 tests) ==================== #[test] fn test_expected_duration_calculation() { // Test: Expected duration correctly calculated as E[T] = 1 / (1 - P[i][i]) let regimes = vec![MarketRegime::Sideways]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.1, 1); // Create high persistence: P(Sideways→Sideways) ≈ 0.9 for _ in 0..20 { features.update(MarketRegime::Sideways); } let result = features.compute_features(); let duration = result[3]; // E[T] = 1 / (1 - 0.9) = 10 periods (approximately) assert!( duration > 5.0, "High persistence should yield duration >5 periods, got {:.2}", duration ); } #[test] fn test_expected_duration_integration_with_transition_matrix() { // Test: Duration feature integrates correctly with underlying TransitionMatrix let regimes = vec![MarketRegime::Bull, MarketRegime::Bear]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Create persistent Bull regime for _ in 0..15 { features.update(MarketRegime::Bull); } let result = features.compute_features(); let duration = result[3]; // Direct validation: duration from TransitionMatrix should match feature let matrix_duration = features .transition_matrix() .get_expected_duration(MarketRegime::Bull); assert!( (duration - matrix_duration).abs() < 1e-6, "Feature duration should match TransitionMatrix, got feature={:.4}, matrix={:.4}", duration, matrix_duration ); } #[test] fn test_expected_duration_edge_cases() { // Test: Edge cases - zero persistence, low observations let regimes = vec![MarketRegime::Bull, MarketRegime::Bear]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.5, 1); // Alternate between regimes (zero persistence in each regime) for _ in 0..10 { features.update(MarketRegime::Bull); features.update(MarketRegime::Bear); } // Ensure current regime is Bull features.update(MarketRegime::Bull); let result = features.compute_features(); let duration = result[3]; // Low persistence: duration should be near 1.0 (immediate exit) assert!( duration >= 1.0 && duration < 3.0, "Low persistence should yield duration near 1.0, got {:.2}", duration ); } // ==================== CATEGORY 5: CHANGE PROBABILITY TESTS (3 tests) ==================== #[test] fn test_change_probability_complement_of_stability() { // Test: Change probability = 1 - stability (exact complement) let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Create mixed transition pattern for _ in 0..5 { features.update(MarketRegime::Bull); features.update(MarketRegime::Bear); } let result = features.compute_features(); let stability = result[0]; let change_prob = result[4]; // Change probability should be exact complement of stability assert!( (stability + change_prob - 1.0).abs() < 1e-10, "stability + change_prob should equal 1.0, got {:.10} + {:.10} = {:.10}", stability, change_prob, stability + change_prob ); } #[test] fn test_change_probability_bounds() { // Test: Change probability stays within [0, 1] let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::HighVolatility, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Create diverse transitions let sequence = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::HighVolatility, ]; for regime in sequence { features.update(regime); } let result = features.compute_features(); let change_prob = result[4]; assert!( change_prob >= 0.0 && change_prob <= 1.0, "Change probability should be in [0, 1], got {:.4}", change_prob ); } #[test] fn test_change_probability_deterministic_vs_random() { // Test: Compare change probability for deterministic vs random transitions // Deterministic case: single regime (low change probability) let regimes_det = vec![MarketRegime::Sideways]; let mut features_det = TransitionProbabilityFeatures::new(regimes_det, 0.1, 1); for _ in 0..20 { features_det.update(MarketRegime::Sideways); } let result_det = features_det.compute_features(); let change_prob_det = result_det[4]; // Random case: alternating regimes (high change probability) let regimes_rand = vec![MarketRegime::Bull, MarketRegime::Bear]; let mut features_rand = TransitionProbabilityFeatures::new(regimes_rand, 0.2, 1); for _ in 0..10 { features_rand.update(MarketRegime::Bull); features_rand.update(MarketRegime::Bear); } let result_rand = features_rand.compute_features(); let change_prob_rand = result_rand[4]; // Random transitions should have higher change probability than deterministic assert!( change_prob_rand > change_prob_det, "Random transitions should have higher change probability than deterministic, got random={:.4}, det={:.4}", change_prob_rand, change_prob_det ); // Deterministic case should have low change probability (<0.1) assert!( change_prob_det < 0.1, "Deterministic case should have change probability <0.1, got {:.4}", change_prob_det ); // Random case should have high change probability (>0.5) assert!( change_prob_rand > 0.5, "Random case should have change probability >0.5, got {:.4}", change_prob_rand ); }