1use std::iter;
2
3use colored::Colorize;
4use fastnum::UD64;
5use tabled::{
6 Table,
7 settings::{
8 Alignment, Panel, Style, Width,
9 object::{Row, Rows},
10 },
11};
12
13use super::{BookLevel, OrderBook};
14use crate::state::Order;
15
16pub trait OrderHighlight {
23 fn highlight(&self, order: &Order, rendered: &str) -> Option<String>;
26}
27
28pub struct OrderBookView<'a> {
32 book: &'a OrderBook,
33 depth: Option<usize>,
34 orders_per_level: Option<usize>,
35 show_expired: bool,
36 highlight: Option<&'a dyn OrderHighlight>,
37}
38
39impl<'a> OrderBookView<'a> {
40 pub(crate) fn new(
41 book: &'a OrderBook,
42 depth: Option<usize>,
43 orders_per_level: Option<usize>,
44 show_expired: bool,
45 ) -> Self {
46 Self { book, depth, orders_per_level, show_expired, highlight: None }
47 }
48
49 pub fn highlighted_by(mut self, highlight: &'a dyn OrderHighlight) -> Self {
51 self.highlight = Some(highlight);
52 self
53 }
54}
55
56impl<'a> std::fmt::Display for OrderBookView<'a> {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 let spread_panel = |table: &mut Table, row_idx: usize| {
59 if let Some(((best_ask, _), (best_bid, _))) =
60 self.book.best_ask().zip(self.book.best_bid())
61 {
62 table.with(Panel::horizontal(
63 row_idx,
64 format!(
65 "Best ASK: {} :: Best BID: {} :: Spread: {} ({:.2} %)",
66 best_ask,
67 best_bid,
68 best_ask - best_bid,
69 (best_ask - best_bid) / ((best_ask + best_bid) / 2) * 100
70 ),
71 ));
72 table.modify(Row::from(row_idx), Alignment::right());
73 }
74 };
75
76 if f.alternate() {
77 let level_orders = |level: &BookLevel| {
80 let mut level_orders = String::with_capacity(64 * level.num_orders() as usize); for (i, order) in self
82 .book
83 .level_orders(level)
84 .filter(|o| !o.is_expired() || self.show_expired)
85 .take(self.orders_per_level.unwrap_or(usize::MAX))
86 .enumerate()
87 {
88 if i > 0 && i % 4 == 0 {
89 level_orders.push('\n');
90 }
91 let chip = format!("{:#}", *(*order));
95 match self.highlight.and_then(|h| h.highlight(order, &chip)) {
96 Some(highlighted) => level_orders.push_str(&highlighted),
97 None if order.is_expired() => {
98 level_orders.push_str(chip.bright_red().to_string().as_str())
99 },
100 None => level_orders.push_str(&chip),
101 }
102 level_orders.push(' ');
103 }
104
105 level_orders
106 };
107
108 let mut asks = Vec::with_capacity(self.book.asks.len());
110 let mut num_ask_levels = 0;
111 let mut num_ask_orders = 0;
112 let mut cumulative_ask_size = UD64::ZERO;
113 for (price, level) in self
114 .book
115 .asks
116 .iter()
117 .filter(|(_, lvl)| lvl.num_orders() > 0 || self.show_expired)
118 {
119 num_ask_levels += 1;
120 num_ask_orders += level.num_orders();
121 cumulative_ask_size += level.size();
122 asks.push(vec![
123 price.to_string().red().to_string(),
124 level.size().to_string().red().to_string(),
125 cumulative_ask_size.to_string().red().to_string(),
126 level.num_orders().to_string().red().to_string(),
127 level_orders(level),
128 ]);
129 }
130
131 let mut bids = Vec::with_capacity(self.book.bids.len());
133 let mut num_bid_levels = 0;
134 let mut num_bid_orders = 0;
135 let mut cumulative_bid_size = UD64::ZERO;
136 for (price, level) in self
137 .book
138 .bids
139 .iter()
140 .filter(|(_, lvl)| lvl.num_orders() > 0 || self.show_expired)
141 {
142 num_bid_levels += 1;
143 num_bid_orders += level.num_orders();
144 cumulative_bid_size += level.size();
145 bids.push(vec![
146 price.0.to_string().green().to_string(),
147 level.size().to_string().green().to_string(),
148 cumulative_bid_size.to_string().green().to_string(),
149 level.num_orders().to_string().green().to_string(),
150 level_orders(level),
151 ]);
152 }
153
154 let mut table = Table::from_iter(
156 iter::once(&vec![
157 "Price".to_string(),
158 "Size".to_string(),
159 "Cum Size".to_string(),
160 "Num Orders".to_string(),
161 "Orders".to_string(),
162 ])
163 .chain(
164 asks.iter()
165 .take(self.depth.unwrap_or(num_ask_levels))
166 .rev()
167 .chain(bids.iter().take(self.depth.unwrap_or(num_bid_levels))),
168 ),
169 );
170
171 let (ask_pct, bid_pct) =
173 if cumulative_ask_size > UD64::ZERO || cumulative_bid_size > UD64::ZERO {
174 let total = cumulative_ask_size + cumulative_bid_size;
175 let ask_pct = (cumulative_ask_size / total) * UD64::from(100u32);
176 let bid_pct = (cumulative_bid_size / total) * UD64::from(100u32);
177 (ask_pct, bid_pct)
178 } else {
179 (UD64::ZERO, UD64::ZERO)
180 };
181 table.with(Panel::header(format!(
182 "Total orders: {} :: ASK orders: {}, levels: {}, size: {} ({:.1}%) :: BID orders: \
183 {}, levels: {}, size: {} ({:.1}%)",
184 num_ask_orders + num_bid_orders,
185 num_ask_orders,
186 num_ask_levels,
187 cumulative_ask_size,
188 ask_pct,
189 num_bid_orders,
190 num_bid_levels,
191 cumulative_bid_size,
192 bid_pct,
193 )));
194 table.modify(Rows::first(), Alignment::right());
195
196 spread_panel(&mut table, self.depth.unwrap_or(num_ask_levels).min(num_ask_levels) + 2);
198
199 if let Some(max_width) = f.width() {
200 table.with(Width::wrap(max_width));
201 }
202
203 table.with(Style::modern());
204 writeln!(f, "{}", table)
205 } else {
206 let ask_orders = self.book.ask_orders().collect::<Vec<_>>();
208 let num_ask_orders = ask_orders.len();
209 let mut table = Table::new(
210 ask_orders
211 .iter()
212 .rev()
213 .map(|o| &*(**o))
214 .chain(self.book.bid_orders().map(|o| &*(*o))),
215 );
216 spread_panel(&mut table, num_ask_orders + 1);
217 table.with(Style::sharp());
218 writeln!(f, "{}", table)
219 }
220 }
221}