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}
18
19pub trait OrderBookUnits {
21 fn price_to_ifixed(&self, price: u64) -> IFixed {
22 let price_ifixed = IFixed::from(price);
23 let lot_size_ifixed = IFixed::from(self.lot_size());
24 let tick_size_ifixed = IFixed::from(self.tick_size());
25 price_ifixed * tick_size_ifixed / lot_size_ifixed
26 }
27
28 fn ifixed_to_price(&self, ifixed: IFixed) -> Result<u64, Error> {
34 if ifixed.is_zero() {
35 return Ok(0);
36 }
37 let price_ifixed =
40 (ifixed * IFixed::from(self.lot_size())) / IFixed::from(self.tick_size());
41 let price: u64 = price_ifixed
42 .integer()
43 .uabs()
44 .try_into()
45 .map_err(|_| Error::Overflow)?;
46 if price == 0 {
47 return Err(Error::Precision);
48 }
49 Ok(price)
50 }
51
52 fn lots_to_ifixed(&self, lots: u64) -> IFixed {
53 let ifixed_lots: IFixed = lots.into();
54 let ifixed_lot_size: IFixed = Balance9::from_inner(self.lot_size()).into();
55 ifixed_lots * ifixed_lot_size
56 }
57
58 fn ifixed_to_lots(&self, ifixed: IFixed) -> Result<u64, Error> {
59 let balance: Balance9 = ifixed.try_into().map_err(|_| Error::Overflow)?;
60 Ok(balance.into_inner() / self.lot_size())
61 }
62
63 fn lot_size(&self) -> u64;
66 fn tick_size(&self) -> u64;
67}
68
69impl OrderBookUnits for MarketParams {
70 fn lot_size(&self) -> u64 {
71 self.lot_size
72 }
73
74 fn tick_size(&self) -> u64 {
75 self.tick_size
76 }
77}
78
79impl<T: MoveType> OrderBookUnits for ClearingHouse<T> {
80 fn lot_size(&self) -> u64 {
81 self.market_params.lot_size
82 }
83
84 fn tick_size(&self) -> u64 {
85 self.market_params.tick_size
86 }
87}
88
89impl<T: MoveType> ClearingHouse<T> {
90 pub fn liquidation_price(&self, pos: &Position, coll_price: IFixed) -> Option<IFixed> {
94 pos.liquidation_price(
95 coll_price,
96 self.market_state.cum_funding_rate_long,
97 self.market_state.cum_funding_rate_short,
98 self.market_params.margin_ratio_maintenance,
99 )
100 }
101}
102
103impl MarketParams {
104 pub fn margin_requirements(&self, notional: IFixed) -> (IFixed, IFixed) {
108 let min_margin = notional * self.margin_ratio_initial;
109 let liq_margin = notional * self.margin_ratio_maintenance;
110 (min_margin, liq_margin)
111 }
112}
113
114impl MarketState {
115 pub fn unrealized_funding(&self, pos: &Position) -> IFixed {
119 pos.unrealized_funding(self.cum_funding_rate_long, self.cum_funding_rate_short)
120 }
121}
122
123impl Position {
124 pub fn liquidation_price(
125 &self,
126 coll_price: IFixed,
127 cum_funding_rate_long: IFixed,
128 cum_funding_rate_short: IFixed,
129 maintenance_margin_ratio: IFixed,
130 ) -> Option<IFixed> {
131 let coll = self.collateral * coll_price;
132 let ufunding = self.unrealized_funding(cum_funding_rate_long, cum_funding_rate_short);
133 let quote = self.quote_asset_notional_amount;
134
135 let size = self.base_asset_amount;
136 let bids_net_abs = (size + self.bids_quantity).abs();
137 let asks_net_abs = (size - self.asks_quantity).abs();
138 let max_abs_net_base = max(bids_net_abs, asks_net_abs);
139
140 let denominator = max_abs_net_base * maintenance_margin_ratio - size;
141 if denominator.is_zero() {
142 None
143 } else {
144 Some((coll + ufunding - quote) / denominator)
145 }
146 }
147
148 pub fn entry_price(&self) -> IFixed {
149 self.base_asset_amount / self.quote_asset_notional_amount
150 }
151
152 pub fn unrealized_funding(
154 &self,
155 cum_funding_rate_long: IFixed,
156 cum_funding_rate_short: IFixed,
157 ) -> IFixed {
158 if self.base_asset_amount.is_neg() {
159 unrealized_funding(
160 cum_funding_rate_short,
161 self.cum_funding_rate_short,
162 self.base_asset_amount,
163 )
164 } else {
165 unrealized_funding(
166 cum_funding_rate_long,
167 self.cum_funding_rate_long,
168 self.base_asset_amount,
169 )
170 }
171 }
172
173 pub fn unrealized_pnl(&self, price: IFixed) -> IFixed {
175 (self.base_asset_amount * price) - self.quote_asset_notional_amount
176 }
177
178 pub fn notional(&self, price: IFixed) -> IFixed {
180 let size = self.base_asset_amount;
181 let bids_net_abs = (size + self.bids_quantity).abs();
182 let asks_net_abs = (size - self.asks_quantity).abs();
183 let max_abs_net_base = max(bids_net_abs, asks_net_abs);
184 max_abs_net_base * price
185 }
186}
187
188fn unrealized_funding(
189 cum_funding_rate_now: IFixed,
190 cum_funding_rate_before: IFixed,
191 size: IFixed,
192) -> IFixed {
193 if cum_funding_rate_now == cum_funding_rate_before {
194 return IFixed::zero();
195 };
196
197 (cum_funding_rate_now - cum_funding_rate_before) * (-size)
198}
199
200#[cfg(test)]
201mod tests {
202 use std::num::NonZeroU64;
203
204 use proptest::prelude::*;
205 use test_strategy::{Arbitrary, proptest};
206
207 use super::*;
208
209 impl OrderBookUnits for (u64, u64) {
210 fn lot_size(&self) -> u64 {
211 self.0
212 }
213
214 fn tick_size(&self) -> u64 {
215 self.1
216 }
217 }
218
219 #[test]
220 fn orderbook_units() {
221 let mut units = (10_000_000, 1_000_000);
222 let mut ifixed: IFixed;
223
224 ifixed = u64::MAX.into();
225 ifixed += IFixed::from_inner(1.into());
226 insta::assert_snapshot!(ifixed, @"18446744073709551615.000000000000000001");
227 let err = units.ifixed_to_lots(ifixed).unwrap_err();
228 insta::assert_snapshot!(err, @"Overflow when converting types");
229
230 ifixed = IFixed::from_inner(1.into());
232 insta::assert_snapshot!(ifixed, @"0.000000000000000001");
233 let ok = units.ifixed_to_lots(ifixed).unwrap();
234 assert_eq!(ok, 0);
235
236 ifixed = 0.001.try_into().unwrap();
237 insta::assert_snapshot!(ifixed, @"0.001");
238 let err = units.ifixed_to_price(ifixed).unwrap_err();
239 insta::assert_snapshot!(err, @"Not enough precision to represent price");
240
241 ifixed = 0.0.try_into().unwrap();
242 insta::assert_snapshot!(ifixed, @"0.0");
243 let ok = units.ifixed_to_price(ifixed).unwrap();
244 assert_eq!(ok, 0);
245
246 ifixed = 0.1.try_into().unwrap();
247 insta::assert_snapshot!(ifixed, @"0.1");
248 let ok = units.ifixed_to_price(ifixed).unwrap();
249 assert_eq!(ok, 1);
250
251 ifixed = 0.15.try_into().unwrap();
253 insta::assert_snapshot!(ifixed, @"0.15");
254 let ok = units.ifixed_to_price(ifixed).unwrap();
255 assert_eq!(ok, 1);
256
257 ifixed = units.price_to_ifixed(0);
258 insta::assert_snapshot!(ifixed, @"0.0");
259
260 units = (1, u64::MAX);
262 let ok = units.price_to_ifixed(u64::MAX);
263 insta::assert_snapshot!(ok, @"340282366920938463426481119284349108225.0");
264
265 units = (u64::MAX, 1);
267 let ok = units.lots_to_ifixed(u64::MAX);
268 insta::assert_snapshot!(ok, @"340282366920938463426481119284.349108225");
269
270 units = (100000, 1000);
271 let min_amount = units.lots_to_ifixed(1);
272 insta::assert_snapshot!(min_amount, @"0.0001");
273 let price_precision = units.price_to_ifixed(1);
274 insta::assert_snapshot!(price_precision, @"0.01");
275 }
276
277 #[derive(Arbitrary, Debug)]
278 struct Contracts {
279 lots: NonZeroU64,
280 ticks: NonZeroU64,
281 short: bool,
282 }
283
284 impl Position {
285 fn from_contracts(
286 collateral: IFixed,
287 contracts: Contracts,
288 params: &impl OrderBookUnits,
289 ) -> Self {
290 let mut base = params.lots_to_ifixed(contracts.lots.into());
291 if contracts.short {
292 base = -base;
293 }
294 let mut quote = params.lots_to_ifixed(contracts.ticks.into());
295 if contracts.short {
296 quote = -quote;
297 }
298 Self {
299 collateral,
300 base_asset_amount: base,
301 quote_asset_notional_amount: quote,
302 cum_funding_rate_long: 0.into(),
303 cum_funding_rate_short: 0.into(),
304 asks_quantity: 0.into(),
305 bids_quantity: 0.into(),
306 pending_orders: 0,
307 maker_fee: 1.into(),
308 taker_fee: 1.into(),
309 }
310 }
311
312 fn empty(collateral: IFixed) -> Self {
313 Self {
314 collateral,
315 base_asset_amount: 0.into(),
316 quote_asset_notional_amount: 0.into(),
317 cum_funding_rate_long: 0.into(),
318 cum_funding_rate_short: 0.into(),
319 asks_quantity: 0.into(),
320 bids_quantity: 0.into(),
321 pending_orders: 0,
322 maker_fee: 1.into(),
323 taker_fee: 1.into(),
324 }
325 }
326 }
327
328 #[proptest]
329 fn liquidation_price_is_positive(
330 contracts: Contracts,
331 #[strategy(0.0001..=1e12)] coll_price: f64,
332 #[strategy(0.0001..=0.5)] maintenance_margin_ratio: f64,
333 #[strategy(1..=1_000_000_000_u64)] lot_size: u64,
334 #[strategy(1..=#lot_size)] tick_size: u64,
335 ) {
336 let position = Position::from_contracts(1.into(), contracts, &(lot_size, tick_size));
337 let liq_price = position
338 .liquidation_price(
339 coll_price.try_into().unwrap(),
340 IFixed::zero(),
341 IFixed::zero(),
342 maintenance_margin_ratio.try_into().unwrap(),
343 )
344 .unwrap();
345 dbg!(liq_price.to_string());
346 assert!(liq_price > IFixed::zero());
347 }
348
349 #[proptest]
350 fn liquidation_price_none(
351 #[strategy(any::<NonZeroU64>())]
352 #[map(|x: NonZeroU64| Balance9::from_inner(x.get()))]
353 collateral: Balance9,
354 #[strategy(0.0001..=1e12)] coll_price: f64,
355 ) {
356 let position = Position::empty(collateral.into());
357 let liq_price = position.liquidation_price(
358 coll_price.try_into().unwrap(),
359 IFixed::zero(),
360 IFixed::zero(),
361 0.001.try_into().unwrap(),
362 );
363 assert_eq!(liq_price, None);
364 }
365}