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