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

233 lines
8.3 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.
//! WAVE 15 (Agent 33): Feature Audit and Cleanup Test
//!
//! This test documents the baseline 54-feature state before cleanup and validates
//! the 125-feature state after unstable feature removal.
//!
//! **Agent 29 Findings** (Primary instability causes):
//! 1. **Statistical Features (indices 175-200)**: Skewness/kurtosis EXTREMELY UNSTABLE
//! - Can jump from 0 → 3 in single bar with one outlier
//! - PRIMARY SUSPECT for gradient explosions
//! 2. **Microstructure Features (indices 115-164)**: Division by zero risk
//! - `amihud_illiquidity = |Return| / Volume` → ∞ when Volume → 0
//! 3. **Redundant TA Indicators**: 80+ feature pairs with correlation >0.95
//! - Multiple momentum variants, RSI variants, MACD variants
//!
//! **Removal Plan** (100 features total):
//! - Statistical: Remove 6/26 (skewness × 3, kurtosis × 3)
//! - Microstructure: Remove 30/50 (Amihud + 28 placeholders, keep Roll + Corwin-Schultz)
//! - Price patterns: Remove 45/60 (redundant momentum/trend indicators)
//! - Volume patterns: Remove 19/40 (redundant volume ratios)
//! - **Result**: 54 → 125 features
use anyhow::Result;
use chrono::Utc;
use ml::features::extraction::{extract_ml_features, OHLCVBar};
/// Create test OHLCV bars with controlled characteristics
fn create_test_bars(count: usize) -> Vec<OHLCVBar> {
(0..count)
.map(|i| OHLCVBar {
timestamp: Utc::now(),
open: 100.0 + (i as f64 * 0.1),
high: 101.0 + (i as f64 * 0.1),
low: 99.0 + (i as f64 * 0.1),
close: 100.5 + (i as f64 * 0.1),
volume: 10000.0 + (i as f64 * 100.0),
})
.collect()
}
/// Create test bars with outlier to demonstrate skewness/kurtosis instability
fn create_bars_with_outlier(
count: usize,
outlier_idx: usize,
outlier_magnitude: f64,
) -> Vec<OHLCVBar> {
(0..count)
.map(|i| {
let base_price = 100.0;
let price = if i == outlier_idx {
base_price + outlier_magnitude // Outlier
} else {
base_price + (i as f64 * 0.01) // Normal price movement
};
OHLCVBar {
timestamp: Utc::now(),
open: price,
high: price * 1.01,
low: price * 0.99,
close: price,
volume: 10000.0,
}
})
.collect()
}
#[test]
#[ignore] // Will fail after cleanup (expected)
fn test_feature_count_before_cleanup() {
// BASELINE: 54 features before cleanup
let bars = create_test_bars(60);
let features = extract_ml_features(&bars).expect("Feature extraction failed");
assert!(!features.is_empty(), "Should extract features after warmup");
let feature_vec = features.last().unwrap();
assert_eq!(
feature_vec.len(),
54,
"Baseline: 54 features before cleanup (indices 0-224)"
);
}
#[test]
fn test_feature_count_after_cleanup() {
// AFTER CLEANUP: 125 stable features
let bars = create_test_bars(60);
let features = extract_ml_features(&bars).expect("Feature extraction failed");
assert!(!features.is_empty(), "Should extract features after warmup");
let feature_vec = features.last().unwrap();
assert_eq!(feature_vec.len(), 125, "After cleanup: 125 stable features");
}
#[test]
fn test_unstable_features_removed() {
// Verify specific unstable features are removed
let bars = create_test_bars(60);
let features = extract_ml_features(&bars).expect("Feature extraction failed");
let feature_vec = features.last().unwrap();
// After cleanup, feature vector should be 125
assert_eq!(feature_vec.len(), 125, "Feature count should be 125");
// Verify all features are finite (no NaN/Inf)
for (i, &val) in feature_vec.iter().enumerate() {
assert!(
val.is_finite(),
"Feature {} should be finite, got: {}",
i,
val
);
}
}
#[test]
#[ignore] // Demonstrates instability - will be fixed after cleanup
fn test_skewness_instability_demonstration() {
// Demonstrate that skewness is EXTREMELY UNSTABLE with outliers
// Test case 1: No outlier
let bars_normal = create_bars_with_outlier(60, 999, 0.0); // No outlier
let features_normal = extract_ml_features(&bars_normal).expect("Extraction failed");
let vec_normal = features_normal.last().unwrap();
// Test case 2: Single outlier (+50 points)
let bars_outlier = create_bars_with_outlier(60, 55, 50.0); // Outlier at index 55
let features_outlier = extract_ml_features(&bars_outlier).expect("Extraction failed");
let vec_outlier = features_outlier.last().unwrap();
// In 54-feature system:
// - Skewness is at indices 178-180 (features 175-200 are statistical)
// - One outlier can cause skewness to jump from ~0 → ~3
//
// This test will PASS before cleanup (demonstrating instability)
// This test will be REMOVED after cleanup (skewness features removed)
if vec_normal.len() == 54 {
// Before cleanup: Statistical features at indices 175-200
let skew_5_normal = vec_normal[178];
let skew_5_outlier = vec_outlier[178];
let skewness_delta = (skew_5_outlier - skew_5_normal).abs();
// Demonstrate instability: Single outlier causes massive skewness jump
assert!(
skewness_delta > 1.0,
"Skewness should jump by >1.0 with single outlier, got delta: {}",
skewness_delta
);
println!("❌ INSTABILITY DEMONSTRATED:");
println!(" Skewness (no outlier): {:.4}", skew_5_normal);
println!(" Skewness (1 outlier): {:.4}", skew_5_outlier);
println!(
" Delta: {:.4} (>1.0 = UNSTABLE)",
skewness_delta
);
}
}
#[test]
fn test_feature_stability_after_cleanup() {
// After cleanup: Features should be STABLE with outliers
// Test case 1: No outlier
let bars_normal = create_bars_with_outlier(60, 999, 0.0);
let features_normal = extract_ml_features(&bars_normal).expect("Extraction failed");
let vec_normal = features_normal.last().unwrap();
// Test case 2: Single outlier (+50 points)
let bars_outlier = create_bars_with_outlier(60, 55, 50.0);
let features_outlier = extract_ml_features(&bars_outlier).expect("Extraction failed");
let vec_outlier = features_outlier.last().unwrap();
// After cleanup: No feature should jump >3 standard deviations
let mut max_delta = 0.0;
let mut unstable_feature_idx = None;
for i in 0..vec_normal.len().min(vec_outlier.len()) {
let delta = (vec_outlier[i] - vec_normal[i]).abs();
if delta > max_delta {
max_delta = delta;
unstable_feature_idx = Some(i);
}
}
assert!(
max_delta < 3.0,
"Feature {} has excessive jump: {:.2} (threshold: 3.0) - unstable feature not removed!",
unstable_feature_idx.unwrap_or(0),
max_delta
);
println!("✅ STABILITY VERIFIED:");
println!(" Max feature delta: {:.4} (threshold: 3.0)", max_delta);
println!(" All features stable with outlier present");
}
#[test]
fn test_removed_features_documented() {
// Document which features were removed
let removed_features = vec![
("Statistical: Skewness (5-period)", "Index 178 → REMOVED"),
("Statistical: Skewness (10-period)", "Index 179 → REMOVED"),
("Statistical: Skewness (20-period)", "Index 180 → REMOVED"),
("Statistical: Kurtosis (5-period)", "Index 181 → REMOVED"),
("Statistical: Kurtosis (10-period)", "Index 182 → REMOVED"),
("Statistical: Kurtosis (20-period)", "Index 183 → REMOVED"),
(
"Microstructure: Amihud Illiquidity",
"Index 116 → REMOVED (div-by-zero risk)",
),
(
"Microstructure: 28 placeholders",
"Indices 122-149 → REMOVED",
),
(
"Price Patterns: 45 redundant TA",
"Various indices → REMOVED (>0.95 correlation)",
),
("Volume Patterns: 19 redundant", "Various indices → REMOVED"),
];
println!("\n📋 REMOVED FEATURES SUMMARY:");
for (feature_name, status) in removed_features {
println!(" - {}: {}", feature_name, status);
}
println!("\n TOTAL REMOVED: 100 features");
println!(" REMAINING: 125 stable features\n");
}