#![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 test for 54-dimension feature extraction //! //! Tests the extract_ml_features() function with real OHLCV data use chrono::Utc; use ml::features::extraction::{extract_ml_features, OHLCVBar}; use tracing::info; use tracing::warn; #[test] fn test_extract_256_dim_features() { // Create synthetic OHLCV bars (100 bars to exceed warmup period of 50) let bars: Vec = (0..100) .map(|i| OHLCVBar { timestamp: Utc::now() + chrono::Duration::hours(i), open: 4500.0 + i as f64 * 0.5, high: 4510.0 + i as f64 * 0.5, low: 4490.0 + i as f64 * 0.5, close: 4505.0 + i as f64 * 0.5, volume: 10000.0 + i as f64 * 100.0, }) .collect(); // Extract features let result = extract_ml_features(&bars); assert!( result.is_ok(), "Feature extraction failed: {:?}", result.err() ); let features = result.unwrap(); // Should return features for bars after warmup period (100 - 50 = 50) assert_eq!( features.len(), 50, "Expected 50 feature vectors (100 bars - 50 warmup), got {}", features.len() ); // Each feature vector should be exactly 54 dimensions for (i, feature_vec) in features.iter().enumerate() { assert_eq!( feature_vec.len(), 54, "Feature vector {} has wrong dimension: {}", i, feature_vec.len() ); // Validate no NaN/Inf values for (j, &val) in feature_vec.iter().enumerate() { assert!( val.is_finite(), "Feature vector {} has non-finite value at index {}: {}", i, j, val ); } } info!(count = features.len(), "Successfully extracted 54-dim feature vectors"); info!(first_10 = ?&features[0][0..10], "First feature vector sample"); } #[test] fn test_feature_dimensions() { // Create 60 bars (10 above minimum warmup) let bars: Vec = (0..60) .map(|i| { OHLCVBar { timestamp: Utc::now() + chrono::Duration::minutes(i), open: 4500.0, high: 4510.0, low: 4490.0, close: 4505.0 + (i as f64 * 0.1).sin() * 5.0, // Add some variation volume: 10000.0, } }) .collect(); let features = extract_ml_features(&bars).unwrap(); // Should have 10 feature vectors (60 - 50 warmup) assert_eq!(features.len(), 10); // Check output shape (num_bars, 54) assert_eq!(features.len(), 10, "Wrong number of bars"); for feature_vec in &features { assert_eq!(feature_vec.len(), 54, "Wrong feature dimension"); } // Validate no NaN/Inf for feature_vec in &features { for &val in feature_vec.iter() { assert!(val.is_finite(), "Found non-finite value: {}", val); } } info!(bars = features.len(), "Feature dimensions validated: bars x 54 features"); } #[test] fn test_insufficient_data_error() { // Create only 10 bars (below 50 warmup requirement) let bars: Vec = (0..10) .map(|i| OHLCVBar { timestamp: Utc::now() + chrono::Duration::hours(i), open: 4500.0, high: 4510.0, low: 4490.0, close: 4505.0, volume: 10000.0, }) .collect(); let result = extract_ml_features(&bars); assert!(result.is_err(), "Should fail with insufficient data"); let error_msg = result.unwrap_err().to_string(); assert!( error_msg.contains("Insufficient data"), "Expected 'Insufficient data' error, got: {}", error_msg ); info!("Insufficient data error handled correctly"); } #[test] fn test_feature_normalization() { // Create bars with extreme values to test normalization let bars: Vec = (0..100) .map(|i| { OHLCVBar { timestamp: Utc::now() + chrono::Duration::hours(i), open: 4500.0 + i as f64 * 10.0, // Large price changes high: 4600.0 + i as f64 * 10.0, low: 4400.0 + i as f64 * 10.0, close: 4500.0 + i as f64 * 10.0, volume: 100000.0 + i as f64 * 5000.0, // Large volume changes } }) .collect(); let features = extract_ml_features(&bars).unwrap(); // Check that features are reasonably normalized for (i, feature_vec) in features.iter().enumerate() { for (j, &val) in feature_vec.iter().enumerate() { // Most features should be in reasonable range (not all, but most) // This is a sanity check, not strict validation if !(-10.0..=10.0).contains(&val) { // Log but don't fail - some features may legitimately be outside this range warn!(feature_idx = j, vector_idx = i, value = val, "Feature value outside [-10, 10]"); } } } info!("Feature normalization validated"); } #[test] fn test_feature_consistency() { // Test that same input produces same output (deterministic) let bars: Vec = (0..100) .map(|i| OHLCVBar { timestamp: Utc::now() + chrono::Duration::hours(i), open: 4500.0, high: 4510.0, low: 4490.0, close: 4505.0, volume: 10000.0, }) .collect(); let features1 = extract_ml_features(&bars).unwrap(); let features2 = extract_ml_features(&bars).unwrap(); assert_eq!(features1.len(), features2.len()); for (vec1, vec2) in features1.iter().zip(features2.iter()) { for (&val1, &val2) in vec1.iter().zip(vec2.iter()) { assert!( (val1 - val2).abs() < 1e-10, "Features not consistent: {} vs {}", val1, val2 ); } } info!("Feature extraction is deterministic"); }