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