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