Skip to main content

troy/market/
ob.rs

1use super::PriceAmount;
2use crate::Dec;
3
4const DEFAULT_CAPACITY: usize = 64;
5
6/// One side of a level 2 book.
7///
8/// Levels are kept best first — descending for bids, ascending for asks — so
9/// the best level, the n-th level and the depth cut are all index arithmetic.
10#[derive(Clone, Debug)]
11pub struct OrderBookSide {
12    // A vector rather than a deque, measured rather than assumed. A deque
13    // inserts near the front without shifting, which is where a busy venue
14    // adds, and `book_insert` in the benchmarks is 34% faster on one at 512
15    // levels. It buys nothing at the depths a book is actually capped to - one
16    // nanosecond at 32, less than nothing at 8 - and it charges for the ring
17    // arithmetic on every read: 5% on `best_price`, which is the hottest and
18    // cheapest call here, and 4% on an update. A compact book does far more of
19    // those than it does inserts.
20    levels: Vec<PriceAmount>,
21    desc: bool,
22    max_depth: Option<usize>,
23}
24
25/// A level 2 order book.
26///
27/// Designed to be efficient for high-frequency trading scenarios.
28#[derive(Clone, Debug)]
29pub struct OrderBook {
30    /// The bids side of the order book.
31    pub bids: OrderBookSide,
32    /// The asks side of the order book.
33    pub asks: OrderBookSide,
34}
35
36/// The best bid and ask: the smallest snapshot of a book that still prices a
37/// trade, and what a top-of-book feed carries.
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39pub struct OrderBookTop {
40    /// The best bid.
41    pub bid: PriceAmount,
42    /// The best ask.
43    pub ask: PriceAmount,
44}
45
46/// A batch of level updates to apply to a book.
47#[derive(Clone, Debug, Default)]
48pub struct OrderBookDiff {
49    /// Bid levels to set.
50    pub bids: Vec<PriceAmount>,
51    /// Ask levels to set.
52    pub asks: Vec<PriceAmount>,
53}
54
55impl OrderBookSide {
56    fn bids(max_depth: Option<usize>) -> Self {
57        Self::new(true, max_depth)
58    }
59
60    fn asks(max_depth: Option<usize>) -> Self {
61        Self::new(false, max_depth)
62    }
63
64    fn new(desc: bool, max_depth: Option<usize>) -> Self {
65        // max_depth is a ceiling, not a forecast: a side capped at ten
66        // thousand levels usually holds a handful, so reserving the cap would
67        // allocate for a book that never arrives — and `usize::MAX`, which is
68        // a legal cap meaning "no limit", aborts the process outright
69        let capacity = max_depth.unwrap_or(DEFAULT_CAPACITY).min(DEFAULT_CAPACITY);
70        Self {
71            levels: Vec::with_capacity(capacity),
72            desc,
73            max_depth,
74        }
75    }
76
77    /// The number of levels held.
78    pub fn len(&self) -> usize {
79        self.levels.len()
80    }
81
82    /// Whether the side holds no levels.
83    pub fn is_empty(&self) -> bool {
84        self.levels.is_empty()
85    }
86
87    /// The level cap this side was built with, or `None` when uncapped.
88    pub fn max_depth(&self) -> Option<usize> {
89        self.max_depth
90    }
91
92    /// Iterate every level, best first.
93    pub fn iter(&self) -> std::slice::Iter<'_, PriceAmount> {
94        self.levels.iter()
95    }
96
97    /// The level at exactly `price`, if the side holds one.
98    ///
99    /// Keyed by price, where [`OrderBookSide::at`] is keyed by depth.
100    pub fn find(&self, price: Dec) -> Option<&PriceAmount> {
101        self.search(price).ok().map(|index| &self.levels[index])
102    }
103
104    /// The best level: highest bid, lowest ask.
105    pub fn best(&self) -> Option<&PriceAmount> {
106        self.levels.first()
107    }
108
109    /// The price of the best level.
110    pub fn best_price(&self) -> Option<Dec> {
111        self.best().map(|level| level.price)
112    }
113
114    /// The worst level held, which is the deepest one, not the worst possible.
115    pub fn worst(&self) -> Option<&PriceAmount> {
116        self.levels.last()
117    }
118
119    /// The price of the worst level held.
120    pub fn worst_price(&self) -> Option<Dec> {
121        self.worst().map(|level| level.price)
122    }
123
124    /// The n-th level from the best, counting from zero.
125    pub fn at(&self, n: usize) -> Option<&PriceAmount> {
126        self.levels.get(n)
127    }
128
129    /// The price of the n-th level from the best.
130    pub fn price_at(&self, n: usize) -> Option<Dec> {
131        self.at(n).map(|level| level.price)
132    }
133
134    /// Set a level, returning the amount it replaced. A zero amount removes it.
135    ///
136    /// A level failing [`PriceAmount::is_valid`] is rejected and the book left
137    /// untouched. `None` therefore covers four cases: the level was inserted,
138    /// it was rejected as invalid, it was refused for sitting past
139    /// `max_depth`, or a zero amount asked to remove a price that was absent.
140    pub fn set(&mut self, entry: PriceAmount) -> Option<Dec> {
141        if !entry.is_valid() {
142            return None;
143        }
144        match self.search(entry.price) {
145            Ok(index) => {
146                let previous = self.levels[index].amount;
147                if entry.amount.is_zero() {
148                    self.levels.remove(index);
149                } else {
150                    self.levels[index].amount = entry.amount;
151                }
152                Some(previous)
153            }
154            Err(index) => {
155                if entry.amount.is_zero() || self.max_depth.is_some_and(|depth| index >= depth) {
156                    return None;
157                }
158                self.levels.insert(index, entry);
159                if let Some(depth) = self.max_depth {
160                    self.trim(depth);
161                }
162                None
163            }
164        }
165    }
166
167    /// [`OrderBookSide::set`] taking the price and amount separately.
168    pub fn set_price_amount(&mut self, price: Dec, amount: Dec) -> Option<Dec> {
169        self.set(PriceAmount { price, amount })
170    }
171
172    /// Retain at most `depth` levels, dropping the worst ones beyond that.
173    pub fn trim(&mut self, depth: usize) {
174        self.levels.truncate(depth);
175    }
176
177    /// Iterate levels within `[low, high]` in best-first order.
178    ///
179    /// An empty iterator when no level falls inside, `low` above `high`
180    /// included.
181    pub fn range(&self, low: Dec, high: Dec) -> std::slice::Iter<'_, PriceAmount> {
182        let (start, end) = match self.desc {
183            true => (
184                self.levels.partition_point(|level| level.price > high),
185                self.levels.partition_point(|level| level.price >= low),
186            ),
187            false => (
188                self.levels.partition_point(|level| level.price < low),
189                self.levels.partition_point(|level| level.price <= high),
190            ),
191        };
192        // the two partition points cross rather than meet when `low` sits
193        // above `high`, and the slice would panic on a backwards range
194        self.levels[start..end.max(start)].iter()
195    }
196
197    /// Get the volume up to a given level in the orderbook side
198    pub fn volume_at(&self, level: usize) -> Option<Dec> {
199        self.levels
200            .iter()
201            .take(level)
202            .map(|entry| entry.amount)
203            .reduce(|acc, x| acc + x)
204    }
205
206    /// Total amount over the levels in `from..to`, best first, or `None` when
207    /// the range holds none.
208    ///
209    /// `to` past the end takes what is there. A `to` at or below `from` is an
210    /// empty range and answers `None`: the take yields fewer levels than the
211    /// skip discards, so nothing is summed, which is the same answer an empty
212    /// side gives and the right one for a band with no levels in it.
213    fn volume_in(&self, from: usize, to: usize) -> Option<Dec> {
214        self.levels
215            .iter()
216            .take(to)
217            .skip(from)
218            .map(|entry| entry.amount)
219            .reduce(|total, amount| total + amount)
220    }
221
222    /// Volume weighted price paid to fill `quantity`, or `None` if the side is
223    /// too thin.
224    pub fn price_for_quantity(&self, quantity: f64) -> Option<f64> {
225        if quantity <= 0.0 {
226            return None;
227        }
228        let mut accumulated = 0.0;
229        let mut notional = 0.0;
230        for entry in self.levels.iter() {
231            let amount = (quantity - accumulated).min(entry.amount.to_f64());
232            notional += amount * entry.price.to_f64();
233            accumulated += amount;
234            if accumulated >= quantity {
235                return Some(notional / accumulated);
236            }
237        }
238        None
239    }
240
241    /// Mean level amount, or `None` when the side is empty.
242    ///
243    /// Summed on demand rather than kept as a running total. A running one
244    /// would have to be corrected on every `set`, which is the hot path, to
245    /// save an addition per level here, which is not; and one non-finite amount
246    /// would poison it for good, since the subtraction that removes the level
247    /// cannot take a NaN back out again.
248    pub fn amount_mean(&self) -> Option<f64> {
249        match self.levels.len() {
250            0 => None,
251            count => Some(self.total_amount().to_f64() / count as f64),
252        }
253    }
254
255    /// The sum of every level amount, exactly. [`Dec::ZERO`] for an empty side.
256    fn total_amount(&self) -> Dec {
257        self.levels.iter().map(|level| level.amount).sum()
258    }
259
260    /// Population standard deviation of the level amounts, or `None` when the
261    /// side is empty.
262    ///
263    /// Computed from the levels rather than from a running sum of squares. A
264    /// running one drifts: adding and removing a level far larger than the
265    /// rest cancels badly enough to report no dispersion at all on a book that
266    /// has some, and the clamp that hid it turned corruption into a plausible
267    /// number. The mean comes from the exact `Dec` total, so only the
268    /// deviations are floating point, and their squares cannot go negative.
269    ///
270    /// Two passes, then: the mean has to be known before a deviation can be
271    /// measured against it, which is the same reason the one-pass form is the
272    /// one that drifts.
273    pub fn amount_std_dev(&self) -> Option<f64> {
274        let count = self.levels.len();
275        if count == 0 {
276            return None;
277        }
278        let n = count as f64;
279        let mean = self.total_amount().to_f64() / n;
280        let variance = self
281            .levels
282            .iter()
283            .map(|level| {
284                let deviation = level.amount.to_f64() - mean;
285                deviation * deviation
286            })
287            .sum::<f64>()
288            / n;
289        Some(variance.sqrt())
290    }
291
292    #[inline]
293    fn search(&self, price: Dec) -> Result<usize, usize> {
294        match self.desc {
295            true => self
296                .levels
297                .binary_search_by(|level| price.cmp(&level.price)),
298            false => self
299                .levels
300                .binary_search_by(|level| level.price.cmp(&price)),
301        }
302    }
303}
304
305impl Default for OrderBook {
306    fn default() -> Self {
307        Self::new(None)
308    }
309}
310
311impl OrderBook {
312    /// An empty book, each side capped at `max_depth` levels, or uncapped on
313    /// `None`.
314    pub fn new(max_depth: Option<usize>) -> Self {
315        Self {
316            bids: OrderBookSide::bids(max_depth),
317            asks: OrderBookSide::asks(max_depth),
318        }
319    }
320
321    /// The level cap both sides were built with.
322    pub fn max_depth(&self) -> Option<usize> {
323        self.bids.max_depth()
324    }
325
326    /// O(1) operation to get the mid price of the order book.
327    ///
328    /// Returns `None` if either the bid or ask side is empty.
329    pub fn mid_price(&self) -> Option<Dec> {
330        match (self.bids.best_price(), self.asks.best_price()) {
331            (Some(bid), Some(ask)) => Some(bid.midpoint(ask)),
332            _ => None,
333        }
334    }
335
336    /// Best ask less best bid, or `None` if either side is empty.
337    ///
338    /// Negative when the book is crossed, which a feed can produce.
339    pub fn spread(&self) -> Option<Dec> {
340        match (self.bids.best_price(), self.asks.best_price()) {
341            (Some(bid), Some(ask)) => Some(ask - bid),
342            _ => None,
343        }
344    }
345
346    /// Order book imbalance at the top: the pressure the best bid and the best
347    /// ask exert against each other.
348    ///
349    /// `(bid - ask) / (bid + ask)` over the two best amounts, which lands in
350    /// `[-1, 1]`: `1` when only bids stand at the touch, `-1` when only asks
351    /// do, and zero when they match. Positive is buying pressure.
352    ///
353    /// `None` when either side is empty, or when both best amounts are zero and
354    /// there is no pressure to take a ratio of.
355    ///
356    /// Exact rather than floating point. The amounts are exact, the sums are
357    /// exact, and only the division rounds, at the scale everything else here
358    /// carries.
359    pub fn imbalance(&self) -> Option<Dec> {
360        imbalance_of(self.bids.best()?.amount, self.asks.best()?.amount)
361    }
362
363    /// [`OrderBook::imbalance`] over a band of the book rather than the touch.
364    ///
365    /// The range is half open, `from..to`, counted in levels from the best on
366    /// each side, so `range_imbalance(0, 5)` weighs the top five levels of one
367    /// side against the top five of the other. A `to` past the end of a side
368    /// takes what that side holds, so an unevenly deep book still answers.
369    ///
370    /// `None` when either side holds no level in the range, when the amounts on
371    /// both sides of it are zero, or when the range is empty — `to` at or below
372    /// `from`, which includes an inverted one, is a band with no levels rather
373    /// than a band read backwards.
374    ///
375    /// Reading deeper than the touch is the point: the best level alone is the
376    /// easiest part of a book to move, and an imbalance measured there is the
377    /// easiest to manufacture.
378    pub fn range_imbalance(&self, from: usize, to: usize) -> Option<Dec> {
379        imbalance_of(
380            self.bids.volume_in(from, to)?,
381            self.asks.volume_in(from, to)?,
382        )
383    }
384
385    /// Apply every level in `diff`, each as [`OrderBookSide::set`] does.
386    pub fn apply_diff(&mut self, diff: &OrderBookDiff) {
387        for bid in diff.bids.iter().copied() {
388            self.bids.set(bid);
389        }
390        for ask in diff.asks.iter().copied() {
391            self.asks.set(ask);
392        }
393    }
394}
395
396/// `(bid - ask) / (bid + ask)`, or `None` when that has no answer.
397///
398/// Both amounts come from levels a book accepted, so both are finite and not
399/// negative, which puts the result in `[-1, 1]` and makes a zero total the only
400/// case with nothing to divide by. The checked arithmetic covers a sum past the
401/// range as well, rather than returning a ratio built on a NaN.
402fn imbalance_of(bid: Dec, ask: Dec) -> Option<Dec> {
403    let total = bid.checked_add(ask)?;
404    bid.checked_sub(ask)?.checked_div(total)
405}
406
407impl From<OrderBookTop> for OrderBook {
408    fn from(top: OrderBookTop) -> Self {
409        let mut order_book = OrderBook::default();
410        order_book.bids.set(top.bid);
411        order_book.asks.set(top.ask);
412        order_book
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    #![allow(clippy::unwrap_used, clippy::expect_used)]
419
420    use super::*;
421    use crate::dec;
422    use crate::testing::{OrderBookBuilder, RandomWalk, Side};
423
424    fn bids(levels: &[(Dec, Dec)], max_depth: Option<usize>) -> OrderBookSide {
425        let mut side = OrderBookSide::bids(max_depth);
426        for (price, amount) in levels.iter().copied() {
427            side.set_price_amount(price, amount);
428        }
429        side
430    }
431
432    fn asks(levels: &[(Dec, Dec)], max_depth: Option<usize>) -> OrderBookSide {
433        let mut side = OrderBookSide::asks(max_depth);
434        for (price, amount) in levels.iter().copied() {
435            side.set_price_amount(price, amount);
436        }
437        side
438    }
439
440    #[test]
441    fn test_bids_are_ordered_best_first() {
442        let side = bids(
443            &[
444                (dec!(99), dec!(1)),
445                (dec!(101), dec!(2)),
446                (dec!(100), dec!(3)),
447            ],
448            None,
449        );
450        let prices: Vec<Dec> = side.iter().map(|level| level.price).collect();
451        assert_eq!(prices, vec![dec!(101), dec!(100), dec!(99)]);
452        assert_eq!(side.best_price(), Some(dec!(101)));
453        assert_eq!(side.worst_price(), Some(dec!(99)));
454        assert_eq!(side.price_at(1), Some(dec!(100)));
455    }
456
457    #[test]
458    fn test_asks_are_ordered_best_first() {
459        let side = asks(
460            &[
461                (dec!(101), dec!(1)),
462                (dec!(99), dec!(2)),
463                (dec!(100), dec!(3)),
464            ],
465            None,
466        );
467        let prices: Vec<Dec> = side.iter().map(|level| level.price).collect();
468        assert_eq!(prices, vec![dec!(99), dec!(100), dec!(101)]);
469        assert_eq!(side.best_price(), Some(dec!(99)));
470        assert_eq!(side.worst_price(), Some(dec!(101)));
471    }
472
473    #[test]
474    fn test_set_replaces_and_removes() {
475        let mut side = bids(&[(dec!(100), dec!(1)), (dec!(99), dec!(2))], None);
476        assert_eq!(side.set_price_amount(dec!(100), dec!(5)), Some(dec!(1)));
477        assert_eq!(side.volume_at(side.len()), Some(dec!(7)));
478        assert_eq!(side.set_price_amount(dec!(100), dec!(0)), Some(dec!(5)));
479        assert_eq!(side.len(), 1);
480        assert_eq!(side.volume_at(side.len()), Some(dec!(2)));
481        assert_eq!(side.find(dec!(100)), None);
482        assert_eq!(side.set_price_amount(dec!(98), dec!(0)), None);
483    }
484
485    #[test]
486    fn test_is_valid_rejects_what_a_book_cannot_hold() {
487        let valid = PriceAmount {
488            price: dec!(100),
489            amount: dec!(1),
490        };
491        assert!(valid.is_valid());
492        assert!(
493            PriceAmount {
494                price: dec!(100),
495                amount: Dec::ZERO,
496            }
497            .is_valid(),
498            "a zero amount is the removal of a level, not an invalid one"
499        );
500        for bad in [
501            PriceAmount {
502                price: Dec::NAN,
503                amount: dec!(1),
504            },
505            PriceAmount {
506                price: dec!(100),
507                amount: Dec::NAN,
508            },
509            PriceAmount {
510                price: dec!(100),
511                amount: dec!(-1),
512            },
513        ] {
514            assert!(!bad.is_valid(), "{bad:?}");
515        }
516    }
517
518    #[test]
519    fn test_an_invalid_level_leaves_the_book_untouched() {
520        let mut side = bids(&[(dec!(100), dec!(1)), (dec!(99), dec!(2))], None);
521        for bad in [
522            (Dec::NAN, dec!(5)),
523            (dec!(101), Dec::NAN),
524            (dec!(101), dec!(-5)),
525        ] {
526            assert_eq!(side.set_price_amount(bad.0, bad.1), None);
527        }
528        let prices: Vec<Dec> = side.iter().map(|level| level.price).collect();
529        assert_eq!(prices, vec![dec!(100), dec!(99)]);
530        // a NaN amount can never be taken back out of the total, so the test
531        // that matters is that it never went in
532        assert_eq!(side.volume_at(side.len()), Some(dec!(3)));
533        assert_eq!(side.amount_mean(), Some(1.5));
534    }
535
536    #[test]
537    fn test_range_with_low_above_high_is_empty() {
538        let side = asks(&[(dec!(100), dec!(1)), (dec!(101), dec!(1))], None);
539        assert_eq!(side.range(dec!(101), dec!(100)).count(), 0);
540        let side = bids(&[(dec!(101), dec!(1)), (dec!(100), dec!(1))], None);
541        assert_eq!(side.range(dec!(101), dec!(100)).count(), 0);
542    }
543
544    #[test]
545    fn test_a_large_max_depth_does_not_reserve_it() {
546        // usize::MAX is a legal cap meaning "no limit"; reserving it aborts
547        let mut book = OrderBook::new(Some(usize::MAX));
548        book.asks.set_price_amount(dec!(100), dec!(1));
549        assert_eq!(book.asks.len(), 1);
550        assert_eq!(book.max_depth(), Some(usize::MAX));
551    }
552
553    #[test]
554    fn test_max_depth_drops_worst_levels() {
555        let mut side = bids(
556            &[
557                (dec!(100), dec!(1)),
558                (dec!(99), dec!(2)),
559                (dec!(98), dec!(3)),
560            ],
561            Some(2),
562        );
563        assert_eq!(side.len(), 2);
564        assert_eq!(side.volume_at(side.len()), Some(dec!(3)));
565        // a better price evicts the worst level
566        side.set_price_amount(dec!(101), dec!(4));
567        assert_eq!(side.len(), 2);
568        assert_eq!(side.best_price(), Some(dec!(101)));
569        assert_eq!(side.worst_price(), Some(dec!(100)));
570        assert_eq!(side.volume_at(side.len()), Some(dec!(5)));
571        // a worse price is ignored
572        assert_eq!(side.set_price_amount(dec!(97), dec!(9)), None);
573        assert_eq!(side.len(), 2);
574    }
575
576    #[test]
577    fn test_trim() {
578        let mut side = asks(
579            &[
580                (dec!(100), dec!(1)),
581                (dec!(101), dec!(2)),
582                (dec!(102), dec!(3)),
583            ],
584            None,
585        );
586        side.trim(1);
587        assert_eq!(side.len(), 1);
588        assert_eq!(side.volume_at(side.len()), Some(dec!(1)));
589        assert_eq!(side.amount_std_dev(), Some(0.0));
590    }
591
592    #[test]
593    fn test_volume_and_stats() {
594        let side = asks(
595            &[
596                (dec!(100), dec!(1)),
597                (dec!(101), dec!(2)),
598                (dec!(102), dec!(3)),
599            ],
600            None,
601        );
602        assert_eq!(side.volume_at(2), Some(dec!(3)));
603        assert_eq!(side.volume_at(10), Some(dec!(6)));
604        assert_eq!(side.amount_mean(), Some(2.0));
605        let std_dev = side.amount_std_dev().expect("non empty side");
606        assert!((std_dev - (2.0_f64 / 3.0).sqrt()).abs() < 1e-12);
607    }
608
609    #[test]
610    fn test_price_for_quantity() {
611        let side = asks(&[(dec!(100), dec!(1)), (dec!(102), dec!(1))], None);
612        assert_eq!(side.price_for_quantity(1.0), Some(100.0));
613        assert_eq!(side.price_for_quantity(2.0), Some(101.0));
614        assert_eq!(side.price_for_quantity(3.0), None);
615        assert_eq!(side.price_for_quantity(0.0), None);
616    }
617
618    #[test]
619    fn test_imbalance_at_the_touch() {
620        let mut book = OrderBook::new(None);
621        assert_eq!(book.imbalance(), None, "an empty book has no pressure");
622
623        book.bids.set_price_amount(dec!(100), dec!(3));
624        assert_eq!(book.imbalance(), None, "one side alone is not an imbalance");
625
626        book.asks.set_price_amount(dec!(101), dec!(1));
627        // (3 - 1) / (3 + 1)
628        assert_eq!(book.imbalance(), Some(dec!(0.5)));
629
630        // matched amounts cancel
631        book.asks.set_price_amount(dec!(101), dec!(3));
632        assert_eq!(book.imbalance(), Some(Dec::ZERO));
633
634        // the ask side heavier is negative, and symmetric with the reverse
635        book.bids.set_price_amount(dec!(100), dec!(1));
636        assert_eq!(book.imbalance(), Some(dec!(-0.5)));
637    }
638
639    #[test]
640    fn test_imbalance_reaches_its_limits_and_stops() {
641        let mut book = OrderBook::new(None);
642        // a zero amount removes rather than resting, so the extremes are
643        // reached with an amount small against the other side, not with none
644        book.bids.set_price_amount(dec!(100), dec!(1));
645        book.asks.set_price_amount(dec!(101), Dec::EPSILON);
646        let imbalance = book.imbalance().expect("both sides stand");
647        assert!(imbalance < Dec::ONE, "{imbalance} reached the limit");
648        assert!(imbalance > dec!(0.999999), "{imbalance} fell short of it");
649    }
650
651    #[test]
652    fn test_range_imbalance_weighs_a_band() {
653        let mut book = OrderBook::new(None);
654        for level in 0..4 {
655            let step = dec!(0.01) * Dec::from(level);
656            book.bids.set_price_amount(dec!(100) - step, dec!(2));
657            book.asks.set_price_amount(dec!(100.01) + step, Dec::ONE);
658        }
659        // the whole band: 8 against 4
660        assert_eq!(book.range_imbalance(0, 4), Some(dec!(0.333333333333333333)));
661        // the touch alone agrees with the dedicated call
662        assert_eq!(book.range_imbalance(0, 1), book.imbalance());
663        // a band below the touch, which is the point of taking a range
664        assert_eq!(book.range_imbalance(2, 4), Some(dec!(0.333333333333333333)));
665        // an empty range has nothing to weigh
666        assert_eq!(book.range_imbalance(2, 2), None);
667        assert_eq!(book.range_imbalance(3, 1), None);
668        // past the end takes what is there rather than failing
669        assert_eq!(book.range_imbalance(0, 99), book.range_imbalance(0, 4));
670        assert_eq!(book.range_imbalance(9, 99), None);
671    }
672
673    #[test]
674    fn test_range_imbalance_of_an_empty_or_inverted_band_is_none() {
675        let mut book = OrderBook::new(None);
676        for level in 0..4 {
677            let step = dec!(0.01) * Dec::from(level);
678            book.bids.set_price_amount(dec!(100) - step, dec!(2));
679            book.asks.set_price_amount(dec!(100.01) + step, Dec::ONE);
680        }
681        // every band that asks for nothing, over a book that holds plenty
682        for from in 0..7 {
683            for to in 0..=from {
684                assert_eq!(
685                    book.range_imbalance(from, to),
686                    None,
687                    "range_imbalance({from}, {to}) read a band with no levels in it"
688                );
689            }
690        }
691        // and the first band that does ask for something answers
692        assert!(book.range_imbalance(0, 1).is_some());
693    }
694
695    #[test]
696    fn test_range_imbalance_on_an_unevenly_deep_book() {
697        let mut book = OrderBook::new(None);
698        book.bids.set_price_amount(dec!(100), dec!(4));
699        book.bids.set_price_amount(dec!(99), dec!(4));
700        book.asks.set_price_amount(dec!(101), dec!(2));
701        // the ask side runs out inside the range and contributes what it has
702        assert_eq!(book.range_imbalance(0, 2), Some(dec!(0.6)));
703        // and once the range starts past everything it holds, there is no ratio
704        assert_eq!(book.range_imbalance(1, 2), None);
705    }
706
707    #[test]
708    fn test_apply_diff() {
709        let mut book = OrderBook::new(None);
710        book.apply_diff(&OrderBookDiff {
711            bids: vec![PriceAmount {
712                price: dec!(99),
713                amount: dec!(1),
714            }],
715            asks: vec![PriceAmount {
716                price: dec!(101),
717                amount: dec!(2),
718            }],
719        });
720        assert_eq!(book.mid_price(), Some(dec!(100)));
721        assert_eq!(book.spread(), Some(dec!(2)));
722        book.apply_diff(&OrderBookDiff {
723            bids: vec![PriceAmount {
724                price: dec!(99),
725                amount: dec!(0),
726            }],
727            ..Default::default()
728        });
729        assert!(book.bids.is_empty());
730        assert_eq!(book.mid_price(), None);
731    }
732
733    #[test]
734    fn test_order_book_from_top() {
735        let top = OrderBookTop {
736            bid: PriceAmount {
737                price: dec!(99),
738                amount: dec!(1),
739            },
740            ask: PriceAmount {
741                price: dec!(101),
742                amount: dec!(2),
743            },
744        };
745        let book = OrderBook::from(top);
746        assert_eq!(book.bids.best_price(), Some(dec!(99)));
747        assert_eq!(book.asks.best_price(), Some(dec!(101)));
748        assert_eq!(book.mid_price(), Some(dec!(100)));
749    }
750
751    /// After a simulation step the book must not be crossed: best bid < best ask,
752    /// and spread() must equal ask - bid.
753    #[test]
754    fn test_simulate_book_never_crossed() {
755        let mut builder = OrderBookBuilder::new()
756            .with_tick_size(dec!(0.01))
757            .with_spread(dec!(0.02))
758            .with_amount(dec!(10));
759        let prices = RandomWalk::new(100).lognormal(100.0, 0.0, 0.2).unwrap();
760        for mid in prices {
761            let (book, _) = builder.simulate(Dec::from_f64(mid).unwrap(), 5);
762            let bid = book.bids.best_price().expect("bids must be non-empty");
763            let ask = book.asks.best_price().expect("asks must be non-empty");
764            assert!(
765                bid < ask,
766                "book crossed after simulate: bid={bid} ask={ask}"
767            );
768            assert_eq!(book.spread(), Some(ask - bid));
769            assert_eq!(
770                book.bids.len(),
771                5,
772                "expected 5 bid levels, got {}",
773                book.bids.len()
774            );
775            assert_eq!(
776                book.asks.len(),
777                5,
778                "expected 5 ask levels, got {}",
779                book.asks.len()
780            );
781        }
782    }
783
784    /// All amounts in the book must be strictly positive after every simulate step —
785    /// zero-amount levels are removals and must never persist.
786    #[test]
787    fn test_simulate_all_amounts_positive() {
788        let mut builder = OrderBookBuilder::new()
789            .with_tick_size(dec!(0.01))
790            .with_spread(dec!(0.02))
791            .with_amount(dec!(10));
792        let prices = RandomWalk::new(100).lognormal(100.0, 0.0, 0.2).unwrap();
793        for mid in prices {
794            let (book, _) = builder.simulate(Dec::from_f64(mid).unwrap(), 20);
795            for level in book.bids.iter() {
796                assert!(
797                    level.amount > Dec::ZERO,
798                    "bid level at {} has non-positive amount {}",
799                    level.price,
800                    level.amount
801                );
802            }
803            for level in book.asks.iter() {
804                assert!(
805                    level.amount > Dec::ZERO,
806                    "ask level at {} has non-positive amount {}",
807                    level.price,
808                    level.amount
809                );
810            }
811        }
812    }
813
814    /// All prices must be multiples of tick_size and all amounts multiples of lot_size.
815    #[test]
816    fn test_simulate_prices_and_amounts_aligned() {
817        let tick = dec!(0.01);
818        let lot = dec!(0.01);
819        let mut builder = OrderBookBuilder::new()
820            .with_tick_size(tick)
821            .with_lot_size(lot)
822            .with_spread(dec!(0.02))
823            .with_amount(dec!(10));
824        let prices = RandomWalk::new(100).lognormal(100.0, 0.0, 0.2).unwrap();
825        for mid in prices {
826            let (book, _) = builder.simulate(Dec::from_f64(mid).unwrap(), 20);
827            for level in book.bids.iter().chain(book.asks.iter()) {
828                assert_eq!(
829                    level.price.into_raw() % tick.into_raw(),
830                    0,
831                    "price {} is not a multiple of tick {}",
832                    level.price,
833                    tick
834                );
835                assert_eq!(
836                    level.amount.into_raw() % lot.into_raw(),
837                    0,
838                    "amount {} is not a multiple of lot {}",
839                    level.amount,
840                    lot
841                );
842            }
843        }
844    }
845
846    /// Trades returned in events must correspond to levels that crossed the new spread:
847    /// bid-side trades (Side::Sell) have price > new bid, ask-side trades (Side::Buy) have price < new ask.
848    #[test]
849    fn test_simulate_trades_are_crossed_levels() {
850        let mut builder = OrderBookBuilder::new()
851            .with_tick_size(dec!(0.01))
852            .with_spread(dec!(0.02))
853            .with_amount(dec!(10));
854        let prices = RandomWalk::new(100).lognormal(100.0, 0.0, 0.2).unwrap();
855        for mid in prices {
856            let (book, events) = builder.simulate(Dec::from_f64(mid).unwrap(), 20);
857            let bid = book.bids.best_price().expect("bids must be non-empty");
858            let ask = book.asks.best_price().expect("asks must be non-empty");
859            for trade in &events.trades {
860                match trade.side {
861                    Side::Sell => assert!(
862                        trade.price_amount.price > bid,
863                        "bid-side trade at {} is not above new bid {}",
864                        trade.price_amount.price,
865                        bid
866                    ),
867                    Side::Buy => assert!(
868                        trade.price_amount.price < ask,
869                        "ask-side trade at {} is not below new ask {}",
870                        trade.price_amount.price,
871                        ask
872                    ),
873                }
874            }
875        }
876    }
877
878    /// Every new order returned in events must be present in the book after simulate.
879    #[test]
880    fn test_simulate_new_orders_in_book() {
881        let mut builder = OrderBookBuilder::new()
882            .with_tick_size(dec!(0.01))
883            .with_spread(dec!(0.02))
884            .with_amount(dec!(10));
885        let prices = RandomWalk::new(100).lognormal(100.0, 0.0, 0.2).unwrap();
886        for mid in prices {
887            let (book, events) = builder.simulate(Dec::from_f64(mid).unwrap(), 20);
888            for order in &events.new_orders {
889                match order.side {
890                    Side::Buy => assert!(
891                        book.bids
892                            .iter()
893                            .any(|level| level.price == order.price_amount.price),
894                        "new bid at {} not found in book",
895                        order.price_amount.price
896                    ),
897                    Side::Sell => assert!(
898                        book.asks
899                            .iter()
900                            .any(|level| level.price == order.price_amount.price),
901                        "new ask at {} not found in book",
902                        order.price_amount.price
903                    ),
904                }
905            }
906        }
907    }
908
909    /// volume_at properties: None at 0, equals best amount at 1,
910    /// monotonically non-decreasing, and equals total at full depth.
911    #[test]
912    fn test_simulate_volume_at_properties() {
913        let mut builder = OrderBookBuilder::new()
914            .with_tick_size(dec!(0.01))
915            .with_spread(dec!(0.02))
916            .with_amount(dec!(10));
917        let prices = RandomWalk::new(100).lognormal(100.0, 0.0, 0.2).unwrap();
918        for mid in prices {
919            let (book, _) = builder.simulate(Dec::from_f64(mid).unwrap(), 20);
920            for side in [&book.bids, &book.asks] {
921                // volume_at(0) is always None
922                assert_eq!(side.volume_at(0), None);
923
924                // volume_at(1) equals the best level amount
925                let best_amount = side.best().unwrap().amount;
926                assert_eq!(side.volume_at(1), Some(best_amount));
927
928                // volume_at is monotonically non-decreasing
929                let mut prev = Dec::ZERO;
930                for n in 1..=side.len() {
931                    let vol = side.volume_at(n).unwrap();
932                    assert!(
933                        vol >= prev,
934                        "volume_at({n})={vol} < volume_at({})={prev}",
935                        n - 1
936                    );
937                    prev = vol;
938                }
939
940                // volume_at(len) equals sum of all amounts
941                let total: Dec = side.iter().map(|level| level.amount).sum();
942                assert_eq!(side.volume_at(side.len()), Some(total));
943            }
944        }
945    }
946
947    #[test]
948    fn test_range_empty_returns_nothing() {
949        let book = OrderBookBuilder::new()
950            .with_ask(dec!(100), dec!(1))
951            .with_ask(dec!(101), dec!(1))
952            .build();
953        let result: Vec<_> = book.asks.range(dec!(200), dec!(300)).collect();
954        assert!(result.is_empty());
955    }
956
957    #[test]
958    fn test_range_asks_ascending_best_first() {
959        let book = OrderBookBuilder::new()
960            .with_ask(dec!(100), dec!(1))
961            .with_ask(dec!(101), dec!(2))
962            .with_ask(dec!(102), dec!(3))
963            .with_ask(dec!(103), dec!(4))
964            .build();
965        let prices: Vec<_> = book
966            .asks
967            .range(dec!(100), dec!(102))
968            .map(|l| l.price)
969            .collect();
970        assert_eq!(prices, vec![dec!(100), dec!(101), dec!(102)]);
971    }
972
973    #[test]
974    fn test_range_bids_descending_best_first() {
975        let book = OrderBookBuilder::new()
976            .with_bid(dec!(100), dec!(1))
977            .with_bid(dec!(101), dec!(2))
978            .with_bid(dec!(102), dec!(3))
979            .with_bid(dec!(103), dec!(4))
980            .build();
981        let prices: Vec<_> = book
982            .bids
983            .range(dec!(101), dec!(103))
984            .map(|l| l.price)
985            .collect();
986        assert_eq!(prices, vec![dec!(103), dec!(102), dec!(101)]);
987    }
988
989    #[test]
990    fn test_range_bounds_not_in_book() {
991        let book = OrderBookBuilder::new()
992            .with_ask(dec!(99), dec!(1))
993            .with_ask(dec!(100), dec!(2))
994            .with_ask(dec!(101), dec!(3))
995            .with_ask(dec!(102), dec!(4))
996            .build();
997        let prices: Vec<_> = book
998            .asks
999            .range(dec!(99.5), dec!(101.5))
1000            .map(|l| l.price)
1001            .collect();
1002        assert_eq!(prices, vec![dec!(100), dec!(101)]);
1003    }
1004
1005    #[test]
1006    fn test_amount_stats_after_trim() {
1007        // amounts 1,2,3,4,5 at bids 101..105 — trim to 3 keeps the 3 best (highest) bids
1008        // remaining amounts: 3, 4, 5 → mean=4, variance=2/3, std_dev=sqrt(2/3)
1009        let mut book = OrderBookBuilder::new()
1010            .with_bid(dec!(101), dec!(1))
1011            .with_bid(dec!(102), dec!(2))
1012            .with_bid(dec!(103), dec!(3))
1013            .with_bid(dec!(104), dec!(4))
1014            .with_bid(dec!(105), dec!(5))
1015            .build();
1016        book.bids.trim(3);
1017        assert_eq!(book.bids.len(), 3);
1018        assert_eq!(book.bids.amount_mean(), Some(4.0));
1019        let std_dev = book.bids.amount_std_dev().unwrap();
1020        let expected = (2.0f64 / 3.0).sqrt();
1021        let diff = (std_dev - expected).abs();
1022        assert!(diff < 1e-7, "std_dev={std_dev} expected={expected}");
1023    }
1024
1025    #[test]
1026    fn test_amount_stats_empty() {
1027        let book = OrderBookBuilder::new().build();
1028        assert_eq!(book.bids.amount_mean(), None);
1029        assert_eq!(book.bids.amount_std_dev(), None);
1030    }
1031
1032    #[test]
1033    fn test_amount_stats_single_level() {
1034        let book = OrderBookBuilder::new().with_bid(dec!(100), dec!(5)).build();
1035        assert_eq!(book.bids.amount_mean(), Some(5.0));
1036        assert_eq!(book.bids.amount_std_dev(), Some(0.0));
1037    }
1038
1039    #[test]
1040    fn test_amount_stats_multiple_levels() {
1041        // amounts 1, 2, 3, 4, 5 → mean=3, variance=2, std_dev=sqrt(2)
1042        let book = OrderBookBuilder::new()
1043            .with_bid(dec!(101), dec!(1))
1044            .with_bid(dec!(102), dec!(2))
1045            .with_bid(dec!(103), dec!(3))
1046            .with_bid(dec!(104), dec!(4))
1047            .with_bid(dec!(105), dec!(5))
1048            .build();
1049        assert_eq!(book.bids.amount_mean(), Some(3.0));
1050        let std_dev = book.bids.amount_std_dev().unwrap();
1051        let expected = 2.0f64.sqrt();
1052        let diff = (std_dev - expected).abs();
1053        assert!(diff < 1e-7, "std_dev={std_dev} expected={expected}");
1054    }
1055
1056    #[test]
1057    fn test_amount_stats_after_removal() {
1058        // amounts 2, 4, 6 → remove 2 → remaining 4, 6 → mean=5, variance=1, std_dev=1
1059        let mut book = OrderBookBuilder::new()
1060            .with_bid(dec!(100), dec!(2))
1061            .with_bid(dec!(101), dec!(4))
1062            .with_bid(dec!(102), dec!(6))
1063            .build();
1064        book.bids.set_price_amount(dec!(100), Dec::ZERO);
1065        assert_eq!(book.bids.amount_mean(), Some(5.0));
1066        assert_eq!(book.bids.amount_std_dev(), Some(1.0));
1067    }
1068
1069    #[test]
1070    fn test_amount_stats_after_update() {
1071        // amounts 2, 4 → update 2→6 → amounts 6, 4 → mean=5, variance=1, std_dev=1
1072        let mut book = OrderBookBuilder::new()
1073            .with_bid(dec!(100), dec!(2))
1074            .with_bid(dec!(101), dec!(4))
1075            .build();
1076        book.bids.set_price_amount(dec!(100), dec!(6));
1077        assert_eq!(book.bids.amount_mean(), Some(5.0));
1078        assert_eq!(book.bids.amount_std_dev(), Some(1.0));
1079    }
1080}