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