#![allow(unsafe_code)] // Intentional unsafe for AVX2 vectorized backtesting performance //! Adaptive Strategy Runner for Backtesting //! //! This module provides the bridge between the backtesting engine and the adaptive strategy //! system, allowing historical data to flow through real ML models for validation. use anyhow::Result; use async_trait::async_trait; use common::ml_strategy::ProductionFeatureExtractor225; use common::Order; use common::{OrderSide, OrderStatus, Position, Price, Quantity, Symbol}; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use trading_engine::types::events::MarketEvent; // Use canonical types from ML module and real ML registry use chrono::{DateTime, Utc}; use dashmap::DashMap; use ml::features::ProductionFeatureExtractorAdapter; use ml::{get_global_registry, Features, ModelPrediction}; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tracing::{debug, info, warn}; // SIMD optimizations for HFT performance #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; use crate::strategy_tester::{ PerformanceSnapshot, SignalType, Strategy, StrategyConfig, StrategyContext, StrategyResult, TradeRecord, TradingSignal, }; /// Adaptive strategy runner that integrates ML models with backtesting pub struct AdaptiveStrategyRunner { /// Strategy configuration config: AdaptiveStrategyConfig, /// Current market state market_state: Arc>, /// Model predictions cache (lock-free for HFT performance) predictions_cache: Arc>, /// Performance tracking performance_tracker: Arc>, /// Feature extractor (legacy 3-5 dim fallback) feature_extractor: Arc, /// Production 51-dimension feature extractor from ml::features production_extractor: Arc>, /// Risk manager risk_manager: Arc, } /// Configuration for adaptive strategy #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdaptiveStrategyConfig { /// Models to use for predictions pub active_models: Vec, /// Minimum confidence threshold for trading pub min_confidence: f64, /// Maximum position size as fraction of portfolio pub max_position_size: f64, /// Lookback period for feature extraction pub lookback_period: usize, /// Model update frequency (in ticks) pub model_update_frequency: u64, /// Risk management settings pub risk_settings: RiskSettings, /// Feature extraction settings pub feature_settings: FeatureSettings, } impl Default for AdaptiveStrategyConfig { fn default() -> Self { Self { active_models: vec![ "TLOB".to_string(), "MAMBA".to_string(), "TFT".to_string(), "DQN".to_string(), "PPO".to_string(), ], min_confidence: 0.65, max_position_size: 0.05, // 5% max position lookback_period: 100, model_update_frequency: 1000, risk_settings: RiskSettings::default(), feature_settings: FeatureSettings::default(), } } } /// Risk management settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RiskSettings { /// Maximum drawdown before stopping pub max_drawdown: f64, /// Stop loss percentage pub stop_loss: f64, /// Take profit percentage pub take_profit: f64, /// Kelly fraction multiplier pub kelly_fraction: f64, } impl Default for RiskSettings { fn default() -> Self { Self { max_drawdown: 0.10, // 10% max drawdown stop_loss: 0.02, // 2% stop loss take_profit: 0.04, // 4% take profit kelly_fraction: 0.25, // Conservative 25% of Kelly } } } /// Feature extraction settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureSettings { /// Price features to extract pub price_features: Vec, /// Volume features to extract pub volume_features: Vec, /// Technical indicators to compute pub technical_indicators: Vec, /// Microstructure features pub microstructure_features: Vec, } impl Default for FeatureSettings { fn default() -> Self { Self { price_features: vec![ "returns".to_string(), "log_returns".to_string(), "volatility".to_string(), "price_momentum".to_string(), ], volume_features: vec![ "volume".to_string(), "volume_momentum".to_string(), "vwap".to_string(), ], technical_indicators: vec![ "rsi".to_string(), "macd".to_string(), "bollinger_bands".to_string(), ], microstructure_features: vec![ "bid_ask_spread".to_string(), "order_flow_imbalance".to_string(), "market_impact".to_string(), ], } } } /// Current market state #[derive(Debug, Clone)] struct MarketState { /// Current timestamp current_time: DateTime, /// Price history price_history: Vec<(DateTime, Decimal)>, /// Volume history volume_history: Vec<(DateTime, Decimal)>, /// Current position current_position: Option, /// Last prediction time #[allow(dead_code)] last_prediction_time: Option>, } impl Default for MarketState { fn default() -> Self { Self { current_time: Utc::now(), price_history: Vec::new(), volume_history: Vec::new(), current_position: None, last_prediction_time: None, } } } /// Tracks an open (or partially open) entry for PnL accounting. #[derive(Debug, Clone)] struct OpenEntry { /// Side of the original entry side: OrderSide, /// Average entry price entry_price: Price, /// Remaining quantity in the entry quantity: Decimal, /// Timestamp when the position was opened entry_time: DateTime, } /// Performance tracking for the strategy #[derive(Debug, Clone)] struct PerformanceTracker { /// Total trades executed total_trades: u64, /// Winning trades winning_trades: u64, /// Total PnL total_pnl: Decimal, /// Maximum drawdown max_drawdown: Decimal, /// Current drawdown current_drawdown: Decimal, /// Peak portfolio value peak_value: Decimal, /// Initial capital for equity curve baseline initial_capital: Decimal, /// Model prediction accuracy #[allow(dead_code)] model_accuracy: HashMap, /// Completed round-trip trade records trade_records: Vec, /// Equity snapshots: (timestamp, portfolio_value) equity_snapshots: Vec<(DateTime, Decimal)>, /// Open position entries keyed by symbol for PnL accounting open_entries: HashMap, } impl Default for PerformanceTracker { fn default() -> Self { Self { total_trades: 0, winning_trades: 0, total_pnl: Decimal::ZERO, max_drawdown: Decimal::ZERO, current_drawdown: Decimal::ZERO, peak_value: Decimal::ZERO, initial_capital: Decimal::ZERO, model_accuracy: HashMap::new(), trade_records: Vec::new(), equity_snapshots: Vec::new(), open_entries: HashMap::new(), } } } /// Feature extractor for ML models with object pooling for performance struct FeatureExtractor { config: FeatureSettings, // OPTIMIZATION: Reusable buffers to avoid allocations in hot paths #[allow(dead_code)] price_buffer: Vec, #[allow(dead_code)] volume_buffer: Vec, #[allow(dead_code)] returns_buffer: Vec, #[allow(dead_code)] gains_buffer: Vec, #[allow(dead_code)] losses_buffer: Vec, } impl FeatureExtractor { /// Create new feature extractor with configuration /// /// # Arguments /// * `config` - Feature extraction configuration /// /// # Returns /// * `Self` - New feature extractor instance with pre-allocated buffers fn new(config: FeatureSettings) -> Self { Self { config, // Pre-allocate buffers with reasonable capacity price_buffer: Vec::with_capacity(1024), volume_buffer: Vec::with_capacity(1024), returns_buffer: Vec::with_capacity(1024), gains_buffer: Vec::with_capacity(1024), losses_buffer: Vec::with_capacity(1024), } } /// Extract features from market data /// /// # Arguments /// * `market_state` - Current market state containing price and volume history /// /// # Returns /// * `Result` - Extracted feature vector ready for ML model input /// /// # Errors /// /// Returns error if feature extraction fails or insufficient data async fn extract_features(&self, market_state: &MarketState) -> Result { let mut feature_values = Vec::new(); let mut feature_names = Vec::new(); // Extract price features if let Some(features) = self.extract_price_features(market_state).await? { feature_values.extend(features.0); feature_names.extend(features.1); } // Extract volume features if let Some(features) = self.extract_volume_features(market_state).await? { feature_values.extend(features.0); feature_names.extend(features.1); } // Extract technical indicators if let Some(features) = self.extract_technical_features(market_state).await? { feature_values.extend(features.0); feature_names.extend(features.1); } Ok(Features { values: feature_values, names: feature_names, timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_micros() as u64, symbol: None, // Symbol will be set by the calling context when available }) } /// Extract price-based features from market data /// /// # Arguments /// * `market_state` - Market state with price history /// /// # Returns /// * `Result, Vec)>>` - Feature values and names, or None if insufficient data async fn extract_price_features( &self, market_state: &MarketState, ) -> Result, Vec)>> { if market_state.price_history.len() < 2 { return Ok(None); } let mut values = Vec::new(); let mut names = Vec::new(); let prices: Vec = market_state .price_history .iter() .map(|(_, price)| price.to_f64().unwrap_or(0.0)) .collect(); // Calculate returns if self.config.price_features.contains(&"returns".to_string()) { let returns = self.calculate_returns(&prices); values.extend(returns); names.push("returns".to_string()); } // Calculate volatility if self .config .price_features .contains(&"volatility".to_string()) { let volatility = self.calculate_volatility(&prices); values.push(volatility); names.push("volatility".to_string()); } Ok(Some((values, names))) } /// Extract volume-based features from market data /// /// # Arguments /// * `market_state` - Market state with volume history /// /// # Returns /// * `Result, Vec)>>` - Feature values and names, or None if insufficient data async fn extract_volume_features( &self, market_state: &MarketState, ) -> Result, Vec)>> { if market_state.volume_history.len() < 2 { return Ok(None); } let mut values = Vec::new(); let mut names = Vec::new(); let volumes: Vec = market_state .volume_history .iter() .map(|(_, volume)| volume.to_f64().unwrap_or(0.0)) .collect(); // Average volume if self.config.volume_features.contains(&"volume".to_string()) { let avg_volume = volumes.iter().sum::() / volumes.len() as f64; values.push(avg_volume); names.push("avg_volume".to_string()); } Ok(Some((values, names))) } /// Extract technical indicator features from market data /// /// # Arguments /// * `market_state` - Market state with sufficient price history for indicators /// /// # Returns /// * `Result, Vec)>>` - Technical indicator values and names, or None if insufficient data async fn extract_technical_features( &self, market_state: &MarketState, ) -> Result, Vec)>> { if market_state.price_history.len() < 14 { // Need minimum data for indicators return Ok(None); } let mut values = Vec::new(); let mut names = Vec::new(); let prices: Vec = market_state .price_history .iter() .map(|(_, price)| price.to_f64().unwrap_or(0.0)) .collect(); // RSI if self .config .technical_indicators .contains(&"rsi".to_string()) { let rsi = self.calculate_rsi(&prices, 14); values.push(rsi); names.push("rsi_14".to_string()); } Ok(Some((values, names))) } /// Calculate returns from price series with SIMD optimization /// /// # Arguments /// * `prices` - Array of price values /// /// # Returns /// * `Vec` - Vector of return percentages /// /// # Note /// /// Uses SIMD instructions on x86_64 for performance when available fn calculate_returns(&self, prices: &[f64]) -> Vec { // OPTIMIZATION: Use SIMD for vectorized return calculations if prices.len() < 2 { return Vec::new(); } #[cfg(target_arch = "x86_64")] { if is_x86_feature_detected!("avx2") { return self.calculate_returns_simd(prices); } } // Fallback to scalar implementation self.calculate_returns_scalar(prices) } /// Calculate returns using scalar operations (fallback) /// /// # Arguments /// * `prices` - Array of price values /// /// # Returns /// * `Vec` - Vector of return percentages fn calculate_returns_scalar(&self, prices: &[f64]) -> Vec { prices .windows(2) .map(|window| { if window[0] != 0.0 { (window[1] - window[0]) / window[0] } else { 0.0 } }) .collect() } /// Calculate returns using SIMD AVX2 instructions (x86_64 only) /// /// # Arguments /// * `prices` - Array of price values /// /// # Returns /// * `Vec` - Vector of return percentages /// /// # Safety /// /// Uses unsafe AVX2 intrinsics for vectorized computation #[cfg(target_arch = "x86_64")] fn calculate_returns_simd(&self, prices: &[f64]) -> Vec { let mut returns = Vec::with_capacity(prices.len() - 1); let len = prices.len() - 1; // SAFETY: SIMD intrinsics validated with feature detection and proper data alignment unsafe { // Process 4 elements at a time with AVX2 let mut i = 0; while i + 4 <= len { let prev = _mm256_loadu_pd(prices.as_ptr().add(i)); let curr = _mm256_loadu_pd(prices.as_ptr().add(i + 1)); // Calculate (curr - prev) / prev let diff = _mm256_sub_pd(curr, prev); let result = _mm256_div_pd(diff, prev); // Store results let mut temp = [0.0; 4]; _mm256_storeu_pd(temp.as_mut_ptr(), result); for j in 0..4 { returns.push(if prices[i + j] != 0.0 { temp[j] } else { 0.0 }); } i += 4; } // Handle remaining elements for j in i..len { let ret = if prices[j] != 0.0 { (prices[j + 1] - prices[j]) / prices[j] } else { 0.0 }; returns.push(ret); } } returns } /// Calculate price volatility (standard deviation of returns) /// /// # Arguments /// * `prices` - Array of price values /// /// # Returns /// * `f64` - Volatility as standard deviation of returns fn calculate_volatility(&self, prices: &[f64]) -> f64 { let returns = self.calculate_returns(prices); if returns.is_empty() { return 0.0; } let mean = returns.iter().sum::() / returns.len() as f64; let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::() / returns.len() as f64; variance.sqrt() } /// Calculate Relative Strength Index (RSI) /// /// # Arguments /// * `prices` - Array of price values /// /// * `period` - Period for RSI calculation (typically 14) /// /// # Returns /// * `f64` - RSI value between 0 and 100 fn calculate_rsi(&self, prices: &[f64], period: usize) -> f64 { if prices.len() < period + 1 { return 50.0; // Neutral RSI } let mut gains = Vec::new(); let mut losses = Vec::new(); for window in prices.windows(2) { let change = window[1] - window[0]; if change > 0.0 { gains.push(change); losses.push(0.0); } else { gains.push(0.0); losses.push(-change); } } if gains.len() < period { return 50.0; } let avg_gain = gains.iter().rev().take(period).sum::() / period as f64; let avg_loss = losses.iter().rev().take(period).sum::() / period as f64; if avg_loss == 0.0 { return 100.0; } let rs = avg_gain / avg_loss; 100.0 - (100.0 / (1.0 + rs)) } } /// Risk manager for position sizing and risk controls struct RiskManager { config: RiskSettings, } impl RiskManager { /// Create new risk manager with configuration /// /// # Arguments /// * `config` - Risk management settings /// /// # Returns /// * `Self` - New risk manager instance fn new(config: RiskSettings) -> Self { Self { config } } /// Calculate position size using Kelly criterion /// /// # Arguments /// * `prediction` - Model prediction with confidence score /// /// * `account_value` - Current account value /// * `current_price` - Current market price /// /// # Returns /// * `Result` - Position size in shares/units /// /// # Note /// /// Uses conservative Kelly fraction scaling for risk management fn calculate_position_size( &self, prediction: &ModelPrediction, account_value: Decimal, current_price: Decimal, ) -> Result { // Simple Kelly-based position sizing let confidence = prediction.confidence; let edge = (confidence - 0.5) * 2.0; // Convert to [-1, 1] range if edge <= 0.0 { return Ok(Decimal::ZERO); } // Kelly fraction with conservative scaling let kelly_size = Decimal::try_from(edge * self.config.kelly_fraction).unwrap_or(Decimal::ZERO); let max_size = account_value * Decimal::try_from(self.config.max_drawdown).unwrap_or(Decimal::new(5, 2)); // 5% fallback let position_value = kelly_size * account_value; let position_size = if current_price > Decimal::ZERO { position_value / current_price } else { Decimal::ZERO }; Ok(position_size.min(max_size / current_price)) } /// Check if trade passes risk checks /// /// # Arguments /// * `signal` - Trading signal to validate /// /// * `current_position` - Current position if any /// * `account_value` - Current account value /// /// # Returns /// * `Result` - True if trade passes all risk checks fn validate_trade( &self, signal: &TradingSignal, _current_position: Option<&Position>, account_value: Decimal, ) -> Result { // Check position size limits if let Some(price) = signal.target_price { let trade_value = signal.quantity.to_decimal()? * price.to_decimal()?; let position_fraction = trade_value / account_value; if position_fraction > Decimal::try_from(self.config.max_drawdown).unwrap_or(Decimal::new(10, 2)) { debug!( "Trade rejected: position size too large ({:.2}%)", position_fraction * Decimal::from(100) ); return Ok(false); } } // Additional risk checks can be added here Ok(true) } } impl AdaptiveStrategyRunner { /// Create new adaptive strategy runner /// /// # Arguments /// * `config` - Configuration for adaptive strategy /// /// # Returns /// * `Self` - New adaptive strategy runner with initialized components pub fn new(config: AdaptiveStrategyConfig) -> Self { let feature_extractor = Arc::new(FeatureExtractor::new(config.feature_settings.clone())); let risk_manager = Arc::new(RiskManager::new(config.risk_settings.clone())); let production_extractor = Arc::new(RwLock::new(ProductionFeatureExtractorAdapter::new())); Self { config, market_state: Arc::new(RwLock::new(MarketState::default())), predictions_cache: Arc::new(DashMap::new()), performance_tracker: Arc::new(RwLock::new(PerformanceTracker::default())), feature_extractor, production_extractor, risk_manager, } } /// Get ensemble prediction from all active models (optimized for HFT performance) /// /// # Arguments /// * `features` - Feature vector for prediction /// /// # Returns /// * `Result` - Ensemble prediction with confidence-weighted averaging /// /// # Note /// /// Uses lock-free caching and parallel model execution for low-latency performance async fn get_ensemble_prediction(&self, features: &Features) -> Result { let registry = get_global_registry(); // OPTIMIZATION: Use predict_selected for parallel model execution let predictions = registry .predict_selected(&self.config.active_models, features) .await; // Filter successful predictions let valid_predictions: Vec = predictions .into_iter() .filter_map(|result| result.ok()) .collect(); if valid_predictions.is_empty() { return Err(anyhow::anyhow!("No valid predictions from any model")); } // Ensemble using confidence-weighted average let total_confidence: f64 = valid_predictions.iter().map(|p| p.confidence).sum(); if total_confidence == 0.0 { return Err(anyhow::anyhow!("Zero total confidence in predictions")); } let weighted_value = valid_predictions .iter() .map(|p| p.value * p.confidence) .sum::() / total_confidence; let ensemble_confidence = valid_predictions.iter().map(|p| p.confidence).sum::() / valid_predictions.len() as f64; // OPTIMIZATION: Use lock-free DashMap instead of async RwLock for prediction in &valid_predictions { self.predictions_cache .insert(prediction.model_id.clone(), prediction.clone()); } Ok(ModelPrediction::new( "ensemble".to_string(), weighted_value, ensemble_confidence, )) } /// Generate trading signal from prediction /// /// # Arguments /// * `prediction` - Model prediction with confidence and direction /// /// * `symbol` - Symbol to trade /// * `current_price` - Current market price /// /// * `account_value` - Current account value for position sizing /// /// # Returns /// * `Result>` - Trading signal if confidence threshold is met fn generate_signal( &self, prediction: &ModelPrediction, symbol: Symbol, current_price: Decimal, account_value: Decimal, ) -> Result> { // Check confidence threshold if prediction.confidence < self.config.min_confidence { debug!( "Prediction confidence {:.3} below threshold {:.3}", prediction.confidence, self.config.min_confidence ); return Ok(None); } // Determine trade direction let side = if prediction.value > 0.5 { OrderSide::Buy } else if prediction.value < -0.5 { OrderSide::Sell } else { return Ok(None); // Neutral signal }; // Calculate position size let quantity = self.risk_manager .calculate_position_size(prediction, account_value, current_price)?; if quantity <= Decimal::ZERO { return Ok(None); } let signal_type = match side { OrderSide::Buy => SignalType::Buy, OrderSide::Sell => SignalType::Sell, }; let quantity_as_quantity = Quantity::from_f64(quantity.to_f64().unwrap_or(0.0))?; let mut metadata = HashMap::new(); metadata.insert("strategy".to_string(), serde_json::json!("adaptive_ml")); metadata.insert( "ensemble_confidence".to_string(), serde_json::json!(prediction.confidence), ); metadata.insert( "prediction_value".to_string(), serde_json::json!(prediction.value), ); metadata.insert( "model_count".to_string(), serde_json::json!(self.config.active_models.len()), ); let signal = TradingSignal { symbol, signal_type, quantity: quantity_as_quantity, target_price: Some(Price::from_f64(current_price.to_f64().unwrap_or(0.0))?), stop_loss: None, take_profit: None, confidence: Decimal::try_from(prediction.confidence).unwrap_or(Decimal::ZERO), metadata, }; Ok(Some(signal)) } } #[async_trait(?Send)] impl Strategy for AdaptiveStrategyRunner { fn name(&self) -> &str { "adaptive_ml_strategy" } async fn initialize( &mut self, initial_capital: Decimal, _config: StrategyConfig, ) -> Result<()> { info!( "Initializing Adaptive ML Strategy with capital: {}", initial_capital ); { let mut tracker = self.performance_tracker.write(); tracker.peak_value = initial_capital; tracker.initial_capital = initial_capital; } // Verify models are available let registry = get_global_registry(); let available_models = registry.get_model_names(); for model_name in &self.config.active_models { if !available_models.contains(model_name) { warn!("Model {} not found in registry", model_name); } } info!("Adaptive ML Strategy initialized successfully"); Ok(()) } async fn on_market_event( &mut self, event: &MarketEvent, context: &StrategyContext, ) -> Result> { let mut signals = Vec::new(); match event { MarketEvent::Trade { symbol, price, timestamp, .. } => { // Update market state { let mut state = self.market_state.write(); state.current_time = *timestamp; state.price_history.push((*timestamp, price.to_decimal()?)); // Note: volume not available in MarketEvent::Trade, using placeholder // state.volume_history.push((*timestamp, Volume::ZERO)); // Keep only recent history let max_history = self.config.lookback_period; if state.price_history.len() > max_history { let excess = state.price_history.len() - max_history; state.price_history.drain(0..excess); } if state.volume_history.len() > max_history { let excess = state.volume_history.len() - max_history; state.volume_history.drain(0..excess); } } // Update production extractor with new price data let price_f64 = price.to_decimal()?.to_f64().unwrap_or(0.0); // Volume not available in MarketEvent::Trade; use a default let volume_f64 = 1000.0; { let mut prod_ext = self.production_extractor.write(); if let Err(e) = prod_ext.update(price_f64, volume_f64, *timestamp) { debug!("Production extractor update failed: {}", e); } } // Try production 51-dim feature extraction first let production_features = { let mut prod_ext = self.production_extractor.write(); prod_ext.extract_features() }; match production_features { Ok(feat_values) if feat_values.len() == 51 => { // Build feature names for the 51-dim vector let feature_names: Vec = (0..51) .map(|i| format!("prod_feature_{}", i)) .collect(); let ts_micros = timestamp .timestamp_micros(); let features = Features { values: feat_values, names: feature_names, timestamp: ts_micros as u64, symbol: Some(symbol.to_string()), }; match self.get_ensemble_prediction(&features).await { Ok(prediction) => { if let Some(signal) = self.generate_signal( &prediction, symbol.clone(), price.to_decimal()?, context.account_balance, )? { let current_position = context.positions.get(symbol); if self.risk_manager.validate_trade( &signal, current_position, context.account_balance, )? { signals.push(signal); } } } Err(e) => { debug!("Prediction failed: {}", e); } } } Ok(_) | Err(_) => { // Production extractor not warmed up yet, fall back to legacy extractor let market_state = self.market_state.read().clone(); if market_state.price_history.len() >= 10 { match self .feature_extractor .extract_features(&market_state) .await { Ok(features) => { match self.get_ensemble_prediction(&features).await { Ok(prediction) => { if let Some(signal) = self.generate_signal( &prediction, symbol.clone(), price.to_decimal()?, context.account_balance, )? { let current_position = context.positions.get(symbol); if self.risk_manager.validate_trade( &signal, current_position, context.account_balance, )? { signals.push(signal); } } } Err(e) => { debug!("Prediction failed: {}", e); } } } Err(e) => { debug!("Feature extraction failed: {}", e); } } } } } }, _ => { // Handle other event types if needed }, } Ok(signals) } async fn on_order_update(&mut self, order: &Order, context: &StrategyContext) -> Result<()> { if order.status == OrderStatus::Filled { let fill_price = order .average_price .unwrap_or_else(|| order.price.unwrap_or(Price::ZERO)); let fill_qty = order.quantity.to_decimal().unwrap_or(Decimal::ZERO); let symbol_key = order.symbol.to_string(); let mut tracker = self.performance_tracker.write(); // Check if this fill closes (or reduces) an existing open entry let mut closed = false; if let Some(entry) = tracker.open_entries.get(&symbol_key).cloned() { // A Buy closes a short entry; a Sell closes a long entry let is_closing = matches!( (&entry.side, &order.side), (OrderSide::Buy, OrderSide::Sell) | (OrderSide::Sell, OrderSide::Buy) ); if is_closing { let closed_qty = entry.quantity.min(fill_qty); let entry_dec = entry.entry_price.to_decimal().unwrap_or(Decimal::ZERO); let exit_dec = fill_price.to_decimal().unwrap_or(Decimal::ZERO); // PnL: long close = (exit - entry) * qty // short close = (entry - exit) * qty let pnl = if entry.side == OrderSide::Buy { (exit_dec - entry_dec) * closed_qty } else { (entry_dec - exit_dec) * closed_qty }; let return_pct = if entry_dec != Decimal::ZERO { pnl / (entry_dec * closed_qty) * Decimal::from(100) } else { Decimal::ZERO }; let trade = TradeRecord { trade_id: uuid::Uuid::new_v4().to_string(), symbol: order.symbol.clone(), side: entry.side.clone(), entry_price: entry.entry_price, exit_price: fill_price, quantity: order.quantity, entry_time: entry.entry_time, exit_time: context.current_time, pnl, return_pct, commission: Decimal::ZERO, }; tracker.total_trades += 1; tracker.total_pnl += pnl; if pnl > Decimal::ZERO { tracker.winning_trades += 1; } tracker.trade_records.push(trade); // Update or remove the open entry let remaining = entry.quantity - closed_qty; if remaining <= Decimal::ZERO { tracker.open_entries.remove(&symbol_key); } else { if let Some(e) = tracker.open_entries.get_mut(&symbol_key) { e.quantity = remaining; } } // If the fill is larger than the old entry, open a new entry // for the remainder (position flip) let excess = fill_qty - closed_qty; if excess > Decimal::ZERO { tracker.open_entries.insert( symbol_key.clone(), OpenEntry { side: order.side.clone(), entry_price: fill_price, quantity: excess, entry_time: context.current_time, }, ); } closed = true; } } if !closed { // Opening a new position or adding to existing let entry = tracker .open_entries .entry(symbol_key) .or_insert_with(|| OpenEntry { side: order.side.clone(), entry_price: fill_price, quantity: Decimal::ZERO, entry_time: context.current_time, }); entry.quantity += fill_qty; // Update average entry price (weighted average) // This simple version just keeps the latest; for proper // accounting the PositionTracker in strategy_tester handles it } // Take an equity snapshot tracker .equity_snapshots .push((context.current_time, context.account_balance)); info!( "Order filled: {} {} @ {} | total_pnl={} trades={} wins={}", order.side, order.quantity, fill_price, tracker.total_pnl, tracker.total_trades, tracker.winning_trades, ); } Ok(()) } async fn on_position_update( &mut self, position: &Position, context: &StrategyContext, ) -> Result<()> { // Update market state with current position { let mut state = self.market_state.write(); state.current_position = Some(position.clone()); } // Update performance tracking { let mut tracker = self.performance_tracker.write(); if context.account_balance > tracker.peak_value { tracker.peak_value = context.account_balance; tracker.current_drawdown = Decimal::ZERO; } else if tracker.peak_value > Decimal::ZERO { tracker.current_drawdown = (tracker.peak_value - context.account_balance) / tracker.peak_value; if tracker.current_drawdown > tracker.max_drawdown { tracker.max_drawdown = tracker.current_drawdown; } } // Record equity snapshot on every position update tracker .equity_snapshots .push((context.current_time, context.account_balance)); } Ok(()) } async fn finalize(&mut self, context: &StrategyContext) -> Result { let tracker = self.performance_tracker.read(); let baseline = if tracker.initial_capital > Decimal::ZERO { tracker.initial_capital } else { tracker.peak_value }; let total_return = if baseline > Decimal::ZERO { (context.account_balance - baseline) / baseline } else { Decimal::ZERO }; let win_rate = if tracker.total_trades > 0 { Decimal::from(tracker.winning_trades) / Decimal::from(tracker.total_trades) } else { Decimal::ZERO }; // Calculate Sharpe ratio (simplified) let sharpe_ratio = if tracker.max_drawdown > Decimal::ZERO { total_return / tracker.max_drawdown } else { Decimal::ZERO }; // Build performance timeline from equity snapshots let performance_timeline: Vec = tracker .equity_snapshots .iter() .map(|(ts, value)| { let realized = tracker.total_pnl; let dd = if tracker.peak_value > Decimal::ZERO { (tracker.peak_value - *value) / tracker.peak_value } else { Decimal::ZERO }; PerformanceSnapshot { timestamp: *ts, portfolio_value: *value, cash_balance: *value, unrealized_pnl: Decimal::ZERO, realized_pnl: realized, open_positions: 0, drawdown: if dd > Decimal::ZERO { dd } else { Decimal::ZERO }, } }) .collect(); let trades = tracker.trade_records.clone(); Ok(StrategyResult { strategy_name: "adaptive_ml_strategy".to_string(), total_return, annualized_return: total_return, // Simplified max_drawdown: tracker.max_drawdown, sharpe_ratio, total_trades: tracker.total_trades, win_rate, avg_trade_return: if tracker.total_trades > 0 { tracker.total_pnl / Decimal::from(tracker.total_trades) } else { Decimal::ZERO }, final_value: context.account_balance, trades, performance_timeline, }) } async fn get_state(&self) -> Result { let market_state = self.market_state.read(); let tracker = self.performance_tracker.read(); let cache = &self.predictions_cache; Ok(serde_json::json!({ "strategy_name": "adaptive_ml_strategy", "config": self.config, "current_time": market_state.current_time, "price_history_length": market_state.price_history.len(), "volume_history_length": market_state.volume_history.len(), "current_position": market_state.current_position, "total_trades": tracker.total_trades, "max_drawdown": tracker.max_drawdown, "cached_predictions": cache.len(), "active_models": self.config.active_models, })) } } /// Create a configured adaptive strategy runner /// /// # Returns /// * `AdaptiveStrategyRunner` - Strategy runner with default configuration pub fn create_adaptive_strategy() -> AdaptiveStrategyRunner { AdaptiveStrategyRunner::new(AdaptiveStrategyConfig::default()) } /// Create adaptive strategy with custom configuration /// /// # Arguments /// * `config` - Custom adaptive strategy configuration /// /// # Returns /// * `AdaptiveStrategyRunner` - Strategy runner with specified configuration pub fn create_adaptive_strategy_with_config( config: AdaptiveStrategyConfig, ) -> AdaptiveStrategyRunner { AdaptiveStrategyRunner::new(config) } #[cfg(test)] mod tests { use super::*; #[test] fn test_adaptive_strategy_config_default() { let config = AdaptiveStrategyConfig::default(); assert_eq!(config.active_models.len(), 5); assert_eq!(config.min_confidence, 0.65); assert!(config.max_position_size > 0.0); } #[test] fn test_risk_settings_default() { let risk = RiskSettings::default(); assert_eq!(risk.max_drawdown, 0.10); assert!(risk.kelly_fraction > 0.0); } #[tokio::test] async fn test_adaptive_strategy_creation() { let strategy = create_adaptive_strategy(); assert_eq!(strategy.name(), "adaptive_ml_strategy"); } #[tokio::test] async fn test_feature_extractor() { let config = FeatureSettings::default(); let extractor = FeatureExtractor::new(config); let mut market_state = MarketState::default(); market_state.price_history = vec![ (Utc::now(), Decimal::from(100)), (Utc::now(), Decimal::from(101)), (Utc::now(), Decimal::from(102)), ]; let features = extractor.extract_features(&market_state).await; assert!(features.is_ok()); } #[test] fn test_production_features_have_51_dimensions() -> Result<(), Box> { let mut extractor = ProductionFeatureExtractorAdapter::new(); // Feed 55 price updates (past the warmup period of 50) for i in 0..55 { let price = 100.0 + i as f64 * 0.1; let volume = 1000.0; let timestamp = Utc::now(); extractor.update(price, volume, timestamp)?; } let features = extractor.extract_features()?; assert_eq!( features.len(), 51, "Production extractor should produce exactly 51 features, got {}", features.len() ); // 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_production_extractor_warmup_skips_gracefully() { let mut extractor = ProductionFeatureExtractorAdapter::new(); // Feed only 5 price updates (below warmup threshold) for i in 0..5 { let price = 100.0 + i as f64 * 0.1; let _ = extractor.update(price, 1000.0, Utc::now()); } // Should either return an error or return a short vector // (during warmup, extraction may fail) let result = extractor.extract_features(); // We accept either an error (warmup not ready) or a valid 51-dim result match result { Ok(features) => { assert_eq!(features.len(), 51); } Err(_) => { // Expected during warmup - this is fine } } } #[test] fn test_adaptive_strategy_has_production_extractor() { let strategy = create_adaptive_strategy(); // Verify the production extractor is initialized let ext = strategy.production_extractor.read(); // Should be able to access the extractor without issues drop(ext); } #[test] fn test_performance_tracker_records_trade() { use crate::strategy_tester::TradeRecord; let mut tracker = PerformanceTracker::default(); tracker.initial_capital = Decimal::from(100_000); tracker.peak_value = Decimal::from(100_000); // Simulate opening a long entry tracker.open_entries.insert( "AAPL".to_string(), OpenEntry { side: OrderSide::Buy, entry_price: Price::from_f64(100.0).unwrap(), quantity: Decimal::from(10), entry_time: Utc::now(), }, ); // Simulate a winning close: sold at 110 let pnl = (Decimal::from(110) - Decimal::from(100)) * Decimal::from(10); // +100 tracker.total_trades += 1; tracker.total_pnl += pnl; if pnl > Decimal::ZERO { tracker.winning_trades += 1; } tracker.trade_records.push(TradeRecord { trade_id: "test-1".to_string(), symbol: Symbol::from("AAPL"), side: OrderSide::Buy, entry_price: Price::from_f64(100.0).unwrap(), exit_price: Price::from_f64(110.0).unwrap(), quantity: Quantity::from_f64(10.0).unwrap(), entry_time: Utc::now(), exit_time: Utc::now(), pnl, return_pct: Decimal::from(10), commission: Decimal::ZERO, }); tracker.open_entries.remove("AAPL"); assert_eq!(tracker.total_trades, 1); assert_eq!(tracker.winning_trades, 1); assert_eq!(tracker.total_pnl, Decimal::from(100)); assert_eq!(tracker.trade_records.len(), 1); assert_eq!(tracker.trade_records.first().map(|t| &t.symbol), Some(&Symbol::from("AAPL"))); } #[test] fn test_performance_tracker_losing_trade() { let mut tracker = PerformanceTracker::default(); tracker.initial_capital = Decimal::from(100_000); tracker.peak_value = Decimal::from(100_000); // Simulate opening a long entry tracker.open_entries.insert( "TSLA".to_string(), OpenEntry { side: OrderSide::Buy, entry_price: Price::from_f64(200.0).unwrap(), quantity: Decimal::from(5), entry_time: Utc::now(), }, ); // Simulate a losing close: sold at 180 let pnl = (Decimal::from(180) - Decimal::from(200)) * Decimal::from(5); // -100 tracker.total_trades += 1; tracker.total_pnl += pnl; if pnl > Decimal::ZERO { tracker.winning_trades += 1; } tracker.trade_records.push(TradeRecord { trade_id: "test-2".to_string(), symbol: Symbol::from("TSLA"), side: OrderSide::Buy, entry_price: Price::from_f64(200.0).unwrap(), exit_price: Price::from_f64(180.0).unwrap(), quantity: Quantity::from_f64(5.0).unwrap(), entry_time: Utc::now(), exit_time: Utc::now(), pnl, return_pct: Decimal::from(-10), commission: Decimal::ZERO, }); tracker.open_entries.remove("TSLA"); assert_eq!(tracker.total_trades, 1); assert_eq!(tracker.winning_trades, 0); assert_eq!(tracker.total_pnl, Decimal::from(-100)); assert_eq!(tracker.trade_records.len(), 1); } #[tokio::test] async fn test_finalize_populates_trades() { let mut runner = create_adaptive_strategy(); let initial_capital = Decimal::from(100_000); // Initialize the runner so peak_value / initial_capital are set runner .initialize(initial_capital, StrategyConfig::default()) .await .unwrap(); // Manually inject trade records and equity snapshots into the tracker { let mut tracker = runner.performance_tracker.write(); tracker.total_trades = 2; tracker.winning_trades = 1; tracker.total_pnl = Decimal::from(50); // net +50 tracker.trade_records.push(TradeRecord { trade_id: "finalize-1".to_string(), symbol: Symbol::from("AAPL"), side: OrderSide::Buy, entry_price: Price::from_f64(100.0).unwrap(), exit_price: Price::from_f64(110.0).unwrap(), quantity: Quantity::from_f64(10.0).unwrap(), entry_time: Utc::now(), exit_time: Utc::now(), pnl: Decimal::from(100), return_pct: Decimal::from(10), commission: Decimal::ZERO, }); tracker.trade_records.push(TradeRecord { trade_id: "finalize-2".to_string(), symbol: Symbol::from("MSFT"), side: OrderSide::Buy, entry_price: Price::from_f64(300.0).unwrap(), exit_price: Price::from_f64(290.0).unwrap(), quantity: Quantity::from_f64(5.0).unwrap(), entry_time: Utc::now(), exit_time: Utc::now(), pnl: Decimal::from(-50), return_pct: Decimal::new(-333, 2), commission: Decimal::ZERO, }); // Add equity snapshots let now = Utc::now(); tracker.equity_snapshots.push((now, Decimal::from(100_100))); tracker.equity_snapshots.push((now, Decimal::from(100_050))); } // Build a context with the final balance let context = StrategyContext { current_time: Utc::now(), account_balance: Decimal::from(100_050), buying_power: Decimal::from(100_050), positions: HashMap::new(), open_orders: HashMap::new(), market_prices: HashMap::new(), performance: crate::strategy_tester::PerformanceMetrics::default(), }; let result = runner.finalize(&context).await.unwrap(); // Trades should be populated assert_eq!(result.trades.len(), 2, "finalize() must return trade records"); assert_eq!(result.trades.first().map(|t| t.trade_id.as_str()), Some("finalize-1")); // Performance timeline should be populated from equity snapshots assert_eq!( result.performance_timeline.len(), 2, "finalize() must return performance timeline" ); // Verify aggregated stats assert_eq!(result.total_trades, 2); assert_eq!(result.win_rate, Decimal::from(1) / Decimal::from(2)); assert_eq!(result.avg_trade_return, Decimal::from(25)); // 50 / 2 assert_eq!(result.final_value, Decimal::from(100_050)); } #[tokio::test] async fn test_on_order_update_tracks_pnl() { let mut runner = create_adaptive_strategy(); let initial_capital = Decimal::from(100_000); runner .initialize(initial_capital, StrategyConfig::default()) .await .unwrap(); let now = Utc::now(); let context = StrategyContext { current_time: now, account_balance: initial_capital, buying_power: initial_capital, positions: HashMap::new(), open_orders: HashMap::new(), market_prices: HashMap::new(), performance: crate::strategy_tester::PerformanceMetrics::default(), }; // Step 1: simulate a Buy fill at $100 (opening) let buy_order = Order { id: "order_1".to_string().into(), client_order_id: None, broker_order_id: None, account_id: None, symbol: Symbol::from("AAPL"), side: OrderSide::Buy, order_type: common::OrderType::Market, status: OrderStatus::Filled, time_in_force: common::TimeInForce::Day, quantity: Quantity::from_f64(10.0).unwrap(), price: Some(Price::from_f64(100.0).unwrap()), stop_price: None, filled_quantity: Quantity::from_f64(10.0).unwrap(), remaining_quantity: Quantity::ZERO, average_price: Some(Price::from_f64(100.0).unwrap()), avg_fill_price: None, average_fill_price: None, exchange_order_id: None, parent_id: None, execution_algorithm: None, execution_params: serde_json::json!({}), stop_loss: None, take_profit: None, created_at: common::HftTimestamp::now_or_zero(), updated_at: None, expires_at: None, metadata: serde_json::json!({}), }; runner.on_order_update(&buy_order, &context).await.unwrap(); // After a buy open, no round-trip trade yet { let tracker = runner.performance_tracker.read(); assert_eq!(tracker.total_trades, 0, "Opening buy should not count as a completed trade"); assert!(tracker.open_entries.contains_key("AAPL")); } // Step 2: simulate a Sell fill at $110 (closing the long) let sell_order = Order { id: "order_2".to_string().into(), client_order_id: None, broker_order_id: None, account_id: None, symbol: Symbol::from("AAPL"), side: OrderSide::Sell, order_type: common::OrderType::Market, status: OrderStatus::Filled, time_in_force: common::TimeInForce::Day, quantity: Quantity::from_f64(10.0).unwrap(), price: Some(Price::from_f64(110.0).unwrap()), stop_price: None, filled_quantity: Quantity::from_f64(10.0).unwrap(), remaining_quantity: Quantity::ZERO, average_price: Some(Price::from_f64(110.0).unwrap()), avg_fill_price: None, average_fill_price: None, exchange_order_id: None, parent_id: None, execution_algorithm: None, execution_params: serde_json::json!({}), stop_loss: None, take_profit: None, created_at: common::HftTimestamp::now_or_zero(), updated_at: None, expires_at: None, metadata: serde_json::json!({}), }; let close_context = StrategyContext { current_time: now, account_balance: Decimal::from(100_100), // gained 100 buying_power: Decimal::from(100_100), positions: HashMap::new(), open_orders: HashMap::new(), market_prices: HashMap::new(), performance: crate::strategy_tester::PerformanceMetrics::default(), }; runner .on_order_update(&sell_order, &close_context) .await .unwrap(); // After close, we should have a completed trade with PnL = +100 { let tracker = runner.performance_tracker.read(); assert_eq!(tracker.total_trades, 1, "Closing sell should create a completed trade"); assert_eq!(tracker.winning_trades, 1); assert_eq!(tracker.total_pnl, Decimal::from(100)); assert_eq!(tracker.trade_records.len(), 1); assert!(!tracker.open_entries.contains_key("AAPL"), "Entry should be removed after close"); // Equity snapshots recorded on each order update assert!(tracker.equity_snapshots.len() >= 2); } } }