use rand::{Rng, SeedableRng, rngs::Xoshiro256PlusPlus};
use rust_decimal::Decimal;
use rustrade_instrument::Side;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
)]
#[serde(try_from = "SlippageRepr", into = "SlippageRepr")]
pub struct SlippageConfig(SlippageRepr);
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
)]
enum SlippageRepr {
#[default]
None,
Fixed {
#[serde(with = "rust_decimal::serde::str")]
rate: Decimal,
},
Uniform {
#[serde(with = "rust_decimal::serde::str")]
low: Decimal,
#[serde(with = "rust_decimal::serde::str")]
high: Decimal,
seed: u64,
},
}
impl SlippageConfig {
pub const fn none() -> Self {
Self(SlippageRepr::None)
}
pub fn fixed(rate: Decimal) -> Result<Self, SlippageConfigError> {
Self::try_from(SlippageRepr::Fixed { rate })
}
pub fn uniform(low: Decimal, high: Decimal, seed: u64) -> Result<Self, SlippageConfigError> {
Self::try_from(SlippageRepr::Uniform { low, high, seed })
}
pub fn low(&self) -> Decimal {
match self.0 {
SlippageRepr::None => Decimal::ZERO,
SlippageRepr::Fixed { rate } => rate,
SlippageRepr::Uniform { low, .. } => low,
}
}
pub fn high(&self) -> Decimal {
match self.0 {
SlippageRepr::None => Decimal::ZERO,
SlippageRepr::Fixed { rate } => rate,
SlippageRepr::Uniform { high, .. } => high,
}
}
pub fn seed(&self) -> Option<u64> {
match self.0 {
SlippageRepr::Uniform { seed, .. } => Some(seed),
SlippageRepr::None | SlippageRepr::Fixed { .. } => None,
}
}
}
impl TryFrom<SlippageRepr> for SlippageConfig {
type Error = SlippageConfigError;
fn try_from(repr: SlippageRepr) -> Result<Self, Self::Error> {
let (low, high) = match repr {
SlippageRepr::None => return Ok(Self(repr)),
SlippageRepr::Fixed { rate } => (rate, rate),
SlippageRepr::Uniform { low, high, .. } => (low, high),
};
if low.is_sign_negative() && !low.is_zero() {
return Err(SlippageConfigError::Negative(low));
}
if high >= Decimal::ONE {
return Err(SlippageConfigError::NotBelowOne(high));
}
if low > high {
return Err(SlippageConfigError::Inverted { low, high });
}
Ok(Self(repr))
}
}
impl From<SlippageConfig> for SlippageRepr {
fn from(config: SlippageConfig) -> Self {
config.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum SlippageConfigError {
#[error("slippage rate {0} is negative; slippage is adverse, so a rate is at least 0")]
Negative(Decimal),
#[error("slippage rate {0} is not below 1, so a sell would fill at or below zero")]
NotBelowOne(Decimal),
#[error("slippage range is inverted: low {low} is above high {high}")]
Inverted { low: Decimal, high: Decimal },
}
pub const RATE_DECIMAL_PLACES: u32 = 12;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Slippage {
config: SlippageConfig,
rng: Xoshiro256PlusPlus,
}
impl Slippage {
pub(crate) fn new(config: SlippageConfig) -> Self {
Self {
config,
rng: Xoshiro256PlusPlus::seed_from_u64(config.seed().unwrap_or_default()),
}
}
pub(crate) fn apply(&mut self, side: Side, reference: Decimal) -> Decimal {
let rate = self.draw();
if rate.is_zero() {
return reference;
}
match side {
Side::Buy => reference * (Decimal::ONE + rate),
Side::Sell => reference * (Decimal::ONE - rate),
}
}
fn draw(&mut self) -> Decimal {
let (low, high) = (self.config.low(), self.config.high());
if low == high {
return low;
}
let unit = Decimal::from(self.rng.next_u64()) / Decimal::from(u64::MAX);
(low + (high - low) * unit)
.round_dp(RATE_DECIMAL_PLACES)
.clamp(low, high)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
fn d(s: &str) -> Decimal {
s.parse().unwrap()
}
#[test]
fn none_is_the_default_and_leaves_every_price_alone() {
let mut slippage = Slippage::new(SlippageConfig::default());
assert_eq!(SlippageConfig::default(), SlippageConfig::none());
assert_eq!(slippage.apply(Side::Buy, d("100.5")), d("100.5"));
assert_eq!(slippage.apply(Side::Sell, d("100.5")), d("100.5"));
}
#[test]
fn a_fixed_rate_moves_a_buy_up_and_a_sell_down() {
let mut slippage = Slippage::new(SlippageConfig::fixed(d("0.01")).unwrap());
assert_eq!(slippage.apply(Side::Buy, d("200")), d("202"));
assert_eq!(slippage.apply(Side::Sell, d("200")), d("198"));
}
#[test]
fn several_percent_is_accepted() {
let mut slippage = Slippage::new(SlippageConfig::fixed(d("0.05")).unwrap());
assert_eq!(slippage.apply(Side::Sell, d("100")), d("95"));
}
#[test]
fn a_range_of_one_rate_makes_no_draw() {
let mut ranged = Slippage::new(SlippageConfig::uniform(d("0.02"), d("0.02"), 9).unwrap());
let untouched = ranged.rng.clone();
assert_eq!(ranged.apply(Side::Buy, d("50")), d("51"));
assert_eq!(
ranged.rng, untouched,
"a single-rate range drew from its stream"
);
let mut zero =
Slippage::new(SlippageConfig::uniform(Decimal::ZERO, Decimal::ZERO, 9).unwrap());
assert_eq!(zero.apply(Side::Buy, d("50.25")), d("50.25"));
}
#[test]
fn every_uniform_draw_lies_inside_its_range() {
let (low, high) = (d("0.01"), d("0.025"));
let mut slippage = Slippage::new(SlippageConfig::uniform(low, high, 7).unwrap());
for _ in 0..10_000 {
let rate = slippage.draw();
assert!(
low <= rate && rate <= high,
"{rate} outside [{low}, {high}]"
);
}
}
#[test]
fn the_same_seed_gives_the_same_prices_and_another_seed_does_not() {
let config = |seed| SlippageConfig::uniform(d("0.01"), d("0.025"), seed).unwrap();
let prices = |seed| {
let mut slippage = Slippage::new(config(seed));
(0..32)
.map(|i| {
let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
slippage.apply(side, d("100"))
})
.collect::<Vec<_>>()
};
assert_eq!(prices(42), prices(42));
assert_ne!(prices(42), prices(43));
}
#[test]
fn a_seed_gives_the_same_rates_in_every_version() {
let mut slippage =
Slippage::new(SlippageConfig::uniform(d("0.01"), d("0.025"), 42).unwrap());
let rates = (0..3).map(|_| slippage.draw()).collect::<Vec<_>>();
assert_eq!(rates, GOLDEN_RATES.map(d).to_vec());
}
#[test]
fn a_seed_gives_the_same_prices_in_every_version() {
let mut slippage =
Slippage::new(SlippageConfig::uniform(d("0.01"), d("0.025"), 42).unwrap());
let prices = [Side::Buy, Side::Sell, Side::Buy]
.map(|side| slippage.apply(side, d("49000.5")).to_string());
assert_eq!(prices, GOLDEN_PRICES);
}
const GOLDEN_PRICES: [&str; 3] = [
"50089.0253889615885",
"48276.1591443931995",
"50213.6745928332615",
];
#[test]
fn a_rounded_draw_stays_inside_a_finely_given_range() {
let (low, high) = (d("0.0100000000004"), d("0.0100000000006"));
let mut slippage = Slippage::new(SlippageConfig::uniform(low, high, 1).unwrap());
for _ in 0..1_000 {
let rate = slippage.draw();
assert!(
low <= rate && rate <= high,
"{rate} outside [{low}, {high}]"
);
}
}
const GOLDEN_RATES: [&str; 3] = ["0.022214577177", "0.014782315601", "0.024758412523"];
#[test]
fn the_constructors_refuse_a_rate_the_venue_cannot_apply() {
assert_eq!(
SlippageConfig::fixed(d("-0.01")),
Err(SlippageConfigError::Negative(d("-0.01")))
);
assert_eq!(
SlippageConfig::fixed(Decimal::ONE),
Err(SlippageConfigError::NotBelowOne(Decimal::ONE))
);
assert_eq!(
SlippageConfig::uniform(d("0.02"), d("0.01"), 0),
Err(SlippageConfigError::Inverted {
low: d("0.02"),
high: d("0.01")
})
);
assert!(
SlippageConfig::fixed(d("-0")).is_ok(),
"negative zero is zero"
);
assert!(SlippageConfig::fixed(d("0.999")).is_ok());
}
#[test]
fn deserialising_validates_and_round_trips() {
let config = SlippageConfig::uniform(d("0.01"), d("0.025"), 42).unwrap();
let json = serde_json::to_string(&config).unwrap();
assert_eq!(
json,
r#"{"Uniform":{"low":"0.01","high":"0.025","seed":42}}"#
);
assert_eq!(
serde_json::from_str::<SlippageConfig>(&json).unwrap(),
config
);
assert_eq!(
serde_json::from_str::<SlippageConfig>(r#"{"Fixed":{"rate":"0.01"}}"#).unwrap(),
SlippageConfig::fixed(d("0.01")).unwrap()
);
assert_eq!(
serde_json::from_str::<SlippageConfig>(r#""None""#).unwrap(),
SlippageConfig::none()
);
let refused = serde_json::from_str::<SlippageConfig>(r#"{"Fixed":{"rate":"1.5"}}"#);
assert!(refused.unwrap_err().to_string().contains("not below 1"));
}
}