af_iperps/
math.rs

1use af_move_type::MoveType;
2use af_utilities::{Balance9, IFixed};
3use num_traits::Zero as _;
4
5use crate::clearing_house::ClearingHouse;
6use crate::market::MarketParams;
7
8#[derive(thiserror::Error, Debug)]
9#[non_exhaustive]
10pub enum Error {
11    #[error("Overflow when converting types")]
12    Overflow,
13    #[error("Not enough precision to represent price")]
14    Precision,
15}
16
17/// Convenience trait to convert to/from units used in the orderbook.
18pub trait OrderBookUnits {
19    fn price_to_ifixed(&self, price: u64) -> IFixed {
20        let price_ifixed = IFixed::from(price);
21        let lot_size_ifixed = IFixed::from(self.lot_size());
22        let tick_size_ifixed = IFixed::from(self.tick_size());
23        price_ifixed * tick_size_ifixed / lot_size_ifixed
24    }
25
26    /// The price in ticks/lot closest to the desired value.
27    ///
28    /// Note that this:
29    /// - rounds the equivalent ticks/lot **down** to the nearest integer.
30    /// - errors if the equivalent ticks/lot < 1, signaling not enough precision.
31    fn ifixed_to_price(&self, ifixed: IFixed) -> Result<u64, Error> {
32        if ifixed.is_zero() {
33            return Ok(0);
34        }
35        // ifixed = (price_ifixed * tick_size_ifixed) / lot_size_ifixed
36        // (ifixed * lot_size_ifixed) / tick_size_ifixed = price_ifixed
37        let price_ifixed =
38            (ifixed * IFixed::from(self.lot_size())) / IFixed::from(self.tick_size());
39        let price: u64 = price_ifixed
40            .integer()
41            .uabs()
42            .try_into()
43            .map_err(|_| Error::Overflow)?;
44        if price == 0 {
45            return Err(Error::Precision);
46        }
47        Ok(price)
48    }
49
50    fn lots_to_ifixed(&self, lots: u64) -> IFixed {
51        let ifixed_lots: IFixed = lots.into();
52        let ifixed_lot_size: IFixed = Balance9::from_inner(self.lot_size()).into();
53        ifixed_lots * ifixed_lot_size
54    }
55
56    fn ifixed_to_lots(&self, ifixed: IFixed) -> Result<u64, Error> {
57        let balance: Balance9 = ifixed.try_into().map_err(|_| Error::Overflow)?;
58        Ok(balance.into_inner() / self.lot_size())
59    }
60
61    // NOTE: these could be updated to return NonZeroU64 ensuring division by zero errors are
62    // impossible.
63    fn lot_size(&self) -> u64;
64    fn tick_size(&self) -> u64;
65}
66
67impl OrderBookUnits for MarketParams {
68    fn lot_size(&self) -> u64 {
69        self.lot_size
70    }
71
72    fn tick_size(&self) -> u64 {
73        self.tick_size
74    }
75}
76
77impl<T: MoveType> OrderBookUnits for ClearingHouse<T> {
78    fn lot_size(&self) -> u64 {
79        self.market_params.lot_size
80    }
81
82    fn tick_size(&self) -> u64 {
83        self.market_params.tick_size
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    impl OrderBookUnits for (u64, u64) {
92        fn lot_size(&self) -> u64 {
93            self.0
94        }
95
96        fn tick_size(&self) -> u64 {
97            self.1
98        }
99    }
100
101    #[test]
102    fn orderbook_units() {
103        let mut units = (10_000_000, 1_000_000);
104        let mut ifixed: IFixed;
105
106        ifixed = u64::MAX.into();
107        ifixed += IFixed::from_inner(1.into());
108        insta::assert_snapshot!(ifixed, @"18446744073709551615.000000000000000001");
109        let err = units.ifixed_to_lots(ifixed).unwrap_err();
110        insta::assert_snapshot!(err, @"Overflow when converting types");
111
112        // Values smaller than 1 balance9 get cast to 0
113        ifixed = IFixed::from_inner(1.into());
114        insta::assert_snapshot!(ifixed, @"0.000000000000000001");
115        let ok = units.ifixed_to_lots(ifixed).unwrap();
116        assert_eq!(ok, 0);
117
118        ifixed = 0.001.try_into().unwrap();
119        insta::assert_snapshot!(ifixed, @"0.001");
120        let err = units.ifixed_to_price(ifixed).unwrap_err();
121        insta::assert_snapshot!(err, @"Not enough precision to represent price");
122
123        ifixed = 0.0.try_into().unwrap();
124        insta::assert_snapshot!(ifixed, @"0.0");
125        let ok = units.ifixed_to_price(ifixed).unwrap();
126        assert_eq!(ok, 0);
127
128        ifixed = 0.1.try_into().unwrap();
129        insta::assert_snapshot!(ifixed, @"0.1");
130        let ok = units.ifixed_to_price(ifixed).unwrap();
131        assert_eq!(ok, 1);
132
133        // `ifixed_to_price` truncates
134        ifixed = 0.15.try_into().unwrap();
135        insta::assert_snapshot!(ifixed, @"0.15");
136        let ok = units.ifixed_to_price(ifixed).unwrap();
137        assert_eq!(ok, 1);
138
139        ifixed = units.price_to_ifixed(0);
140        insta::assert_snapshot!(ifixed, @"0.0");
141
142        // Can handle an absurdly large price no problem
143        units = (1, u64::MAX);
144        let ok = units.price_to_ifixed(u64::MAX);
145        insta::assert_snapshot!(ok, @"340282366920938463426481119284349108225.0");
146
147        // Can handle an absurdly large lot size no problem
148        units = (u64::MAX, 1);
149        let ok = units.lots_to_ifixed(u64::MAX);
150        insta::assert_snapshot!(ok, @"340282366920938463426481119284.349108225");
151
152        units = (100000, 1000);
153        let min_amount = units.lots_to_ifixed(1);
154        insta::assert_snapshot!(min_amount, @"0.0001");
155        let price_precision = units.price_to_ifixed(1);
156        insta::assert_snapshot!(price_precision, @"0.01");
157    }
158}