//! Wave C Features 1-50 Validation Script (Agent F1) //! //! This script validates the first 50 features from the 256-feature extraction system: //! - Features 0-4: OHLCV (open, high, low, close, volume) //! - Features 5-14: Technical Indicators (RSI, EMA, MACD, Bollinger, ATR) //! - Features 15-49: Price Patterns & Volume Analysis //! //! Expected behavior: //! - Load real DBN data (ES.FUT) //! - Extract features 0-49 from the 256-feature extraction system //! - Validate no NaN/Inf values //! - Measure extraction latency (<1ms per bar target) //! - Report pass/fail status use anyhow::{Context, Result}; use chrono::{TimeZone, Utc}; use dbn::decode::{DbnDecoder, DecodeRecordRef}; use dbn::OhlcvMsg; use ml::features::extraction::OHLCVBar; use std::time::Instant; fn main() -> Result<()> { println!("=== Agent F1: Features 1-50 Validation Report ===\n"); // Stage 1: Load real DBN data println!("### Stage 1: Loading DBN Data"); let dbn_path = "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ml_training/ES.FUT_ohlcv-1m_2024-03-25.dbn"; println!(" - File: {}", dbn_path); let mut decoder = DbnDecoder::from_file(dbn_path) .with_context(|| format!("Failed to open DBN file: {}", dbn_path))?; // Decode OHLCV records let mut bars = Vec::new(); let mut record_count = 0; while let Some(record_ref) = decoder .decode_record_ref() .context("Failed to decode DBN record")? { if let Some(ohlcv) = record_ref.get::() { record_count += 1; // Convert timestamp let ts_nanos = ohlcv.hd.ts_event as i64; let secs = ts_nanos / 1_000_000_000; let nanos = (ts_nanos % 1_000_000_000) as u32; let timestamp = Utc .timestamp_opt(secs, nanos) .single() .ok_or_else(|| anyhow::anyhow!("Invalid timestamp: {}", ts_nanos))?; // Convert prices (fixed-point to f64) let bar = OHLCVBar { timestamp, open: ohlcv.open as f64 / 1_000_000_000.0, high: ohlcv.high as f64 / 1_000_000_000.0, low: ohlcv.low as f64 / 1_000_000_000.0, close: ohlcv.close as f64 / 1_000_000_000.0, volume: ohlcv.volume as f64, }; bars.push(bar); } } println!(" - Total records loaded: {}", record_count); println!(" - Total bars: {}\n", bars.len()); if bars.len() < 50 { anyhow::bail!( "Insufficient data: {} bars (need at least 50 for warmup)", bars.len() ); } // Stage 2: Feature Extraction Configuration println!("### Stage 2: Feature Extraction Setup"); println!(" - System: 256-feature extraction (extraction.rs)"); println!(" - Target features: 0-49 (first 50 features)"); println!(" - Warmup period: 50 bars\n"); // Stage 3: Extract features using the existing feature extraction system println!("### Stage 3: Feature Extraction"); let start = Instant::now(); let features = ml::features::extraction::extract_ml_features(&bars[..])?; let total_extraction_time = start.elapsed(); println!(" - Total bars processed: {}", bars.len()); println!(" - Feature vectors generated: {}", features.len()); println!(" - Total extraction time: {:?}", total_extraction_time); if features.is_empty() { anyhow::bail!("No features extracted (warmup period too long?)"); } // Calculate per-bar latency let avg_latency_per_bar = total_extraction_time.as_micros() as f64 / features.len() as f64; println!(" - Average latency per bar: {:.2}μs", avg_latency_per_bar); println!(" - Target: <1000μs (1ms) per bar"); let latency_status = if avg_latency_per_bar < 1000.0 { "PASS ✓" } else { "FAIL ✗" }; println!(" - Latency status: {}\n", latency_status); // Stage 4: Validation - Check features 0-49 println!("### Stage 4: Feature Validation (Features 0-49)"); let mut nan_count = 0; let mut inf_count = 0; let mut valid_count = 0; let mut feature_stats = vec![FeatureStats::default(); 50]; for feature_vec in &features { for i in 0..50.min(feature_vec.len()) { let val = feature_vec[i]; if val.is_nan() { nan_count += 1; } else if val.is_infinite() { inf_count += 1; } else { valid_count += 1; feature_stats[i].update(val); } } } let total_values = features.len() * 50; println!(" - Total values checked: {}", total_values); println!( " - Valid values: {} ({:.2}%)", valid_count, valid_count as f64 / total_values as f64 * 100.0 ); println!( " - NaN values: {} ({:.2}%)", nan_count, nan_count as f64 / total_values as f64 * 100.0 ); println!( " - Inf values: {} ({:.2}%)", inf_count, inf_count as f64 / total_values as f64 * 100.0 ); let validation_status = if nan_count == 0 && inf_count == 0 { "PASS ✓" } else { "FAIL ✗" }; println!(" - Validation status: {}\n", validation_status); // Stage 5: Feature Statistics println!("### Stage 5: Feature Statistics (First 10 Features)"); println!(" Idx | Min | Max | Mean | StdDev"); println!(" ----|-------------|-------------|-------------|-------------"); for i in 0..10.min(feature_stats.len()) { let stats = &feature_stats[i]; println!( " {:3} | {:11.6} | {:11.6} | {:11.6} | {:11.6}", i, stats.min, stats.max, stats.mean(), stats.stddev() ); } println!(); // Stage 6: Feature Names println!("### Features Validated (Indices 0-49)"); println!(" - Features 0-4: OHLCV (open, high, low, close, volume)"); println!(" - Features 5-14: Technical Indicators (RSI, EMA fast/slow, MACD, MACD signal, MACD histogram, BB middle/upper/lower, ATR)"); println!(" - Features 15-74: Price Patterns (returns, MA ratios, high/low analysis, trend detection, support/resistance, etc.)"); println!(" - Features 0-49 represent foundational features from the 256-feature extraction system\n"); // Final Report println!("### Validation Results"); println!(" - Total features tested: 50/50"); let features_passing = if nan_count == 0 && inf_count == 0 { 50 } else { 50 - ((nan_count + inf_count) / features.len()).min(50) }; println!(" - Features passing: {}/50", features_passing); println!(" - Features with issues: {}/50", 50 - features_passing); println!(); // Performance Metrics println!("### Performance Metrics"); println!( " - Average extraction latency: {:.2}μs per bar", avg_latency_per_bar ); println!(" - Target: <1000μs (1ms) per bar"); println!(" - Status: {}", latency_status); println!(); // Issues Found (if any) if nan_count > 0 || inf_count > 0 { println!("### Issues Found"); if nan_count > 0 { println!( " 1. NaN values detected: {} occurrences across {} feature vectors", nan_count, features.len() ); } if inf_count > 0 { println!( " 2. Inf values detected: {} occurrences across {} feature vectors", inf_count, features.len() ); } println!(); } // Final Status println!("### Status"); let final_status = if nan_count == 0 && inf_count == 0 && avg_latency_per_bar < 1000.0 { "COMPLETE ✓" } else if nan_count > 0 || inf_count > 0 { "BLOCKED - Invalid values detected ✗" } else { "PARTIAL - Latency target not met ⚠" }; println!(" {}", final_status); Ok(()) } #[derive(Debug, Clone, Default)] struct FeatureStats { min: f64, max: f64, sum: f64, sum_sq: f64, count: usize, } impl FeatureStats { fn update(&mut self, val: f64) { if self.count == 0 { self.min = val; self.max = val; } else { self.min = self.min.min(val); self.max = self.max.max(val); } self.sum += val; self.sum_sq += val * val; self.count += 1; } fn mean(&self) -> f64 { if self.count == 0 { 0.0 } else { self.sum / self.count as f64 } } fn stddev(&self) -> f64 { if self.count == 0 { 0.0 } else { let mean = self.mean(); let variance = (self.sum_sq / self.count as f64) - (mean * mean); variance.max(0.0).sqrt() } } }