wickra_core/indicators/
ob_imbalance_topn.rs1use crate::error::{Error, Result};
4use crate::microstructure::OrderBook;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
40pub struct OrderBookImbalanceTopN {
41 levels: usize,
42 has_emitted: bool,
43}
44
45impl OrderBookImbalanceTopN {
46 pub fn new(levels: usize) -> Result<Self> {
52 if levels == 0 {
53 return Err(Error::PeriodZero);
54 }
55 if levels > crate::error::MAX_PERIOD {
56 return Err(Error::InvalidPeriod {
57 message: crate::error::PERIOD_ABOVE_MAX,
58 });
59 }
60 Ok(Self {
61 levels,
62 has_emitted: false,
63 })
64 }
65
66 pub fn levels(&self) -> usize {
68 self.levels
69 }
70}
71
72impl Indicator for OrderBookImbalanceTopN {
73 type Input = OrderBook;
74 type Output = f64;
75
76 #[inline]
77 fn update(&mut self, book: OrderBook) -> Option<f64> {
78 self.has_emitted = true;
79 let bid_depth: f64 = book.bids.iter().take(self.levels).map(|l| l.size).sum();
80 let ask_depth: f64 = book.asks.iter().take(self.levels).map(|l| l.size).sum();
81 let total = bid_depth + ask_depth;
82 if total <= 0.0 {
83 return Some(0.0);
84 }
85 Some((bid_depth - ask_depth) / total)
86 }
87
88 fn reset(&mut self) {
89 self.has_emitted = false;
90 }
91
92 #[inline]
93 fn warmup_period(&self) -> usize {
94 1
95 }
96
97 #[inline]
98 fn is_ready(&self) -> bool {
99 self.has_emitted
100 }
101
102 #[inline]
103 fn name(&self) -> &'static str {
104 "OrderBookImbalanceTopN"
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::microstructure::Level;
112 use crate::traits::BatchExt;
113
114 fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
115 let to_levels = |xs: &[(f64, f64)]| {
116 xs.iter()
117 .map(|&(p, s)| Level::new(p, s).unwrap())
118 .collect::<Vec<_>>()
119 };
120 OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
121 }
122
123 #[test]
124 fn rejects_zero_levels() {
125 assert!(matches!(
126 OrderBookImbalanceTopN::new(0),
127 Err(Error::PeriodZero)
128 ));
129 }
130
131 #[test]
132 fn accessors_and_metadata() {
133 let obi = OrderBookImbalanceTopN::new(3).unwrap();
134 assert_eq!(obi.name(), "OrderBookImbalanceTopN");
135 assert_eq!(obi.warmup_period(), 1);
136 assert_eq!(obi.levels(), 3);
137 assert!(!obi.is_ready());
138 }
139
140 #[test]
141 fn sums_top_two_levels() {
142 let mut obi = OrderBookImbalanceTopN::new(2).unwrap();
143 let b = book(&[(100.0, 2.0), (99.0, 1.0)], &[(101.0, 1.0), (102.0, 1.0)]);
144 assert_eq!(obi.update(b), Some(0.2));
146 assert!(obi.is_ready());
147 }
148
149 #[test]
150 fn caps_at_available_depth() {
151 let mut obi = OrderBookImbalanceTopN::new(5).unwrap();
153 assert_eq!(
154 obi.update(book(&[(100.0, 3.0)], &[(101.0, 1.0)])),
155 Some(0.5)
156 );
157 }
158
159 #[test]
160 fn zero_size_is_zero() {
161 let mut obi = OrderBookImbalanceTopN::new(2).unwrap();
162 assert_eq!(
163 obi.update(book(&[(100.0, 0.0)], &[(101.0, 0.0)])),
164 Some(0.0)
165 );
166 }
167
168 #[test]
169 fn batch_equals_streaming() {
170 let books: Vec<OrderBook> = (0..20)
171 .map(|i| {
172 let ask = 1.0 + f64::from(i % 4);
173 book(&[(100.0, 2.0), (99.0, 1.0)], &[(101.0, ask), (102.0, 1.0)])
174 })
175 .collect();
176 let mut a = OrderBookImbalanceTopN::new(2).unwrap();
177 let mut b = OrderBookImbalanceTopN::new(2).unwrap();
178 assert_eq!(
179 a.batch(&books),
180 books
181 .iter()
182 .map(|x| b.update(x.clone()))
183 .collect::<Vec<_>>()
184 );
185 }
186
187 #[test]
188 fn reset_clears_state() {
189 let mut obi = OrderBookImbalanceTopN::new(2).unwrap();
190 obi.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)]));
191 assert!(obi.is_ready());
192 obi.reset();
193 assert!(!obi.is_ready());
194 }
195}