#![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 ADX Feature Extractor (Agent D14) //! //! This test suite validates the 5 ADX features: //! - Feature 211: ADX (Average Directional Index) //! - Feature 212: +DI (Positive Directional Indicator) //! - Feature 213: -DI (Negative Directional Indicator) //! - Feature 214: DX (Directional Movement Index) //! - Feature 215: Trend Classification (0=weak, 1=moderate, 2=strong) //! //! ## Test Coverage //! 1. Wilder's 14-period algorithm correctness //! 2. Incremental vs. batch processing consistency //! 3. Performance benchmark (<80μs target) //! 4. Real market data validation //! 5. Edge case handling (constant prices, extreme volatility) use ml::features::adx_features::{AdxFeatureExtractor, OHLCVBar}; use std::collections::VecDeque; use std::time::Instant; use tracing::info; // ===== Test Helper Functions ===== fn create_bars(prices: Vec) -> VecDeque { prices .into_iter() .map(|p| OHLCVBar { timestamp: chrono::Utc::now(), open: p, high: p * 1.01, low: p * 0.99, close: p, volume: 1000.0, }) .collect() } fn create_trending_bars(start: f64, count: usize, trend_strength: f64) -> VecDeque { (0..count) .map(|i| { let price = start + trend_strength * i as f64; OHLCVBar { timestamp: chrono::Utc::now(), open: price, high: price * 1.02, low: price * 0.98, close: price, volume: 1000.0, } }) .collect() } fn create_ranging_bars(center: f64, count: usize) -> VecDeque { (0..count) .map(|i| { let price = center + 0.5 * ((i as f64 * 0.5).sin()); OHLCVBar { timestamp: chrono::Utc::now(), open: price, high: price * 1.005, low: price * 0.995, close: price, volume: 1000.0, } }) .collect() } fn assert_approx_eq(a: f64, b: f64, epsilon: f64) { assert!( (a - b).abs() < epsilon, "{} != {} (epsilon: {})", a, b, epsilon ); } // ===== Feature Validation Tests ===== #[test] fn test_adx_trending_uptrend() { let mut extractor = AdxFeatureExtractor::new(); let bars = create_trending_bars(100.0, 40, 0.5); // Strong uptrend let mut features = [0.0; 5]; for bar in bars.iter() { features = extractor.update(bar); } // ADX should detect trending market assert!( extractor.is_initialized(), "Extractor not initialized after 40 bars" ); assert!(features[0] > 0.0, "ADX: {}", features[0]); // ADX > 0 assert!( features[1] > features[2], "+DI ({}) should be > -DI ({}) in uptrend", features[1], features[2] ); // +DI > -DI in uptrend assert!(features[3] > 0.0, "DX: {}", features[3]); // DX > 0 // Validate feature ranges assert!( features[0] >= 0.0 && features[0] <= 100.0, "ADX out of range: {}", features[0] ); assert!( features[1] >= 0.0 && features[1] <= 100.0, "+DI out of range: {}", features[1] ); assert!( features[2] >= 0.0 && features[2] <= 100.0, "-DI out of range: {}", features[2] ); assert!( features[3] >= 0.0 && features[3] <= 100.0, "DX out of range: {}", features[3] ); assert!( features[4] == 0.0 || features[4] == 1.0 || features[4] == 2.0, "Classification invalid: {}", features[4] ); } #[test] fn test_adx_trending_downtrend() { let mut extractor = AdxFeatureExtractor::new(); let bars = create_trending_bars(150.0, 40, -0.5); // Strong downtrend let mut features = [0.0; 5]; for bar in bars.iter() { features = extractor.update(bar); } // ADX should detect trending market assert!(extractor.is_initialized()); assert!(features[0] > 0.0, "ADX: {}", features[0]); assert!( features[2] > features[1], "-DI ({}) should be > +DI ({}) in downtrend", features[2], features[1] ); // -DI > +DI in downtrend assert!(features[3] > 0.0, "DX: {}", features[3]); } #[test] fn test_adx_ranging_market() { let mut extractor = AdxFeatureExtractor::new(); let bars = create_ranging_bars(100.0, 40); // Oscillating market let mut features = [0.0; 5]; for bar in bars.iter() { features = extractor.update(bar); } // ADX should be lower in ranging market assert!(extractor.is_initialized()); assert!( features[0] >= 0.0 && features[0] <= 100.0, "ADX: {}", features[0] ); // Classification should be valid assert!( features[4] >= 0.0 && features[4] <= 2.0, "Classification: {}", features[4] ); } #[test] fn test_adx_constant_prices() { let mut extractor = AdxFeatureExtractor::new(); let bars = create_bars(vec![100.0; 40]); let mut features = [0.0; 5]; for bar in bars.iter() { features = extractor.update(bar); } // Constant prices should result in very low ADX assert!( features[0] < 5.0, "ADX should be low for constant prices: {}", features[0] ); assert_eq!( features[4], 0.0, "Classification should be weak: {}", features[4] ); } #[test] fn test_adx_initialization_phase() { let mut extractor = AdxFeatureExtractor::new(); let bars = create_trending_bars(100.0, 15, 0.3); // Process bars incrementally for (i, bar) in bars.iter().enumerate() { let features = extractor.update(bar); if i < 27 { // Before bar 28, ADX should be zero assert_eq!(features[0], 0.0, "ADX should be 0 at bar {}", i + 1); } } // After 27 bars, should not be initialized yet assert!( !extractor.is_initialized(), "Should not be initialized before 28 bars" ); // Add more bars to reach initialization let more_bars = create_trending_bars(105.0, 15, 0.3); for bar in more_bars.iter() { extractor.update(bar); } // Now should be initialized assert!( extractor.is_initialized(), "Should be initialized after 28+ bars" ); } #[test] fn test_adx_classification_thresholds() { // Test weak trend classification (ADX < 20) let mut extractor = AdxFeatureExtractor::new(); let bars = create_ranging_bars(100.0, 40); let mut features = [0.0; 5]; for bar in bars.iter() { features = extractor.update(bar); } // Note: Ranging market might not always produce ADX < 20 depending on oscillation // This test validates that classification is in valid range assert!( features[4] == 0.0 || features[4] == 1.0 || features[4] == 2.0, "Classification: {}", features[4] ); // Test strong trend classification (ADX >= 40) // This requires very strong trending data let mut extractor_strong = AdxFeatureExtractor::new(); let strong_bars = create_trending_bars(100.0, 50, 1.0); // Very strong trend let mut strong_features = [0.0; 5]; for bar in strong_bars.iter() { strong_features = extractor_strong.update(bar); } // Strong trend should have high ADX assert!( strong_features[0] > 20.0, "Strong trend should have ADX > 20: {}", strong_features[0] ); } // ===== Consistency Tests ===== #[test] fn test_incremental_vs_batch_consistency() { let bars = create_trending_bars(100.0, 40, 0.4); // Incremental processing let mut extractor_incremental = AdxFeatureExtractor::new(); let mut features_incremental = [0.0; 5]; for bar in bars.iter() { features_incremental = extractor_incremental.update(bar); } // Batch processing let features_batch = AdxFeatureExtractor::extract_from_window(&bars); // Results should be identical for i in 0..5 { assert_approx_eq(features_incremental[i], features_batch[i], 0.01); } } #[test] fn test_reset_functionality() { let mut extractor = AdxFeatureExtractor::new(); let bars = create_trending_bars(100.0, 30, 0.5); // Process bars for bar in bars.iter() { extractor.update(bar); } assert!(extractor.bar_count() > 0); // Reset extractor.reset(); // Verify reset state assert_eq!(extractor.bar_count(), 0); assert!(!extractor.is_initialized()); // Process new bars after reset let new_bars = create_trending_bars(150.0, 30, -0.5); for bar in new_bars.iter() { extractor.update(bar); } assert_eq!(extractor.bar_count(), 30); } // ===== Performance Tests ===== #[test] fn test_performance_benchmark() { let bars = create_trending_bars(100.0, 1000, 0.3); let mut extractor = AdxFeatureExtractor::new(); // Warm-up: Initialize extractor for bar in bars.iter().take(28) { extractor.update(bar); } // Benchmark: Process remaining bars let start = Instant::now(); let iterations = bars.len() - 28; for bar in bars.iter().skip(28) { extractor.update(bar); } let elapsed = start.elapsed(); let avg_time_us = elapsed.as_micros() as f64 / iterations as f64; info!(avg_time_us, iterations, "ADX Performance (target: <80μs)"); // Target: <80μs per bar assert!( avg_time_us < 80.0, "Performance regression: {:.2}μs per bar (target: <80μs)", avg_time_us ); } #[test] fn test_batch_processing_performance() { let bars = create_trending_bars(100.0, 1000, 0.3); let start = Instant::now(); let _features = AdxFeatureExtractor::extract_from_window(&bars); let elapsed = start.elapsed(); let avg_time_us = elapsed.as_micros() as f64 / bars.len() as f64; info!(avg_time_us, num_bars = bars.len(), "ADX Batch Performance (target: <80μs)"); // Batch processing should also meet performance target assert!( avg_time_us < 80.0, "Batch performance regression: {:.2}μs per bar (target: <80μs)", avg_time_us ); } // ===== Edge Case Tests ===== #[test] fn test_extreme_volatility() { let mut extractor = AdxFeatureExtractor::new(); let mut bars = create_ranging_bars(100.0, 30); // Add extreme spike bars.push_back(OHLCVBar { timestamp: chrono::Utc::now(), open: 150.0, high: 180.0, low: 140.0, close: 170.0, volume: 5000.0, }); let mut features = [0.0; 5]; for bar in bars.iter() { features = extractor.update(bar); } // Should handle extreme volatility gracefully assert!( features[0].is_finite() && features[0] >= 0.0, "ADX should be finite: {}", features[0] ); assert!( features[1].is_finite() && features[1] >= 0.0, "+DI should be finite: {}", features[1] ); assert!( features[2].is_finite() && features[2] >= 0.0, "-DI should be finite: {}", features[2] ); } #[test] fn test_custom_period() { let mut extractor = AdxFeatureExtractor::with_period(10); assert_eq!(extractor.bar_count(), 0); let bars = create_trending_bars(100.0, 30, 0.5); let mut features = [0.0; 5]; for bar in bars.iter() { features = extractor.update(bar); } // Should initialize faster with shorter period (10 × 2 = 20 bars) assert!(extractor.is_initialized()); assert!(features[0] >= 0.0); } #[test] fn test_insufficient_data() { let mut extractor = AdxFeatureExtractor::new(); let bars = create_bars(vec![100.0, 101.0, 102.0]); for bar in bars.iter() { let features = extractor.update(bar); // All zeros until we have enough data assert_eq!( features, [0.0; 5], "Features should be zero with insufficient data" ); } } // ===== Real Market Data Simulation ===== #[test] fn test_realistic_market_data() { let mut extractor = AdxFeatureExtractor::new(); // Simulate realistic price movement with noise let mut bars = VecDeque::new(); let mut price = 100.0; for i in 0..60 { // Add trend + noise price += 0.1 + 0.05 * ((i as f64 * 0.3).sin()); bars.push_back(OHLCVBar { timestamp: chrono::Utc::now(), open: price - 0.2, high: price + 0.5, low: price - 0.5, close: price, volume: 1000.0 + (i as f64 * 10.0), }); } let mut features = [0.0; 5]; for bar in bars.iter() { features = extractor.update(bar); } // After 60 bars, should be initialized and have valid features assert!(extractor.is_initialized()); assert!( features[0].is_finite() && features[0] >= 0.0, "ADX: {}", features[0] ); assert!( features[1].is_finite() && features[1] >= 0.0, "+DI: {}", features[1] ); assert!( features[2].is_finite() && features[2] >= 0.0, "-DI: {}", features[2] ); assert!( features[3].is_finite() && features[3] >= 0.0, "DX: {}", features[3] ); assert!( features[4] == 0.0 || features[4] == 1.0 || features[4] == 2.0, "Classification: {}", features[4] ); } // ===== Integration Test Summary ===== #[test] fn test_integration_summary() { info!("ADX Feature Extractor Integration Test Summary: 5 features (211-215), Wilder's 14-period smoothing, 28-bar init, <80μs target"); }