#[cfg(test)] mod walk_forward_tests { use chrono::{DateTime, Duration, Utc}; /// Test data structure representing temporal market data #[derive(Debug, Clone)] struct TemporalDataPoint { timestamp: DateTime, features: Vec, target: f32, } /// Walk-forward validation configuration #[derive(Debug, Clone)] struct WalkForwardConfig { train_window_days: i64, validation_window_days: i64, embargo_days: i64, step_days: i64, } /// Result of a single fold in walk-forward validation #[derive(Debug)] struct FoldResult { train_start: DateTime, train_end: DateTime, embargo_start: DateTime, embargo_end: DateTime, val_start: DateTime, val_end: DateTime, train_indices: Vec, val_indices: Vec, } // Helper function to generate mock temporal data fn generate_mock_data(start_date: DateTime, num_days: usize) -> Vec { (0..num_days) .map(|i| TemporalDataPoint { timestamp: start_date + Duration::days(i as i64), features: vec![i as f32; 10], target: (i as f32) * 0.01, }) .collect() } // Mock walk-forward split function (to be implemented) fn walk_forward_split( data: &[TemporalDataPoint], config: &WalkForwardConfig, ) -> Vec { // This will be implemented in the actual codebase // For now, return empty vec to make tests compilable but failing vec![] } #[test] fn test_temporal_split_maintains_order() { // GIVEN: A dataset with known temporal ordering let start_date = Utc::now(); let data = generate_mock_data(start_date, 365); let config = WalkForwardConfig { train_window_days: 90, validation_window_days: 30, embargo_days: 5, step_days: 30, }; // WHEN: We perform walk-forward validation splits let folds = walk_forward_split(&data, &config); // THEN: Every fold must maintain temporal order for fold in &folds { // Training data must come before embargo period assert!( fold.train_end <= fold.embargo_start, "Training period must end before embargo period starts. \ Train end: {:?}, Embargo start: {:?}", fold.train_end, fold.embargo_start ); // Embargo period must come before validation period assert!( fold.embargo_end <= fold.val_start, "Embargo period must end before validation period starts. \ Embargo end: {:?}, Val start: {:?}", fold.embargo_end, fold.val_start ); // Overall: train_start < train_end < embargo_start < embargo_end < val_start < val_end assert!(fold.train_start < fold.train_end); assert!(fold.embargo_start < fold.embargo_end); assert!(fold.val_start < fold.val_end); // Verify indices maintain temporal order for window in fold.train_indices.windows(2) { let earlier_timestamp = data[window[0]].timestamp; let later_timestamp = data[window[1]].timestamp; assert!( earlier_timestamp <= later_timestamp, "Training indices must be in temporal order" ); } for window in fold.val_indices.windows(2) { let earlier_timestamp = data[window[0]].timestamp; let later_timestamp = data[window[1]].timestamp; assert!( earlier_timestamp <= later_timestamp, "Validation indices must be in temporal order" ); } } } #[test] fn test_embargo_period_prevents_leakage() { // GIVEN: A dataset with daily data points let start_date = Utc::now(); let data = generate_mock_data(start_date, 365); let config = WalkForwardConfig { train_window_days: 90, validation_window_days: 30, embargo_days: 5, // Critical gap to prevent leakage step_days: 30, }; // WHEN: We perform walk-forward validation splits let folds = walk_forward_split(&data, &config); // THEN: There must be exactly embargo_days gap between train and validation for fold in &folds { // Calculate actual gap duration let gap_duration = fold.val_start.signed_duration_since(fold.train_end); let expected_gap = Duration::days(config.embargo_days); assert_eq!( gap_duration, expected_gap, "Embargo period must be exactly {} days. Found: {} days", config.embargo_days, gap_duration.num_days() ); // Verify no data points exist in embargo period for &train_idx in &fold.train_indices { let train_timestamp = data[train_idx].timestamp; assert!( train_timestamp < fold.embargo_start, "Training data timestamp ({:?}) must not overlap with embargo period ({:?} to {:?})", train_timestamp, fold.embargo_start, fold.embargo_end ); } for &val_idx in &fold.val_indices { let val_timestamp = data[val_idx].timestamp; assert!( val_timestamp >= fold.embargo_end, "Validation data timestamp ({:?}) must not overlap with embargo period ({:?} to {:?})", val_timestamp, fold.embargo_start, fold.embargo_end ); } // Verify embargo period is strictly empty let embargo_data_count = data .iter() .filter(|d| d.timestamp >= fold.embargo_start && d.timestamp < fold.embargo_end) .count(); assert!( embargo_data_count > 0, "Embargo period should contain data points that are excluded from both train and val" ); } } #[test] fn test_multiple_folds_cover_data() { // GIVEN: A dataset spanning 365 days let start_date = Utc::now(); let data = generate_mock_data(start_date, 365); let config = WalkForwardConfig { train_window_days: 90, validation_window_days: 30, embargo_days: 5, step_days: 30, // Move forward 30 days each fold }; // WHEN: We perform walk-forward validation splits let folds = walk_forward_split(&data, &config); // THEN: All data should be used across validation folds // (training data can overlap, but validation should be disjoint) // Verify we have multiple folds assert!( folds.len() >= 3, "Should have at least 3 folds with these parameters. Found: {}", folds.len() ); // Collect all validation indices across folds let mut all_val_indices = std::collections::HashSet::new(); for fold in &folds { for &idx in &fold.val_indices { // Validation periods should be disjoint (no overlap) assert!( all_val_indices.insert(idx), "Validation index {} appears in multiple folds - validation periods must be disjoint", idx ); } } // Calculate expected coverage // Total data points that can be in validation (excluding early train-only period) let min_train_embargo_days = config.train_window_days + config.embargo_days; let validation_eligible_start = data .iter() .position(|d| { d.timestamp >= start_date + Duration::days(min_train_embargo_days) }) .unwrap_or(data.len()); let validation_eligible_count = data.len() - validation_eligible_start; // We should validate on a significant portion of eligible data let coverage_ratio = all_val_indices.len() as f64 / validation_eligible_count as f64; assert!( coverage_ratio >= 0.7, "Should validate on at least 70% of eligible data. Coverage: {:.1}%", coverage_ratio * 100.0 ); // Verify folds are temporally ordered and non-overlapping for i in 1..folds.len() { let prev_fold = &folds[i - 1]; let curr_fold = &folds[i]; // Current fold should start after previous fold's validation assert!( curr_fold.val_start >= prev_fold.val_end, "Fold {} validation period must start after fold {} validation ends", i, i - 1 ); } } #[test] fn test_no_future_data_in_training() { // CRITICAL TEST: Verify no look-ahead bias // GIVEN: A dataset with known temporal ordering let start_date = Utc::now(); let data = generate_mock_data(start_date, 365); let config = WalkForwardConfig { train_window_days: 90, validation_window_days: 30, embargo_days: 5, step_days: 30, }; // WHEN: We perform walk-forward validation splits let folds = walk_forward_split(&data, &config); // THEN: No training data can have timestamps >= validation start (including embargo) for (fold_idx, fold) in folds.iter().enumerate() { for &train_idx in &fold.train_indices { let train_timestamp = data[train_idx].timestamp; // Training data must be strictly before validation period assert!( train_timestamp < fold.val_start, "LOOK-AHEAD BIAS DETECTED in fold {}: Training data timestamp ({:?}) \ is not before validation start ({:?})", fold_idx, train_timestamp, fold.val_start ); // Training data must also be before embargo period assert!( train_timestamp < fold.embargo_start, "LOOK-AHEAD BIAS DETECTED in fold {}: Training data timestamp ({:?}) \ overlaps with embargo period (starts {:?})", fold_idx, train_timestamp, fold.embargo_start ); // Verify no training index >= any validation index for &val_idx in &fold.val_indices { assert!( train_idx < val_idx, "CRITICAL: Training index ({}) must be < validation index ({}) in fold {}", train_idx, val_idx, fold_idx ); } } // Verify validation data is strictly after all training data if let (Some(&last_train_idx), Some(&first_val_idx)) = ( fold.train_indices.last(), fold.val_indices.first() ) { let last_train_timestamp = data[last_train_idx].timestamp; let first_val_timestamp = data[first_val_idx].timestamp; let gap = first_val_timestamp.signed_duration_since(last_train_timestamp); assert!( gap >= Duration::days(config.embargo_days), "Gap between last training point and first validation point must be >= embargo period. \ Found: {} days, Expected: >= {} days", gap.num_days(), config.embargo_days ); } } } #[test] fn test_sliding_window_progression() { // GIVEN: A dataset with daily data let start_date = Utc::now(); let data = generate_mock_data(start_date, 365); let config = WalkForwardConfig { train_window_days: 90, validation_window_days: 30, embargo_days: 5, step_days: 30, // Slide forward by 30 days each fold }; // WHEN: We perform walk-forward validation splits let folds = walk_forward_split(&data, &config); // THEN: Each fold should progress by step_days for i in 1..folds.len() { let prev_fold = &folds[i - 1]; let curr_fold = &folds[i]; // Validation periods should progress by step_days let val_progression = curr_fold .val_start .signed_duration_since(prev_fold.val_start); assert_eq!( val_progression, Duration::days(config.step_days), "Validation period should advance by {} days between folds. \ Fold {} to {}: {} days", config.step_days, i - 1, i, val_progression.num_days() ); } } #[test] fn test_consistent_window_sizes() { // GIVEN: A dataset with sufficient data let start_date = Utc::now(); let data = generate_mock_data(start_date, 365); let config = WalkForwardConfig { train_window_days: 90, validation_window_days: 30, embargo_days: 5, step_days: 30, }; // WHEN: We perform walk-forward validation splits let folds = walk_forward_split(&data, &config); // THEN: Each fold should have consistent window sizes for (fold_idx, fold) in folds.iter().enumerate() { // Training window duration let train_duration = fold.train_end.signed_duration_since(fold.train_start); assert_eq!( train_duration, Duration::days(config.train_window_days), "Fold {} training window should be {} days, found {} days", fold_idx, config.train_window_days, train_duration.num_days() ); // Validation window duration let val_duration = fold.val_end.signed_duration_since(fold.val_start); assert_eq!( val_duration, Duration::days(config.validation_window_days), "Fold {} validation window should be {} days, found {} days", fold_idx, config.validation_window_days, val_duration.num_days() ); // Embargo period duration let embargo_duration = fold.embargo_end.signed_duration_since(fold.embargo_start); assert_eq!( embargo_duration, Duration::days(config.embargo_days), "Fold {} embargo period should be {} days, found {} days", fold_idx, config.embargo_days, embargo_duration.num_days() ); } } #[test] fn test_data_point_assignment_is_exhaustive() { // GIVEN: A dataset let start_date = Utc::now(); let data = generate_mock_data(start_date, 200); let config = WalkForwardConfig { train_window_days: 60, validation_window_days: 20, embargo_days: 3, step_days: 20, }; // WHEN: We perform walk-forward validation splits let folds = walk_forward_split(&data, &config); // THEN: Every fold should have data points assigned for (fold_idx, fold) in folds.iter().enumerate() { assert!( !fold.train_indices.is_empty(), "Fold {} must have training data", fold_idx ); assert!( !fold.val_indices.is_empty(), "Fold {} must have validation data", fold_idx ); // Verify indices are within bounds for &idx in &fold.train_indices { assert!(idx < data.len(), "Training index out of bounds"); } for &idx in &fold.val_indices { assert!(idx < data.len(), "Validation index out of bounds"); } } } #[test] fn test_edge_case_insufficient_data() { // GIVEN: A dataset too small for the configuration let start_date = Utc::now(); let data = generate_mock_data(start_date, 30); // Only 30 days let config = WalkForwardConfig { train_window_days: 90, validation_window_days: 30, embargo_days: 5, step_days: 30, }; // WHEN: We attempt walk-forward validation let folds = walk_forward_split(&data, &config); // THEN: Should return empty or handle gracefully // (Implementation should validate data sufficiency) assert!( folds.is_empty(), "Should return no folds when data is insufficient for window sizes" ); } }