nautilus_model/orderbook/
display.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 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]
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 = Table::new(data).with(Style::rounded()).to_string();
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]
135pub(crate) fn pprint_own_book(
136    own_order_book: &OwnOrderBook,
137    num_levels: usize,
138    group_size: Option<Decimal>,
139) -> String {
140    let data: Vec<BookLevelDisplay> = if let Some(group_size) = group_size {
141        let bid_quantities =
142            own_order_book.bid_quantity(None, Some(num_levels), Some(group_size), None, None);
143        let ask_quantities =
144            own_order_book.ask_quantity(None, Some(num_levels), Some(group_size), None, None);
145
146        // Use the precision of the group_size for consistent formatting
147        let precision = group_size.scale();
148
149        let mut data = Vec::new();
150
151        // Add ask levels (highest to lowest)
152        for (price, qty) in ask_quantities.iter().rev() {
153            data.push(BookLevelDisplay {
154                bids: String::new(),
155                price: format!("{price:.precision$}", precision = precision as usize),
156                asks: qty.to_string(),
157            });
158        }
159
160        // Add bid levels (highest to lowest)
161        for (price, qty) in &bid_quantities {
162            data.push(BookLevelDisplay {
163                bids: qty.to_string(),
164                price: format!("{price:.precision$}", precision = precision as usize),
165                asks: String::new(),
166            });
167        }
168
169        data
170    } else {
171        let ask_levels: Vec<(&BookPrice, &OwnBookLevel)> = own_order_book
172            .asks
173            .levels
174            .iter()
175            .take(num_levels)
176            .rev()
177            .collect();
178        let bid_levels: Vec<(&BookPrice, &OwnBookLevel)> =
179            own_order_book.bids.levels.iter().take(num_levels).collect();
180        let levels: Vec<(&BookPrice, &OwnBookLevel)> =
181            ask_levels.into_iter().chain(bid_levels).collect();
182
183        levels
184            .iter()
185            .map(|(book_price, level)| {
186                let is_bid_level = book_price.side == OrderSideSpecified::Buy;
187                let is_ask_level = book_price.side == OrderSideSpecified::Sell;
188
189                let bid_sizes: Vec<String> = level
190                    .orders
191                    .iter()
192                    .filter(|_| is_bid_level)
193                    .map(|order| format!("{}", order.1.size))
194                    .collect();
195
196                let ask_sizes: Vec<String> = level
197                    .orders
198                    .iter()
199                    .filter(|_| is_ask_level)
200                    .map(|order| format!("{}", order.1.size))
201                    .collect();
202
203                BookLevelDisplay {
204                    bids: if bid_sizes.is_empty() {
205                        String::new()
206                    } else {
207                        format!("[{}]", bid_sizes.join(", "))
208                    },
209                    price: format!("{}", level.price),
210                    asks: if ask_sizes.is_empty() {
211                        String::new()
212                    } else {
213                        format!("[{}]", ask_sizes.join(", "))
214                    },
215                }
216            })
217            .collect()
218    };
219
220    let table = Table::new(data).with(Style::rounded()).to_string();
221
222    let header = format!(
223        "bid_levels: {}\nask_levels: {}\nupdate_count: {}\nts_last: {}",
224        own_order_book.bids.levels.len(),
225        own_order_book.asks.levels.len(),
226        own_order_book.update_count,
227        own_order_book.ts_last,
228    );
229
230    format!("{header}\n{table}")
231}