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