nautilus_model/orderbook/
display.rs1use 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#[must_use]
35#[expect(clippy::needless_collect)] pub(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 let precision = group_size.scale();
47
48 let mut data = Vec::new();
49
50 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 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#[must_use]
135#[expect(clippy::needless_collect)] pub(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 let precision = group_size.scale();
149
150 let mut data = Vec::new();
151
152 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 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}