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

297 lines
9.5 KiB
Rust

//! DQN Integration Tests with 54-Feature Extraction
//!
//! This test suite validates that the 54-feature extraction system
//! integrates correctly with the DQN training pipeline.
#![allow(unused_crate_dependencies)]
use ml::features::extraction::{extract_ml_features, OHLCVBar};
use anyhow::Result;
use chrono::{Duration, Utc};
/// Test: Feature vector type is 54 dimensions
#[test]
fn test_feature_vector_type_is_54() -> Result<()> {
// OBJECTIVE: Verify FeatureVector type is [f64; 54]
// EXPECTED: Compile-time and runtime checks pass
let _: [f64; 54] = [0.0; 54];
println!("✅ FeatureVector type verified as [f64; 54]");
Ok(())
}
/// Test: DQN state dimension should be 54
#[test]
fn test_dqn_state_dim_is_54() -> Result<()> {
// OBJECTIVE: Verify DQN config uses state_dim=54
// EXPECTED: State dimension matches feature count
// Note: Actual DQN config check would happen here when config is accessible
// For now, verify feature dimension
let bars = create_test_bars(100)?;
let features = extract_ml_features(&bars)?;
assert!(!features.is_empty(), "Should extract features");
let state_dim = features[0].len();
assert_eq!(state_dim, 54, "State dimension should match feature count: {}", state_dim);
println!("✅ DQN state_dim would be 54 (matches feature extraction)");
Ok(())
}
/// Test: Features are convertible to tensor format
#[test]
fn test_features_tensor_format_compatible() -> Result<()> {
// OBJECTIVE: Verify features can be used as tensor input
// EXPECTED: Fixed-size [f64; 54] arrays ready for neural network
let bars = create_test_bars(100)?;
let features = extract_ml_features(&bars)?;
// Simulate tensor batch creation
let batch_size = features.len().min(32); // Mini-batch
let _batch: Vec<[f64; 54]> = features.iter().take(batch_size).copied().collect();
println!("✅ Features tensor-compatible: batch of {} vectors", batch_size);
Ok(())
}
/// Test: No NaN/Inf values that would break training
#[test]
fn test_no_training_breaking_values() -> Result<()> {
// OBJECTIVE: Ensure no NaN/Inf values that would break gradient flow
// EXPECTED: 100% valid floating point values
let bars = create_test_bars(100)?;
let features = extract_ml_features(&bars)?;
let mut nan_count = 0;
let mut inf_count = 0;
for (vec_idx, fv) in features.iter().enumerate() {
for (feat_idx, &value) in fv.iter().enumerate() {
if value.is_nan() {
nan_count += 1;
eprintln!("NaN at vector {} feature {}", vec_idx, feat_idx);
}
if value.is_infinite() {
inf_count += 1;
eprintln!("Inf at vector {} feature {}", vec_idx, feat_idx);
}
}
}
assert_eq!(nan_count, 0, "Found {} NaN values that break training", nan_count);
assert_eq!(inf_count, 0, "Found {} Inf values that break training", inf_count);
println!("✅ No training-breaking NaN/Inf values");
Ok(())
}
/// Test: Feature values in reasonable gradient-friendly range
#[test]
fn test_gradient_friendly_feature_ranges() -> Result<()> {
// OBJECTIVE: Verify features are in ranges that don't cause gradient issues
// EXPECTED: No extreme values (> ±1000) that cause gradient explosion/vanishing
let bars = create_test_bars(100)?;
let features = extract_ml_features(&bars)?;
let mut extreme_count = 0;
let mut max_value = 0.0f64;
for fv in &features {
for &value in fv.iter() {
if value.abs() > 1000.0 {
extreme_count += 1;
}
max_value = max_value.max(value.abs());
}
}
let ratio = extreme_count as f64 / (features.len() * 54) as f64;
assert!(ratio < 0.01, "Too many extreme values: {:.2}%", ratio * 100.0);
println!("✅ Feature ranges gradient-friendly: max = {:.2}", max_value);
Ok(())
}
/// Test: Sufficient feature variance for learning
#[test]
fn test_sufficient_feature_variance() -> Result<()> {
// OBJECTIVE: Ensure features have variance for DQN to learn
// EXPECTED: >90% of features have std dev > 1e-6
let bars = create_test_bars(200)?;
let features = extract_ml_features(&bars)?;
let mut feature_values: Vec<Vec<f64>> = vec![Vec::new(); 54];
for fv in &features {
for (i, &val) in fv.iter().enumerate() {
feature_values[i].push(val);
}
}
let mut constant_features = Vec::new();
for (i, values) in feature_values.iter().enumerate() {
if values.is_empty() {
continue;
}
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
let std_dev = variance.sqrt();
if std_dev < 1e-6 {
constant_features.push(i);
}
}
assert!(constant_features.is_empty(),
"Found {} constant features that prevent learning: {:?}",
constant_features.len(), constant_features);
println!("✅ All 54 features have learning-friendly variance");
Ok(())
}
/// Test: Feature extraction speed compatible with DQN training
#[test]
fn test_extraction_speed_training_compatible() -> Result<()> {
// OBJECTIVE: Verify extraction speed doesn't bottleneck DQN training
// EXPECTED: <500μs per bar allows real-time training
use std::time::Instant;
let bars = create_test_bars(1000)?;
let start = Instant::now();
let features = extract_ml_features(&bars)?;
let duration = start.elapsed();
let time_per_bar = duration.as_micros() / features.len() as u128;
// Target: <500μs per bar
assert!(time_per_bar < 2000,
"Extraction too slow for training: {}μs per bar (target <500μs)", time_per_bar);
println!("✅ Extraction speed training-compatible: {}μs per bar", time_per_bar);
Ok(())
}
/// Test: Memory efficiency for DQN replay buffer
#[test]
fn test_memory_efficient_for_replay_buffer() -> Result<()> {
// OBJECTIVE: Verify feature memory allows large replay buffers
// EXPECTED: 54-dim vectors enable efficient replay buffer storage
use std::mem::size_of;
let fv_size = size_of::<[f64; 54]>();
let old_fv_size = 54 * 8; // 54 features
// With 100K capacity buffer
let buffer_capacity = 100_000;
let new_buffer_bytes = fv_size * buffer_capacity;
let old_buffer_bytes = old_fv_size * buffer_capacity;
let reduction_factor = old_buffer_bytes as f64 / new_buffer_bytes as f64;
println!("Replay buffer memory:");
println!(" 54-feature buffer: ~{} MB", new_buffer_bytes / 1024 / 1024);
println!(" 54-feature buffer: ~{} MB", old_buffer_bytes / 1024 / 1024);
println!(" Reduction: {:.1}x", reduction_factor);
assert!(reduction_factor > 1.0, "Should have memory reduction");
println!("✅ Memory efficient for DQN replay buffers");
Ok(())
}
/// Test: Feature categories align with DQN expectations
#[test]
fn test_feature_categories_aligned() -> Result<()> {
// OBJECTIVE: Verify feature categories make sense for DQN
// EXPECTED: All 7 categories present (OHLCV, Technical, Price, Volume, OFI, Time, Statistical)
let bars = create_test_bars(100)?;
let features = extract_ml_features(&bars)?;
assert!(!features.is_empty(), "Should extract features");
let fv = &features[0];
// Expected feature ranges by category
let categories = vec![
(0, 4, "OHLCV"),
(5, 9, "Technical"),
(10, 15, "Price Patterns"),
(16, 21, "Volume"),
(22, 24, "Proxy OFI"),
(25, 29, "Time"),
(30, 45, "Statistical"), // 30-45 = 16 features (but we said 13)
];
// All indices should be accessible
for (start, end, name) in categories {
for i in start..=end.min(45) {
assert!(fv[i].is_finite(), "Feature {} ({}) not finite", i, name);
}
}
println!("✅ Feature categories properly aligned for DQN");
Ok(())
}
/// Test: Batch processing for training
#[test]
fn test_batch_processing_for_training() -> Result<()> {
// OBJECTIVE: Verify features can be batched for DQN training
// EXPECTED: Easy to create mini-batches of variable size
let bars = create_test_bars(500)?;
let features = extract_ml_features(&bars)?;
// Simulate creating training batches
let batch_size = 32;
let num_batches = (features.len() + batch_size - 1) / batch_size;
for batch_idx in 0..num_batches {
let start = batch_idx * batch_size;
let end = (start + batch_size).min(features.len());
let _batch: Vec<[f64; 54]> = features[start..end].to_vec();
// Verify batch is valid
assert!(!_batch.is_empty(), "Batch should not be empty");
}
println!("✅ Feature batching works: {} batches of size {}", num_batches, batch_size);
Ok(())
}
// ============================================================================
// Helper Functions
// ============================================================================
fn create_test_bars(count: usize) -> Result<Vec<OHLCVBar>> {
let mut bars = Vec::with_capacity(count);
let mut timestamp = Utc::now();
let mut price = 4500.0;
for _ in 0..count {
let bar = OHLCVBar {
timestamp,
open: price,
high: price + 2.0,
low: price - 2.0,
close: price + 1.0,
volume: 1000.0,
};
bars.push(bar);
timestamp = timestamp + Duration::minutes(1);
price += (rand::random::<f64>() - 0.5) * 2.0;
}
Ok(bars)
}