1use std::cmp::max;
2
3use af_move_type::MoveType;
4use af_utilities::IFixed;
5use num_traits::Zero as _;
6
7use crate::clearing_house::ClearingHouse;
8use crate::{MarketParams, MarketState, Position};
9
10pub const B9_SCALING: u64 = 1000000000;
11
12#[derive(thiserror::Error, Debug)]
13#[non_exhaustive]
14pub enum Error {
15 #[error(transparent)]
16 FromAfUtilities(#[from] af_utilities::types::errors::Error),
17 #[error("Overflow when converting types")]
18 Overflow,
19 #[error("Not enough precision to represent price")]
20 Precision,
21 #[error("Division by zero")]
22 DivisionByZero,
23}
24
25pub trait OrderBookUnits {
27 fn size_to_ifixed(&self, size: u64) -> IFixed {
33 let size = (size / self.lot_size()) * self.lot_size();
34 IFixed::from_balance_with_scaling(size, B9_SCALING.into())
35 }
36
37 fn price_to_ifixed(&self, price: u64) -> IFixed {
43 let price = (price / self.tick_size()) * self.tick_size();
44 IFixed::from_balance_with_scaling(price, B9_SCALING.into())
45 }
46
47 fn ifixed_to_price(&self, ifixed: IFixed) -> Result<u64, Error> {
49 if ifixed.is_zero() {
50 return Ok(0);
51 }
52 if self.tick_size() == 0 {
53 return Err(Error::DivisionByZero);
54 }
55
56 let price = IFixed::try_into_balance_with_scaling(ifixed, B9_SCALING.into())?;
57 let price = (price / self.tick_size()) * self.tick_size();
58
59 Ok(price)
60 }
61
62 fn ifixed_to_size(&self, ifixed: IFixed) -> Result<u64, Error> {
63 if ifixed.is_zero() {
64 return Ok(0);
65 }
66 if self.lot_size() == 0 {
67 return Err(Error::DivisionByZero);
68 }
69
70 let size = IFixed::try_into_balance_with_scaling(ifixed, B9_SCALING.into())?;
71 let size = (size / self.lot_size()) * self.lot_size();
72
73 Ok(size)
74 }
75
76 fn lot_size(&self) -> u64;
79 fn tick_size(&self) -> u64;
80}
81
82impl OrderBookUnits for MarketParams {
83 fn lot_size(&self) -> u64 {
84 self.lot_size
85 }
86
87 fn tick_size(&self) -> u64 {
88 self.tick_size
89 }
90}
91
92impl<T: MoveType> OrderBookUnits for ClearingHouse<T> {
93 fn lot_size(&self) -> u64 {
94 self.market_params.lot_size
95 }
96
97 fn tick_size(&self) -> u64 {
98 self.market_params.tick_size
99 }
100}
101
102impl<T: MoveType> ClearingHouse<T> {
103 pub fn liquidation_price(&self, pos: &Position, coll_price: IFixed) -> Option<IFixed> {
107 pos.liquidation_price(
108 coll_price,
109 self.market_state.cum_funding_rate_long,
110 self.market_state.cum_funding_rate_short,
111 self.market_params.margin_ratio_maintenance,
112 )
113 }
114}
115
116impl MarketParams {
117 pub fn margin_requirements(&self, notional: IFixed) -> (IFixed, IFixed) {
119 let min_margin = notional * self.margin_ratio_initial;
120 let liq_margin = notional * self.margin_ratio_maintenance;
121 (min_margin, liq_margin)
122 }
123}
124
125impl MarketState {
126 pub fn unrealized_funding(&self, pos: &Position) -> IFixed {
130 pos.unrealized_funding(self.cum_funding_rate_long, self.cum_funding_rate_short)
131 }
132}
133
134impl Position {
135 pub fn liquidation_price(
138 &self,
139 coll_price: IFixed,
140 cum_funding_rate_long: IFixed,
141 cum_funding_rate_short: IFixed,
142 maintenance_margin_ratio: IFixed,
143 ) -> Option<IFixed> {
144 let coll = self.collateral * coll_price;
145 let ufunding = self.unrealized_funding(cum_funding_rate_long, cum_funding_rate_short);
146 let quote = self.quote_asset_notional_amount;
147
148 let base = self.base_asset_amount;
149 let bids_net_abs = (base + self.bids_quantity).abs();
150 let asks_net_abs = (base - self.asks_quantity).abs();
151 let max_abs_net_base = max(bids_net_abs, asks_net_abs);
152
153 println!("Base: {base}");
154 println!("Quote: {quote}");
155 let denominator = max_abs_net_base * maintenance_margin_ratio - base;
156 println!("Den: {}", denominator);
157
158 if denominator.is_zero() {
159 None
160 } else {
161 let num = coll + ufunding - quote;
162 println!("Num: {}", num);
163
164 Some((coll + ufunding - quote) / denominator)
165 }
166 }
167
168 pub fn entry_price(&self) -> Option<IFixed> {
173 if self.base_asset_amount.is_zero() {
174 return None;
175 }
176 Some(self.quote_asset_notional_amount / self.base_asset_amount)
177 }
178
179 pub fn unrealized_funding(
185 &self,
186 cum_funding_rate_long: IFixed,
187 cum_funding_rate_short: IFixed,
188 ) -> IFixed {
189 if self.base_asset_amount.is_neg() {
190 unrealized_funding(
191 cum_funding_rate_short,
192 self.cum_funding_rate_short,
193 self.base_asset_amount,
194 )
195 } else {
196 unrealized_funding(
197 cum_funding_rate_long,
198 self.cum_funding_rate_long,
199 self.base_asset_amount,
200 )
201 }
202 }
203
204 pub fn unrealized_pnl(&self, price: IFixed) -> IFixed {
209 (self.base_asset_amount * price) - self.quote_asset_notional_amount
210 }
211
212 pub fn notional(&self, price: IFixed) -> IFixed {
217 let size = self.base_asset_amount;
218 let bids_net_abs = (size + self.bids_quantity).abs();
219 let asks_net_abs = (size - self.asks_quantity).abs();
220 let max_abs_net_base = max(bids_net_abs, asks_net_abs);
221 max_abs_net_base * price
222 }
223}
224
225fn unrealized_funding(
226 cum_funding_rate_now: IFixed,
227 cum_funding_rate_before: IFixed,
228 size: IFixed,
229) -> IFixed {
230 if cum_funding_rate_now == cum_funding_rate_before {
231 return IFixed::zero();
232 };
233
234 (cum_funding_rate_now - cum_funding_rate_before) * (-size)
235}
236
237#[cfg(test)]
238mod tests {
239 use std::num::NonZeroU64;
240
241 use af_utilities::Balance9;
242 use proptest::prelude::*;
243 use test_strategy::proptest;
244
245 use super::*;
246
247 impl OrderBookUnits for (u64, u64) {
248 fn lot_size(&self) -> u64 {
249 self.0
250 }
251
252 fn tick_size(&self) -> u64 {
253 self.1
254 }
255 }
256
257 #[test]
258 fn orderbook_units() {
259 let mut units = (10_000_000, 1_000_000);
260 let mut ifixed: IFixed;
261
262 ifixed = u64::MAX.into();
263 ifixed += IFixed::from_inner(1.into());
264 insta::assert_snapshot!(ifixed, @"18446744073709551615.000000000000000001");
265 let err = units.ifixed_to_size(ifixed).unwrap_err();
266 insta::assert_snapshot!(err, @"Overflow when converting types");
267
268 ifixed = IFixed::from_inner(1.into());
270 insta::assert_snapshot!(ifixed, @"0.000000000000000001");
271 let ok = units.ifixed_to_size(ifixed).unwrap();
272 assert_eq!(ok, 0);
273
274 ifixed = IFixed::from_inner(1.into());
276 insta::assert_snapshot!(ifixed, @"0.000000000000000001");
277 let ok = units.ifixed_to_price(ifixed).unwrap();
278 assert_eq!(ok, 0);
279
280 ifixed = 0.0.try_into().unwrap();
281 insta::assert_snapshot!(ifixed, @"0.0");
282 let ok = units.ifixed_to_price(ifixed).unwrap();
283 assert_eq!(ok, 0);
284
285 ifixed = 0.1.try_into().unwrap();
286 insta::assert_snapshot!(ifixed, @"0.1");
287 let ok = units.ifixed_to_price(ifixed).unwrap();
288 assert_eq!(ok, 0_100000000);
289
290 ifixed = 0.15.try_into().unwrap();
292 insta::assert_snapshot!(ifixed, @"0.15");
293 let ok = units.ifixed_to_price(ifixed).unwrap();
294 assert_eq!(ok, 0_150000000);
295
296 ifixed = units.price_to_ifixed(0);
297 insta::assert_snapshot!(ifixed, @"0.0");
298
299 units = (1, u64::MAX);
301 let ok = units.price_to_ifixed(u64::MAX);
302 insta::assert_snapshot!(ok, @"18446744073.709551615");
303
304 units = (u64::MAX, 1);
306 let ok = units.size_to_ifixed(u64::MAX);
307 insta::assert_snapshot!(ok, @"18446744073.709551615");
308 }
309
310 #[test]
311 #[should_panic]
312 fn zero_lot_and_tick() {
313 (0u64, 0u64).price_to_ifixed(1);
314 }
315
316 #[test]
317 #[should_panic]
318 fn zero_lot() {
319 (0u64, 1u64).size_to_ifixed(1);
320 }
321
322 #[test]
323 #[should_panic]
324 fn zero_tick() {
325 assert_eq!((1u64, 0u64).price_to_ifixed(1), IFixed::zero());
326 }
327
328 #[test]
329 #[should_panic]
330 fn ifixed_to_price() {
331 (1u64, 0u64).ifixed_to_price(IFixed::one()).expect("Panics");
332 }
333
334 impl Position {
342 fn empty(collateral: IFixed) -> Self {
371 Self {
372 collateral,
373 base_asset_amount: 0.into(),
374 quote_asset_notional_amount: 0.into(),
375 cum_funding_rate_long: 0.into(),
376 cum_funding_rate_short: 0.into(),
377 asks_quantity: 0.into(),
378 bids_quantity: 0.into(),
379 pending_orders: 0,
380 maker_fee: 1.into(),
381 taker_fee: 1.into(),
382 initial_margin_ratio: 1.into(),
383 }
384 }
385 }
386
387 #[proptest]
417 fn liquidation_price_none(
418 #[strategy(any::<NonZeroU64>())]
419 #[map(|x: NonZeroU64| Balance9::from_inner(x.get()))]
420 collateral: Balance9,
421 #[strategy(0.0001..=1e12)] coll_price: f64,
422 ) {
423 let position = Position::empty(collateral.into());
424 let liq_price = position.liquidation_price(
425 coll_price.try_into().unwrap(),
426 IFixed::zero(),
427 IFixed::zero(),
428 0.001.try_into().unwrap(),
429 );
430 assert_eq!(liq_price, None);
431 }
432}