Files
foxhunt/ml/tests/wave_d_e2e_normalization_test.rs
jgrusewski e166a4fc02 Wave 3: Update LOW RISK test files (225→54 features)
- Updated 73 test files across 10 categories
- Total 557 replacements (225 → 54)
- DQN tests: 252/262 passing (9 failures - slice index blocker)
- TFT tests: 98/98 passing
- MAMBA-2 tests: 11/11 passing
- Hyperopt tests: 98/98 passing

Critical findings:
- Blocker: ml/src/trainers/dqn.rs:3444 hardcoded slice indices
- Architecture mismatch: extract_current_features() vs extract_current_features_v2()

Wave 3 Agent breakdown:
- Agent 1: DQN test files (12 files)
- Agent 2: PPO test files (2 files)
- Agent 3: TFT test files (6 files)
- Agent 4: MAMBA-2 test files (2 files)
- Agent 5: Feature extraction tests (3 files)
- Agent 6: Integration test files (9 files)
- Agent 7: Data loader test files (3 files)
- Agent 8: Hyperopt test files (1 file)
- Agent 9: Benchmark test files (9 files)
- Agent 10: Utility & misc test files (73 files)

Next: Fix slice index blocker, then Wave 4 (OFI integration 46→54)
2025-11-23 01:22:32 +01:00

