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