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