Replace 8 unbounded Vec accumulation patterns with bounded VecDeque across ensemble, PPO, DQN, Mamba2, and data pipeline code to prevent OOM on RTX 3050 Ti (4GB VRAM) during live trading and extended training. Key OOM fixes: - Ensemble price/volatility history: Vec → VecDeque with O(1) eviction - Data pipeline: MAX_FEATURES=500K cap (~512MB) prevents unbounded loading - DQN replay buffer: full-array shuffle → HashSet random sampling (8MB → 256B) - PPO loss histories: bounded VecDeque (cap 1K), eliminated batch.clone() - Mamba2 scan: pre-allocated Vecs, explicit drop() after Tensor::cat - Mamba2 training history: capped at 100, Tensor::randn replaces Vec→Tensor - Mamba2 SSM reset: 2 unwrap() violations replaced with proper error handling Battle-testing (19 new integration tests): - KAN: 5 tests (forward, 50-epoch training 89.9% loss reduction, checkpoint) - xLSTM: 7 tests (2D+3D forward, 30-epoch training 82% reduction, checkpoint) - Diffusion: 7 tests (2D+3D forward, 20-epoch pipeline, checkpoint, validation) Bonus: fix pre-existing cache test failure (match .dbn.zst files, graceful skip) All 2390 lib tests pass, 0 new clippy errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
737 lines
24 KiB
Rust
737 lines
24 KiB
Rust
//! Adaptive ML Ensemble Integration
|
|
//!
|
|
//! Combines 6-model ML ensemble with adaptive trading strategy for regime-aware trading.
|
|
//! This module bridges the ML ensemble coordinator with market regime detection to
|
|
//! dynamically adjust model weights based on current market conditions.
|
|
|
|
use crate::ensemble::EnsembleDecision;
|
|
use crate::{MLError, MLResult, ModelPrediction};
|
|
use std::collections::{HashMap, VecDeque};
|
|
use std::sync::Arc;
|
|
use tokio::sync::RwLock;
|
|
use tracing::{debug, info};
|
|
|
|
use super::coordinator_extended::{EnsembleConfig, ExtendedEnsembleCoordinator};
|
|
|
|
/// Market regime types for adaptive weighting
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum MarketRegime {
|
|
/// Normal market conditions with typical volatility and volume
|
|
Normal,
|
|
/// Strong directional movement with clear trends
|
|
Trending,
|
|
/// Bull market - upward trending with moderate volatility
|
|
Bull,
|
|
/// Bear market - downward trending with moderate volatility
|
|
Bear,
|
|
/// Sideways market - low volatility, range-bound
|
|
Sideways,
|
|
/// High volatility - significant price swings
|
|
HighVolatility,
|
|
/// Crisis conditions with extreme volatility and risk
|
|
Crisis,
|
|
/// Unknown/transitioning regime
|
|
Unknown,
|
|
}
|
|
|
|
/// Adaptive ML Ensemble combining ensemble coordinator with regime detection
|
|
#[derive(Debug)]
|
|
pub struct AdaptiveMLEnsemble {
|
|
/// Extended ensemble coordinator (6 models)
|
|
coordinator: Arc<ExtendedEnsembleCoordinator>,
|
|
|
|
/// Current market regime
|
|
current_regime: Arc<RwLock<MarketRegime>>,
|
|
|
|
/// Regime detection parameters
|
|
regime_config: RegimeConfig,
|
|
|
|
/// Price history for regime detection (VecDeque for O(1) front removal)
|
|
price_history: Arc<RwLock<VecDeque<PricePoint>>>,
|
|
|
|
/// Volatility history for regime detection (VecDeque for O(1) front removal)
|
|
volatility_history: Arc<RwLock<VecDeque<f64>>>,
|
|
|
|
/// Performance metrics
|
|
metrics: Arc<RwLock<AdaptiveMetrics>>,
|
|
}
|
|
|
|
/// Configuration for regime detection
|
|
#[derive(Debug, Clone)]
|
|
pub struct RegimeConfig {
|
|
/// Lookback window for trend detection (bars)
|
|
pub trend_lookback: usize,
|
|
|
|
/// Volatility window for regime classification (bars)
|
|
pub volatility_window: usize,
|
|
|
|
/// Bull/Bear threshold (% change)
|
|
pub trend_threshold: f64,
|
|
|
|
/// High volatility threshold (multiple of average)
|
|
pub volatility_threshold: f64,
|
|
|
|
/// Minimum data points for regime detection
|
|
pub min_data_points: usize,
|
|
}
|
|
|
|
impl Default for RegimeConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
trend_lookback: 20,
|
|
volatility_window: 20,
|
|
trend_threshold: 0.02, // 2% trend
|
|
volatility_threshold: 1.5, // 1.5x average volatility
|
|
min_data_points: 20,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Price point for regime detection
|
|
#[derive(Debug, Clone)]
|
|
pub struct PricePoint {
|
|
pub timestamp: u64,
|
|
pub price: f64,
|
|
pub volume: f64,
|
|
}
|
|
|
|
/// Performance metrics for adaptive ensemble
|
|
#[derive(Debug, Clone)]
|
|
pub struct AdaptiveMetrics {
|
|
/// Total predictions made
|
|
pub total_predictions: u64,
|
|
|
|
/// Predictions per regime
|
|
pub predictions_per_regime: HashMap<MarketRegime, u64>,
|
|
|
|
/// Sharpe ratio per regime
|
|
pub sharpe_per_regime: HashMap<MarketRegime, f64>,
|
|
|
|
/// Cumulative returns
|
|
pub cumulative_return: f64,
|
|
|
|
/// Maximum drawdown
|
|
pub max_drawdown: f64,
|
|
|
|
/// Win rate
|
|
pub win_rate: f64,
|
|
|
|
/// Regime transitions
|
|
pub regime_transitions: u64,
|
|
}
|
|
|
|
impl Default for AdaptiveMetrics {
|
|
fn default() -> Self {
|
|
Self {
|
|
total_predictions: 0,
|
|
predictions_per_regime: HashMap::new(),
|
|
sharpe_per_regime: HashMap::new(),
|
|
cumulative_return: 0.0,
|
|
max_drawdown: 0.0,
|
|
win_rate: 0.0,
|
|
regime_transitions: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AdaptiveMLEnsemble {
|
|
/// Create new adaptive ML ensemble
|
|
pub fn new(regime_config: Option<RegimeConfig>) -> Self {
|
|
let ensemble_config = EnsembleConfig {
|
|
adaptive_weighting: true,
|
|
min_correlation_threshold: 0.7,
|
|
diversity_adjustment_factor: 0.2,
|
|
performance_window_size: 1000,
|
|
min_weight: 0.05,
|
|
max_weight: 0.50,
|
|
};
|
|
|
|
let coordinator = Arc::new(ExtendedEnsembleCoordinator::new(ensemble_config));
|
|
|
|
Self {
|
|
coordinator,
|
|
current_regime: Arc::new(RwLock::new(MarketRegime::Unknown)),
|
|
regime_config: regime_config.unwrap_or_default(),
|
|
price_history: Arc::new(RwLock::new(VecDeque::new())),
|
|
volatility_history: Arc::new(RwLock::new(VecDeque::new())),
|
|
metrics: Arc::new(RwLock::new(AdaptiveMetrics::default())),
|
|
}
|
|
}
|
|
|
|
/// Register all 6 models with initial weights
|
|
pub async fn register_models(&self) -> MLResult<()> {
|
|
// Register all 6 models with equal initial weights
|
|
self.coordinator
|
|
.register_model("DQN".to_string(), 0.167)
|
|
.await?;
|
|
self.coordinator
|
|
.register_model("PPO".to_string(), 0.167)
|
|
.await?;
|
|
self.coordinator
|
|
.register_model("TFT".to_string(), 0.167)
|
|
.await?;
|
|
self.coordinator
|
|
.register_model("MAMBA-2".to_string(), 0.166)
|
|
.await?;
|
|
self.coordinator
|
|
.register_model("Liquid".to_string(), 0.166)
|
|
.await?;
|
|
self.coordinator
|
|
.register_model("TLOB".to_string(), 0.167)
|
|
.await?;
|
|
|
|
info!("Registered 6 models in adaptive ensemble");
|
|
Ok(())
|
|
}
|
|
|
|
/// Update market regime based on price data
|
|
pub async fn update_regime(&self, price: f64, volume: f64) -> MLResult<MarketRegime> {
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs();
|
|
|
|
// Add to history
|
|
{
|
|
let mut history = self.price_history.write().await;
|
|
history.push_back(PricePoint {
|
|
timestamp,
|
|
price,
|
|
volume,
|
|
});
|
|
|
|
// Keep only required history — O(1) pop_front via VecDeque
|
|
let max_history = self
|
|
.regime_config
|
|
.trend_lookback
|
|
.max(self.regime_config.volatility_window)
|
|
* 2;
|
|
while history.len() > max_history {
|
|
history.pop_front();
|
|
}
|
|
}
|
|
|
|
// Detect regime
|
|
let new_regime = self.detect_regime().await?;
|
|
|
|
// Update current regime if changed
|
|
{
|
|
let mut current = self.current_regime.write().await;
|
|
if *current != new_regime {
|
|
info!("Regime transition: {:?} -> {:?}", *current, new_regime);
|
|
*current = new_regime;
|
|
|
|
// Update metrics
|
|
let mut metrics = self.metrics.write().await;
|
|
metrics.regime_transitions += 1;
|
|
}
|
|
}
|
|
|
|
Ok(new_regime)
|
|
}
|
|
|
|
/// Detect current market regime from price history
|
|
async fn detect_regime(&self) -> MLResult<MarketRegime> {
|
|
let history = self.price_history.read().await;
|
|
|
|
if history.len() < self.regime_config.min_data_points {
|
|
return Ok(MarketRegime::Unknown);
|
|
}
|
|
|
|
// Calculate trend
|
|
let lookback = self.regime_config.trend_lookback.min(history.len());
|
|
let prices: Vec<f64> = history
|
|
.iter()
|
|
.rev()
|
|
.take(lookback)
|
|
.map(|p| p.price)
|
|
.collect();
|
|
|
|
let first_price = prices.last().copied().unwrap_or(0.0);
|
|
let last_price = prices.first().copied().unwrap_or(0.0);
|
|
let trend = if first_price > 0.0 {
|
|
(last_price - first_price) / first_price
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Calculate volatility
|
|
let returns: Vec<f64> = prices.windows(2).map(|w| (w[0] - w[1]) / w[1]).collect();
|
|
|
|
let volatility = if !returns.is_empty() {
|
|
let mean: f64 = returns.iter().sum::<f64>() / returns.len() as f64;
|
|
let variance: f64 =
|
|
returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / returns.len() as f64;
|
|
variance.sqrt()
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Update volatility history
|
|
{
|
|
let mut vol_history = self.volatility_history.write().await;
|
|
vol_history.push_back(volatility);
|
|
if vol_history.len() > self.regime_config.volatility_window {
|
|
vol_history.pop_front();
|
|
}
|
|
}
|
|
|
|
// Calculate average volatility
|
|
let vol_history = self.volatility_history.read().await;
|
|
let avg_volatility = if !vol_history.is_empty() {
|
|
vol_history.iter().sum::<f64>() / vol_history.len() as f64
|
|
} else {
|
|
volatility
|
|
};
|
|
|
|
// Classify regime
|
|
let regime = if volatility > avg_volatility * self.regime_config.volatility_threshold {
|
|
MarketRegime::HighVolatility
|
|
} else if trend > self.regime_config.trend_threshold {
|
|
MarketRegime::Bull
|
|
} else if trend < -self.regime_config.trend_threshold {
|
|
MarketRegime::Bear
|
|
} else {
|
|
MarketRegime::Sideways
|
|
};
|
|
|
|
debug!(
|
|
"Regime detection: trend={:.4}, volatility={:.4}, avg_vol={:.4}, regime={:?}",
|
|
trend, volatility, avg_volatility, regime
|
|
);
|
|
|
|
Ok(regime)
|
|
}
|
|
|
|
/// Make prediction with regime-adaptive model weighting
|
|
pub async fn predict(&self, predictions: Vec<ModelPrediction>) -> MLResult<EnsembleDecision> {
|
|
if predictions.is_empty() {
|
|
return Err(MLError::ValidationError {
|
|
message: "No predictions provided".to_string(),
|
|
});
|
|
}
|
|
|
|
// Adjust weights based on current regime
|
|
let regime = *self.current_regime.read().await;
|
|
self.apply_regime_weights(regime).await?;
|
|
|
|
// Get ensemble decision
|
|
let decision = self.coordinator.predict(predictions).await?;
|
|
|
|
// Update metrics
|
|
{
|
|
let mut metrics = self.metrics.write().await;
|
|
metrics.total_predictions += 1;
|
|
*metrics.predictions_per_regime.entry(regime).or_insert(0) += 1;
|
|
}
|
|
|
|
Ok(decision)
|
|
}
|
|
|
|
/// Apply regime-conditional model weights
|
|
async fn apply_regime_weights(&self, regime: MarketRegime) -> MLResult<()> {
|
|
// Define regime-specific weights
|
|
let weights: HashMap<String, f64> = match regime {
|
|
MarketRegime::Bull => {
|
|
// Bull market: Weight trend-following models higher (DQN, PPO)
|
|
[
|
|
("DQN".to_string(), 0.30), // Trend follower
|
|
("PPO".to_string(), 0.25), // Reinforcement learning
|
|
("TFT".to_string(), 0.15), // Time-series forecasting
|
|
("MAMBA-2".to_string(), 0.15), // State-space model
|
|
("Liquid".to_string(), 0.10), // Adaptive time constants
|
|
("TLOB".to_string(), 0.05), // Order book (less relevant)
|
|
]
|
|
.iter()
|
|
.cloned()
|
|
.collect()
|
|
},
|
|
MarketRegime::Bear => {
|
|
// Bear market: Weight risk-aware models higher (PPO, TFT)
|
|
[
|
|
("PPO".to_string(), 0.30), // Risk-aware RL
|
|
("TFT".to_string(), 0.25), // Forecasting
|
|
("DQN".to_string(), 0.15), // Q-learning
|
|
("MAMBA-2".to_string(), 0.15), // State-space
|
|
("Liquid".to_string(), 0.10), // Adaptive
|
|
("TLOB".to_string(), 0.05), // Order book
|
|
]
|
|
.iter()
|
|
.cloned()
|
|
.collect()
|
|
},
|
|
MarketRegime::Sideways => {
|
|
// Sideways: Equal weights, focus on mean reversion
|
|
[
|
|
("TLOB".to_string(), 0.25), // Order book microstructure
|
|
("Liquid".to_string(), 0.20), // Adaptive dynamics
|
|
("TFT".to_string(), 0.20), // Pattern recognition
|
|
("MAMBA-2".to_string(), 0.15), // State transitions
|
|
("DQN".to_string(), 0.10), // Reduced trend
|
|
("PPO".to_string(), 0.10), // Reduced trend
|
|
]
|
|
.iter()
|
|
.cloned()
|
|
.collect()
|
|
},
|
|
MarketRegime::HighVolatility => {
|
|
// High volatility: Weight robust models higher
|
|
[
|
|
("PPO".to_string(), 0.35), // Robust RL
|
|
("MAMBA-2".to_string(), 0.25), // State-space handles chaos
|
|
("TFT".to_string(), 0.20), // Forecasting
|
|
("Liquid".to_string(), 0.10), // Adaptive
|
|
("DQN".to_string(), 0.05), // Reduce Q-learning
|
|
("TLOB".to_string(), 0.05), // Order book noise
|
|
]
|
|
.iter()
|
|
.cloned()
|
|
.collect()
|
|
},
|
|
MarketRegime::Normal | MarketRegime::Trending => {
|
|
// Normal/Trending: Balanced weights with slight trend bias
|
|
[
|
|
("DQN".to_string(), 0.20),
|
|
("PPO".to_string(), 0.20),
|
|
("TFT".to_string(), 0.20),
|
|
("MAMBA-2".to_string(), 0.20),
|
|
("Liquid".to_string(), 0.10),
|
|
("TLOB".to_string(), 0.10),
|
|
]
|
|
.iter()
|
|
.cloned()
|
|
.collect()
|
|
},
|
|
MarketRegime::Crisis => {
|
|
// Crisis: Maximum risk aversion, weight PPO heavily
|
|
[
|
|
("PPO".to_string(), 0.50), // Maximum risk control
|
|
("MAMBA-2".to_string(), 0.20), // State transitions
|
|
("TFT".to_string(), 0.15), // Forecasting
|
|
("Liquid".to_string(), 0.10), // Adaptive
|
|
("DQN".to_string(), 0.03), // Minimal risk-taking
|
|
("TLOB".to_string(), 0.02), // Minimal exposure
|
|
]
|
|
.iter()
|
|
.cloned()
|
|
.collect()
|
|
},
|
|
MarketRegime::Unknown => {
|
|
// Unknown: Equal weights
|
|
[
|
|
("DQN".to_string(), 0.167),
|
|
("PPO".to_string(), 0.167),
|
|
("TFT".to_string(), 0.167),
|
|
("MAMBA-2".to_string(), 0.166),
|
|
("Liquid".to_string(), 0.166),
|
|
("TLOB".to_string(), 0.167),
|
|
]
|
|
.iter()
|
|
.cloned()
|
|
.collect()
|
|
},
|
|
};
|
|
|
|
// Apply weights to coordinator
|
|
for (model_id, weight) in weights {
|
|
self.coordinator.register_model(model_id, weight).await?;
|
|
}
|
|
|
|
debug!("Applied regime-specific weights for {:?}", regime);
|
|
Ok(())
|
|
}
|
|
|
|
/// Calculate volatility-adjusted position size using Kelly Criterion
|
|
pub async fn calculate_position_size(
|
|
&self,
|
|
signal: f64,
|
|
confidence: f64,
|
|
account_equity: f64,
|
|
_current_volatility: f64,
|
|
) -> f64 {
|
|
// Kelly Criterion: f = (bp - q) / b
|
|
// where b = odds, p = win probability, q = 1 - p
|
|
|
|
// Estimate win probability from confidence (0.5 to 0.8 range)
|
|
let win_prob = 0.5 + (confidence * 0.3);
|
|
let lose_prob = 1.0 - win_prob;
|
|
|
|
// Estimate odds from signal strength (1:1 to 3:1)
|
|
let odds = 1.0 + (signal.abs() * 2.0);
|
|
|
|
// Kelly fraction
|
|
let kelly_fraction = ((odds * win_prob) - lose_prob) / odds;
|
|
|
|
// Apply fractional Kelly (25% of full Kelly for safety)
|
|
let fractional_kelly = kelly_fraction * 0.25;
|
|
|
|
// Adjust for volatility (reduce position in high volatility)
|
|
let regime = *self.current_regime.read().await;
|
|
let volatility_adjustment = match regime {
|
|
MarketRegime::HighVolatility => 0.5, // 50% reduction
|
|
MarketRegime::Crisis => 0.3, // 70% reduction (max risk control)
|
|
MarketRegime::Bull | MarketRegime::Bear => 0.8, // 20% reduction
|
|
MarketRegime::Sideways => 1.0, // No reduction
|
|
MarketRegime::Normal | MarketRegime::Trending => 0.9, // 10% reduction
|
|
MarketRegime::Unknown => 0.7, // 30% reduction
|
|
};
|
|
|
|
// Calculate position size
|
|
let position_fraction = fractional_kelly.max(0.0).min(0.25) * volatility_adjustment;
|
|
let position_size = account_equity * position_fraction;
|
|
|
|
debug!(
|
|
"Position sizing: signal={:.3}, confidence={:.3}, kelly={:.3}, adj={:.3}, size=${:.2}",
|
|
signal, confidence, fractional_kelly, volatility_adjustment, position_size
|
|
);
|
|
|
|
position_size
|
|
}
|
|
|
|
/// Record outcome for performance tracking
|
|
pub async fn record_outcome(&self, model_id: &str, return_value: f64) -> MLResult<()> {
|
|
self.coordinator
|
|
.record_outcome(model_id, return_value)
|
|
.await?;
|
|
|
|
// Update metrics
|
|
{
|
|
let mut metrics = self.metrics.write().await;
|
|
|
|
// Calculate win rate before incrementing total_predictions
|
|
let total_before = metrics.total_predictions;
|
|
if total_before > 0 {
|
|
if return_value > 0.0 {
|
|
let wins = (metrics.win_rate * total_before as f64) + 1.0;
|
|
metrics.win_rate = wins / (total_before + 1) as f64;
|
|
} else {
|
|
let wins = metrics.win_rate * total_before as f64;
|
|
metrics.win_rate = wins / (total_before + 1) as f64;
|
|
}
|
|
} else {
|
|
// First outcome
|
|
metrics.win_rate = if return_value > 0.0 { 1.0 } else { 0.0 };
|
|
}
|
|
|
|
metrics.total_predictions += 1;
|
|
metrics.cumulative_return += return_value;
|
|
|
|
// Update drawdown
|
|
if return_value < 0.0 && return_value.abs() > metrics.max_drawdown {
|
|
metrics.max_drawdown = return_value.abs();
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get current metrics
|
|
pub async fn get_metrics(&self) -> AdaptiveMetrics {
|
|
self.metrics.read().await.clone()
|
|
}
|
|
|
|
/// Get current regime
|
|
pub async fn get_regime(&self) -> MarketRegime {
|
|
*self.current_regime.read().await
|
|
}
|
|
|
|
/// Get diversity metrics
|
|
pub async fn get_diversity_metrics(&self) -> super::coordinator_extended::DiversityMetrics {
|
|
self.coordinator.get_diversity_metrics().await
|
|
}
|
|
|
|
/// Get performance attribution
|
|
pub async fn get_performance_attribution(
|
|
&self,
|
|
) -> super::coordinator_extended::PerformanceAttribution {
|
|
self.coordinator.get_performance_attribution().await
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_adaptive_ensemble_creation() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
ensemble.register_models().await.unwrap();
|
|
|
|
assert_eq!(ensemble.coordinator.model_count().await, 6);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regime_detection_bull() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
|
|
// Simulate bull market (rising prices)
|
|
for i in 0..30 {
|
|
let price = 100.0 + (i as f64);
|
|
ensemble.update_regime(price, 1000.0).await.unwrap();
|
|
}
|
|
|
|
let regime = ensemble.get_regime().await;
|
|
assert_eq!(regime, MarketRegime::Bull);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regime_detection_bear() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
|
|
// Simulate bear market (falling prices)
|
|
for i in 0..30 {
|
|
let price = 100.0 - (i as f64);
|
|
ensemble.update_regime(price, 1000.0).await.unwrap();
|
|
}
|
|
|
|
let regime = ensemble.get_regime().await;
|
|
assert_eq!(regime, MarketRegime::Bear);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regime_detection_sideways() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
|
|
// Simulate sideways market (oscillating prices)
|
|
for i in 0..30 {
|
|
let price = 100.0 + ((i % 2) as f64 * 0.1);
|
|
ensemble.update_regime(price, 1000.0).await.unwrap();
|
|
}
|
|
|
|
let regime = ensemble.get_regime().await;
|
|
assert_eq!(regime, MarketRegime::Sideways);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regime_adaptive_weights() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
ensemble.register_models().await.unwrap();
|
|
|
|
// Set bull regime
|
|
{
|
|
let mut regime = ensemble.current_regime.write().await;
|
|
*regime = MarketRegime::Bull;
|
|
}
|
|
|
|
// Apply regime weights
|
|
ensemble
|
|
.apply_regime_weights(MarketRegime::Bull)
|
|
.await
|
|
.unwrap();
|
|
|
|
let weights = ensemble.coordinator.get_weights().await;
|
|
|
|
// DQN should have higher weight in bull market
|
|
assert!(weights.get("DQN").copied().unwrap_or(0.0) > 0.25);
|
|
assert!(weights.get("PPO").copied().unwrap_or(0.0) > 0.20);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_position_sizing_kelly() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
|
|
let position = ensemble
|
|
.calculate_position_size(
|
|
0.7, // Strong signal
|
|
0.8, // High confidence
|
|
100000.0, // $100k account
|
|
0.02, // 2% volatility
|
|
)
|
|
.await;
|
|
|
|
// Position should be positive and reasonable (< 25% of equity)
|
|
assert!(position > 0.0);
|
|
assert!(position < 25000.0); // Max 25% of equity
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_volatility_adjusted_position_sizing() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
|
|
// Set high volatility regime
|
|
{
|
|
let mut regime = ensemble.current_regime.write().await;
|
|
*regime = MarketRegime::HighVolatility;
|
|
}
|
|
|
|
let position = ensemble
|
|
.calculate_position_size(
|
|
0.7, 0.8, 100000.0, 0.05, // 5% volatility (high)
|
|
)
|
|
.await;
|
|
|
|
// Position should be reduced due to high volatility
|
|
assert!(position < 15000.0); // Should be less than normal
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_ensemble_prediction_with_regime() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
ensemble.register_models().await.unwrap();
|
|
|
|
// Set regime
|
|
ensemble.update_regime(100.0, 1000.0).await.unwrap();
|
|
|
|
// Create predictions
|
|
let predictions = vec![
|
|
ModelPrediction::new("DQN".to_string(), 0.5, 0.8),
|
|
ModelPrediction::new("PPO".to_string(), 0.6, 0.85),
|
|
ModelPrediction::new("TFT".to_string(), 0.4, 0.75),
|
|
ModelPrediction::new("MAMBA-2".to_string(), 0.55, 0.8),
|
|
ModelPrediction::new("Liquid".to_string(), 0.45, 0.7),
|
|
ModelPrediction::new("TLOB".to_string(), 0.3, 0.65),
|
|
];
|
|
|
|
let decision = ensemble.predict(predictions).await.unwrap();
|
|
|
|
assert!(decision.confidence > 0.0);
|
|
assert!(decision.signal >= -1.0 && decision.signal <= 1.0);
|
|
assert_eq!(decision.model_count(), 6);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_metrics_tracking() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
ensemble.register_models().await.unwrap();
|
|
|
|
// Record some outcomes
|
|
ensemble.record_outcome("DQN", 0.02).await.unwrap();
|
|
ensemble.record_outcome("PPO", 0.01).await.unwrap();
|
|
ensemble.record_outcome("TFT", -0.01).await.unwrap();
|
|
|
|
let metrics = ensemble.get_metrics().await;
|
|
|
|
assert_eq!(metrics.total_predictions, 3);
|
|
assert!(metrics.cumulative_return > 0.0);
|
|
assert!(metrics.win_rate > 0.0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regime_transitions() {
|
|
let ensemble = AdaptiveMLEnsemble::new(None);
|
|
|
|
// Start with bull market
|
|
for i in 0..30 {
|
|
ensemble
|
|
.update_regime(100.0 + i as f64, 1000.0)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
assert_eq!(ensemble.get_regime().await, MarketRegime::Bull);
|
|
|
|
// Transition to bear market
|
|
for i in 0..30 {
|
|
ensemble
|
|
.update_regime(130.0 - i as f64, 1000.0)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
assert_eq!(ensemble.get_regime().await, MarketRegime::Bear);
|
|
|
|
let metrics = ensemble.get_metrics().await;
|
|
assert!(metrics.regime_transitions >= 1);
|
|
}
|
|
}
|