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};
14
15/// View of an order book.
16/// Can be rendered as plain table or compact L3 representation limited by depth
17/// and number of orders per level.
18pub struct OrderBookView<'a> {
19    book: &'a OrderBook,
20    depth: Option<usize>,
21    orders_per_level: Option<usize>,
22    show_expired: bool,
23}
24
25impl<'a> OrderBookView<'a> {
26    pub(crate) fn new(
27        book: &'a OrderBook,
28        depth: Option<usize>,
29        orders_per_level: Option<usize>,
30        show_expired: bool,
31    ) -> Self {
32        Self { book, depth, orders_per_level, show_expired }
33    }
34}
35
36impl<'a> std::fmt::Display for OrderBookView<'a> {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        let spread_panel = |table: &mut Table, row_idx: usize| {
39            if let Some(((best_ask, _), (best_bid, _))) =
40                self.book.best_ask().zip(self.book.best_bid())
41            {
42                table.with(Panel::horizontal(
43                    row_idx,
44                    format!(
45                        "Best ASK: {} :: Best BID: {} :: Spread: {} ({:.2} %)",
46                        best_ask,
47                        best_bid,
48                        best_ask - best_bid,
49                        (best_ask - best_bid) / ((best_ask + best_bid) / 2) * 100
50                    ),
51                ));
52                table.modify(Row::from(row_idx), Alignment::right());
53            }
54        };
55
56        if f.alternate() {
57            // Configured compact representation as an alternate representation
58
59            let level_orders = |level: &BookLevel| {
60                let mut level_orders = String::with_capacity(64 * level.num_orders() as usize); // Guesstimate
61                for (i, order) in self
62                    .book
63                    .level_orders(level)
64                    .filter(|o| !o.is_expired() || self.show_expired)
65                    .take(self.orders_per_level.unwrap_or(usize::MAX))
66                    .enumerate()
67                {
68                    if i > 0 && i % 4 == 0 {
69                        level_orders.push('\n');
70                    }
71                    if !order.is_expired() {
72                        level_orders.push_str(format!("{:#} ", *(*order)).as_str());
73                    } else {
74                        level_orders.push_str(
75                            format!("{:#} ", *(*order))
76                                .bright_red()
77                                .to_string()
78                                .as_str(),
79                        );
80                    }
81                }
82
83                level_orders
84            };
85
86            // Asks
87            let mut asks = Vec::with_capacity(self.book.asks.len());
88            let mut num_ask_levels = 0;
89            let mut num_ask_orders = 0;
90            let mut cumulative_ask_size = UD64::ZERO;
91            for (price, level) in self
92                .book
93                .asks
94                .iter()
95                .filter(|(_, lvl)| lvl.num_orders() > 0 || self.show_expired)
96            {
97                num_ask_levels += 1;
98                num_ask_orders += level.num_orders();
99                cumulative_ask_size += level.size();
100                asks.push(vec![
101                    price.to_string().red().to_string(),
102                    level.size().to_string().red().to_string(),
103                    cumulative_ask_size.to_string().red().to_string(),
104                    level.num_orders().to_string().red().to_string(),
105                    level_orders(level),
106                ]);
107            }
108
109            // Bids
110            let mut bids = Vec::with_capacity(self.book.bids.len());
111            let mut num_bid_levels = 0;
112            let mut num_bid_orders = 0;
113            let mut cumulative_bid_size = UD64::ZERO;
114            for (price, level) in self
115                .book
116                .bids
117                .iter()
118                .filter(|(_, lvl)| lvl.num_orders() > 0 || self.show_expired)
119            {
120                num_bid_levels += 1;
121                num_bid_orders += level.num_orders();
122                cumulative_bid_size += level.size();
123                bids.push(vec![
124                    price.0.to_string().green().to_string(),
125                    level.size().to_string().green().to_string(),
126                    cumulative_bid_size.to_string().green().to_string(),
127                    level.num_orders().to_string().green().to_string(),
128                    level_orders(level),
129                ]);
130            }
131
132            // Table of price levels
133            let mut table = Table::from_iter(
134                iter::once(&vec![
135                    "Price".to_string(),
136                    "Size".to_string(),
137                    "Cum Size".to_string(),
138                    "Num Orders".to_string(),
139                    "Orders".to_string(),
140                ])
141                .chain(
142                    asks.iter()
143                        .take(self.depth.unwrap_or(num_ask_levels))
144                        .rev()
145                        .chain(bids.iter().take(self.depth.unwrap_or(num_bid_levels))),
146                ),
147            );
148
149            // Header with totals
150            let (ask_pct, bid_pct) =
151                if cumulative_ask_size > UD64::ZERO || cumulative_bid_size > UD64::ZERO {
152                    let total = cumulative_ask_size + cumulative_bid_size;
153                    let ask_pct = (cumulative_ask_size / total) * UD64::from(100u32);
154                    let bid_pct = (cumulative_bid_size / total) * UD64::from(100u32);
155                    (ask_pct, bid_pct)
156                } else {
157                    (UD64::ZERO, UD64::ZERO)
158                };
159            table.with(Panel::header(format!(
160                "Total orders: {} :: ASK orders: {}, levels: {}, size: {} ({:.1}%) :: BID orders: \
161                 {}, levels: {}, size: {} ({:.1}%)",
162                num_ask_orders + num_bid_orders,
163                num_ask_orders,
164                num_ask_levels,
165                cumulative_ask_size,
166                ask_pct,
167                num_bid_orders,
168                num_bid_levels,
169                cumulative_bid_size,
170                bid_pct,
171            )));
172            table.modify(Rows::first(), Alignment::right());
173
174            // Spread
175            spread_panel(&mut table, self.depth.unwrap_or(num_ask_levels).min(num_ask_levels) + 2);
176
177            if let Some(max_width) = f.width() {
178                table.with(Width::wrap(max_width));
179            }
180
181            table.with(Style::modern());
182            writeln!(f, "{}", table)
183        } else {
184            // Plain table with full details by default
185            let ask_orders = self.book.ask_orders().collect::<Vec<_>>();
186            let num_ask_orders = ask_orders.len();
187            let mut table = Table::new(
188                ask_orders
189                    .iter()
190                    .rev()
191                    .map(|o| &*(**o))
192                    .chain(self.book.bid_orders().map(|o| &*(*o))),
193            );
194            spread_panel(&mut table, num_ask_orders + 1);
195            table.with(Style::sharp());
196            writeln!(f, "{}", table)
197        }
198    }
199}