//! Wave C Microstructure Features for HFT ML Models //! //! This module implements 12 microstructure features from `MLFinLab` Chapter 19: //! - **Spread Estimators** (3): Roll, Corwin-Schultz, High-Low Spread //! - **Liquidity Metrics** (2): Amihud Illiquidity, Volume-Weighted Spread //! - **Trade Arrival** (2): Tick Count, Inter-Arrival Time //! - **Order Flow** (2): Buy/Sell Imbalance, VPIN (not implemented - O(n) complexity) //! - **Market Impact** (2): Kyle's Lambda (slow-updating), Price Impact //! - **Efficiency** (1): Variance Ratio //! //! ## Integration with Wave A //! Three features are already implemented in `microstructure.rs` (Wave A): //! - Roll Measure (Feature 115) //! - Corwin-Schultz Spread (Feature 116) //! - Amihud Illiquidity (Feature 117) //! //! Wave C adds 9 new features (118-126): //! - High-Low Spread (118) //! - Volume-Weighted Spread (119) //! - Tick Count (120) //! - Inter-Arrival Time (121) //! - Buy/Sell Imbalance (122) //! - Kyle's Lambda (123, slow-updating) //! - Price Impact (124) //! - Variance Ratio (125) //! - Reserved (126) //! //! ## Performance Targets //! - Latency: <200μs for all 12 features per bar (cumulative) //! - Memory: ≤500 bytes per symbol //! - Data: OHLCV-only (no Level-2 order book required) //! //! ## References //! - `MLFinLab` Chapter 19: Market Microstructure Features //! - See `WAVE_C_MICROSTRUCTURE_FEATURE_DESIGN.md` for detailed specifications use std::collections::VecDeque; // ============================================================================ // Trait Definition // ============================================================================ /// Common trait for all microstructure features pub trait MicrostructureFeature { /// Returns the feature name for logging/debugging fn feature_name(&self) -> &'static str; /// Returns the raw feature value fn value(&self) -> f64; /// Returns normalized feature value for ML training (typically [-1, 1]) fn get_normalized(&self) -> f64; /// Resets internal state (useful for backtesting) fn reset(&mut self); } // ============================================================================ // 1. High-Low Spread (Feature 118) // ============================================================================ /// High-Low Spread: Simple spread estimator from intrabar range /// /// ## Formula /// ```text /// High-Low Spread = (High - Low) / ((High + Low) / 2) /// ``` /// /// ## Interpretation /// - Measures intrabar volatility as a proxy for bid-ask spread /// - Higher values indicate wider spreads (less liquid) /// - Smoothed with EMA for stability /// /// ## Performance /// - Latency: <5μs per update /// - Memory: 16 bytes (2 f64 fields) #[derive(Debug, Clone)] pub struct HighLowSpread { /// EMA smoothing factor alpha: f64, /// Exponentially weighted average of spread ema_spread: f64, } impl HighLowSpread { pub fn new(alpha: f64) -> Self { assert!(alpha > 0.0 && alpha <= 1.0, "Alpha must be in (0, 1]"); Self { alpha, ema_spread: 0.0, } } pub fn default() -> Self { Self::new(0.05) // 20-bar effective window } /// Update with new OHLC bar pub fn update(&mut self, high: f64, low: f64) -> f64 { if high <= 0.0 || low <= 0.0 || high < low { return self.ema_spread; } let midpoint = (high + low) / 2.0; let instant_spread = (high - low) / midpoint; // Initialize EMA on first valid update to avoid slow convergence if self.ema_spread == 0.0 { self.ema_spread = instant_spread; } else { self.ema_spread = self.alpha * instant_spread + (1.0 - self.alpha) * self.ema_spread; } self.ema_spread } pub const fn compute(&self) -> f64 { self.ema_spread } } impl Default for HighLowSpread { fn default() -> Self { Self::default() } } impl MicrostructureFeature for HighLowSpread { fn feature_name(&self) -> &'static str { "high_low_spread" } fn value(&self) -> f64 { self.ema_spread } fn get_normalized(&self) -> f64 { // High-low spread typically 0.01% - 5.0% let clamped = self.ema_spread.clamp(0.0, 0.05); (clamped / 0.025) - 1.0 // Map [0, 2.5%] to [-1, 1] } fn reset(&mut self) { self.ema_spread = 0.0; } } // ============================================================================ // 2. Volume-Weighted Spread (Feature 119) // ============================================================================ /// Volume-Weighted Spread: Adjusts spread estimate by relative volume /// /// ## Formula /// ```text /// VW_Spread = Spread * (Volume / Avg_Volume) /// ``` /// /// ## Interpretation /// - High volume + wide spread = illiquid market under stress /// - Low volume + wide spread = normal illiquidity /// - Used for transaction cost estimation /// /// ## Performance /// - Latency: <10μs per update /// - Memory: 32 bytes #[derive(Debug, Clone)] pub struct VolumeWeightedSpread { alpha: f64, ema_volume: f64, ema_spread: f64, } impl VolumeWeightedSpread { pub fn new(alpha: f64) -> Self { assert!(alpha > 0.0 && alpha <= 1.0, "Alpha must be in (0, 1]"); Self { alpha, ema_volume: 0.0, ema_spread: 0.0, } } pub fn default() -> Self { Self::new(0.05) } /// Update with spread and volume pub fn update(&mut self, spread: f64, volume: f64) -> f64 { if volume <= 0.0 { return self.ema_spread; } // Update average volume if self.ema_volume == 0.0 { self.ema_volume = volume; } else { self.ema_volume = self.alpha * volume + (1.0 - self.alpha) * self.ema_volume; } // Calculate volume-weighted spread let volume_ratio = volume / self.ema_volume.max(1.0); let vw_spread = spread * volume_ratio; self.ema_spread = self.alpha * vw_spread + (1.0 - self.alpha) * self.ema_spread; self.ema_spread } pub const fn compute(&self) -> f64 { self.ema_spread } } impl Default for VolumeWeightedSpread { fn default() -> Self { Self::default() } } impl MicrostructureFeature for VolumeWeightedSpread { fn feature_name(&self) -> &'static str { "volume_weighted_spread" } fn value(&self) -> f64 { self.ema_spread } fn get_normalized(&self) -> f64 { // VW spread typically 0.01% - 10.0% (wider range due to volume weighting) let clamped = self.ema_spread.clamp(0.0, 0.10); (clamped / 0.05) - 1.0 // Map [0, 5%] to [-1, 1] } fn reset(&mut self) { self.ema_volume = 0.0; self.ema_spread = 0.0; } } // ============================================================================ // 3. Tick Count (Feature 120) // ============================================================================ /// Tick Count: Number of price changes in rolling window /// /// ## Formula /// ```text /// Tick_Count = Count of bars with non-zero price change /// ``` /// /// ## Interpretation /// - High tick count = active trading, good price discovery /// - Low tick count = stale market, wide spreads /// /// ## Performance /// - Latency: <2μs per update /// - Memory: 24 bytes #[derive(Debug, Clone)] pub struct TickCount { window_size: usize, tick_count: usize, prev_price: f64, price_changes: VecDeque, } impl TickCount { pub fn new(window_size: usize) -> Self { Self { window_size, tick_count: 0, prev_price: 0.0, price_changes: VecDeque::with_capacity(window_size), } } pub fn default() -> Self { Self::new(20) // 20-bar window } /// Update with new price pub fn update(&mut self, price: f64) -> usize { if self.prev_price == 0.0 { self.prev_price = price; return 0; } let price_changed = (price - self.prev_price).abs() > 1e-9; if price_changed { self.tick_count += 1; } self.price_changes.push_back(price_changed); if self.price_changes.len() > self.window_size { if let Some(old_change) = self.price_changes.pop_front() { if old_change { self.tick_count = self.tick_count.saturating_sub(1); } } } self.prev_price = price; self.tick_count } pub const fn compute(&self) -> usize { self.tick_count } } impl Default for TickCount { fn default() -> Self { Self::default() } } impl MicrostructureFeature for TickCount { fn feature_name(&self) -> &'static str { "tick_count" } fn value(&self) -> f64 { self.tick_count as f64 } fn get_normalized(&self) -> f64 { // Tick count 0-20 (window size) let ratio = self.tick_count as f64 / self.window_size as f64; 2.0 * ratio - 1.0 // Map [0, 1] to [-1, 1] } fn reset(&mut self) { self.tick_count = 0; self.prev_price = 0.0; self.price_changes.clear(); } } // ============================================================================ // 4. Inter-Arrival Time (Feature 121) // ============================================================================ /// Inter-Arrival Time: Average time between bars in rolling window /// /// ## Formula /// ```text /// Inter_Arrival = Avg(timestamp`[i]` - timestamp[i-1]) /// ``` /// /// ## Interpretation /// - Short inter-arrival = high trading activity /// - Long inter-arrival = low activity, wider spreads /// /// ## Performance /// - Latency: <5μs per update /// - Memory: 160 bytes (20 timestamps) #[derive(Debug, Clone)] pub struct InterArrivalTime { window_size: usize, timestamps: VecDeque, } impl InterArrivalTime { pub fn new(window_size: usize) -> Self { Self { window_size, timestamps: VecDeque::with_capacity(window_size), } } pub fn default() -> Self { Self::new(20) } /// Update with new timestamp (nanoseconds) pub fn update(&mut self, timestamp_ns: u64) -> f64 { self.timestamps.push_back(timestamp_ns); if self.timestamps.len() > self.window_size { self.timestamps.pop_front(); } self.compute() } /// Compute average inter-arrival time in seconds pub fn compute(&self) -> f64 { if self.timestamps.len() < 2 { return 0.0; } let mut total_diff = 0_u64; for i in 1..self.timestamps.len() { let diff = self.timestamps[i].saturating_sub(self.timestamps[i - 1]); total_diff += diff; } let avg_ns = total_diff as f64 / (self.timestamps.len() - 1) as f64; avg_ns / 1_000_000_000.0 // Convert to seconds } } impl Default for InterArrivalTime { fn default() -> Self { Self::default() } } impl MicrostructureFeature for InterArrivalTime { fn feature_name(&self) -> &'static str { "inter_arrival_time" } fn value(&self) -> f64 { self.compute() } fn get_normalized(&self) -> f64 { // Inter-arrival time: 0.1 - 10 seconds (typical range) let log_time = (self.compute() + 0.01).ln(); let clamped = log_time.clamp(-5.0, 3.0); clamped / 4.0 // Map to [-1.25, 0.75], acceptable asymmetry } fn reset(&mut self) { self.timestamps.clear(); } } // ============================================================================ // 5. Buy/Sell Imbalance (Feature 122) // ============================================================================ /// Buy/Sell Imbalance: Order flow imbalance using tick rule /// /// ## Formula /// ```text /// Imbalance = (Buy_Volume - Sell_Volume) / Total_Volume /// Trade classified as buy if price_t > price_{t-1} /// ``` /// /// ## Interpretation /// - Positive = buying pressure (bullish) /// - Negative = selling pressure (bearish) /// - Used for short-term mean reversion signals /// /// ## Performance /// - Latency: <3μs per update /// - Memory: 32 bytes #[derive(Debug, Clone)] pub struct BuySellImbalance { alpha: f64, ema_imbalance: f64, prev_price: f64, } impl BuySellImbalance { pub fn new(alpha: f64) -> Self { assert!(alpha > 0.0 && alpha <= 1.0, "Alpha must be in (0, 1]"); Self { alpha, ema_imbalance: 0.0, prev_price: 0.0, } } pub fn default() -> Self { Self::new(0.1) // 10-bar effective window } /// Update with price and volume (tick rule classification) pub fn update(&mut self, price: f64, volume: f64) -> f64 { if self.prev_price == 0.0 { self.prev_price = price; return 0.0; } if volume <= 0.0 { return self.ema_imbalance; } // Tick rule: positive price change = buy, negative = sell let instant_imbalance = if price > self.prev_price { 1.0 } else if price < self.prev_price { -1.0 } else { 0.0 // Zero tick: no classification }; self.ema_imbalance = self.alpha * instant_imbalance + (1.0 - self.alpha) * self.ema_imbalance; self.prev_price = price; self.ema_imbalance } pub const fn compute(&self) -> f64 { self.ema_imbalance } } impl Default for BuySellImbalance { fn default() -> Self { Self::default() } } impl MicrostructureFeature for BuySellImbalance { fn feature_name(&self) -> &'static str { "buy_sell_imbalance" } fn value(&self) -> f64 { self.ema_imbalance } fn get_normalized(&self) -> f64 { // Already bounded [-1, 1] self.ema_imbalance } fn reset(&mut self) { self.ema_imbalance = 0.0; self.prev_price = 0.0; } } // ============================================================================ // 6. Kyle's Lambda (Feature 123) - Slow-Updating Feature // ============================================================================ /// Kyle's Lambda: Market impact measure from regression /// /// ## Formula (Incremental OLS) /// ```text /// r_t = α + λ * S_t + ε_t /// S_t = sign(Close - Open) * sqrt(Close * Volume) /// λ = Cov(r, S) / Var(S) /// ``` /// /// ## Interpretation /// - High λ = high price impact (illiquid) /// - Low λ = low price impact (liquid) /// - Slow-updating: Recompute every 5 minutes (50+ bars required) /// /// ## Performance /// - Latency: 50-100μs when updating, 0μs when cached /// - Memory: 800 bytes (50-period buffers) /// /// ## Usage Note /// ⚠️ Use as slow-updating feature (5-minute intervals), not real-time per-bar #[derive(Debug, Clone)] pub struct KyleLambda { update_interval_secs: u64, last_update_ns: u64, cached_lambda: f64, // Incremental statistics returns: VecDeque, signed_volumes: VecDeque, window_size: usize, } impl KyleLambda { pub fn new(update_interval_secs: u64, window_size: usize) -> Self { Self { update_interval_secs, last_update_ns: 0, cached_lambda: 0.0, returns: VecDeque::with_capacity(window_size), signed_volumes: VecDeque::with_capacity(window_size), window_size, } } pub fn default() -> Self { Self::new(300, 50) // 5 minutes, 50 periods } /// Maybe update lambda (only if interval elapsed) pub fn maybe_update(&mut self, timestamp_ns: u64, ret: f64, signed_volume: f64) -> f64 { // Add data point self.returns.push_back(ret); self.signed_volumes.push_back(signed_volume); if self.returns.len() > self.window_size { self.returns.pop_front(); self.signed_volumes.pop_front(); } // Check if update needed if timestamp_ns - self.last_update_ns >= self.update_interval_secs * 1_000_000_000 { self.cached_lambda = self.compute_lambda(); self.last_update_ns = timestamp_ns; } self.cached_lambda } /// Compute Kyle's Lambda via OLS regression fn compute_lambda(&self) -> f64 { if self.returns.len() < 10 { return 0.0; // Insufficient data } let n = self.returns.len() as f64; // Compute means let mean_r: f64 = self.returns.iter().sum::() / n; let mean_s: f64 = self.signed_volumes.iter().sum::() / n; // Compute covariance and variance let mut cov = 0.0; let mut var_s = 0.0; for i in 0..self.returns.len() { let r_dev = self.returns[i] - mean_r; let s_dev = self.signed_volumes[i] - mean_s; cov += r_dev * s_dev; var_s += s_dev * s_dev; } if var_s < 1e-12 { return 0.0; // No variance, no regression } cov / var_s } pub const fn compute(&self) -> f64 { self.cached_lambda } } impl Default for KyleLambda { fn default() -> Self { Self::default() } } impl MicrostructureFeature for KyleLambda { fn feature_name(&self) -> &'static str { "kyles_lambda" } fn value(&self) -> f64 { self.cached_lambda } fn get_normalized(&self) -> f64 { if self.cached_lambda <= 0.0 { return -1.0; } // Kyle's lambda typically 1e-8 to 1e-5 let log_lambda = (self.cached_lambda * 1e8).ln(); let clamped = log_lambda.clamp(-5.0, 5.0); clamped / 5.0 } fn reset(&mut self) { self.last_update_ns = 0; self.cached_lambda = 0.0; self.returns.clear(); self.signed_volumes.clear(); } } // ============================================================================ // 7. Price Impact (Feature 124) // ============================================================================ /// Price Impact: Permanent price change after trade /// /// ## Formula /// ```text /// Price_Impact = D_t * (M_{t+τ} - M_t) /// D_t = Trade direction (+1 buy, -1 sell) /// M_t = Midpoint (approximated as (High + Low) / 2) /// τ = 5 bars (forward-looking delay) /// ``` /// /// ## Interpretation /// - Positive = price moved with trade (expected impact) /// - Negative = adverse selection (price moved against trade) /// /// ## Performance /// - Latency: <8μs per update /// - Memory: 160 bytes (5-bar delay buffers) #[derive(Debug, Clone)] pub struct PriceImpact { alpha: f64, ema_impact: f64, delay_bars: usize, high_buffer: VecDeque, low_buffer: VecDeque, close_buffer: VecDeque, } impl PriceImpact { pub fn new(alpha: f64, delay_bars: usize) -> Self { assert!(alpha > 0.0 && alpha <= 1.0, "Alpha must be in (0, 1]"); Self { alpha, ema_impact: 0.0, delay_bars, high_buffer: VecDeque::with_capacity(delay_bars + 1), low_buffer: VecDeque::with_capacity(delay_bars + 1), close_buffer: VecDeque::with_capacity(delay_bars + 1), } } pub fn default() -> Self { Self::new(0.05, 5) // 20-bar EMA, 5-bar delay } /// Update with new OHLC bar pub fn update(&mut self, high: f64, low: f64, close: f64) -> f64 { let current_midpoint = (high + low) / 2.0; self.high_buffer.push_back(high); self.low_buffer.push_back(low); self.close_buffer.push_back(close); // Only compute impact once we have enough bars to establish direction if self.close_buffer.len() > self.delay_bars + 1 { let old_high = self.high_buffer.pop_front().unwrap_or(high); let old_low = self.low_buffer.pop_front().unwrap_or(low); let old_close = self.close_buffer.pop_front().unwrap_or(close); // Now close_buffer has at least delay_bars+1 elements // close_buffer[0] is the close AFTER old_close // We need the close BEFORE old_close, which we don't have in the buffer // So we need to track it separately or change the approach // Alternative: Use the next close in the buffer as reference // If old_close < next_close, that's a buy (positive direction) let next_close = self.close_buffer.front().copied().unwrap_or(close); let direction = (next_close - old_close).signum(); let old_midpoint = (old_high + old_low) / 2.0; let instant_impact = direction * (current_midpoint - old_midpoint); self.ema_impact = self.alpha * instant_impact + (1.0 - self.alpha) * self.ema_impact; } self.ema_impact } pub const fn compute(&self) -> f64 { self.ema_impact } } impl Default for PriceImpact { fn default() -> Self { Self::default() } } impl MicrostructureFeature for PriceImpact { fn feature_name(&self) -> &'static str { "price_impact" } fn value(&self) -> f64 { self.ema_impact } fn get_normalized(&self) -> f64 { // Price impact typically -2% to +2% let clamped = self.ema_impact.clamp(-0.02, 0.02); clamped / 0.01 // Map [-1%, 1%] to [-1, 1] } fn reset(&mut self) { self.ema_impact = 0.0; self.high_buffer.clear(); self.low_buffer.clear(); self.close_buffer.clear(); } } // ============================================================================ // 8. Variance Ratio (Feature 125) // ============================================================================ /// Variance Ratio: Tests for random walk (market efficiency) /// /// ## Formula /// ```text /// VR(q) = Var(r_t(q)) / (q * Var(r_t)) /// r_t(q) = q-period return /// r_t = 1-period return /// ``` /// /// ## Interpretation /// - VR = 1: Random walk (efficient market) /// - VR > 1: Positive serial correlation (momentum) /// - VR < 1: Negative serial correlation (mean reversion) /// /// ## Performance /// - Latency: <15μs per update /// - Memory: 160 bytes (20-bar window) #[derive(Debug, Clone)] pub struct VarianceRatio { window_size: usize, q: usize, // Multi-period lag returns: VecDeque, } impl VarianceRatio { pub fn new(window_size: usize, q: usize) -> Self { assert!(q >= 2, "q must be >= 2"); assert!(window_size >= q * 2, "Window size must be >= 2*q"); Self { window_size, q, returns: VecDeque::with_capacity(window_size), } } pub fn default() -> Self { Self::new(20, 5) // 20-bar window, 5-period lag } /// Update with new return pub fn update(&mut self, ret: f64) -> f64 { self.returns.push_back(ret); if self.returns.len() > self.window_size { self.returns.pop_front(); } self.compute() } /// Compute variance ratio pub fn compute(&self) -> f64 { if self.returns.len() < self.q * 2 { return 1.0; // Insufficient data, assume random walk } // Compute 1-period variance let var_1 = self.compute_variance(&self.returns); if var_1 < 1e-12 { return 1.0; // No variance } // Compute q-period returns let mut returns_q = VecDeque::new(); for i in 0..self.returns.len() { if i + self.q <= self.returns.len() { let sum_ret: f64 = self.returns.iter().skip(i).take(self.q).sum(); returns_q.push_back(sum_ret); } } if returns_q.is_empty() { return 1.0; } let var_q = self.compute_variance(&returns_q); // VR(q) = Var(r_q) / (q * Var(r_1)) let vr = var_q / (self.q as f64 * var_1); // Clamp to reasonable range vr.clamp(0.1, 3.0) } fn compute_variance(&self, data: &VecDeque) -> f64 { if data.is_empty() { return 0.0; } let n = data.len() as f64; let mean: f64 = data.iter().sum::() / n; let variance: f64 = data.iter().map(|x| (x - mean).powi(2)).sum::() / n; variance } } impl Default for VarianceRatio { fn default() -> Self { Self::default() } } impl MicrostructureFeature for VarianceRatio { fn feature_name(&self) -> &'static str { "variance_ratio" } fn value(&self) -> f64 { self.compute() } fn get_normalized(&self) -> f64 { // Variance ratio typically 0.5 - 2.0 // Map to [-1, 1] with VR=1 at center let vr = self.compute(); if vr < 1.0 { (vr - 0.5) / 0.5 // Map [0.5, 1.0] to [-1, 0] } else { (vr - 1.0) / 1.0 // Map [1.0, 2.0] to [0, 1] } } fn reset(&mut self) { self.returns.clear(); } } // ============================================================================ // Unit Tests // ============================================================================ #[cfg(test)] #[allow(clippy::manual_range_contains)] mod tests { use super::*; // High-Low Spread Tests #[test] fn test_high_low_spread_normal() { let mut spread = HighLowSpread::new(0.1); // Test with 1% spread - first update initializes EMA directly let value = spread.update(101.0, 99.0); let expected = (101.0 - 99.0) / ((101.0 + 99.0) / 2.0); assert!((value - expected).abs() < 1e-6); // First update: direct initialization // Second update should apply EMA smoothing let value2 = spread.update(101.0, 99.0); let expected2 = 0.1 * expected + 0.9 * expected; assert!((value2 - expected2).abs() < 1e-6); // EMA effect } #[test] fn test_high_low_spread_wide() { let mut spread = HighLowSpread::new(0.1); // 5% spread (wide) spread.update(105.0, 95.0); assert!(spread.compute() > 0.04); } // Volume-Weighted Spread Tests #[test] fn test_volume_weighted_spread() { let mut vw_spread = VolumeWeightedSpread::new(0.1); vw_spread.update(0.01, 10000.0); // Normal spread, normal volume let val1 = vw_spread.compute(); vw_spread.update(0.01, 50000.0); // Same spread, 5x volume let val2 = vw_spread.compute(); assert!(val2 > val1); // Higher volume should increase VW spread } // Tick Count Tests #[test] fn test_tick_count_all_changes() { let mut tick_count = TickCount::new(10); for i in 0..10 { tick_count.update(100.0 + i as f64 * 0.1); } assert_eq!(tick_count.compute(), 9); // 9 price changes } #[test] fn test_tick_count_no_changes() { let mut tick_count = TickCount::new(10); for _ in 0..10 { tick_count.update(100.0); } assert_eq!(tick_count.compute(), 0); // No price changes } // Inter-Arrival Time Tests #[test] fn test_inter_arrival_time() { let mut iat = InterArrivalTime::new(5); // 1 second intervals for i in 0..5 { iat.update(i * 1_000_000_000); } let avg_time = iat.compute(); assert!((avg_time - 1.0).abs() < 1e-6); // Should be 1 second } // Buy/Sell Imbalance Tests #[test] fn test_buy_sell_imbalance_all_buys() { let mut imbalance = BuySellImbalance::new(0.2); for i in 0..10 { imbalance.update(100.0 + i as f64, 1000.0); } assert!(imbalance.compute() > 0.5); // Strong buy pressure } #[test] fn test_buy_sell_imbalance_all_sells() { let mut imbalance = BuySellImbalance::new(0.2); for i in 0..10 { imbalance.update(100.0 - i as f64, 1000.0); } assert!(imbalance.compute() < -0.5); // Strong sell pressure } // Kyle's Lambda Tests #[test] fn test_kyles_lambda_insufficient_data() { let mut lambda = KyleLambda::new(300, 50); // Add only 5 data points for i in 0..5 { lambda.maybe_update(i * 1_000_000_000, 0.001, 1000.0); } assert_eq!(lambda.compute(), 0.0); // Should return 0 for insufficient data } #[test] fn test_kyles_lambda_correlation() { let mut lambda = KyleLambda::new(0, 50); // Update every call // Simulate positive correlation between returns and signed volume for i in 0..50 { let ret = 0.001 * (i as f64 / 50.0); let signed_vol = 1000.0 * (i as f64 / 50.0); lambda.maybe_update(i * 1_000_000_000, ret, signed_vol); } assert!(lambda.compute() > 0.0); // Positive lambda for positive correlation } // Price Impact Tests #[test] fn test_price_impact_buy_lifts_price() { let mut impact = PriceImpact::new(0.1, 2); // Simulate buy (close > prev) and subsequent price increase impact.update(100.5, 99.5, 100.0); impact.update(101.0, 100.0, 100.5); // Buy impact.update(101.5, 100.5, 101.0); // Price lifted impact.update(102.0, 101.0, 101.5); // Continued lift // After delay, should see positive impact assert!(impact.compute() >= 0.0); } // Variance Ratio Tests #[test] fn test_variance_ratio_random_walk() { let mut vr = VarianceRatio::new(20, 5); // Feed random returns (simulating random walk) use std::f64::consts::PI; for i in 0..20 { let ret = (i as f64 * PI).sin() * 0.001; vr.update(ret); } let ratio = vr.compute(); assert!(ratio > 0.5 && ratio < 2.0); // Should be near 1.0 for random walk } #[test] fn test_variance_ratio_insufficient_data() { let vr = VarianceRatio::new(20, 5); assert_eq!(vr.compute(), 1.0); // Should default to 1.0 } // Trait Implementation Tests #[test] fn test_trait_implementations() { let features: Vec> = vec![ Box::new(HighLowSpread::default()), Box::new(VolumeWeightedSpread::default()), Box::new(TickCount::default()), Box::new(InterArrivalTime::default()), Box::new(BuySellImbalance::default()), Box::new(KyleLambda::default()), Box::new(PriceImpact::default()), Box::new(VarianceRatio::default()), ]; for feature in features { assert!(!feature.feature_name().is_empty()); assert!(feature.get_normalized().is_finite()); } } // Normalization Tests #[test] fn test_normalization_bounds() { let mut hl_spread = HighLowSpread::new(0.1); hl_spread.update(102.0, 98.0); let normalized = hl_spread.get_normalized(); assert!(normalized >= -1.0 && normalized <= 1.0); let mut imbalance = BuySellImbalance::new(0.1); imbalance.update(101.0, 1000.0); let normalized = imbalance.get_normalized(); assert!(normalized >= -1.0 && normalized <= 1.0); let vr = VarianceRatio::new(20, 5); let normalized = vr.get_normalized(); assert!(normalized >= -1.0 && normalized <= 1.0); } // Reset Tests #[test] fn test_reset_all_features() { let mut hl_spread = HighLowSpread::new(0.1); hl_spread.update(102.0, 98.0); hl_spread.reset(); assert_eq!(hl_spread.value(), 0.0); let mut tick_count = TickCount::new(10); tick_count.update(100.0); tick_count.update(101.0); tick_count.reset(); assert_eq!(tick_count.value(), 0.0); } }