1use std::cmp::max;
2
3use af_move_type::MoveType;
4use af_utilities::{Balance9, IFixed};
5use num_traits::Zero as _;
6
7use crate::clearing_house::ClearingHouse;
8use crate::{MarketParams, MarketState, Position};
9
10#[derive(thiserror::Error, Debug)]
11#[non_exhaustive]
12pub enum Error {
13 #[error("Overflow when converting types")]
14 Overflow,
15 #[error("Not enough precision to represent price")]
16 Precision,
17 #[error("Division by zero")]
18 DivisionByZero,
19}
20
21pub trait OrderBookUnits {
23 fn price_to_ifixed(&self, price: u64) -> IFixed {
29 let price_ifixed = IFixed::from(price);
30 let lot_size_ifixed = IFixed::from(self.lot_size());
31 let tick_size_ifixed = IFixed::from(self.tick_size());
32 price_ifixed * tick_size_ifixed / lot_size_ifixed
33 }
34
35 fn ifixed_to_price(&self, ifixed: IFixed) -> Result<u64, Error> {
41 if ifixed.is_zero() {
42 return Ok(0);
43 }
44 if self.tick_size() == 0 {
47 return Err(Error::DivisionByZero);
48 }
49 let price_ifixed =
51 (ifixed * IFixed::from(self.lot_size())) / IFixed::from(self.tick_size());
52 let price: u64 = price_ifixed
53 .integer()
54 .uabs()
55 .try_into()
56 .map_err(|_| Error::Overflow)?;
57 if price == 0 {
58 return Err(Error::Precision);
59 }
60 Ok(price)
61 }
62
63 fn lots_to_ifixed(&self, lots: u64) -> IFixed {
64 let ifixed_lots: IFixed = lots.into();
65 let ifixed_lot_size: IFixed = Balance9::from_inner(self.lot_size()).into();
66 ifixed_lots * ifixed_lot_size
67 }
68
69 fn ifixed_to_lots(&self, ifixed: IFixed) -> Result<u64, Error> {
70 let balance: Balance9 = ifixed.try_into().map_err(|_| Error::Overflow)?;
71 balance
72 .into_inner()
73 .checked_div(self.lot_size())
74 .ok_or(Error::DivisionByZero)
75 }
76
77 fn lot_size(&self) -> u64;
80 fn tick_size(&self) -> u64;
81}
82
83impl OrderBookUnits for MarketParams {
84 fn lot_size(&self) -> u64 {
85 self.lot_size
86 }
87
88 fn tick_size(&self) -> u64 {
89 self.tick_size
90 }
91}
92
93impl<T: MoveType> OrderBookUnits for ClearingHouse<T> {
94 fn lot_size(&self) -> u64 {
95 self.market_params.lot_size
96 }
97
98 fn tick_size(&self) -> u64 {
99 self.market_params.tick_size
100 }
101}
102
103impl<T: MoveType> ClearingHouse<T> {
104 pub fn liquidation_price(&self, pos: &Position, coll_price: IFixed) -> Option<IFixed> {
108 pos.liquidation_price(
109 coll_price,
110 self.market_state.cum_funding_rate_long,
111 self.market_state.cum_funding_rate_short,
112 self.market_params.margin_ratio_maintenance,
113 )
114 }
115}
116
117impl MarketParams {
118 pub fn margin_requirements(&self, notional: IFixed) -> (IFixed, IFixed) {
120 let min_margin = notional * self.margin_ratio_initial;
121 let liq_margin = notional * self.margin_ratio_maintenance;
122 (min_margin, liq_margin)
123 }
124}
125
126impl MarketState {
127 pub fn unrealized_funding(&self, pos: &Position) -> IFixed {
131 pos.unrealized_funding(self.cum_funding_rate_long, self.cum_funding_rate_short)
132 }
133}
134
135impl Position {
136 pub fn liquidation_price(
139 &self,
140 coll_price: IFixed,
141 cum_funding_rate_long: IFixed,
142 cum_funding_rate_short: IFixed,
143 maintenance_margin_ratio: IFixed,
144 ) -> Option<IFixed> {
145 let coll = self.collateral * coll_price;
146 let ufunding = self.unrealized_funding(cum_funding_rate_long, cum_funding_rate_short);
147 let quote = self.quote_asset_notional_amount;
148
149 let size = self.base_asset_amount;
150 let bids_net_abs = (size + self.bids_quantity).abs();
151 let asks_net_abs = (size - self.asks_quantity).abs();
152 let max_abs_net_base = max(bids_net_abs, asks_net_abs);
153
154 let denominator = max_abs_net_base * maintenance_margin_ratio - size;
155 if denominator.is_zero() {
156 None
157 } else {
158 Some((coll + ufunding - quote) / denominator)
159 }
160 }
161
162 pub fn entry_price(&self) -> Option<IFixed> {
167 if self.base_asset_amount.is_zero() {
168 return None;
169 }
170 Some(self.quote_asset_notional_amount / self.base_asset_amount)
171 }
172
173 pub fn unrealized_funding(
179 &self,
180 cum_funding_rate_long: IFixed,
181 cum_funding_rate_short: IFixed,
182 ) -> IFixed {
183 if self.base_asset_amount.is_neg() {
184 unrealized_funding(
185 cum_funding_rate_short,
186 self.cum_funding_rate_short,
187 self.base_asset_amount,
188 )
189 } else {
190 unrealized_funding(
191 cum_funding_rate_long,
192 self.cum_funding_rate_long,
193 self.base_asset_amount,
194 )
195 }
196 }
197
198 pub fn unrealized_pnl(&self, price: IFixed) -> IFixed {
203 (self.base_asset_amount * price) - self.quote_asset_notional_amount
204 }
205
206 pub fn notional(&self, price: IFixed) -> IFixed {
211 let size = self.base_asset_amount;
212 let bids_net_abs = (size + self.bids_quantity).abs();
213 let asks_net_abs = (size - self.asks_quantity).abs();
214 let max_abs_net_base = max(bids_net_abs, asks_net_abs);
215 max_abs_net_base * price
216 }
217}
218
219fn unrealized_funding(
220 cum_funding_rate_now: IFixed,
221 cum_funding_rate_before: IFixed,
222 size: IFixed,
223) -> IFixed {
224 if cum_funding_rate_now == cum_funding_rate_before {
225 return IFixed::zero();
226 };
227
228 (cum_funding_rate_now - cum_funding_rate_before) * (-size)
229}
230
231#[cfg(test)]
232mod tests {
233 use std::num::NonZeroU64;
234
235 use proptest::prelude::*;
236 use test_strategy::{Arbitrary, proptest};
237
238 use super::*;
239
240 impl OrderBookUnits for (u64, u64) {
241 fn lot_size(&self) -> u64 {
242 self.0
243 }
244
245 fn tick_size(&self) -> u64 {
246 self.1
247 }
248 }
249
250 #[test]
251 fn orderbook_units() {
252 let mut units = (10_000_000, 1_000_000);
253 let mut ifixed: IFixed;
254
255 ifixed = u64::MAX.into();
256 ifixed += IFixed::from_inner(1.into());
257 insta::assert_snapshot!(ifixed, @"18446744073709551615.000000000000000001");
258 let err = units.ifixed_to_lots(ifixed).unwrap_err();
259 insta::assert_snapshot!(err, @"Overflow when converting types");
260
261 ifixed = IFixed::from_inner(1.into());
263 insta::assert_snapshot!(ifixed, @"0.000000000000000001");
264 let ok = units.ifixed_to_lots(ifixed).unwrap();
265 assert_eq!(ok, 0);
266
267 ifixed = 0.001.try_into().unwrap();
268 insta::assert_snapshot!(ifixed, @"0.001");
269 let err = units.ifixed_to_price(ifixed).unwrap_err();
270 insta::assert_snapshot!(err, @"Not enough precision to represent price");
271
272 ifixed = 0.0.try_into().unwrap();
273 insta::assert_snapshot!(ifixed, @"0.0");
274 let ok = units.ifixed_to_price(ifixed).unwrap();
275 assert_eq!(ok, 0);
276
277 ifixed = 0.1.try_into().unwrap();
278 insta::assert_snapshot!(ifixed, @"0.1");
279 let ok = units.ifixed_to_price(ifixed).unwrap();
280 assert_eq!(ok, 1);
281
282 ifixed = 0.15.try_into().unwrap();
284 insta::assert_snapshot!(ifixed, @"0.15");
285 let ok = units.ifixed_to_price(ifixed).unwrap();
286 assert_eq!(ok, 1);
287
288 ifixed = units.price_to_ifixed(0);
289 insta::assert_snapshot!(ifixed, @"0.0");
290
291 units = (1, u64::MAX);
293 let ok = units.price_to_ifixed(u64::MAX);
294 insta::assert_snapshot!(ok, @"340282366920938463426481119284349108225.0");
295
296 units = (u64::MAX, 1);
298 let ok = units.lots_to_ifixed(u64::MAX);
299 insta::assert_snapshot!(ok, @"340282366920938463426481119284.349108225");
300
301 units = (100000, 1000);
302 let min_amount = units.lots_to_ifixed(1);
303 insta::assert_snapshot!(min_amount, @"0.0001");
304 let price_precision = units.price_to_ifixed(1);
305 insta::assert_snapshot!(price_precision, @"0.01");
306 }
307
308 #[test]
309 #[should_panic]
310 fn zero_lot_and_tick() {
311 (0u64, 0u64).price_to_ifixed(1);
312 }
313
314 #[test]
315 #[should_panic]
316 fn zero_lot() {
317 (0u64, 1u64).price_to_ifixed(1);
318 }
319
320 #[test]
321 fn zero_tick() {
322 assert_eq!((1u64, 0u64).price_to_ifixed(1), IFixed::zero());
323 }
324
325 #[test]
326 #[should_panic]
327 fn ifixed_to_price() {
328 (1u64, 0u64).ifixed_to_price(IFixed::one()).expect("Panics");
329 }
330
331 #[derive(Arbitrary, Debug)]
332 struct Contracts {
333 lots: NonZeroU64,
334 ticks: NonZeroU64,
335 short: bool,
336 }
337
338 impl Position {
339 fn from_contracts(
340 collateral: IFixed,
341 contracts: Contracts,
342 params: &impl OrderBookUnits,
343 ) -> Self {
344 let mut base = params.lots_to_ifixed(contracts.lots.into());
345 if contracts.short {
346 base = -base;
347 }
348 let mut quote = params.lots_to_ifixed(contracts.ticks.into());
349 if contracts.short {
350 quote = -quote;
351 }
352 Self {
353 collateral,
354 base_asset_amount: base,
355 quote_asset_notional_amount: quote,
356 cum_funding_rate_long: 0.into(),
357 cum_funding_rate_short: 0.into(),
358 asks_quantity: 0.into(),
359 bids_quantity: 0.into(),
360 pending_orders: 0,
361 maker_fee: 1.into(),
362 taker_fee: 1.into(),
363 initial_margin_ratio: 1.into(),
364 }
365 }
366
367 fn empty(collateral: IFixed) -> Self {
368 Self {
369 collateral,
370 base_asset_amount: 0.into(),
371 quote_asset_notional_amount: 0.into(),
372 cum_funding_rate_long: 0.into(),
373 cum_funding_rate_short: 0.into(),
374 asks_quantity: 0.into(),
375 bids_quantity: 0.into(),
376 pending_orders: 0,
377 maker_fee: 1.into(),
378 taker_fee: 1.into(),
379 initial_margin_ratio: 1.into(),
380 }
381 }
382 }
383
384 #[proptest]
385 fn liquidation_price_is_positive(
386 contracts: Contracts,
387 #[strategy(0.0001..=1e12)] coll_price: f64,
388 #[strategy(0.0001..=0.5)] maintenance_margin_ratio: f64,
389 #[strategy(1..=1_000_000_000_u64)] lot_size: u64,
390 #[strategy(1..=#lot_size)] tick_size: u64,
391 ) {
392 let position = Position::from_contracts(1.into(), contracts, &(lot_size, tick_size));
393 let liq_price = position
394 .liquidation_price(
395 coll_price.try_into().unwrap(),
396 IFixed::zero(),
397 IFixed::zero(),
398 maintenance_margin_ratio.try_into().unwrap(),
399 )
400 .unwrap();
401 dbg!(liq_price.to_string());
402 assert!(liq_price > IFixed::zero());
403 }
404
405 #[proptest]
406 fn liquidation_price_none(
407 #[strategy(any::<NonZeroU64>())]
408 #[map(|x: NonZeroU64| Balance9::from_inner(x.get()))]
409 collateral: Balance9,
410 #[strategy(0.0001..=1e12)] coll_price: f64,
411 ) {
412 let position = Position::empty(collateral.into());
413 let liq_price = position.liquidation_price(
414 coll_price.try_into().unwrap(),
415 IFixed::zero(),
416 IFixed::zero(),
417 0.001.try_into().unwrap(),
418 );
419 assert_eq!(liq_price, None);
420 }
421}