1use rand::{Rng, SeedableRng, rngs::Xoshiro256PlusPlus};
14use rust_decimal::Decimal;
15use rustrade_instrument::Side;
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18
19#[derive(
64 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
65)]
66#[serde(try_from = "SlippageRepr", into = "SlippageRepr")]
67pub struct SlippageConfig(SlippageRepr);
68
69#[derive(
71 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
72)]
73enum SlippageRepr {
74 #[default]
75 None,
76 Fixed {
77 #[serde(with = "rust_decimal::serde::str")]
78 rate: Decimal,
79 },
80 Uniform {
81 #[serde(with = "rust_decimal::serde::str")]
82 low: Decimal,
83 #[serde(with = "rust_decimal::serde::str")]
84 high: Decimal,
85 seed: u64,
86 },
87}
88
89impl SlippageConfig {
90 pub const fn none() -> Self {
92 Self(SlippageRepr::None)
93 }
94
95 pub fn fixed(rate: Decimal) -> Result<Self, SlippageConfigError> {
100 Self::try_from(SlippageRepr::Fixed { rate })
101 }
102
103 pub fn uniform(low: Decimal, high: Decimal, seed: u64) -> Result<Self, SlippageConfigError> {
109 Self::try_from(SlippageRepr::Uniform { low, high, seed })
110 }
111
112 pub fn low(&self) -> Decimal {
115 match self.0 {
116 SlippageRepr::None => Decimal::ZERO,
117 SlippageRepr::Fixed { rate } => rate,
118 SlippageRepr::Uniform { low, .. } => low,
119 }
120 }
121
122 pub fn high(&self) -> Decimal {
125 match self.0 {
126 SlippageRepr::None => Decimal::ZERO,
127 SlippageRepr::Fixed { rate } => rate,
128 SlippageRepr::Uniform { high, .. } => high,
129 }
130 }
131
132 pub fn seed(&self) -> Option<u64> {
134 match self.0 {
135 SlippageRepr::Uniform { seed, .. } => Some(seed),
136 SlippageRepr::None | SlippageRepr::Fixed { .. } => None,
137 }
138 }
139}
140
141impl TryFrom<SlippageRepr> for SlippageConfig {
142 type Error = SlippageConfigError;
143
144 fn try_from(repr: SlippageRepr) -> Result<Self, Self::Error> {
145 let (low, high) = match repr {
146 SlippageRepr::None => return Ok(Self(repr)),
147 SlippageRepr::Fixed { rate } => (rate, rate),
148 SlippageRepr::Uniform { low, high, .. } => (low, high),
149 };
150 if low.is_sign_negative() && !low.is_zero() {
151 return Err(SlippageConfigError::Negative(low));
152 }
153 if high >= Decimal::ONE {
154 return Err(SlippageConfigError::NotBelowOne(high));
155 }
156 if low > high {
157 return Err(SlippageConfigError::Inverted { low, high });
158 }
159 Ok(Self(repr))
160 }
161}
162
163impl From<SlippageConfig> for SlippageRepr {
164 fn from(config: SlippageConfig) -> Self {
165 config.0
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
171pub enum SlippageConfigError {
172 #[error("slippage rate {0} is negative; slippage is adverse, so a rate is at least 0")]
174 Negative(Decimal),
175 #[error("slippage rate {0} is not below 1, so a sell would fill at or below zero")]
177 NotBelowOne(Decimal),
178 #[error("slippage range is inverted: low {low} is above high {high}")]
180 Inverted { low: Decimal, high: Decimal },
181}
182
183pub const RATE_DECIMAL_PLACES: u32 = 12;
185
186#[derive(Debug, Clone, PartialEq, Eq)]
192pub(crate) struct Slippage {
193 config: SlippageConfig,
194 rng: Xoshiro256PlusPlus,
195}
196
197impl Slippage {
198 pub(crate) fn new(config: SlippageConfig) -> Self {
200 Self {
201 config,
202 rng: Xoshiro256PlusPlus::seed_from_u64(config.seed().unwrap_or_default()),
204 }
205 }
206
207 pub(crate) fn apply(&mut self, side: Side, reference: Decimal) -> Decimal {
214 let rate = self.draw();
215 if rate.is_zero() {
216 return reference;
217 }
218 match side {
219 Side::Buy => reference * (Decimal::ONE + rate),
220 Side::Sell => reference * (Decimal::ONE - rate),
221 }
222 }
223
224 fn draw(&mut self) -> Decimal {
226 let (low, high) = (self.config.low(), self.config.high());
227 if low == high {
228 return low;
229 }
230 let unit = Decimal::from(self.rng.next_u64()) / Decimal::from(u64::MAX);
235 (low + (high - low) * unit)
238 .round_dp(RATE_DECIMAL_PLACES)
239 .clamp(low, high)
240 }
241}
242
243#[cfg(test)]
244#[allow(clippy::unwrap_used)] mod tests {
246 use super::*;
247
248 fn d(s: &str) -> Decimal {
249 s.parse().unwrap()
250 }
251
252 #[test]
253 fn none_is_the_default_and_leaves_every_price_alone() {
254 let mut slippage = Slippage::new(SlippageConfig::default());
255 assert_eq!(SlippageConfig::default(), SlippageConfig::none());
256 assert_eq!(slippage.apply(Side::Buy, d("100.5")), d("100.5"));
257 assert_eq!(slippage.apply(Side::Sell, d("100.5")), d("100.5"));
258 }
259
260 #[test]
261 fn a_fixed_rate_moves_a_buy_up_and_a_sell_down() {
262 let mut slippage = Slippage::new(SlippageConfig::fixed(d("0.01")).unwrap());
263 assert_eq!(slippage.apply(Side::Buy, d("200")), d("202"));
264 assert_eq!(slippage.apply(Side::Sell, d("200")), d("198"));
265 }
266
267 #[test]
268 fn several_percent_is_accepted() {
269 let mut slippage = Slippage::new(SlippageConfig::fixed(d("0.05")).unwrap());
270 assert_eq!(slippage.apply(Side::Sell, d("100")), d("95"));
271 }
272
273 #[test]
276 fn a_range_of_one_rate_makes_no_draw() {
277 let mut ranged = Slippage::new(SlippageConfig::uniform(d("0.02"), d("0.02"), 9).unwrap());
278 let untouched = ranged.rng.clone();
279 assert_eq!(ranged.apply(Side::Buy, d("50")), d("51"));
280 assert_eq!(
281 ranged.rng, untouched,
282 "a single-rate range drew from its stream"
283 );
284
285 let mut zero =
286 Slippage::new(SlippageConfig::uniform(Decimal::ZERO, Decimal::ZERO, 9).unwrap());
287 assert_eq!(zero.apply(Side::Buy, d("50.25")), d("50.25"));
288 }
289
290 #[test]
291 fn every_uniform_draw_lies_inside_its_range() {
292 let (low, high) = (d("0.01"), d("0.025"));
293 let mut slippage = Slippage::new(SlippageConfig::uniform(low, high, 7).unwrap());
294 for _ in 0..10_000 {
295 let rate = slippage.draw();
296 assert!(
297 low <= rate && rate <= high,
298 "{rate} outside [{low}, {high}]"
299 );
300 }
301 }
302
303 #[test]
304 fn the_same_seed_gives_the_same_prices_and_another_seed_does_not() {
305 let config = |seed| SlippageConfig::uniform(d("0.01"), d("0.025"), seed).unwrap();
306 let prices = |seed| {
307 let mut slippage = Slippage::new(config(seed));
308 (0..32)
309 .map(|i| {
310 let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
311 slippage.apply(side, d("100"))
312 })
313 .collect::<Vec<_>>()
314 };
315 assert_eq!(prices(42), prices(42));
316 assert_ne!(prices(42), prices(43));
317 }
318
319 #[test]
322 fn a_seed_gives_the_same_rates_in_every_version() {
323 let mut slippage =
324 Slippage::new(SlippageConfig::uniform(d("0.01"), d("0.025"), 42).unwrap());
325 let rates = (0..3).map(|_| slippage.draw()).collect::<Vec<_>>();
326 assert_eq!(rates, GOLDEN_RATES.map(d).to_vec());
327 }
328
329 #[test]
331 fn a_seed_gives_the_same_prices_in_every_version() {
332 let mut slippage =
333 Slippage::new(SlippageConfig::uniform(d("0.01"), d("0.025"), 42).unwrap());
334 let prices = [Side::Buy, Side::Sell, Side::Buy]
335 .map(|side| slippage.apply(side, d("49000.5")).to_string());
336 assert_eq!(prices, GOLDEN_PRICES);
337 }
338
339 const GOLDEN_PRICES: [&str; 3] = [
340 "50089.0253889615885",
341 "48276.1591443931995",
342 "50213.6745928332615",
343 ];
344
345 #[test]
347 fn a_rounded_draw_stays_inside_a_finely_given_range() {
348 let (low, high) = (d("0.0100000000004"), d("0.0100000000006"));
349 let mut slippage = Slippage::new(SlippageConfig::uniform(low, high, 1).unwrap());
350 for _ in 0..1_000 {
351 let rate = slippage.draw();
352 assert!(
353 low <= rate && rate <= high,
354 "{rate} outside [{low}, {high}]"
355 );
356 }
357 }
358
359 const GOLDEN_RATES: [&str; 3] = ["0.022214577177", "0.014782315601", "0.024758412523"];
363
364 #[test]
365 fn the_constructors_refuse_a_rate_the_venue_cannot_apply() {
366 assert_eq!(
367 SlippageConfig::fixed(d("-0.01")),
368 Err(SlippageConfigError::Negative(d("-0.01")))
369 );
370 assert_eq!(
371 SlippageConfig::fixed(Decimal::ONE),
372 Err(SlippageConfigError::NotBelowOne(Decimal::ONE))
373 );
374 assert_eq!(
375 SlippageConfig::uniform(d("0.02"), d("0.01"), 0),
376 Err(SlippageConfigError::Inverted {
377 low: d("0.02"),
378 high: d("0.01")
379 })
380 );
381 assert!(
382 SlippageConfig::fixed(d("-0")).is_ok(),
383 "negative zero is zero"
384 );
385 assert!(SlippageConfig::fixed(d("0.999")).is_ok());
386 }
387
388 #[test]
389 fn deserialising_validates_and_round_trips() {
390 let config = SlippageConfig::uniform(d("0.01"), d("0.025"), 42).unwrap();
391 let json = serde_json::to_string(&config).unwrap();
392 assert_eq!(
393 json,
394 r#"{"Uniform":{"low":"0.01","high":"0.025","seed":42}}"#
395 );
396 assert_eq!(
397 serde_json::from_str::<SlippageConfig>(&json).unwrap(),
398 config
399 );
400
401 assert_eq!(
402 serde_json::from_str::<SlippageConfig>(r#"{"Fixed":{"rate":"0.01"}}"#).unwrap(),
403 SlippageConfig::fixed(d("0.01")).unwrap()
404 );
405 assert_eq!(
406 serde_json::from_str::<SlippageConfig>(r#""None""#).unwrap(),
407 SlippageConfig::none()
408 );
409 let refused = serde_json::from_str::<SlippageConfig>(r#"{"Fixed":{"rate":"1.5"}}"#);
410 assert!(refused.unwrap_err().to_string().contains("not below 1"));
411 }
412}