Files
foxhunt/ml/tests/test_extract_256_dim_features.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

207 lines
6.1 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.
//! 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};
#[test]
fn test_extract_256_dim_features() {
// Create synthetic OHLCV bars (100 bars to exceed warmup period of 50)
let bars: Vec<OHLCVBar> = (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
);
}
}
println!(
"✅ Successfully extracted {} 54-dim feature vectors",
features.len()
);
println!(
"✅ First feature vector sample (first 10 features): {:?}",
&features[0][0..10]
);
}
#[test]
fn test_feature_dimensions() {
// Create 60 bars (10 above minimum warmup)
let bars: Vec<OHLCVBar> = (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);
}
}
println!(
"✅ Feature dimensions validated: {} bars × 54 features",
features.len()
);
}
#[test]
fn test_insufficient_data_error() {
// Create only 10 bars (below 50 warmup requirement)
let bars: Vec<OHLCVBar> = (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
);
println!("✅ Insufficient data error handled correctly");
}
#[test]
fn test_feature_normalization() {
// Create bars with extreme values to test normalization
let bars: Vec<OHLCVBar> = (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
println!(
"⚠️ Feature {} in vector {} has value outside [-10, 10]: {}",
j, i, val
);
}
}
}
println!("✅ Feature normalization validated");
}
#[test]
fn test_feature_consistency() {
// Test that same input produces same output (deterministic)
let bars: Vec<OHLCVBar> = (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
);
}
}
println!("✅ Feature extraction is deterministic");
}