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