Files
raptorbt/src/execution/slippage.rs
T
vatsal 0f032dd39e Fix CI workflow: use correct rust-toolchain action (#1)
* Fix CI workflow: use correct rust-toolchain action

* Fix CI: resolve Rust compilation and formatting issues
- Fix rust-toolchain action name in CI workflow
- Add missing Direction import in test modules
- Add missing entry_fees argument to open_position test calls
- Comment out nightly-only rustfmt options
- Auto-format code with cargo fmt
2026-01-28 15:38:31 +05:30

205 lines
6.4 KiB
Rust

//! 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>,
) -> 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<f64>,
) -> 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);
}
}