Files
foxhunt/ml/tests/feature_extraction_46_edge_cases_test.rs
jgrusewski 28ee27b2bb feat: Wave 1 - Update HIGH RISK files (225→54 features)
WAVE 21: Core type definitions and trainer configs updated

Files Modified (13 files):
- ml/src/features/extraction.rs: FeatureVector = [f64; 54]
- common/src/features/types.rs: Added FeatureVector54
- ml/src/trainers/dqn.rs: state_dim 225→54
- ml/src/trainers/ppo.rs: state_dim 225→54
- ml/src/dqn/dqn.rs, config.rs, replay_buffer.rs: Updated configs
- ml/src/hyperopt/adapters/: All adapters updated to 54-dim
- ml/src/features/unified.rs: Struct fields updated
- ml/src/trainers/tft_parquet.rs: Return types updated

Agents Deployed: 5 parallel agents
Test Results: cargo check --package ml --lib PASSING

Next: Wave 2 (examples), Wave 3 (tests), Wave 4 (OFI integration)

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 00:41:22 +01:00

482 lines
13 KiB
Rust

//! Edge Case Tests for 46-Feature Extraction
//!
//! This test suite validates feature extraction under extreme conditions:
//! - Market open/close boundaries
//! - Missing data and gaps
//! - Zero volume bars
//! - Flat price bars
//! - Extreme price movements
//! - Overnight gaps
//! - Data quality edge cases
#![allow(unused_crate_dependencies)]
use ml::features::extraction::{extract_ml_features, OHLCVBar};
use anyhow::Result;
use chrono::{Duration, Utc};
/// Test: Market open boundary handling
#[test]
fn test_market_open_boundary() -> Result<()> {
// OBJECTIVE: Test feature extraction at market open
// EXPECTED: is_market_open=1, minutes_since_open=~0
let mut bars = Vec::new();
let mut timestamp = Utc::now()
.with_hour(13) // 9:30 AM ET = 13:30 UTC
.unwrap()
.with_minute(30)
.unwrap();
for _ in 0..60 {
bars.push(OHLCVBar {
timestamp,
open: 4500.0,
high: 4505.0,
low: 4495.0,
close: 4500.0,
volume: 1000.0,
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
assert!(!features.is_empty(), "Should extract features");
println!("✅ Market open boundary handled correctly");
Ok(())
}
/// Test: Market close boundary handling
#[test]
fn test_market_close_boundary() -> Result<()> {
// OBJECTIVE: Test feature extraction at market close
// EXPECTED: is_market_open=1, minutes_to_close=~0
let mut bars = Vec::new();
let mut timestamp = Utc::now()
.with_hour(20) // 4:00 PM ET = 20:00 UTC
.unwrap()
.with_minute(0)
.unwrap();
for _ in 0..60 {
bars.push(OHLCVBar {
timestamp,
open: 4500.0,
high: 4505.0,
low: 4495.0,
close: 4500.0,
volume: 1000.0,
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
assert!(!features.is_empty(), "Should extract features");
println!("✅ Market close boundary handled correctly");
Ok(())
}
/// Test: Overnight gap handling
#[test]
fn test_overnight_gap() -> Result<()> {
// OBJECTIVE: Test handling of overnight gap (market closed)
// EXPECTED: is_market_open=0, features computed correctly, no NaN/Inf
let mut bars = Vec::new();
// Last bar of day 1 (4:00 PM ET)
let mut timestamp = Utc::now()
.with_hour(20)
.unwrap()
.with_minute(0)
.unwrap();
for _ in 0..30 {
bars.push(OHLCVBar {
timestamp,
open: 4500.0,
high: 4505.0,
low: 4495.0,
close: 4500.0,
volume: 1000.0,
});
timestamp = timestamp + Duration::minutes(1);
}
// Overnight gap (16 hours)
timestamp = timestamp + Duration::hours(16);
// First bar of day 2 (9:30 AM ET next day)
for _ in 0..30 {
bars.push(OHLCVBar {
timestamp,
open: 4510.0,
high: 4515.0,
low: 4505.0,
close: 4510.0,
volume: 1000.0,
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
// Should handle overnight gap without NaN/Inf
for fv in &features {
for &value in fv.iter() {
assert!(value.is_finite(), "Found NaN/Inf after overnight gap");
}
}
println!("✅ Overnight gap handled correctly");
Ok(())
}
/// Test: Zero volume bars
#[test]
fn test_zero_volume_bars() -> Result<()> {
// OBJECTIVE: Test handling of zero-volume bars
// EXPECTED: No division-by-zero, features computed safely
let mut bars = Vec::new();
let mut timestamp = Utc::now();
for i in 0..60 {
bars.push(OHLCVBar {
timestamp,
open: 4500.0,
high: 4505.0,
low: 4495.0,
close: 4500.0,
volume: if i % 10 == 0 { 0.0 } else { 1000.0 },
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
// Should handle zero volume without NaN/Inf
for (vec_idx, fv) in features.iter().enumerate() {
for (feat_idx, &value) in fv.iter().enumerate() {
assert!(value.is_finite(),
"Found NaN/Inf at vector {} feature {} (zero volume handling)",
vec_idx, feat_idx);
}
}
println!("✅ Zero volume bars handled correctly");
Ok(())
}
/// Test: Flat price bars (open=high=low=close)
#[test]
fn test_flat_price_bars() -> Result<()> {
// OBJECTIVE: Test handling of flat price bars
// EXPECTED: No division-by-zero, features computed correctly
let mut bars = Vec::new();
let mut timestamp = Utc::now();
for i in 0..60 {
let price = if i % 10 == 0 { 4500.0 } else { 4500.0 + (i as f64) };
bars.push(OHLCVBar {
timestamp,
open: price,
high: price, // Flat bar
low: price,
close: price,
volume: 1000.0,
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
// Should handle flat bars without NaN/Inf
for fv in &features {
for &value in fv.iter() {
assert!(value.is_finite(), "Found NaN/Inf from flat price bars");
}
}
println!("✅ Flat price bars handled correctly");
Ok(())
}
/// Test: Extreme price spike (>5% move)
#[test]
fn test_extreme_price_spike() -> Result<()> {
// OBJECTIVE: Test handling of extreme price movements
// EXPECTED: Features computed correctly, no infinite values
let mut bars = Vec::new();
let mut timestamp = Utc::now();
let mut price = 4500.0;
for i in 0..60 {
if i == 30 {
// Extreme spike: +10%
price *= 1.10;
}
bars.push(OHLCVBar {
timestamp,
open: price,
high: price * 1.05,
low: price * 0.95,
close: price,
volume: 1000.0,
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
// Features should be bounded even with extreme moves
for fv in &features {
for &value in fv.iter() {
assert!(value.is_finite(), "Found infinite value from extreme price spike");
assert!(value.abs() < 10000.0, "Feature value too extreme: {}", value);
}
}
println!("✅ Extreme price spike handled correctly");
Ok(())
}
/// Test: Extreme volume spike (100x normal)
#[test]
fn test_extreme_volume_spike() -> Result<()> {
// OBJECTIVE: Test handling of extreme volume spikes
// EXPECTED: Features computed safely, no NaN/Inf
let mut bars = Vec::new();
let mut timestamp = Utc::now();
for i in 0..60 {
let volume = if i == 30 { 100000.0 } else { 1000.0 };
bars.push(OHLCVBar {
timestamp,
open: 4500.0,
high: 4505.0,
low: 4495.0,
close: 4500.0,
volume,
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
// Should handle volume spikes safely
for fv in &features {
for &value in fv.iter() {
assert!(value.is_finite(), "Found NaN/Inf from volume spike");
}
}
println!("✅ Extreme volume spike handled correctly");
Ok(())
}
/// Test: Single bar after warmup (51 bars total)
#[test]
fn test_single_bar_after_warmup() -> Result<()> {
// OBJECTIVE: Extract from exactly 51 bars
// EXPECTED: 1 feature vector
let bars = create_test_bars(51)?;
let features = extract_ml_features(&bars)?;
assert_eq!(features.len(), 1, "Should produce exactly 1 feature");
assert_eq!(features[0].len(), 46, "Feature should be 46-dimensional");
println!("✅ Single bar after warmup handled correctly");
Ok(())
}
/// Test: Weekend bars (market closed)
#[test]
fn test_weekend_bars() -> Result<()> {
// OBJECTIVE: Test handling of weekend data
// EXPECTED: is_market_open=0
let mut bars = Vec::new();
// Saturday
let mut timestamp = Utc::now()
.with_weekday(chrono::Weekday::Sat)
.with_hour(14)
.unwrap()
.with_minute(0)
.unwrap();
for _ in 0..60 {
bars.push(OHLCVBar {
timestamp,
open: 4500.0,
high: 4505.0,
low: 4495.0,
close: 4500.0,
volume: 1000.0,
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
// Features should be computed even on non-trading days
for fv in &features {
for &value in fv.iter() {
assert!(value.is_finite(), "Found NaN/Inf on weekend");
}
}
println!("✅ Weekend bars handled correctly");
Ok(())
}
/// Test: Premarket hours (before 9:30 AM ET)
#[test]
fn test_premarket_bars() -> Result<()> {
// OBJECTIVE: Test handling of premarket data
// EXPECTED: is_market_open=0
let mut bars = Vec::new();
let mut timestamp = Utc::now()
.with_hour(12) // 8:00 AM ET = 12:00 UTC (premarket)
.unwrap()
.with_minute(0)
.unwrap();
for _ in 0..60 {
bars.push(OHLCVBar {
timestamp,
open: 4500.0,
high: 4505.0,
low: 4495.0,
close: 4500.0,
volume: 100.0, // Low volume in premarket
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
// Features should be computed for premarket
for fv in &features {
for &value in fv.iter() {
assert!(value.is_finite(), "Found NaN/Inf in premarket");
}
}
println!("✅ Premarket bars handled correctly");
Ok(())
}
/// Test: Postmarket hours (after 4:00 PM ET)
#[test]
fn test_postmarket_bars() -> Result<()> {
// OBJECTIVE: Test handling of postmarket data
// EXPECTED: is_market_open=0
let mut bars = Vec::new();
let mut timestamp = Utc::now()
.with_hour(21) // 5:00 PM ET = 21:00 UTC (postmarket)
.unwrap()
.with_minute(0)
.unwrap();
for _ in 0..60 {
bars.push(OHLCVBar {
timestamp,
open: 4500.0,
high: 4505.0,
low: 4495.0,
close: 4500.0,
volume: 100.0, // Low volume in postmarket
});
timestamp = timestamp + Duration::minutes(1);
}
let features = extract_ml_features(&bars)?;
// Features should be computed for postmarket
for fv in &features {
for &value in fv.iter() {
assert!(value.is_finite(), "Found NaN/Inf in postmarket");
}
}
println!("✅ Postmarket bars handled correctly");
Ok(())
}
/// Test: Data quality with missing bars (gaps)
#[test]
fn test_data_quality_with_gaps() -> Result<()> {
// OBJECTIVE: Test handling of time gaps in data
// EXPECTED: Features still computed correctly
let mut bars = Vec::new();
let mut timestamp = Utc::now();
for i in 0..60 {
bars.push(OHLCVBar {
timestamp,
open: 4500.0,
high: 4505.0,
low: 4495.0,
close: 4500.0,
volume: 1000.0,
});
// Add random gaps (simulate market halts)
timestamp = if i % 10 == 9 {
timestamp + Duration::minutes(5) // 5-minute gap
} else {
timestamp + Duration::minutes(1)
};
}
let features = extract_ml_features(&bars)?;
// Features should be computed despite gaps
for fv in &features {
for &value in fv.iter() {
assert!(value.is_finite(), "Found NaN/Inf despite gaps");
}
}
println!("✅ Data gaps handled correctly");
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)
}