Skip to main content

fin_primitives/orderbook/
mod.rs

1//! # Module: orderbook
2//!
3//! ## Responsibility
4//! Maintains a level-2 order book for a single symbol. Processes incremental
5//! `BookDelta` updates with sequence-number validation, and provides best bid/ask,
6//! spread, VWAP-to-fill, and top-N level queries.
7//!
8//! ## Guarantees
9//! - Sequence numbers are validated: each delta must be exactly `self.sequence + 1`
10//! - Bids are maintained in descending price order (best bid = highest price)
11//! - Asks are maintained in ascending price order (best ask = lowest price)
12//! - `vwap_for_qty` returns `InsufficientLiquidity` when the book cannot fill `qty`
13//! - Thread-safe: `OrderBook` implements neither `Send` nor `Sync` by default (use `Arc<Mutex>` externally)
14//!
15//! ## NOT Responsible For
16//! - Cross-symbol aggregation
17//! - Persistence
18
19use crate::error::FinError;
20use crate::types::{Price, Quantity, Side, Symbol};
21use rust_decimal::Decimal;
22use std::collections::BTreeMap;
23
24/// A single price level in the order book.
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct PriceLevel {
27    /// The price of this level.
28    pub price: Price,
29    /// The resting quantity at this price.
30    pub quantity: Quantity,
31}
32
33/// Whether a delta sets or removes a price level.
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub enum DeltaAction {
36    /// Set the quantity at this price level.
37    Set,
38    /// Remove this price level entirely.
39    Remove,
40}
41
42/// An incremental update to an order book.
43#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
44pub struct BookDelta {
45    /// Which side of the book this update applies to.
46    pub side: Side,
47    /// The price level being updated.
48    pub price: Price,
49    /// The new quantity (used for `Set`; ignored for `Remove`).
50    pub quantity: Quantity,
51    /// The action to take.
52    pub action: DeltaAction,
53    /// Must equal `book.sequence() + 1`.
54    pub sequence: u64,
55}
56
57/// A level-2 order book for a single symbol.
58#[derive(Debug, Clone)]
59pub struct OrderBook {
60    /// The instrument this book tracks.
61    pub symbol: Symbol,
62    /// Bid levels: price → quantity. Iterated in ascending key order by `BTreeMap`;
63    /// we use `.iter().rev()` to get descending (best bid first).
64    bids: BTreeMap<Decimal, Decimal>,
65    /// Ask levels: price → quantity. Iterated in ascending key order (best ask first).
66    asks: BTreeMap<Decimal, Decimal>,
67    /// Last successfully applied sequence number.
68    sequence: u64,
69}
70
71impl OrderBook {
72    /// Constructs a new empty `OrderBook` for `symbol`. Sequence starts at 0.
73    pub fn new(symbol: Symbol) -> Self {
74        Self {
75            symbol,
76            bids: BTreeMap::new(),
77            asks: BTreeMap::new(),
78            sequence: 0,
79        }
80    }
81
82    /// Applies a `BookDelta` to the order book.
83    ///
84    /// # Errors
85    /// Returns [`FinError::SequenceMismatch`] if `delta.sequence != self.sequence + 1`.
86    #[allow(clippy::needless_pass_by_value)]
87    pub fn apply_delta(&mut self, delta: BookDelta) -> Result<(), FinError> {
88        let expected = self.sequence + 1;
89        if delta.sequence != expected {
90            return Err(FinError::SequenceMismatch {
91                expected,
92                got: delta.sequence,
93            });
94        }
95        // Save the pre-mutation value for potential rollback of a Remove action.
96        let prev_val = match delta.side {
97            Side::Bid => self.bids.get(&delta.price.value()).copied(),
98            Side::Ask => self.asks.get(&delta.price.value()).copied(),
99        };
100
101        let book_side = match delta.side {
102            Side::Bid => &mut self.bids,
103            Side::Ask => &mut self.asks,
104        };
105        match delta.action {
106            DeltaAction::Set => {
107                book_side.insert(delta.price.value(), delta.quantity.value());
108            }
109            DeltaAction::Remove => {
110                book_side.remove(&delta.price.value());
111            }
112        }
113        self.sequence = delta.sequence;
114
115        // Guard against inverted spreads that would corrupt VWAP and mid-price.
116        // Copy the prices out before any mutable borrow.
117        let maybe_inversion = {
118            let best_bid_p = self.bids.keys().next_back().copied();
119            let best_ask_p = self.asks.keys().next().copied();
120            match (best_bid_p, best_ask_p) {
121                (Some(b), Some(a)) if b >= a => Some((b, a)),
122                _ => None,
123            }
124        };
125        if let Some((best_bid_p, best_ask_p)) = maybe_inversion {
126            // Roll back the mutation to keep the book consistent.
127            match delta.action {
128                DeltaAction::Set => match delta.side {
129                    Side::Bid => {
130                        self.bids.remove(&delta.price.value());
131                    }
132                    Side::Ask => {
133                        self.asks.remove(&delta.price.value());
134                    }
135                },
136                // Restore the level to its prior quantity (not delta.quantity, which is
137                // zero by convention for Remove deltas and would corrupt the book).
138                DeltaAction::Remove => match delta.side {
139                    Side::Bid => {
140                        if let Some(qty) = prev_val {
141                            self.bids.insert(delta.price.value(), qty);
142                        }
143                    }
144                    Side::Ask => {
145                        if let Some(qty) = prev_val {
146                            self.asks.insert(delta.price.value(), qty);
147                        }
148                    }
149                },
150            }
151            self.sequence = expected - 1;
152            return Err(FinError::InvertedSpread {
153                best_bid: best_bid_p,
154                best_ask: best_ask_p,
155            });
156        }
157
158        Ok(())
159    }
160
161    /// Returns the best bid (highest price) or `None` if the bid side is empty.
162    ///
163    /// Returns `None` if the book is empty or if the stored price is somehow
164    /// non-positive (which is structurally prevented by `apply_delta`).
165    pub fn best_bid(&self) -> Option<PriceLevel> {
166        self.bids.iter().next_back().and_then(|(p, q)| {
167            Some(PriceLevel {
168                price: Price::new(*p).ok()?,
169                quantity: Quantity::new(*q).unwrap_or_else(|_| Quantity::zero()),
170            })
171        })
172    }
173
174    /// Returns `(best_bid, best_ask)` as a tuple, or `None` if either side is empty.
175    ///
176    /// Convenience wrapper for accessing both sides of the top-of-book in one call.
177    pub fn best_quote(&self) -> Option<(PriceLevel, PriceLevel)> {
178        Some((self.best_bid()?, self.best_ask()?))
179    }
180
181    /// Returns the best ask (lowest price) or `None` if the ask side is empty.
182    ///
183    /// Returns `None` if the book is empty or if the stored price is somehow
184    /// non-positive (which is structurally prevented by `apply_delta`).
185    pub fn best_ask(&self) -> Option<PriceLevel> {
186        self.asks.iter().next().and_then(|(p, q)| {
187            Some(PriceLevel {
188                price: Price::new(*p).ok()?,
189                quantity: Quantity::new(*q).unwrap_or_else(|_| Quantity::zero()),
190            })
191        })
192    }
193
194    /// Returns the mid-price `(best_ask + best_bid) / 2`, or `None` if either side is empty.
195    pub fn mid_price(&self) -> Option<Decimal> {
196        let bid = self.best_bid()?.price.value();
197        let ask = self.best_ask()?.price.value();
198        Some((bid + ask) / Decimal::TWO)
199    }
200
201    /// Returns the spread `best_ask - best_bid`, or `None` if either side is empty.
202    pub fn spread(&self) -> Option<Decimal> {
203        let bid = self.best_bid()?.price.value();
204        let ask = self.best_ask()?.price.value();
205        Some(ask - bid)
206    }
207
208    /// Returns the spread as a percentage of the mid-price: `spread / mid * 100`.
209    ///
210    /// Returns `None` when either side is empty or mid-price is zero.
211    pub fn spread_pct(&self) -> Option<Decimal> {
212        let mid = self.mid_price()?;
213        if mid.is_zero() {
214            return None;
215        }
216        let spread = self.spread()?;
217        Some(spread / mid * Decimal::ONE_HUNDRED)
218    }
219
220    /// Returns the resting quantity at a specific price level, or `None` if the level is absent.
221    pub fn depth_at(&self, side: Side, price: Price) -> Option<Decimal> {
222        let key = price.value();
223        match side {
224            Side::Bid => self.bids.get(&key).copied(),
225            Side::Ask => self.asks.get(&key).copied(),
226        }
227    }
228
229    /// Returns the top `n` bid levels in descending price order.
230    pub fn top_bids(&self, n: usize) -> Vec<PriceLevel> {
231        self.bids
232            .iter()
233            .rev()
234            .take(n)
235            .filter_map(|(p, q)| {
236                let price = Price::new(*p).ok()?;
237                let quantity = Quantity::new(*q).ok()?;
238                Some(PriceLevel { price, quantity })
239            })
240            .collect()
241    }
242
243    /// Returns the top `n` ask levels in ascending price order.
244    pub fn top_asks(&self, n: usize) -> Vec<PriceLevel> {
245        self.asks
246            .iter()
247            .take(n)
248            .filter_map(|(p, q)| {
249                let price = Price::new(*p).ok()?;
250                let quantity = Quantity::new(*q).ok()?;
251                Some(PriceLevel { price, quantity })
252            })
253            .collect()
254    }
255
256    /// Computes the volume-weighted average price to fill `qty` on `side`.
257    ///
258    /// Walks levels from best to worst until `qty` is filled.
259    ///
260    /// # Errors
261    /// Returns [`FinError::InsufficientLiquidity`] if the book cannot fill `qty`.
262    pub fn vwap_for_qty(&self, side: Side, qty: Quantity) -> Result<Decimal, FinError> {
263        let target = qty.value();
264        if target <= Decimal::ZERO {
265            return Ok(Decimal::ZERO);
266        }
267        match side {
268            Side::Bid => Self::vwap_fill(self.bids.iter().rev(), target),
269            Side::Ask => Self::vwap_fill(self.asks.iter(), target),
270        }
271    }
272
273    fn vwap_fill<'a>(
274        levels: impl Iterator<Item = (&'a Decimal, &'a Decimal)>,
275        target: Decimal,
276    ) -> Result<Decimal, FinError> {
277        let mut remaining = target;
278        let mut total_cost = Decimal::ZERO;
279
280        for (price, avail_qty) in levels {
281            let fill = remaining.min(*avail_qty);
282            total_cost += fill * price;
283            remaining -= fill;
284            if remaining <= Decimal::ZERO {
285                break;
286            }
287        }
288
289        if remaining > Decimal::ZERO {
290            return Err(FinError::InsufficientLiquidity(target));
291        }
292
293        Ok(total_cost / target)
294    }
295
296    /// Returns the last successfully applied sequence number.
297    pub fn sequence(&self) -> u64 {
298        self.sequence
299    }
300
301    /// Returns the top `n` bid and ask levels as a snapshot.
302    ///
303    /// Returns `(bids, asks)` where bids are in descending price order and
304    /// asks are in ascending price order.
305    pub fn snapshot(&self, n: usize) -> (Vec<PriceLevel>, Vec<PriceLevel>) {
306        (self.top_bids(n), self.top_asks(n))
307    }
308
309    /// Returns the number of bid price levels.
310    pub fn bid_count(&self) -> usize {
311        self.bids.len()
312    }
313
314    /// Returns the number of ask price levels.
315    pub fn ask_count(&self) -> usize {
316        self.asks.len()
317    }
318
319    /// Returns the number of price levels on the given `side`.
320    pub fn level_count(&self, side: Side) -> usize {
321        match side {
322            Side::Bid => self.bids.len(),
323            Side::Ask => self.asks.len(),
324        }
325    }
326
327    /// Removes all price levels from both sides of the book, resetting sequence to 0.
328    pub fn clear(&mut self) {
329        self.bids.clear();
330        self.asks.clear();
331        self.sequence = 0;
332    }
333
334    /// Removes all resting levels from `side`, leaving the opposite side intact.
335    ///
336    /// Useful when a snapshot update arrives for one side only (e.g., bid-side snapshot).
337    pub fn remove_all(&mut self, side: crate::types::Side) {
338        use crate::types::Side;
339        match side {
340            Side::Bid => self.bids.clear(),
341            Side::Ask => self.asks.clear(),
342        }
343    }
344
345    /// Returns `true` if the book is currently in a crossed (inverted) state.
346    ///
347    /// A book is crossed when `best_bid >= best_ask`. Under normal operation this
348    /// is always `false` since `apply_delta` rejects crossing deltas.
349    /// Provided for diagnostic / assertion use.
350    pub fn is_crossed(&self) -> bool {
351        match (self.best_bid(), self.best_ask()) {
352            (Some(bid), Some(ask)) => bid.price >= ask.price,
353            _ => false,
354        }
355    }
356
357    /// Returns `true` if both sides of the book have no resting quantity.
358    pub fn is_empty(&self) -> bool {
359        self.bids.is_empty() && self.asks.is_empty()
360    }
361
362    /// Returns the total number of distinct price levels across both sides.
363    pub fn total_levels(&self) -> usize {
364        self.bids.len() + self.asks.len()
365    }
366
367    /// Returns the total resting quantity available on `side` up to and including `price`.
368    ///
369    /// For bids: sums all bid levels at prices `>= price` (levels at or above the given price).
370    /// For asks: sums all ask levels at prices `<= price` (levels at or below the given price).
371    ///
372    /// Returns `Decimal::ZERO` when there are no matching levels.
373    pub fn cumulative_depth(&self, side: Side, price: Price) -> Decimal {
374        let p = price.value();
375        match side {
376            Side::Bid => self
377                .bids
378                .range(p..)
379                .map(|(_, qty)| *qty)
380                .sum(),
381            Side::Ask => self
382                .asks
383                .range(..=p)
384                .map(|(_, qty)| *qty)
385                .sum(),
386        }
387    }
388
389    /// Returns the total resting quantity on the bid side.
390    pub fn total_bid_volume(&self) -> Decimal {
391        self.bids.values().copied().sum()
392    }
393
394    /// Returns the total resting quantity on the ask side.
395    pub fn total_ask_volume(&self) -> Decimal {
396        self.asks.values().copied().sum()
397    }
398
399    /// Returns the best bid price, or `None` if the bid side is empty.
400    pub fn best_bid_price(&self) -> Option<Price> {
401        self.bids.keys().next_back().and_then(|p| Price::new(*p).ok())
402    }
403
404    /// Returns the best ask price, or `None` if the ask side is empty.
405    pub fn best_ask_price(&self) -> Option<Price> {
406        self.asks.keys().next().and_then(|p| Price::new(*p).ok())
407    }
408
409    /// Returns the resting quantity at the best bid, or `None` if the bid side is empty.
410    pub fn best_bid_qty(&self) -> Option<Quantity> {
411        self.bids
412            .values()
413            .next_back()
414            .and_then(|q| Quantity::new(*q).ok())
415    }
416
417    /// Returns the resting quantity at the best ask, or `None` if the ask side is empty.
418    pub fn best_ask_qty(&self) -> Option<Quantity> {
419        self.asks
420            .values()
421            .next()
422            .and_then(|q| Quantity::new(*q).ok())
423    }
424
425    /// Returns the total resting quantity on `side` within `pct_from_mid` percent of the mid-price.
426    ///
427    /// For example, `liquidity_at_pct(Side::Ask, dec!(0.5))` returns all ask volume
428    /// within 0.5% above the mid-price. Returns `None` when the book has no mid-price.
429    pub fn liquidity_at_pct(&self, side: Side, pct_from_mid: Decimal) -> Option<Decimal> {
430        let mid = self.mid_price()?;
431        let band = mid * pct_from_mid / Decimal::ONE_HUNDRED;
432        let (lo, hi) = match side {
433            Side::Bid => (mid - band, mid),
434            Side::Ask => (mid, mid + band),
435        };
436        let qty: Decimal = match side {
437            Side::Bid => self
438                .bids
439                .range(lo..=hi)
440                .map(|(_, q)| *q)
441                .sum(),
442            Side::Ask => self
443                .asks
444                .range(lo..=hi)
445                .map(|(_, q)| *q)
446                .sum(),
447        };
448        Some(qty)
449    }
450
451    /// Returns `true` if `price` is present in the given `side` of the book.
452    pub fn has_price(&self, side: Side, price: Price) -> bool {
453        let key = price.value();
454        match side {
455            Side::Bid => self.bids.contains_key(&key),
456            Side::Ask => self.asks.contains_key(&key),
457        }
458    }
459
460    /// Returns the quantity-weighted midpoint (micro-price).
461    ///
462    /// Weights best-bid by ask quantity and best-ask by bid quantity:
463    /// `(bid_price × ask_qty + ask_price × bid_qty) / (bid_qty + ask_qty)`.
464    /// Returns `None` when either side is empty.
465    pub fn weighted_mid(&self) -> Option<Decimal> {
466        let bid = self.best_bid()?;
467        let ask = self.best_ask()?;
468        let bid_qty = bid.quantity.value();
469        let ask_qty = ask.quantity.value();
470        let total = bid_qty + ask_qty;
471        if total.is_zero() {
472            return None;
473        }
474        Some((bid.price.value() * ask_qty + ask.price.value() * bid_qty) / total)
475    }
476
477    /// Returns the order-book imbalance: `(bid_vol - ask_vol) / (bid_vol + ask_vol)`.
478    ///
479    /// Returns `None` when both sides are empty (division by zero).
480    /// Range is `(-1, 1)`: positive = bid-heavy, negative = ask-heavy.
481    pub fn imbalance(&self) -> Option<Decimal> {
482        let bid_vol = self.total_bid_volume();
483        let ask_vol = self.total_ask_volume();
484        let total = bid_vol + ask_vol;
485        if total == Decimal::ZERO {
486            return None;
487        }
488        Some((bid_vol - ask_vol) / total)
489    }
490
491    /// Returns the depth ratio `top_n_bid_vol / top_n_ask_vol` for the best `n` levels.
492    ///
493    /// A ratio > 1 indicates more buying pressure at the top of book; < 1 more selling pressure.
494    /// Returns `None` when either side has no levels in the top-`n` or ask volume is zero.
495    pub fn depth_ratio(&self, n: usize) -> Option<Decimal> {
496        let bid_vol: Decimal = self.bids.values().rev().take(n).copied().sum();
497        let ask_vol: Decimal = self.asks.values().take(n).copied().sum();
498        if ask_vol.is_zero() {
499            return None;
500        }
501        Some(bid_vol / ask_vol)
502    }
503
504    /// Returns the weighted mid price: `(best_bid * ask_qty + best_ask * bid_qty) / (bid_qty + ask_qty)`.
505    ///
506    /// Weights the midpoint by the opposite side's quantity, so a thick ask pulls the WMP toward bid.
507    /// Returns `None` when either side is empty.
508    pub fn weighted_mid_price(&self) -> Option<Decimal> {
509        let (bid_p, bid_q) = self.bids.iter().next_back()?;
510        let (ask_p, ask_q) = self.asks.iter().next()?;
511        let total_q = bid_q + ask_q;
512        if total_q.is_zero() {
513            return None;
514        }
515        Some((*bid_p * *ask_q + *ask_p * *bid_q) / total_q)
516    }
517
518    /// Returns all price levels on `side` whose price falls within `[lo, hi]` (inclusive).
519    ///
520    /// Useful for computing the available liquidity within a price band.
521    pub fn price_levels_between(&self, side: Side, lo: Price, hi: Price) -> Vec<PriceLevel> {
522        let lo_val = lo.value();
523        let hi_val = hi.value();
524        match side {
525            Side::Bid => self
526                .bids
527                .range(lo_val..=hi_val)
528                .map(|(p, q)| PriceLevel {
529                    price: Price::new(*p).unwrap_or(lo),
530                    quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
531                })
532                .collect(),
533            Side::Ask => self
534                .asks
535                .range(lo_val..=hi_val)
536                .map(|(p, q)| PriceLevel {
537                    price: Price::new(*p).unwrap_or(lo),
538                    quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
539                })
540                .collect(),
541        }
542    }
543
544    /// Returns the smallest price increment between adjacent levels on either side.
545    ///
546    /// Useful for estimating the instrument's native tick size from live book data.
547    /// Returns `None` when both sides have fewer than 2 levels.
548    pub fn tick_size(&self) -> Option<Decimal> {
549        let bid_tick = self
550            .bids
551            .keys()
552            .collect::<Vec<_>>()
553            .windows(2)
554            .map(|w| (*w[1] - *w[0]).abs())
555            .filter(|d| !d.is_zero())
556            .reduce(Decimal::min);
557        let ask_tick = self
558            .asks
559            .keys()
560            .collect::<Vec<_>>()
561            .windows(2)
562            .map(|w| (*w[1] - *w[0]).abs())
563            .filter(|d| !d.is_zero())
564            .reduce(Decimal::min);
565        match (bid_tick, ask_tick) {
566            (Some(b), Some(a)) => Some(b.min(a)),
567            (Some(b), None) => Some(b),
568            (None, Some(a)) => Some(a),
569            (None, None) => None,
570        }
571    }
572
573    /// Returns the bid-to-ask volume ratio: `total_bid_volume / total_ask_volume`.
574    ///
575    /// Values > 1 indicate more buy-side depth; values < 1 indicate more sell-side depth.
576    /// Returns `None` if either side is empty (to avoid division by zero).
577    pub fn bid_ask_ratio(&self) -> Option<Decimal> {
578        let bid = self.total_bid_volume();
579        let ask = self.total_ask_volume();
580        if ask.is_zero() || bid.is_zero() {
581            return None;
582        }
583        Some(bid / ask)
584    }
585
586    /// Estimates the average fill price for a market order of `qty` on `side`.
587    ///
588    /// Walks the book levels in price-time priority and returns the volume-weighted
589    /// average price. Returns `None` if `qty` is zero or the book cannot fill `qty`
590    /// in full (insufficient depth).
591    pub fn price_impact(&self, side: crate::types::Side, qty: crate::types::Quantity) -> Option<Decimal> {
592        use crate::types::Side;
593        if qty.is_zero() {
594            return None;
595        }
596        let levels: Vec<_> = match side {
597            Side::Bid => {
598                // Buying: walk asks from lowest to highest price
599                let mut asks: Vec<_> = self.asks.iter().collect();
600                asks.sort_by(|a, b| a.0.cmp(b.0));
601                asks.into_iter().map(|(p, q)| (*p, *q)).collect()
602            }
603            Side::Ask => {
604                // Selling: walk bids from highest to lowest price
605                let mut bids: Vec<_> = self.bids.iter().collect();
606                bids.sort_by(|a, b| b.0.cmp(a.0));
607                bids.into_iter().map(|(p, q)| (*p, *q)).collect()
608            }
609        };
610        let target = qty.value();
611        let mut remaining = target;
612        let mut notional = Decimal::ZERO;
613        for (price, level_qty) in levels {
614            let fill = level_qty.min(remaining);
615            notional += price * fill;
616            remaining -= fill;
617            if remaining <= Decimal::ZERO {
618                break;
619            }
620        }
621        if remaining > Decimal::ZERO {
622            None // insufficient depth
623        } else {
624            Some(notional / target)
625        }
626    }
627
628    /// Returns the top `n` bid levels in descending price order (best bid first).
629    ///
630    /// Returns fewer than `n` levels if the bid side has fewer entries.
631    pub fn bid_depth(&self, n: usize) -> Vec<PriceLevel> {
632        self.bids
633            .iter()
634            .rev()
635            .take(n)
636            .map(|(price, qty)| PriceLevel {
637                price: Price::new(*price).unwrap(),
638                quantity: Quantity::new(*qty).unwrap(),
639            })
640            .collect()
641    }
642
643    /// Returns the top `n` ask levels in ascending price order (best ask first).
644    ///
645    /// Returns fewer than `n` levels if the ask side has fewer entries.
646    pub fn ask_depth(&self, n: usize) -> Vec<PriceLevel> {
647        self.asks
648            .iter()
649            .take(n)
650            .map(|(price, qty)| PriceLevel {
651                price: Price::new(*price).unwrap(),
652                quantity: Quantity::new(*qty).unwrap(),
653            })
654            .collect()
655    }
656
657    /// Returns the depth imbalance ratio: `(bid_qty - ask_qty) / (bid_qty + ask_qty)`.
658    ///
659    /// Result is in `[-1.0, 1.0]`:
660    /// - Positive → more bid-side depth (buying pressure)
661    /// - Negative → more ask-side depth (selling pressure)
662    /// - `None` when both sides are empty (total depth is zero)
663    pub fn depth_imbalance(&self) -> Option<Decimal> {
664        let bid_qty: Decimal = self.bids.values().sum();
665        let ask_qty: Decimal = self.asks.values().sum();
666        let total = bid_qty + ask_qty;
667        if total.is_zero() {
668            return None;
669        }
670        Some((bid_qty - ask_qty) / total)
671    }
672
673    /// Returns the ask-to-bid quantity ratio: `total_ask_qty / total_bid_qty`.
674    ///
675    /// Values above 1 indicate more supply than demand at visible depth levels.
676    /// Returns `None` when total bid quantity is zero (avoid division by zero).
677    pub fn ask_bid_ratio(&self) -> Option<Decimal> {
678        let bid_qty: Decimal = self.bids.values().sum();
679        let ask_qty: Decimal = self.asks.values().sum();
680        if bid_qty.is_zero() {
681            return None;
682        }
683        Some(ask_qty / bid_qty)
684    }
685
686    /// Returns the total quantity across all bid price levels.
687    pub fn total_bid_depth(&self) -> Decimal {
688        self.bids.values().sum()
689    }
690
691    /// Returns the total quantity across all ask price levels.
692    pub fn total_ask_depth(&self) -> Decimal {
693        self.asks.values().sum()
694    }
695
696    /// Walks the book on `side` to find the price level reached after consuming `target_qty`.
697    ///
698    /// For `Side::Ask` walks ascending (cheapest ask first).
699    /// For `Side::Bid` walks descending (highest bid first).
700    ///
701    /// Returns the price of the level where `target_qty` is fully absorbed, or the last
702    /// available level if the book lacks sufficient depth.
703    /// Returns `None` when the side has no levels or `target_qty` is zero.
704    pub fn price_at_volume(&self, side: Side, target_qty: Decimal) -> Option<Price> {
705        if target_qty.is_zero() {
706            return None;
707        }
708        let mut remaining = target_qty;
709        let mut last_price: Option<Price> = None;
710
711        match side {
712            Side::Ask => {
713                for (&px, &qty) in &self.asks {
714                    last_price = Price::new(px).ok();
715                    if qty >= remaining {
716                        return last_price;
717                    }
718                    remaining -= qty;
719                }
720            }
721            Side::Bid => {
722                for (&px, &qty) in self.bids.iter().rev() {
723                    last_price = Price::new(px).ok();
724                    if qty >= remaining {
725                        return last_price;
726                    }
727                    remaining -= qty;
728                }
729            }
730        }
731        last_price
732    }
733
734    /// Returns up to `n` best bid levels in descending price order (best bid first).
735    ///
736    /// Returns an empty `Vec` when the bid side is empty or `n == 0`.
737    pub fn top_n_bid_levels(&self, n: usize) -> Vec<PriceLevel> {
738        if n == 0 {
739            return vec![];
740        }
741        self.bids
742            .iter()
743            .rev()
744            .take(n)
745            .filter_map(|(&px, &qty)| {
746                let price = Price::new(px).ok()?;
747                let quantity = Quantity::new(qty).ok()?;
748                Some(PriceLevel { price, quantity })
749            })
750            .collect()
751    }
752
753    /// Returns up to `n` best ask levels in ascending price order (best ask first).
754    ///
755    /// Returns an empty `Vec` when the ask side is empty or `n == 0`.
756    pub fn top_n_ask_levels(&self, n: usize) -> Vec<PriceLevel> {
757        if n == 0 {
758            return vec![];
759        }
760        self.asks
761            .iter()
762            .take(n)
763            .filter_map(|(&px, &qty)| {
764                let price = Price::new(px).ok()?;
765                let quantity = Quantity::new(qty).ok()?;
766                Some(PriceLevel { price, quantity })
767            })
768            .collect()
769    }
770
771    /// Returns the total quantity across the top `n` bid levels.
772    ///
773    /// Sweeps from the best (highest) bid downwards and sums quantities.
774    /// Returns zero when the bid side is empty or `n == 0`.
775    pub fn cumulative_bid_qty(&self, n: usize) -> Decimal {
776        if n == 0 {
777            return Decimal::ZERO;
778        }
779        self.bids.iter().rev().take(n).map(|(_, &qty)| qty).sum()
780    }
781
782    /// Returns the bid-to-ask depth skew across the top `n` levels on each side.
783    ///
784    /// `bid_depth_skew = cumulative_bid_qty(n) / (cumulative_bid_qty(n) + cumulative_ask_qty(n))`.
785    /// Range: 0.0 (all ask-side depth) to 1.0 (all bid-side depth).
786    /// Returns `None` if both sides are empty or `n == 0`.
787    pub fn bid_depth_skew(&self, n: usize) -> Option<Decimal> {
788        if n == 0 {
789            return None;
790        }
791        let bid_qty = self.cumulative_bid_qty(n);
792        let ask_qty = self.cumulative_ask_qty(n);
793        let total = bid_qty + ask_qty;
794        if total.is_zero() {
795            return None;
796        }
797        bid_qty.checked_div(total)
798    }
799
800    /// Returns the bid-ask spread in basis points.
801    ///
802    /// `spread_bps = (best_ask - best_bid) / mid_price * 10_000`.
803    /// Returns `None` if either side is empty or mid-price is zero.
804    pub fn spread_bps(&self) -> Option<Decimal> {
805        let bid = self.best_bid()?.price.value();
806        let ask = self.best_ask()?.price.value();
807        let mid = (bid + ask) / Decimal::TWO;
808        if mid.is_zero() {
809            return None;
810        }
811        let spread = ask - bid;
812        spread.checked_div(mid).map(|r| r * Decimal::from(10_000u32))
813    }
814
815    /// Returns the total quantity across the top `n` ask levels.
816    ///
817    /// Sweeps from the best (lowest) ask upwards and sums quantities.
818    /// Returns zero when the ask side is empty or `n == 0`.
819    pub fn cumulative_ask_qty(&self, n: usize) -> Decimal {
820        if n == 0 {
821            return Decimal::ZERO;
822        }
823        self.asks.iter().take(n).map(|(_, &qty)| qty).sum()
824    }
825}
826
827#[cfg(test)]
828mod tests {
829    use super::*;
830    use rust_decimal_macros::dec;
831
832    fn make_book() -> OrderBook {
833        OrderBook::new(Symbol::new("AAPL").unwrap())
834    }
835
836    fn set_delta(side: Side, price: &str, qty: &str, seq: u64) -> BookDelta {
837        BookDelta {
838            side,
839            price: Price::new(price.parse().unwrap()).unwrap(),
840            quantity: Quantity::new(qty.parse().unwrap()).unwrap(),
841            action: DeltaAction::Set,
842            sequence: seq,
843        }
844    }
845
846    fn remove_delta(side: Side, price: &str, seq: u64) -> BookDelta {
847        BookDelta {
848            side,
849            price: Price::new(price.parse().unwrap()).unwrap(),
850            quantity: Quantity::zero(),
851            action: DeltaAction::Remove,
852            sequence: seq,
853        }
854    }
855
856    #[test]
857    fn test_orderbook_apply_delta_updates_bid() {
858        let mut book = make_book();
859        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
860            .unwrap();
861        let best = book.best_bid().unwrap();
862        assert_eq!(best.price.value(), dec!(100));
863        assert_eq!(best.quantity.value(), dec!(10));
864    }
865
866    #[test]
867    fn test_orderbook_apply_delta_updates_ask() {
868        let mut book = make_book();
869        book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
870            .unwrap();
871        let best = book.best_ask().unwrap();
872        assert_eq!(best.price.value(), dec!(101));
873        assert_eq!(best.quantity.value(), dec!(5));
874    }
875
876    #[test]
877    fn test_orderbook_sequence_mismatch_returns_error() {
878        let mut book = make_book();
879        let result = book.apply_delta(set_delta(Side::Bid, "100", "10", 2));
880        assert!(matches!(
881            result,
882            Err(FinError::SequenceMismatch {
883                expected: 1,
884                got: 2
885            })
886        ));
887    }
888
889    #[test]
890    fn test_orderbook_sequence_advances_correctly() {
891        let mut book = make_book();
892        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
893            .unwrap();
894        assert_eq!(book.sequence(), 1);
895        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
896            .unwrap();
897        assert_eq!(book.sequence(), 2);
898    }
899
900    #[test]
901    fn test_orderbook_best_bid_max_price() {
902        let mut book = make_book();
903        book.apply_delta(set_delta(Side::Bid, "99", "10", 1))
904            .unwrap();
905        book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
906            .unwrap();
907        book.apply_delta(set_delta(Side::Bid, "98", "20", 3))
908            .unwrap();
909        let best = book.best_bid().unwrap();
910        assert_eq!(best.price.value(), dec!(100));
911    }
912
913    #[test]
914    fn test_orderbook_best_ask_min_price() {
915        let mut book = make_book();
916        book.apply_delta(set_delta(Side::Ask, "102", "10", 1))
917            .unwrap();
918        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
919            .unwrap();
920        book.apply_delta(set_delta(Side::Ask, "103", "20", 3))
921            .unwrap();
922        let best = book.best_ask().unwrap();
923        assert_eq!(best.price.value(), dec!(101));
924    }
925
926    #[test]
927    fn test_orderbook_spread_positive() {
928        let mut book = make_book();
929        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
930            .unwrap();
931        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
932            .unwrap();
933        let spread = book.spread().unwrap();
934        assert_eq!(spread, dec!(1));
935        assert!(spread > Decimal::ZERO);
936    }
937
938    #[test]
939    fn test_orderbook_mid_price() {
940        let mut book = make_book();
941        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
942            .unwrap();
943        book.apply_delta(set_delta(Side::Ask, "102", "5", 2))
944            .unwrap();
945        let mid = book.mid_price().unwrap();
946        assert_eq!(mid, dec!(101));
947    }
948
949    #[test]
950    fn test_orderbook_spread_none_when_empty() {
951        let book = make_book();
952        assert!(book.spread().is_none());
953    }
954
955    #[test]
956    fn test_orderbook_vwap_insufficient_liquidity() {
957        let mut book = make_book();
958        book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
959            .unwrap();
960        let result = book.vwap_for_qty(Side::Ask, Quantity::new(dec!(100)).unwrap());
961        assert!(matches!(result, Err(FinError::InsufficientLiquidity(_))));
962    }
963
964    #[test]
965    fn test_orderbook_vwap_single_level() {
966        let mut book = make_book();
967        book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
968            .unwrap();
969        let vwap = book
970            .vwap_for_qty(Side::Ask, Quantity::new(dec!(5)).unwrap())
971            .unwrap();
972        assert_eq!(vwap, dec!(100));
973    }
974
975    #[test]
976    fn test_orderbook_vwap_multi_level() {
977        let mut book = make_book();
978        book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
979            .unwrap();
980        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
981            .unwrap();
982        // 5 @ 100 + 5 @ 101 = 1005 / 10 = 100.5
983        let vwap = book
984            .vwap_for_qty(Side::Ask, Quantity::new(dec!(10)).unwrap())
985            .unwrap();
986        assert_eq!(vwap, dec!(100.5));
987    }
988
989    #[test]
990    fn test_orderbook_remove_level_delta() {
991        let mut book = make_book();
992        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
993            .unwrap();
994        book.apply_delta(remove_delta(Side::Bid, "100", 2)).unwrap();
995        assert!(book.best_bid().is_none());
996    }
997
998    #[test]
999    fn test_orderbook_top_bids_order() {
1000        let mut book = make_book();
1001        book.apply_delta(set_delta(Side::Bid, "98", "10", 1))
1002            .unwrap();
1003        book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
1004            .unwrap();
1005        book.apply_delta(set_delta(Side::Bid, "99", "20", 3))
1006            .unwrap();
1007        let top = book.top_bids(2);
1008        assert_eq!(top[0].price.value(), dec!(100));
1009        assert_eq!(top[1].price.value(), dec!(99));
1010    }
1011
1012    #[test]
1013    fn test_orderbook_top_asks_order() {
1014        let mut book = make_book();
1015        book.apply_delta(set_delta(Side::Ask, "103", "10", 1))
1016            .unwrap();
1017        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
1018            .unwrap();
1019        book.apply_delta(set_delta(Side::Ask, "102", "20", 3))
1020            .unwrap();
1021        let top = book.top_asks(2);
1022        assert_eq!(top[0].price.value(), dec!(101));
1023        assert_eq!(top[1].price.value(), dec!(102));
1024    }
1025
1026    #[test]
1027    fn test_orderbook_bid_count_ask_count() {
1028        let mut book = make_book();
1029        book.apply_delta(set_delta(Side::Bid, "100", "1", 1))
1030            .unwrap();
1031        book.apply_delta(set_delta(Side::Ask, "101", "1", 2))
1032            .unwrap();
1033        assert_eq!(book.bid_count(), 1);
1034        assert_eq!(book.ask_count(), 1);
1035    }
1036
1037    #[test]
1038    fn test_orderbook_vwap_zero_qty_returns_zero() {
1039        let mut book = make_book();
1040        book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
1041            .unwrap();
1042        let vwap = book.vwap_for_qty(Side::Ask, Quantity::zero()).unwrap();
1043        assert_eq!(vwap, Decimal::ZERO);
1044    }
1045
1046    // ── Inverted spread guard ─────────────────────────────────────────────────
1047
1048    #[test]
1049    fn test_apply_delta_rejects_inverted_spread() {
1050        let mut book = make_book();
1051        // Set ask at 100
1052        book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1053            .unwrap();
1054        // Try to set bid at 101 (would cross the ask): must fail
1055        let result = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1056        assert!(
1057            matches!(result, Err(FinError::InvertedSpread { .. })),
1058            "expected InvertedSpread, got {:?}",
1059            result
1060        );
1061    }
1062
1063    #[test]
1064    fn test_apply_delta_inverted_spread_rolls_back_sequence() {
1065        let mut book = make_book();
1066        book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1067            .unwrap();
1068        assert_eq!(book.sequence(), 1);
1069        // This should fail and leave sequence unchanged
1070        let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1071        assert_eq!(
1072            book.sequence(),
1073            1,
1074            "sequence must not advance on rejected delta"
1075        );
1076    }
1077
1078    #[test]
1079    fn test_apply_delta_inverted_spread_rolled_back_book_state() {
1080        let mut book = make_book();
1081        book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1082            .unwrap();
1083        // Rejected bid at 101 must not persist in the book
1084        let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1085        assert!(
1086            book.best_bid().is_none(),
1087            "rejected bid must not appear in book"
1088        );
1089    }
1090
1091    /// Empty book mid_price returns None.
1092    #[test]
1093    fn test_empty_book_mid_price_returns_none() {
1094        let book = make_book();
1095        assert!(
1096            book.mid_price().is_none(),
1097            "empty book mid_price must be None"
1098        );
1099    }
1100
1101    /// Empty book best_bid returns None.
1102    #[test]
1103    fn test_empty_book_best_bid_returns_none() {
1104        let book = make_book();
1105        assert!(book.best_bid().is_none());
1106    }
1107
1108    /// Empty book best_ask returns None.
1109    #[test]
1110    fn test_empty_book_best_ask_returns_none() {
1111        let book = make_book();
1112        assert!(book.best_ask().is_none());
1113    }
1114
1115    /// Best bid/ask after many inserts and removes reflects only surviving levels.
1116    #[test]
1117    fn test_best_bid_after_many_inserts_and_removes() {
1118        let mut book = make_book();
1119        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
1120            .unwrap();
1121        book.apply_delta(set_delta(Side::Bid, "105", "5", 2))
1122            .unwrap();
1123        book.apply_delta(set_delta(Side::Bid, "103", "8", 3))
1124            .unwrap();
1125        // Remove 105 (was best bid)
1126        book.apply_delta(remove_delta(Side::Bid, "105", 4)).unwrap();
1127        let best = book.best_bid().unwrap();
1128        assert_eq!(
1129            best.price.value(),
1130            dec!(103),
1131            "best bid after removing top level must be 103"
1132        );
1133    }
1134
1135    #[test]
1136    fn test_best_ask_after_many_inserts_and_removes() {
1137        let mut book = make_book();
1138        book.apply_delta(set_delta(Side::Ask, "110", "10", 1))
1139            .unwrap();
1140        book.apply_delta(set_delta(Side::Ask, "108", "5", 2))
1141            .unwrap();
1142        book.apply_delta(set_delta(Side::Ask, "109", "8", 3))
1143            .unwrap();
1144        // Remove 108 (was best ask)
1145        book.apply_delta(remove_delta(Side::Ask, "108", 4)).unwrap();
1146        let best = book.best_ask().unwrap();
1147        assert_eq!(
1148            best.price.value(),
1149            dec!(109),
1150            "best ask after removing top level must be 109"
1151        );
1152    }
1153
1154    /// Crossed book detection: ask <= bid must return InvertedSpread.
1155    #[test]
1156    fn test_crossed_book_ask_at_bid_price_rejected() {
1157        let mut book = make_book();
1158        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
1159            .unwrap();
1160        let result = book.apply_delta(set_delta(Side::Ask, "100", "5", 2));
1161        assert!(
1162            matches!(result, Err(FinError::InvertedSpread { .. })),
1163            "ask at bid price must produce InvertedSpread"
1164        );
1165    }
1166
1167    /// Empty book spread returns None.
1168    #[test]
1169    fn test_empty_book_spread_returns_none() {
1170        let book = make_book();
1171        assert!(book.spread().is_none());
1172    }
1173
1174    #[test]
1175    fn test_orderbook_snapshot_returns_top_n_both_sides() {
1176        let mut book = make_book();
1177        book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
1178        book.apply_delta(set_delta(Side::Bid, "100", "5", 2)).unwrap();
1179        book.apply_delta(set_delta(Side::Ask, "101", "3", 3)).unwrap();
1180        book.apply_delta(set_delta(Side::Ask, "102", "7", 4)).unwrap();
1181        let (bids, asks) = book.snapshot(2);
1182        assert_eq!(bids.len(), 2);
1183        assert_eq!(asks.len(), 2);
1184        assert_eq!(bids[0].price.value(), dec!(100));
1185        assert_eq!(asks[0].price.value(), dec!(101));
1186    }
1187
1188    #[test]
1189    fn test_orderbook_snapshot_empty_book() {
1190        let book = make_book();
1191        let (bids, asks) = book.snapshot(5);
1192        assert!(bids.is_empty());
1193        assert!(asks.is_empty());
1194    }
1195
1196    #[test]
1197    fn test_orderbook_clear_removes_all_levels() {
1198        let mut book = make_book();
1199        book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
1200        book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1201        assert_eq!(book.bid_count(), 1);
1202        assert_eq!(book.ask_count(), 1);
1203        book.clear();
1204        assert_eq!(book.bid_count(), 0);
1205        assert_eq!(book.ask_count(), 0);
1206        assert_eq!(book.sequence(), 0);
1207    }
1208
1209    #[test]
1210    fn test_orderbook_clear_allows_fresh_deltas() {
1211        let mut book = make_book();
1212        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1213        book.clear();
1214        // After clear, sequence resets to 0, so next delta must be seq=1
1215        assert!(book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).is_ok());
1216    }
1217
1218    #[test]
1219    fn test_orderbook_total_bid_volume() {
1220        let mut book = make_book();
1221        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1222        book.apply_delta(set_delta(Side::Bid, "99", "3", 2)).unwrap();
1223        assert_eq!(book.total_bid_volume(), dec!(8));
1224    }
1225
1226    #[test]
1227    fn test_orderbook_total_ask_volume() {
1228        let mut book = make_book();
1229        book.apply_delta(set_delta(Side::Ask, "101", "4", 1)).unwrap();
1230        book.apply_delta(set_delta(Side::Ask, "102", "6", 2)).unwrap();
1231        assert_eq!(book.total_ask_volume(), dec!(10));
1232    }
1233
1234    #[test]
1235    fn test_orderbook_total_bid_volume_empty() {
1236        let book = make_book();
1237        assert_eq!(book.total_bid_volume(), dec!(0));
1238    }
1239
1240    #[test]
1241    fn test_orderbook_imbalance_balanced() {
1242        let mut book = make_book();
1243        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1244        book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1245        assert_eq!(book.imbalance().unwrap(), dec!(0));
1246    }
1247
1248    #[test]
1249    fn test_orderbook_imbalance_bid_heavy() {
1250        let mut book = make_book();
1251        book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
1252        book.apply_delta(set_delta(Side::Ask, "101", "1", 2)).unwrap();
1253        // (9 - 1) / 10 = 0.8
1254        assert_eq!(book.imbalance().unwrap(), dec!(0.8));
1255    }
1256
1257    #[test]
1258    fn test_orderbook_imbalance_ask_heavy() {
1259        let mut book = make_book();
1260        book.apply_delta(set_delta(Side::Bid, "100", "1", 1)).unwrap();
1261        book.apply_delta(set_delta(Side::Ask, "101", "9", 2)).unwrap();
1262        // (1 - 9) / 10 = -0.8
1263        assert_eq!(book.imbalance().unwrap(), dec!(-0.8));
1264    }
1265
1266    #[test]
1267    fn test_orderbook_imbalance_empty_returns_none() {
1268        let book = make_book();
1269        assert!(book.imbalance().is_none());
1270    }
1271
1272    #[test]
1273    fn test_orderbook_has_price_bid_present() {
1274        let mut book = make_book();
1275        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1276        let price = Price::new(dec!(100)).unwrap();
1277        assert!(book.has_price(Side::Bid, price));
1278        assert!(!book.has_price(Side::Ask, price));
1279    }
1280
1281    #[test]
1282    fn test_orderbook_has_price_ask_present() {
1283        let mut book = make_book();
1284        book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1285        let price = Price::new(dec!(101)).unwrap();
1286        assert!(book.has_price(Side::Ask, price));
1287        assert!(!book.has_price(Side::Bid, price));
1288    }
1289
1290    #[test]
1291    fn test_orderbook_has_price_absent() {
1292        let book = make_book();
1293        let price = Price::new(dec!(100)).unwrap();
1294        assert!(!book.has_price(Side::Bid, price));
1295        assert!(!book.has_price(Side::Ask, price));
1296    }
1297
1298    #[test]
1299    fn test_orderbook_has_price_false_after_remove() {
1300        let mut book = make_book();
1301        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1302        book.apply_delta(BookDelta {
1303            side: Side::Bid,
1304            price: Price::new(dec!(100)).unwrap(),
1305            quantity: Quantity::zero(),
1306            action: DeltaAction::Remove,
1307            sequence: 2,
1308        })
1309        .unwrap();
1310        let price = Price::new(dec!(100)).unwrap();
1311        assert!(!book.has_price(Side::Bid, price));
1312    }
1313
1314    #[test]
1315    fn test_orderbook_level_count_bids() {
1316        let mut book = make_book();
1317        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1318        book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1319        assert_eq!(book.level_count(Side::Bid), 2);
1320        assert_eq!(book.level_count(Side::Ask), 0);
1321    }
1322
1323    #[test]
1324    fn test_orderbook_level_count_asks() {
1325        let mut book = make_book();
1326        book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1327        assert_eq!(book.level_count(Side::Ask), 1);
1328        assert_eq!(book.level_count(Side::Bid), 0);
1329    }
1330
1331    #[test]
1332    fn test_orderbook_weighted_mid_equal_qty() {
1333        let mut book = make_book();
1334        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1335        book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1336        // Equal qty → simple midpoint
1337        assert_eq!(book.weighted_mid().unwrap(), dec!(101));
1338    }
1339
1340    #[test]
1341    fn test_orderbook_weighted_mid_bid_heavy() {
1342        let mut book = make_book();
1343        book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
1344        book.apply_delta(set_delta(Side::Ask, "110", "1", 2)).unwrap();
1345        // (100*1 + 110*9) / (9+1) = (100 + 990) / 10 = 109
1346        assert_eq!(book.weighted_mid().unwrap(), dec!(109));
1347    }
1348
1349    #[test]
1350    fn test_orderbook_weighted_mid_empty_returns_none() {
1351        let book = make_book();
1352        assert!(book.weighted_mid().is_none());
1353    }
1354
1355    #[test]
1356    fn test_orderbook_bid_ask_ratio_equal_volumes() {
1357        let mut book = make_book();
1358        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1359        book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
1360        assert_eq!(book.bid_ask_ratio().unwrap(), dec!(1));
1361    }
1362
1363    #[test]
1364    fn test_orderbook_bid_ask_ratio_bid_heavy() {
1365        let mut book = make_book();
1366        book.apply_delta(set_delta(Side::Bid, "100", "20", 1)).unwrap();
1367        book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
1368        assert_eq!(book.bid_ask_ratio().unwrap(), dec!(2));
1369    }
1370
1371    #[test]
1372    fn test_orderbook_bid_ask_ratio_empty_returns_none() {
1373        let book = make_book();
1374        assert!(book.bid_ask_ratio().is_none());
1375    }
1376
1377    #[test]
1378    fn test_orderbook_price_impact_buy_single_level() {
1379        let mut book = make_book();
1380        book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1381        let qty = Quantity::new(dec!(5)).unwrap();
1382        let avg = book.price_impact(Side::Bid, qty).unwrap();
1383        assert_eq!(avg, dec!(101));
1384    }
1385
1386    #[test]
1387    fn test_orderbook_price_impact_buy_spans_two_levels() {
1388        let mut book = make_book();
1389        book.apply_delta(set_delta(Side::Ask, "100", "5", 1)).unwrap();
1390        book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1391        // 5 @ 100 + 5 @ 102 = 1010 / 10 = 101
1392        let qty = Quantity::new(dec!(10)).unwrap();
1393        let avg = book.price_impact(Side::Bid, qty).unwrap();
1394        assert_eq!(avg, dec!(101));
1395    }
1396
1397    #[test]
1398    fn test_orderbook_price_impact_insufficient_depth_returns_none() {
1399        let mut book = make_book();
1400        book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1401        let qty = Quantity::new(dec!(10)).unwrap();
1402        assert!(book.price_impact(Side::Bid, qty).is_none());
1403    }
1404
1405    #[test]
1406    fn test_orderbook_price_impact_zero_qty_returns_none() {
1407        let mut book = make_book();
1408        book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1409        let qty = Quantity::zero();
1410        assert!(book.price_impact(Side::Bid, qty).is_none());
1411    }
1412
1413    #[test]
1414    fn test_orderbook_depth_at_existing_bid_level() {
1415        let mut book = make_book();
1416        // make_book sets seq=0; add a bid at 99 qty=5 with seq=1
1417        book.apply_delta(set_delta(Side::Bid, "99", "5", 1)).unwrap();
1418        let price = Price::new(dec!(99)).unwrap();
1419        assert_eq!(book.depth_at(Side::Bid, price), Some(dec!(5)));
1420    }
1421
1422    #[test]
1423    fn test_orderbook_depth_at_absent_level_returns_none() {
1424        let book = make_book();
1425        let price = Price::new(dec!(50)).unwrap();
1426        assert!(book.depth_at(Side::Bid, price).is_none());
1427        assert!(book.depth_at(Side::Ask, price).is_none());
1428    }
1429
1430    #[test]
1431    fn test_orderbook_bid_depth_returns_top_n_descending() {
1432        let mut book = make_book();
1433        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1434        book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1435        book.apply_delta(set_delta(Side::Bid, "98", "3", 3)).unwrap();
1436        let levels = book.bid_depth(2);
1437        assert_eq!(levels.len(), 2);
1438        assert_eq!(levels[0].price.value(), dec!(100)); // best bid first
1439        assert_eq!(levels[1].price.value(), dec!(99));
1440    }
1441
1442    #[test]
1443    fn test_orderbook_ask_depth_returns_top_n_ascending() {
1444        let mut book = make_book();
1445        book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1446        book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1447        book.apply_delta(set_delta(Side::Ask, "103", "3", 3)).unwrap();
1448        let levels = book.ask_depth(2);
1449        assert_eq!(levels.len(), 2);
1450        assert_eq!(levels[0].price.value(), dec!(101)); // best ask first
1451        assert_eq!(levels[1].price.value(), dec!(102));
1452    }
1453
1454    #[test]
1455    fn test_orderbook_bid_depth_fewer_than_n() {
1456        let mut book = make_book();
1457        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1458        let levels = book.bid_depth(5);
1459        assert_eq!(levels.len(), 1);
1460    }
1461
1462    #[test]
1463    fn test_orderbook_ask_depth_empty_book() {
1464        let book = make_book();
1465        assert!(book.ask_depth(3).is_empty());
1466    }
1467
1468    #[test]
1469    fn test_orderbook_remove_all_bids_clears_bid_side() {
1470        let mut book = make_book();
1471        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1472        book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1473        book.remove_all(Side::Bid);
1474        assert!(book.best_bid().is_none());
1475    }
1476
1477    #[test]
1478    fn test_orderbook_remove_all_bids_leaves_asks_intact() {
1479        let mut book = make_book();
1480        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1481        book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1482        book.remove_all(Side::Bid);
1483        assert!(book.best_bid().is_none());
1484        assert!(book.best_ask().is_some());
1485    }
1486
1487    #[test]
1488    fn test_orderbook_remove_all_asks_clears_ask_side() {
1489        let mut book = make_book();
1490        book.apply_delta(set_delta(Side::Ask, "101", "5", 1)).unwrap();
1491        book.apply_delta(set_delta(Side::Ask, "102", "3", 2)).unwrap();
1492        book.remove_all(Side::Ask);
1493        assert!(book.best_ask().is_none());
1494    }
1495
1496    #[test]
1497    fn test_orderbook_total_levels_sums_both_sides() {
1498        let mut book = make_book();
1499        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1500        book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1501        book.apply_delta(set_delta(Side::Ask, "101", "8", 3)).unwrap();
1502        assert_eq!(book.total_levels(), 3);
1503    }
1504
1505    #[test]
1506    fn test_orderbook_total_levels_empty_book() {
1507        let book = make_book();
1508        assert_eq!(book.total_levels(), 0);
1509    }
1510}