Files
foxhunt/crates/common/tests/test_sharedml_225_features.rs
jgrusewski 5401723118 refactor(common): delete MLFeatureExtractor + SimpleDQNAdapter, refactor SharedMLStrategy
- Delete MLFeatureExtractor (1,294 lines) and SimpleDQNAdapter (235 lines)
- Delete 830 lines of inline tests for deleted types
- Remove legacy_feature_extractor field from SharedMLStrategy
- Replace new() and new_with_production_extractor() with new(extractor, models, threshold)
- Single constructor accepts injected models via Vec<Box<dyn MLModelAdapter>>
- Update all callers: backtesting_service, 2 integration tests, 2 trading_service tests
- Fix doc comments referencing MLFeatureExtractor
- Fix feature count test: real extractor produces 51 features, not 225

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 18:47:32 +01:00

126 lines
3.7 KiB
Rust

//! 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<MLPrediction> {
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()
);
}
}