nautilus_model/orderbook/
analysis.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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///
30/// # Panics
31///
32/// Panics if `order_side` is neither [`OrderSide::Buy`] nor [`OrderSide::Sell`].
33#[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/// Calculates the estimated average price for a specified quantity from a set of
62/// order book levels.
63#[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/// Calculates the estimated average price for a specified exposure from a set of
86/// order book levels.
87#[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
128/// Checks the integrity of the given order `book`.
129///
130/// # Errors
131///
132/// Returns an error if a book integrity check fails.
133pub 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}