nautilus_model/orderbook/
analysis.rs1use std::collections::BTreeMap;
19
20use nautilus_core::correctness::{CorrectnessResult, CorrectnessResultExt};
21#[cfg(not(feature = "defi"))]
22use rust_decimal::Decimal;
23
24use super::{BookLevel, BookPrice, OrderBook};
25#[cfg(feature = "defi")]
26pub use crate::defi::orderbook::{
27 get_avg_px_for_quantity, get_avg_px_qty_for_exposure, get_worst_px_for_quantity,
28};
29#[cfg(not(feature = "defi"))]
30use crate::types::{fixed::FIXED_SCALAR, quantity::QuantityRaw};
31use crate::{
32 enums::{BookType, OrderSide},
33 orderbook::BookIntegrityError,
34 types::{Price, Quantity, fixed::check_fixed_precision},
35};
36
37#[must_use]
40pub fn get_quantity_for_price(
41 price: Price,
42 order_side: OrderSide,
43 levels: &BTreeMap<BookPrice, BookLevel>,
44) -> f64 {
45 let mut matched_size: f64 = 0.0;
46
47 for (book_price, level) in levels {
48 if !is_level_within_price(order_side, book_price.value, price) {
49 break;
50 }
51
52 matched_size += level.size();
53 }
54
55 matched_size
56}
57
58#[must_use]
70pub fn get_levels_for_price(
71 price: Price,
72 order_side: OrderSide,
73 levels: &BTreeMap<BookPrice, BookLevel>,
74 size_precision: u8,
75) -> Vec<(Price, Quantity)> {
76 get_levels_for_price_checked(price, order_side, levels, size_precision)
77 .expect_display("Failed to collect crossed order book levels")
78}
79
80pub(crate) fn get_levels_for_price_checked(
88 price: Price,
89 order_side: OrderSide,
90 levels: &BTreeMap<BookPrice, BookLevel>,
91 size_precision: u8,
92) -> CorrectnessResult<Vec<(Price, Quantity)>> {
93 check_fixed_precision(size_precision)?;
94 let mut result = Vec::new();
95
96 for (book_price, level) in levels {
97 if !is_level_within_price(order_side, book_price.value, price) {
98 break;
99 }
100
101 let level_size = Quantity::from_raw_checked(level.size_raw_checked()?, size_precision)?;
102 result.push((level.price.value, level_size));
103 }
104
105 Ok(result)
106}
107
108fn is_level_within_price(order_side: OrderSide, level_price: Price, limit_price: Price) -> bool {
109 match order_side {
110 OrderSide::Buy => level_price <= limit_price,
111 OrderSide::Sell => level_price >= limit_price,
112 }
113}
114
115#[cfg(not(feature = "defi"))]
122#[must_use]
123pub fn get_avg_px_for_quantity(qty: Quantity, levels: &BTreeMap<BookPrice, BookLevel>) -> f64 {
124 let mut cumulative_size_raw: QuantityRaw = 0;
125 let mut cumulative_size = Decimal::ZERO;
126 let mut cumulative_value = Decimal::ZERO;
127
128 for (book_price, level) in levels {
129 let size_this_level = level.size_raw().min(qty.raw() - cumulative_size_raw);
130 let size_this_level_decimal = Quantity::raw_as_decimal(size_this_level);
131 cumulative_size_raw += size_this_level;
132 cumulative_size += size_this_level_decimal;
133 cumulative_value += book_price.value.as_decimal() * size_this_level_decimal;
134
135 if cumulative_size_raw >= qty.raw() {
136 break;
137 }
138 }
139
140 if cumulative_size_raw == 0 {
141 0.0
142 } else {
143 (cumulative_value / cumulative_size)
144 .to_string()
145 .parse::<f64>()
146 .expect("Decimal average price must parse as f64")
147 }
148}
149
150#[cfg(not(feature = "defi"))]
156#[must_use]
157pub fn get_worst_px_for_quantity(
158 qty: Quantity,
159 levels: &BTreeMap<BookPrice, BookLevel>,
160) -> Option<Price> {
161 let mut cumulative_size_raw: QuantityRaw = 0;
162 let mut worst_price: Option<Price> = None;
163
164 for (book_price, level) in levels {
165 let size_this_level = level.size_raw().min(qty.raw() - cumulative_size_raw);
166
167 if size_this_level == 0 {
168 continue;
169 }
170
171 cumulative_size_raw += size_this_level;
172 worst_price = Some(book_price.value);
173
174 if cumulative_size_raw >= qty.raw() {
175 break;
176 }
177 }
178
179 if cumulative_size_raw == 0 {
180 None
181 } else {
182 worst_price
183 }
184}
185
186#[cfg(not(feature = "defi"))]
189#[must_use]
190pub fn get_avg_px_qty_for_exposure(
191 target_exposure: Quantity,
192 levels: &BTreeMap<BookPrice, BookLevel>,
193) -> (f64, f64, f64) {
194 let mut cumulative_exposure = 0.0;
195 let mut cumulative_size_raw: QuantityRaw = 0;
196 let mut final_price = levels
197 .first_key_value()
198 .map_or(0.0, |(price, _)| price.value.as_f64());
199
200 let target_exposure_raw = target_exposure.raw() as f64;
201
202 for (book_price, level) in levels {
203 let price = book_price.value.as_f64();
204
205 if price == 0.0 {
206 continue;
207 }
208
209 let level_exposure = price * level.size_raw() as f64;
210 let exposure_this_level = level_exposure.min(target_exposure_raw - cumulative_exposure);
211 let size_this_level = (exposure_this_level / price).floor() as QuantityRaw;
212
213 if size_this_level == 0 {
214 continue;
215 }
216
217 final_price = price;
218 cumulative_exposure += price * size_this_level as f64;
219 cumulative_size_raw += size_this_level;
220
221 if cumulative_exposure >= target_exposure_raw {
222 break;
223 }
224 }
225
226 if cumulative_size_raw == 0 {
227 (0.0, 0.0, final_price)
228 } else {
229 let avg_price = cumulative_exposure / cumulative_size_raw as f64;
230 (
231 avg_price,
232 cumulative_size_raw as f64 / FIXED_SCALAR,
233 final_price,
234 )
235 }
236}
237
238pub fn book_check_integrity(book: &OrderBook) -> Result<(), BookIntegrityError> {
244 match book.book_type {
245 BookType::L1_MBP => {
246 for (side, ladder) in [(OrderSide::Buy, &book.bids), (OrderSide::Sell, &book.asks)] {
247 let level_count = ladder.len();
248
249 if level_count > 1 {
250 return Err(BookIntegrityError::TooManyLevels(side, level_count));
251 }
252 }
253 }
254 BookType::L2_MBP => {
255 for (side, ladder) in [(OrderSide::Buy, &book.bids), (OrderSide::Sell, &book.asks)] {
256 for level in ladder.levels.values() {
257 let order_count = level.orders.len();
258
259 if order_count > 1 {
260 return Err(BookIntegrityError::TooManyOrders(side, order_count));
261 }
262 }
263 }
264 }
265 BookType::L3_MBO => {}
266 }
267
268 if let (Some(top_bid_level), Some(top_ask_level)) = (book.bids.top(), book.asks.top()) {
269 let best_bid = top_bid_level.price;
270 let best_ask = top_ask_level.price;
271
272 if best_bid.value > best_ask.value {
274 return Err(BookIntegrityError::OrdersCrossed(best_bid, best_ask));
275 }
276 }
277
278 Ok(())
279}