Files
foxhunt/crates/ml/src/features/production_adapter.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

124 lines
3.7 KiB
Rust

//! Production Feature Extractor Adapter for Common ML Strategy
//!
//! This adapter bridges the ml::features::extraction::FeatureExtractor with
//! the common::ml_strategy::ProductionFeatureExtractor225 trait to inject
//! 225-feature extraction into SharedMLStrategy without circular dependencies.
use anyhow::Result;
use chrono::{DateTime, Utc};
use common::ml_strategy::ProductionFeatureExtractor225;
use super::extraction::{FeatureExtractor, OHLCVBar};
/// Production-grade 225-feature extractor adapter for SharedMLStrategy
///
/// This struct wraps the ml::features::extraction::FeatureExtractor and implements
/// the ProductionFeatureExtractor225 trait from common, enabling dependency injection
/// of the full 225-feature extractor into SharedMLStrategy.
///
/// # Usage
/// ```rust,ignore
/// use ml::features::ProductionFeatureExtractorAdapter;
/// use common::ml_strategy::SharedMLStrategy;
///
/// let extractor = Box::new(ProductionFeatureExtractorAdapter::new());
/// let strategy = SharedMLStrategy::new(extractor, vec![], 0.7);
/// ```
#[derive(Debug)]
pub struct ProductionFeatureExtractorAdapter {
inner: FeatureExtractor,
}
impl ProductionFeatureExtractorAdapter {
/// Create new production feature extractor adapter
pub fn new() -> Self {
Self {
inner: FeatureExtractor::new(),
}
}
}
impl Default for ProductionFeatureExtractorAdapter {
fn default() -> Self {
Self::new()
}
}
impl ProductionFeatureExtractor225 for ProductionFeatureExtractorAdapter {
fn update(&mut self, price: f64, volume: f64, timestamp: DateTime<Utc>) -> Result<()> {
// Convert price/volume to OHLCV bar (approximate high/low from price)
let bar = OHLCVBar {
timestamp,
open: price,
high: price * 1.001, // Approximate high (0.1% above close)
low: price * 0.999, // Approximate low (0.1% below close)
close: price,
volume,
};
// Update internal state
self.inner.update(&bar)
}
fn extract_features(&mut self) -> Result<Vec<f64>> {
// Extract 225-dimensional feature vector
let feature_array = self.inner.extract_current_features()?;
Ok(feature_array.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_adapter_basic_usage() -> Result<()> {
let mut adapter = ProductionFeatureExtractorAdapter::new();
// Feed 60 bars (warmup period = 50)
for i in 0..60 {
let price = 100.0 + i as f64;
let volume = 1000.0;
let timestamp = Utc::now();
adapter.update(price, volume, timestamp)?;
}
// Extract features
let features = adapter.extract_features()?;
// Validate 51 features
assert_eq!(features.len(), 51, "Should extract exactly 51 features");
// Validate all features are finite
for (i, &val) in features.iter().enumerate() {
assert!(
val.is_finite(),
"Feature {} should be finite, found {}",
i, val
);
}
Ok(())
}
#[test]
fn test_adapter_warmup_period() -> Result<()> {
let mut adapter = ProductionFeatureExtractorAdapter::new();
// Feed only warmup bars (50)
for i in 0..50 {
let price = 100.0 + i as f64;
let volume = 1000.0;
let timestamp = Utc::now();
adapter.update(price, volume, timestamp)?;
}
// Should be able to extract features after warmup
let features = adapter.extract_features()?;
assert_eq!(features.len(), 51);
Ok(())
}
}