Skip to main content

fin_primitives/orderbook/
mod.rs

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