nautilus_model/orderbook/
analysis.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Functions related to order book analysis.
17
18use 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/// Calculates the estimated fill quantity for a specified price from a set of
28/// order book levels and order side.
29#[must_use]
30pub fn get_quantity_for_price(
31    price: Price,
32    order_side: OrderSide,
33    levels: &BTreeMap<BookPrice, BookLevel>,
34) -> f64 {
35    let mut matched_size: f64 = 0.0;
36
37    for (book_price, level) in levels {
38        match order_side {
39            OrderSide::Buy => {
40                if book_price.value > price {
41                    break;
42                }
43            }
44            OrderSide::Sell => {
45                if book_price.value < price {
46                    break;
47                }
48            }
49            _ => panic!("Invalid `OrderSide` {order_side}"),
50        }
51        matched_size += level.size();
52    }
53
54    matched_size
55}
56
57/// Calculates the estimated average price for a specified quantity from a set of
58/// order book levels.
59#[must_use]
60pub fn get_avg_px_for_quantity(qty: Quantity, levels: &BTreeMap<BookPrice, BookLevel>) -> f64 {
61    let mut cumulative_size_raw: QuantityRaw = 0;
62    let mut cumulative_value = 0.0;
63
64    for (book_price, level) in levels {
65        let size_this_level = level.size_raw().min(qty.raw - cumulative_size_raw);
66        cumulative_size_raw += size_this_level;
67        cumulative_value += book_price.value.as_f64() * size_this_level as f64;
68
69        if cumulative_size_raw >= qty.raw {
70            break;
71        }
72    }
73
74    if cumulative_size_raw == 0 {
75        0.0
76    } else {
77        cumulative_value / cumulative_size_raw as f64
78    }
79}
80
81/// Calculates the estimated average price for a specified exposure from a set of
82/// order book levels.
83#[must_use]
84pub fn get_avg_px_qty_for_exposure(
85    target_exposure: Quantity,
86    levels: &BTreeMap<BookPrice, BookLevel>,
87) -> (f64, f64, f64) {
88    let mut cumulative_exposure = 0.0;
89    let mut cumulative_size_raw: QuantityRaw = 0;
90    let mut final_price = levels
91        .first_key_value()
92        .map(|(price, _)| price.value.as_f64())
93        .unwrap_or(0.0);
94
95    for (book_price, level) in levels {
96        let price = book_price.value.as_f64();
97        final_price = price;
98
99        let level_exposure = price * level.size_raw() as f64;
100        let exposure_this_level =
101            level_exposure.min(target_exposure.raw as f64 - cumulative_exposure);
102        let size_this_level = (exposure_this_level / price).floor() as QuantityRaw;
103
104        cumulative_exposure += price * size_this_level as f64;
105        cumulative_size_raw += size_this_level;
106
107        if cumulative_exposure >= target_exposure.as_f64() {
108            break;
109        }
110    }
111
112    if cumulative_size_raw == 0 {
113        (0.0, 0.0, final_price)
114    } else {
115        let avg_price = cumulative_exposure / cumulative_size_raw as f64;
116        (
117            avg_price,
118            cumulative_size_raw as f64 / FIXED_SCALAR,
119            final_price,
120        )
121    }
122}
123
124pub fn book_check_integrity(book: &OrderBook) -> Result<(), BookIntegrityError> {
125    match book.book_type {
126        BookType::L1_MBP => {
127            if book.bids.len() > 1 {
128                return Err(BookIntegrityError::TooManyLevels(
129                    OrderSide::Buy,
130                    book.bids.len(),
131                ));
132            }
133            if book.asks.len() > 1 {
134                return Err(BookIntegrityError::TooManyLevels(
135                    OrderSide::Sell,
136                    book.asks.len(),
137                ));
138            }
139        }
140        BookType::L2_MBP => {
141            for bid_level in book.bids.levels.values() {
142                let num_orders = bid_level.orders.len();
143                if num_orders > 1 {
144                    return Err(BookIntegrityError::TooManyOrders(
145                        OrderSide::Buy,
146                        num_orders,
147                    ));
148                }
149            }
150
151            for ask_level in book.asks.levels.values() {
152                let num_orders = ask_level.orders.len();
153                if num_orders > 1 {
154                    return Err(BookIntegrityError::TooManyOrders(
155                        OrderSide::Sell,
156                        num_orders,
157                    ));
158                }
159            }
160        }
161        BookType::L3_MBO => {}
162    };
163
164    if let (Some(top_bid_level), Some(top_ask_level)) = (book.bids.top(), book.asks.top()) {
165        let best_bid = top_bid_level.price;
166        let best_ask = top_ask_level.price;
167
168        if best_bid.value >= best_ask.value {
169            return Err(BookIntegrityError::OrdersCrossed(best_bid, best_ask));
170        }
171    }
172
173    Ok(())
174}