|
|
|
|
@@ -15,19 +15,19 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
// Test Types - Simplified versions for property testing
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
struct Price(u64); // Fixed-point representation, 8 decimals
|
|
|
|
|
struct TestPrice(u64); // Fixed-point representation, 8 decimals
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
struct Quantity(u64);
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
struct Volume(u64);
|
|
|
|
|
struct TestVolume(u64);
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
struct Amount(i64);
|
|
|
|
|
|
|
|
|
|
// Implementations
|
|
|
|
|
impl Price {
|
|
|
|
|
impl TestPrice {
|
|
|
|
|
pub fn new(value: f64) -> Self {
|
|
|
|
|
// Convert to fixed-point representation matching production types
|
|
|
|
|
let decimal_value = Decimal::from_f64(value).unwrap_or(Decimal::ZERO);
|
|
|
|
|
@@ -40,22 +40,22 @@ impl Price {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn from_f64(value: f64) -> Self {
|
|
|
|
|
Price::new(value.max(0.0)) // Clamp to non-negative
|
|
|
|
|
TestTestPrice::new(value.max(0.0)) // Clamp to non-negative
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn to_f64(&self) -> f64 {
|
|
|
|
|
self.value()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn add(&self, other: &Price) -> Price {
|
|
|
|
|
Price::from_f64(self.value().expect("Valid price") + other.value())
|
|
|
|
|
pub fn add(&self, other: &TestPrice) -> TestPrice {
|
|
|
|
|
TestTestPrice::from_f64(self.value() + other.value())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn multiply(&self, factor: f64) -> Price {
|
|
|
|
|
Price::from_f64(self.value().expect("Valid price") * factor)
|
|
|
|
|
pub fn multiply(&self, factor: f64) -> TestPrice {
|
|
|
|
|
TestTestPrice::from_f64(self.value() * factor)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn percentage_change(&self, old_price: &Price) -> f64 {
|
|
|
|
|
pub fn percentage_change(&self, old_price: &TestPrice) -> f64 {
|
|
|
|
|
let old_val = old_price.value();
|
|
|
|
|
if old_val == 0.0 {
|
|
|
|
|
0.0
|
|
|
|
|
@@ -88,11 +88,11 @@ impl Quantity {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Volume {
|
|
|
|
|
impl TestVolume {
|
|
|
|
|
pub fn new(value: u64) -> Self {
|
|
|
|
|
Volume(value)
|
|
|
|
|
TestVolume(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn to_u64(&self) -> u64 {
|
|
|
|
|
self.0
|
|
|
|
|
}
|
|
|
|
|
@@ -104,13 +104,13 @@ pub struct Position {
|
|
|
|
|
pub symbol: String,
|
|
|
|
|
pub side: Side,
|
|
|
|
|
pub quantity: Quantity,
|
|
|
|
|
pub average_price: Price,
|
|
|
|
|
pub current_price: Price,
|
|
|
|
|
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: Price) -> Self {
|
|
|
|
|
pub fn new(symbol: String, side: Side, quantity: Quantity, average_price: TestPrice) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
symbol,
|
|
|
|
|
side,
|
|
|
|
|
@@ -121,7 +121,7 @@ impl Position {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn calculate_unrealized_pnl(&mut self, current_price: &Price) -> f64 {
|
|
|
|
|
pub fn calculate_unrealized_pnl(&mut self, current_price: &TestPrice) -> f64 {
|
|
|
|
|
self.current_price = current_price.clone();
|
|
|
|
|
|
|
|
|
|
let price_diff = match self.side {
|
|
|
|
|
@@ -189,8 +189,8 @@ proptest! {
|
|
|
|
|
a in valid_price_strategy(),
|
|
|
|
|
b in valid_price_strategy()
|
|
|
|
|
) {
|
|
|
|
|
let price_a = Price::from_f64(a).expect("Valid price");
|
|
|
|
|
let price_b = Price::from_f64(b).expect("Valid price");
|
|
|
|
|
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);
|
|
|
|
|
@@ -205,9 +205,9 @@ proptest! {
|
|
|
|
|
b in valid_price_strategy(),
|
|
|
|
|
c in valid_price_strategy()
|
|
|
|
|
) {
|
|
|
|
|
let price_a = Price::from_f64(a).expect("Valid price");
|
|
|
|
|
let price_b = Price::from_f64(b).expect("Valid price");
|
|
|
|
|
let price_c = Price::from_f64(c).expect("Valid price");
|
|
|
|
|
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));
|
|
|
|
|
@@ -223,8 +223,8 @@ proptest! {
|
|
|
|
|
a in any::<f64>(),
|
|
|
|
|
b in any::<f64>()
|
|
|
|
|
) {
|
|
|
|
|
let price_a = Price::from_f64(a).expect("Valid price");
|
|
|
|
|
let price_b = Price::from_f64(b).expect("Valid price");
|
|
|
|
|
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);
|
|
|
|
|
@@ -242,7 +242,7 @@ proptest! {
|
|
|
|
|
price in valid_price_strategy(),
|
|
|
|
|
factor in -1000.0f64..1000.0f64
|
|
|
|
|
) {
|
|
|
|
|
let p = Price::from_f64(price).expect("Valid price");
|
|
|
|
|
let p = TestPrice::from_f64(price).expect("Valid price");
|
|
|
|
|
let result = p.multiply(factor);
|
|
|
|
|
|
|
|
|
|
// Result should always be non-negative due to clamping
|
|
|
|
|
@@ -271,8 +271,8 @@ proptest! {
|
|
|
|
|
old_price in valid_price_strategy(),
|
|
|
|
|
new_price in valid_price_strategy()
|
|
|
|
|
) {
|
|
|
|
|
let old_p = Price::from_f64(old_price).expect("Valid price");
|
|
|
|
|
let new_p = Price::from_f64(new_price).expect("Valid price");
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
@@ -379,10 +379,10 @@ proptest! {
|
|
|
|
|
"EURUSD".to_string(),
|
|
|
|
|
Side::Buy,
|
|
|
|
|
Quantity::new(quantity),
|
|
|
|
|
Price::from_f64(entry_price).expect("Valid price")
|
|
|
|
|
TestPrice::from_f64(entry_price).expect("Valid price")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let pnl = position.calculate_unrealized_pnl(&Price::from_f64(current_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;
|
|
|
|
|
@@ -416,10 +416,10 @@ proptest! {
|
|
|
|
|
"EURUSD".to_string(),
|
|
|
|
|
Side::Sell,
|
|
|
|
|
Quantity::new(quantity),
|
|
|
|
|
Price::from_f64(entry_price).expect("Valid price")
|
|
|
|
|
TestPrice::from_f64(entry_price).expect("Valid price")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let pnl = position.calculate_unrealized_pnl(&Price::from_f64(current_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;
|
|
|
|
|
@@ -452,7 +452,7 @@ proptest! {
|
|
|
|
|
"EURUSD".to_string(),
|
|
|
|
|
Side::Buy,
|
|
|
|
|
Quantity::new(quantity),
|
|
|
|
|
Price::from_f64(price).expect("Valid price")
|
|
|
|
|
TestPrice::from_f64(price).expect("Valid price")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let notional = position.notional_value();
|
|
|
|
|
@@ -482,10 +482,10 @@ proptest! {
|
|
|
|
|
"EURUSD".to_string(),
|
|
|
|
|
Side::Buy,
|
|
|
|
|
Quantity::new(quantity),
|
|
|
|
|
Price::from_f64(entry_price).expect("Valid price")
|
|
|
|
|
TestPrice::from_f64(entry_price).expect("Valid price")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let pnl = position.calculate_unrealized_pnl(&Price::from_f64(current_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());
|
|
|
|
|
@@ -514,7 +514,7 @@ proptest! {
|
|
|
|
|
fn extreme_price_handling(
|
|
|
|
|
price in extreme_price_strategy()
|
|
|
|
|
) {
|
|
|
|
|
let p = Price::from_f64(price).expect("Valid price");
|
|
|
|
|
let p = TestPrice::from_f64(price).expect("Valid price");
|
|
|
|
|
|
|
|
|
|
// Should handle extreme values gracefully
|
|
|
|
|
prop_assert!(p.to_f64().is_finite());
|
|
|
|
|
@@ -524,7 +524,7 @@ proptest! {
|
|
|
|
|
let doubled = p.multiply(2.0);
|
|
|
|
|
prop_assert!(doubled.to_f64().is_finite());
|
|
|
|
|
|
|
|
|
|
let added = p.add(&Price::from_f64(1.0).expect("Valid price"));
|
|
|
|
|
let added = p.add(&TestPrice::from_f64(1.0).expect("Valid price"));
|
|
|
|
|
prop_assert!(added.to_f64().is_finite());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -537,8 +537,8 @@ proptest! {
|
|
|
|
|
let pip_value = 0.0001; // Standard pip for EUR/USD
|
|
|
|
|
let price_movement = pip_count as f64 * pip_value;
|
|
|
|
|
|
|
|
|
|
let entry_price = Price::from_f64(base_price).expect("Valid price");
|
|
|
|
|
let exit_price = Price::from_f64(base_price + price_movement).expect("Valid price");
|
|
|
|
|
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(
|
|
|
|
|
@@ -570,7 +570,7 @@ proptest! {
|
|
|
|
|
Just(f64::MIN_POSITIVE)
|
|
|
|
|
]
|
|
|
|
|
) {
|
|
|
|
|
let price = Price::from_f64(special_value).expect("Valid price");
|
|
|
|
|
let price = TestPrice::from_f64(special_value).expect("Valid price");
|
|
|
|
|
|
|
|
|
|
// Should convert special values to safe values
|
|
|
|
|
prop_assert!(price.to_f64().is_finite());
|
|
|
|
|
@@ -588,11 +588,11 @@ proptest! {
|
|
|
|
|
prices in prop::collection::vec(valid_price_strategy(), 1..100)
|
|
|
|
|
) {
|
|
|
|
|
// Sum all prices
|
|
|
|
|
let mut total = Price::from_f64(0.0).expect("Valid price");
|
|
|
|
|
let mut total = TestPrice::from_f64(0.0).expect("Valid price");
|
|
|
|
|
let mut expected_sum = 0.0;
|
|
|
|
|
|
|
|
|
|
for price_val in &prices {
|
|
|
|
|
let price = Price::from_f64(*price_val).expect("Valid price");
|
|
|
|
|
let price = TestPrice::from_f64(*price_val).expect("Valid price");
|
|
|
|
|
total = total.add(&price);
|
|
|
|
|
expected_sum += price_val;
|
|
|
|
|
}
|
|
|
|
|
@@ -617,7 +617,7 @@ proptest! {
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_realistic_trading_scenario_precision() {
|
|
|
|
|
// Test a realistic EUR/USD trading scenario
|
|
|
|
|
let entry_price = Price::from_f64(1.1000).expect("Valid price");
|
|
|
|
|
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(
|
|
|
|
|
@@ -631,7 +631,7 @@ async fn test_realistic_trading_scenario_precision() {
|
|
|
|
|
let pip_movements = vec![1, 5, 10, 20];
|
|
|
|
|
|
|
|
|
|
for pips in pip_movements {
|
|
|
|
|
let current_price = Price::from_f64(1.1000 + pips as f64 * 0.0001).expect("Valid price");
|
|
|
|
|
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
|
|
|
|
|
@@ -650,8 +650,8 @@ async fn test_portfolio_level_precision() {
|
|
|
|
|
let mut total_pnl = 0.0;
|
|
|
|
|
|
|
|
|
|
for (i, symbol) in symbols.iter().enumerate() {
|
|
|
|
|
let entry_price = Price::from_f64(1.0 + i as f64 * 0.1).expect("Valid price");
|
|
|
|
|
let current_price = Price::from_f64(1.0 + i as f64 * 0.1 + 0.01).expect("Valid price"); // 100 pip profit each
|
|
|
|
|
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(
|
|
|
|
|
@@ -679,8 +679,8 @@ async fn test_portfolio_level_precision() {
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_stress_financial_calculations() {
|
|
|
|
|
// Stress test with many small operations
|
|
|
|
|
let base_price = Price::from_f64(1.0).expect("Valid price");
|
|
|
|
|
let increment = Price::from_f64(0.00001).expect("Valid price"); // Half pip
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|