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    fn update(&mut self, book: OrderBook) -> Option<f64> {
52        self.has_emitted = true;
53        let (Some(bid), Some(ask)) = (book.best_bid(), book.best_ask()) else {
54            return Some(0.0);
55        };
56        let total = bid.size + ask.size;
57        if total <= 0.0 {
58            return Some(0.0);
59        }
60        Some((bid.size - ask.size) / total)
61    }
62
63    fn reset(&mut self) {
64        self.has_emitted = false;
65    }
66
67    fn warmup_period(&self) -> usize {
68        1
69    }
70
71    fn is_ready(&self) -> bool {
72        self.has_emitted
73    }
74
75    fn name(&self) -> &'static str {
76        "OrderBookImbalanceTop1"
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::microstructure::Level;
84    use crate::traits::BatchExt;
85
86    fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
87        let to_levels = |xs: &[(f64, f64)]| {
88            xs.iter()
89                .map(|&(p, s)| Level::new(p, s).unwrap())
90                .collect::<Vec<_>>()
91        };
92        OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
93    }
94
95    #[test]
96    fn accessors_and_metadata() {
97        let obi = OrderBookImbalanceTop1::new();
98        assert_eq!(obi.name(), "OrderBookImbalanceTop1");
99        assert_eq!(obi.warmup_period(), 1);
100        assert!(!obi.is_ready());
101    }
102
103    #[test]
104    fn balanced_top_is_zero() {
105        let mut obi = OrderBookImbalanceTop1::new();
106        assert_eq!(
107            obi.update(book(&[(100.0, 2.0)], &[(101.0, 2.0)])),
108            Some(0.0)
109        );
110        assert!(obi.is_ready());
111    }
112
113    #[test]
114    fn bid_heavy_is_positive() {
115        let mut obi = OrderBookImbalanceTop1::new();
116        assert_eq!(
117            obi.update(book(&[(100.0, 3.0)], &[(101.0, 1.0)])),
118            Some(0.5)
119        );
120    }
121
122    #[test]
123    fn ask_heavy_is_negative() {
124        let mut obi = OrderBookImbalanceTop1::new();
125        assert_eq!(
126            obi.update(book(&[(100.0, 1.0)], &[(101.0, 3.0)])),
127            Some(-0.5)
128        );
129    }
130
131    #[test]
132    fn zero_size_top_is_zero() {
133        let mut obi = OrderBookImbalanceTop1::new();
134        assert_eq!(
135            obi.update(book(&[(100.0, 0.0)], &[(101.0, 0.0)])),
136            Some(0.0)
137        );
138    }
139
140    #[test]
141    fn empty_book_is_zero() {
142        let mut obi = OrderBookImbalanceTop1::new();
143        assert_eq!(
144            obi.update(OrderBook::new_unchecked(vec![], vec![])),
145            Some(0.0)
146        );
147    }
148
149    #[test]
150    fn batch_equals_streaming() {
151        let books: Vec<OrderBook> = (0..20)
152            .map(|i| {
153                let bid = 1.0 + f64::from(i % 5);
154                book(&[(100.0, bid)], &[(101.0, 2.0)])
155            })
156            .collect();
157        let mut a = OrderBookImbalanceTop1::new();
158        let mut b = OrderBookImbalanceTop1::new();
159        assert_eq!(
160            a.batch(&books),
161            books
162                .iter()
163                .map(|x| b.update(x.clone()))
164                .collect::<Vec<_>>()
165        );
166    }
167
168    #[test]
169    fn reset_clears_state() {
170        let mut obi = OrderBookImbalanceTop1::new();
171        obi.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)]));
172        assert!(obi.is_ready());
173        obi.reset();
174        assert!(!obi.is_ready());
175    }
176}