nautilus_model/orderbook/
analysis.rs1use std::collections::BTreeMap;
19
20use super::{BookLevel, BookPrice, OrderBook};
21use crate::{
22 enums::{BookType, OrderSide, OrderSideSpecified},
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: OrderSideSpecified,
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 OrderSideSpecified::Buy => {
40 if book_price.value > price {
41 break;
42 }
43 }
44 OrderSideSpecified::Sell => {
45 if book_price.value < price {
46 break;
47 }
48 }
49 }
50 matched_size += level.size();
51 }
52
53 matched_size
54}
55
56#[must_use]
62pub fn get_levels_for_price(
63 price: Price,
64 order_side: OrderSideSpecified,
65 levels: &BTreeMap<BookPrice, BookLevel>,
66 size_precision: u8,
67) -> Vec<(Price, Quantity)> {
68 let mut result = Vec::new();
69
70 for (book_price, level) in levels {
71 match order_side {
72 OrderSideSpecified::Buy => {
73 if book_price.value > price {
74 break;
75 }
76 }
77 OrderSideSpecified::Sell => {
78 if book_price.value < price {
79 break;
80 }
81 }
82 }
83 let level_size = Quantity::new(level.size(), size_precision);
84 result.push((level.price.value, level_size));
85 }
86
87 result
88}
89
90#[must_use]
93pub fn get_avg_px_for_quantity(qty: Quantity, levels: &BTreeMap<BookPrice, BookLevel>) -> f64 {
94 let mut cumulative_size_raw: QuantityRaw = 0;
95 let mut cumulative_value = 0.0;
96
97 for (book_price, level) in levels {
98 let size_this_level = level.size_raw().min(qty.raw - cumulative_size_raw);
99 cumulative_size_raw += size_this_level;
100 cumulative_value += book_price.value.as_f64() * size_this_level as f64;
101
102 if cumulative_size_raw >= qty.raw {
103 break;
104 }
105 }
106
107 if cumulative_size_raw == 0 {
108 0.0
109 } else {
110 cumulative_value / cumulative_size_raw as f64
111 }
112}
113
114#[must_use]
120pub fn get_worst_px_for_quantity(
121 qty: Quantity,
122 levels: &BTreeMap<BookPrice, BookLevel>,
123) -> Option<Price> {
124 let mut cumulative_size_raw: QuantityRaw = 0;
125 let mut worst_price: Option<Price> = None;
126
127 for (book_price, level) in levels {
128 let size_this_level = level.size_raw().min(qty.raw - cumulative_size_raw);
129
130 if size_this_level == 0 {
131 continue;
132 }
133
134 cumulative_size_raw += size_this_level;
135 worst_price = Some(book_price.value);
136
137 if cumulative_size_raw >= qty.raw {
138 break;
139 }
140 }
141
142 if cumulative_size_raw == 0 {
143 None
144 } else {
145 worst_price
146 }
147}
148
149#[must_use]
152pub fn get_avg_px_qty_for_exposure(
153 target_exposure: Quantity,
154 levels: &BTreeMap<BookPrice, BookLevel>,
155) -> (f64, f64, f64) {
156 let mut cumulative_exposure = 0.0;
157 let mut cumulative_size_raw: QuantityRaw = 0;
158 let mut final_price = levels
159 .first_key_value()
160 .map_or(0.0, |(price, _)| price.value.as_f64());
161
162 let target_exposure_raw = target_exposure.raw as f64;
163
164 for (book_price, level) in levels {
165 let price = book_price.value.as_f64();
166
167 if price == 0.0 {
168 continue;
169 }
170
171 let level_exposure = price * level.size_raw() as f64;
172 let exposure_this_level = level_exposure.min(target_exposure_raw - cumulative_exposure);
173 let size_this_level = (exposure_this_level / price).floor() as QuantityRaw;
174
175 if size_this_level == 0 {
176 continue;
177 }
178
179 final_price = price;
180 cumulative_exposure += price * size_this_level as f64;
181 cumulative_size_raw += size_this_level;
182
183 if cumulative_exposure >= target_exposure_raw {
184 break;
185 }
186 }
187
188 if cumulative_size_raw == 0 {
189 (0.0, 0.0, final_price)
190 } else {
191 let avg_price = cumulative_exposure / cumulative_size_raw as f64;
192 (
193 avg_price,
194 cumulative_size_raw as f64 / FIXED_SCALAR,
195 final_price,
196 )
197 }
198}
199
200pub fn book_check_integrity(book: &OrderBook) -> Result<(), BookIntegrityError> {
206 match book.book_type {
207 BookType::L1_MBP => {
208 if book.bids.len() > 1 {
209 return Err(BookIntegrityError::TooManyLevels(
210 OrderSide::Buy,
211 book.bids.len(),
212 ));
213 }
214
215 if book.asks.len() > 1 {
216 return Err(BookIntegrityError::TooManyLevels(
217 OrderSide::Sell,
218 book.asks.len(),
219 ));
220 }
221 }
222 BookType::L2_MBP => {
223 for bid_level in book.bids.levels.values() {
224 let num_orders = bid_level.orders.len();
225 if num_orders > 1 {
226 return Err(BookIntegrityError::TooManyOrders(
227 OrderSide::Buy,
228 num_orders,
229 ));
230 }
231 }
232
233 for ask_level in book.asks.levels.values() {
234 let num_orders = ask_level.orders.len();
235 if num_orders > 1 {
236 return Err(BookIntegrityError::TooManyOrders(
237 OrderSide::Sell,
238 num_orders,
239 ));
240 }
241 }
242 }
243 BookType::L3_MBO => {}
244 }
245
246 if let (Some(top_bid_level), Some(top_ask_level)) = (book.bids.top(), book.asks.top()) {
247 let best_bid = top_bid_level.price;
248 let best_ask = top_ask_level.price;
249
250 if best_bid.value > best_ask.value {
252 return Err(BookIntegrityError::OrdersCrossed(best_bid, best_ask));
253 }
254 }
255
256 Ok(())
257}