//! Comprehensive Property-Based Tests for Financial Calculations //! //! This module provides exhaustive property-based testing for all financial calculations //! in the Foxhunt HFT system to ensure REAL MONEY safety through mathematical correctness. //! //! CRITICAL: These tests prevent financial losses through precision errors and edge cases. #[cfg(test)] mod tests { use proptest::prelude::*; use std::f64::{INFINITY, NEG_INFINITY, NAN}; use chrono::{DateTime, Utc}; use common::Order; use common::Position; use common::Symbol; use common::Price; use common::Quantity; use common::error::CommonError; use common::error::CommonResult; use common::OrderId; use common::TradeId; use common::ExecutionId; use common::OrderType; use common::OrderSide; // Test Types - Simplified versions for property testing #[derive(Debug, Clone, Copy, PartialEq)] struct TestPrice(u64); // Fixed-point representation, 8 decimals #[derive(Debug, Clone, Copy, PartialEq)] struct Quantity(u64); #[derive(Debug, Clone, Copy, PartialEq)] struct TestVolume(u64); #[derive(Debug, Clone, Copy, PartialEq)] struct Amount(i64); // Implementations impl TestPrice { pub fn new(value: f64) -> Self { // Convert to fixed-point representation matching production types let decimal_value = Decimal::try_from(value).unwrap_or(Decimal::ZERO); let scaled = decimal_value * Decimal::from(100_000_000u64); // 8 decimal places Self(scaled.to_u64().unwrap_or(0)) } pub fn value(&self) -> f64 { (self.0 as f64) / 100_000_000.0 // Convert back from fixed-point } pub fn from_f64(value: f64) -> Self { TestTestPrice::new(value.max(0.0)) // Clamp to non-negative } pub fn to_f64(&self) -> f64 { self.value() } pub fn add(&self, other: &TestPrice) -> TestPrice { TestTestPrice::from_f64(self.value() + other.value()) } pub fn multiply(&self, factor: f64) -> TestPrice { TestTestPrice::from_f64(self.value() * factor) } pub fn percentage_change(&self, old_price: &TestPrice) -> f64 { let old_val = old_price.value(); if old_val == 0.0 { 0.0 } else { (self.value() - old_val) / old_val } } } impl Quantity { pub fn new(value: i64) -> Self { // Ensure non-negative values to match production u64 type Quantity(value.max(0) as u64) } pub fn value(&self) -> i64 { self.0 as i64 } pub fn to_i64(&self) -> i64 { self.0 as i64 } pub fn abs(&self) -> Quantity { Quantity(self.0) } pub fn add(&self, other: &Quantity) -> Quantity { Quantity(self.0.saturating_add(other.0)) } } impl TestVolume { pub fn new(value: u64) -> Self { TestVolume(value) } pub fn to_u64(&self) -> u64 { self.0 } } #[derive(Debug, Clone, PartialEq)] /// Position component. pub struct Position { pub symbol: String, pub side: Side, pub quantity: Quantity, pub average_price: TestPrice, pub current_price: TestPrice, pub unrealized_pnl: Amount, } impl Position { pub fn new(symbol: String, side: Side, quantity: Quantity, average_price: TestPrice) -> Self { Self { symbol, side, quantity, average_price, current_price: average_price.clone(), unrealized_pnl: Amount(0), } } pub fn calculate_unrealized_pnl(&mut self, current_price: &TestPrice) -> f64 { self.current_price = current_price.clone(); let price_diff = match self.side { Side::Buy => current_price.0 as f64 - self.average_price.0 as f64, Side::Sell => self.average_price.0 as f64 - current_price.0 as f64, }; let pnl = price_diff * self.quantity.0 as f64; self.unrealized_pnl = Amount(pnl as i64); pnl } pub fn notional_value(&self) -> f64 { (self.average_price.0 as f64) * (self.quantity.0 as f64) } } // ============================================================================ // Property-Based Test Strategies // ============================================================================ /// Strategy for generating valid prices (positive, finite) fn valid_price_strategy() -> impl Strategy { (0.0001f64..1_000_000.0f64) } /// Strategy for generating extreme but valid prices fn extreme_price_strategy() -> impl Strategy { prop_oneof![ Just(f64::MIN_POSITIVE), Just(f64::MAX), (0.0001f64..0.01f64), // Very small prices (100_000.0f64..1_000_000.0f64), // Very large prices ] } /// Strategy for generating quantities with edge cases fn quantity_strategy() -> impl Strategy { prop_oneof![ Just(i64::MIN), Just(i64::MAX), Just(0), Just(1), Just(-1), (-1_000_000i64..1_000_000i64), ] } /// Strategy for generating large quantities for stress testing fn large_quantity_strategy() -> impl Strategy { prop_oneof![ (1_000_000i64..i64::MAX / 2), (i64::MIN / 2..-1_000_000i64), ] } // ============================================================================ // Price Arithmetic Properties // ============================================================================ proptest! { /// Test that `price` addition is commutative: a + b = b + a #[test] fn price_addition_commutative( a in valid_price_strategy(), b in valid_price_strategy() ) { let price_a = TestPrice::from_f64(a).expect("Valid price"); let price_b = TestPrice::from_f64(b).expect("Valid price"); let sum_ab = price_a.add(&price_b); let sum_ba = price_b.add(&price_a); prop_assert_eq!(sum_ab, sum_ba); } /// Test that `price` addition is associative: (a + b) + c = a + (b + c) #[test] fn price_addition_associative( a in valid_price_strategy(), b in valid_price_strategy(), c in valid_price_strategy() ) { let price_a = TestPrice::from_f64(a).expect("Valid price"); let price_b = TestPrice::from_f64(b).expect("Valid price"); let price_c = TestPrice::from_f64(c).expect("Valid price"); let left = price_a.add(&price_b).add(&price_c); let right = price_a.add(&price_b.add(&price_c)); // Allow small floating point differences let diff = (left.to_f64() - right.to_f64()).abs(); prop_assert!(diff < 1e-10); } /// Test that `price` remains non-negative under all operations #[test] fn price_always_non_negative( a in any::(), b in any::() ) { let price_a = TestPrice::from_f64(a).expect("Valid price"); let price_b = TestPrice::from_f64(b).expect("Valid price"); prop_assert!(price_a.to_f64() >= 0.0); prop_assert!(price_b.to_f64() >= 0.0); let sum = price_a.add(&price_b); prop_assert!(sum.to_f64() >= 0.0); let product = price_a.multiply(2.0); prop_assert!(product.to_f64() >= 0.0); } /// Test `price` multiplication properties #[test] fn price_multiplication_properties( price in valid_price_strategy(), factor in -1000.0f64..1000.0f64 ) { let p = TestPrice::from_f64(price).expect("Valid price"); let result = p.multiply(factor); // Result should always be non-negative due to clamping prop_assert!(result.to_f64() >= 0.0); // If factor is positive, result should be factor * price (or 0 if negative) if factor >= 0.0 { let expected = price * factor; let diff = (result.to_f64() - expected).abs(); prop_assert!(diff < 1e-10 || expected < 0.0); // Account for clamping } // Multiplication by 0 should give 0 let zero_result = p.multiply(0.0); prop_assert_eq!(zero_result.to_f64(), 0.0); // Multiplication by 1 should give original price let identity_result = p.multiply(1.0); let diff = (identity_result.to_f64() - price).abs(); prop_assert!(diff < 1e-10); } /// Test percentage change calculations #[test] fn price_percentage_change_properties( old_price in valid_price_strategy(), new_price in valid_price_strategy() ) { let old_p = TestPrice::from_f64(old_price).expect("Valid price"); let new_p = TestPrice::from_f64(new_price).expect("Valid price"); let pct_change = new_p.percentage_change(&old_p); // Percentage change should be finite unless old_price is 0 if old_price > f64::EPSILON { prop_assert!(pct_change.is_finite()); // Verify the calculation: new = old * (1 + pct_change) let expected_new = old_price * (1.0 + pct_change); let diff = (new_price - expected_new).abs(); prop_assert!(diff < 1e-10); } // When prices are equal, percentage change should be 0 let zero_change = old_p.percentage_change(&old_p); prop_assert!(zero_change.abs() < 1e-10); } } // ============================================================================ // Quantity Arithmetic Properties // ============================================================================ proptest! { /// Test `quantity` addition with overflow protection #[test] fn quantity_addition_overflow_safe( a in quantity_strategy(), b in quantity_strategy() ) { let qty_a = Quantity::new(a); let qty_b = Quantity::new(b); let sum = qty_a.add(&qty_b); // Addition should never panic (uses saturating_add) prop_assert!(sum.to_i64() >= i64::MIN); prop_assert!(sum.to_i64() <= i64::MAX); // If no overflow would occur, result should be exact if let Some(expected) = a.checked_add(b) { prop_assert_eq!(sum.to_i64(), expected); } else { // Overflow occurred, result should be saturated if (a > 0 && b > 0) || (a > 0 && b > 0) { prop_assert_eq!(sum.to_i64(), i64::MAX); } else if a < 0 && b < 0 { prop_assert_eq!(sum.to_i64(), i64::MIN); } } } /// Test `quantity` absolute value properties #[test] fn quantity_absolute_value_properties( value in quantity_strategy() ) { let qty = Quantity::new(value); let abs_qty = qty.abs(); // Absolute value should always be non-negative prop_assert!(abs_qty.to_i64() >= 0); // |x| = x if x >= 0, -x if x < 0 if value >= 0 { prop_assert_eq!(abs_qty.to_i64(), value); } else if value > i64::MIN { // Avoid overflow on MIN prop_assert_eq!(abs_qty.to_i64(), -value); } // ||x|| = |x| (idempotent) prop_assert_eq!(abs_qty.abs().to_i64(), abs_qty.to_i64()); } /// Test `quantity` edge cases #[test] fn quantity_edge_cases( value in prop_oneof![Just(i64::MIN), Just(i64::MAX), Just(0)] ) { let qty = Quantity::new(value); // Should handle extreme values without panicking prop_assert_eq!(qty.to_i64(), value); // Test operations don't panic let _abs = qty.abs(); let _sum = qty.add(&Quantity::new(0)); } } // ============================================================================ // Position P&L Calculation Properties // ============================================================================ proptest! { /// Test `P`&`L` calculation properties for buy positions #[test] fn buy_position_pnl_properties( entry_price in valid_price_strategy(), current_price in valid_price_strategy(), quantity in 1i64..1_000_000i64 // Positive quantities for buy positions ) { let mut position = Position::new( "EURUSD".to_string(), Side::Buy, Quantity::new(quantity), TestPrice::from_f64(entry_price).expect("Valid price") ); let pnl = position.calculate_unrealized_pnl(&TestPrice::from_f64(current_price).expect("Valid price")); // P&L should equal (current_price - entry_price) * quantity let expected_pnl = (current_price - entry_price) * quantity as f64; let diff = (pnl - expected_pnl).abs(); prop_assert!(diff < 1e-10); // If current price > entry price, P&L should be positive if current_price > entry_price { prop_assert!(pnl > 0.0); } // If current price < entry price, P&L should be negative if current_price < entry_price { prop_assert!(pnl < 0.0); } // If prices are equal, P&L should be zero if (current_price - entry_price).abs() < f64::EPSILON { prop_assert!(pnl.abs() < 1e-10); } } /// Test `P`&`L` calculation properties for sell positions #[test] fn sell_position_pnl_properties( entry_price in valid_price_strategy(), current_price in valid_price_strategy(), quantity in 1i64..1_000_000i64 // Positive quantities for sell positions ) { let mut position = Position::new( "EURUSD".to_string(), Side::Sell, Quantity::new(quantity), TestPrice::from_f64(entry_price).expect("Valid price") ); let pnl = position.calculate_unrealized_pnl(&TestPrice::from_f64(current_price).expect("Valid price")); // P&L should equal (entry_price - current_price) * quantity let expected_pnl = (entry_price - current_price) * quantity as f64; let diff = (pnl - expected_pnl).abs(); prop_assert!(diff < 1e-10); // If current price < entry price, P&L should be positive if current_price < entry_price { prop_assert!(pnl > 0.0); } // If current price > entry price, P&L should be negative if current_price > entry_price { prop_assert!(pnl < 0.0); } // If prices are equal, P&L should be zero if (current_price - entry_price).abs() < f64::EPSILON { prop_assert!(pnl.abs() < 1e-10); } } /// Test position notional value calculation #[test] fn position_notional_value_properties( price in valid_price_strategy(), quantity in quantity_strategy() ) { let position = Position::new( "EURUSD".to_string(), Side::Buy, Quantity::new(quantity), TestPrice::from_f64(price).expect("Valid price") ); let notional = position.notional_value(); // Notional should equal price * |quantity| let expected = price * quantity.abs() as f64; let diff = (notional - expected).abs(); prop_assert!(diff < 1e-10); // Notional should always be non-negative prop_assert!(notional >= 0.0); // Notional should be zero if price or quantity is zero if price == 0.0 || quantity == 0 { prop_assert_eq!(notional, 0.0); } } /// Test large position `P`&`L` calculations don't overflow #[test] fn large_position_pnl_no_overflow( entry_price in valid_price_strategy(), current_price in valid_price_strategy(), quantity in large_quantity_strategy() ) { let mut position = Position::new( "EURUSD".to_string(), Side::Buy, Quantity::new(quantity), TestPrice::from_f64(entry_price).expect("Valid price") ); let pnl = position.calculate_unrealized_pnl(&TestPrice::from_f64(current_price).expect("Valid price")); // P&L calculation should not overflow to infinity prop_assert!(pnl.is_finite()); // Large quantities should still produce mathematically correct results // within floating point precision limits if quantity.abs() < 1_000_000 { let expected_pnl = (current_price - entry_price) * quantity as f64; let relative_error = if expected_pnl != 0.0 { ((pnl - expected_pnl) / expected_pnl).abs() } else { pnl.abs() }; prop_assert!(relative_error < 1e-10); } } } // ============================================================================ // Edge Case and Boundary Condition Tests // ============================================================================ proptest! { /// Test behavior with extreme `price` values #[test] fn extreme_price_handling( price in extreme_price_strategy() ) { let p = TestPrice::from_f64(price).expect("Valid price"); // Should handle extreme values gracefully prop_assert!(p.to_f64().is_finite()); prop_assert!(p.to_f64() >= 0.0); // Operations should remain stable let doubled = p.multiply(2.0); prop_assert!(doubled.to_f64().is_finite()); let added = p.add(&TestPrice::from_f64(1.0).expect("Valid price")); prop_assert!(added.to_f64().is_finite()); } /// Test precision preservation in financial calculations #[test] fn financial_precision_preservation( base_price in 1.0f64..2.0f64, pip_count in 1u32..10000u32 ) { let pip_value = 0.0001; // Standard pip for EUR/USD let price_movement = pip_count as f64 * pip_value; let entry_price = TestPrice::from_f64(base_price).expect("Valid price"); let exit_price = TestPrice::from_f64(base_price + price_movement).expect("Valid price"); // Calculate P&L for a standard lot (100,000 units) let mut position = Position::new( "EURUSD".to_string(), Side::Buy, Quantity::new(100_000), entry_price ); let pnl = position.calculate_unrealized_pnl(&exit_price); // P&L should be close to pip_count * 10 USD (for EUR/USD) let expected_pnl_usd = pip_count as f64 * 10.0; let error = (pnl - expected_pnl_usd).abs(); // Allow small error due to floating point arithmetic prop_assert!(error < 0.01, "P&L error too large: {} vs {}", pnl, expected_pnl_usd); } /// Test handling of special floating point values #[test] fn special_float_value_handling( special_value in prop_oneof![ Just(NAN), Just(INFINITY), Just(NEG_INFINITY), Just(f64::MIN), Just(f64::MAX), Just(f64::MIN_POSITIVE) ] ) { let price = TestPrice::from_f64(special_value).expect("Valid price"); // Should convert special values to safe values prop_assert!(price.to_f64().is_finite()); prop_assert!(price.to_f64() >= 0.0); // Operations should not propagate special values let result = price.multiply(1.5); prop_assert!(result.to_f64().is_finite()); prop_assert!(result.to_f64() >= 0.0); } /// Test compound operations maintain precision #[test] fn compound_operations_precision( prices in prop::collection::vec(valid_price_strategy(), 1..100) ) { // Sum all prices let mut total = TestPrice::from_f64(0.0).expect("Valid price"); let mut expected_sum = 0.0; for price_val in &prices { let price = TestPrice::from_f64(*price_val).expect("Valid price"); total = total.add(&price); expected_sum += price_val; } // Precision should be maintained within reasonable bounds let error = (total.to_f64() - expected_sum).abs(); let relative_error = if expected_sum > 0.0 { error / expected_sum } else { error }; // Allow small accumulation of floating point errors prop_assert!(relative_error < 1e-12); } } // ============================================================================ // Integration Tests for Financial Operations // ============================================================================ #[tokio::test] async fn test_realistic_trading_scenario_precision() { // Test a realistic EUR/USD trading scenario let entry_price = TestPrice::from_f64(1.1000).expect("Valid price"); let position_size = Quantity::new(100_000); // Standard lot let mut position = Position::new( "EURUSD".to_string(), Side::Buy, position_size, entry_price.clone() ); // Simulate market movements of 1, 5, 10, 20 pips let pip_movements = vec![1, 5, 10, 20]; for pips in pip_movements { let current_price = TestPrice::from_f64(1.1000 + pips as f64 * 0.0001).expect("Valid price"); let pnl = position.calculate_unrealized_pnl(¤t_price); // Each pip should be worth approximately $10 for EUR/USD standard lot let expected_pnl = pips as f64 * 10.0; let error = (pnl - expected_pnl).abs(); assert!(error < 0.01, "P&L error for {} pips: {} vs {}", pips, pnl, expected_pnl); } } #[tokio::test] async fn test_portfolio_level_precision() { // Test precision when managing multiple positions let symbols = vec!["EURUSD", "GBPUSD", "USDJPY", "USDCHF", "AUDUSD"]; let mut positions = Vec::new(); let mut total_pnl = 0.0; for (i, symbol) in symbols.into_iter().enumerate() { let entry_price = TestPrice::from_f64(1.0 + i as f64 * 0.1).expect("Valid price"); let current_price = TestPrice::from_f64(1.0 + i as f64 * 0.1 + 0.01).expect("Valid price"); // 100 pip profit each let quantity = Quantity::new(10_000 * (i as i64 + 1)); // Different sizes let mut position = Position::new( symbol.to_string(), Side::Buy, quantity, entry_price ); let pnl = position.calculate_unrealized_pnl(¤t_price); total_pnl += pnl; positions.push(position); } // Total P&L should be the sum of individual P&Ls let manual_total = positions.iter() .map(|p| p.unrealized_pnl.0 as f64) .sum::(); let error = (total_pnl - manual_total).abs(); assert!(error < 1e-10, "Portfolio P&L calculation error: {}", error); } #[test] fn test_stress_financial_calculations() { // Stress test with many small operations let base_price = TestPrice::from_f64(1.0).expect("Valid price"); let increment = TestPrice::from_f64(0.00001).expect("Valid price"); // Half pip let mut accumulated = base_price.clone(); // Perform 100,000 small additions for _ in 0..100_000 { accumulated = accumulated.add(&increment); } // Final price should be approximately 1.0 + 100,000 * 0.00001 = 2.0 let expected = 2.0; let error = (accumulated.to_f64() - expected).abs(); // Allow some accumulation of floating point errors assert!(error < 1e-8, "Stress test precision error: {}", error); } } // end mod tests