//! Slippage models for realistic trade execution. use crate::core::types::{Direction, Price}; /// Slippage model for simulating execution price deviation. #[derive(Debug, Clone)] pub enum SlippageModel { /// No slippage. None, /// Fixed percentage slippage. Percentage(f64), /// Fixed point slippage. Fixed(f64), /// Volume-based slippage (higher volume = lower slippage). VolumeBased { base: f64, volume_factor: f64 }, /// Spread-based slippage (uses bid-ask spread). SpreadBased { half_spread: f64 }, } impl Default for SlippageModel { fn default() -> Self { SlippageModel::None } } impl SlippageModel { /// Create a new percentage slippage model. pub fn percentage(rate: f64) -> Self { SlippageModel::Percentage(rate) } /// Create a new fixed slippage model. pub fn fixed(points: f64) -> Self { SlippageModel::Fixed(points) } /// Create a volume-based slippage model. pub fn volume_based(base: f64, volume_factor: f64) -> Self { SlippageModel::VolumeBased { base, volume_factor } } /// Calculate slippage for a trade. /// /// For long entries and short exits: slippage is ADDED to price (pay more/receive less) /// For short entries and long exits: slippage is SUBTRACTED from price /// /// # Arguments /// * `price` - Base execution price /// * `direction` - Trade direction /// * `is_entry` - Whether this is an entry or exit /// * `volume` - Optional volume for volume-based models /// /// # Returns /// Slippage amount (positive = unfavorable) pub fn calculate( &self, price: Price, direction: Direction, is_entry: bool, volume: Option, ) -> f64 { let base_slippage = match self { SlippageModel::None => 0.0, SlippageModel::Percentage(rate) => price * rate, SlippageModel::Fixed(points) => *points, SlippageModel::VolumeBased { base, volume_factor } => { if let Some(vol) = volume { if vol > 0.0 { base * (1.0 / (1.0 + vol * volume_factor)) } else { *base } } else { *base } } SlippageModel::SpreadBased { half_spread } => *half_spread, }; // Determine sign based on trade type // Long entry: pay higher price (positive slippage) // Long exit: receive lower price (negative slippage) // Short entry: receive higher price (negative slippage means worse) // Short exit: pay higher price match (direction, is_entry) { (Direction::Long, true) => base_slippage, // Pay more (Direction::Long, false) => -base_slippage, // Receive less (Direction::Short, true) => -base_slippage, // Receive less (Direction::Short, false) => base_slippage, // Pay more } } /// Apply slippage to get execution price. /// /// # Arguments /// * `price` - Base price /// * `direction` - Trade direction /// * `is_entry` - Whether this is an entry or exit /// * `volume` - Optional volume for volume-based models /// /// # Returns /// Execution price after slippage pub fn apply( &self, price: Price, direction: Direction, is_entry: bool, volume: Option, ) -> Price { price + self.calculate(price, direction, is_entry, volume) } } /// Market impact model for large orders. #[derive(Debug, Clone)] pub struct MarketImpact { /// Temporary impact coefficient. pub temporary_impact: f64, /// Permanent impact coefficient. pub permanent_impact: f64, /// Average daily volume for normalization. pub avg_daily_volume: f64, } impl MarketImpact { /// Create a new market impact model. pub fn new(temporary: f64, permanent: f64, adv: f64) -> Self { Self { temporary_impact: temporary, permanent_impact: permanent, avg_daily_volume: adv } } /// Calculate market impact for an order. /// /// Uses simplified square-root model: impact = sigma * sqrt(Q / ADV) /// /// # Arguments /// * `order_size` - Number of shares/contracts /// * `price` - Current price /// * `volatility` - Price volatility (sigma) /// /// # Returns /// Total market impact in price terms pub fn calculate(&self, order_size: f64, price: Price, volatility: f64) -> f64 { if self.avg_daily_volume <= 0.0 { return 0.0; } let participation_rate = order_size / self.avg_daily_volume; let sqrt_participation = participation_rate.sqrt(); let temporary = self.temporary_impact * volatility * price * sqrt_participation; let permanent = self.permanent_impact * volatility * price * participation_rate; temporary + permanent } } #[cfg(test)] mod tests { use super::*; #[test] fn test_percentage_slippage() { let slip = SlippageModel::percentage(0.001); // Long entry: pay more let entry_slip = slip.calculate(100.0, Direction::Long, true, None); assert!((entry_slip - 0.1).abs() < 1e-10); // Long exit: receive less let exit_slip = slip.calculate(100.0, Direction::Long, false, None); assert!((exit_slip - (-0.1)).abs() < 1e-10); } #[test] fn test_apply_slippage() { let slip = SlippageModel::percentage(0.001); // Long entry at 100 should pay 100.1 let entry_price = slip.apply(100.0, Direction::Long, true, None); assert!((entry_price - 100.1).abs() < 1e-10); // Long exit at 100 should receive 99.9 let exit_price = slip.apply(100.0, Direction::Long, false, None); assert!((exit_price - 99.9).abs() < 1e-10); } #[test] fn test_no_slippage() { let slip = SlippageModel::None; let result = slip.apply(100.0, Direction::Long, true, None); assert!((result - 100.0).abs() < 1e-10); } #[test] fn test_volume_based_slippage() { let slip = SlippageModel::volume_based(0.1, 0.0001); // High volume should have lower slippage let high_vol = slip.calculate(100.0, Direction::Long, true, Some(100000.0)); let low_vol = slip.calculate(100.0, Direction::Long, true, Some(1000.0)); assert!(high_vol < low_vol); } }