//! Validation: Test that SharedMLStrategy correctly passes features from //! ProductionFeatureExtractorAdapter to model adapters. //! //! NOTE: ProductionFeatureExtractorAdapter currently produces 51 features //! (43 base + 8 OFI placeholders). Full 225 features require MBP-10 data //! and sequence-level feature engineering done at training time. use anyhow::Result; use chrono::Utc; use common::ml_strategy::{MLModelAdapter, MLPrediction, SharedMLStrategy}; use ml::features::ProductionFeatureExtractorAdapter; /// Mock adapter that captures features from predictions struct FeatureCapturingAdapter { id: String, } impl FeatureCapturingAdapter { fn new(id: &str) -> Self { Self { id: id.to_string(), } } } impl MLModelAdapter for FeatureCapturingAdapter { fn predict(&self, features: &[f64]) -> Result { Ok(MLPrediction { model_id: self.id.clone(), prediction_value: 0.5, confidence: 1.0, // Always pass threshold so we can inspect features features: features.to_vec(), timestamp: Utc::now(), inference_latency_us: 10, }) } fn model_id(&self) -> &str { &self.id } fn validate_prediction(&mut self, _prediction: &MLPrediction, _actual_outcome: bool) {} } #[tokio::test] async fn test_production_extractor_feature_count() { let extractor = Box::new(ProductionFeatureExtractorAdapter::new()); let strategy = SharedMLStrategy::new( extractor, vec![Box::new(FeatureCapturingAdapter::new("capture_v1"))], 0.0, ); // Warm up the feature extractor with historical data for i in 0..100 { let price = 100.0 + (i as f64 * 0.1); let volume = 1000.0 + (i as f64 * 10.0); let _ = strategy .get_ensemble_prediction(price, volume, Utc::now()) .await; } let predictions = strategy .get_ensemble_prediction(100.0, 1000.0, Utc::now()) .await .expect("Should extract features successfully"); assert!( !predictions.is_empty(), "Should have at least one prediction" ); let features = &predictions[0].features; // ProductionFeatureExtractorAdapter produces 51 features (43 base + 8 OFI placeholders) assert_eq!( features.len(), 51, "ProductionFeatureExtractorAdapter should produce 51 features, got {}", features.len() ); // All features must be finite (no NaN, no Inf) for (i, f) in features.iter().enumerate() { assert!(f.is_finite(), "Feature at index {} is not finite: {}", i, f); } } #[tokio::test] async fn test_feature_extraction_all_finite() { let extractor = Box::new(ProductionFeatureExtractorAdapter::new()); let strategy = SharedMLStrategy::new( extractor, vec![Box::new(FeatureCapturingAdapter::new("capture_v1"))], 0.0, ); // Warm up for i in 0..100 { let price = 100.0 + (i as f64 * 0.1); let volume = 1000.0 + (i as f64 * 10.0); let _ = strategy .get_ensemble_prediction(price, volume, Utc::now()) .await; } let predictions = strategy .get_ensemble_prediction(100.0, 1000.0, Utc::now()) .await .expect("Should extract features successfully"); let features = &predictions[0].features; // All 51 features should be finite for i in 0..features.len() { let value = features.get(i); assert!(value.is_some(), "Feature at index {} is missing", i); assert!( value.unwrap().is_finite(), "Feature at index {} is not finite: {}", i, value.unwrap() ); } }