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}