Skip to main content

bat_markets_core/
numeric.rs

1use core::fmt;
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6use crate::error::{ErrorKind, MarketError, Result};
7
8macro_rules! decimal_value {
9    ($name:ident) => {
10        #[derive(
11            Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
12        )]
13        pub struct $name(Decimal);
14
15        impl $name {
16            #[must_use]
17            pub const fn new(value: Decimal) -> Self {
18                Self(value)
19            }
20
21            #[must_use]
22            pub const fn value(self) -> Decimal {
23                self.0
24            }
25        }
26
27        impl From<Decimal> for $name {
28            fn from(value: Decimal) -> Self {
29                Self::new(value)
30            }
31        }
32
33        impl fmt::Display for $name {
34            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35                write!(f, "{}", self.0)
36            }
37        }
38    };
39}
40
41macro_rules! fast_value {
42    ($name:ident) => {
43        #[derive(
44            Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
45        )]
46        pub struct $name(i64);
47
48        impl $name {
49            #[must_use]
50            pub const fn new(value: i64) -> Self {
51                Self(value)
52            }
53
54            #[must_use]
55            pub const fn value(self) -> i64 {
56                self.0
57            }
58        }
59    };
60}
61
62decimal_value!(Amount);
63decimal_value!(Price);
64decimal_value!(Quantity);
65decimal_value!(Notional);
66decimal_value!(Leverage);
67decimal_value!(Rate);
68
69fast_value!(FastPrice);
70fast_value!(FastQuantity);
71fast_value!(FastNotional);
72
73impl Notional {
74    #[must_use]
75    pub fn from_price_qty(price: Price, quantity: Quantity) -> Self {
76        Self(price.value() * quantity.value())
77    }
78}
79
80impl Price {
81    pub fn quantize(self, scale: u32) -> Result<FastPrice> {
82        quantize_decimal(self.value(), scale).map(FastPrice::new)
83    }
84}
85
86impl Quantity {
87    pub fn quantize(self, scale: u32) -> Result<FastQuantity> {
88        quantize_decimal(self.value(), scale).map(FastQuantity::new)
89    }
90}
91
92impl Notional {
93    pub fn quantize(self, scale: u32) -> Result<FastNotional> {
94        quantize_decimal(self.value(), scale).map(FastNotional::new)
95    }
96}
97
98impl FastPrice {
99    #[must_use]
100    pub fn to_price(self, scale: u32) -> Price {
101        Price::new(Decimal::new(self.value(), scale))
102    }
103}
104
105impl FastQuantity {
106    #[must_use]
107    pub fn to_quantity(self, scale: u32) -> Quantity {
108        Quantity::new(Decimal::new(self.value(), scale))
109    }
110}
111
112impl FastNotional {
113    #[must_use]
114    pub fn to_notional(self, scale: u32) -> Notional {
115        Notional::new(Decimal::new(self.value(), scale))
116    }
117}
118
119fn quantize_decimal(mut value: Decimal, scale: u32) -> Result<i64> {
120    value.rescale(scale);
121    i64::try_from(value.mantissa()).map_err(|_| {
122        MarketError::new(
123            ErrorKind::ConfigError,
124            format!("value {value} does not fit into fast-path i64 representation"),
125        )
126    })
127}
128
129#[cfg(test)]
130mod tests {
131    use proptest::prelude::*;
132    use proptest::test_runner::TestCaseError;
133    use rust_decimal::Decimal;
134
135    use super::{Price, Quantity};
136
137    proptest! {
138        #[test]
139        fn price_quantize_roundtrip(units in 1_i64..1_000_000_i64, scale in 0_u32..4_u32) {
140            let value = Price::new(Decimal::new(units, scale));
141            let fast = match value.quantize(scale) {
142                Ok(value) => value,
143                Err(error) => return Err(TestCaseError::fail(error.to_string())),
144            };
145            prop_assert_eq!(fast.to_price(scale), value);
146        }
147
148        #[test]
149        fn quantity_quantize_roundtrip(units in 1_i64..1_000_000_i64, scale in 0_u32..4_u32) {
150            let value = Quantity::new(Decimal::new(units, scale));
151            let fast = match value.quantize(scale) {
152                Ok(value) => value,
153                Err(error) => return Err(TestCaseError::fail(error.to_string())),
154            };
155            prop_assert_eq!(fast.to_quantity(scale), value);
156        }
157    }
158}