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(|(price, _)| price.value.as_f64())
97 .unwrap_or(0.0);
98
99 for (book_price, level) in levels {
100 let price = book_price.value.as_f64();
101 final_price = price;
102
103 let level_exposure = price * level.size_raw() as f64;
104 let exposure_this_level =
105 level_exposure.min(target_exposure.raw as f64 - cumulative_exposure);
106 let size_this_level = (exposure_this_level / price).floor() as QuantityRaw;
107
108 cumulative_exposure += price * size_this_level as f64;
109 cumulative_size_raw += size_this_level;
110
111 if cumulative_exposure >= target_exposure.as_f64() {
112 break;
113 }
114 }
115
116 if cumulative_size_raw == 0 {
117 (0.0, 0.0, final_price)
118 } else {
119 let avg_price = cumulative_exposure / cumulative_size_raw as f64;
120 (
121 avg_price,
122 cumulative_size_raw as f64 / FIXED_SCALAR,
123 final_price,
124 )
125 }
126}
127
128pub fn book_check_integrity(book: &OrderBook) -> Result<(), BookIntegrityError> {
134 match book.book_type {
135 BookType::L1_MBP => {
136 if book.bids.len() > 1 {
137 return Err(BookIntegrityError::TooManyLevels(
138 OrderSide::Buy,
139 book.bids.len(),
140 ));
141 }
142 if book.asks.len() > 1 {
143 return Err(BookIntegrityError::TooManyLevels(
144 OrderSide::Sell,
145 book.asks.len(),
146 ));
147 }
148 }
149 BookType::L2_MBP => {
150 for bid_level in book.bids.levels.values() {
151 let num_orders = bid_level.orders.len();
152 if num_orders > 1 {
153 return Err(BookIntegrityError::TooManyOrders(
154 OrderSide::Buy,
155 num_orders,
156 ));
157 }
158 }
159
160 for ask_level in book.asks.levels.values() {
161 let num_orders = ask_level.orders.len();
162 if num_orders > 1 {
163 return Err(BookIntegrityError::TooManyOrders(
164 OrderSide::Sell,
165 num_orders,
166 ));
167 }
168 }
169 }
170 BookType::L3_MBO => {}
171 };
172
173 if let (Some(top_bid_level), Some(top_ask_level)) = (book.bids.top(), book.asks.top()) {
174 let best_bid = top_bid_level.price;
175 let best_ask = top_ask_level.price;
176
177 if best_bid.value >= best_ask.value {
178 return Err(BookIntegrityError::OrdersCrossed(best_bid, best_ask));
179 }
180 }
181
182 Ok(())
183}