711 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Agent D31: Wave D E2E Normalization Integration Test
//!
//! End-to-end validation of Wave D feature normalization integration with real feature extraction.
//! This test validates the complete pipeline: DBN data → feature extraction → normalization → validation.
//!
//! ## Test Objectives
//!
//! 1. Load real DBN market data (ES.FUT)
//! 2. Extract all 54 features (201 Wave C + 24 Wave D)
//! 3. Normalize all features using FeatureNormalizer
//! 4. Validate Wave D features (indices 201-224) are properly normalized
//! 5. Verify no NaN/Inf in any feature after normalization
//! 6. Validate feature ranges are within expected bounds
//! 7. Performance: <200μs per bar for normalization
//!
//! ## Success Criteria
//!
//! - ✅ Test passes with 100% success rate
//! - ✅ All 54 features extracted and normalized for real DBN data
//! - ✅ No NaN/Inf values in normalized output
//! - ✅ Wave D features (201-224) within expected ranges
//! - ✅ Performance: <200μs per bar for normalization
//! - ✅ Incremental normalization produces consistent results
use anyhow::Result;
use ml::features::config::FeatureConfig;
use ml::features::normalization::FeatureNormalizer;
use ml::features::regime_adaptive::RegimeAdaptiveFeatures;
use ml::features::regime_adx::RegimeADXFeatures;
use ml::features::regime_cusum::RegimeCUSUMFeatures;
use ml::features::regime_transition::RegimeTransitionFeatures;
use std::time::Instant;
/// Simulated OHLCV bar for regime feature extraction
#[derive(Debug, Clone)]
struct RegimeOHLCVBar {
timestamp: i64, // Unix timestamp in nanoseconds
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
}
// ========================================
// Test 1: Wave D Full Normalization E2E
// ========================================
#[test]
fn test_wave_d_full_normalization_e2e() -> Result<()> {
println!("\n=== Test 1: Wave D Full Normalization E2E (54 Features) ===");
println!("Testing complete pipeline: data → extraction → normalization → validation");
// Step 1: Generate simulated ES.FUT-like bars
let start_gen = Instant::now();
let bars = generate_simulated_es_fut_bars(1000);
let gen_duration = start_gen.elapsed();
println!(
"✓ Generated {} simulated ES.FUT bars in {:.2}ms",
bars.len(),
gen_duration.as_secs_f64() * 1000.0
);
// Step 2: Initialize feature extractors
let mut cusum = RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0);
let mut adx = RegimeADXFeatures::new(14);
let mut transition = RegimeTransitionFeatures::new(100);
let mut adaptive = RegimeAdaptiveFeatures::new();
// Step 3: Initialize normalizer with Wave D support
let mut normalizer = FeatureNormalizer::new();
println!("✓ Initialized feature extractors and normalizer");
// Step 4: Extract and normalize features
let start_extract = Instant::now();
let mut all_normalized_features = Vec::new();
let mut normalization_times = Vec::new();
for (idx, bar) in bars.iter().enumerate() {
// Extract Wave C features (placeholder for indices 0-200)
let mut features = vec![0.0; 54];
// Simulate Wave C features (indices 0-200) with realistic values
for i in 0..201 {
let base_value = ((i + idx) as f64 * 0.01).sin();
let noise = ((i * idx) % 100) as f64 / 100.0 - 0.5;
features[i] = base_value + noise * 0.1;
}
// Extract real Wave D features (indices 201-224)
let log_return = if idx > 0 {
(bar.close / bars[idx - 1].close).ln()
} else {
0.0
};
// CUSUM features (indices 201-210)
let cusum_features = cusum.update(log_return);
for (i, &val) in cusum_features.iter().enumerate() {
features[201 + i] = val;
}
// ADX features (indices 211-215)
let adx_features = adx.update(bar.high, bar.low, bar.close);
for (i, &val) in adx_features.iter().enumerate() {
features[211 + i] = val;
}
// Transition features (indices 216-220)
let transition_features = transition.update(&determine_regime(&bars, idx));
for (i, &val) in transition_features.iter().enumerate() {
features[216 + i] = val;
}
// Adaptive features (indices 221-224)
let adaptive_features = adaptive.update(
&determine_regime(&bars, idx),
calculate_recent_volatility(&bars, idx),
);
for (i, &val) in adaptive_features.iter().enumerate() {
features[221 + i] = val;
}
// Normalize all features
let norm_start = Instant::now();
normalizer.normalize(&mut features)?;
let norm_duration = norm_start.elapsed();
normalization_times.push(norm_duration.as_micros() as f64);
all_normalized_features.push(features);
}
let extract_duration = start_extract.elapsed();
println!(
"✓ Extracted and normalized features for {} bars in {:.2}ms",
bars.len(),
extract_duration.as_secs_f64() * 1000.0
);
println!(
" - Average: {:.2}μs per bar",
extract_duration.as_micros() as f64 / bars.len() as f64
);
// Step 5: Validate feature dimensions
assert_eq!(
all_normalized_features.len(),
1000,
"Should have 1000 feature vectors"
);
for (idx, features) in all_normalized_features.iter().enumerate() {
assert_eq!(
features.len(),
54,
"Bar {} should have 54 features, got {}",
idx,
features.len()
);
}
println!(
"✓ Feature dimensions validated: {} bars × 54 features",
all_normalized_features.len()
);
// Step 6: Validate no NaN/Inf in normalized features
let mut nan_count = 0;
let mut inf_count = 0;
for (bar_idx, features) in all_normalized_features.iter().enumerate() {
for (feat_idx, &val) in features.iter().enumerate() {
if val.is_nan() {
nan_count += 1;
if nan_count <= 5 {
eprintln!(" NaN detected at bar {}, feature {}", bar_idx, feat_idx);
}
}
if val.is_infinite() {
inf_count += 1;
if inf_count <= 5 {
eprintln!(" Inf detected at bar {}, feature {}", bar_idx, feat_idx);
}
}
}
}
assert_eq!(
nan_count, 0,
"Found {} NaN values in normalized features",
nan_count
);
assert_eq!(
inf_count, 0,
"Found {} Inf values in normalized features",
inf_count
);
println!(
"✓ No NaN/Inf values detected in {} normalized features",
all_normalized_features.len() * 54
);
// Step 7: Validate Wave D feature ranges
println!("\nValidating Wave D normalized features (indices 201-224):");
validate_cusum_normalized_features(&all_normalized_features)?;
validate_adx_normalized_features(&all_normalized_features)?;
validate_transition_normalized_features(&all_normalized_features)?;
validate_adaptive_normalized_features(&all_normalized_features)?;
// Step 8: Performance validation
let avg_norm_time = normalization_times.iter().sum::<f64>() / normalization_times.len() as f64;
let max_norm_time = normalization_times.iter().cloned().fold(0.0, f64::max);
let p95_norm_time = {
let mut sorted = normalization_times.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
sorted[(sorted.len() as f64 * 0.95) as usize]
};
println!("\nNormalization performance:");
println!(" - Average: {:.2}μs per bar", avg_norm_time);
println!(" - P95: {:.2}μs per bar", p95_norm_time);
println!(" - Max: {:.2}μs per bar", max_norm_time);
assert!(
avg_norm_time < 200.0,
"Normalization too slow: {:.2}μs avg (target: <200μs)",
avg_norm_time
);
println!("\n✅ All validations passed!");
println!(
" - Total bars processed: {}",
all_normalized_features.len()
);
println!(" - Features per bar: 54 (201 Wave C + 24 Wave D)");
println!(
" - Average normalization time: {:.2}μs per bar",
avg_norm_time
);
println!(" - Performance target: <200μs ✓");
Ok(())
}
// ========================================
// Test 2: Wave D Normalization Warmup
// ========================================
#[test]
fn test_wave_d_normalization_warmup() -> Result<()> {
println!("\n=== Test 2: Wave D Normalization Warmup Behavior ===");
let bars = generate_simulated_es_fut_bars(100);
let mut normalizer = FeatureNormalizer::new();
let mut cusum = RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0);
let mut adx = RegimeADXFeatures::new(14);
let mut transition = RegimeTransitionFeatures::new(100);
let mut adaptive = RegimeAdaptiveFeatures::new();
println!("Testing normalization during warmup period (first 30 bars)...");
for (idx, bar) in bars.iter().take(50).enumerate() {
let mut features = vec![0.0; 54];
// Extract Wave D features
let log_return = if idx > 0 {
(bar.close / bars[idx - 1].close).ln()
} else {
0.0
};
let cusum_features = cusum.update(log_return);
for (i, &val) in cusum_features.iter().enumerate() {
features[201 + i] = val;
}
let adx_features = adx.update(bar.high, bar.low, bar.close);
for (i, &val) in adx_features.iter().enumerate() {
features[211 + i] = val;
}
let transition_features = transition.update(&determine_regime(&bars, idx));
for (i, &val) in transition_features.iter().enumerate() {
features[216 + i] = val;
}
let adaptive_features = adaptive.update(
&determine_regime(&bars, idx),
calculate_recent_volatility(&bars, idx),
);
for (i, &val) in adaptive_features.iter().enumerate() {
features[221 + i] = val;
}
// Normalize
normalizer.normalize(&mut features)?;
// Validate all features are finite during warmup
for (feat_idx, &val) in features.iter().enumerate() {
assert!(
val.is_finite(),
"Feature {} at bar {} is not finite during warmup: {}",
feat_idx,
idx,
val
);
}
if idx < 10 || idx == 20 || idx == 30 {
println!(" Bar {}: All features finite ✓", idx);
}
}
println!("✓ Normalization handles warmup period correctly");
Ok(())
}
// ========================================
// Test 3: Wave D Normalization Consistency
// ========================================
#[test]
fn test_wave_d_normalization_consistency() -> Result<()> {
println!("\n=== Test 3: Wave D Normalization Consistency ===");
let bars = generate_simulated_es_fut_bars(500);
// Run normalization twice with same data
let mut features1 = extract_and_normalize_all(&bars)?;
let mut features2 = extract_and_normalize_all(&bars)?;
println!("✓ Extracted features twice with same data");
// Compare results
assert_eq!(features1.len(), features2.len(), "Feature count mismatch");
let mut max_diff = 0.0;
let mut mismatch_count = 0;
for (bar_idx, (f1, f2)) in features1.iter().zip(features2.iter()).enumerate() {
for (feat_idx, (&v1, &v2)) in f1.iter().zip(f2.iter()).enumerate() {
let diff = (v1 - v2).abs();
if diff > max_diff {
max_diff = diff;
}
if diff > 1e-10 {
mismatch_count += 1;
if mismatch_count <= 3 {
eprintln!(
" Mismatch at bar {}, feature {}: {} vs {} (diff: {})",
bar_idx, feat_idx, v1, v2, diff
);
}
}
}
}
assert_eq!(
mismatch_count, 0,
"Found {} mismatches between runs",
mismatch_count
);
println!(
"✓ Normalization is deterministic (max diff: {:.2e})",
max_diff
);
Ok(())
}
// ========================================
// Test 4: Wave D Normalizer Reset
// ========================================
#[test]
fn test_wave_d_normalizer_reset() -> Result<()> {
println!("\n=== Test 4: Wave D Normalizer Reset ===");
let bars = generate_simulated_es_fut_bars(200);
let mut normalizer = FeatureNormalizer::new();
// Extract and normalize first 100 bars
let features_before = extract_and_normalize_with_normalizer(&bars[..100], &mut normalizer)?;
println!("✓ Normalized first 100 bars");
// Reset normalizer
normalizer.reset();
println!("✓ Reset normalizer");
// Extract and normalize next 100 bars (should be like starting fresh)
let features_after = extract_and_normalize_with_normalizer(&bars[100..], &mut normalizer)?;
println!("✓ Normalized next 100 bars after reset");
// Validate both runs produced valid features
for features in features_before.iter() {
for &val in features.iter() {
assert!(val.is_finite(), "Feature not finite before reset");
}
}
for features in features_after.iter() {
for &val in features.iter() {
assert!(val.is_finite(), "Feature not finite after reset");
}
}
println!("✓ Reset works correctly (all features finite)");
Ok(())
}
// ========================================
// Helper Functions
// ========================================
/// Generate simulated ES.FUT-like bars with realistic price movements
fn generate_simulated_es_fut_bars(count: usize) -> Vec<RegimeOHLCVBar> {
let mut bars = Vec::with_capacity(count);
let mut price = 4500.0; // ES.FUT typical price level
for i in 0..count {
// Simulate price movement with trend, volatility, and regime changes
let trend = (i as f64 / 100.0).sin() * 5.0;
let volatility = if i % 100 < 50 { 2.0 } else { 5.0 }; // Regime changes
let random_walk = ((i * 7919) % 100) as f64 / 50.0 - 1.0; // Deterministic "random"
price = price + trend + random_walk * volatility;
let open = price;
let high = price + (((i * 1039) % 50) as f64 / 100.0);
let low = price - (((i * 1301) % 50) as f64 / 100.0);
let close = low + (high - low) * (((i * 1009) % 100) as f64 / 100.0);
let volume = 1000.0 + (((i * 9973) % 500) as f64);
bars.push(RegimeOHLCVBar {
timestamp: (1700000000 + i as i64 * 60) * 1_000_000_000,
open,
high,
low,
close,
volume,
});
}
bars
}
/// Determine regime for a given bar
fn determine_regime(bars: &[RegimeOHLCVBar], idx: usize) -> String {
if idx < 20 {
return "trending".to_string();
}
// Calculate recent volatility
let recent_prices: Vec<f64> = bars
.iter()
.skip(idx.saturating_sub(20))
.take(20)
.map(|b| b.close)
.collect();
let mean = recent_prices.iter().sum::<f64>() / recent_prices.len() as f64;
let variance = recent_prices
.iter()
.map(|&p| (p - mean).powi(2))
.sum::<f64>()
/ recent_prices.len() as f64;
let std = variance.sqrt();
let cv = std / (mean + 1e-8);
if cv > 0.03 {
"volatile".to_string()
} else if idx % 50 < 25 {
"trending".to_string()
} else {
"ranging".to_string()
}
}
/// Calculate recent volatility for adaptive features
fn calculate_recent_volatility(bars: &[RegimeOHLCVBar], idx: usize) -> f64 {
if idx < 2 {
return 0.02; // Default 2% volatility
}
let recent_returns: Vec<f64> = bars
.iter()
.skip(idx.saturating_sub(20))
.take(20)
.map(|b| b.close)
.collect::<Vec<_>>()
.windows(2)
.map(|w| (w[1] - w[0]) / (w[0] + 1e-8))
.collect();
if recent_returns.is_empty() {
return 0.02;
}
let mean = recent_returns.iter().sum::<f64>() / recent_returns.len() as f64;
let variance = recent_returns
.iter()
.map(|&r| (r - mean).powi(2))
.sum::<f64>()
/ recent_returns.len() as f64;
variance.sqrt()
}
/// Extract and normalize all features for given bars
fn extract_and_normalize_all(bars: &[RegimeOHLCVBar]) -> Result<Vec<Vec<f64>>> {
let mut normalizer = FeatureNormalizer::new();
extract_and_normalize_with_normalizer(bars, &mut normalizer)
}
/// Extract and normalize features with provided normalizer
fn extract_and_normalize_with_normalizer(
bars: &[RegimeOHLCVBar],
normalizer: &mut FeatureNormalizer,
) -> Result<Vec<Vec<f64>>> {
let mut cusum = RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0);
let mut adx = RegimeADXFeatures::new(14);
let mut transition = RegimeTransitionFeatures::new(100);
let mut adaptive = RegimeAdaptiveFeatures::new();
let mut all_features = Vec::new();
for (idx, bar) in bars.iter().enumerate() {
let mut features = vec![0.0; 54];
// Simulate Wave C features
for i in 0..201 {
let base_value = ((i + idx) as f64 * 0.01).sin();
features[i] = base_value;
}
// Extract Wave D features
let log_return = if idx > 0 {
(bar.close / bars[idx - 1].close).ln()
} else {
0.0
};
let cusum_features = cusum.update(log_return);
for (i, &val) in cusum_features.iter().enumerate() {
features[201 + i] = val;
}
let adx_features = adx.update(bar.high, bar.low, bar.close);
for (i, &val) in adx_features.iter().enumerate() {
features[211 + i] = val;
}
let transition_features = transition.update(&determine_regime(bars, idx));
for (i, &val) in transition_features.iter().enumerate() {
features[216 + i] = val;
}
let adaptive_features = adaptive.update(
&determine_regime(bars, idx),
calculate_recent_volatility(bars, idx),
);
for (i, &val) in adaptive_features.iter().enumerate() {
features[221 + i] = val;
}
// Normalize
normalizer.normalize(&mut features)?;
all_features.push(features);
}
Ok(all_features)
}
/// Validate CUSUM normalized features (indices 201-210)
fn validate_cusum_normalized_features(all_features: &[Vec<f64>]) -> Result<()> {
println!("\n CUSUM Normalized Features (indices 201-210):");
// Skip first 20 bars for warmup
let features_after_warmup: Vec<_> = all_features.iter().skip(20).collect();
for idx in 201..211 {
let values: Vec<f64> = features_after_warmup.iter().map(|f| f[idx]).collect();
let mean = values.iter().sum::<f64>() / values.len() as f64;
let std =
(values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64).sqrt();
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
println!(
" - Feature {}: mean={:.4}, std={:.4}, range=[{:.4}, {:.4}]",
idx, mean, std, min, max
);
// Validate Z-score normalization: mean ≈ 0, values in [-3, 3]
assert!(
min >= -5.0 && max <= 5.0,
"Feature {} outside expected range [-5, 5]: [{}, {}]",
idx,
min,
max
);
}
println!(" ✓ CUSUM normalized features validated");
Ok(())
}
/// Validate ADX normalized features (indices 211-215)
fn validate_adx_normalized_features(all_features: &[Vec<f64>]) -> Result<()> {
println!("\n ADX Normalized Features (indices 211-215):");
let features_after_warmup: Vec<_> = all_features.iter().skip(20).collect();
for idx in 211..216 {
let values: Vec<f64> = features_after_warmup.iter().map(|f| f[idx]).collect();
let mean = values.iter().sum::<f64>() / values.len() as f64;
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
println!(
" - Feature {}: mean={:.4}, range=[{:.4}, {:.4}]",
idx, mean, min, max
);
// ADX features use percentile rank, should be in [0, 1] after normalization
assert!(
min >= -0.5 && max <= 2.0,
"Feature {} outside expected range [-0.5, 2.0]: [{}, {}]",
idx,
min,
max
);
}
println!(" ✓ ADX normalized features validated");
Ok(())
}
/// Validate transition normalized features (indices 216-220)
fn validate_transition_normalized_features(all_features: &[Vec<f64>]) -> Result<()> {
println!("\n Transition Normalized Features (indices 216-220):");
let features_after_warmup: Vec<_> = all_features.iter().skip(20).collect();
for idx in 216..221 {
let values: Vec<f64> = features_after_warmup.iter().map(|f| f[idx]).collect();
let mean = values.iter().sum::<f64>() / values.len() as f64;
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
println!(
" - Feature {}: mean={:.4}, range=[{:.4}, {:.4}]",
idx, mean, min, max
);
// Transition features use Z-score, should be in [-3, 3]
assert!(
min >= -5.0 && max <= 5.0,
"Feature {} outside expected range [-5, 5]: [{}, {}]",
idx,
min,
max
);
}
println!(" ✓ Transition normalized features validated");
Ok(())
}
/// Validate adaptive normalized features (indices 221-224)
fn validate_adaptive_normalized_features(all_features: &[Vec<f64>]) -> Result<()> {
println!("\n Adaptive Normalized Features (indices 221-224):");
let features_after_warmup: Vec<_> = all_features.iter().skip(20).collect();
for idx in 221..54 {
let values: Vec<f64> = features_after_warmup.iter().map(|f| f[idx]).collect();
let mean = values.iter().sum::<f64>() / values.len() as f64;
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
println!(
" - Feature {}: mean={:.4}, range=[{:.4}, {:.4}]",
idx, mean, min, max
);
// Adaptive features use percentile rank, should be in [0, 2]
assert!(
min >= -0.5 && max <= 3.0,
"Feature {} outside expected range [-0.5, 3.0]: [{}, {}]",
idx,
min,
max
);
}
println!(" ✓ Adaptive normalized features validated");
Ok(())
}