Skip to main content

nautilus_model/orderbook/
display.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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 display.
17
18use indexmap::IndexMap;
19use rust_decimal::Decimal;
20use tabled::{builder::Builder, settings::Style};
21
22use super::{BookPrice, level::BookLevel, own::OwnBookLevel};
23use crate::{
24    enums::OrderSide,
25    orderbook::{OrderBook, own::OwnOrderBook},
26};
27
28struct BookLevelDisplay {
29    bids: String,
30    price: String,
31    asks: String,
32}
33
34/// Return a [`String`] representation of the order book in a human-readable table format.
35#[must_use]
36#[expect(clippy::needless_collect)] // Collect needed for .rev() and .chain()
37pub(crate) fn pprint_book(
38    order_book: &OrderBook,
39    num_levels: usize,
40    group_size: Option<Decimal>,
41) -> String {
42    let data: Vec<BookLevelDisplay> = if let Some(group_size) = group_size {
43        let bid_quantities = order_book.group_bids(group_size, Some(num_levels));
44        let ask_quantities = order_book.group_asks(group_size, Some(num_levels));
45
46        grouped_levels(
47            &bid_quantities,
48            &ask_quantities,
49            group_size.scale() as usize,
50        )
51    } else {
52        let ask_levels: Vec<(&BookPrice, &BookLevel)> = order_book
53            .asks
54            .levels
55            .iter()
56            .take(num_levels)
57            .rev()
58            .collect();
59        let bid_levels: Vec<(&BookPrice, &BookLevel)> =
60            order_book.bids.levels.iter().take(num_levels).collect();
61        let levels: Vec<(&BookPrice, &BookLevel)> =
62            ask_levels.into_iter().chain(bid_levels).collect();
63
64        levels
65            .iter()
66            .map(|(book_price, level)| {
67                let is_bid_level = book_price.side == OrderSide::Buy;
68                let is_ask_level = book_price.side == OrderSide::Sell;
69
70                let bid_sizes: Vec<String> = level
71                    .orders
72                    .iter()
73                    .filter(|_| is_bid_level)
74                    .map(|order| format!("{}", order.1.size))
75                    .collect();
76
77                let ask_sizes: Vec<String> = level
78                    .orders
79                    .iter()
80                    .filter(|_| is_ask_level)
81                    .map(|order| format!("{}", order.1.size))
82                    .collect();
83
84                BookLevelDisplay {
85                    bids: if bid_sizes.is_empty() {
86                        String::new()
87                    } else {
88                        format!("[{}]", bid_sizes.join(", "))
89                    },
90                    price: format!("{}", level.price),
91                    asks: if ask_sizes.is_empty() {
92                        String::new()
93                    } else {
94                        format!("[{}]", ask_sizes.join(", "))
95                    },
96                }
97            })
98            .collect()
99    };
100
101    let table = render_book_levels(data);
102
103    let header = format!(
104        "bid_levels: {}\nask_levels: {}\nsequence: {}\nupdate_count: {}\nts_last: {}",
105        order_book.bids.levels.len(),
106        order_book.asks.levels.len(),
107        order_book.sequence,
108        order_book.update_count,
109        order_book.ts_last,
110    );
111
112    format!("{header}\n{table}")
113}
114
115/// Return a [`String`] representation of the own order book in a human-readable table format.
116#[must_use]
117#[expect(clippy::needless_collect)] // Collect needed for .rev() and .chain()
118pub(crate) fn pprint_own_book(
119    own_order_book: &OwnOrderBook,
120    num_levels: usize,
121    group_size: Option<Decimal>,
122) -> String {
123    let data: Vec<BookLevelDisplay> = if let Some(group_size) = group_size {
124        // Rendering is membership-neutral, so acceptance-time filtering stays disabled.
125        let bid_quantities =
126            own_order_book.bid_quantity(None, Some(num_levels), Some(group_size), None, None);
127        let ask_quantities =
128            own_order_book.ask_quantity(None, Some(num_levels), Some(group_size), None, None);
129
130        grouped_levels(
131            &bid_quantities,
132            &ask_quantities,
133            group_size.scale() as usize,
134        )
135    } else {
136        let ask_levels: Vec<(&BookPrice, &OwnBookLevel)> = own_order_book
137            .asks
138            .levels
139            .iter()
140            .take(num_levels)
141            .rev()
142            .collect();
143        let bid_levels: Vec<(&BookPrice, &OwnBookLevel)> =
144            own_order_book.bids.levels.iter().take(num_levels).collect();
145        let levels: Vec<(&BookPrice, &OwnBookLevel)> =
146            ask_levels.into_iter().chain(bid_levels).collect();
147
148        levels
149            .iter()
150            .map(|(book_price, level)| {
151                let is_bid_level = book_price.side == OrderSide::Buy;
152                let is_ask_level = book_price.side == OrderSide::Sell;
153
154                let bid_sizes: Vec<String> = level
155                    .orders
156                    .iter()
157                    .filter(|_| is_bid_level)
158                    .map(|order| format!("{}", order.1.size))
159                    .collect();
160
161                let ask_sizes: Vec<String> = level
162                    .orders
163                    .iter()
164                    .filter(|_| is_ask_level)
165                    .map(|order| format!("{}", order.1.size))
166                    .collect();
167
168                BookLevelDisplay {
169                    bids: if bid_sizes.is_empty() {
170                        String::new()
171                    } else {
172                        format!("[{}]", bid_sizes.join(", "))
173                    },
174                    price: format!("{}", level.price),
175                    asks: if ask_sizes.is_empty() {
176                        String::new()
177                    } else {
178                        format!("[{}]", ask_sizes.join(", "))
179                    },
180                }
181            })
182            .collect()
183    };
184
185    let table = render_book_levels(data);
186
187    let header = format!(
188        "bid_levels: {}\nask_levels: {}\nupdate_count: {}\nts_last: {}",
189        own_order_book.bids.levels.len(),
190        own_order_book.asks.levels.len(),
191        own_order_book.update_count,
192        own_order_book.ts_last,
193    );
194
195    format!("{header}\n{table}")
196}
197
198fn grouped_levels(
199    bid_quantities: &IndexMap<Decimal, Decimal>,
200    ask_quantities: &IndexMap<Decimal, Decimal>,
201    precision: usize,
202) -> Vec<BookLevelDisplay> {
203    let mut data = Vec::with_capacity(bid_quantities.len() + ask_quantities.len());
204
205    for (price, quantity) in ask_quantities.iter().rev() {
206        data.push(BookLevelDisplay {
207            bids: String::new(),
208            price: format!("{price:.precision$}"),
209            asks: quantity.to_string(),
210        });
211    }
212
213    for (price, quantity) in bid_quantities {
214        data.push(BookLevelDisplay {
215            bids: quantity.to_string(),
216            price: format!("{price:.precision$}"),
217            asks: String::new(),
218        });
219    }
220
221    data
222}
223
224fn render_book_levels(data: Vec<BookLevelDisplay>) -> String {
225    let mut builder = Builder::with_capacity(data.len() + 1, 3);
226    builder.push_record(["bids", "price", "asks"]);
227
228    for level in data {
229        builder.push_record([level.bids, level.price, level.asks]);
230    }
231
232    builder.build().with(Style::rounded()).to_string()
233}