Skip to main content

rustrade_backtest/
slippage.rs

1//! Slippage models applied between a brain's signal and the simulated
2//! fill price.
3//!
4//! Slippage is modelled as a *price adjustment*: the brain decides at a
5//! reference price (the candle's `close`), the engine adjusts that price
6//! against the side of the trade, and the resulting price becomes the
7//! fill price. Buys fill higher than the reference; sells fill lower.
8
9use rustrade_core::Side;
10use serde::{Deserialize, Serialize};
11
12/// Slippage policy for the backtest engine.
13///
14/// Pluggable: future variants can include book-walk slippage (which
15/// needs an order-book replay, not just candles) or per-symbol overrides.
16///
17/// # Example
18///
19/// ```
20/// use rustrade_backtest::SlippageModel;
21/// use rustrade_core::Side;
22///
23/// // 5 bps adverse slippage applied symmetrically.
24/// let m = SlippageModel::FixedBps(5.0);
25/// let buy = m.apply(Side::Buy, 100.0);
26/// let sell = m.apply(Side::Sell, 100.0);
27/// assert!((buy - 100.05).abs() < 1e-9);
28/// assert!((sell - 99.95).abs() < 1e-9);
29/// ```
30#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
31pub enum SlippageModel {
32    /// No slippage — fills land exactly at the reference price.
33    #[default]
34    Zero,
35    /// Fixed slippage in basis points applied symmetrically against the
36    /// trade side. `5.0` bps = 0.05% adverse slippage.
37    FixedBps(f64),
38}
39
40impl SlippageModel {
41    /// Adjust `reference_price` for an order of the given side.
42    /// Returns the simulated fill price.
43    pub fn apply(self, side: Side, reference_price: f64) -> f64 {
44        match self {
45            Self::Zero => reference_price,
46            Self::FixedBps(bps) => {
47                let factor = 1.0 + (bps / 10_000.0) * direction_sign(side);
48                reference_price * factor
49            }
50        }
51    }
52}
53
54/// `+1` for buys (slippage adds to fill price), `-1` for sells
55/// (slippage subtracts).
56fn direction_sign(side: Side) -> f64 {
57    match side {
58        Side::Buy => 1.0,
59        Side::Sell => -1.0,
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn zero_returns_reference() {
69        assert_eq!(SlippageModel::Zero.apply(Side::Buy, 100.0), 100.0);
70        assert_eq!(SlippageModel::Zero.apply(Side::Sell, 100.0), 100.0);
71    }
72
73    #[test]
74    fn fixed_bps_buy_adds() {
75        let fill = SlippageModel::FixedBps(10.0).apply(Side::Buy, 100.0);
76        // 10 bps = 0.1% → 100 * 1.001 = 100.1
77        assert!((fill - 100.1).abs() < 1e-9);
78    }
79
80    #[test]
81    fn fixed_bps_sell_subtracts() {
82        let fill = SlippageModel::FixedBps(10.0).apply(Side::Sell, 100.0);
83        assert!((fill - 99.9).abs() < 1e-9);
84    }
85
86    #[test]
87    fn fixed_bps_symmetric_about_reference() {
88        let buy = SlippageModel::FixedBps(25.0).apply(Side::Buy, 1_000.0);
89        let sell = SlippageModel::FixedBps(25.0).apply(Side::Sell, 1_000.0);
90        assert!(((buy - 1_000.0) + (sell - 1_000.0)).abs() < 1e-9);
91    }
92}