- 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)
711 lines
23 KiB
Rust
711 lines
23 KiB
Rust
//! 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(())
|
||
}
|