Expand FeatureVector from 40 to 42 dimensions by including ADX(14) at index 40 and CUSUM direction at index 41 from the existing CPU feature extraction pipeline. This eliminates proxy-based regime classification and enables GPU-native regime detection via tensor narrow/comparison ops. Key changes: - extraction.rs: wire RegimeADXFeatures + RegimeCUSUMFeatures into extract_current_features_v2(), output 42 features per bar - regime_conditional.rs: classify_regime_masks_gpu() creates per-regime mask tensors entirely on GPU (ADX > 0.25 = trending, |CUSUM| > 0.7 = volatile, else ranging). Zero CPU roundtrip in training hot path. - trainer.rs/config.rs: state_dim 43→45 (no OFI), 51→53 (with OFI), aligned dims unchanged (48/56). GPU batch insertion for all 3 heads. - CUDA header: MARKET_DIM 40→42 - walk_forward.rs: FEATURE_DIM 40→42 - 42 files updated, all [f64;40]→[f64;42] propagated across workspace Test results: ml=874/0, ml-dqn=354/0, ml-features=282/0, ml-core=274/0 Real data GPU smoke tests: 7/7 passed (OHLCV + OFI + trade enrichment) Hyperopt baseline RL: 2 trials completed on local RTX 3050 Ti Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
129 lines
4.3 KiB
Rust
129 lines
4.3 KiB
Rust
//! Bridge between ml model registry and common::ml_strategy::MLModelAdapter trait.
|
|
//!
|
|
//! EnsembleModelAdapter wraps a model ID and implements MLModelAdapter so it can
|
|
//! be injected into SharedMLStrategy. When real checkpoint loading is wired, this
|
|
//! adapter will delegate to the loaded model for inference.
|
|
|
|
use anyhow::Result;
|
|
use chrono::Utc;
|
|
use common::ml_strategy::{MLModelAdapter, MLPrediction, SharedMLStrategy};
|
|
use crate::features::production_adapter::ProductionFeatureExtractorAdapter;
|
|
|
|
/// Adapter that will delegate predict() to a loaded model checkpoint.
|
|
///
|
|
/// Currently returns neutral predictions with zero confidence (filtered out
|
|
/// by the confidence threshold) until checkpoint loading is production-ready.
|
|
#[derive(Debug)]
|
|
pub struct EnsembleModelAdapter {
|
|
model_id: String,
|
|
}
|
|
|
|
impl EnsembleModelAdapter {
|
|
pub fn new<S: Into<String>>(model_id: S) -> Self {
|
|
Self {
|
|
model_id: model_id.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MLModelAdapter for EnsembleModelAdapter {
|
|
fn predict(&self, features: &[f64]) -> Result<MLPrediction> {
|
|
// TODO: Wire to real model inference via checkpoint loading when
|
|
// the model registry is production-ready. For now, return neutral
|
|
// prediction so the ensemble pipeline is fully wired end-to-end.
|
|
let _ = features;
|
|
Ok(MLPrediction {
|
|
model_id: self.model_id.clone(),
|
|
prediction_value: 0.5,
|
|
confidence: 0.0, // Zero confidence = filtered out by threshold
|
|
features: vec![],
|
|
timestamp: Utc::now(),
|
|
inference_latency_us: 0,
|
|
})
|
|
}
|
|
|
|
fn model_id(&self) -> &str {
|
|
&self.model_id
|
|
}
|
|
|
|
fn validate_prediction(&mut self, _prediction: &MLPrediction, _actual_outcome: bool) {
|
|
// Performance tracking handled by SharedMLStrategy
|
|
}
|
|
}
|
|
|
|
/// Build a production-ready SharedMLStrategy with model adapters for each
|
|
/// model in the 10-model ensemble.
|
|
///
|
|
/// Returns a strategy with:
|
|
/// - ProductionFeatureExtractorAdapter (42 market features currently)
|
|
/// - One EnsembleModelAdapter per known model type
|
|
///
|
|
/// When no checkpoints are loaded, models return neutral predictions with
|
|
/// zero confidence, which get filtered out by the confidence threshold.
|
|
pub fn build_production_strategy(min_confidence_threshold: f64) -> SharedMLStrategy {
|
|
let extractor = Box::new(ProductionFeatureExtractorAdapter::new());
|
|
|
|
let model_ids = [
|
|
"dqn", "ppo", "tft", "mamba2", "tggn", "tlob", "liquid", "kan", "xlstm", "diffusion",
|
|
];
|
|
let models: Vec<Box<dyn MLModelAdapter>> = model_ids
|
|
.iter()
|
|
.map(|&id| Box::new(EnsembleModelAdapter::new(id)) as Box<dyn MLModelAdapter>)
|
|
.collect();
|
|
|
|
SharedMLStrategy::new(extractor, models, min_confidence_threshold)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_ensemble_model_adapter_neutral_prediction() {
|
|
let adapter = EnsembleModelAdapter::new("dqn");
|
|
let features = vec![0.1; 51];
|
|
let prediction = adapter.predict(&features).unwrap();
|
|
|
|
assert_eq!(prediction.model_id, "dqn");
|
|
assert_eq!(prediction.prediction_value, 0.5);
|
|
assert_eq!(prediction.confidence, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_production_strategy() {
|
|
let strategy = build_production_strategy(0.6);
|
|
assert_eq!(strategy.min_confidence_threshold(), 0.6);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_production_strategy_filters_neutral_predictions() {
|
|
let strategy = build_production_strategy(0.5);
|
|
let predictions = strategy
|
|
.get_ensemble_prediction(100.0, 1000.0, Utc::now())
|
|
.await
|
|
.unwrap();
|
|
|
|
// All 10 adapters return confidence=0.0, so threshold=0.5 filters them all
|
|
assert!(
|
|
predictions.is_empty(),
|
|
"Neutral predictions (confidence=0.0) should be filtered by threshold=0.5"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_production_strategy_zero_threshold_keeps_all() {
|
|
let strategy = build_production_strategy(0.0);
|
|
let predictions = strategy
|
|
.get_ensemble_prediction(100.0, 1000.0, Utc::now())
|
|
.await
|
|
.unwrap();
|
|
|
|
// With threshold=0.0, all 10 neutral predictions pass through
|
|
assert_eq!(
|
|
predictions.len(),
|
|
10,
|
|
"Zero threshold should keep all 10 model predictions"
|
|
);
|
|
}
|
|
}
|