Files
foxhunt/crates/data/tests/data_normalization.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

597 lines
15 KiB
Rust

//! 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"));
}