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.core_params.lot_size
85 }
86
87 fn tick_size(&self) -> u64 {
88 self.core_params.tick_size
89 }
90}
91
92impl<T: MoveType> OrderBookUnits for ClearingHouse<T> {
93 fn lot_size(&self) -> u64 {
94 self.market_params.core_params.lot_size
95 }
96
97 fn tick_size(&self) -> u64 {
98 self.market_params.core_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.core_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.core_params.margin_ratio_initial;
120 let liq_margin = notional * self.core_params.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 let base = self.base_asset_amount;
148 let bids_net_abs = (base + self.bids_quantity).abs();
149 let asks_net_abs = (base - self.asks_quantity).abs();
150 let max_abs_net_base = max(bids_net_abs, asks_net_abs);
151
152 let denominator = max_abs_net_base * maintenance_margin_ratio - base;
153
154 if denominator.is_zero() {
155 None
156 } else {
157 let numerator = coll + ufunding - quote;
158
159 Some(numerator / denominator)
160 }
161 }
162
163 pub fn entry_price(&self) -> Option<IFixed> {
168 if self.base_asset_amount.is_zero() {
169 return None;
170 }
171 Some(self.quote_asset_notional_amount / self.base_asset_amount)
172 }
173
174 pub fn unrealized_funding(
180 &self,
181 cum_funding_rate_long: IFixed,
182 cum_funding_rate_short: IFixed,
183 ) -> IFixed {
184 if self.base_asset_amount.is_neg() {
185 unrealized_funding(
186 cum_funding_rate_short,
187 self.cum_funding_rate_short,
188 self.base_asset_amount,
189 )
190 } else {
191 unrealized_funding(
192 cum_funding_rate_long,
193 self.cum_funding_rate_long,
194 self.base_asset_amount,
195 )
196 }
197 }
198
199 pub fn unrealized_pnl(&self, price: IFixed) -> IFixed {
204 (self.base_asset_amount * price) - self.quote_asset_notional_amount
205 }
206
207 pub fn notional(&self, price: IFixed) -> IFixed {
212 let size = self.base_asset_amount;
213 let bids_net_abs = (size + self.bids_quantity).abs();
214 let asks_net_abs = (size - self.asks_quantity).abs();
215 let max_abs_net_base = max(bids_net_abs, asks_net_abs);
216 max_abs_net_base * price
217 }
218}
219
220fn unrealized_funding(
221 cum_funding_rate_now: IFixed,
222 cum_funding_rate_before: IFixed,
223 size: IFixed,
224) -> IFixed {
225 if cum_funding_rate_now == cum_funding_rate_before {
226 return IFixed::zero();
227 };
228
229 (cum_funding_rate_now - cum_funding_rate_before) * (-size)
230}
231
232#[cfg(test)]
233mod tests {
234 use std::num::NonZeroU64;
235
236 use af_utilities::Balance9;
237 use proptest::prelude::*;
238 use test_strategy::proptest;
239
240 use super::*;
241
242 impl OrderBookUnits for (u64, u64) {
243 fn lot_size(&self) -> u64 {
244 self.0
245 }
246
247 fn tick_size(&self) -> u64 {
248 self.1
249 }
250 }
251
252 #[test]
253 fn orderbook_units() {
254 let mut units = (10_000_000, 1_000_000);
255 let mut ifixed: IFixed;
256
257 ifixed = u64::MAX.into();
258 ifixed += IFixed::from_inner(1.into());
259 insta::assert_snapshot!(ifixed, @"18446744073709551615.000000000000000001");
260 let err = units.ifixed_to_size(ifixed).unwrap_err();
261 insta::assert_snapshot!(err, @"Overflow when converting types");
262
263 ifixed = IFixed::from_inner(1.into());
265 insta::assert_snapshot!(ifixed, @"0.000000000000000001");
266 let ok = units.ifixed_to_size(ifixed).unwrap();
267 assert_eq!(ok, 0);
268
269 ifixed = IFixed::from_inner(1.into());
271 insta::assert_snapshot!(ifixed, @"0.000000000000000001");
272 let ok = units.ifixed_to_price(ifixed).unwrap();
273 assert_eq!(ok, 0);
274
275 ifixed = 0.0.try_into().unwrap();
276 insta::assert_snapshot!(ifixed, @"0.0");
277 let ok = units.ifixed_to_price(ifixed).unwrap();
278 assert_eq!(ok, 0);
279
280 ifixed = 0.1.try_into().unwrap();
281 insta::assert_snapshot!(ifixed, @"0.1");
282 let ok = units.ifixed_to_price(ifixed).unwrap();
283 assert_eq!(ok, 0_100000000);
284
285 ifixed = 0.15.try_into().unwrap();
287 insta::assert_snapshot!(ifixed, @"0.15");
288 let ok = units.ifixed_to_price(ifixed).unwrap();
289 assert_eq!(ok, 0_150000000);
290
291 ifixed = units.price_to_ifixed(0);
292 insta::assert_snapshot!(ifixed, @"0.0");
293
294 units = (1, u64::MAX);
296 let ok = units.price_to_ifixed(u64::MAX);
297 insta::assert_snapshot!(ok, @"18446744073.709551615");
298
299 units = (u64::MAX, 1);
301 let ok = units.size_to_ifixed(u64::MAX);
302 insta::assert_snapshot!(ok, @"18446744073.709551615");
303 }
304
305 #[test]
306 #[should_panic]
307 fn zero_lot_and_tick() {
308 (0u64, 0u64).price_to_ifixed(1);
309 }
310
311 #[test]
312 #[should_panic]
313 fn zero_lot() {
314 (0u64, 1u64).size_to_ifixed(1);
315 }
316
317 #[test]
318 #[should_panic]
319 fn zero_tick() {
320 assert_eq!((1u64, 0u64).price_to_ifixed(1), IFixed::zero());
321 }
322
323 #[test]
324 #[should_panic]
325 fn ifixed_to_price() {
326 (1u64, 0u64).ifixed_to_price(IFixed::one()).expect("Panics");
327 }
328
329 impl Position {
337 fn empty(collateral: IFixed) -> Self {
366 Self {
367 collateral,
368 base_asset_amount: 0.into(),
369 quote_asset_notional_amount: 0.into(),
370 cum_funding_rate_long: 0.into(),
371 cum_funding_rate_short: 0.into(),
372 asks_quantity: 0.into(),
373 bids_quantity: 0.into(),
374 pending_orders: 0,
375 maker_fee: 1.into(),
376 taker_fee: 1.into(),
377 initial_margin_ratio: 1.into(),
378 }
379 }
380 }
381
382 #[proptest]
412 fn liquidation_price_none(
413 #[strategy(any::<NonZeroU64>())]
414 #[map(|x: NonZeroU64| Balance9::from_inner(x.get()))]
415 collateral: Balance9,
416 #[strategy(0.0001..=1e12)] coll_price: f64,
417 ) {
418 let position = Position::empty(collateral.into());
419 let liq_price = position.liquidation_price(
420 coll_price.try_into().unwrap(),
421 IFixed::zero(),
422 IFixed::zero(),
423 0.001.try_into().unwrap(),
424 );
425 assert_eq!(liq_price, None);
426 }
427}