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