Skip to main content

wickra_core/indicators/
ob_imbalance_full.rs

1//! Order-Book Imbalance over the full visible depth.
2
3use crate::microstructure::OrderBook;
4use crate::traits::Indicator;
5
6/// Order-Book Imbalance aggregated over the full visible depth of each side.
7///
8/// Sums the resting size of every bid level and every ask level in the
9/// snapshot and compares them:
10///
11/// ```text
12/// bidDepth  = Σ size of all bids
13/// askDepth  = Σ size of all asks
14/// imbalance = (bidDepth − askDepth) / (bidDepth + askDepth)
15/// ```
16///
17/// The output lies in `[−1, +1]`. A book with zero total size yields `0`. Use
18/// [`crate::OrderBookImbalanceTopN`] to bound the depth to the most relevant
19/// near-touch levels instead of the full visible book.
20///
21/// `Input = OrderBook`, `Output = f64`. Stateless; ready after the first
22/// snapshot.
23///
24/// # Example
25///
26/// ```
27/// use wickra_core::{Indicator, Level, OrderBook, OrderBookImbalanceFull};
28///
29/// let book = OrderBook::new(
30///     vec![Level::new(100.0, 2.0).unwrap(), Level::new(99.0, 1.0).unwrap()],
31///     vec![Level::new(101.0, 0.5).unwrap(), Level::new(102.0, 0.5).unwrap()],
32/// )
33/// .unwrap();
34/// let mut obi = OrderBookImbalanceFull::new();
35/// assert_eq!(obi.update(book), Some(0.5)); // (3 − 1) / (3 + 1)
36/// ```
37#[derive(Debug, Clone, Default)]
38pub struct OrderBookImbalanceFull {
39    has_emitted: bool,
40}
41
42impl OrderBookImbalanceFull {
43    /// Construct a new full-depth imbalance indicator.
44    pub const fn new() -> Self {
45        Self { has_emitted: false }
46    }
47}
48
49impl Indicator for OrderBookImbalanceFull {
50    type Input = OrderBook;
51    type Output = f64;
52
53    fn update(&mut self, book: OrderBook) -> Option<f64> {
54        self.has_emitted = true;
55        let bid_depth: f64 = book.bids.iter().map(|l| l.size).sum();
56        let ask_depth: f64 = book.asks.iter().map(|l| l.size).sum();
57        let total = bid_depth + ask_depth;
58        if total <= 0.0 {
59            return Some(0.0);
60        }
61        Some((bid_depth - ask_depth) / total)
62    }
63
64    fn reset(&mut self) {
65        self.has_emitted = false;
66    }
67
68    fn warmup_period(&self) -> usize {
69        1
70    }
71
72    fn is_ready(&self) -> bool {
73        self.has_emitted
74    }
75
76    fn name(&self) -> &'static str {
77        "OrderBookImbalanceFull"
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::microstructure::Level;
85    use crate::traits::BatchExt;
86
87    fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
88        let to_levels = |xs: &[(f64, f64)]| {
89            xs.iter()
90                .map(|&(p, s)| Level::new(p, s).unwrap())
91                .collect::<Vec<_>>()
92        };
93        OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
94    }
95
96    #[test]
97    fn accessors_and_metadata() {
98        let obi = OrderBookImbalanceFull::new();
99        assert_eq!(obi.name(), "OrderBookImbalanceFull");
100        assert_eq!(obi.warmup_period(), 1);
101        assert!(!obi.is_ready());
102    }
103
104    #[test]
105    fn sums_full_depth() {
106        let mut obi = OrderBookImbalanceFull::new();
107        let b = book(&[(100.0, 2.0), (99.0, 2.0)], &[(101.0, 1.0), (102.0, 1.0)]);
108        // bidDepth 4, askDepth 2 -> (4 - 2) / 6 = 1/3.
109        assert_eq!(obi.update(b), Some(1.0 / 3.0));
110        assert!(obi.is_ready());
111    }
112
113    #[test]
114    fn ask_heavy_full_depth_is_negative() {
115        let mut obi = OrderBookImbalanceFull::new();
116        let b = book(&[(100.0, 1.0)], &[(101.0, 2.0), (102.0, 1.0)]);
117        // (1 - 3) / 4 = -0.5.
118        assert_eq!(obi.update(b), Some(-0.5));
119    }
120
121    #[test]
122    fn zero_size_is_zero() {
123        let mut obi = OrderBookImbalanceFull::new();
124        assert_eq!(
125            obi.update(book(&[(100.0, 0.0)], &[(101.0, 0.0)])),
126            Some(0.0)
127        );
128    }
129
130    #[test]
131    fn batch_equals_streaming() {
132        let books: Vec<OrderBook> = (0..20)
133            .map(|i| {
134                let bid = 1.0 + f64::from(i % 3);
135                book(&[(100.0, bid), (99.0, 1.0)], &[(101.0, 2.0), (102.0, 1.0)])
136            })
137            .collect();
138        let mut a = OrderBookImbalanceFull::new();
139        let mut b = OrderBookImbalanceFull::new();
140        assert_eq!(
141            a.batch(&books),
142            books
143                .iter()
144                .map(|x| b.update(x.clone()))
145                .collect::<Vec<_>>()
146        );
147    }
148
149    #[test]
150    fn reset_clears_state() {
151        let mut obi = OrderBookImbalanceFull::new();
152        obi.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)]));
153        assert!(obi.is_ready());
154        obi.reset();
155        assert!(!obi.is_ready());
156    }
157}