Skip to main content

perpl_sdk/state/l3_book/
view.rs

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
16/// Repaints individual orders as a book is rendered, so callers can attribute
17/// resting orders to the accounts they track.
18///
19/// Applies to the compact (`{:#}`) rendering only, where every order is drawn
20/// as a self-contained chip. The plain table form renders orders through
21/// [`tabled::Tabled`] and is left alone.
22pub trait OrderHighlight {
23    /// Repaints `rendered` - the unstyled chip drawn for `order` - or returns
24    /// `None` to leave the book's own styling in place.
25    fn highlight(&self, order: &Order, rendered: &str) -> Option<String>;
26}
27
28/// View of an order book.
29/// Can be rendered as plain table or compact L3 representation limited by depth
30/// and number of orders per level.
31pub 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    /// Repaints the orders `highlight` picks out, see [`OrderHighlight`].
50    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            // Configured compact representation as an alternate representation
78
79            let level_orders = |level: &BookLevel| {
80                let mut level_orders = String::with_capacity(64 * level.num_orders() as usize); // Guesstimate
81                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                    // The chip is rendered unstyled first so a highlight can
92                    // paint it whole, rather than nesting inside a style whose
93                    // reset would cut the highlight short
94                    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            // Asks
109            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            // Bids
132            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            // Table of price levels
155            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            // Header with totals
172            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
197            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            // Plain table with full details by default
207            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}