- 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>
126 lines
3.7 KiB
Rust
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()
|
|
);
|
|
}
|
|
}
|