nautilus_model/orderbook/
analysis.rs1use std::collections::BTreeMap;
19
20use rust_decimal::Decimal;
21
22use super::{BookLevel, BookPrice, OrderBook};
23use crate::{
24 enums::{BookType, OrderSide},
25 orderbook::BookIntegrityError,
26 types::{Price, Quantity, fixed::FIXED_SCALAR, quantity::QuantityRaw},
27};
28
29#[must_use]
32pub fn get_quantity_for_price(
33 price: Price,
34 order_side: OrderSide,
35 levels: &BTreeMap<BookPrice, BookLevel>,
36) -> f64 {
37 let mut matched_size: f64 = 0.0;
38
39 for (book_price, level) in levels {
40 if !is_level_within_price(order_side, book_price.value, price) {
41 break;
42 }
43
44 matched_size += level.size();
45 }
46
47 matched_size
48}
49
50#[must_use]
56pub fn get_levels_for_price(
57 price: Price,
58 order_side: OrderSide,
59 levels: &BTreeMap<BookPrice, BookLevel>,
60 size_precision: u8,
61) -> Vec<(Price, Quantity)> {
62 let mut result = Vec::new();
63
64 for (book_price, level) in levels {
65 if !is_level_within_price(order_side, book_price.value, price) {
66 break;
67 }
68
69 let level_size = Quantity::from_raw(level.size_raw(), size_precision);
70 result.push((level.price.value, level_size));
71 }
72
73 result
74}
75
76fn is_level_within_price(order_side: OrderSide, level_price: Price, limit_price: Price) -> bool {
77 match order_side {
78 OrderSide::Buy => level_price <= limit_price,
79 OrderSide::Sell => level_price >= limit_price,
80 }
81}
82
83#[must_use]
90pub fn get_avg_px_for_quantity(qty: Quantity, levels: &BTreeMap<BookPrice, BookLevel>) -> f64 {
91 let mut cumulative_size_raw: QuantityRaw = 0;
92 let mut cumulative_size = Decimal::ZERO;
93 let mut cumulative_value = Decimal::ZERO;
94
95 for (book_price, level) in levels {
96 let size_this_level = level.size_raw().min(qty.raw - cumulative_size_raw);
97 let size_this_level_decimal = Quantity::raw_as_decimal(size_this_level);
98 cumulative_size_raw += size_this_level;
99 cumulative_size += size_this_level_decimal;
100 cumulative_value += book_price.value.as_decimal() * size_this_level_decimal;
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)
111 .to_string()
112 .parse::<f64>()
113 .expect("Decimal average price must parse as f64")
114 }
115}
116
117#[must_use]
123pub fn get_worst_px_for_quantity(
124 qty: Quantity,
125 levels: &BTreeMap<BookPrice, BookLevel>,
126) -> Option<Price> {
127 let mut cumulative_size_raw: QuantityRaw = 0;
128 let mut worst_price: Option<Price> = None;
129
130 for (book_price, level) in levels {
131 let size_this_level = level.size_raw().min(qty.raw - cumulative_size_raw);
132
133 if size_this_level == 0 {
134 continue;
135 }
136
137 cumulative_size_raw += size_this_level;
138 worst_price = Some(book_price.value);
139
140 if cumulative_size_raw >= qty.raw {
141 break;
142 }
143 }
144
145 if cumulative_size_raw == 0 {
146 None
147 } else {
148 worst_price
149 }
150}
151
152#[must_use]
155pub fn get_avg_px_qty_for_exposure(
156 target_exposure: Quantity,
157 levels: &BTreeMap<BookPrice, BookLevel>,
158) -> (f64, f64, f64) {
159 let mut cumulative_exposure = 0.0;
160 let mut cumulative_size_raw: QuantityRaw = 0;
161 let mut final_price = levels
162 .first_key_value()
163 .map_or(0.0, |(price, _)| price.value.as_f64());
164
165 let target_exposure_raw = target_exposure.raw as f64;
166
167 for (book_price, level) in levels {
168 let price = book_price.value.as_f64();
169
170 if price == 0.0 {
171 continue;
172 }
173
174 let level_exposure = price * level.size_raw() as f64;
175 let exposure_this_level = level_exposure.min(target_exposure_raw - cumulative_exposure);
176 let size_this_level = (exposure_this_level / price).floor() as QuantityRaw;
177
178 if size_this_level == 0 {
179 continue;
180 }
181
182 final_price = price;
183 cumulative_exposure += price * size_this_level as f64;
184 cumulative_size_raw += size_this_level;
185
186 if cumulative_exposure >= target_exposure_raw {
187 break;
188 }
189 }
190
191 if cumulative_size_raw == 0 {
192 (0.0, 0.0, final_price)
193 } else {
194 let avg_price = cumulative_exposure / cumulative_size_raw as f64;
195 (
196 avg_price,
197 cumulative_size_raw as f64 / FIXED_SCALAR,
198 final_price,
199 )
200 }
201}
202
203pub fn book_check_integrity(book: &OrderBook) -> Result<(), BookIntegrityError> {
209 match book.book_type {
210 BookType::L1_MBP => {
211 for (side, ladder) in [(OrderSide::Buy, &book.bids), (OrderSide::Sell, &book.asks)] {
212 let level_count = ladder.len();
213
214 if level_count > 1 {
215 return Err(BookIntegrityError::TooManyLevels(side, level_count));
216 }
217 }
218 }
219 BookType::L2_MBP => {
220 for (side, ladder) in [(OrderSide::Buy, &book.bids), (OrderSide::Sell, &book.asks)] {
221 for level in ladder.levels.values() {
222 let order_count = level.orders.len();
223
224 if order_count > 1 {
225 return Err(BookIntegrityError::TooManyOrders(side, order_count));
226 }
227 }
228 }
229 }
230 BookType::L3_MBO => {}
231 }
232
233 if let (Some(top_bid_level), Some(top_ask_level)) = (book.bids.top(), book.asks.top()) {
234 let best_bid = top_bid_level.price;
235 let best_ask = top_ask_level.price;
236
237 if best_bid.value > best_ask.value {
239 return Err(BookIntegrityError::OrdersCrossed(best_bid, best_ask));
240 }
241 }
242
243 Ok(())
244}