1use std::collections::BTreeMap;
19
20use alloy_primitives::{I256, U256};
21use rust_decimal::Decimal;
22
23use crate::{
24 defi::{WEI_PRECISION, tick_map::full_math::FullMath},
25 orderbook::{BookLevel, BookPrice},
26 types::{
27 Price, Quantity,
28 fixed::{FIXED_PRECISION, raw_scale},
29 quantity::QuantityRaw,
30 },
31};
32
33const DEPTH_SCALE_FACTORS: [u128; 3] = [1, 10, 100];
34
35#[must_use]
42pub fn get_avg_px_for_quantity(qty: Quantity, levels: &BTreeMap<BookPrice, BookLevel>) -> f64 {
43 let precision = WEI_PRECISION;
44 let target_size_raw = scaled_quantity_raw(qty, precision);
45 let mut cumulative_size_raw: QuantityRaw = 0;
46 let mut cumulative_value = I256::ZERO;
47
48 for (book_price, level) in levels {
49 let size_this_level =
50 scaled_level_size_raw(level, precision).min(target_size_raw - cumulative_size_raw);
51 cumulative_size_raw += size_this_level;
52 let price_raw = book_price.value.raw
53 * DEPTH_SCALE_FACTORS
54 [usize::from(precision - book_price.value.precision.max(FIXED_PRECISION))]
55 as i128;
56 cumulative_value += I256::try_from(price_raw)
57 .expect("Normalized raw price must fit in I256")
58 * I256::from_raw(U256::from(size_this_level));
59
60 if cumulative_size_raw >= target_size_raw {
61 break;
62 }
63 }
64
65 if cumulative_size_raw == 0 {
66 0.0
67 } else {
68 depth_average_as_f64(cumulative_value, cumulative_size_raw, precision)
69 }
70}
71
72#[must_use]
78pub fn get_worst_px_for_quantity(
79 qty: Quantity,
80 levels: &BTreeMap<BookPrice, BookLevel>,
81) -> Option<Price> {
82 let precision = WEI_PRECISION;
83 let target_size_raw = scaled_quantity_raw(qty, precision);
84 let mut cumulative_size_raw: QuantityRaw = 0;
85 let mut worst_price: Option<Price> = None;
86
87 for (book_price, level) in levels {
88 let size_this_level =
89 scaled_level_size_raw(level, precision).min(target_size_raw - cumulative_size_raw);
90
91 if size_this_level == 0 {
92 continue;
93 }
94
95 cumulative_size_raw += size_this_level;
96 worst_price = Some(book_price.value);
97
98 if cumulative_size_raw >= target_size_raw {
99 break;
100 }
101 }
102
103 if cumulative_size_raw == 0 {
104 None
105 } else {
106 worst_price
107 }
108}
109
110#[must_use]
113pub fn get_avg_px_qty_for_exposure(
114 target_exposure: Quantity,
115 levels: &BTreeMap<BookPrice, BookLevel>,
116) -> (f64, f64, f64) {
117 let precision = depth_precision(levels);
119 let mut cumulative_exposure = 0.0;
120 let mut cumulative_size_raw: QuantityRaw = 0;
121 let mut final_price = levels
122 .first_key_value()
123 .map_or(0.0, |(price, _)| price.value.as_f64());
124
125 let target_exposure_raw = exposure_raw_as_f64(target_exposure, precision);
126
127 for (book_price, level) in levels {
128 let price = book_price.value.as_f64();
129
130 if price == 0.0 {
131 continue;
132 }
133
134 let level_size_raw = scaled_level_size_raw(level, precision);
135 let level_exposure = price * level_size_raw as f64;
136 let exposure_this_level = level_exposure.min(target_exposure_raw - cumulative_exposure);
137 let size_this_level = (exposure_this_level / price).floor() as QuantityRaw;
138
139 if size_this_level == 0 {
140 continue;
141 }
142
143 final_price = price;
144 cumulative_exposure += price * size_this_level as f64;
145 cumulative_size_raw += size_this_level;
146
147 if cumulative_exposure >= target_exposure_raw {
148 break;
149 }
150 }
151
152 if cumulative_size_raw == 0 {
153 (0.0, 0.0, final_price)
154 } else {
155 let size_raw = cumulative_size_raw as f64;
156 let avg_price = cumulative_exposure / size_raw;
157 (
158 avg_price,
159 size_raw / raw_scale(precision) as f64,
160 final_price,
161 )
162 }
163}
164
165fn depth_average_as_f64(value: I256, size_raw: QuantityRaw, precision: u8) -> f64 {
166 if value.is_zero() {
167 return 0.0;
168 }
169
170 let mut numerator = value.unsigned_abs();
171 let mut denominator = U256::from(size_raw) * U256::from(raw_scale(precision));
172 let mut exponent = 0_i32;
173 let ten = U256::from(10);
174 while numerator < denominator {
175 numerator *= ten;
176 exponent -= 1;
177 }
178
179 while numerator >= denominator * ten {
180 denominator *= ten;
181 exponent += 1;
182 }
183
184 let scale = Decimal::MAX_SCALE - 1;
186 let mantissa = FullMath::mul_div(numerator, U256::from(10_u128.pow(scale)), denominator)
187 .expect("Normalized average price mantissa must fit in U256");
188
189 let sign = if value.is_negative() { "-" } else { "" };
190 format!("{sign}{mantissa}e{}", exponent - scale as i32)
191 .parse::<f64>()
192 .expect("Average price must parse as f64")
193}
194
195fn depth_precision(levels: &BTreeMap<BookPrice, BookLevel>) -> u8 {
196 levels
197 .values()
198 .flat_map(BookLevel::iter)
199 .map(|order| order.size.precision)
200 .max()
201 .unwrap_or(FIXED_PRECISION)
202 .max(FIXED_PRECISION)
203}
204
205fn scaled_quantity_raw(qty: Quantity, precision: u8) -> QuantityRaw {
206 let precision_diff = precision - qty.precision.max(FIXED_PRECISION);
207 if precision_diff == 0 {
208 return qty.raw();
209 }
210
211 qty.raw() * DEPTH_SCALE_FACTORS[usize::from(precision_diff)]
212}
213
214fn scaled_level_size_raw(level: &BookLevel, precision: u8) -> QuantityRaw {
215 level
216 .iter()
217 .try_fold(0_u128, |total, order| {
218 total.checked_add(scaled_quantity_raw(order.size, precision))
219 })
220 .expect("Overflow occurred when summing scaled order book size")
221}
222
223fn exposure_raw_as_f64(exposure: Quantity, precision: u8) -> f64 {
224 if exposure.precision > precision {
225 let factor = raw_scale(exposure.precision) / raw_scale(precision);
226 let raw = exposure.raw();
227 return (raw / factor) as f64 + (raw % factor) as f64 / factor as f64;
228 }
229
230 scaled_quantity_raw(exposure, precision) as f64
231}
232
233#[cfg(test)]
234mod tests {
235 use rstest::rstest;
236 use rust_decimal_macros::dec;
237
238 use super::*;
239 use crate::{
240 data::order::BookOrder,
241 enums::{BookType, OrderSide},
242 identifiers::InstrumentId,
243 orderbook::OrderBook,
244 types::quantity::QUANTITY_RAW_MAX,
245 };
246
247 #[rstest]
248 #[case(2, 18, 18)]
249 #[case(18, 2, 2)]
250 #[case(2, 17, 17)]
251 #[case(17, 2, 2)]
252 #[case(2, 2, 18)]
253 #[case(18, 18, 2)]
254 #[case(17, 18, 17)]
255 fn test_book_depth_normalizes_quantity_scales(
256 #[case] query_precision: u8,
257 #[case] first_precision: u8,
258 #[case] second_precision: u8,
259 #[values(OrderSide::Buy, OrderSide::Sell)] side: OrderSide,
260 #[values(false, true)] mixed_orders: bool,
261 ) {
262 let mut book = OrderBook::new(InstrumentId::from("ETHUSDT-PERP.BINANCE"), BookType::L3_MBO);
263 let order_side = side.opposite();
264
265 let (first_price, second_price) = match side {
266 OrderSide::Buy => (100, 200),
267 OrderSide::Sell => (200, 100),
268 };
269
270 for (level, price, precision) in [
271 (0, first_price, first_precision),
272 (1, second_price, second_precision),
273 ] {
274 let sizes = if mixed_orders {
275 [
276 (dec!(0.25), first_precision),
277 (dec!(0.75), second_precision),
278 ]
279 } else {
280 [(dec!(0.25), precision), (dec!(0.75), precision)]
281 };
282
283 for (order, (size, size_precision)) in sizes.into_iter().enumerate() {
284 book.add(
285 BookOrder::new(
286 order_side,
287 Price::from(price.to_string()),
288 Quantity::from_decimal_dp(size, size_precision).unwrap(),
289 level * 2 + order as u64,
290 ),
291 0,
292 0,
293 0.into(),
294 );
295 }
296 }
297
298 let query = Quantity::from_decimal_dp(dec!(1.5), query_precision).unwrap();
299 let exposure = Quantity::from_decimal_dp(
300 Decimal::from(first_price + second_price / 2),
301 query_precision,
302 )
303 .unwrap();
304 let expected_average = f64::from(first_price + second_price / 2) / 1.5;
305
306 assert_eq!(book.get_avg_px_for_quantity(query, side), expected_average);
307 assert_eq!(
308 book.get_worst_px_for_quantity(query, side),
309 Some(Price::from(second_price.to_string()))
310 );
311 assert_eq!(
312 book.get_avg_px_qty_for_exposure(exposure, side),
313 (expected_average, 1.5, f64::from(second_price))
314 );
315 }
316
317 #[rstest]
318 #[case(2, 2)]
319 #[case(2, 17)]
320 #[case(2, 18)]
321 #[case(17, 2)]
322 #[case(18, 2)]
323 fn test_book_exposure_preserves_book_scale_rounding(
324 #[case] size_precision: u8,
325 #[case] target_precision: u8,
326 ) {
327 let mut book = OrderBook::new(InstrumentId::from("ETHUSDT-PERP.BINANCE"), BookType::L2_MBP);
328 book.add(
329 BookOrder::new(
330 OrderSide::Sell,
331 Price::from("99"),
332 Quantity::from_decimal_dp(dec!(1), size_precision).unwrap(),
333 1,
334 ),
335 0,
336 0,
337 0.into(),
338 );
339 let target = Quantity::from_decimal_dp(dec!(1), target_precision).unwrap();
340
341 let expected = if size_precision <= FIXED_PRECISION {
342 (99.000_000_000_000_01, 0.010_101_010_101_010_1, 99.0)
343 } else if size_precision == 17 {
344 (99.0, 0.010_101_010_101_010_1, 99.0)
345 } else {
346 (99.0, 0.010_101_010_101_010_102, 99.0)
347 };
348
349 assert_eq!(
350 book.get_avg_px_qty_for_exposure(target, OrderSide::Buy),
351 expected
352 );
353 }
354
355 #[rstest]
356 fn test_book_depth_preserves_native_units() {
357 let mut book = OrderBook::new(InstrumentId::from("ETHUSDT-PERP.BINANCE"), BookType::L2_MBP);
358 for (id, price) in [(1, 100), (2, 200)] {
359 book.add(
360 BookOrder::new(
361 OrderSide::Sell,
362 Price::from(price.to_string()),
363 Quantity::from_raw(1, 18),
364 id,
365 ),
366 0,
367 0,
368 0.into(),
369 );
370 }
371
372 assert_eq!(
373 book.get_avg_px_for_quantity(Quantity::from(1), OrderSide::Buy),
374 150.0
375 );
376 assert_eq!(
377 book.get_worst_px_for_quantity(Quantity::from(1), OrderSide::Buy),
378 Some(Price::from("200"))
379 );
380 assert_eq!(
381 book.get_avg_px_qty_for_exposure(Quantity::from(1), OrderSide::Buy),
382 (150.0, 2e-18, 200.0)
383 );
384 }
385
386 #[rstest]
387 #[case("1", "2", 1.9)]
388 #[case("10000000000000", "11000000000000", 10_900_000_000_000.0)]
389 fn test_book_depth_normalizes_max_raw_sizes(
390 #[case] first_price: &str,
391 #[case] second_price: &str,
392 #[case] expected_average: f64,
393 ) {
394 let mut book = OrderBook::new(InstrumentId::from("ETHUSDT-PERP.BINANCE"), BookType::L2_MBP);
395 book.add(
396 BookOrder::new(
397 OrderSide::Sell,
398 Price::from(first_price),
399 Quantity::from_raw(QUANTITY_RAW_MAX, 18),
400 1,
401 ),
402 0,
403 0,
404 0.into(),
405 );
406 book.add(
407 BookOrder::new(
408 OrderSide::Sell,
409 Price::from(second_price),
410 Quantity::from_raw(QUANTITY_RAW_MAX, FIXED_PRECISION),
411 2,
412 ),
413 0,
414 0,
415 0.into(),
416 );
417 let query = Quantity::from_raw(QUANTITY_RAW_MAX / 10, FIXED_PRECISION);
418
419 assert_eq!(
420 book.get_avg_px_for_quantity(query, OrderSide::Buy),
421 expected_average
422 );
423 assert_eq!(
424 book.get_worst_px_for_quantity(query, OrderSide::Buy),
425 Some(Price::from(second_price))
426 );
427 }
428
429 #[rstest]
430 #[case("0.00000000001", 1e-11)]
431 #[case("0.0000000000000001", 1e-16)]
432 #[case("-0.00000000001", -1e-11)]
433 #[case("0.000000000000000001", 1e-18)]
434 fn test_book_depth_native_query_preserves_small_prices(
435 #[case] price: &str,
436 #[case] expected: f64,
437 ) {
438 let mut book = OrderBook::new(InstrumentId::from("ETHUSDT-PERP.BINANCE"), BookType::L2_MBP);
439 book.add(
440 BookOrder::new(OrderSide::Sell, Price::from(price), Quantity::from(1), 1),
441 0,
442 0,
443 0.into(),
444 );
445
446 assert_eq!(
447 book.get_avg_px_for_quantity(Quantity::from_raw(1, 18), OrderSide::Buy),
448 expected
449 );
450 }
451
452 #[rstest]
453 fn test_book_depth_average_preserves_sub_price_units() {
454 let mut book = OrderBook::new(InstrumentId::from("ETHUSDT-PERP.BINANCE"), BookType::L2_MBP);
455 for (id, price, size) in [
456 (1, "0", Quantity::from(1)),
457 (2, "0.0000000000000001", Quantity::from_raw(1, 18)),
458 ] {
459 book.add(
460 BookOrder::new(OrderSide::Sell, Price::from(price), size, id),
461 0,
462 0,
463 0.into(),
464 );
465 }
466
467 assert_eq!(
468 book.get_avg_px_for_quantity(Quantity::from(2), OrderSide::Buy),
469 1e-34
470 );
471 }
472
473 #[rstest]
474 #[case(0, 0.0, None)]
475 #[case(1, 100.0, Some(Price::from("100")))]
476 fn test_book_depth_native_query_against_fixed_sizes(
477 #[case] query_raw: QuantityRaw,
478 #[case] expected_average: f64,
479 #[case] expected_worst: Option<Price>,
480 ) {
481 let mut book = OrderBook::new(InstrumentId::from("ETHUSDT-PERP.BINANCE"), BookType::L2_MBP);
482 book.add(
483 BookOrder::new(OrderSide::Sell, Price::from("100"), Quantity::from(1), 1),
484 0,
485 0,
486 0.into(),
487 );
488 let query = Quantity::from_raw(query_raw, 18);
489
490 assert_eq!(
491 book.get_avg_px_for_quantity(query, OrderSide::Buy),
492 expected_average
493 );
494 assert_eq!(
495 book.get_worst_px_for_quantity(query, OrderSide::Buy),
496 expected_worst
497 );
498 }
499
500 #[rstest]
501 #[case(199, (0.0, 0.0, 3.0))]
502 #[case(300, (3.0, 1e-16, 3.0))]
503 fn test_book_exposure_finer_target_preserves_fill_quantum(
504 #[case] target_raw: QuantityRaw,
505 #[case] expected: (f64, f64, f64),
506 ) {
507 let mut book = OrderBook::new(InstrumentId::from("ETHUSDT-PERP.BINANCE"), BookType::L2_MBP);
508 book.add(
509 BookOrder::new(OrderSide::Sell, Price::from("3"), Quantity::from(1), 1),
510 0,
511 0,
512 0.into(),
513 );
514 let target = Quantity::from_raw(target_raw, 18);
515
516 assert_eq!(
517 book.get_avg_px_qty_for_exposure(target, OrderSide::Buy),
518 expected
519 );
520 }
521
522 #[rstest]
523 fn test_book_worst_price_preserves_mixed_scale_traversal() {
524 let mut book = OrderBook::new(InstrumentId::from("AAPL.XNAS"), BookType::L2_MBP);
525 let price = Price::from("1.00");
526 book.add(
527 BookOrder::new(OrderSide::Sell, price, Quantity::from_raw(1, 18), 1),
528 0,
529 0,
530 0.into(),
531 );
532 assert_eq!(
533 book.get_worst_px_for_quantity(Quantity::from(2), OrderSide::Buy),
534 Some(price)
535 );
536 }
537
538 #[rstest]
539 fn test_book_exposure_accepts_native_scale_quantities() {
540 let mut book = OrderBook::new(InstrumentId::from("AAPL.XNAS"), BookType::L2_MBP);
541 let target = Quantity::from_raw(1_000_000_000_000_000_000, 18);
542 assert_eq!(
543 book.get_avg_px_qty_for_exposure(target, OrderSide::Buy),
544 (0.0, 0.0, 0.0)
545 );
546 book.add(
547 BookOrder::new(
548 OrderSide::Sell,
549 Price::from("1.00"),
550 Quantity::from_raw(2_000_000_000_000_000_000, 18),
551 1,
552 ),
553 0,
554 0,
555 0.into(),
556 );
557 assert_eq!(
558 book.get_avg_px_qty_for_exposure(target, OrderSide::Buy),
559 (1.0, 1.0, 1.0)
560 );
561 }
562}