//! Transition Probability Features Tests (Indices 216-220) //! //! TDD tests for 5 transition probability features: //! - Feature 216: Stability P(i→i) //! - Feature 217: Most likely next regime (index) //! - Feature 218: Shannon entropy H = -Σ P(i→j) log₂ P(i→j) //! - Feature 219: Expected duration (REUSE existing method!) //! - Feature 220: Change probability (1 - stability) //! //! **SUCCESS CRITERIA**: //! - All 5 features calculated correctly //! - expected_duration() reused from existing TransitionMatrix //! - Shannon entropy computed with numerical stability //! - Most likely regime correctly identified use ml::ensemble::MarketRegime; use ml::regime::transition_probability_features::TransitionProbabilityFeatures; #[test] fn test_initialization() { let regimes = vec![ MarketRegime::Normal, MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::HighVolatility, MarketRegime::Crisis, MarketRegime::Unknown, ]; let features = TransitionProbabilityFeatures::new(regimes, 0.1, 10); // Initially at Unknown regime (last in list) let result = features.current_regime(); assert_eq!(result, MarketRegime::Unknown); } #[test] fn test_stability_feature_216() { let regimes = vec![MarketRegime::Bull, MarketRegime::Bear]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Make Bull regime persistent: Bull -> Bull for _ in 0..10 { features.update(MarketRegime::Bull); } let result = features.compute_features(); // Feature 216: Stability should be high (>0.7) assert!( result[0] > 0.7, "Stability should be high, got {}", result[0] ); assert!( result[0] <= 1.0, "Stability should be ≤1.0, got {}", result[0] ); } #[test] fn test_most_likely_next_regime_feature_217() { let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.3, 1); // Pattern: Bull -> Bear repeatedly features.update(MarketRegime::Bull); for _ in 0..15 { features.update(MarketRegime::Bear); features.update(MarketRegime::Bull); } features.update(MarketRegime::Bear); let result = features.compute_features(); // Feature 217: Most likely next regime index // From Bear, most likely to go to Bull (index 0) let most_likely_idx = result[1] as usize; assert!( most_likely_idx <= 2, "Index should be 0-2, got {}", most_likely_idx ); } #[test] fn test_shannon_entropy_feature_218() { let regimes = vec![MarketRegime::Bull, MarketRegime::Bear]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Uniform transitions (50/50) -> maximum entropy for _ in 0..20 { features.update(MarketRegime::Bull); features.update(MarketRegime::Bear); } let result = features.compute_features(); // Feature 218: Shannon entropy // Max entropy for 2 states = log₂(2) = 1.0 let entropy = result[2]; assert!(entropy > 0.0, "Entropy should be positive, got {}", entropy); assert!( entropy <= 1.0, "Entropy should be ≤1.0 for 2 states, got {}", entropy ); } #[test] fn test_entropy_zero_for_deterministic_transition() { let regimes = vec![MarketRegime::Sideways, MarketRegime::HighVolatility]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.3, 1); // Deterministic: Sideways -> Sideways (100%) for _ in 0..30 { features.update(MarketRegime::Sideways); } let result = features.compute_features(); // Feature 218: Entropy should approach 0 (low uncertainty) let entropy = result[2]; assert!( entropy < 0.3, "Entropy should be low for deterministic transition, got {}", entropy ); } #[test] fn test_expected_duration_feature_219() { let regimes = vec![MarketRegime::Bull, MarketRegime::Bear]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Make Bull persistent: P(Bull->Bull) ≈ 0.9 for _ in 0..20 { features.update(MarketRegime::Bull); features.update(MarketRegime::Bull); features.update(MarketRegime::Bear); } features.update(MarketRegime::Bull); let result = features.compute_features(); // Feature 219: Expected duration let duration = result[3]; assert!( duration > 1.0, "Expected duration should be >1, got {}", duration ); assert!( duration < 100.0, "Expected duration should be reasonable, got {}", duration ); } #[test] fn test_change_probability_feature_220() { let regimes = vec![MarketRegime::HighVolatility, MarketRegime::Sideways]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.3, 1); // Volatile regime transitions frequently for _ in 0..10 { features.update(MarketRegime::HighVolatility); features.update(MarketRegime::Sideways); } let result = features.compute_features(); // Feature 220: Change probability = 1 - stability let stability = result[0]; let change_prob = result[4]; let expected_change_prob = 1.0 - stability; assert!( (change_prob - expected_change_prob).abs() < 1e-6, "Change prob should be 1 - stability, got {} vs expected {}", change_prob, expected_change_prob ); // For frequent transitions, change probability should be high assert!( change_prob > 0.3, "Change probability should be high, got {}", change_prob ); } #[test] fn test_all_five_features_together() { let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::HighVolatility, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.15, 1); // Realistic regime sequence let sequence = vec![ MarketRegime::Sideways, MarketRegime::Sideways, MarketRegime::Bull, MarketRegime::Bull, MarketRegime::Bull, MarketRegime::HighVolatility, MarketRegime::Bear, MarketRegime::Bear, MarketRegime::Sideways, ]; for regime in sequence { features.update(regime); } let result = features.compute_features(); // Verify all 5 features are computed assert_eq!(result.len(), 5, "Should return exactly 5 features"); // Feature 216: Stability assert!( result[0] >= 0.0 && result[0] <= 1.0, "Stability should be in [0,1], got {}", result[0] ); // Feature 217: Most likely next regime index assert!( (result[1] as usize) < 4, "Most likely index should be 0-3, got {}", result[1] ); // Feature 218: Entropy assert!( result[2] >= 0.0, "Entropy should be non-negative, got {}", result[2] ); // Feature 219: Expected duration assert!( result[3] >= 1.0, "Expected duration should be ≥1, got {}", result[3] ); // Feature 220: Change probability assert!( result[4] >= 0.0 && result[4] <= 1.0, "Change probability should be in [0,1], got {}", result[4] ); // Verify complementary relationship let stability = result[0]; let change_prob = result[4]; assert!( (stability + change_prob - 1.0).abs() < 1e-6, "Stability + change_prob should = 1.0, got {} + {} = {}", stability, change_prob, stability + change_prob ); } #[test] fn test_regime_transition_updates_matrix() { let regimes = vec![MarketRegime::Bull, MarketRegime::Bear]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Transition: Bull -> Bear features.update(MarketRegime::Bull); features.update(MarketRegime::Bear); // Current regime should be updated assert_eq!(features.current_regime(), MarketRegime::Bear); // Matrix should track this transition let result = features.compute_features(); assert!( result[0] >= 0.0, "Features should be computed after transitions" ); } #[test] fn test_same_regime_no_transition() { let regimes = vec![MarketRegime::Sideways]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Stay in same regime for _ in 0..10 { features.update(MarketRegime::Sideways); } let result = features.compute_features(); // Feature 216: Stability should approach 1.0 (always stays) assert!( result[0] > 0.8, "Stability should be very high, got {}", result[0] ); // Feature 220: Change probability should approach 0.0 assert!( result[4] < 0.2, "Change probability should be low, got {}", result[4] ); } #[test] fn test_entropy_with_three_regimes() { let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Equal probability transitions from Bull features.update(MarketRegime::Bull); for _ in 0..30 { features.update(MarketRegime::Bull); features.update(MarketRegime::Bear); features.update(MarketRegime::Bull); features.update(MarketRegime::Sideways); } features.update(MarketRegime::Bull); let result = features.compute_features(); // Feature 218: Entropy should be high (multiple options) // Max entropy for 3 states = log₂(3) ≈ 1.585 let entropy = result[2]; assert!( entropy > 0.5, "Entropy should be high for multiple options, got {}", entropy ); assert!( entropy <= 1.585, "Entropy should be ≤log₂(3), got {}", entropy ); } #[test] fn test_numerical_stability_near_zero_probabilities() { let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::HighVolatility, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.1, 1); // Only transitions between Bull and Bear (others have near-zero probability) for _ in 0..50 { features.update(MarketRegime::Bull); features.update(MarketRegime::Bear); } features.update(MarketRegime::Bull); let result = features.compute_features(); // Feature 218: Entropy should not be NaN or Inf let entropy = result[2]; assert!( entropy.is_finite(), "Entropy should be finite, got {}", entropy ); assert!( entropy >= 0.0, "Entropy should be non-negative, got {}", entropy ); } #[test] fn test_most_likely_regime_changes_over_time() { let regimes = vec![MarketRegime::Bull, MarketRegime::Bear]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.3, 1); // First pattern: Bull -> Bear features.update(MarketRegime::Bull); for _ in 0..10 { features.update(MarketRegime::Bear); features.update(MarketRegime::Bull); } features.update(MarketRegime::Bear); let result1 = features.compute_features(); let most_likely_1 = result1[1] as usize; // Now switch pattern: Bear -> Bear (persistence) for _ in 0..20 { features.update(MarketRegime::Bear); } let result2 = features.compute_features(); let most_likely_2 = result2[1] as usize; // Most likely regime should adapt to new pattern assert!( result2[0] > result1[0], "Stability should increase with persistence" ); } #[test] fn test_expected_duration_matches_transition_matrix() { let regimes = vec![MarketRegime::Sideways, MarketRegime::HighVolatility]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Make Sideways persistent for _ in 0..20 { features.update(MarketRegime::Sideways); features.update(MarketRegime::Sideways); features.update(MarketRegime::HighVolatility); } features.update(MarketRegime::Sideways); let result = features.compute_features(); let feature_duration = result[3]; // Verify duration matches the formula: 1 / (1 - stability) let stability = result[0]; let expected_duration = 1.0 / (1.0 - stability).max(0.001); assert!( (feature_duration - expected_duration).abs() < 0.1, "Feature duration {} should match calculated duration {}", feature_duration, expected_duration ); } #[test] fn test_feature_216_220_complementary() { let regimes = vec![ MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, ]; let mut features = TransitionProbabilityFeatures::new(regimes, 0.2, 1); // Various transitions let transitions = vec![ MarketRegime::Bull, MarketRegime::Bull, MarketRegime::Bear, MarketRegime::Sideways, MarketRegime::Sideways, MarketRegime::Bull, ]; for regime in transitions { features.update(regime); } let result = features.compute_features(); // Feature 216 and 220 should be complementary let stability = result[0]; let change_prob = result[4]; assert!( (stability + change_prob - 1.0).abs() < 1e-10, "Stability + change probability must equal 1.0, got {} + {} = {}", stability, change_prob, stability + change_prob ); }