//! Comprehensive Data Normalization Tests //! //! Tests for market data transformation, normalization, and standardization. use chrono::Utc; use common::{MarketDataEvent, QuoteEvent, Symbol, TradeEvent}; use rust_decimal::Decimal; use rust_decimal_macros::dec; #[tokio::test] async fn test_trade_event_normalization() { let trade = TradeEvent { symbol: "AAPL".to_string(), price: dec!(150.25), size: dec!(100), timestamp: Utc::now(), trade_id: Some("TRADE-001".to_string()), exchange: Some("NYSE".to_string()), conditions: vec!["REGULAR".to_string()], sequence: 1, }; assert_eq!(trade.symbol, "AAPL".to_string()); assert_eq!(trade.price, dec!(150.25)); assert_eq!(trade.size, dec!(100)); assert!(trade.trade_id.is_some()); } #[tokio::test] async fn test_quote_event_normalization() { let quote = QuoteEvent { symbol: "MSFT".to_string(), bid: Some(dec!(380.50)), ask: Some(dec!(380.55)), bid_size: Some(dec!(200)), ask_size: Some(dec!(150)), timestamp: Utc::now(), exchange: Some("NASDAQ".to_string()), bid_exchange: Some("NASDAQ".to_string()), ask_exchange: Some("NASDAQ".to_string()), conditions: vec![], sequence: 2, }; assert_eq!(quote.symbol, "MSFT".to_string()); assert!(quote.bid.is_some()); assert!(quote.ask.is_some()); assert!(quote.bid.unwrap() < quote.ask.unwrap()); } #[tokio::test] async fn test_price_normalization_to_decimal() { let prices = vec![ (100.0, dec!(100)), (150.25, dec!(150.25)), (0.01, dec!(0.01)), (999999.99, dec!(999999.99)), ]; for (float_price, expected_decimal) in prices { let decimal_price = Decimal::try_from(float_price).unwrap(); assert_eq!(decimal_price, expected_decimal); } } #[tokio::test] async fn test_volume_normalization() { let volumes = vec![(1, dec!(1)), (100, dec!(100)), (1000000, dec!(1000000))]; for (int_volume, expected_decimal) in volumes { let decimal_volume = Decimal::from(int_volume); assert_eq!(decimal_volume, expected_decimal); } } #[tokio::test] async fn test_symbol_normalization() { let symbol_strings = vec![ ("AAPL", "AAPL"), ("aapl", "AAPL"), ("SPY", "SPY"), ("QQQ", "QQQ"), ]; for (input, expected) in symbol_strings { let symbol = Symbol::from(input); assert_eq!(symbol.to_string().to_uppercase(), expected); } } #[tokio::test] async fn test_exchange_code_normalization() { let exchanges = vec!["NYSE", "NASDAQ", "AMEX", "ARCA", "BATS"]; for exchange in exchanges { let trade = TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(100), timestamp: Utc::now(), trade_id: None, exchange: Some(exchange.to_string()), conditions: vec![], sequence: 1, }; assert_eq!(trade.exchange.unwrap(), exchange); } } #[tokio::test] async fn test_trade_conditions_normalization() { let conditions = vec![ vec!["REGULAR"], vec!["OPENING", "REGULAR"], vec!["CLOSING"], vec!["ODD_LOT"], vec!["INTERMARKET_SWEEP"], ]; for conds in conditions { let trade = TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(100), timestamp: Utc::now(), trade_id: None, exchange: None, conditions: conds.iter().map(|s| s.to_string()).collect(), sequence: 1, }; assert_eq!(trade.conditions.len(), conds.len()); } } #[tokio::test] async fn test_timestamp_normalization() { let now = Utc::now(); let trade = TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(100), timestamp: now, trade_id: None, exchange: None, conditions: vec![], sequence: 1, }; assert_eq!(trade.timestamp, now); } #[tokio::test] async fn test_bid_ask_spread_calculation() { let quote = QuoteEvent { symbol: "TEST".to_string(), bid: Some(dec!(100.00)), ask: Some(dec!(100.05)), bid_size: Some(dec!(100)), ask_size: Some(dec!(100)), timestamp: Utc::now(), exchange: None, bid_exchange: None, ask_exchange: None, conditions: vec![], sequence: 1, }; let spread = quote.ask.unwrap() - quote.bid.unwrap(); assert_eq!(spread, dec!(0.05)); } #[tokio::test] async fn test_midpoint_price_calculation() { let quote = QuoteEvent { symbol: "TEST".to_string(), bid: Some(dec!(100.00)), ask: Some(dec!(100.10)), bid_size: Some(dec!(100)), ask_size: Some(dec!(100)), timestamp: Utc::now(), exchange: None, bid_exchange: None, ask_exchange: None, conditions: vec![], sequence: 1, }; let midpoint = (quote.bid.unwrap() + quote.ask.unwrap()) / dec!(2); assert_eq!(midpoint, dec!(100.05)); } #[tokio::test] async fn test_zero_price_handling() { let trade = TradeEvent { symbol: "TEST".to_string(), price: dec!(0), size: dec!(100), timestamp: Utc::now(), trade_id: None, exchange: None, conditions: vec![], sequence: 1, }; assert_eq!(trade.price, dec!(0)); assert!(trade.price >= dec!(0)); } #[tokio::test] async fn test_negative_price_representation() { // While negative prices are invalid, test that Decimal can represent them let price = dec!(-10.5); assert!(price < dec!(0)); assert_eq!(price.abs(), dec!(10.5)); } #[tokio::test] async fn test_very_small_prices() { let small_prices = vec![dec!(0.0001), dec!(0.00001), dec!(0.000001)]; for price in small_prices { assert!(price > dec!(0)); assert!(price < dec!(1)); } } #[tokio::test] async fn test_very_large_prices() { let large_prices = vec![dec!(100000), dec!(1000000), dec!(10000000)]; for price in large_prices { assert!(price > dec!(10000)); assert!(price >= dec!(100000)); } } #[tokio::test] async fn test_decimal_precision() { let price = dec!(123.456789); // Decimal maintains precision assert!(price > dec!(123.456)); assert!(price < dec!(123.457)); } #[tokio::test] async fn test_size_normalization_zero() { let trade = TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(0), timestamp: Utc::now(), trade_id: None, exchange: None, conditions: vec![], sequence: 1, }; assert_eq!(trade.size, dec!(0)); } #[tokio::test] async fn test_fractional_shares() { let trade = TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(0.5), timestamp: Utc::now(), trade_id: None, exchange: None, conditions: vec![], sequence: 1, }; assert_eq!(trade.size, dec!(0.5)); assert!(trade.size > dec!(0)); assert!(trade.size < dec!(1)); } #[tokio::test] async fn test_market_data_event_variants() { let trade = MarketDataEvent::Trade(TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(100), timestamp: Utc::now(), trade_id: None, exchange: None, conditions: vec![], sequence: 1, }); let quote = MarketDataEvent::Quote(QuoteEvent { symbol: "TEST".to_string(), bid: Some(dec!(100)), ask: Some(dec!(101)), bid_size: Some(dec!(100)), ask_size: Some(dec!(100)), timestamp: Utc::now(), exchange: None, bid_exchange: None, ask_exchange: None, conditions: vec![], sequence: 1, }); assert!(matches!(trade, MarketDataEvent::Trade(_))); assert!(matches!(quote, MarketDataEvent::Quote(_))); } #[tokio::test] async fn test_sequence_number_normalization() { let events = vec![ TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(100), timestamp: Utc::now(), trade_id: None, exchange: None, conditions: vec![], sequence: 1, }, TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(100), timestamp: Utc::now(), trade_id: None, exchange: None, conditions: vec![], sequence: 2, }, ]; assert_eq!(events[0].sequence, 1); assert_eq!(events[1].sequence, 2); assert!(events[1].sequence > events[0].sequence); } #[tokio::test] async fn test_trade_id_normalization() { let trade_ids = vec![ Some("TRADE-001".to_string()), Some("12345".to_string()), Some("ABC-XYZ-789".to_string()), None, ]; for trade_id in trade_ids { let trade = TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(100), timestamp: Utc::now(), trade_id: trade_id.clone(), exchange: None, conditions: vec![], sequence: 1, }; assert_eq!(trade.trade_id, trade_id); } } #[tokio::test] async fn test_multiple_exchange_quotes() { let quote = QuoteEvent { symbol: "TEST".to_string(), bid: Some(dec!(100.00)), ask: Some(dec!(100.05)), bid_size: Some(dec!(200)), ask_size: Some(dec!(150)), timestamp: Utc::now(), exchange: Some("CONSOLIDATED".to_string()), bid_exchange: Some("NYSE".to_string()), ask_exchange: Some("NASDAQ".to_string()), conditions: vec![], sequence: 1, }; assert_eq!(quote.exchange.unwrap(), "CONSOLIDATED"); assert_eq!(quote.bid_exchange.unwrap(), "NYSE"); assert_eq!(quote.ask_exchange.unwrap(), "NASDAQ"); } #[tokio::test] async fn test_quote_without_sizes() { let quote = QuoteEvent { symbol: "TEST".to_string(), bid: Some(dec!(100)), ask: Some(dec!(101)), bid_size: None, ask_size: None, timestamp: Utc::now(), exchange: None, bid_exchange: None, ask_exchange: None, conditions: vec![], sequence: 1, }; assert!(quote.bid_size.is_none()); assert!(quote.ask_size.is_none()); } #[tokio::test] async fn test_quote_with_missing_prices() { let quote = QuoteEvent { symbol: "TEST".to_string(), bid: None, ask: None, bid_size: Some(dec!(100)), ask_size: Some(dec!(100)), timestamp: Utc::now(), exchange: None, bid_exchange: None, ask_exchange: None, conditions: vec![], sequence: 1, }; assert!(quote.bid.is_none()); assert!(quote.ask.is_none()); } #[tokio::test] async fn test_price_arithmetic_precision() { let price1 = dec!(100.123); let price2 = dec!(50.456); let sum = price1 + price2; let diff = price1 - price2; let product = price1 * price2; assert_eq!(sum, dec!(150.579)); assert_eq!(diff, dec!(49.667)); assert!(product > dec!(5000)); } #[tokio::test] async fn test_volume_weighted_average_price() { let trades = vec![ (dec!(100), dec!(100)), // price, size (dec!(101), dec!(200)), (dec!(99), dec!(150)), ]; let total_value: Decimal = trades.iter().map(|(price, size)| price * size).sum(); let total_volume: Decimal = trades.iter().map(|(_, size)| size).sum(); let vwap = total_value / total_volume; assert!(vwap > dec!(99)); assert!(vwap < dec!(101)); } #[tokio::test] async fn test_percentage_change_calculation() { let old_price = dec!(100); let new_price = dec!(105); let change = new_price - old_price; let pct_change = (change / old_price) * dec!(100); assert_eq!(pct_change, dec!(5)); } #[tokio::test] async fn test_tick_size_normalization() { let tick_sizes = vec![ dec!(0.01), // Penny tick dec!(0.05), // Nickel tick dec!(0.10), // Dime tick dec!(0.25), // Quarter tick ]; for tick_size in tick_sizes { assert!(tick_size > dec!(0)); assert!(tick_size < dec!(1)); } } #[tokio::test] async fn test_round_lot_normalization() { let lot_sizes = vec![ dec!(100), // Standard round lot dec!(10), // Small round lot dec!(1), // Odd lot ]; for lot_size in lot_sizes { assert!(lot_size > dec!(0)); assert_eq!(lot_size % dec!(1), dec!(0)); // Whole number } } #[tokio::test] async fn test_data_type_conversion_safety() { // Test that conversions between types are safe let float_price: f64 = 123.45; let decimal_price = Decimal::try_from(float_price).unwrap(); assert!(decimal_price > dec!(123)); assert!(decimal_price < dec!(124)); } #[tokio::test] async fn test_cross_exchange_price_comparison() { let nyse_quote = QuoteEvent { symbol: "AAPL".to_string(), bid: Some(dec!(150.00)), ask: Some(dec!(150.05)), bid_size: Some(dec!(100)), ask_size: Some(dec!(100)), timestamp: Utc::now(), exchange: Some("NYSE".to_string()), bid_exchange: Some("NYSE".to_string()), ask_exchange: Some("NYSE".to_string()), conditions: vec![], sequence: 1, }; let nasdaq_quote = QuoteEvent { symbol: "AAPL".to_string(), bid: Some(dec!(150.01)), ask: Some(dec!(150.04)), bid_size: Some(dec!(200)), ask_size: Some(dec!(150)), timestamp: Utc::now(), exchange: Some("NASDAQ".to_string()), bid_exchange: Some("NASDAQ".to_string()), ask_exchange: Some("NASDAQ".to_string()), conditions: vec![], sequence: 2, }; // Compare NBBO let best_bid = nyse_quote.bid.unwrap().max(nasdaq_quote.bid.unwrap()); let best_ask = nyse_quote.ask.unwrap().min(nasdaq_quote.ask.unwrap()); assert_eq!(best_bid, dec!(150.01)); // NASDAQ has better bid assert_eq!(best_ask, dec!(150.04)); // NASDAQ has better ask } #[tokio::test] async fn test_market_data_event_timestamp_access() { let trade_event = MarketDataEvent::Trade(TradeEvent { symbol: "TEST".to_string(), price: dec!(100), size: dec!(100), timestamp: Utc::now(), trade_id: None, exchange: None, conditions: vec![], sequence: 1, }); let quote_event = MarketDataEvent::Quote(QuoteEvent { symbol: "TEST".to_string(), bid: Some(dec!(100)), ask: Some(dec!(101)), bid_size: Some(dec!(100)), ask_size: Some(dec!(100)), timestamp: Utc::now(), exchange: None, bid_exchange: None, ask_exchange: None, conditions: vec![], sequence: 1, }); // Both should have accessible timestamps assert!(trade_event.timestamp() <= Some(Utc::now())); assert!(quote_event.timestamp() <= Some(Utc::now())); } #[tokio::test] async fn test_market_data_event_symbol_access() { let event = MarketDataEvent::Trade(TradeEvent { symbol: "AAPL".to_string(), price: dec!(150), size: dec!(100), timestamp: Utc::now(), trade_id: None, exchange: None, conditions: vec![], sequence: 1, }); assert_eq!(event.symbol(), Symbol::from("AAPL")); }