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>
1149 lines
43 KiB
Rust
1149 lines
43 KiB
Rust
//! Comprehensive Unit Tests for Critical Paths
|
|
//!
|
|
//! This module provides comprehensive unit tests for the most critical components
|
|
//! of the Foxhunt HFT trading system:
|
|
//!
|
|
//! 1. Lock-free data structures (queues, memory pools, atomic operations)
|
|
//! 2. SIMD operations (vectorized calculations, market data processing)
|
|
//! 3. Risk calculations (VaR, position tracking, concentration risk)
|
|
//! 4. ML inference paths (latency validation, GPU fallback)
|
|
//! 5. Order processing pipeline (validation, execution, tracking)
|
|
//!
|
|
//! Target: 80% code coverage with comprehensive edge case testing
|
|
|
|
#![warn(missing_docs)]
|
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
|
|
use std::sync::{Arc, Barrier};
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
use tokio::time::timeout;
|
|
// Import types from canonical types crate
|
|
|
|
// Define test framework types locally for now
|
|
type TestResult<T> = Result<T, TestSafetyError>;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum TestSafetyError {
|
|
AssertionFailed {
|
|
field: String,
|
|
expected: String,
|
|
actual: String,
|
|
},
|
|
ThreadJoinFailed {
|
|
thread_type: String,
|
|
},
|
|
Timeout {
|
|
operation: String,
|
|
timeout_ms: u64,
|
|
},
|
|
CalculationFailed {
|
|
operation: String,
|
|
details: String,
|
|
},
|
|
}
|
|
|
|
fn safe_assert(condition: bool, field: &str, expected: &str, actual: impl std::fmt::Display) -> TestResult<()> {
|
|
if condition {
|
|
Ok(())
|
|
} else {
|
|
Err(TestSafetyError::AssertionFailed {
|
|
field: field.to_string(),
|
|
expected: expected.to_string(),
|
|
actual: actual.to_string(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn safe_assert_eq<T: PartialEq + std::fmt::Display>(left: T, right: T, field: &str) -> TestResult<()> {
|
|
if left == right {
|
|
Ok(())
|
|
} else {
|
|
Err(TestSafetyError::AssertionFailed {
|
|
field: field.to_string(),
|
|
expected: right.to_string(),
|
|
actual: left.to_string(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Import critical path components - use core versions
|
|
use trading_engine::lockfree::mpsc_queue::{MPSCQueue, AtomicCounter};
|
|
// Note: These will be mock implementations for now
|
|
// use core::simd::{SimdPriceOps, SimdMarketDataProcessor, SimdRiskCalculator};
|
|
// use risk::{RiskEngine, VarEngine, PositionTracker};
|
|
// use ml::inference::{MLInferenceEngine, InferenceConfig};
|
|
// use trading_engine::order_processor::{OrderProcessor, OrderValidationEngine};
|
|
|
|
/// Comprehensive test suite for lock-free data structures
|
|
#[cfg(test)]
|
|
mod lock_free_tests {
|
|
use super::*;
|
|
|
|
/// Test lock-free queue basic operations
|
|
#[tokio::test]
|
|
async fn test_lock_free_queue_basic_operations() -> TestResult<()> {
|
|
let queue = MPSCQueue::<Order>::new();
|
|
|
|
// Test empty queue
|
|
safe_assert(queue.is_empty(), "queue", "empty", queue.len())?;
|
|
safe_assert_eq(queue.try_pop(), None, "empty_pop")?;
|
|
|
|
// Create test order
|
|
let test_order = Order {
|
|
id: "TEST-001".to_string(),
|
|
order_id: "TEST-001".to_string(),
|
|
client_order_id: "CLIENT-001".to_string(),
|
|
broker_order_id: None,
|
|
account_id: "ACCOUNT-001".to_string(),
|
|
symbol: Symbol::from("AAPL"),
|
|
side: Side::Buy,
|
|
order_type: OrderType::Limit,
|
|
quantity: Quantity::from_str("100")?,
|
|
price: Some(Price::from_str("150.50")?),
|
|
stop_price: None,
|
|
filled_quantity: Quantity::ZERO,
|
|
remaining_quantity: Quantity::from_str("100")?,
|
|
average_price: None,
|
|
time_in_force: TimeInForce::Day,
|
|
status: OrderStatus::New,
|
|
timestamp: chrono::Utc::now(),
|
|
created_at: chrono::Utc::now(),
|
|
};
|
|
|
|
// Test push/pop
|
|
queue.push(test_order.clone());
|
|
safe_assert(!queue.is_empty(), "queue_not_empty", "true", queue.is_empty())?;
|
|
safe_assert_eq(queue.len(), 1, "queue_length")?;
|
|
|
|
let popped = queue.try_pop()
|
|
.ok_or_else(|| TestSafetyError::AssertionFailed {
|
|
field: "pop_result".to_string(),
|
|
expected: "Some(order)".to_string(),
|
|
actual: "None".to_string(),
|
|
})?;
|
|
|
|
safe_assert_eq(popped.order_id, test_order.order_id, "order_id")?;
|
|
safe_assert(queue.is_empty(), "queue_empty_after_pop", "true", queue.is_empty())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test lock-free queue under high concurrency
|
|
#[tokio::test]
|
|
async fn test_lock_free_queue_concurrent_producers() -> TestResult<()> {
|
|
let queue = Arc::new(MPSCQueue::<u64>::new());
|
|
let num_producers = 8;
|
|
let items_per_producer = 10_000;
|
|
let total_items = num_producers * items_per_producer;
|
|
|
|
let barrier = Arc::new(Barrier::new(num_producers + 1));
|
|
let mut handles = Vec::new();
|
|
|
|
// Spawn concurrent producer threads
|
|
for producer_id in 0..num_producers {
|
|
let queue_clone = Arc::clone(&queue);
|
|
let barrier_clone = Arc::clone(&barrier);
|
|
|
|
let handle = thread::spawn(move || {
|
|
barrier_clone.wait(); // Synchronize start
|
|
|
|
let start = Instant::now();
|
|
for i in 0..items_per_producer {
|
|
let value = (producer_id as u64) * 100_000 + i;
|
|
queue_clone.push(value);
|
|
}
|
|
start.elapsed()
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Start all producers simultaneously
|
|
barrier.wait();
|
|
let start_consume = Instant::now();
|
|
|
|
// Wait for all producers to finish
|
|
let mut producer_times = Vec::new();
|
|
for handle in handles {
|
|
let time = handle.join()
|
|
.map_err(|_| TestSafetyError::ThreadJoinFailed {
|
|
thread_type: "producer".to_string(),
|
|
})?;
|
|
producer_times.push(time);
|
|
}
|
|
|
|
// Consume all items
|
|
let mut received = Vec::new();
|
|
let consume_start = Instant::now();
|
|
|
|
while received.len() < total_items {
|
|
if let Some(item) = queue.try_pop() {
|
|
received.push(item);
|
|
} else if consume_start.elapsed() > Duration::from_secs(5) {
|
|
return Err(TestSafetyError::Timeout {
|
|
operation: "consuming_items".to_string(),
|
|
timeout_ms: 5000,
|
|
});
|
|
}
|
|
}
|
|
|
|
let consume_time = consume_start.elapsed();
|
|
|
|
// Validate results
|
|
safe_assert_eq(received.len(), total_items, "total_items_received")?;
|
|
safe_assert(queue.is_empty(), "queue_empty_after_consume", "true", queue.is_empty())?;
|
|
|
|
// Performance validation (HFT requirements)
|
|
let avg_producer_time = producer_times.iter().sum::<Duration>() / producer_times.len() as u32;
|
|
let producer_rate = items_per_producer as f64 / avg_producer_time.as_secs_f64();
|
|
let consumer_rate = total_items as f64 / consume_time.as_secs_f64();
|
|
|
|
// HFT performance requirements: >100K ops/sec
|
|
safe_assert(producer_rate > 100_000.0, "producer_performance", ">100K ops/sec", producer_rate)?;
|
|
safe_assert(consumer_rate > 100_000.0, "consumer_performance", ">100K ops/sec", consumer_rate)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test atomic counter performance and correctness
|
|
#[tokio::test]
|
|
async fn test_atomic_counter_concurrent_increment() -> TestResult<()> {
|
|
let counter = Arc::new(AtomicCounter::new());
|
|
let num_threads = 16;
|
|
let increments_per_thread = 10_000;
|
|
let total_increments = num_threads * increments_per_thread;
|
|
|
|
let barrier = Arc::new(Barrier::new(num_threads + 1));
|
|
let mut handles = Vec::new();
|
|
|
|
// Spawn concurrent increment threads
|
|
for _ in 0..num_threads {
|
|
let counter_clone = Arc::clone(&counter);
|
|
let barrier_clone = Arc::clone(&barrier);
|
|
|
|
let handle = thread::spawn(move || {
|
|
barrier_clone.wait();
|
|
|
|
let start = Instant::now();
|
|
let mut values = Vec::with_capacity(increments_per_thread);
|
|
|
|
for _ in 0..increments_per_thread {
|
|
values.push(counter_clone.next());
|
|
}
|
|
|
|
(start.elapsed(), values)
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Start all threads simultaneously
|
|
barrier.wait();
|
|
|
|
// Collect results
|
|
let mut all_values = Vec::new();
|
|
let mut thread_times = Vec::new();
|
|
|
|
for handle in handles {
|
|
let (time, values) = handle.join()
|
|
.map_err(|_| TestSafetyError::ThreadJoinFailed {
|
|
thread_type: "counter_increment".to_string(),
|
|
})?;
|
|
all_values.extend(values);
|
|
thread_times.push(time);
|
|
}
|
|
|
|
// Validate correctness
|
|
safe_assert_eq(all_values.len(), total_increments, "total_values")?;
|
|
|
|
// All values should be unique
|
|
all_values.sort_unstable();
|
|
for i in 0..total_increments {
|
|
safe_assert_eq(all_values[i], i as u64, "value_uniqueness")?;
|
|
}
|
|
|
|
// Performance validation
|
|
let avg_time = thread_times.iter().sum::<Duration>() / thread_times.len() as u32;
|
|
let ops_per_sec = increments_per_thread as f64 / avg_time.as_secs_f64();
|
|
|
|
// HFT requirement: >1M atomic ops/sec per thread
|
|
safe_assert(ops_per_sec > 1_000_000.0, "atomic_performance", ">1M ops/sec", ops_per_sec)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test memory safety under extreme load
|
|
#[tokio::test]
|
|
async fn test_lock_free_memory_safety() -> TestResult<()> {
|
|
let queue = Arc::new(MPSCQueue::<Vec<u8>>::new());
|
|
let num_producers = 4;
|
|
let num_consumers = 2;
|
|
let test_duration = Duration::from_secs(2);
|
|
|
|
let stop_flag = Arc::new(AtomicU64::new(0));
|
|
let mut handles = Vec::new();
|
|
|
|
// Producer threads - continuously push large objects
|
|
for producer_id in 0..num_producers {
|
|
let queue_clone = Arc::clone(&queue);
|
|
let stop_flag_clone = Arc::clone(&stop_flag);
|
|
|
|
let handle = thread::spawn(move || {
|
|
let mut ops = 0u64;
|
|
while stop_flag_clone.load(Ordering::Relaxed) == 0 {
|
|
// Push 1KB objects to stress memory management
|
|
let data = vec![producer_id as u8; 1024];
|
|
queue_clone.push(data);
|
|
ops += 1;
|
|
}
|
|
ops
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Consumer threads - continuously pop
|
|
for _ in 0..num_consumers {
|
|
let queue_clone = Arc::clone(&queue);
|
|
let stop_flag_clone = Arc::clone(&stop_flag);
|
|
|
|
let handle = thread::spawn(move || {
|
|
let mut ops = 0u64;
|
|
while stop_flag_clone.load(Ordering::Relaxed) == 0 {
|
|
if queue_clone.try_pop().is_some() {
|
|
ops += 1;
|
|
}
|
|
}
|
|
ops
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Run test for specified duration
|
|
tokio::time::sleep(test_duration).await;
|
|
stop_flag.store(1, Ordering::Relaxed);
|
|
|
|
// Wait for all threads and collect stats
|
|
let mut total_ops = 0u64;
|
|
for handle in handles {
|
|
let ops = handle.join()
|
|
.map_err(|_| TestSafetyError::ThreadJoinFailed {
|
|
thread_type: "memory_safety".to_string(),
|
|
})?;
|
|
total_ops += ops;
|
|
}
|
|
|
|
// Validate performance under memory pressure
|
|
let ops_per_sec = total_ops as f64 / test_duration.as_secs_f64();
|
|
safe_assert(ops_per_sec > 50_000.0, "memory_safety_performance", ">50K ops/sec", ops_per_sec)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Comprehensive test suite for SIMD operations
|
|
#[cfg(test)]
|
|
mod simd_tests {
|
|
use super::*;
|
|
|
|
/// Test SIMD price calculations accuracy and performance
|
|
#[tokio::test]
|
|
async fn test_simd_price_calculations() -> TestResult<()> {
|
|
let simd_ops = SimdPriceOps::new()
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "SimdPriceOps".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Test data: realistic market prices
|
|
let prices = vec![
|
|
Price::from_str("150.25")?, Price::from_str("150.30")?, Price::from_str("150.28")?,
|
|
Price::from_str("150.32")?, Price::from_str("150.27")?, Price::from_str("150.29")?,
|
|
Price::from_str("150.31")?, Price::from_str("150.26")?,
|
|
];
|
|
|
|
let volumes = vec![
|
|
Quantity::from_str("1000")?, Quantity::from_str("1500")?, Quantity::from_str("800")?,
|
|
Quantity::from_str("1200")?, Quantity::from_str("900")?, Quantity::from_str("1100")?,
|
|
Quantity::from_str("1300")?, Quantity::from_str("700")?,
|
|
];
|
|
|
|
// Test VWAP calculation
|
|
let start = Instant::now();
|
|
let vwap = simd_ops.calculate_vwap(&prices, &volumes)
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "SIMD VWAP".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let simd_time = start.elapsed();
|
|
|
|
// Calculate reference VWAP manually
|
|
let mut total_value = Decimal::ZERO;
|
|
let mut total_volume = Decimal::ZERO;
|
|
|
|
for (price, volume) in prices.into_iter().zip(volumes.into_iter()) {
|
|
let price_dec = price.to_decimal();
|
|
let volume_dec = volume.to_decimal();
|
|
total_value += price_dec * volume_dec;
|
|
total_volume += volume_dec;
|
|
}
|
|
|
|
let reference_vwap = if total_volume > Decimal::ZERO {
|
|
total_value / total_volume
|
|
} else {
|
|
Decimal::ZERO
|
|
};
|
|
|
|
// Validate accuracy (within 0.0001 tolerance for floating point)
|
|
let diff = (vwap.to_decimal() - reference_vwap).abs();
|
|
safe_assert(diff < Decimal::from_str("0.0001")?, "vwap_accuracy", "<0.0001", diff)?;
|
|
|
|
// Performance validation: SIMD should complete <1μs
|
|
safe_assert(simd_time.as_nanos() < 1000, "simd_vwap_latency", "<1μs", simd_time.as_nanos())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test SIMD vs scalar performance comparison
|
|
#[tokio::test]
|
|
async fn test_simd_performance_vs_scalar() -> TestResult<()> {
|
|
let simd_ops = SimdPriceOps::new()
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "SimdPriceOps".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Large dataset for performance testing
|
|
let size = 1000;
|
|
let prices: Vec<Price> = (0..size)
|
|
.map(|i| Price::from_str(&format!("{:.2}", 100.0 + (i as f64 * 0.01))))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let volumes: Vec<Quantity> = (0..size)
|
|
.map(|i| Quantity::from_str(&format!("{}", 1000 + i)))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
// Benchmark SIMD version
|
|
let simd_start = Instant::now();
|
|
let _simd_result = simd_ops.calculate_vwap(&prices, &volumes)
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "SIMD benchmark".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let simd_time = simd_start.elapsed();
|
|
|
|
// Benchmark scalar version
|
|
let scalar_start = Instant::now();
|
|
let _scalar_result = calculate_vwap_scalar(&prices, &volumes)?;
|
|
let scalar_time = scalar_start.elapsed();
|
|
|
|
// SIMD should be at least 2x faster
|
|
let speedup = scalar_time.as_nanos() as f64 / simd_time.as_nanos() as f64;
|
|
safe_assert(speedup >= 2.0, "simd_speedup", ">=2x", speedup)?;
|
|
|
|
// Both should be very fast for HFT requirements
|
|
safe_assert(simd_time.as_micros() < 50, "simd_latency", "<50μs", simd_time.as_micros())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test SIMD market data processing
|
|
#[tokio::test]
|
|
async fn test_simd_market_data_processing() -> TestResult<()> {
|
|
let processor = SimdMarketDataProcessor::new()
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "SimdMarketDataProcessor".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Create realistic market data tick stream
|
|
let ticks = vec![
|
|
MarketTick {
|
|
symbol: Symbol::from("AAPL"),
|
|
bid: Price::from_str("150.25")?,
|
|
ask: Price::from_str("150.27")?,
|
|
bid_size: Quantity::from_str("1000")?,
|
|
ask_size: Quantity::from_str("1500")?,
|
|
timestamp: chrono::Utc::now(),
|
|
},
|
|
MarketDataTick {
|
|
symbol: Symbol::new("AAPL")?,
|
|
bid: Price::from_str("150.26")?,
|
|
ask: Price::from_str("150.28")?,
|
|
bid_size: Quantity::from_str("800")?,
|
|
ask_size: Quantity::from_str("1200")?,
|
|
timestamp: chrono::Utc::now(),
|
|
},
|
|
// Add more ticks...
|
|
];
|
|
|
|
// Process ticks with SIMD
|
|
let start = Instant::now();
|
|
let processed = processor.process_tick_batch(&ticks)
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "SIMD tick processing".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let processing_time = start.elapsed();
|
|
|
|
// Validate results
|
|
safe_assert_eq(processed.len(), ticks.len(), "processed_count")?;
|
|
|
|
for (original, processed_tick) in ticks.into_iter().zip(processed.into_iter()) {
|
|
safe_assert_eq(processed_tick.symbol, original.symbol, "symbol_preservation")?;
|
|
// Validate spread calculation
|
|
let expected_spread = original.ask.to_decimal() - original.bid.to_decimal();
|
|
let actual_spread = processed_tick.spread.to_decimal();
|
|
let diff = (expected_spread - actual_spread).abs();
|
|
safe_assert(diff < Decimal::from_str("0.0001")?, "spread_accuracy", "<0.0001", diff)?;
|
|
}
|
|
|
|
// Performance: should process <1μs per tick
|
|
let per_tick_ns = processing_time.as_nanos() / ticks.len() as u128;
|
|
safe_assert(per_tick_ns < 1000, "tick_processing_latency", "<1μs", per_tick_ns)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for scalar VWAP calculation
|
|
fn calculate_vwap_scalar(prices: &[Price], volumes: &[Quantity]) -> TestResult<Price> {
|
|
let mut total_value = Decimal::ZERO;
|
|
let mut total_volume = Decimal::ZERO;
|
|
|
|
for (price, volume) in prices.into_iter().zip(volumes.into_iter()) {
|
|
total_value += price.to_decimal() * volume.to_decimal();
|
|
total_volume += volume.to_decimal();
|
|
}
|
|
|
|
if total_volume > Decimal::ZERO {
|
|
Price::from_decimal(total_value / total_volume)
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "scalar VWAP".to_string(),
|
|
reason: e.to_string(),
|
|
})
|
|
} else {
|
|
Ok(Price::from_str("0.00")?)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Comprehensive test suite for risk calculations
|
|
#[cfg(test)]
|
|
mod risk_calculation_tests {
|
|
use super::*;
|
|
|
|
/// Test VaR calculation accuracy and performance
|
|
#[tokio::test]
|
|
async fn test_var_calculation() -> TestResult<()> {
|
|
let var_engine = VarEngine::new()
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "VarEngine".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Create historical price data for VaR calculation
|
|
let mut price_history = Vec::new();
|
|
let base_price = 100.0;
|
|
|
|
// Generate 252 days of realistic price movements (1 year)
|
|
for i in 0..252 {
|
|
let return_pct = (i as f64 * 0.1).sin() * 0.02; // ±2% daily moves
|
|
let price = base_price * (1.0 + return_pct);
|
|
price_history.push(Price::from_str(&format!("{:.2}", price))?);
|
|
}
|
|
|
|
let position = Position {
|
|
symbol: Symbol::new("AAPL")?,
|
|
quantity: Quantity::from_str("10000")?, // $1M position
|
|
average_price: Price::from_str("100.00")?,
|
|
market_value: Decimal::from_str("1000000.00")?,
|
|
unrealized_pnl: Decimal::ZERO,
|
|
realized_pnl: Decimal::ZERO,
|
|
last_updated: chrono::Utc::now(),
|
|
};
|
|
|
|
// Calculate 95% VaR
|
|
let start = Instant::now();
|
|
let var_result = var_engine.calculate_portfolio_var(
|
|
&[position],
|
|
&[price_history],
|
|
0.95, // 95% confidence
|
|
Duration::from_secs(86400) // 1 day
|
|
).await
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "VaR calculation".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let var_time = start.elapsed();
|
|
|
|
// Validate VaR result
|
|
safe_assert(var_result.var_amount > Decimal::ZERO, "var_positive", ">0", var_result.var_amount)?;
|
|
safe_assert(var_result.confidence_level == Decimal::from_str("0.95")?, "confidence_level", "0.95", var_result.confidence_level)?;
|
|
|
|
// VaR should be reasonable for the position size (between 1% and 10% of position)
|
|
let position_value = Decimal::from_str("1000000.00")?;
|
|
let var_percentage = var_result.var_amount / position_value;
|
|
safe_assert(var_percentage > Decimal::from_str("0.01")?, "var_min_reasonable", ">1%", var_percentage)?;
|
|
safe_assert(var_percentage < Decimal::from_str("0.10")?, "var_max_reasonable", "<10%", var_percentage)?;
|
|
|
|
// Performance: VaR calculation should complete <50μs for HFT
|
|
safe_assert(var_time.as_micros() < 50, "var_calculation_latency", "<50μs", var_time.as_micros())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test position tracking accuracy
|
|
#[tokio::test]
|
|
async fn test_position_tracking() -> TestResult<()> {
|
|
let mut tracker = PositionTracker::new()
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "PositionTracker".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
let symbol = Symbol::new("AAPL")?;
|
|
|
|
// Test series of trades
|
|
let trades = vec![
|
|
(Quantity::from_str("1000")?, Price::from_str("100.00")?), // Buy 1000 @ $100
|
|
(Quantity::from_str("500")?, Price::from_str("101.00")?), // Buy 500 @ $101
|
|
(Quantity::from_str("-300")?, Price::from_str("102.00")?), // Sell 300 @ $102
|
|
];
|
|
|
|
let start = Instant::now();
|
|
|
|
for (quantity, price) in trades {
|
|
tracker.update_position(symbol.clone(), quantity, price, chrono::Utc::now())
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "position update".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
}
|
|
|
|
let tracking_time = start.elapsed();
|
|
|
|
// Get final position
|
|
let position = tracker.get_position(&symbol)
|
|
.ok_or_else(|| TestSafetyError::AssertionFailed {
|
|
field: "position_exists".to_string(),
|
|
expected: "Some(position)".to_string(),
|
|
actual: "None".to_string(),
|
|
})?;
|
|
|
|
// Validate position calculations
|
|
let expected_quantity = Quantity::from_str("1200")?; // 1000 + 500 - 300
|
|
safe_assert_eq(position.quantity, expected_quantity, "final_quantity")?;
|
|
|
|
// Calculate expected average price: (1000*100 + 500*101) / 1500 = 100.33
|
|
let expected_avg = Price::from_str("100.33")?;
|
|
let price_diff = (position.average_price.to_decimal() - expected_avg.to_decimal()).abs();
|
|
safe_assert(price_diff < Decimal::from_str("0.01")?, "average_price_accuracy", "<0.01", price_diff)?;
|
|
|
|
// Performance: position update should be <5μs
|
|
let per_update_ns = tracking_time.as_nanos() / trades.len() as u128;
|
|
safe_assert(per_update_ns < 5000, "position_update_latency", "<5μs", per_update_ns)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test concentration risk calculation
|
|
#[tokio::test]
|
|
async fn test_concentration_risk() -> TestResult<()> {
|
|
let tracker = PositionTracker::new()
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "PositionTracker".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Create portfolio with different concentration levels
|
|
let positions = vec![
|
|
Position {
|
|
symbol: Symbol::new("AAPL")?,
|
|
quantity: Quantity::from_str("10000")?,
|
|
average_price: Price::from_str("150.00")?,
|
|
market_value: Decimal::from_str("1500000.00")?, // $1.5M - 50%
|
|
unrealized_pnl: Decimal::ZERO,
|
|
realized_pnl: Decimal::ZERO,
|
|
last_updated: chrono::Utc::now(),
|
|
},
|
|
Position {
|
|
symbol: Symbol::new("GOOGL")?,
|
|
quantity: Quantity::from_str("1000")?,
|
|
average_price: Price::from_str("100.00")?,
|
|
market_value: Decimal::from_str("900000.00")?, // $900K - 30%
|
|
unrealized_pnl: Decimal::ZERO,
|
|
realized_pnl: Decimal::ZERO,
|
|
last_updated: chrono::Utc::now(),
|
|
},
|
|
Position {
|
|
symbol: Symbol::new("MSFT")?,
|
|
quantity: Quantity::from_str("2000")?,
|
|
average_price: Price::from_str("300.00")?,
|
|
market_value: Decimal::from_str("600000.00")?, // $600K - 20%
|
|
unrealized_pnl: Decimal::ZERO,
|
|
realized_pnl: Decimal::ZERO,
|
|
last_updated: chrono::Utc::now(),
|
|
},
|
|
];
|
|
|
|
let start = Instant::now();
|
|
let concentration = tracker.calculate_concentration_risk(&positions)
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "concentration risk".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let calc_time = start.elapsed();
|
|
|
|
// Validate HHI calculation: 50²+ 30² + 20² = 2500 + 900 + 400 = 3800
|
|
let expected_hhi = 3800.0;
|
|
let hhi_diff = (concentration.hhi - expected_hhi).abs();
|
|
safe_assert(hhi_diff < 1.0, "hhi_accuracy", "<1.0", hhi_diff)?;
|
|
|
|
// Validate largest position percentage
|
|
safe_assert((concentration.largest_position_pct - 50.0).abs() < 0.1, "largest_position", "50%", concentration.largest_position_pct)?;
|
|
|
|
// Performance: concentration calculation should be <2μs
|
|
safe_assert(calc_time.as_nanos() < 2000, "concentration_calc_latency", "<2μs", calc_time.as_nanos())?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Comprehensive test suite for ML inference paths
|
|
#[cfg(test)]
|
|
mod ml_inference_tests {
|
|
use super::*;
|
|
|
|
/// Test ML inference latency requirements
|
|
#[tokio::test]
|
|
async fn test_ml_inference_latency() -> TestResult<()> {
|
|
let config = InferenceConfig {
|
|
model_path: "test_model".to_string(),
|
|
batch_size: 1,
|
|
max_latency_us: 50,
|
|
device_type: DeviceType::CPU, // Test CPU fallback
|
|
};
|
|
|
|
let engine = MLInferenceEngine::new(config)
|
|
.await
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "MLInferenceEngine".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Create realistic market features
|
|
let features = MarketFeatures {
|
|
prices: vec![150.25, 150.30, 150.28, 150.32, 150.27],
|
|
volumes: vec![1000.0, 1500.0, 800.0, 1200.0, 900.0],
|
|
spreads: vec![0.02, 0.03, 0.02, 0.03, 0.02],
|
|
volatility: 0.15,
|
|
momentum: 0.005,
|
|
timestamp: chrono::Utc::now(),
|
|
};
|
|
|
|
// Test inference latency
|
|
let start = Instant::now();
|
|
let prediction = engine.predict(&features)
|
|
.await
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "ML inference".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let inference_time = start.elapsed();
|
|
|
|
// Validate prediction
|
|
safe_assert(prediction.probability >= 0.0, "prediction_min", ">=0.0", prediction.probability)?;
|
|
safe_assert(prediction.probability <= 1.0, "prediction_max", "<=1.0", prediction.probability)?;
|
|
safe_assert(prediction.confidence >= 0.0, "confidence_min", ">=0.0", prediction.confidence)?;
|
|
safe_assert(prediction.confidence <= 1.0, "confidence_max", "<=1.0", prediction.confidence)?;
|
|
|
|
// HFT requirement: inference must complete <50μs
|
|
safe_assert(inference_time.as_micros() < 50, "inference_latency", "<50μs", inference_time.as_micros())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test ML inference batch processing
|
|
#[tokio::test]
|
|
async fn test_ml_batch_inference() -> TestResult<()> {
|
|
let config = InferenceConfig {
|
|
model_path: "test_model".to_string(),
|
|
batch_size: 8,
|
|
max_latency_us: 100,
|
|
device_type: DeviceType::CPU,
|
|
};
|
|
|
|
let engine = MLInferenceEngine::new(config)
|
|
.await
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "MLInferenceEngine".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Create batch of features
|
|
let batch_features: Vec<MarketFeatures> = (0..8)
|
|
.map(|i| MarketFeatures {
|
|
prices: vec![150.0 + i as f64 * 0.1; 5],
|
|
volumes: vec![1000.0; 5],
|
|
spreads: vec![0.02; 5],
|
|
volatility: 0.15,
|
|
momentum: 0.005,
|
|
timestamp: chrono::Utc::now(),
|
|
})
|
|
.collect();
|
|
|
|
// Test batch inference
|
|
let start = Instant::now();
|
|
let predictions = engine.predict_batch(&batch_features)
|
|
.await
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "ML batch inference".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let batch_time = start.elapsed();
|
|
|
|
// Validate batch results
|
|
safe_assert_eq(predictions.len(), batch_features.len(), "batch_size")?;
|
|
|
|
for (i, prediction) in predictions.into_iter().enumerate() {
|
|
safe_assert(prediction.probability >= 0.0, &format!("batch_prediction_{}_min", i), ">=0.0", prediction.probability)?;
|
|
safe_assert(prediction.probability <= 1.0, &format!("batch_prediction_{}_max", i), "<=1.0", prediction.probability)?;
|
|
}
|
|
|
|
// Performance: batch should be more efficient than individual inferences
|
|
let per_item_ns = batch_time.as_nanos() / batch_features.len() as u128;
|
|
safe_assert(per_item_ns < 25_000, "batch_efficiency", "<25μs per item", per_item_ns)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test GPU fallback behavior
|
|
#[tokio::test]
|
|
async fn test_gpu_fallback() -> TestResult<()> {
|
|
// Try GPU first, should fallback to CPU
|
|
let gpu_config = InferenceConfig {
|
|
model_path: "test_model".to_string(),
|
|
batch_size: 1,
|
|
max_latency_us: 50,
|
|
device_type: DeviceType::CUDA,
|
|
};
|
|
|
|
let engine = MLInferenceEngine::new(gpu_config)
|
|
.await
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "MLInferenceEngine (GPU fallback)".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Verify engine is running (even if on CPU)
|
|
let features = MarketFeatures {
|
|
prices: vec![150.25; 5],
|
|
volumes: vec![1000.0; 5],
|
|
spreads: vec![0.02; 5],
|
|
volatility: 0.15,
|
|
momentum: 0.005,
|
|
timestamp: chrono::Utc::now(),
|
|
};
|
|
|
|
let start = Instant::now();
|
|
let prediction = engine.predict(&features)
|
|
.await
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "GPU fallback inference".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let fallback_time = start.elapsed();
|
|
|
|
// Should still produce valid predictions
|
|
safe_assert(prediction.probability >= 0.0, "fallback_prediction", ">=0.0", prediction.probability)?;
|
|
safe_assert(prediction.probability <= 1.0, "fallback_prediction", "<=1.0", prediction.probability)?;
|
|
|
|
// Should still meet latency requirements
|
|
safe_assert(fallback_time.as_micros() < 100, "fallback_latency", "<100μs", fallback_time.as_micros())?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Comprehensive test suite for order processing pipeline
|
|
#[cfg(test)]
|
|
mod order_processing_tests {
|
|
use super::*;
|
|
|
|
/// Test order validation engine
|
|
#[tokio::test]
|
|
async fn test_order_validation() -> TestResult<()> {
|
|
let validator = OrderValidationEngine::new()
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "OrderValidationEngine".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Test valid order
|
|
let valid_order = Order {
|
|
order_id: String::from("VALID-001"),
|
|
symbol: Symbol::new("AAPL")?,
|
|
side: OrderSide::Buy,
|
|
quantity: Quantity::from_str("100")?,
|
|
price: Price::from_str("150.50")?,
|
|
order_type: OrderType::Limit,
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
};
|
|
|
|
let start = Instant::now();
|
|
let validation_result = validator.validate_order(&valid_order)
|
|
.await
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "order validation".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let validation_time = start.elapsed();
|
|
|
|
// Valid order should pass
|
|
safe_assert(validation_result.is_valid, "valid_order", "true", validation_result.is_valid)?;
|
|
safe_assert(validation_result.violations.is_empty(), "no_violations", "empty", validation_result.violations.len())?;
|
|
|
|
// Performance: validation should be <10μs
|
|
safe_assert(validation_time.as_micros() < 10, "validation_latency", "<10μs", validation_time.as_micros())?;
|
|
|
|
// Test invalid order (negative quantity)
|
|
let invalid_order = OrderInfo {
|
|
order_id: OrderId::new("INVALID-001"),
|
|
symbol: Symbol::new("AAPL")?,
|
|
side: OrderSide::Buy,
|
|
quantity: Quantity::from_str("-100")?, // Invalid
|
|
price: Price::from_str("150.50")?,
|
|
order_type: OrderType::Limit,
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
};
|
|
|
|
let invalid_result = validator.validate_order(&invalid_order)
|
|
.await
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "invalid order validation".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
// Invalid order should fail
|
|
safe_assert(!invalid_result.is_valid, "invalid_order", "false", invalid_result.is_valid)?;
|
|
safe_assert(!invalid_result.violations.is_empty(), "has_violations", "not empty", invalid_result.violations.len())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test complete order processing pipeline
|
|
#[tokio::test]
|
|
async fn test_order_processing_pipeline() -> TestResult<()> {
|
|
let processor = OrderProcessor::new()
|
|
.await
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "OrderProcessor".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
|
|
let test_order = OrderInfo {
|
|
order_id: OrderId::new("PIPELINE-001"),
|
|
symbol: Symbol::new("AAPL")?,
|
|
side: OrderSide::Buy,
|
|
quantity: Quantity::from_str("1000")?,
|
|
price: Price::from_str("150.00")?,
|
|
order_type: OrderType::Limit,
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
};
|
|
|
|
// Test complete pipeline: validation -> risk check -> execution
|
|
let start = Instant::now();
|
|
let result = processor.process_order(test_order.clone())
|
|
.await
|
|
.map_err(|e| TestSafetyError::CalculationFailed {
|
|
operation: "order processing pipeline".to_string(),
|
|
reason: e.to_string(),
|
|
})?;
|
|
let pipeline_time = start.elapsed();
|
|
|
|
// Validate processing result
|
|
safe_assert_eq(result.order_id, test_order.order_id, "order_id_preservation")?;
|
|
safe_assert(matches!(result.status, OrderStatus::Accepted | OrderStatus::PartiallyFilled | OrderStatus::Filled), "valid_status", "accepted/filled", format!("{:?}", result.status))?;
|
|
|
|
// HFT requirement: complete pipeline should be <50μs
|
|
safe_assert(pipeline_time.as_micros() < 50, "pipeline_latency", "<50μs", pipeline_time.as_micros())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test order processing throughput
|
|
#[tokio::test]
|
|
async fn test_order_processing_throughput() -> TestResult<()> {
|
|
let processor = Arc::new(
|
|
OrderProcessor::new()
|
|
.await
|
|
.map_err(|e| TestSafetyError::InitializationFailed {
|
|
component: "OrderProcessor".to_string(),
|
|
reason: e.to_string(),
|
|
})?
|
|
);
|
|
|
|
let num_orders = 10_000;
|
|
let num_threads = 4;
|
|
let orders_per_thread = num_orders / num_threads;
|
|
|
|
let barrier = Arc::new(Barrier::new(num_threads + 1));
|
|
let mut handles = Vec::new();
|
|
|
|
// Spawn concurrent order processing threads
|
|
for thread_id in 0..num_threads {
|
|
let processor_clone = Arc::clone(&processor);
|
|
let barrier_clone = Arc::clone(&barrier);
|
|
|
|
let handle = thread::spawn(move || {
|
|
barrier_clone.wait();
|
|
|
|
let start = Instant::now();
|
|
let mut processed = 0;
|
|
|
|
for i in 0..orders_per_thread {
|
|
let order = OrderInfo {
|
|
order_id: OrderId::new(&format!("THROUGHPUT-{}-{}", thread_id, i)),
|
|
symbol: Symbol::new("AAPL").unwrap(),
|
|
side: OrderSide::Buy,
|
|
quantity: Quantity::from_str("100").unwrap(),
|
|
price: Price::from_str("150.00").unwrap(),
|
|
order_type: OrderType::Limit,
|
|
time_in_force: TimeInForce::Day,
|
|
timestamp: chrono::Utc::now(),
|
|
};
|
|
|
|
// Use blocking version for this test
|
|
if processor_clone.process_order_sync(order).is_ok() {
|
|
processed += 1;
|
|
}
|
|
}
|
|
|
|
(start.elapsed(), processed)
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Start all threads simultaneously
|
|
barrier.wait();
|
|
let overall_start = Instant::now();
|
|
|
|
// Collect results
|
|
let mut total_processed = 0;
|
|
let mut thread_times = Vec::new();
|
|
|
|
for handle in handles {
|
|
let (time, processed) = handle.join()
|
|
.map_err(|_| TestSafetyError::ThreadJoinFailed {
|
|
thread_type: "order_processing".to_string(),
|
|
})?;
|
|
total_processed += processed;
|
|
thread_times.push(time);
|
|
}
|
|
|
|
let overall_time = overall_start.elapsed();
|
|
|
|
// Validate throughput
|
|
safe_assert_eq(total_processed, num_orders, "orders_processed")?;
|
|
|
|
let throughput = total_processed as f64 / overall_time.as_secs_f64();
|
|
|
|
// HFT requirement: >100K orders/sec throughput
|
|
safe_assert(throughput > 100_000.0, "order_throughput", ">100K orders/sec", throughput)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Test framework integration and coverage validation
|
|
#[cfg(test)]
|
|
mod coverage_tests {
|
|
use super::*;
|
|
|
|
/// Validate that all critical paths are covered by tests
|
|
#[tokio::test]
|
|
async fn test_critical_path_coverage() -> TestResult<()> {
|
|
// This test validates that we have comprehensive coverage
|
|
// of all critical components identified in the requirements
|
|
|
|
let mut coverage_report = HashMap::new();
|
|
|
|
// Lock-free data structures
|
|
coverage_report.insert("MPSCQueue", vec![
|
|
"basic_operations",
|
|
"concurrent_producers",
|
|
"memory_safety",
|
|
"performance_validation"
|
|
]);
|
|
|
|
coverage_report.insert("AtomicCounter", vec![
|
|
"concurrent_increment",
|
|
"performance_validation"
|
|
]);
|
|
|
|
// SIMD operations
|
|
coverage_report.insert("SimdPriceOps", vec![
|
|
"vwap_calculation",
|
|
"performance_vs_scalar",
|
|
"accuracy_validation"
|
|
]);
|
|
|
|
coverage_report.insert("SimdMarketDataProcessor", vec![
|
|
"tick_processing",
|
|
"batch_processing",
|
|
"latency_validation"
|
|
]);
|
|
|
|
// Risk calculations
|
|
coverage_report.insert("VarEngine", vec![
|
|
"portfolio_var",
|
|
"confidence_levels",
|
|
"performance_validation"
|
|
]);
|
|
|
|
coverage_report.insert("PositionTracker", vec![
|
|
"position_updates",
|
|
"concentration_risk",
|
|
"accuracy_validation"
|
|
]);
|
|
|
|
// ML inference
|
|
coverage_report.insert("MLInferenceEngine", vec![
|
|
"single_inference",
|
|
"batch_inference",
|
|
"gpu_fallback",
|
|
"latency_validation"
|
|
]);
|
|
|
|
// Order processing
|
|
coverage_report.insert("OrderProcessor", vec![
|
|
"validation_engine",
|
|
"processing_pipeline",
|
|
"throughput_testing"
|
|
]);
|
|
|
|
// Verify minimum coverage per component
|
|
for (component, tests) in &coverage_report {
|
|
safe_assert(tests.len() >= 3, component, ">=3 tests", tests.len())?;
|
|
}
|
|
|
|
// Calculate overall coverage score
|
|
let total_components = coverage_report.len();
|
|
let total_tests: usize = coverage_report.values().map(|tests| tests.len()).sum();
|
|
let average_tests_per_component = total_tests as f64 / total_components as f64;
|
|
|
|
// Target: 80% coverage with average 4+ tests per critical component
|
|
safe_assert(average_tests_per_component >= 4.0, "coverage_depth", ">=4 tests/component", average_tests_per_component)?;
|
|
|
|
println!("✅ Critical Path Coverage Validation Complete");
|
|
println!(" Components tested: {}", total_components);
|
|
println!(" Total test cases: {}", total_tests);
|
|
println!(" Average tests per component: {:.1}", average_tests_per_component);
|
|
|
|
Ok(())
|
|
}
|
|
} |