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

583 lines
20 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: ML Model Input Format Validation (54 Features)
//!
//! This test suite validates that the 54-feature tensor format (Wave C 201 + Wave D 24)
//! is compatible with all ML models (MAMBA-2, DQN, PPO, TFT) and ready for retraining.
//!
//! ## Test Coverage
//!
//! 1. **MAMBA-2 Input Format**:
//! - Shape: (batch_size=32, seq_len=100, features=54)
//! - dtype: f32
//! - Memory layout: row-major (C-contiguous)
//! - No NaN/Inf validation
//!
//! 2. **DQN Input Format**:
//! - Shape: (batch_size=64, state_dim=54)
//! - Action space: 3 (buy/sell/hold)
//! - Reward function: PnL-based
//!
//! 3. **PPO Input Format**:
//! - Observation space: Box(54,)
//! - Action space: Discrete(3)
//! - Reward: Sharpe-adjusted PnL
//!
//! 4. **TFT Input Format**:
//! - Static features: 24 Wave D features (indices 201-224)
//! - Time-varying features: 201 Wave C features (indices 0-200)
//! - Temporal encoding: hour_sin, hour_cos, day_of_week
//!
//! ## Success Criteria
//!
//! - ✅ All 4 models accept 54-feature input
//! - ✅ Tensor shapes correct for each model
//! - ✅ No NaN/Inf in tensors
//! - ✅ Backward compatibility verified (models trained on 201 can be retrained)
//!
//! ## TDD Workflow
//!
//! **RED**: These tests are expected to FAIL initially until Wave D features are integrated.
//! **GREEN**: Tests will pass once DbnSequenceLoader generates 54-feature tensors.
//! **REFACTOR**: Document model input format specifications.
use anyhow::{Context, Result};
use candle_core::{DType, Device, Tensor};
use ndarray::{Array1, Array2};
use ml::data_loaders::DbnSequenceLoader;
use ml::features::config::FeatureConfig;
/// Test configuration constants
const BATCH_SIZE_MAMBA: usize = 32;
const BATCH_SIZE_DQN: usize = 64;
const BATCH_SIZE_PPO: usize = 64;
const SEQ_LEN: usize = 100;
const _NUM_SAMPLES: usize = 100; // For generating test data
const WAVE_D_FEATURE_COUNT: usize = 54;
const WAVE_C_FEATURE_COUNT: usize = 201;
// ============================================================================
// Test 1: MAMBA-2 Input Format
// ============================================================================
#[tokio::test]
async fn test_mamba2_input_format_225_features() -> Result<()> {
println!("🔬 TEST: MAMBA-2 Input Format (54 features)");
println!(" Expected: [batch=32, seq_len=100, features=54]");
// Create Wave D feature configuration
let config = FeatureConfig::wave_d();
assert_eq!(
config.feature_count(),
54,
"Wave D config must have 54 features"
);
// Create device (CPU fallback for testing)
let device = Device::cuda_if_available(0)?;
println!(" Device: {:?}", device);
// Generate synthetic 54-feature tensor for MAMBA-2
// Shape: [batch_size, seq_len, features]
let tensor =
generate_synthetic_features(BATCH_SIZE_MAMBA, SEQ_LEN, WAVE_D_FEATURE_COUNT, &device)?;
// Validate shape
let dims = tensor.dims();
assert_eq!(dims.len(), 3, "MAMBA-2 input must be 3D tensor");
assert_eq!(dims[0], BATCH_SIZE_MAMBA, "Batch size mismatch");
assert_eq!(dims[1], SEQ_LEN, "Sequence length mismatch");
assert_eq!(
dims[2], WAVE_D_FEATURE_COUNT,
"Feature count mismatch: expected 54 features"
);
// Validate dtype
assert_eq!(tensor.dtype(), DType::F32, "MAMBA-2 requires f32 dtype");
// Validate memory layout (contiguous)
assert!(
tensor.is_contiguous(),
"Tensor must be contiguous for GPU efficiency"
);
// Validate no NaN/Inf
validate_no_nan_inf(&tensor)?;
println!(" ✅ Shape: {:?}", dims);
println!(" ✅ dtype: {:?}", tensor.dtype());
println!(" ✅ Contiguous: {}", tensor.is_contiguous());
println!(" ✅ No NaN/Inf detected");
// Validate Wave D feature indices (201-224)
let wave_d_features = config.get_wave_d_features();
assert_eq!(wave_d_features.len(), 24, "Wave D must have 24 features");
assert_eq!(
wave_d_features[0].index, 201,
"Wave D features start at index 201"
);
assert_eq!(
wave_d_features[23].index, 224,
"Wave D features end at index 224"
);
println!(" ✅ Wave D features validated: indices 201-224");
Ok(())
}
#[tokio::test]
async fn test_mamba2_backward_compatibility_201_to_225() -> Result<()> {
println!("🔬 TEST: MAMBA-2 Backward Compatibility (201 → 54 features)");
// Create Wave C feature configuration (201 features)
let config_c = FeatureConfig::wave_c();
assert_eq!(config_c.feature_count(), 201);
// Create Wave D feature configuration (54 features)
let config_d = FeatureConfig::wave_d();
assert_eq!(config_d.feature_count(), 54);
// Models trained on 201 features can be retrained (not fine-tuned) with 54 features
// This requires retraining the input embedding layer from scratch
println!(" ✅ Wave C: 201 features");
println!(" ✅ Wave D: 54 features (+24)");
println!(" ✅ Retraining required for input layer (201 → 54 expansion)");
Ok(())
}
// ============================================================================
// Test 2: DQN Input Format
// ============================================================================
#[tokio::test]
async fn test_dqn_input_format_225_features() -> Result<()> {
println!("🔬 TEST: DQN Input Format (54 features)");
println!(" Expected: [batch=64, state_dim=54]");
// Create Wave D feature configuration
let config = FeatureConfig::wave_d();
assert_eq!(config.feature_count(), 54);
let device = Device::cuda_if_available(0)?;
// Generate synthetic 54-feature state tensor for DQN
// Shape: [batch_size, state_dim] (no sequence dimension)
let tensor = Tensor::randn(0f32, 1f32, (BATCH_SIZE_DQN, WAVE_D_FEATURE_COUNT), &device)?;
// Validate shape
let dims = tensor.dims();
assert_eq!(dims.len(), 2, "DQN input must be 2D tensor");
assert_eq!(dims[0], BATCH_SIZE_DQN, "Batch size mismatch");
assert_eq!(
dims[1], WAVE_D_FEATURE_COUNT,
"State dimension mismatch: expected 54 features"
);
// Validate dtype
assert_eq!(tensor.dtype(), DType::F32, "DQN requires f32 dtype");
// Validate no NaN/Inf
validate_no_nan_inf(&tensor)?;
println!(" ✅ Shape: {:?}", dims);
println!(" ✅ dtype: {:?}", tensor.dtype());
println!(" ✅ No NaN/Inf detected");
// Validate action space unchanged
const ACTION_SPACE: usize = 3; // buy, sell, hold
println!(" ✅ Action space: {} (buy/sell/hold)", ACTION_SPACE);
Ok(())
}
#[tokio::test]
async fn test_dqn_action_space_unchanged() -> Result<()> {
println!("🔬 TEST: DQN Action Space (unchanged with 54 features)");
// DQN action space remains: buy, sell, hold (3 actions)
const ACTION_SPACE: usize = 3;
println!(" Action space size: {}", ACTION_SPACE);
println!(" Actions: [0=buy, 1=sell, 2=hold]");
println!(" ✅ Action space unchanged (independent of feature count)");
Ok(())
}
// ============================================================================
// Test 3: PPO Input Format
// ============================================================================
#[tokio::test]
async fn test_ppo_input_format_225_features() -> Result<()> {
println!("🔬 TEST: PPO Input Format (54 features)");
println!(" Expected: observation_space=Box(54,)");
// Create Wave D feature configuration
let config = FeatureConfig::wave_d();
assert_eq!(config.feature_count(), 54);
let device = Device::cuda_if_available(0)?;
// Generate synthetic 54-feature observation tensor for PPO
// Shape: [batch_size, obs_dim]
let tensor = Tensor::randn(0f32, 1f32, (BATCH_SIZE_PPO, WAVE_D_FEATURE_COUNT), &device)?;
// Validate shape
let dims = tensor.dims();
assert_eq!(dims.len(), 2, "PPO observation must be 2D tensor");
assert_eq!(dims[0], BATCH_SIZE_PPO, "Batch size mismatch");
assert_eq!(
dims[1], WAVE_D_FEATURE_COUNT,
"Observation dimension mismatch: expected 54 features"
);
// Validate dtype
assert_eq!(tensor.dtype(), DType::F32, "PPO requires f32 dtype");
// Validate no NaN/Inf
validate_no_nan_inf(&tensor)?;
println!(" ✅ Shape: {:?}", dims);
println!(" ✅ dtype: {:?}", tensor.dtype());
println!(" ✅ No NaN/Inf detected");
// Validate observation space: Box(54,)
println!(" ✅ Observation space: Box(54,)");
Ok(())
}
#[tokio::test]
async fn test_ppo_reward_function_unchanged() -> Result<()> {
println!("🔬 TEST: PPO Reward Function (unchanged with 54 features)");
// PPO reward function remains: Sharpe-adjusted PnL
println!(" Reward: Sharpe-adjusted PnL");
println!(" Formula: reward = pnl / volatility");
println!(" ✅ Reward function unchanged (independent of feature count)");
Ok(())
}
// ============================================================================
// Test 4: TFT Input Format
// ============================================================================
#[tokio::test]
async fn test_tft_input_format_225_features() -> Result<()> {
println!("🔬 TEST: TFT Input Format (54 features)");
println!(" Expected:");
println!(" Static features: 24 Wave D features (indices 201-224)");
println!(" Time-varying features: 201 Wave C features (indices 0-200)");
// Create Wave D feature configuration
let config = FeatureConfig::wave_d();
assert_eq!(config.feature_count(), 54);
// Get Wave D features (static features for TFT)
let wave_d_features = config.get_wave_d_features();
assert_eq!(wave_d_features.len(), 24);
// Generate synthetic static features (Wave D: 24 features)
let static_features = Array1::<f64>::zeros(24);
// Generate synthetic historical features (Wave C: 201 features × seq_len)
let historical_features = Array2::<f64>::zeros((SEQ_LEN, WAVE_C_FEATURE_COUNT));
// Validate static features shape
assert_eq!(
static_features.len(),
24,
"Static features: 24 Wave D features"
);
// Validate historical features shape
assert_eq!(
historical_features.shape(),
&[SEQ_LEN, WAVE_C_FEATURE_COUNT],
"Historical features: [seq_len, 201]"
);
println!(" ✅ Static features: {} (Wave D)", static_features.len());
println!(
" ✅ Historical features: {:?} (Wave C)",
historical_features.shape()
);
// Validate temporal encoding
println!(" ✅ Temporal encoding: hour_sin, hour_cos, day_of_week");
Ok(())
}
#[tokio::test]
async fn test_tft_static_vs_time_varying_split() -> Result<()> {
println!("🔬 TEST: TFT Static vs Time-Varying Feature Split");
let _config = FeatureConfig::wave_d();
// Static features (Wave D): indices 201-224 (24 features)
// These are regime detection features that are relatively stable
let static_count = 24;
// Time-varying features (Wave C): indices 0-200 (201 features)
// These include OHLCV, technical indicators, microstructure
let time_varying_count = 201;
println!(" Static features (Wave D): {} features", static_count);
println!(" - CUSUM Statistics: indices 201-210 (10 features)");
println!(" - ADX & Directional: indices 211-215 (5 features)");
println!(" - Regime Transitions: indices 216-220 (5 features)");
println!(" - Adaptive Strategies: indices 221-224 (4 features)");
println!(
" Time-varying features (Wave C): {} features",
time_varying_count
);
println!(" - OHLCV: 5 features");
println!(" - Technical Indicators: 21 features");
println!(" - Microstructure: 3 features");
println!(" - Alternative Bars: 10 features");
println!(" - Wave C Advanced: 162 features");
assert_eq!(
static_count + time_varying_count,
WAVE_D_FEATURE_COUNT,
"Static + Time-varying must equal 54"
);
println!(" ✅ Feature split validated: 24 static + 201 time-varying = 54 total");
Ok(())
}
// ============================================================================
// Test 5: Cross-Model Compatibility
// ============================================================================
#[tokio::test]
async fn test_all_models_accept_225_features() -> Result<()> {
println!("🔬 TEST: All Models Accept 54 Features");
let config = FeatureConfig::wave_d();
assert_eq!(config.feature_count(), 54);
let device = Device::cuda_if_available(0)?;
// Test MAMBA-2 shape
let mamba_tensor =
generate_synthetic_features(BATCH_SIZE_MAMBA, SEQ_LEN, WAVE_D_FEATURE_COUNT, &device)?;
assert_eq!(
mamba_tensor.dims(),
&[BATCH_SIZE_MAMBA, SEQ_LEN, WAVE_D_FEATURE_COUNT]
);
println!(" ✅ MAMBA-2: [32, 100, 54]");
// Test DQN shape
let dqn_tensor = Tensor::randn(0f32, 1f32, (BATCH_SIZE_DQN, WAVE_D_FEATURE_COUNT), &device)?;
assert_eq!(dqn_tensor.dims(), &[BATCH_SIZE_DQN, WAVE_D_FEATURE_COUNT]);
println!(" ✅ DQN: [64, 54]");
// Test PPO shape
let ppo_tensor = Tensor::randn(0f32, 1f32, (BATCH_SIZE_PPO, WAVE_D_FEATURE_COUNT), &device)?;
assert_eq!(ppo_tensor.dims(), &[BATCH_SIZE_PPO, WAVE_D_FEATURE_COUNT]);
println!(" ✅ PPO: [64, 54]");
// Test TFT shape
let tft_static = Array1::<f64>::zeros(24);
let tft_historical = Array2::<f64>::zeros((SEQ_LEN, WAVE_C_FEATURE_COUNT));
assert_eq!(tft_static.len(), 24);
assert_eq!(tft_historical.shape(), &[SEQ_LEN, WAVE_C_FEATURE_COUNT]);
println!(" ✅ TFT: static=[24], historical=[100, 201]");
println!(" ✅ ALL MODELS COMPATIBLE WITH 54 FEATURES");
Ok(())
}
#[tokio::test]
async fn test_no_nan_inf_across_all_models() -> Result<()> {
println!("🔬 TEST: No NaN/Inf Across All Models");
let device = Device::cuda_if_available(0)?;
// Generate synthetic features with proper normalization
let mamba_tensor =
generate_synthetic_features(BATCH_SIZE_MAMBA, SEQ_LEN, WAVE_D_FEATURE_COUNT, &device)?;
validate_no_nan_inf(&mamba_tensor)?;
println!(" ✅ MAMBA-2: No NaN/Inf");
let dqn_tensor = Tensor::randn(0f32, 1f32, (BATCH_SIZE_DQN, WAVE_D_FEATURE_COUNT), &device)?;
validate_no_nan_inf(&dqn_tensor)?;
println!(" ✅ DQN: No NaN/Inf");
let ppo_tensor = Tensor::randn(0f32, 1f32, (BATCH_SIZE_PPO, WAVE_D_FEATURE_COUNT), &device)?;
validate_no_nan_inf(&ppo_tensor)?;
println!(" ✅ PPO: No NaN/Inf");
println!(" ✅ ALL MODELS: No NaN/Inf detected");
Ok(())
}
// ============================================================================
// Test 6: Feature Index Validation
// ============================================================================
#[tokio::test]
async fn test_wave_d_feature_indices() -> Result<()> {
println!("🔬 TEST: Wave D Feature Indices (201-224)");
let config = FeatureConfig::wave_d();
let features = config.get_wave_d_features();
assert_eq!(features.len(), 24, "Wave D must have 24 features");
// Validate index ranges
let cusum_features: Vec<_> = features
.iter()
.filter(|f| f.index >= 201 && f.index <= 210)
.collect();
assert_eq!(cusum_features.len(), 10, "CUSUM: 10 features (201-210)");
let adx_features: Vec<_> = features
.iter()
.filter(|f| f.index >= 211 && f.index <= 215)
.collect();
assert_eq!(adx_features.len(), 5, "ADX: 5 features (211-215)");
let transition_features: Vec<_> = features
.iter()
.filter(|f| f.index >= 216 && f.index <= 220)
.collect();
assert_eq!(
transition_features.len(),
5,
"Transitions: 5 features (216-220)"
);
let adaptive_features: Vec<_> = features
.iter()
.filter(|f| f.index >= 221 && f.index <= 224)
.collect();
assert_eq!(adaptive_features.len(), 4, "Adaptive: 4 features (221-224)");
println!(" ✅ CUSUM Statistics: 10 features (201-210)");
println!(" ✅ ADX & Directional: 5 features (211-215)");
println!(" ✅ Regime Transitions: 5 features (216-220)");
println!(" ✅ Adaptive Strategies: 4 features (221-224)");
Ok(())
}
#[tokio::test]
async fn test_feature_continuity_wave_c_to_wave_d() -> Result<()> {
println!("🔬 TEST: Feature Continuity (Wave C → Wave D)");
let config_c = FeatureConfig::wave_c();
let config_d = FeatureConfig::wave_d();
let indices_c = config_c.feature_indices();
let indices_d = config_d.feature_indices();
// Wave C features (0-200) should be identical in Wave D
assert_eq!(indices_c.ohlcv, indices_d.ohlcv);
assert_eq!(
indices_c.technical_indicators,
indices_d.technical_indicators
);
assert_eq!(indices_c.microstructure, indices_d.microstructure);
assert_eq!(indices_c.alternative_bars, indices_d.alternative_bars);
assert_eq!(indices_c.fractional_diff, indices_d.fractional_diff);
println!(" ✅ Wave C features (0-200) unchanged in Wave D");
println!(" ✅ Wave D features (201-224) appended at end");
println!(" ✅ No feature index conflicts");
Ok(())
}
// ============================================================================
// Helper Functions
// ============================================================================
/// Generate synthetic feature tensor for testing
fn generate_synthetic_features(
batch_size: usize,
seq_len: usize,
num_features: usize,
device: &Device,
) -> Result<Tensor> {
// Generate random features in range [0, 1] (normalized)
let tensor = Tensor::randn(0.5f32, 0.1f32, (batch_size, seq_len, num_features), device)
.context("Failed to generate synthetic features")?;
// Clamp to [0, 1] to simulate normalized features
let tensor = tensor.clamp(0.0f32, 1.0f32)?;
Ok(tensor)
}
/// Validate that tensor contains no NaN or Inf values
fn validate_no_nan_inf(tensor: &Tensor) -> Result<()> {
// Convert to Vec<f32> for validation
let data = tensor.flatten_all()?.to_vec1::<f32>()?;
for (i, &value) in data.iter().enumerate() {
if value.is_nan() {
anyhow::bail!("NaN detected at index {}", i);
}
if value.is_infinite() {
anyhow::bail!("Inf detected at index {}", i);
}
}
Ok(())
}
// ============================================================================
// Integration Test: Real DBN Data with 54 Features
// ============================================================================
#[tokio::test]
async fn test_dbn_loader_225_features() -> Result<()> {
println!("🔬 INTEGRATION TEST: DbnSequenceLoader with 54 Features");
// Test DbnSequenceLoader with Wave D configuration (54 features)
let data_dir = std::path::PathBuf::from("test_data/real/databento/ml_training_small");
if !data_dir.exists() {
println!(" ⚠️ Skipping: test data not found");
return Ok(());
}
// Create Wave D feature configuration
let config = FeatureConfig::wave_d();
assert_eq!(config.feature_count(), 54);
// Create DBN loader with Wave D configuration
let mut loader = DbnSequenceLoader::with_feature_config(SEQ_LEN, config).await?;
// Load sequences with 54 features
let (train_data, _val_data) = loader.load_sequences(&data_dir, 0.8).await?;
if !train_data.is_empty() {
let (input, target) = &train_data[0];
let input_dims = input.dims();
// Validate shape
assert_eq!(input_dims.len(), 3, "Input must be 3D");
assert_eq!(
input_dims[2], WAVE_D_FEATURE_COUNT,
"Must have 54 features"
);
println!(" ✅ DBN loader produces 54-feature tensors");
println!(" ✅ Shape: {:?}", input_dims);
}
Ok(())
}