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>
710 lines
23 KiB
Rust
710 lines
23 KiB
Rust
//! 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<Value = f64> {
|
|
(0.0001f64..1_000_000.0f64)
|
|
}
|
|
|
|
/// Strategy for generating extreme but valid prices
|
|
fn extreme_price_strategy() -> impl Strategy<Value = f64> {
|
|
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<Value = i64> {
|
|
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<Value = i64> {
|
|
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::<f64>(),
|
|
b in any::<f64>()
|
|
) {
|
|
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::<f64>();
|
|
|
|
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
|