Skip to main content

wickra_core/indicators/
ob_imbalance_top1.rs

1//! Order-Book Imbalance at the top of book.
2
3use crate::microstructure::OrderBook;
4use crate::traits::Indicator;
5
6/// Order-Book Imbalance (top-of-book).
7///
8/// Measures the pressure between the best bid and best ask by comparing their
9/// resting sizes:
10///
11/// ```text
12/// imbalance = (bidSize₁ − askSize₁) / (bidSize₁ + askSize₁)
13/// ```
14///
15/// The output lies in `[−1, +1]`: `+1` means all size sits on the bid (buy
16/// pressure), `−1` means all size sits on the ask (sell pressure), `0` means a
17/// balanced top of book. A book with zero size on both top levels yields `0`.
18///
19/// `Input = OrderBook`, `Output = f64`. The indicator is stateless and ready
20/// after the first snapshot.
21///
22/// # Example
23///
24/// ```
25/// use wickra_core::{Indicator, Level, OrderBook, OrderBookImbalanceTop1};
26///
27/// let book = OrderBook::new(
28///     vec![Level::new(100.0, 3.0).unwrap()],
29///     vec![Level::new(101.0, 1.0).unwrap()],
30/// )
31/// .unwrap();
32/// let mut obi = OrderBookImbalanceTop1::new();
33/// assert_eq!(obi.update(book), Some(0.5)); // (3 − 1) / (3 + 1)
34/// ```
35#[derive(Debug, Clone, Default)]
36pub struct OrderBookImbalanceTop1 {
37    has_emitted: bool,
38}
39
40impl OrderBookImbalanceTop1 {
41    /// Construct a new top-of-book imbalance indicator.
42    pub const fn new() -> Self {
43        Self { has_emitted: false }
44    }
45}
46
47impl Indicator for OrderBookImbalanceTop1 {
48    type Input = OrderBook;
49    type Output = f64;
50
51    #[inline]
52    fn update(&mut self, book: OrderBook) -> Option<f64> {
53        self.has_emitted = true;
54        let (Some(bid), Some(ask)) = (book.best_bid(), book.best_ask()) else {
55            return Some(0.0);
56        };
57        let total = bid.size + ask.size;
58        if total <= 0.0 {
59            return Some(0.0);
60        }
61        Some((bid.size - ask.size) / total)
62    }
63
64    fn reset(&mut self) {
65        self.has_emitted = false;
66    }
67
68    #[inline]
69    fn warmup_period(&self) -> usize {
70        1
71    }
72
73    #[inline]
74    fn is_ready(&self) -> bool {
75        self.has_emitted
76    }
77
78    #[inline]
79    fn name(&self) -> &'static str {
80        "OrderBookImbalanceTop1"
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::microstructure::Level;
88    use crate::traits::BatchExt;
89
90    fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
91        let to_levels = |xs: &[(f64, f64)]| {
92            xs.iter()
93                .map(|&(p, s)| Level::new(p, s).unwrap())
94                .collect::<Vec<_>>()
95        };
96        OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
97    }
98
99    #[test]
100    fn accessors_and_metadata() {
101        let obi = OrderBookImbalanceTop1::new();
102        assert_eq!(obi.name(), "OrderBookImbalanceTop1");
103        assert_eq!(obi.warmup_period(), 1);
104        assert!(!obi.is_ready());
105    }
106
107    #[test]
108    fn balanced_top_is_zero() {
109        let mut obi = OrderBookImbalanceTop1::new();
110        assert_eq!(
111            obi.update(book(&[(100.0, 2.0)], &[(101.0, 2.0)])),
112            Some(0.0)
113        );
114        assert!(obi.is_ready());
115    }
116
117    #[test]
118    fn bid_heavy_is_positive() {
119        let mut obi = OrderBookImbalanceTop1::new();
120        assert_eq!(
121            obi.update(book(&[(100.0, 3.0)], &[(101.0, 1.0)])),
122            Some(0.5)
123        );
124    }
125
126    #[test]
127    fn ask_heavy_is_negative() {
128        let mut obi = OrderBookImbalanceTop1::new();
129        assert_eq!(
130            obi.update(book(&[(100.0, 1.0)], &[(101.0, 3.0)])),
131            Some(-0.5)
132        );
133    }
134
135    #[test]
136    fn zero_size_top_is_zero() {
137        let mut obi = OrderBookImbalanceTop1::new();
138        assert_eq!(
139            obi.update(book(&[(100.0, 0.0)], &[(101.0, 0.0)])),
140            Some(0.0)
141        );
142    }
143
144    #[test]
145    fn empty_book_is_zero() {
146        let mut obi = OrderBookImbalanceTop1::new();
147        assert_eq!(
148            obi.update(OrderBook::new_unchecked(vec![], vec![])),
149            Some(0.0)
150        );
151    }
152
153    #[test]
154    fn batch_equals_streaming() {
155        let books: Vec<OrderBook> = (0..20)
156            .map(|i| {
157                let bid = 1.0 + f64::from(i % 5);
158                book(&[(100.0, bid)], &[(101.0, 2.0)])
159            })
160            .collect();
161        let mut a = OrderBookImbalanceTop1::new();
162        let mut b = OrderBookImbalanceTop1::new();
163        assert_eq!(
164            a.batch(&books),
165            books
166                .iter()
167                .map(|x| b.update(x.clone()))
168                .collect::<Vec<_>>()
169        );
170    }
171
172    #[test]
173    fn reset_clears_state() {
174        let mut obi = OrderBookImbalanceTop1::new();
175        obi.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)]));
176        assert!(obi.is_ready());
177        obi.reset();
178        assert!(!obi.is_ready());
179    }
180}