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 draws: u64,
199}
200
201impl Slippage {
202 pub(crate) fn new(config: SlippageConfig) -> Self {
204 Self {
205 config,
206 rng: Xoshiro256PlusPlus::seed_from_u64(config.seed().unwrap_or_default()),
208 draws: 0,
209 }
210 }
211
212 pub(crate) fn resumed(config: SlippageConfig, draws: u64) -> Self {
217 let mut slippage = Self::new(config);
218 for _ in 0..draws {
219 slippage.rng.next_u64();
220 }
221 slippage.draws = draws;
222 slippage
223 }
224
225 pub(crate) fn draws(&self) -> u64 {
227 self.draws
228 }
229
230 pub(crate) fn apply(&mut self, side: Side, reference: Decimal) -> Decimal {
237 let rate = self.draw();
238 if rate.is_zero() {
239 return reference;
240 }
241 match side {
242 Side::Buy => reference * (Decimal::ONE + rate),
243 Side::Sell => reference * (Decimal::ONE - rate),
244 }
245 }
246
247 fn draw(&mut self) -> Decimal {
249 let (low, high) = (self.config.low(), self.config.high());
250 if low == high {
251 return low;
252 }
253 self.draws += 1;
258 let unit = Decimal::from(self.rng.next_u64()) / Decimal::from(u64::MAX);
259 (low + (high - low) * unit)
262 .round_dp(RATE_DECIMAL_PLACES)
263 .clamp(low, high)
264 }
265}
266
267#[cfg(test)]
268#[allow(clippy::unwrap_used)] mod tests {
270 use super::*;
271
272 fn d(s: &str) -> Decimal {
273 s.parse().unwrap()
274 }
275
276 #[test]
277 fn none_is_the_default_and_leaves_every_price_alone() {
278 let mut slippage = Slippage::new(SlippageConfig::default());
279 assert_eq!(SlippageConfig::default(), SlippageConfig::none());
280 assert_eq!(slippage.apply(Side::Buy, d("100.5")), d("100.5"));
281 assert_eq!(slippage.apply(Side::Sell, d("100.5")), d("100.5"));
282 }
283
284 #[test]
285 fn a_fixed_rate_moves_a_buy_up_and_a_sell_down() {
286 let mut slippage = Slippage::new(SlippageConfig::fixed(d("0.01")).unwrap());
287 assert_eq!(slippage.apply(Side::Buy, d("200")), d("202"));
288 assert_eq!(slippage.apply(Side::Sell, d("200")), d("198"));
289 }
290
291 #[test]
292 fn several_percent_is_accepted() {
293 let mut slippage = Slippage::new(SlippageConfig::fixed(d("0.05")).unwrap());
294 assert_eq!(slippage.apply(Side::Sell, d("100")), d("95"));
295 }
296
297 #[test]
300 fn a_range_of_one_rate_makes_no_draw() {
301 let mut ranged = Slippage::new(SlippageConfig::uniform(d("0.02"), d("0.02"), 9).unwrap());
302 let untouched = ranged.rng.clone();
303 assert_eq!(ranged.apply(Side::Buy, d("50")), d("51"));
304 assert_eq!(
305 ranged.rng, untouched,
306 "a single-rate range drew from its stream"
307 );
308
309 let mut zero =
310 Slippage::new(SlippageConfig::uniform(Decimal::ZERO, Decimal::ZERO, 9).unwrap());
311 assert_eq!(zero.apply(Side::Buy, d("50.25")), d("50.25"));
312 }
313
314 #[test]
315 fn every_uniform_draw_lies_inside_its_range() {
316 let (low, high) = (d("0.01"), d("0.025"));
317 let mut slippage = Slippage::new(SlippageConfig::uniform(low, high, 7).unwrap());
318 for _ in 0..10_000 {
319 let rate = slippage.draw();
320 assert!(
321 low <= rate && rate <= high,
322 "{rate} outside [{low}, {high}]"
323 );
324 }
325 }
326
327 #[test]
328 fn the_same_seed_gives_the_same_prices_and_another_seed_does_not() {
329 let config = |seed| SlippageConfig::uniform(d("0.01"), d("0.025"), seed).unwrap();
330 let prices = |seed| {
331 let mut slippage = Slippage::new(config(seed));
332 (0..32)
333 .map(|i| {
334 let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
335 slippage.apply(side, d("100"))
336 })
337 .collect::<Vec<_>>()
338 };
339 assert_eq!(prices(42), prices(42));
340 assert_ne!(prices(42), prices(43));
341 }
342
343 #[test]
346 fn a_resumed_stream_continues_where_its_draws_left_off() {
347 let config = SlippageConfig::uniform(d("0.01"), d("0.025"), 7).unwrap();
348 let mut original = Slippage::new(config);
349 for _ in 0..5 {
350 original.apply(Side::Buy, d("100"));
351 }
352 assert_eq!(original.draws(), 5);
353
354 let mut resumed = Slippage::resumed(config, original.draws());
355 assert_eq!(resumed, original);
356 assert_eq!(
357 resumed.apply(Side::Sell, d("100")),
358 original.apply(Side::Sell, d("100"))
359 );
360
361 let mut fixed = Slippage::new(SlippageConfig::fixed(d("0.01")).unwrap());
362 fixed.apply(Side::Buy, d("100"));
363 assert_eq!(fixed.draws(), 0);
364 }
365
366 #[test]
369 fn a_seed_gives_the_same_rates_in_every_version() {
370 let mut slippage =
371 Slippage::new(SlippageConfig::uniform(d("0.01"), d("0.025"), 42).unwrap());
372 let rates = (0..3).map(|_| slippage.draw()).collect::<Vec<_>>();
373 assert_eq!(rates, GOLDEN_RATES.map(d).to_vec());
374 }
375
376 #[test]
378 fn a_seed_gives_the_same_prices_in_every_version() {
379 let mut slippage =
380 Slippage::new(SlippageConfig::uniform(d("0.01"), d("0.025"), 42).unwrap());
381 let prices = [Side::Buy, Side::Sell, Side::Buy]
382 .map(|side| slippage.apply(side, d("49000.5")).to_string());
383 assert_eq!(prices, GOLDEN_PRICES);
384 }
385
386 const GOLDEN_PRICES: [&str; 3] = [
387 "50089.0253889615885",
388 "48276.1591443931995",
389 "50213.6745928332615",
390 ];
391
392 #[test]
394 fn a_rounded_draw_stays_inside_a_finely_given_range() {
395 let (low, high) = (d("0.0100000000004"), d("0.0100000000006"));
396 let mut slippage = Slippage::new(SlippageConfig::uniform(low, high, 1).unwrap());
397 for _ in 0..1_000 {
398 let rate = slippage.draw();
399 assert!(
400 low <= rate && rate <= high,
401 "{rate} outside [{low}, {high}]"
402 );
403 }
404 }
405
406 const GOLDEN_RATES: [&str; 3] = ["0.022214577177", "0.014782315601", "0.024758412523"];
410
411 #[test]
412 fn the_constructors_refuse_a_rate_the_venue_cannot_apply() {
413 assert_eq!(
414 SlippageConfig::fixed(d("-0.01")),
415 Err(SlippageConfigError::Negative(d("-0.01")))
416 );
417 assert_eq!(
418 SlippageConfig::fixed(Decimal::ONE),
419 Err(SlippageConfigError::NotBelowOne(Decimal::ONE))
420 );
421 assert_eq!(
422 SlippageConfig::uniform(d("0.02"), d("0.01"), 0),
423 Err(SlippageConfigError::Inverted {
424 low: d("0.02"),
425 high: d("0.01")
426 })
427 );
428 assert!(
429 SlippageConfig::fixed(d("-0")).is_ok(),
430 "negative zero is zero"
431 );
432 assert!(SlippageConfig::fixed(d("0.999")).is_ok());
433 }
434
435 #[test]
436 fn deserialising_validates_and_round_trips() {
437 let config = SlippageConfig::uniform(d("0.01"), d("0.025"), 42).unwrap();
438 let json = serde_json::to_string(&config).unwrap();
439 assert_eq!(
440 json,
441 r#"{"Uniform":{"low":"0.01","high":"0.025","seed":42}}"#
442 );
443 assert_eq!(
444 serde_json::from_str::<SlippageConfig>(&json).unwrap(),
445 config
446 );
447
448 assert_eq!(
449 serde_json::from_str::<SlippageConfig>(r#"{"Fixed":{"rate":"0.01"}}"#).unwrap(),
450 SlippageConfig::fixed(d("0.01")).unwrap()
451 );
452 assert_eq!(
453 serde_json::from_str::<SlippageConfig>(r#""None""#).unwrap(),
454 SlippageConfig::none()
455 );
456 let refused = serde_json::from_str::<SlippageConfig>(r#"{"Fixed":{"rate":"1.5"}}"#);
457 assert!(refused.unwrap_err().to_string().contains("not below 1"));
458 }
459}