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    ///
508    /// Alias for [`weighted_mid`](Self::weighted_mid).
509    #[deprecated(since = "2.1.0", note = "Use `weighted_mid` instead")]
510    pub fn weighted_mid_price(&self) -> Option<Decimal> {
511        self.weighted_mid()
512    }
513
514    /// Returns all price levels on `side` whose price falls within `[lo, hi]` (inclusive).
515    ///
516    /// Useful for computing the available liquidity within a price band.
517    pub fn price_levels_between(&self, side: Side, lo: Price, hi: Price) -> Vec<PriceLevel> {
518        let lo_val = lo.value();
519        let hi_val = hi.value();
520        match side {
521            Side::Bid => self
522                .bids
523                .range(lo_val..=hi_val)
524                .map(|(p, q)| PriceLevel {
525                    price: Price::new(*p).unwrap_or(lo),
526                    quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
527                })
528                .collect(),
529            Side::Ask => self
530                .asks
531                .range(lo_val..=hi_val)
532                .map(|(p, q)| PriceLevel {
533                    price: Price::new(*p).unwrap_or(lo),
534                    quantity: crate::types::Quantity::new(*q).unwrap_or_else(|_| crate::types::Quantity::zero()),
535                })
536                .collect(),
537        }
538    }
539
540    /// Returns the smallest price increment between adjacent levels on either side.
541    ///
542    /// Useful for estimating the instrument's native tick size from live book data.
543    /// Returns `None` when both sides have fewer than 2 levels.
544    pub fn tick_size(&self) -> Option<Decimal> {
545        let bid_tick = self
546            .bids
547            .keys()
548            .collect::<Vec<_>>()
549            .windows(2)
550            .map(|w| (*w[1] - *w[0]).abs())
551            .filter(|d| !d.is_zero())
552            .reduce(Decimal::min);
553        let ask_tick = self
554            .asks
555            .keys()
556            .collect::<Vec<_>>()
557            .windows(2)
558            .map(|w| (*w[1] - *w[0]).abs())
559            .filter(|d| !d.is_zero())
560            .reduce(Decimal::min);
561        match (bid_tick, ask_tick) {
562            (Some(b), Some(a)) => Some(b.min(a)),
563            (Some(b), None) => Some(b),
564            (None, Some(a)) => Some(a),
565            (None, None) => None,
566        }
567    }
568
569    /// Returns the bid-to-ask volume ratio: `total_bid_volume / total_ask_volume`.
570    ///
571    /// Values > 1 indicate more buy-side depth; values < 1 indicate more sell-side depth.
572    /// Returns `None` if either side is empty (to avoid division by zero).
573    pub fn bid_ask_ratio(&self) -> Option<Decimal> {
574        let bid = self.total_bid_volume();
575        let ask = self.total_ask_volume();
576        if ask.is_zero() || bid.is_zero() {
577            return None;
578        }
579        Some(bid / ask)
580    }
581
582    /// Estimates the average fill price for a market order of `qty` on `side`.
583    ///
584    /// Walks the book levels in price-time priority and returns the volume-weighted
585    /// average price. Returns `None` if `qty` is zero or the book cannot fill `qty`
586    /// in full (insufficient depth).
587    pub fn price_impact(&self, side: crate::types::Side, qty: crate::types::Quantity) -> Option<Decimal> {
588        use crate::types::Side;
589        if qty.is_zero() {
590            return None;
591        }
592        let levels: Vec<_> = match side {
593            Side::Bid => {
594                // Buying: walk asks from lowest to highest price
595                let mut asks: Vec<_> = self.asks.iter().collect();
596                asks.sort_by(|a, b| a.0.cmp(b.0));
597                asks.into_iter().map(|(p, q)| (*p, *q)).collect()
598            }
599            Side::Ask => {
600                // Selling: walk bids from highest to lowest price
601                let mut bids: Vec<_> = self.bids.iter().collect();
602                bids.sort_by(|a, b| b.0.cmp(a.0));
603                bids.into_iter().map(|(p, q)| (*p, *q)).collect()
604            }
605        };
606        let target = qty.value();
607        let mut remaining = target;
608        let mut notional = Decimal::ZERO;
609        for (price, level_qty) in levels {
610            let fill = level_qty.min(remaining);
611            notional += price * fill;
612            remaining -= fill;
613            if remaining <= Decimal::ZERO {
614                break;
615            }
616        }
617        if remaining > Decimal::ZERO {
618            None // insufficient depth
619        } else {
620            Some(notional / target)
621        }
622    }
623
624    /// Returns the top `n` bid levels in descending price order (best bid first).
625    ///
626    /// Returns fewer than `n` levels if the bid side has fewer entries.
627    pub fn bid_depth(&self, n: usize) -> Vec<PriceLevel> {
628        self.bids
629            .iter()
630            .rev()
631            .take(n)
632            .map(|(price, qty)| PriceLevel {
633                price: Price::new(*price).unwrap(),
634                quantity: Quantity::new(*qty).unwrap(),
635            })
636            .collect()
637    }
638
639    /// Returns the top `n` ask levels in ascending price order (best ask first).
640    ///
641    /// Returns fewer than `n` levels if the ask side has fewer entries.
642    pub fn ask_depth(&self, n: usize) -> Vec<PriceLevel> {
643        self.asks
644            .iter()
645            .take(n)
646            .map(|(price, qty)| PriceLevel {
647                price: Price::new(*price).unwrap(),
648                quantity: Quantity::new(*qty).unwrap(),
649            })
650            .collect()
651    }
652
653    /// Returns the depth imbalance ratio: `(bid_qty - ask_qty) / (bid_qty + ask_qty)`.
654    ///
655    /// Result is in `[-1.0, 1.0]`:
656    /// - Positive → more bid-side depth (buying pressure)
657    /// - Negative → more ask-side depth (selling pressure)
658    /// - `None` when both sides are empty (total depth is zero)
659    pub fn depth_imbalance(&self) -> Option<Decimal> {
660        let bid_qty: Decimal = self.bids.values().sum();
661        let ask_qty: Decimal = self.asks.values().sum();
662        let total = bid_qty + ask_qty;
663        if total.is_zero() {
664            return None;
665        }
666        Some((bid_qty - ask_qty) / total)
667    }
668
669    /// Returns the ask-to-bid quantity ratio: `total_ask_qty / total_bid_qty`.
670    ///
671    /// Values above 1 indicate more supply than demand at visible depth levels.
672    /// Returns `None` when total bid quantity is zero (avoid division by zero).
673    pub fn ask_bid_ratio(&self) -> Option<Decimal> {
674        let bid_qty: Decimal = self.bids.values().sum();
675        let ask_qty: Decimal = self.asks.values().sum();
676        if bid_qty.is_zero() {
677            return None;
678        }
679        Some(ask_qty / bid_qty)
680    }
681
682    /// Returns the total quantity across all bid price levels.
683    pub fn total_bid_depth(&self) -> Decimal {
684        self.bids.values().sum()
685    }
686
687    /// Returns the total quantity across all ask price levels.
688    pub fn total_ask_depth(&self) -> Decimal {
689        self.asks.values().sum()
690    }
691
692    /// Walks the book on `side` to find the price level reached after consuming `target_qty`.
693    ///
694    /// For `Side::Ask` walks ascending (cheapest ask first).
695    /// For `Side::Bid` walks descending (highest bid first).
696    ///
697    /// Returns the price of the level where `target_qty` is fully absorbed, or the last
698    /// available level if the book lacks sufficient depth.
699    /// Returns `None` when the side has no levels or `target_qty` is zero.
700    pub fn price_at_volume(&self, side: Side, target_qty: Decimal) -> Option<Price> {
701        if target_qty.is_zero() {
702            return None;
703        }
704        let mut remaining = target_qty;
705        let mut last_price: Option<Price> = None;
706
707        match side {
708            Side::Ask => {
709                for (&px, &qty) in &self.asks {
710                    last_price = Price::new(px).ok();
711                    if qty >= remaining {
712                        return last_price;
713                    }
714                    remaining -= qty;
715                }
716            }
717            Side::Bid => {
718                for (&px, &qty) in self.bids.iter().rev() {
719                    last_price = Price::new(px).ok();
720                    if qty >= remaining {
721                        return last_price;
722                    }
723                    remaining -= qty;
724                }
725            }
726        }
727        last_price
728    }
729
730    /// Returns up to `n` best bid levels in descending price order (best bid first).
731    ///
732    /// Returns an empty `Vec` when the bid side is empty or `n == 0`.
733    pub fn top_n_bid_levels(&self, n: usize) -> Vec<PriceLevel> {
734        if n == 0 {
735            return vec![];
736        }
737        self.bids
738            .iter()
739            .rev()
740            .take(n)
741            .filter_map(|(&px, &qty)| {
742                let price = Price::new(px).ok()?;
743                let quantity = Quantity::new(qty).ok()?;
744                Some(PriceLevel { price, quantity })
745            })
746            .collect()
747    }
748
749    /// Returns up to `n` best ask levels in ascending price order (best ask first).
750    ///
751    /// Returns an empty `Vec` when the ask side is empty or `n == 0`.
752    pub fn top_n_ask_levels(&self, n: usize) -> Vec<PriceLevel> {
753        if n == 0 {
754            return vec![];
755        }
756        self.asks
757            .iter()
758            .take(n)
759            .filter_map(|(&px, &qty)| {
760                let price = Price::new(px).ok()?;
761                let quantity = Quantity::new(qty).ok()?;
762                Some(PriceLevel { price, quantity })
763            })
764            .collect()
765    }
766
767    /// Returns the total quantity across the top `n` bid levels.
768    ///
769    /// Sweeps from the best (highest) bid downwards and sums quantities.
770    /// Returns zero when the bid side is empty or `n == 0`.
771    pub fn cumulative_bid_qty(&self, n: usize) -> Decimal {
772        if n == 0 {
773            return Decimal::ZERO;
774        }
775        self.bids.iter().rev().take(n).map(|(_, &qty)| qty).sum()
776    }
777
778    /// Returns the bid-to-ask depth skew across the top `n` levels on each side.
779    ///
780    /// `bid_depth_skew = cumulative_bid_qty(n) / (cumulative_bid_qty(n) + cumulative_ask_qty(n))`.
781    /// Range: 0.0 (all ask-side depth) to 1.0 (all bid-side depth).
782    /// Returns `None` if both sides are empty or `n == 0`.
783    pub fn bid_depth_skew(&self, n: usize) -> Option<Decimal> {
784        if n == 0 {
785            return None;
786        }
787        let bid_qty = self.cumulative_bid_qty(n);
788        let ask_qty = self.cumulative_ask_qty(n);
789        let total = bid_qty + ask_qty;
790        if total.is_zero() {
791            return None;
792        }
793        bid_qty.checked_div(total)
794    }
795
796    /// Returns the bid-ask spread in basis points.
797    ///
798    /// `spread_bps = (best_ask - best_bid) / mid_price * 10_000`.
799    /// Returns `None` if either side is empty or mid-price is zero.
800    pub fn spread_bps(&self) -> Option<Decimal> {
801        let bid = self.best_bid()?.price.value();
802        let ask = self.best_ask()?.price.value();
803        let mid = (bid + ask) / Decimal::TWO;
804        if mid.is_zero() {
805            return None;
806        }
807        let spread = ask - bid;
808        spread.checked_div(mid).map(|r| r * Decimal::from(10_000u32))
809    }
810
811    /// Returns the total quantity across the top `n` ask levels.
812    ///
813    /// Sweeps from the best (lowest) ask upwards and sums quantities.
814    /// Returns zero when the ask side is empty or `n == 0`.
815    pub fn cumulative_ask_qty(&self, n: usize) -> Decimal {
816        if n == 0 {
817            return Decimal::ZERO;
818        }
819        self.asks.iter().take(n).map(|(_, &qty)| qty).sum()
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826    use rust_decimal_macros::dec;
827
828    fn make_book() -> OrderBook {
829        OrderBook::new(Symbol::new("AAPL").unwrap())
830    }
831
832    fn set_delta(side: Side, price: &str, qty: &str, seq: u64) -> BookDelta {
833        BookDelta {
834            side,
835            price: Price::new(price.parse().unwrap()).unwrap(),
836            quantity: Quantity::new(qty.parse().unwrap()).unwrap(),
837            action: DeltaAction::Set,
838            sequence: seq,
839        }
840    }
841
842    fn remove_delta(side: Side, price: &str, seq: u64) -> BookDelta {
843        BookDelta {
844            side,
845            price: Price::new(price.parse().unwrap()).unwrap(),
846            quantity: Quantity::zero(),
847            action: DeltaAction::Remove,
848            sequence: seq,
849        }
850    }
851
852    #[test]
853    fn test_orderbook_apply_delta_updates_bid() {
854        let mut book = make_book();
855        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
856            .unwrap();
857        let best = book.best_bid().unwrap();
858        assert_eq!(best.price.value(), dec!(100));
859        assert_eq!(best.quantity.value(), dec!(10));
860    }
861
862    #[test]
863    fn test_orderbook_apply_delta_updates_ask() {
864        let mut book = make_book();
865        book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
866            .unwrap();
867        let best = book.best_ask().unwrap();
868        assert_eq!(best.price.value(), dec!(101));
869        assert_eq!(best.quantity.value(), dec!(5));
870    }
871
872    #[test]
873    fn test_orderbook_sequence_mismatch_returns_error() {
874        let mut book = make_book();
875        let result = book.apply_delta(set_delta(Side::Bid, "100", "10", 2));
876        assert!(matches!(
877            result,
878            Err(FinError::SequenceMismatch {
879                expected: 1,
880                got: 2
881            })
882        ));
883    }
884
885    #[test]
886    fn test_orderbook_sequence_advances_correctly() {
887        let mut book = make_book();
888        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
889            .unwrap();
890        assert_eq!(book.sequence(), 1);
891        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
892            .unwrap();
893        assert_eq!(book.sequence(), 2);
894    }
895
896    #[test]
897    fn test_orderbook_best_bid_max_price() {
898        let mut book = make_book();
899        book.apply_delta(set_delta(Side::Bid, "99", "10", 1))
900            .unwrap();
901        book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
902            .unwrap();
903        book.apply_delta(set_delta(Side::Bid, "98", "20", 3))
904            .unwrap();
905        let best = book.best_bid().unwrap();
906        assert_eq!(best.price.value(), dec!(100));
907    }
908
909    #[test]
910    fn test_orderbook_best_ask_min_price() {
911        let mut book = make_book();
912        book.apply_delta(set_delta(Side::Ask, "102", "10", 1))
913            .unwrap();
914        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
915            .unwrap();
916        book.apply_delta(set_delta(Side::Ask, "103", "20", 3))
917            .unwrap();
918        let best = book.best_ask().unwrap();
919        assert_eq!(best.price.value(), dec!(101));
920    }
921
922    #[test]
923    fn test_orderbook_spread_positive() {
924        let mut book = make_book();
925        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
926            .unwrap();
927        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
928            .unwrap();
929        let spread = book.spread().unwrap();
930        assert_eq!(spread, dec!(1));
931        assert!(spread > Decimal::ZERO);
932    }
933
934    #[test]
935    fn test_orderbook_mid_price() {
936        let mut book = make_book();
937        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
938            .unwrap();
939        book.apply_delta(set_delta(Side::Ask, "102", "5", 2))
940            .unwrap();
941        let mid = book.mid_price().unwrap();
942        assert_eq!(mid, dec!(101));
943    }
944
945    #[test]
946    fn test_orderbook_spread_none_when_empty() {
947        let book = make_book();
948        assert!(book.spread().is_none());
949    }
950
951    #[test]
952    fn test_orderbook_vwap_insufficient_liquidity() {
953        let mut book = make_book();
954        book.apply_delta(set_delta(Side::Ask, "101", "5", 1))
955            .unwrap();
956        let result = book.vwap_for_qty(Side::Ask, Quantity::new(dec!(100)).unwrap());
957        assert!(matches!(result, Err(FinError::InsufficientLiquidity(_))));
958    }
959
960    #[test]
961    fn test_orderbook_vwap_single_level() {
962        let mut book = make_book();
963        book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
964            .unwrap();
965        let vwap = book
966            .vwap_for_qty(Side::Ask, Quantity::new(dec!(5)).unwrap())
967            .unwrap();
968        assert_eq!(vwap, dec!(100));
969    }
970
971    #[test]
972    fn test_orderbook_vwap_multi_level() {
973        let mut book = make_book();
974        book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
975            .unwrap();
976        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
977            .unwrap();
978        // 5 @ 100 + 5 @ 101 = 1005 / 10 = 100.5
979        let vwap = book
980            .vwap_for_qty(Side::Ask, Quantity::new(dec!(10)).unwrap())
981            .unwrap();
982        assert_eq!(vwap, dec!(100.5));
983    }
984
985    #[test]
986    fn test_orderbook_remove_level_delta() {
987        let mut book = make_book();
988        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
989            .unwrap();
990        book.apply_delta(remove_delta(Side::Bid, "100", 2)).unwrap();
991        assert!(book.best_bid().is_none());
992    }
993
994    #[test]
995    fn test_orderbook_top_bids_order() {
996        let mut book = make_book();
997        book.apply_delta(set_delta(Side::Bid, "98", "10", 1))
998            .unwrap();
999        book.apply_delta(set_delta(Side::Bid, "100", "5", 2))
1000            .unwrap();
1001        book.apply_delta(set_delta(Side::Bid, "99", "20", 3))
1002            .unwrap();
1003        let top = book.top_bids(2);
1004        assert_eq!(top[0].price.value(), dec!(100));
1005        assert_eq!(top[1].price.value(), dec!(99));
1006    }
1007
1008    #[test]
1009    fn test_orderbook_top_asks_order() {
1010        let mut book = make_book();
1011        book.apply_delta(set_delta(Side::Ask, "103", "10", 1))
1012            .unwrap();
1013        book.apply_delta(set_delta(Side::Ask, "101", "5", 2))
1014            .unwrap();
1015        book.apply_delta(set_delta(Side::Ask, "102", "20", 3))
1016            .unwrap();
1017        let top = book.top_asks(2);
1018        assert_eq!(top[0].price.value(), dec!(101));
1019        assert_eq!(top[1].price.value(), dec!(102));
1020    }
1021
1022    #[test]
1023    fn test_orderbook_bid_count_ask_count() {
1024        let mut book = make_book();
1025        book.apply_delta(set_delta(Side::Bid, "100", "1", 1))
1026            .unwrap();
1027        book.apply_delta(set_delta(Side::Ask, "101", "1", 2))
1028            .unwrap();
1029        assert_eq!(book.bid_count(), 1);
1030        assert_eq!(book.ask_count(), 1);
1031    }
1032
1033    #[test]
1034    fn test_orderbook_vwap_zero_qty_returns_zero() {
1035        let mut book = make_book();
1036        book.apply_delta(set_delta(Side::Ask, "100", "10", 1))
1037            .unwrap();
1038        let vwap = book.vwap_for_qty(Side::Ask, Quantity::zero()).unwrap();
1039        assert_eq!(vwap, Decimal::ZERO);
1040    }
1041
1042    // ── Inverted spread guard ─────────────────────────────────────────────────
1043
1044    #[test]
1045    fn test_apply_delta_rejects_inverted_spread() {
1046        let mut book = make_book();
1047        // Set ask at 100
1048        book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1049            .unwrap();
1050        // Try to set bid at 101 (would cross the ask): must fail
1051        let result = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1052        assert!(
1053            matches!(result, Err(FinError::InvertedSpread { .. })),
1054            "expected InvertedSpread, got {:?}",
1055            result
1056        );
1057    }
1058
1059    #[test]
1060    fn test_apply_delta_inverted_spread_rolls_back_sequence() {
1061        let mut book = make_book();
1062        book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1063            .unwrap();
1064        assert_eq!(book.sequence(), 1);
1065        // This should fail and leave sequence unchanged
1066        let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1067        assert_eq!(
1068            book.sequence(),
1069            1,
1070            "sequence must not advance on rejected delta"
1071        );
1072    }
1073
1074    #[test]
1075    fn test_apply_delta_inverted_spread_rolled_back_book_state() {
1076        let mut book = make_book();
1077        book.apply_delta(set_delta(Side::Ask, "100", "5", 1))
1078            .unwrap();
1079        // Rejected bid at 101 must not persist in the book
1080        let _ = book.apply_delta(set_delta(Side::Bid, "101", "5", 2));
1081        assert!(
1082            book.best_bid().is_none(),
1083            "rejected bid must not appear in book"
1084        );
1085    }
1086
1087    /// Empty book mid_price returns None.
1088    #[test]
1089    fn test_empty_book_mid_price_returns_none() {
1090        let book = make_book();
1091        assert!(
1092            book.mid_price().is_none(),
1093            "empty book mid_price must be None"
1094        );
1095    }
1096
1097    /// Empty book best_bid returns None.
1098    #[test]
1099    fn test_empty_book_best_bid_returns_none() {
1100        let book = make_book();
1101        assert!(book.best_bid().is_none());
1102    }
1103
1104    /// Empty book best_ask returns None.
1105    #[test]
1106    fn test_empty_book_best_ask_returns_none() {
1107        let book = make_book();
1108        assert!(book.best_ask().is_none());
1109    }
1110
1111    /// Best bid/ask after many inserts and removes reflects only surviving levels.
1112    #[test]
1113    fn test_best_bid_after_many_inserts_and_removes() {
1114        let mut book = make_book();
1115        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
1116            .unwrap();
1117        book.apply_delta(set_delta(Side::Bid, "105", "5", 2))
1118            .unwrap();
1119        book.apply_delta(set_delta(Side::Bid, "103", "8", 3))
1120            .unwrap();
1121        // Remove 105 (was best bid)
1122        book.apply_delta(remove_delta(Side::Bid, "105", 4)).unwrap();
1123        let best = book.best_bid().unwrap();
1124        assert_eq!(
1125            best.price.value(),
1126            dec!(103),
1127            "best bid after removing top level must be 103"
1128        );
1129    }
1130
1131    #[test]
1132    fn test_best_ask_after_many_inserts_and_removes() {
1133        let mut book = make_book();
1134        book.apply_delta(set_delta(Side::Ask, "110", "10", 1))
1135            .unwrap();
1136        book.apply_delta(set_delta(Side::Ask, "108", "5", 2))
1137            .unwrap();
1138        book.apply_delta(set_delta(Side::Ask, "109", "8", 3))
1139            .unwrap();
1140        // Remove 108 (was best ask)
1141        book.apply_delta(remove_delta(Side::Ask, "108", 4)).unwrap();
1142        let best = book.best_ask().unwrap();
1143        assert_eq!(
1144            best.price.value(),
1145            dec!(109),
1146            "best ask after removing top level must be 109"
1147        );
1148    }
1149
1150    /// Crossed book detection: ask <= bid must return InvertedSpread.
1151    #[test]
1152    fn test_crossed_book_ask_at_bid_price_rejected() {
1153        let mut book = make_book();
1154        book.apply_delta(set_delta(Side::Bid, "100", "10", 1))
1155            .unwrap();
1156        let result = book.apply_delta(set_delta(Side::Ask, "100", "5", 2));
1157        assert!(
1158            matches!(result, Err(FinError::InvertedSpread { .. })),
1159            "ask at bid price must produce InvertedSpread"
1160        );
1161    }
1162
1163    /// Empty book spread returns None.
1164    #[test]
1165    fn test_empty_book_spread_returns_none() {
1166        let book = make_book();
1167        assert!(book.spread().is_none());
1168    }
1169
1170    #[test]
1171    fn test_orderbook_snapshot_returns_top_n_both_sides() {
1172        let mut book = make_book();
1173        book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
1174        book.apply_delta(set_delta(Side::Bid, "100", "5", 2)).unwrap();
1175        book.apply_delta(set_delta(Side::Ask, "101", "3", 3)).unwrap();
1176        book.apply_delta(set_delta(Side::Ask, "102", "7", 4)).unwrap();
1177        let (bids, asks) = book.snapshot(2);
1178        assert_eq!(bids.len(), 2);
1179        assert_eq!(asks.len(), 2);
1180        assert_eq!(bids[0].price.value(), dec!(100));
1181        assert_eq!(asks[0].price.value(), dec!(101));
1182    }
1183
1184    #[test]
1185    fn test_orderbook_snapshot_empty_book() {
1186        let book = make_book();
1187        let (bids, asks) = book.snapshot(5);
1188        assert!(bids.is_empty());
1189        assert!(asks.is_empty());
1190    }
1191
1192    #[test]
1193    fn test_orderbook_clear_removes_all_levels() {
1194        let mut book = make_book();
1195        book.apply_delta(set_delta(Side::Bid, "99", "10", 1)).unwrap();
1196        book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1197        assert_eq!(book.bid_count(), 1);
1198        assert_eq!(book.ask_count(), 1);
1199        book.clear();
1200        assert_eq!(book.bid_count(), 0);
1201        assert_eq!(book.ask_count(), 0);
1202        assert_eq!(book.sequence(), 0);
1203    }
1204
1205    #[test]
1206    fn test_orderbook_clear_allows_fresh_deltas() {
1207        let mut book = make_book();
1208        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1209        book.clear();
1210        // After clear, sequence resets to 0, so next delta must be seq=1
1211        assert!(book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).is_ok());
1212    }
1213
1214    #[test]
1215    fn test_orderbook_total_bid_volume() {
1216        let mut book = make_book();
1217        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1218        book.apply_delta(set_delta(Side::Bid, "99", "3", 2)).unwrap();
1219        assert_eq!(book.total_bid_volume(), dec!(8));
1220    }
1221
1222    #[test]
1223    fn test_orderbook_total_ask_volume() {
1224        let mut book = make_book();
1225        book.apply_delta(set_delta(Side::Ask, "101", "4", 1)).unwrap();
1226        book.apply_delta(set_delta(Side::Ask, "102", "6", 2)).unwrap();
1227        assert_eq!(book.total_ask_volume(), dec!(10));
1228    }
1229
1230    #[test]
1231    fn test_orderbook_total_bid_volume_empty() {
1232        let book = make_book();
1233        assert_eq!(book.total_bid_volume(), dec!(0));
1234    }
1235
1236    #[test]
1237    fn test_orderbook_imbalance_balanced() {
1238        let mut book = make_book();
1239        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1240        book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1241        assert_eq!(book.imbalance().unwrap(), dec!(0));
1242    }
1243
1244    #[test]
1245    fn test_orderbook_imbalance_bid_heavy() {
1246        let mut book = make_book();
1247        book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
1248        book.apply_delta(set_delta(Side::Ask, "101", "1", 2)).unwrap();
1249        // (9 - 1) / 10 = 0.8
1250        assert_eq!(book.imbalance().unwrap(), dec!(0.8));
1251    }
1252
1253    #[test]
1254    fn test_orderbook_imbalance_ask_heavy() {
1255        let mut book = make_book();
1256        book.apply_delta(set_delta(Side::Bid, "100", "1", 1)).unwrap();
1257        book.apply_delta(set_delta(Side::Ask, "101", "9", 2)).unwrap();
1258        // (1 - 9) / 10 = -0.8
1259        assert_eq!(book.imbalance().unwrap(), dec!(-0.8));
1260    }
1261
1262    #[test]
1263    fn test_orderbook_imbalance_empty_returns_none() {
1264        let book = make_book();
1265        assert!(book.imbalance().is_none());
1266    }
1267
1268    #[test]
1269    fn test_orderbook_has_price_bid_present() {
1270        let mut book = make_book();
1271        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1272        let price = Price::new(dec!(100)).unwrap();
1273        assert!(book.has_price(Side::Bid, price));
1274        assert!(!book.has_price(Side::Ask, price));
1275    }
1276
1277    #[test]
1278    fn test_orderbook_has_price_ask_present() {
1279        let mut book = make_book();
1280        book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1281        let price = Price::new(dec!(101)).unwrap();
1282        assert!(book.has_price(Side::Ask, price));
1283        assert!(!book.has_price(Side::Bid, price));
1284    }
1285
1286    #[test]
1287    fn test_orderbook_has_price_absent() {
1288        let book = make_book();
1289        let price = Price::new(dec!(100)).unwrap();
1290        assert!(!book.has_price(Side::Bid, price));
1291        assert!(!book.has_price(Side::Ask, price));
1292    }
1293
1294    #[test]
1295    fn test_orderbook_has_price_false_after_remove() {
1296        let mut book = make_book();
1297        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1298        book.apply_delta(BookDelta {
1299            side: Side::Bid,
1300            price: Price::new(dec!(100)).unwrap(),
1301            quantity: Quantity::zero(),
1302            action: DeltaAction::Remove,
1303            sequence: 2,
1304        })
1305        .unwrap();
1306        let price = Price::new(dec!(100)).unwrap();
1307        assert!(!book.has_price(Side::Bid, price));
1308    }
1309
1310    #[test]
1311    fn test_orderbook_level_count_bids() {
1312        let mut book = make_book();
1313        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1314        book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1315        assert_eq!(book.level_count(Side::Bid), 2);
1316        assert_eq!(book.level_count(Side::Ask), 0);
1317    }
1318
1319    #[test]
1320    fn test_orderbook_level_count_asks() {
1321        let mut book = make_book();
1322        book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1323        assert_eq!(book.level_count(Side::Ask), 1);
1324        assert_eq!(book.level_count(Side::Bid), 0);
1325    }
1326
1327    #[test]
1328    fn test_orderbook_weighted_mid_equal_qty() {
1329        let mut book = make_book();
1330        book.apply_delta(set_delta(Side::Bid, "100", "5", 1)).unwrap();
1331        book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1332        // Equal qty → simple midpoint
1333        assert_eq!(book.weighted_mid().unwrap(), dec!(101));
1334    }
1335
1336    #[test]
1337    fn test_orderbook_weighted_mid_bid_heavy() {
1338        let mut book = make_book();
1339        book.apply_delta(set_delta(Side::Bid, "100", "9", 1)).unwrap();
1340        book.apply_delta(set_delta(Side::Ask, "110", "1", 2)).unwrap();
1341        // (100*1 + 110*9) / (9+1) = (100 + 990) / 10 = 109
1342        assert_eq!(book.weighted_mid().unwrap(), dec!(109));
1343    }
1344
1345    #[test]
1346    fn test_orderbook_weighted_mid_empty_returns_none() {
1347        let book = make_book();
1348        assert!(book.weighted_mid().is_none());
1349    }
1350
1351    #[test]
1352    fn test_orderbook_bid_ask_ratio_equal_volumes() {
1353        let mut book = make_book();
1354        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1355        book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
1356        assert_eq!(book.bid_ask_ratio().unwrap(), dec!(1));
1357    }
1358
1359    #[test]
1360    fn test_orderbook_bid_ask_ratio_bid_heavy() {
1361        let mut book = make_book();
1362        book.apply_delta(set_delta(Side::Bid, "100", "20", 1)).unwrap();
1363        book.apply_delta(set_delta(Side::Ask, "101", "10", 2)).unwrap();
1364        assert_eq!(book.bid_ask_ratio().unwrap(), dec!(2));
1365    }
1366
1367    #[test]
1368    fn test_orderbook_bid_ask_ratio_empty_returns_none() {
1369        let book = make_book();
1370        assert!(book.bid_ask_ratio().is_none());
1371    }
1372
1373    #[test]
1374    fn test_orderbook_price_impact_buy_single_level() {
1375        let mut book = make_book();
1376        book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1377        let qty = Quantity::new(dec!(5)).unwrap();
1378        let avg = book.price_impact(Side::Bid, qty).unwrap();
1379        assert_eq!(avg, dec!(101));
1380    }
1381
1382    #[test]
1383    fn test_orderbook_price_impact_buy_spans_two_levels() {
1384        let mut book = make_book();
1385        book.apply_delta(set_delta(Side::Ask, "100", "5", 1)).unwrap();
1386        book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1387        // 5 @ 100 + 5 @ 102 = 1010 / 10 = 101
1388        let qty = Quantity::new(dec!(10)).unwrap();
1389        let avg = book.price_impact(Side::Bid, qty).unwrap();
1390        assert_eq!(avg, dec!(101));
1391    }
1392
1393    #[test]
1394    fn test_orderbook_price_impact_insufficient_depth_returns_none() {
1395        let mut book = make_book();
1396        book.apply_delta(set_delta(Side::Ask, "101", "3", 1)).unwrap();
1397        let qty = Quantity::new(dec!(10)).unwrap();
1398        assert!(book.price_impact(Side::Bid, qty).is_none());
1399    }
1400
1401    #[test]
1402    fn test_orderbook_price_impact_zero_qty_returns_none() {
1403        let mut book = make_book();
1404        book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1405        let qty = Quantity::zero();
1406        assert!(book.price_impact(Side::Bid, qty).is_none());
1407    }
1408
1409    #[test]
1410    fn test_orderbook_depth_at_existing_bid_level() {
1411        let mut book = make_book();
1412        // make_book sets seq=0; add a bid at 99 qty=5 with seq=1
1413        book.apply_delta(set_delta(Side::Bid, "99", "5", 1)).unwrap();
1414        let price = Price::new(dec!(99)).unwrap();
1415        assert_eq!(book.depth_at(Side::Bid, price), Some(dec!(5)));
1416    }
1417
1418    #[test]
1419    fn test_orderbook_depth_at_absent_level_returns_none() {
1420        let book = make_book();
1421        let price = Price::new(dec!(50)).unwrap();
1422        assert!(book.depth_at(Side::Bid, price).is_none());
1423        assert!(book.depth_at(Side::Ask, price).is_none());
1424    }
1425
1426    #[test]
1427    fn test_orderbook_bid_depth_returns_top_n_descending() {
1428        let mut book = make_book();
1429        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1430        book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1431        book.apply_delta(set_delta(Side::Bid, "98", "3", 3)).unwrap();
1432        let levels = book.bid_depth(2);
1433        assert_eq!(levels.len(), 2);
1434        assert_eq!(levels[0].price.value(), dec!(100)); // best bid first
1435        assert_eq!(levels[1].price.value(), dec!(99));
1436    }
1437
1438    #[test]
1439    fn test_orderbook_ask_depth_returns_top_n_ascending() {
1440        let mut book = make_book();
1441        book.apply_delta(set_delta(Side::Ask, "101", "10", 1)).unwrap();
1442        book.apply_delta(set_delta(Side::Ask, "102", "5", 2)).unwrap();
1443        book.apply_delta(set_delta(Side::Ask, "103", "3", 3)).unwrap();
1444        let levels = book.ask_depth(2);
1445        assert_eq!(levels.len(), 2);
1446        assert_eq!(levels[0].price.value(), dec!(101)); // best ask first
1447        assert_eq!(levels[1].price.value(), dec!(102));
1448    }
1449
1450    #[test]
1451    fn test_orderbook_bid_depth_fewer_than_n() {
1452        let mut book = make_book();
1453        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1454        let levels = book.bid_depth(5);
1455        assert_eq!(levels.len(), 1);
1456    }
1457
1458    #[test]
1459    fn test_orderbook_ask_depth_empty_book() {
1460        let book = make_book();
1461        assert!(book.ask_depth(3).is_empty());
1462    }
1463
1464    #[test]
1465    fn test_orderbook_remove_all_bids_clears_bid_side() {
1466        let mut book = make_book();
1467        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1468        book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1469        book.remove_all(Side::Bid);
1470        assert!(book.best_bid().is_none());
1471    }
1472
1473    #[test]
1474    fn test_orderbook_remove_all_bids_leaves_asks_intact() {
1475        let mut book = make_book();
1476        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1477        book.apply_delta(set_delta(Side::Ask, "101", "5", 2)).unwrap();
1478        book.remove_all(Side::Bid);
1479        assert!(book.best_bid().is_none());
1480        assert!(book.best_ask().is_some());
1481    }
1482
1483    #[test]
1484    fn test_orderbook_remove_all_asks_clears_ask_side() {
1485        let mut book = make_book();
1486        book.apply_delta(set_delta(Side::Ask, "101", "5", 1)).unwrap();
1487        book.apply_delta(set_delta(Side::Ask, "102", "3", 2)).unwrap();
1488        book.remove_all(Side::Ask);
1489        assert!(book.best_ask().is_none());
1490    }
1491
1492    #[test]
1493    fn test_orderbook_total_levels_sums_both_sides() {
1494        let mut book = make_book();
1495        book.apply_delta(set_delta(Side::Bid, "100", "10", 1)).unwrap();
1496        book.apply_delta(set_delta(Side::Bid, "99", "5", 2)).unwrap();
1497        book.apply_delta(set_delta(Side::Ask, "101", "8", 3)).unwrap();
1498        assert_eq!(book.total_levels(), 3);
1499    }
1500
1501    #[test]
1502    fn test_orderbook_total_levels_empty_book() {
1503        let book = make_book();
1504        assert_eq!(book.total_levels(), 0);
1505    }
1506}