Skip to main content

perpl_sdk/state/l3_book/
mod.rs

1//! Order book implementation with intrusive linked lists.
2//!
3//! This module provides the order book data structure that tracks orders
4//! at each price level with FIFO time-priority ordering using doubly-linked
5//! lists.
6
7mod error;
8mod level;
9mod order;
10#[cfg(feature = "display")]
11mod view;
12
13#[cfg(test)]
14mod tests;
15
16use std::{
17    cmp::Reverse,
18    collections::{BTreeMap, HashMap},
19};
20
21pub use error::{OrderBookError, OrderBookResult};
22use fastnum::{
23    UD64, UD128,
24    decimal::{Context, RoundingMode},
25};
26use itertools::{FoldWhile, Itertools};
27pub use level::BookLevel;
28pub use order::BookOrder;
29#[cfg(feature = "display")]
30pub use view::{OrderBookView, OrderHighlight};
31
32use crate::{state::Order, types};
33
34/// L3 order book with intrusive linked lists.
35///
36/// Orders are stored in a HashMap keyed by OrderId, with each price level
37/// maintaining a doubly-linked list of orders in FIFO (time-priority) order.
38/// Provides both L2 (aggregated price levels) and L3 (individual orders) views.
39#[derive(Clone, Debug, Default)]
40pub struct OrderBook {
41    /// Storage for all orders, keyed by OrderId.
42    orders: HashMap<types::OrderId, BookOrder>,
43    /// Orders keyed by client order ID, per account ID.
44    client_orders: HashMap<(types::AccountId, types::RequestId), types::OrderId>,
45    /// Ask levels sorted by price (ascending, best ask first).
46    asks: BTreeMap<UD64, BookLevel>,
47    /// Bid levels sorted by price (descending, best bid first).
48    bids: BTreeMap<Reverse<UD64>, BookLevel>,
49}
50
51impl OrderBook {
52    pub(crate) fn new() -> Self { Self::default() }
53
54    // === L2 API ===
55
56    /// Asks sorted away from the spread.
57    pub fn asks(&self) -> &BTreeMap<UD64, BookLevel> { &self.asks }
58
59    /// Bids sorted away from the spread.
60    pub fn bids(&self) -> &BTreeMap<Reverse<UD64>, BookLevel> { &self.bids }
61
62    /// Best ask price/size.
63    pub fn best_ask(&self) -> Option<(UD64, UD64)> {
64        self.asks
65            .iter()
66            .find(|(_, lvl)| lvl.size() > UD64::ZERO)
67            .map(|(k, v)| (*k, v.size()))
68    }
69
70    /// Best bid price/size.
71    pub fn best_bid(&self) -> Option<(UD64, UD64)> {
72        self.bids
73            .iter()
74            .find(|(_, lvl)| lvl.size() > UD64::ZERO)
75            .map(|(k, v)| (k.0, v.size()))
76    }
77
78    /// Ask impact price for the requested size, along with the fillable size
79    /// and size-averaged price.
80    pub fn ask_impact(&self, want_size: UD64) -> Option<(UD64, UD64, UD64)> {
81        Self::impact(self.asks.iter(), want_size)
82    }
83
84    /// Bid impact price for the requested size, along with the fillable size
85    /// and size-averaged price.
86    pub fn bid_impact(&self, want_size: UD64) -> Option<(UD64, UD64, UD64)> {
87        Self::impact(self.bids.iter().map(|(k, v)| (&k.0, v)), want_size)
88    }
89
90    /// Ask impact price for the requested notional amount, along with the
91    /// fillable size, size-averaged price, and filled notional.
92    /// CAREFUL: See [`impact_notional`](Self::impact_notional) for partial-fill
93    /// semantics.
94    pub fn ask_impact_notional(&self, want_notional: UD128) -> Option<(UD64, UD64, UD64, UD128)> {
95        Self::impact_notional(self.asks.iter(), want_notional)
96    }
97
98    /// Bid impact price for the requested notional amount, along with the
99    /// fillable size, size-averaged price, and filled notional.
100    /// CAREFUL: See [`impact_notional`](Self::impact_notional) for partial-fill
101    /// semantics.
102    pub fn bid_impact_notional(&self, want_notional: UD128) -> Option<(UD64, UD64, UD64, UD128)> {
103        Self::impact_notional(self.bids.iter().map(|(k, v)| (&k.0, v)), want_notional)
104    }
105
106    // === L3 API ===
107
108    /// Get L3 level at a specific ask price.
109    pub fn ask_level(&self, price: UD64) -> Option<&BookLevel> { self.asks.get(&price) }
110
111    /// Get L3 level at a specific bid price.
112    pub fn bid_level(&self, price: UD64) -> Option<&BookLevel> { self.bids.get(&Reverse(price)) }
113
114    /// Get a specific order by ID (O(1) via HashMap lookup).
115    pub fn get_order(&self, order_id: types::OrderId) -> Option<&BookOrder> {
116        self.orders.get(&order_id)
117    }
118
119    /// Get a specific order by client order ID (O(1) via HashMap lookup).
120    pub fn get_order_by_client_id(
121        &self,
122        account_id: types::AccountId,
123        client_id: types::RequestId,
124    ) -> Option<&BookOrder> {
125        self.client_orders
126            .get(&(account_id, client_id))
127            .and_then(|order_id| self.orders.get(order_id))
128    }
129
130    /// Iterator over all L3 orders on the ask side in price-time priority.
131    pub fn ask_orders(&self) -> impl Iterator<Item = &BookOrder> {
132        self.asks
133            .values()
134            .flat_map(|level| self.level_orders(level))
135    }
136
137    /// Iterator over all L3 orders on the bid side in price-time priority.
138    pub fn bid_orders(&self) -> impl Iterator<Item = &BookOrder> {
139        self.bids
140            .values()
141            .flat_map(|level| self.level_orders(level))
142    }
143
144    /// Total number of orders in the book.
145    pub fn total_orders(&self) -> usize { self.orders.len() }
146
147    /// Access to all orders in the book keyed by order ID.
148    pub fn all_orders(&self) -> &HashMap<types::OrderId, BookOrder> { &self.orders }
149
150    /// Iterator over orders at a specific level (follows the linked list).
151    pub(crate) fn level_orders<'a>(&'a self, level: &'a BookLevel) -> LevelOrdersIter<'a> {
152        LevelOrdersIter { orders: &self.orders, current: level.head() }
153    }
154
155    #[cfg(feature = "display")]
156    pub fn view<'a>(
157        &'a self,
158        depth: Option<usize>,
159        orders_per_level: Option<usize>,
160        show_expired: bool,
161    ) -> OrderBookView<'a> {
162        OrderBookView::new(self, depth, orders_per_level, show_expired)
163    }
164
165    // === Mutation methods ===
166
167    /// Add an order to the book (at the back of the queue for its price level).
168    ///
169    /// # Errors
170    ///
171    /// Returns an error if:
172    /// - The order already exists in the book
173    /// - The order has zero size
174    /// - The order has zero price
175    pub(crate) fn add_order(&mut self, order: &Order) -> OrderBookResult<()> {
176        let order_id = order.order_id();
177
178        // Validate order
179        if order.size() == UD64::ZERO {
180            return Err(OrderBookError::InvalidOrderSize { order_id, size: order.size() });
181        }
182        if order.price() == UD64::ZERO {
183            return Err(OrderBookError::InvalidOrderPrice { order_id, price: order.price() });
184        }
185
186        // Check if order already exists
187        if let Some(existing) = self.orders.get(&order_id) {
188            return Err(OrderBookError::OrderAlreadyExists {
189                order_id,
190                existing_price: existing.price(),
191            });
192        }
193
194        // Get or create the level and capture tail before inserting
195        let side = order.r#type().side();
196        let old_tail = self.get_or_create_level_mut(side, order.price()).tail();
197
198        // Create the BookOrder with prev pointing to current tail
199        let mut l3_order = BookOrder::new(*order);
200        l3_order.set_prev(old_tail);
201
202        // Insert into hashmaps
203        self.orders.insert(order_id, l3_order);
204        if let Some(client_order_id) = order.client_order_id() {
205            self.client_orders
206                .insert((order.account_id(), client_order_id), order_id);
207        }
208
209        // Link at tail
210        self.link_at_tail(side, order.price(), old_tail, order_id, order.size());
211
212        Ok(())
213    }
214
215    /// Update an order's size (same price level, keeps queue position).
216    /// Expected to be called only for non-expired orders.
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if:
221    /// - The order doesn't exist in the book
222    /// - The new size is zero
223    pub(crate) fn update_order(
224        &mut self,
225        order: &Order,
226        prev_order: &BookOrder,
227    ) -> OrderBookResult<()> {
228        let order_id = order.order_id();
229
230        // Validate new size
231        if order.size() == UD64::ZERO {
232            return Err(OrderBookError::InvalidOrderSize { order_id, size: order.size() });
233        }
234
235        let old_size = prev_order.size();
236        let price = prev_order.price();
237        let side = prev_order.r#type().side();
238
239        // Update the order data
240        self.orders
241            .get_mut(&order_id)
242            .ok_or(OrderBookError::OrderNotFound { order_id })?
243            .update_order(*order);
244
245        // Update level cached size
246        let level = self
247            .get_level_mut(side, price)
248            .ok_or(OrderBookError::LevelNotFound { price, side })?;
249        level.update_size(old_size, order.size());
250
251        Ok(())
252    }
253
254    /// Remove an order from the book by ID.
255    ///
256    /// Returns the removed order.
257    ///
258    /// # Errors
259    ///
260    /// Returns an error if:
261    /// - The order doesn't exist in the book
262    pub(crate) fn remove_order(&mut self, prev_order: &BookOrder) -> OrderBookResult<Order> {
263        let order_id = prev_order.order_id();
264        let prev_id = prev_order.prev();
265        let next_id = prev_order.next();
266        let price = prev_order.price();
267        let size = prev_order.size();
268        let side = prev_order.r#type().side();
269
270        // Unlink from list
271        self.unlink_node(prev_id, next_id);
272
273        // Update level head/tail and check if empty
274        let level = self
275            .get_level_mut(side, price)
276            .ok_or(OrderBookError::LevelNotFound { price, side })?;
277        if level.head() == Some(order_id) {
278            level.set_head(next_id);
279        }
280        if level.tail() == Some(order_id) {
281            level.set_tail(prev_id);
282        }
283        if !prev_order.is_expired() {
284            // Expired orders already removed from the cached level size/count
285            level.sub_size(size);
286        }
287        let should_remove_level = level.is_empty();
288
289        // Prune empty level
290        if should_remove_level {
291            self.remove_level(side, price);
292        }
293
294        // Remove from hashmap and return the order
295        let removed = self
296            .orders
297            .remove(&order_id)
298            .ok_or(OrderBookError::OrderNotFound { order_id })?;
299        if let Some(client_order_id) = removed.client_order_id() {
300            self.client_orders
301                .remove(&(removed.account_id(), client_order_id));
302        }
303
304        Ok(*removed)
305    }
306
307    /// Move an order to the back of the queue (for size increases).
308    ///
309    /// # Errors
310    ///
311    /// Returns an error if:
312    /// - The order doesn't exist in the book
313    pub(crate) fn move_to_back(
314        &mut self,
315        order: &Order,
316        prev_order: &BookOrder,
317    ) -> OrderBookResult<()> {
318        let order_id = order.order_id();
319
320        let prev_id = prev_order.prev();
321        let next_id = prev_order.next();
322        let price = prev_order.price();
323        let old_size = prev_order.size();
324        let side = prev_order.r#type().side();
325
326        // If already at tail, just update the order data
327        let is_at_tail = self
328            .get_level(side, price)
329            .ok_or(OrderBookError::LevelNotFound { price, side })?
330            .tail()
331            == Some(order_id);
332
333        if is_at_tail {
334            // Already at back, just update order data
335            if let Some(l3_order) = self.orders.get_mut(&order_id) {
336                l3_order.update_order(*order);
337            }
338            let level = self
339                .get_level_mut(side, price)
340                .ok_or(OrderBookError::LevelNotFound { price, side })?;
341            if prev_order.is_expired() {
342                // Expired order was removed from the cached level size/count
343                level.add_size(order.size());
344            } else {
345                level.update_size(old_size, order.size());
346            }
347            return Ok(());
348        }
349
350        // Unlink from current position
351        self.unlink_node(prev_id, next_id);
352
353        // Update level head if we were the head
354        let level = self
355            .get_level_mut(side, price)
356            .ok_or(OrderBookError::LevelNotFound { price, side })?;
357        if level.head() == Some(order_id) {
358            level.set_head(next_id);
359        }
360
361        // Get old tail before updating
362        let old_tail = level.tail();
363
364        // Update old tail's next pointer
365        if let Some(old_tail_id) = old_tail
366            && let Some(old_tail_order) = self.orders.get_mut(&old_tail_id)
367        {
368            old_tail_order.set_next(Some(order_id));
369        }
370
371        // Update this order's links and data
372        if let Some(l3_order) = self.orders.get_mut(&order_id) {
373            l3_order.set_prev(old_tail);
374            l3_order.set_next(None);
375            l3_order.update_order(*order);
376        } else {
377            return Err(OrderBookError::OrderNotFound { order_id });
378        }
379
380        // Update level tail and size - need to re-borrow
381        let level = self
382            .get_level_mut(side, price)
383            .ok_or(OrderBookError::LevelNotFound { price, side })?;
384        level.set_tail(Some(order_id));
385        if prev_order.is_expired() {
386            // Expired order was removed from the cached level size/count
387            level.add_size(order.size());
388        } else {
389            level.update_size(old_size, order.size());
390        }
391
392        Ok(())
393    }
394
395    /// Add orders from a snapshot, reconstructing FIFO order from linked list
396    /// pointers.
397    ///
398    /// Uses the `prev_order_id`/`next_order_id` fields to determine the correct
399    /// queue position within each price level.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error if any order has invalid size or price.
404    pub(crate) fn add_orders_from_snapshot(&mut self, orders: &[Order]) -> OrderBookResult<()> {
405        // Collect order IDs for validation
406        let order_ids: std::collections::HashSet<types::OrderId> =
407            orders.iter().map(|o| o.order_id()).collect();
408
409        // First pass: insert all orders and set prev/next pointers directly
410        for order in orders {
411            let order_id = order.order_id();
412
413            if order.size() == UD64::ZERO {
414                return Err(OrderBookError::InvalidOrderSize { order_id, size: order.size() });
415            }
416            if order.price() == UD64::ZERO {
417                return Err(OrderBookError::InvalidOrderPrice { order_id, price: order.price() });
418            }
419
420            // Validate that referenced orders exist in this snapshot
421            if let Some(prev_id) = order.prev_order_id()
422                && !order_ids.contains(&prev_id)
423            {
424                return Err(OrderBookError::DanglingOrderReference {
425                    order_id,
426                    referenced_id: prev_id,
427                    pointer: "prev",
428                });
429            }
430            if let Some(next_id) = order.next_order_id()
431                && !order_ids.contains(&next_id)
432            {
433                return Err(OrderBookError::DanglingOrderReference {
434                    order_id,
435                    referenced_id: next_id,
436                    pointer: "next",
437                });
438            }
439
440            // Create BookOrder with prev/next pointing directly to OrderIds
441            let mut l3_order = BookOrder::new(*order);
442            l3_order.set_prev(order.prev_order_id());
443            l3_order.set_next(order.next_order_id());
444
445            self.orders.insert(order_id, l3_order);
446        }
447
448        // Second pass: build levels with head/tail and cached aggregates
449        // Group orders by (price, side)
450        let mut level_orders: HashMap<(UD64, types::OrderSide), Vec<types::OrderId>> =
451            HashMap::new();
452        for order in orders {
453            let key = (order.price(), order.r#type().side());
454            level_orders.entry(key).or_default().push(order.order_id());
455        }
456
457        for ((price, side), order_ids) in level_orders {
458            // Find head (order with no prev in this level)
459            let head = order_ids.iter().find(|&&id| {
460                self.orders
461                    .get(&id)
462                    .is_some_and(|o| o.prev().is_none_or(|p| !order_ids.contains(&p)))
463            });
464
465            // Find tail (order with no next in this level)
466            let tail = order_ids.iter().find(|&&id| {
467                self.orders
468                    .get(&id)
469                    .is_some_and(|o| o.next().is_none_or(|n| !order_ids.contains(&n)))
470            });
471
472            // Build level with head/tail and cached aggregates
473            let mut level = BookLevel::new();
474            level.set_head(head.copied());
475            level.set_tail(tail.copied());
476            for &id in &order_ids {
477                if let Some(order) = self.orders.get(&id)
478                    && !order.is_expired()
479                {
480                    level.add_size(order.size());
481                }
482            }
483
484            match side {
485                types::OrderSide::Ask => {
486                    self.asks.insert(price, level);
487                },
488                types::OrderSide::Bid => {
489                    self.bids.insert(Reverse(price), level);
490                },
491            }
492        }
493
494        Ok(())
495    }
496
497    /// Check if any orders are expired and update cached L2 book state.
498    pub(crate) fn check_expired(&mut self, instant: types::StateInstant) {
499        let mut update_levels = vec![];
500        for order in self.orders.values_mut() {
501            if order.update_if_expired(instant) {
502                update_levels.push((order.r#type().side(), order.price(), order.size()));
503            }
504        }
505        for (side, price, size) in update_levels {
506            if let Some(level) = self.get_level_mut(side, price) {
507                level.sub_size(size);
508            }
509        }
510    }
511
512    // === Linked list helpers ===
513
514    /// Get a level by side and price (immutable).
515    fn get_level(&self, side: types::OrderSide, price: UD64) -> Option<&BookLevel> {
516        match side {
517            types::OrderSide::Ask => self.asks.get(&price),
518            types::OrderSide::Bid => self.bids.get(&Reverse(price)),
519        }
520    }
521
522    /// Get a level by side and price (mutable).
523    fn get_level_mut(&mut self, side: types::OrderSide, price: UD64) -> Option<&mut BookLevel> {
524        match side {
525            types::OrderSide::Ask => self.asks.get_mut(&price),
526            types::OrderSide::Bid => self.bids.get_mut(&Reverse(price)),
527        }
528    }
529
530    /// Get or create a level by side and price.
531    fn get_or_create_level_mut(&mut self, side: types::OrderSide, price: UD64) -> &mut BookLevel {
532        match side {
533            types::OrderSide::Ask => self.asks.entry(price).or_default(),
534            types::OrderSide::Bid => self.bids.entry(Reverse(price)).or_default(),
535        }
536    }
537
538    /// Remove a level by side and price.
539    fn remove_level(&mut self, side: types::OrderSide, price: UD64) {
540        match side {
541            types::OrderSide::Ask => {
542                self.asks.remove(&price);
543            },
544            types::OrderSide::Bid => {
545                self.bids.remove(&Reverse(price));
546            },
547        }
548    }
549
550    /// Force remove a level (for testing state inconsistency handling).
551    #[cfg(test)]
552    pub(crate) fn force_remove_level(&mut self, side: types::OrderSide, price: UD64) {
553        self.remove_level(side, price);
554    }
555
556    /// Unlink a node from the doubly-linked list by updating its neighbors.
557    fn unlink_node(&mut self, prev_id: Option<types::OrderId>, next_id: Option<types::OrderId>) {
558        // Update prev's next pointer
559        if let Some(prev) = prev_id
560            && let Some(prev_order) = self.orders.get_mut(&prev)
561        {
562            prev_order.set_next(next_id);
563        }
564
565        // Update next's prev pointer
566        if let Some(next) = next_id
567            && let Some(next_order) = self.orders.get_mut(&next)
568        {
569            next_order.set_prev(prev_id);
570        }
571    }
572
573    /// Link a new order at the tail of a level.
574    /// Takes old_tail to avoid borrowing level while mutating orders.
575    fn link_at_tail(
576        &mut self,
577        side: types::OrderSide,
578        price: UD64,
579        old_tail: Option<types::OrderId>,
580        order_id: types::OrderId,
581        size: UD64,
582    ) {
583        // Update old tail's next pointer
584        if let Some(old_tail_id) = old_tail
585            && let Some(old_tail_order) = self.orders.get_mut(&old_tail_id)
586        {
587            old_tail_order.set_next(Some(order_id));
588        }
589
590        // Update level head/tail (re-borrow level after updating orders)
591        let level = self.get_or_create_level_mut(side, price);
592        if level.head().is_none() {
593            level.set_head(Some(order_id));
594        }
595        level.set_tail(Some(order_id));
596        level.add_size(size);
597    }
598
599    /// Gets the impact price for a market order of the requested size, along
600    /// with the fillable size and size-averaged price.
601    fn impact<'a>(
602        mut side: impl Iterator<Item = (&'a UD64, &'a BookLevel)>,
603        want_size: UD64,
604    ) -> Option<(UD64, UD64, UD64)> {
605        let (price, unfilled, price_size) = side
606            .fold_while(
607                (UD64::ZERO, want_size, UD128::ZERO),
608                |(_, unfilled, price_size), (price, level)| {
609                    let level_size = level.size();
610                    if unfilled > level_size {
611                        FoldWhile::Continue((
612                            *price,
613                            unfilled - level_size,
614                            price_size + (price.resize() * level_size.resize()),
615                        ))
616                    } else {
617                        FoldWhile::Done((
618                            *price,
619                            UD64::ZERO,
620                            price_size + (price.resize() * unfilled.resize()),
621                        ))
622                    }
623                },
624            )
625            .into_inner();
626        let filled = want_size - unfilled;
627        if filled > UD64::ZERO {
628            Some((price, filled, (price_size / filled.resize()).resize()))
629        } else {
630            None
631        }
632    }
633
634    /// Gets the impact price for a market order of the requested notional
635    /// amount, along with the fillable size, size-averaged price, and
636    /// filled notional amount. `want_notional` is the target cumulative
637    /// notional (sum of price * size).
638    ///
639    /// CAREFUL: Returns whatever values can be filled, up to `want_notional`.
640    /// It is incumbent upon the caller to check that the returned
641    /// `filled_notional` meets or exceeds the value specified for
642    /// `want_notional`.
643    fn impact_notional<'a>(
644        mut side: impl Iterator<Item = (&'a UD64, &'a BookLevel)>,
645        want_notional: UD128,
646    ) -> Option<(UD64, UD64, UD64, UD128)> {
647        let (price, _, filled_size, filled_notional) = side
648            .fold_while(
649                (UD64::ZERO, want_notional, UD64::ZERO, UD128::ZERO),
650                |(_, remaining, filled_size, filled_notional), (price, level)| {
651                    let level_size = level.size();
652                    let level_notional = price.resize() * level_size.resize();
653                    if remaining > level_notional {
654                        FoldWhile::Continue((
655                            *price,
656                            remaining - level_notional,
657                            filled_size + level_size,
658                            filled_notional + level_notional,
659                        ))
660                    } else {
661                        let partial_size: UD64 = (remaining / price.resize()).resize();
662                        FoldWhile::Done((
663                            *price,
664                            UD128::ZERO,
665                            filled_size + partial_size,
666                            filled_notional + remaining,
667                        ))
668                    }
669                },
670            )
671            .into_inner();
672        if filled_size.is_zero() {
673            None
674        } else {
675            let ctx = Context::default().with_rounding_mode(RoundingMode::HalfUp);
676            let vwap: UD64 =
677                (filled_notional.with_ctx(ctx) / filled_size.resize().with_ctx(ctx)).resize();
678            Some((price, filled_size, vwap, filled_notional))
679        }
680    }
681}
682
683#[cfg(feature = "display")]
684impl std::fmt::Display for OrderBook {
685    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686        self.view(None, None, true).fmt(f)
687    }
688}
689
690/// Iterator over orders at a price level (follows linked list).
691pub(crate) struct LevelOrdersIter<'a> {
692    orders: &'a HashMap<types::OrderId, BookOrder>,
693    current: Option<types::OrderId>,
694}
695
696impl<'a> Iterator for LevelOrdersIter<'a> {
697    type Item = &'a BookOrder;
698
699    fn next(&mut self) -> Option<Self::Item> {
700        let id = self.current?;
701        let order = self.orders.get(&id)?;
702        self.current = order.next();
703        Some(order)
704    }
705}