//! Feature Engineering for Financial ML Models //! //! Comprehensive feature engineering pipeline for HFT trading systems including: //! - Technical indicators (SMA, EMA, RSI, MACD, Bollinger Bands) //! - Market microstructure features (spreads, imbalances, price impact) //! - TLOB (Time-Limited Order Book) features //! - Temporal and regime detection features //! - Portfolio performance and risk features use crate::training_pipeline::{MicrostructureConfig, TLOBConfig, TechnicalIndicatorsConfig}; use chrono::{DateTime, Datelike, Timelike, Utc}; use core::types::prelude::*; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap, VecDeque}; /// Feature vector for ML model training #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureVector { /// Timestamp pub timestamp: DateTime, /// Symbol pub symbol: String, /// Feature values pub features: HashMap, /// Metadata pub metadata: FeatureMetadata, } /// Feature metadata #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureMetadata { /// Feature names and descriptions pub feature_descriptions: HashMap, /// Feature categories pub feature_categories: HashMap, /// Data quality indicators pub quality_indicators: HashMap, } /// Feature categories for organization #[derive(Debug, Clone, Serialize, Deserialize)] pub enum FeatureCategory { Price, Volume, TechnicalIndicator, Microstructure, Temporal, Regime, TLOB, Portfolio, Risk, } /// Technical indicators calculator pub struct TechnicalIndicators { config: TechnicalIndicatorsConfig, price_data: BTreeMap>, volume_data: BTreeMap>, indicators: BTreeMap, } /// Price point for calculations #[derive(Debug, Clone)] pub struct PricePoint { pub timestamp: DateTime, pub open: f64, pub high: f64, pub low: f64, pub close: f64, } /// Volume point for calculations #[derive(Debug, Clone)] pub struct VolumePoint { pub timestamp: DateTime, pub volume: f64, pub volume_weighted_price: f64, } /// Indicator state for maintaining calculations #[derive(Debug, Clone)] pub struct IndicatorState { pub sma: HashMap, pub ema: HashMap, pub rsi: HashMap, pub macd: MACDState, pub bollinger: HashMap, } /// MACD indicator state #[derive(Debug, Clone)] pub struct MACDState { pub macd_line: f64, pub signal_line: f64, pub histogram: f64, pub fast_ema: f64, pub slow_ema: f64, pub signal_ema: f64, } /// Bollinger Bands state #[derive(Debug, Clone)] pub struct BollingerBandsState { pub upper_band: f64, pub middle_band: f64, pub lower_band: f64, pub bandwidth: f64, pub percent_b: f64, } /// Market microstructure analyzer pub struct MicrostructureAnalyzer { config: MicrostructureConfig, order_books: HashMap, trade_data: BTreeMap>, quote_data: BTreeMap>, } /// Order book state for microstructure analysis #[derive(Debug, Clone)] pub struct OrderBookState { pub timestamp: DateTime, pub bids: Vec, pub asks: Vec, pub mid_price: f64, pub spread: f64, pub imbalance: f64, pub depth: f64, } /// Price level in order book #[derive(Debug, Clone)] pub struct PriceLevel { pub price: f64, pub size: f64, } /// Trade data for microstructure analysis #[derive(Debug, Clone)] pub struct TradeData { pub timestamp: DateTime, pub price: f64, pub size: f64, pub direction: TradeDirection, } /// Quote data for microstructure analysis #[derive(Debug, Clone)] pub struct QuoteData { pub timestamp: DateTime, pub bid: f64, pub ask: f64, pub bid_size: f64, pub ask_size: f64, } /// Trade direction classification #[derive(Debug, Clone)] pub enum TradeDirection { Buy, Sell, Unknown, } /// TLOB (Time-Limited Order Book) analyzer pub struct TLOBAnalyzer { config: TLOBConfig, book_snapshots: BTreeMap>, order_flow: BTreeMap>, } /// TLOB snapshot for analysis #[derive(Debug, Clone)] pub struct TLOBSnapshot { pub timestamp: DateTime, pub book: OrderBookState, pub flow_imbalance: f64, pub volume_imbalance: f64, pub price_impact: f64, pub liquidity_score: f64, } /// Order flow event for TLOB analysis #[derive(Debug, Clone)] pub struct OrderFlowEvent { pub timestamp: DateTime, pub event_type: OrderFlowEventType, pub price: f64, pub size: f64, pub side: OrderSide, } /// Order flow event types #[derive(Debug, Clone)] pub enum OrderFlowEventType { NewOrder, OrderCancel, OrderModify, Trade, MarketDataUpdate, } /// Temporal feature extractor pub struct TemporalFeatures; /// Regime detection analyzer pub struct RegimeDetector { pub volatility_history: BTreeMap>, pub volume_history: BTreeMap>, pub price_history: BTreeMap>, pub correlation_matrix: HashMap>, } /// Portfolio performance analyzer pub struct PortfolioAnalyzer { pub positions: HashMap, pub pnl_history: VecDeque, pub risk_metrics: RiskMetrics, } /// Position information #[derive(Debug, Clone)] pub struct Position { pub symbol: String, pub quantity: f64, pub avg_price: f64, pub market_value: f64, pub unrealized_pnl: f64, pub realized_pnl: f64, } /// P&L tracking point #[derive(Debug, Clone)] pub struct PnLPoint { pub timestamp: DateTime, pub total_pnl: f64, pub unrealized_pnl: f64, pub realized_pnl: f64, pub portfolio_value: f64, } /// Risk metrics #[derive(Debug, Clone)] pub struct RiskMetrics { pub var_95: f64, pub var_99: f64, pub expected_shortfall: f64, pub maximum_drawdown: f64, pub sharpe_ratio: f64, pub sortino_ratio: f64, pub beta: f64, pub alpha: f64, } impl TechnicalIndicators { /// Create new technical indicators calculator pub fn new(config: TechnicalIndicatorsConfig) -> Self { Self { config, price_data: BTreeMap::new(), volume_data: BTreeMap::new(), indicators: BTreeMap::new(), } } /// Update with new price data pub fn update_price(&mut self, symbol: &str, price_point: PricePoint) { let data = self .price_data .entry(symbol.to_string()) .or_insert_with(VecDeque::new); data.push_back(price_point.clone()); // Keep only required data based on largest period let max_period = self.config.ma_periods.iter().max().unwrap_or(&200); while data.len() > *max_period as usize { data.pop_front(); } // Update indicators self.update_indicators(symbol); } /// Calculate features for a symbol pub fn calculate_features(&self, symbol: &str) -> HashMap { let mut features = HashMap::new(); if let Some(indicators) = self.indicators.get(symbol) { // Simple Moving Averages for &period in &self.config.ma_periods { if let Some(&sma) = indicators.sma.get(&period) { features.insert(format!("sma_{}", period), sma); } } // Exponential Moving Averages for &period in &self.config.ma_periods { if let Some(&ema) = indicators.ema.get(&period) { features.insert(format!("ema_{}", period), ema); } } // RSI for &period in &self.config.rsi_periods { if let Some(&rsi) = indicators.rsi.get(&period) { features.insert(format!("rsi_{}", period), rsi); } } // MACD features.insert("macd_line".to_string(), indicators.macd.macd_line); features.insert("macd_signal".to_string(), indicators.macd.signal_line); features.insert("macd_histogram".to_string(), indicators.macd.histogram); // Bollinger Bands for &period in &self.config.bollinger_periods { if let Some(bb) = indicators.bollinger.get(&period) { features.insert(format!("bb_upper_{}", period), bb.upper_band); features.insert(format!("bb_middle_{}", period), bb.middle_band); features.insert(format!("bb_lower_{}", period), bb.lower_band); features.insert(format!("bb_bandwidth_{}", period), bb.bandwidth); features.insert(format!("bb_percent_b_{}", period), bb.percent_b); } } } features } /// Update all indicators for a symbol fn update_indicators(&mut self, symbol: &str) { // Get price data first to avoid borrowing conflicts let price_data = match self.price_data.get(symbol) { Some(data) => data.clone(), None => return, }; // Get configuration periods let ma_periods = self.config.ma_periods.clone(); let rsi_periods = self.config.rsi_periods.clone(); let bollinger_periods = self.config.bollinger_periods.clone(); // Get or create indicator state let indicator_state = self .indicators .entry(symbol.to_string()) .or_insert_with(|| IndicatorState { sma: HashMap::new(), ema: HashMap::new(), rsi: HashMap::new(), macd: MACDState { macd_line: 0.0, signal_line: 0.0, histogram: 0.0, fast_ema: 0.0, slow_ema: 0.0, signal_ema: 0.0, }, bollinger: HashMap::new(), }); // Store current EMA values and MACD state for calculations let current_ema_values: HashMap = indicator_state.ema.clone(); let current_macd_state = indicator_state.macd.clone(); // Now we can perform calculations without borrowing conflicts // Update SMA for &period in &ma_periods { if let Some(sma) = Self::calculate_sma_static(&price_data, period) { indicator_state.sma.insert(period, sma); } } // Update EMA for &period in &ma_periods { if let Some(ema) = Self::calculate_ema_static( &price_data, period, current_ema_values.get(&period).copied(), ) { indicator_state.ema.insert(period, ema); } } // Update RSI for &period in &rsi_periods { if let Some(rsi) = Self::calculate_rsi_static(&price_data, period) { indicator_state.rsi.insert(period, rsi); } } // Update MACD indicator_state.macd = Self::calculate_macd_static(&price_data, ¤t_macd_state); // Update Bollinger Bands for &period in &bollinger_periods { if let Some(bb) = Self::calculate_bollinger_bands_static(&price_data, period) { indicator_state.bollinger.insert(period, bb); } } } /// Calculate Simple Moving Average fn calculate_sma(&self, data: &VecDeque, period: u32) -> Option { if data.len() < period as usize { return None; } let sum: f64 = data .iter() .rev() .take(period as usize) .map(|p| p.close) .sum(); Some(sum / period as f64) } /// Calculate Exponential Moving Average fn calculate_ema( &self, data: &VecDeque, period: u32, prev_ema: Option, ) -> Option { if data.is_empty() { return None; } let current_price = data.back().unwrap().close; let alpha = 2.0 / (period as f64 + 1.0); match prev_ema { Some(prev) => Some(alpha * current_price + (1.0 - alpha) * prev), None => Some(current_price), // First EMA value is the first price } } /// Calculate Relative Strength Index fn calculate_rsi(&self, data: &VecDeque, period: u32) -> Option { if data.len() < (period + 1) as usize { return None; } let mut gains = Vec::new(); let mut losses = Vec::new(); for window in data .iter() .rev() .take(period as usize + 1) .collect::>() .windows(2) { let change = window[0].close - window[1].close; if change > 0.0 { gains.push(change); losses.push(0.0); } else { gains.push(0.0); losses.push(-change); } } let avg_gain: f64 = gains.iter().sum::() / period as f64; let avg_loss: f64 = losses.iter().sum::() / period as f64; if avg_loss == 0.0 { return Some(100.0); } let rs = avg_gain / avg_loss; Some(100.0 - (100.0 / (1.0 + rs))) } /// Calculate MACD fn calculate_macd(&self, data: &VecDeque, prev_state: &MACDState) -> MACDState { if data.is_empty() { return prev_state.clone(); } let current_price = data.back().unwrap().close; let fast_alpha = 2.0 / (self.config.macd.fast_period as f64 + 1.0); let slow_alpha = 2.0 / (self.config.macd.slow_period as f64 + 1.0); let signal_alpha = 2.0 / (self.config.macd.signal_period as f64 + 1.0); let fast_ema = if prev_state.fast_ema == 0.0 { current_price } else { fast_alpha * current_price + (1.0 - fast_alpha) * prev_state.fast_ema }; let slow_ema = if prev_state.slow_ema == 0.0 { current_price } else { slow_alpha * current_price + (1.0 - slow_alpha) * prev_state.slow_ema }; let macd_line = fast_ema - slow_ema; let signal_line = if prev_state.signal_ema == 0.0 { macd_line } else { signal_alpha * macd_line + (1.0 - signal_alpha) * prev_state.signal_line }; let histogram = macd_line - signal_line; MACDState { macd_line, signal_line, histogram, fast_ema, slow_ema, signal_ema: signal_line, } } /// Calculate Bollinger Bands fn calculate_bollinger_bands( &self, data: &VecDeque, period: u32, ) -> Option { if data.len() < period as usize { return None; } let prices: Vec = data .iter() .rev() .take(period as usize) .map(|p| p.close) .collect(); let mean = prices.iter().sum::() / period as f64; let variance = prices.iter().map(|&p| (p - mean).powi(2)).sum::() / period as f64; let std_dev = variance.sqrt(); let upper_band = mean + 2.0 * std_dev; let lower_band = mean - 2.0 * std_dev; let bandwidth = (upper_band - lower_band) / mean; let current_price = data.back().unwrap().close; let percent_b = if upper_band != lower_band { (current_price - lower_band) / (upper_band - lower_band) } else { 0.5 }; Some(BollingerBandsState { upper_band, middle_band: mean, lower_band, bandwidth, percent_b, }) } // Static methods to avoid borrowing conflicts /// Calculate Simple Moving Average (static version) fn calculate_sma_static(data: &VecDeque, period: u32) -> Option { if data.len() < period as usize { return None; } let sum: f64 = data .iter() .rev() .take(period as usize) .map(|p| p.close) .sum(); Some(sum / period as f64) } /// Calculate Exponential Moving Average (static version) fn calculate_ema_static( data: &VecDeque, period: u32, prev_ema: Option, ) -> Option { if data.is_empty() { return None; } let current_price = data.back().unwrap().close; let alpha = 2.0 / (period as f64 + 1.0); match prev_ema { Some(prev) => Some(alpha * current_price + (1.0 - alpha) * prev), None => Some(current_price), // First EMA value is the current price } } /// Calculate RSI (static version) fn calculate_rsi_static(data: &VecDeque, period: u32) -> Option { if data.len() < (period + 1) as usize { return None; } let mut gains = 0.0; let mut losses = 0.0; for i in 0..period { let idx = data.len() - 1 - i as usize; let prev_idx = data.len() - 2 - i as usize; let change = data[idx].close - data[prev_idx].close; if change > 0.0 { gains += change; } else { losses += -change; } } let avg_gain = gains / period as f64; let avg_loss = losses / period as f64; if avg_loss == 0.0 { return Some(100.0); } let rs = avg_gain / avg_loss; Some(100.0 - (100.0 / (1.0 + rs))) } /// Calculate MACD (static version) fn calculate_macd_static(data: &VecDeque, prev_state: &MACDState) -> MACDState { if data.is_empty() { return prev_state.clone(); } let current_price = data.back().unwrap().close; // Use fixed MACD parameters let fast_alpha = 2.0 / 13.0; // 12-day EMA let slow_alpha = 2.0 / 27.0; // 26-day EMA let signal_alpha = 2.0 / 10.0; // 9-day EMA let fast_ema = if prev_state.fast_ema == 0.0 { current_price } else { fast_alpha * current_price + (1.0 - fast_alpha) * prev_state.fast_ema }; let slow_ema = if prev_state.slow_ema == 0.0 { current_price } else { slow_alpha * current_price + (1.0 - slow_alpha) * prev_state.slow_ema }; let macd_line = fast_ema - slow_ema; let signal_line = if prev_state.signal_line == 0.0 { macd_line } else { signal_alpha * macd_line + (1.0 - signal_alpha) * prev_state.signal_line }; let histogram = macd_line - signal_line; MACDState { macd_line, signal_line, histogram, fast_ema, slow_ema, signal_ema: signal_line, } } /// Calculate Bollinger Bands (static version) fn calculate_bollinger_bands_static( data: &VecDeque, period: u32, ) -> Option { if data.len() < period as usize { return None; } let prices: Vec = data .iter() .rev() .take(period as usize) .map(|p| p.close) .collect(); let mean = prices.iter().sum::() / period as f64; let variance = prices.iter().map(|&p| (p - mean).powi(2)).sum::() / period as f64; let std_dev = variance.sqrt(); let upper_band = mean + 2.0 * std_dev; let lower_band = mean - 2.0 * std_dev; let bandwidth = (upper_band - lower_band) / mean; let current_price = data.back().unwrap().close; let percent_b = if upper_band != lower_band { (current_price - lower_band) / (upper_band - lower_band) } else { 0.5 }; Some(BollingerBandsState { upper_band, middle_band: mean, lower_band, bandwidth, percent_b, }) } } impl MicrostructureAnalyzer { /// Create new microstructure analyzer pub fn new(config: MicrostructureConfig) -> Self { Self { config, order_books: HashMap::new(), trade_data: BTreeMap::new(), quote_data: BTreeMap::new(), } } /// Update with new quote data pub fn update_quote(&mut self, symbol: &str, quote: QuoteData) { let data = self .quote_data .entry(symbol.to_string()) .or_insert_with(VecDeque::new); data.push_back(quote); // Keep only recent data while data.len() > 1000 { data.pop_front(); } } /// Update with new trade data pub fn update_trade(&mut self, symbol: &str, trade: TradeData) { let data = self .trade_data .entry(symbol.to_string()) .or_insert_with(VecDeque::new); data.push_back(trade); // Keep only recent data while data.len() > 1000 { data.pop_front(); } } /// Calculate microstructure features pub fn calculate_features(&self, symbol: &str) -> HashMap { let mut features = HashMap::new(); // Bid-ask spread features if self.config.bid_ask_spread { if let Some(spread) = self.calculate_bid_ask_spread(symbol) { features.insert("bid_ask_spread".to_string(), spread.absolute); features.insert("bid_ask_spread_bps".to_string(), spread.basis_points); features.insert("bid_ask_spread_pct".to_string(), spread.percentage); } } // Volume imbalance if self.config.volume_imbalance { if let Some(imbalance) = self.calculate_volume_imbalance(symbol) { features.insert("volume_imbalance".to_string(), imbalance); } } // Price impact if self.config.price_impact { if let Some(impact) = self.calculate_price_impact(symbol) { features.insert("price_impact".to_string(), impact); } } // Kyle's lambda if self.config.kyle_lambda { if let Some(lambda) = self.calculate_kyle_lambda(symbol) { features.insert("kyle_lambda".to_string(), lambda); } } // Amihud illiquidity ratio if self.config.amihud_ratio { if let Some(ratio) = self.calculate_amihud_ratio(symbol) { features.insert("amihud_ratio".to_string(), ratio); } } // Roll spread if self.config.roll_spread { if let Some(roll) = self.calculate_roll_spread(symbol) { features.insert("roll_spread".to_string(), roll); } } features } /// Calculate bid-ask spread metrics fn calculate_bid_ask_spread(&self, symbol: &str) -> Option { let quote_data = self.quote_data.get(symbol)?; let latest_quote = quote_data.back()?; let absolute = latest_quote.ask - latest_quote.bid; let mid_price = (latest_quote.ask + latest_quote.bid) / 2.0; let percentage = absolute / mid_price; let basis_points = percentage * 10000.0; Some(SpreadMetrics { absolute, percentage, basis_points, }) } /// Calculate volume imbalance fn calculate_volume_imbalance(&self, symbol: &str) -> Option { let quote_data = self.quote_data.get(symbol)?; let latest_quote = quote_data.back()?; let total_volume = latest_quote.bid_size + latest_quote.ask_size; if total_volume == 0.0 { return Some(0.0); } Some((latest_quote.bid_size - latest_quote.ask_size) / total_volume) } /// Calculate price impact fn calculate_price_impact(&self, symbol: &str) -> Option { let trade_data = self.trade_data.get(symbol)?; if trade_data.len() < 2 { return None; } let recent_trades: Vec<&TradeData> = trade_data.iter().rev().take(10).collect(); let price_changes: Vec = recent_trades .windows(2) .map(|w| w[0].price - w[1].price) .collect(); if price_changes.is_empty() { return None; } let avg_price_change = price_changes.iter().sum::() / price_changes.len() as f64; Some(avg_price_change) } /// Calculate Kyle's lambda (price impact parameter) fn calculate_kyle_lambda(&self, symbol: &str) -> Option { // Simplified Kyle's lambda calculation // In practice, this would require more sophisticated regression analysis let trade_data = self.trade_data.get(symbol)?; let quote_data = self.quote_data.get(symbol)?; if trade_data.len() < 10 || quote_data.len() < 10 { return None; } // This is a simplified placeholder implementation // Real Kyle's lambda requires regression of price changes on signed order flow Some(0.001) // Placeholder value } /// Calculate Amihud illiquidity ratio fn calculate_amihud_ratio(&self, symbol: &str) -> Option { let trade_data = self.trade_data.get(symbol)?; if trade_data.len() < 2 { return None; } let recent_trades: Vec<&TradeData> = trade_data.iter().rev().take(20).collect(); let mut total_ratio = 0.0; let mut count = 0; for window in recent_trades.windows(2) { let price_change = (window[0].price - window[1].price).abs(); let volume = window[0].size; if volume > 0.0 { total_ratio += price_change / volume; count += 1; } } if count > 0 { Some(total_ratio / count as f64) } else { None } } /// Calculate Roll spread estimator fn calculate_roll_spread(&self, symbol: &str) -> Option { let trade_data = self.trade_data.get(symbol)?; if trade_data.len() < 3 { return None; } let recent_trades: Vec<&TradeData> = trade_data.iter().rev().take(50).collect(); let price_changes: Vec = recent_trades .windows(2) .map(|w| w[0].price - w[1].price) .collect(); if price_changes.len() < 2 { return None; } // Calculate serial covariance let mean_change = price_changes.iter().sum::() / price_changes.len() as f64; let covariance: f64 = price_changes .windows(2) .map(|w| (w[0] - mean_change) * (w[1] - mean_change)) .sum::() / (price_changes.len() - 1) as f64; Some(2.0 * (-covariance).max(0.0).sqrt()) } } /// Spread metrics #[derive(Debug, Clone)] pub struct SpreadMetrics { pub absolute: f64, pub percentage: f64, pub basis_points: f64, } impl TemporalFeatures { /// Extract temporal features from timestamp pub fn extract_features(timestamp: DateTime) -> HashMap { let mut features = HashMap::new(); // Time of day features let hour = timestamp.hour() as f64; let minute = timestamp.minute() as f64; features.insert("hour".to_string(), hour); features.insert("minute".to_string(), minute); features.insert( "hour_sin".to_string(), (hour * 2.0 * std::f64::consts::PI / 24.0).sin(), ); features.insert( "hour_cos".to_string(), (hour * 2.0 * std::f64::consts::PI / 24.0).cos(), ); // Day of week features let weekday = timestamp.weekday().num_days_from_monday() as f64; features.insert("weekday".to_string(), weekday); features.insert( "is_weekend".to_string(), if weekday >= 5.0 { 1.0 } else { 0.0 }, ); // Market session features (assuming US market hours) let is_premarket = hour < 9.0 || (hour == 9.0 && minute < 30.0); let is_regular_hours = (hour > 9.0 || (hour == 9.0 && minute >= 30.0)) && hour < 16.0; let is_aftermarket = hour >= 16.0 && hour < 20.0; features.insert( "is_premarket".to_string(), if is_premarket { 1.0 } else { 0.0 }, ); features.insert( "is_regular_hours".to_string(), if is_regular_hours { 1.0 } else { 0.0 }, ); features.insert( "is_aftermarket".to_string(), if is_aftermarket { 1.0 } else { 0.0 }, ); // Month and day features let month = timestamp.month() as f64; let day = timestamp.day() as f64; features.insert("month".to_string(), month); features.insert("day".to_string(), day); features.insert( "is_month_end".to_string(), if day >= 28.0 { 1.0 } else { 0.0 }, ); features.insert( "is_quarter_end".to_string(), if month % 3.0 == 0.0 && day >= 28.0 { 1.0 } else { 0.0 }, ); features } } #[cfg(test)] mod tests { use super::*; #[test] fn test_technical_indicators_creation() { let config = TechnicalIndicatorsConfig { ma_periods: vec![10, 20], rsi_periods: vec![14], bollinger_periods: vec![20], macd: crate::training_pipeline::MACDConfig { fast_period: 12, slow_period: 26, signal_period: 9, }, volume_indicators: true, }; let indicators = TechnicalIndicators::new(config); assert!(indicators.price_data.is_empty()); assert!(indicators.indicators.is_empty()); } #[test] fn test_temporal_features() { let timestamp = Utc::now(); let features = TemporalFeatures::extract_features(timestamp); assert!(features.contains_key("hour")); assert!(features.contains_key("weekday")); assert!(features.contains_key("is_regular_hours")); } #[test] fn test_microstructure_analyzer() { let config = MicrostructureConfig { bid_ask_spread: true, volume_imbalance: true, price_impact: true, kyle_lambda: false, amihud_ratio: false, roll_spread: false, }; let analyzer = MicrostructureAnalyzer::new(config); assert!(analyzer.order_books.is_empty()); assert!(analyzer.trade_data.is_empty()); } }