- Fixed systematic array indexing corruption: [0_i32] → [0] - Fixed numeric literal suffixes across 835 files - Fixed iterator patterns on RwLockReadGuard (.iter() required) - Fixed float type annotations (365.25_f64 for sqrt) - Fixed missing semicolons in position manager - Fixed reference dereferencing in data loader Root cause: Mass refactoring incorrectly added _i32 suffixes to array indices Impact: Complete compilation failure (463 errors) Resolution: Automated regex + targeted fixes Result: 100% compilation success (0 errors) Validated: cargo check --workspace passes Ready for: Production deployment
769 lines
26 KiB
Rust
769 lines
26 KiB
Rust
//! HFT Specific Test Utilities
|
|
//!
|
|
//! Specialized testing utilities for high frequency trading components
|
|
//! with focus on performance, latency, and financial accuracy.
|
|
|
|
use super::test_safety::{TestError, TestResult};
|
|
use chrono::{DateTime, Utc};
|
|
use ::rust_decimal::Decimal;
|
|
use std::collections::VecDeque;
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// Performance measurement utilities for HFT testing
|
|
pub mod performance {
|
|
use super::*;
|
|
|
|
/// Latency measurement with statistical analysis
|
|
///
|
|
/// Collects latency measurements and provides statistical analysis
|
|
/// including average, percentiles, min, and max values.
|
|
#[derive(Debug)]
|
|
pub struct LatencyMeasurement {
|
|
/// Collection of latency measurements
|
|
measurements: VecDeque<Duration>,
|
|
/// Maximum number of samples to retain
|
|
max_samples: usize,
|
|
}
|
|
|
|
impl LatencyMeasurement {
|
|
/// Create a new latency measurement collector
|
|
///
|
|
/// # Arguments
|
|
/// * `max_samples` - Maximum number of samples to retain in memory
|
|
pub fn new(max_samples: usize) -> Self {
|
|
Self {
|
|
measurements: VecDeque::with_capacity(max_samples),
|
|
max_samples,
|
|
}
|
|
}
|
|
|
|
/// Record a new latency measurement
|
|
///
|
|
/// If the maximum number of samples is reached, the oldest sample is removed.
|
|
///
|
|
/// # Arguments
|
|
/// * `latency` - The latency duration to record
|
|
pub fn record(&mut self, latency: Duration) {
|
|
if self.measurements.len() >= self.max_samples {
|
|
self.measurements.pop_front();
|
|
}
|
|
self.measurements.push_back(latency);
|
|
}
|
|
|
|
/// Calculate the average latency across all recorded measurements
|
|
///
|
|
/// # Returns
|
|
/// * `Some(Duration)` - The average latency if measurements exist
|
|
///
|
|
/// * `None` - If no measurements have been recorded
|
|
pub fn average(&self) -> Option<Duration> {
|
|
if self.measurements.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let total_nanos: u64 = self.measurements.iter().map(|d| d.as_nanos() as u64).sum();
|
|
|
|
Some(Duration::from_nanos(
|
|
total_nanos / self.measurements.len() as u64,
|
|
))
|
|
}
|
|
|
|
/// Calculate the specified percentile latency
|
|
///
|
|
/// # Arguments
|
|
/// * `p` - Percentile value between 0.0 and 1.0 (e.g., 0.99 for 99th percentile)
|
|
///
|
|
/// # Returns
|
|
/// * `Some(Duration)` - The percentile latency if measurements exist
|
|
///
|
|
/// * `None` - If no measurements have been recorded
|
|
pub fn percentile(&self, p: f64) -> Option<Duration> {
|
|
if self.measurements.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let mut sorted: Vec<Duration> = self.measurements.iter().copied().collect();
|
|
sorted.sort();
|
|
|
|
let index = ((sorted.len() as f64 - 1.0) * p).round() as usize;
|
|
sorted.get(index).copied()
|
|
}
|
|
|
|
/// Get the maximum latency from all recorded measurements
|
|
///
|
|
/// # Returns
|
|
/// * `Some(Duration)` - The maximum latency if measurements exist
|
|
///
|
|
/// * `None` - If no measurements have been recorded
|
|
pub fn max(&self) -> Option<Duration> {
|
|
self.measurements.iter().max().copied()
|
|
}
|
|
|
|
/// Get the minimum latency from all recorded measurements
|
|
///
|
|
/// # Returns
|
|
/// * `Some(Duration)` - The minimum latency if measurements exist
|
|
///
|
|
/// * `None` - If no measurements have been recorded
|
|
pub fn min(&self) -> Option<Duration> {
|
|
self.measurements.iter().min().copied()
|
|
}
|
|
}
|
|
|
|
/// Measure operation latency with safety checks
|
|
///
|
|
/// Executes an operation and measures its execution time with proper error handling.
|
|
///
|
|
/// # Arguments
|
|
/// * `operation` - The async operation to measure
|
|
///
|
|
/// * `context` - Description of the operation for error reporting
|
|
///
|
|
/// # Returns
|
|
/// * `Ok((T, Duration))` - The operation result and its execution time
|
|
///
|
|
/// * `Err(TestError)` - If the operation fails
|
|
pub async fn measure_latency<F, T, Fut>(
|
|
operation: F,
|
|
context: &str,
|
|
) -> TestResult<(T, Duration)>
|
|
where
|
|
F: FnOnce() -> Fut,
|
|
Fut: std::future::Future<Output = TestResult<T>>,
|
|
{
|
|
let start = Instant::now();
|
|
let result = operation().await?;
|
|
let latency = start.elapsed();
|
|
|
|
println!("Operation '{}' completed in {:?}", context, latency);
|
|
Ok((result, latency))
|
|
}
|
|
|
|
/// Validate HFT latency requirements (sub-microsecond)
|
|
///
|
|
/// Checks if a measured latency meets HFT performance requirements.
|
|
///
|
|
/// # Arguments
|
|
/// * `latency` - The measured latency to validate
|
|
///
|
|
/// * `max_allowed` - Maximum allowed latency for HFT operations
|
|
/// * `context` - Description of the operation for error reporting
|
|
///
|
|
/// # Returns
|
|
/// * `Ok(())` - If latency meets requirements
|
|
///
|
|
/// * `Err(TestError)` - If latency exceeds maximum allowed
|
|
pub fn validate_hft_latency(
|
|
latency: Duration,
|
|
max_allowed: Duration,
|
|
context: &str,
|
|
) -> TestResult<()> {
|
|
if latency > max_allowed {
|
|
return Err(TestError::assertion(format!(
|
|
"{}: Latency {:?} exceeds HFT requirement of {:?}",
|
|
context, latency, max_allowed
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Benchmark operation with warmup and multiple iterations
|
|
///
|
|
/// Performs a comprehensive benchmark with warmup phase and statistical measurement.
|
|
///
|
|
/// # Arguments
|
|
/// * `operation` - The operation to benchmark (must be cloneable for multiple runs)
|
|
///
|
|
/// * `warmup_iterations` - Number of warmup iterations to run before measurement
|
|
/// * `measurement_iterations` - Number of iterations to measure for statistics
|
|
///
|
|
/// * `context` - Description of the operation for error reporting
|
|
///
|
|
/// # Returns
|
|
/// * `Ok(LatencyMeasurement)` - Statistical analysis of the benchmark results
|
|
///
|
|
/// * `Err(TestError)` - If any iteration fails
|
|
pub async fn benchmark_operation<F, T, Fut>(
|
|
operation: F,
|
|
warmup_iterations: usize,
|
|
measurement_iterations: usize,
|
|
context: &str,
|
|
) -> TestResult<LatencyMeasurement>
|
|
where
|
|
F: Fn() -> Fut + Clone,
|
|
Fut: std::future::Future<Output = TestResult<T>>,
|
|
{
|
|
// Warmup phase
|
|
for _ in 0..warmup_iterations {
|
|
operation().await.map_err(|e| {
|
|
TestError::setup(format!("Benchmark warmup failed for {}: {}", context, e))
|
|
})?;
|
|
}
|
|
|
|
// Measurement phase
|
|
let mut measurements = LatencyMeasurement::new(measurement_iterations);
|
|
for _ in 0..measurement_iterations {
|
|
let (_, latency) = measure_latency(operation.clone(), context).await?;
|
|
measurements.record(latency);
|
|
}
|
|
|
|
Ok(measurements)
|
|
}
|
|
}
|
|
|
|
/// Financial accuracy testing utilities
|
|
pub mod financial {
|
|
use super::*;
|
|
|
|
/// Precision threshold for financial calculations
|
|
///
|
|
/// This constant defines the maximum acceptable difference between
|
|
/// two financial amounts for them to be considered equal.
|
|
pub const FINANCIAL_PRECISION: f64 = 1e-8;
|
|
|
|
/// Safe comparison of financial amounts with precision handling
|
|
///
|
|
/// Compares two Decimal values for equality within the financial precision threshold.
|
|
///
|
|
/// This is essential for testing financial calculations where floating-point precision
|
|
/// errors can cause exact equality comparisons to fail.
|
|
///
|
|
/// # Arguments
|
|
/// * `left` - First decimal value to compare
|
|
///
|
|
/// * `right` - Second decimal value to compare
|
|
/// * `context` - Description of the comparison for error reporting
|
|
///
|
|
/// # Returns
|
|
/// * `Ok(())` - If values are equal within precision
|
|
///
|
|
/// * `Err(TestError)` - If values differ beyond acceptable precision
|
|
pub fn assert_decimal_eq(left: Decimal, right: Decimal, context: &str) -> TestResult<()> {
|
|
let diff = (left - right).abs();
|
|
let precision_decimal = Decimal::try_from(FINANCIAL_PRECISION)
|
|
.map_err(|_| TestError::setup("Failed to create precision decimal"))?;
|
|
|
|
if diff > precision_decimal {
|
|
return Err(TestError::assertion(format!(
|
|
"{}: Financial amounts not equal within precision\n left: {}\n right: {}\n diff: {}\n max_allowed: {}",
|
|
context, left, right, diff, precision_decimal
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Generate test prices with realistic market behavior
|
|
///
|
|
/// Creates a sequence of realistic price movements for testing market data processing.
|
|
///
|
|
/// Uses random walk with specified volatility to simulate real market conditions.
|
|
///
|
|
/// # Arguments
|
|
/// * `base_price` - Starting price for the sequence
|
|
///
|
|
/// * `count` - Number of price points to generate
|
|
/// * `volatility` - Volatility as a percentage (e.g., 0.02 for 2% volatility)
|
|
///
|
|
/// # Returns
|
|
/// * `Ok(Vec<Decimal>)` - Sequence of generated prices
|
|
///
|
|
/// * `Err(TestError)` - If price generation fails
|
|
pub fn generate_realistic_prices(
|
|
base_price: Decimal,
|
|
count: usize,
|
|
volatility: f64,
|
|
) -> TestResult<Vec<Decimal>> {
|
|
use rand::Rng;
|
|
let mut rng = rand::thread_rng();
|
|
let mut prices = Vec::with_capacity(count);
|
|
let mut current_price = base_price;
|
|
|
|
for _ in 0..count {
|
|
let change_pct = rng.gen_range(-volatility..volatility);
|
|
let change_decimal = Decimal::try_from(change_pct / 100.0)
|
|
.map_err(|_| TestError::setup("Failed to create change decimal"))?;
|
|
let change = current_price * change_decimal;
|
|
current_price =
|
|
(current_price + change).max(Decimal::try_from(0.01).unwrap_or_default());
|
|
prices.push(current_price);
|
|
}
|
|
|
|
Ok(prices)
|
|
}
|
|
}
|
|
|
|
/// Market data testing utilities
|
|
pub mod market_data {
|
|
use super::*;
|
|
|
|
/// Mock market data tick for testing
|
|
///
|
|
/// Represents a single market data tick with price, volume, and optional bid/ask spread.
|
|
///
|
|
/// Used for testing market data processing and trading algorithms.
|
|
#[derive(Debug, Clone)]
|
|
pub struct TestTick {
|
|
/// Trading symbol (e.g., "BTCUSD", "AAPL")
|
|
pub symbol: String,
|
|
/// UTC timestamp of the tick
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Last traded price
|
|
pub price: Decimal,
|
|
/// Volume traded at this price
|
|
pub volume: Decimal,
|
|
/// Best bid price (optional)
|
|
pub bid: Option<Decimal>,
|
|
/// Best ask price (optional)
|
|
pub ask: Option<Decimal>,
|
|
}
|
|
|
|
impl TestTick {
|
|
/// Create a new test tick with basic price and volume
|
|
///
|
|
/// # Arguments
|
|
/// * `symbol` - Trading symbol for this tick
|
|
///
|
|
/// * `price` - Last traded price
|
|
/// * `volume` - Volume traded
|
|
pub fn new(symbol: &str, price: Decimal, volume: Decimal) -> Self {
|
|
Self {
|
|
symbol: symbol.to_string(),
|
|
timestamp: Utc::now(),
|
|
price,
|
|
volume,
|
|
bid: None,
|
|
ask: None,
|
|
}
|
|
}
|
|
|
|
/// Add bid/ask spread to the tick
|
|
///
|
|
/// # Arguments
|
|
/// * `bid` - Best bid price
|
|
///
|
|
/// * `ask` - Best ask price
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Self with bid/ask prices set
|
|
pub fn with_spread(mut self, bid: Decimal, ask: Decimal) -> Self {
|
|
self.bid = Some(bid);
|
|
self.ask = Some(ask);
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Generate realistic market data stream for testing
|
|
///
|
|
/// Provides a configurable market data generator that produces realistic
|
|
/// price movements and tick patterns for testing trading algorithms.
|
|
#[derive(Debug)]
|
|
pub struct TestMarketDataStream {
|
|
/// List of symbols to generate data for
|
|
symbols: Vec<String>,
|
|
/// Base prices for each symbol
|
|
base_prices: std::collections::HashMap<String, Decimal>,
|
|
/// Tick generation rate in Hz
|
|
tick_rate_hz: u64,
|
|
}
|
|
|
|
impl TestMarketDataStream {
|
|
/// Create a new market data stream generator
|
|
///
|
|
/// # Arguments
|
|
/// * `symbols` - List of trading symbols to generate data for
|
|
///
|
|
/// * `tick_rate_hz` - Rate of tick generation in Hz
|
|
pub fn new(symbols: Vec<String>, tick_rate_hz: u64) -> Self {
|
|
let mut base_prices = std::collections::HashMap::new();
|
|
for symbol in &symbols {
|
|
// Set realistic base prices for different asset types
|
|
let base_price = match symbol.as_str() {
|
|
s if s.contains("USD") => Decimal::try_from(1.2).unwrap_or_default(),
|
|
s if s.starts_with("BTC") => Decimal::try_from(45000.0).unwrap_or_default(),
|
|
_ => Decimal::try_from(100.0).unwrap_or_default(), // Default stock price
|
|
};
|
|
base_prices.insert(symbol.clone(), base_price);
|
|
}
|
|
|
|
Self {
|
|
symbols,
|
|
base_prices,
|
|
tick_rate_hz,
|
|
}
|
|
}
|
|
|
|
/// Generate market data ticks for the specified duration
|
|
///
|
|
/// Creates realistic market data with small price variations and consistent timing.
|
|
///
|
|
/// # Arguments
|
|
/// * `duration` - How long to generate data for
|
|
///
|
|
/// # Returns
|
|
/// * `Ok(Vec<TestTick>)` - Generated market data ticks
|
|
///
|
|
/// * `Err(TestError)` - If tick generation fails
|
|
pub async fn generate_ticks(&self, duration: Duration) -> TestResult<Vec<TestTick>> {
|
|
let total_ticks = (duration.as_secs_f64() * self.tick_rate_hz as f64) as usize;
|
|
let mut ticks = Vec::with_capacity(total_ticks);
|
|
let tick_interval = Duration::from_nanos(1_000_000_000 / self.tick_rate_hz);
|
|
|
|
for i in 0..total_ticks {
|
|
for symbol in &self.symbols {
|
|
let base_price = self.base_prices.get(symbol).ok_or_else(|| {
|
|
TestError::setup(format!("No base price for symbol {}", symbol))
|
|
})?;
|
|
|
|
// Add small random variation
|
|
let variation = (i as f64 * 0.001).sin() * 0.001; // Small deterministic variation
|
|
let variation_decimal = Decimal::try_from(variation).unwrap_or_default();
|
|
let price = *base_price * (Decimal::ONE + variation_decimal);
|
|
|
|
let volume = Decimal::from((100 + i) % 1000);
|
|
let tick = TestTick::new(symbol, price, volume);
|
|
ticks.push(tick);
|
|
}
|
|
|
|
// Simulate real-time tick generation
|
|
if i % 100 == 0 {
|
|
tokio::time::sleep(tick_interval).await;
|
|
}
|
|
}
|
|
|
|
Ok(ticks)
|
|
}
|
|
}
|
|
|
|
/// Validate market data consistency
|
|
///
|
|
/// Performs comprehensive validation of a tick sequence including:
|
|
/// - Timestamp ordering
|
|
///
|
|
/// - Realistic price movements (no more than 50% jumps)
|
|
/// - Data integrity checks
|
|
///
|
|
/// # Arguments
|
|
/// * `ticks` - Sequence of ticks to validate
|
|
///
|
|
/// * `context` - Description for error reporting
|
|
///
|
|
/// # Returns
|
|
/// * `Ok(())` - If all validation checks pass
|
|
///
|
|
/// * `Err(TestError)` - If any validation fails
|
|
pub fn validate_tick_sequence(ticks: &[TestTick], context: &str) -> TestResult<()> {
|
|
if ticks.is_empty() {
|
|
return Err(TestError::assertion(format!(
|
|
"{}: Empty tick sequence",
|
|
context
|
|
)));
|
|
}
|
|
|
|
// Check timestamp ordering
|
|
for window in ticks.windows(2) {
|
|
if window[1].timestamp < window[0].timestamp {
|
|
return Err(TestError::assertion(format!(
|
|
"{}: Timestamps out of order: {} -> {}",
|
|
context, window[0].timestamp, window[1].timestamp
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Check for unrealistic price movements (>50% in single tick)
|
|
for window in ticks.windows(2) {
|
|
if window[0].symbol == window[1].symbol {
|
|
let price_change = ((window[1].price - window[0].price) / window[0].price).abs();
|
|
let fifty_percent = Decimal::try_from(0.5).unwrap_or_default();
|
|
if price_change > fifty_percent {
|
|
return Err(TestError::assertion(format!(
|
|
"{}: Unrealistic price movement in {}: {} -> {} ({:.2}%)",
|
|
context,
|
|
window[0].symbol,
|
|
window[0].price,
|
|
window[1].price,
|
|
price_change * Decimal::from(100)
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Order management testing utilities
|
|
pub mod orders {
|
|
use super::*;
|
|
|
|
// Import order types from common crate
|
|
pub use common::trading::{OrderSide, OrderType};
|
|
pub use common::types::OrderStatus;
|
|
|
|
/// Mock order for testing
|
|
///
|
|
/// Represents a trading order with full lifecycle tracking including
|
|
/// partial fills, status transitions, and average fill price calculation.
|
|
#[derive(Debug, Clone)]
|
|
pub struct TestOrder {
|
|
/// Unique order identifier
|
|
pub id: String,
|
|
/// Trading symbol (e.g., "AAPL", "BTCUSD")
|
|
pub symbol: String,
|
|
/// Order side (Buy or Sell)
|
|
pub side: OrderSide,
|
|
/// Total order quantity
|
|
pub quantity: Decimal,
|
|
/// Limit price (None for market orders)
|
|
pub price: Option<Decimal>,
|
|
/// Order type (Market, Limit, etc.)
|
|
pub order_type: OrderType,
|
|
/// Current order status
|
|
pub status: OrderStatus,
|
|
/// Timestamp when order was created
|
|
pub created_at: DateTime<Utc>,
|
|
/// Quantity that has been filled
|
|
pub filled_quantity: Decimal,
|
|
/// Volume-weighted average fill price
|
|
pub avg_fill_price: Option<Decimal>,
|
|
}
|
|
|
|
impl TestOrder {
|
|
/// Create a new market order
|
|
///
|
|
/// # Arguments
|
|
/// * `symbol` - Trading symbol for the order
|
|
///
|
|
/// * `side` - Order side (Buy or Sell)
|
|
/// * `quantity` - Order quantity
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// A new market order with New status
|
|
pub fn new_market_order(symbol: &str, side: OrderSide, quantity: Decimal) -> Self {
|
|
Self {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
symbol: symbol.to_string(),
|
|
side,
|
|
quantity,
|
|
price: None,
|
|
order_type: OrderType::Market,
|
|
status: OrderStatus::New,
|
|
created_at: Utc::now(),
|
|
filled_quantity: Decimal::ZERO,
|
|
avg_fill_price: None,
|
|
}
|
|
}
|
|
|
|
/// Create a new limit order
|
|
///
|
|
/// # Arguments
|
|
/// * `symbol` - Trading symbol for the order
|
|
///
|
|
/// * `side` - Order side (Buy or Sell)
|
|
/// * `quantity` - Order quantity
|
|
///
|
|
/// * `price` - Limit price
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// A new limit order with New status
|
|
pub fn new_limit_order(
|
|
symbol: &str,
|
|
side: OrderSide,
|
|
quantity: Decimal,
|
|
price: Decimal,
|
|
) -> Self {
|
|
Self {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
symbol: symbol.to_string(),
|
|
side,
|
|
quantity,
|
|
price: Some(price),
|
|
order_type: OrderType::Limit,
|
|
status: OrderStatus::New,
|
|
created_at: Utc::now(),
|
|
filled_quantity: Decimal::ZERO,
|
|
avg_fill_price: None,
|
|
}
|
|
}
|
|
|
|
/// Simulate a fill event for this order
|
|
///
|
|
/// Updates the filled quantity, average fill price, and order status.
|
|
///
|
|
/// Validates that fills don't exceed the order quantity.
|
|
///
|
|
/// # Arguments
|
|
/// * `fill_quantity` - Quantity being filled
|
|
///
|
|
/// * `fill_price` - Price at which the fill occurred
|
|
///
|
|
/// # Returns
|
|
/// * `Ok(())` - If the fill is valid and processed
|
|
///
|
|
/// * `Err(TestError)` - If the fill would exceed order quantity or is invalid
|
|
pub fn simulate_fill(
|
|
&mut self,
|
|
fill_quantity: Decimal,
|
|
fill_price: Decimal,
|
|
) -> TestResult<()> {
|
|
if fill_quantity <= Decimal::ZERO {
|
|
return Err(TestError::assertion("Fill quantity must be positive"));
|
|
}
|
|
|
|
if self.filled_quantity + fill_quantity > self.quantity {
|
|
return Err(TestError::assertion(format!(
|
|
"Fill quantity {} would exceed order quantity {}",
|
|
fill_quantity, self.quantity
|
|
)));
|
|
}
|
|
|
|
// Update average fill price
|
|
if self.filled_quantity == Decimal::ZERO {
|
|
self.avg_fill_price = Some(fill_price);
|
|
} else {
|
|
let current_avg = self.avg_fill_price.unwrap_or_default();
|
|
let total_value = current_avg * self.filled_quantity + fill_price * fill_quantity;
|
|
let new_total_quantity = self.filled_quantity + fill_quantity;
|
|
self.avg_fill_price = Some(total_value / new_total_quantity);
|
|
}
|
|
|
|
self.filled_quantity += fill_quantity;
|
|
|
|
// Update status
|
|
if self.filled_quantity == self.quantity {
|
|
self.status = OrderStatus::Filled;
|
|
} else {
|
|
self.status = OrderStatus::PartiallyFilled;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Validate order lifecycle transitions
|
|
///
|
|
/// Performs comprehensive validation of order state consistency:
|
|
/// - Filled quantity doesn't exceed order quantity
|
|
///
|
|
/// - Order status matches fill state
|
|
/// - Average fill prices are reasonable
|
|
///
|
|
/// # Arguments
|
|
/// * `orders` - Collection of orders to validate
|
|
///
|
|
/// * `context` - Description for error reporting
|
|
///
|
|
/// # Returns
|
|
/// * `Ok(())` - If all orders pass validation
|
|
///
|
|
/// * `Err(TestError)` - If any order has inconsistent state
|
|
pub fn validate_order_lifecycle(orders: &[TestOrder], context: &str) -> TestResult<()> {
|
|
for order in orders {
|
|
// Validate filled quantity doesn't exceed order quantity
|
|
if order.filled_quantity > order.quantity {
|
|
return Err(TestError::assertion(format!(
|
|
"{}: Order {} filled quantity {} exceeds order quantity {}",
|
|
context, order.id, order.filled_quantity, order.quantity
|
|
)));
|
|
}
|
|
|
|
// Validate status consistency
|
|
match order.status {
|
|
OrderStatus::Filled if order.filled_quantity != order.quantity => {
|
|
return Err(TestError::assertion(format!(
|
|
"{}: Order {} marked as filled but quantities don't match: {} != {}",
|
|
context, order.id, order.filled_quantity, order.quantity
|
|
)));
|
|
},
|
|
OrderStatus::PartiallyFilled if order.filled_quantity == Decimal::ZERO => {
|
|
return Err(TestError::assertion(format!(
|
|
"{}: Order {} marked as partially filled but no quantity filled",
|
|
context, order.id
|
|
)));
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_latency_measurement() -> TestResult<()> {
|
|
let mut measurement = performance::LatencyMeasurement::new(100);
|
|
|
|
for i in 1..=10 {
|
|
measurement.record(Duration::from_nanos(i * 1000));
|
|
}
|
|
|
|
let avg = measurement.average().expect("Should have average");
|
|
assert!(avg > Duration::from_nanos(5000));
|
|
assert!(avg < Duration::from_nanos(6000));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_financial_precision() -> TestResult<()> {
|
|
let price1 = Decimal::try_from(100.12345678).unwrap_or_default();
|
|
let price2 = Decimal::try_from(100.12345679).unwrap_or_default();
|
|
|
|
// Should pass within precision
|
|
financial::assert_decimal_eq(price1, price2, "price comparison")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_market_data_generation() -> TestResult<()> {
|
|
let stream = market_data::TestMarketDataStream::new(
|
|
vec!["AAPL".to_string(), "GOOGL".to_string()],
|
|
1000, // 1000 Hz
|
|
);
|
|
|
|
let ticks = stream.generate_ticks(Duration::from_millis(10)).await?;
|
|
assert!(!ticks.is_empty());
|
|
|
|
market_data::validate_tick_sequence(&ticks, "test market data")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_order_simulation() -> TestResult<()> {
|
|
let mut order = orders::TestOrder::new_limit_order(
|
|
"AAPL",
|
|
orders::OrderSide::Buy,
|
|
Decimal::from(100),
|
|
Decimal::try_from(150.0).unwrap_or_default(),
|
|
);
|
|
|
|
// Simulate partial fill
|
|
order.simulate_fill(
|
|
Decimal::from(50),
|
|
Decimal::try_from(149.5).unwrap_or_default(),
|
|
)?;
|
|
|
|
assert_eq!(order.status, orders::OrderStatus::PartiallyFilled);
|
|
assert_eq!(order.filled_quantity, Decimal::from(50));
|
|
|
|
// Complete the fill
|
|
order.simulate_fill(
|
|
Decimal::from(50),
|
|
Decimal::try_from(150.5).unwrap_or_default(),
|
|
)?;
|
|
|
|
assert_eq!(order.status, orders::OrderStatus::Filled);
|
|
assert_eq!(order.filled_quantity, Decimal::from(100));
|
|
|
|
Ok(())
|
|
}
|
|
}
|