//! # Roll Spread Estimator //! //! Implementation of Roll (1984) spread estimator that infers bid-ask spread //! from serial covariance in price changes. //! //! ## Algorithm //! //! Roll Spread = 2 × √(-Cov(ΔP_t, ΔP_{t-1})) //! //! - Uses negative autocovariance in price changes //! - Assumes price changes alternate due to bid-ask bounce //! - Provides spread estimate when quotes are not available //! //! ## Performance //! //! - Target latency: <25μs per calculation //! - Efficient rolling covariance calculation //! - Handles missing or invalid data gracefully use std::collections::VecDeque; use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; use serde::{Deserialize, Serialize}; use common::types::Price; use super::*; use super::{ // use crate::safe_operations; // DISABLED - module not found #[test] fn test_roll_spread_estimator_creation() { let estimator = RollSpreadEstimator::default(); assert_eq!(estimator.get_spread(), 0.0); assert_eq!(estimator.get_price_change_count(), 0); assert!(!estimator.is_valid_estimate()); } #[test] fn test_price_change_tracking() { let mut estimator = RollSpreadEstimator::default(); // Add series of price changes that alternate (bid-ask bounce) let base_price = 100000; // $10.00 for i in 0..50 { let price = if i % 2 == 0 { base_price + 50 // Ask side } else { base_price - 50 // Bid side }; let update = MarketDataUpdate { timestamp: (i * 1000000) as u64, symbol: "AAPL".to_string(), price, volume: 1000, bid: base_price - 50, ask: base_price + 50, bid_size: 100, ask_size: 100, direction: None, }; estimator.update(&update)?; } let result = estimator.get_result(); assert!(result.price_change_count > 0); // Should detect negative autocovariance from alternating prices if result.is_valid_estimate { assert!(result.autocovariance < 0.0); assert!(result.spread > 0.0); println!("Roll spread: {:.4}, Autocovariance: {:.4}", result.spread, result.autocovariance); } } #[test] fn test_high_frequency_estimator() { let estimator = RollSpreadEstimator::high_frequency(200); let config = estimator.get_config(); assert_eq!(config.window_size, 200); assert_eq!(config.update_frequency, 1); // Every trade assert_eq!(config.min_price_change, 0); // No minimum } #[test] fn test_spread_calculation() { let config = RollSpreadConfig { window_size: 20, min_price_changes: 10, update_frequency: 1, ..Default::default() }; let mut estimator = RollSpreadEstimator::new(config); // Create alternating price pattern (classic bid-ask bounce) let prices = [100050, 99950, 100050, 99950, 100050, 99950, 100050, 99950, 100050, 99950, 100050, 99950, 100050, 99950, 100050, 99950, 100050, 99950]; for (i, &price) in prices.into_iter().enumerate() { let update = MarketDataUpdate { timestamp: (i * 1000000) as u64, symbol: "AAPL".to_string(), price, volume: 1000, bid: 99950, ask: 100050, bid_size: 100, ask_size: 100, direction: None, }; estimator.update(&update)?; } let result = estimator.get_result(); // Perfect bid-ask bounce should give negative autocovariance if result.is_valid_estimate { assert!(result.autocovariance < 0.0); assert!(result.spread > 0.0); assert!(result.spread_bps > 0.0); // The spread should be close to the actual bid-ask spread (100 basis points) println!("Detected spread: {:.4} ({:.2} bps), Expected: 0.01 (100 bps)", result.spread, result.spread_bps); } } #[test] fn test_data_quality_score() { let mut estimator = RollSpreadEstimator::default(); // Initially no data assert_eq!(estimator.get_data_quality_score(), 0.0); // Add some price changes for i in 0..10 { let update = MarketDataUpdate { timestamp: (i * 1000000) as u64, symbol: "AAPL".to_string(), price: 100000 + (i % 2) * 100, // Alternating volume: 1000, bid: 99950, ask: 100050, bid_size: 100, ask_size: 100, direction: None, }; estimator.update(&update)?; } let quality_score = estimator.get_data_quality_score(); assert!(quality_score > 0.0); assert!(quality_score <= 1.0); println!("Data quality score: {:.4}", quality_score); } #[test] fn test_midpoint_vs_transaction_prices() { // Test with transaction prices let mut est_transaction = RollSpreadEstimator::new(RollSpreadConfig { use_transaction_prices: true, window_size: 20, min_price_changes: 10, update_frequency: 1, ..Default::default() }); // Test with midpoint prices let mut est_midpoint = RollSpreadEstimator::new(RollSpreadConfig { use_transaction_prices: false, window_size: 20, min_price_changes: 10, update_frequency: 1, ..Default::default() }); // Add same data to both for i in 0..20 { let update = MarketDataUpdate { timestamp: (i * 1000000) as u64, symbol: "AAPL".to_string(), price: if i % 2 == 0 { 100050 } else { 99950 }, // Transaction price alternates volume: 1000, bid: 99950, ask: 100050, // Midpoint = 100000 (constant) bid_size: 100, ask_size: 100, direction: None, }; est_transaction.update(&update)?; est_midpoint.update(&update)?; } let result_trans = est_transaction.get_result(); let result_mid = est_midpoint.get_result(); // Transaction prices should show bid-ask bounce, midpoint should not println!("Transaction spread: {:.4}, Midpoint spread: {:.4}", result_trans.spread, result_mid.spread); if result_trans.is_valid_estimate { assert!(result_trans.spread > 0.0); } // Midpoint prices are constant, so no spread detected assert_eq!(result_mid.spread, 0.0); } }