nautilus_model/orderbook/
analysis.rs1use std::collections::BTreeMap;
19
20use super::{BookLevel, BookPrice, OrderBook};
21use crate::{
22 enums::{BookType, OrderSide},
23 orderbook::BookIntegrityError,
24 types::{Price, Quantity, fixed::FIXED_SCALAR, quantity::QuantityRaw},
25};
26
27#[must_use]
30pub fn get_quantity_for_price(
31 price: Price,
32 order_side: OrderSide,
33 levels: &BTreeMap<BookPrice, BookLevel>,
34) -> f64 {
35 let mut matched_size: f64 = 0.0;
36
37 for (book_price, level) in levels {
38 match order_side {
39 OrderSide::Buy => {
40 if book_price.value > price {
41 break;
42 }
43 }
44 OrderSide::Sell => {
45 if book_price.value < price {
46 break;
47 }
48 }
49 _ => panic!("Invalid `OrderSide` {order_side}"),
50 }
51 matched_size += level.size();
52 }
53
54 matched_size
55}
56
57#[must_use]
60pub fn get_avg_px_for_quantity(qty: Quantity, levels: &BTreeMap<BookPrice, BookLevel>) -> f64 {
61 let mut cumulative_size_raw: QuantityRaw = 0;
62 let mut cumulative_value = 0.0;
63
64 for (book_price, level) in levels {
65 let size_this_level = level.size_raw().min(qty.raw - cumulative_size_raw);
66 cumulative_size_raw += size_this_level;
67 cumulative_value += book_price.value.as_f64() * size_this_level as f64;
68
69 if cumulative_size_raw >= qty.raw {
70 break;
71 }
72 }
73
74 if cumulative_size_raw == 0 {
75 0.0
76 } else {
77 cumulative_value / cumulative_size_raw as f64
78 }
79}
80
81#[must_use]
84pub fn get_avg_px_qty_for_exposure(
85 target_exposure: Quantity,
86 levels: &BTreeMap<BookPrice, BookLevel>,
87) -> (f64, f64, f64) {
88 let mut cumulative_exposure = 0.0;
89 let mut cumulative_size_raw: QuantityRaw = 0;
90 let mut final_price = levels
91 .first_key_value()
92 .map(|(price, _)| price.value.as_f64())
93 .unwrap_or(0.0);
94
95 for (book_price, level) in levels {
96 let price = book_price.value.as_f64();
97 final_price = price;
98
99 let level_exposure = price * level.size_raw() as f64;
100 let exposure_this_level =
101 level_exposure.min(target_exposure.raw as f64 - cumulative_exposure);
102 let size_this_level = (exposure_this_level / price).floor() as QuantityRaw;
103
104 cumulative_exposure += price * size_this_level as f64;
105 cumulative_size_raw += size_this_level;
106
107 if cumulative_exposure >= target_exposure.as_f64() {
108 break;
109 }
110 }
111
112 if cumulative_size_raw == 0 {
113 (0.0, 0.0, final_price)
114 } else {
115 let avg_price = cumulative_exposure / cumulative_size_raw as f64;
116 (
117 avg_price,
118 cumulative_size_raw as f64 / FIXED_SCALAR,
119 final_price,
120 )
121 }
122}
123
124pub fn book_check_integrity(book: &OrderBook) -> Result<(), BookIntegrityError> {
125 match book.book_type {
126 BookType::L1_MBP => {
127 if book.bids.len() > 1 {
128 return Err(BookIntegrityError::TooManyLevels(
129 OrderSide::Buy,
130 book.bids.len(),
131 ));
132 }
133 if book.asks.len() > 1 {
134 return Err(BookIntegrityError::TooManyLevels(
135 OrderSide::Sell,
136 book.asks.len(),
137 ));
138 }
139 }
140 BookType::L2_MBP => {
141 for bid_level in book.bids.levels.values() {
142 let num_orders = bid_level.orders.len();
143 if num_orders > 1 {
144 return Err(BookIntegrityError::TooManyOrders(
145 OrderSide::Buy,
146 num_orders,
147 ));
148 }
149 }
150
151 for ask_level in book.asks.levels.values() {
152 let num_orders = ask_level.orders.len();
153 if num_orders > 1 {
154 return Err(BookIntegrityError::TooManyOrders(
155 OrderSide::Sell,
156 num_orders,
157 ));
158 }
159 }
160 }
161 BookType::L3_MBO => {}
162 };
163
164 if let (Some(top_bid_level), Some(top_ask_level)) = (book.bids.top(), book.asks.top()) {
165 let best_bid = top_bid_level.price;
166 let best_ask = top_ask_level.price;
167
168 if best_bid.value >= best_ask.value {
169 return Err(BookIntegrityError::OrdersCrossed(best_bid, best_ask));
170 }
171 }
172
173 Ok(())
174}