wickra_core/indicators/
trade_imbalance.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::microstructure::Trade;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
38pub struct TradeImbalance {
39 window: usize,
40 history: VecDeque<(f64, f64)>,
41 buy_sum: f64,
42 sell_sum: f64,
43}
44
45impl TradeImbalance {
46 pub fn new(window: usize) -> Result<Self> {
52 if window == 0 {
53 return Err(Error::PeriodZero);
54 }
55 Ok(Self {
56 window,
57 history: VecDeque::with_capacity(window),
58 buy_sum: 0.0,
59 sell_sum: 0.0,
60 })
61 }
62
63 pub fn window(&self) -> usize {
65 self.window
66 }
67}
68
69impl Indicator for TradeImbalance {
70 type Input = Trade;
71 type Output = f64;
72
73 fn update(&mut self, trade: Trade) -> Option<f64> {
74 let (buy, sell) = if trade.side.sign() > 0.0 {
75 (trade.size, 0.0)
76 } else {
77 (0.0, trade.size)
78 };
79 self.history.push_back((buy, sell));
80 self.buy_sum += buy;
81 self.sell_sum += sell;
82 if self.history.len() > self.window {
83 let (old_buy, old_sell) = self.history.pop_front().expect("window >= 1, len > window");
84 self.buy_sum -= old_buy;
85 self.sell_sum -= old_sell;
86 }
87 if self.history.len() < self.window {
88 return None;
89 }
90 let total = self.buy_sum + self.sell_sum;
91 if total <= 0.0 {
92 return Some(0.0);
93 }
94 Some((self.buy_sum - self.sell_sum) / total)
95 }
96
97 fn reset(&mut self) {
98 self.history.clear();
99 self.buy_sum = 0.0;
100 self.sell_sum = 0.0;
101 }
102
103 fn warmup_period(&self) -> usize {
104 self.window
105 }
106
107 fn is_ready(&self) -> bool {
108 self.history.len() >= self.window
109 }
110
111 fn name(&self) -> &'static str {
112 "TradeImbalance"
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::microstructure::Side;
120 use crate::traits::BatchExt;
121
122 fn trade(size: f64, side: Side, ts: i64) -> Trade {
123 Trade::new(100.0, size, side, ts).unwrap()
124 }
125
126 #[test]
127 fn rejects_zero_window() {
128 assert!(matches!(TradeImbalance::new(0), Err(Error::PeriodZero)));
129 }
130
131 #[test]
132 fn accessors_and_metadata() {
133 let ti = TradeImbalance::new(5).unwrap();
134 assert_eq!(ti.name(), "TradeImbalance");
135 assert_eq!(ti.warmup_period(), 5);
136 assert_eq!(ti.window(), 5);
137 assert!(!ti.is_ready());
138 }
139
140 #[test]
141 fn warms_up_then_emits() {
142 let mut ti = TradeImbalance::new(2).unwrap();
143 assert_eq!(ti.update(trade(3.0, Side::Buy, 0)), None);
144 assert!(!ti.is_ready());
145 assert_eq!(ti.update(trade(1.0, Side::Sell, 1)), Some(0.5));
147 assert!(ti.is_ready());
148 }
149
150 #[test]
151 fn rolls_off_old_trades() {
152 let mut ti = TradeImbalance::new(2).unwrap();
153 ti.update(trade(3.0, Side::Buy, 0));
154 ti.update(trade(1.0, Side::Sell, 1)); let out = ti.update(trade(5.0, Side::Buy, 2)).unwrap();
157 assert!((out - (4.0 / 6.0)).abs() < 1e-12);
158 }
159
160 #[test]
161 fn zero_volume_window_is_zero() {
162 let mut ti = TradeImbalance::new(2).unwrap();
163 ti.update(trade(0.0, Side::Buy, 0));
164 assert_eq!(ti.update(trade(0.0, Side::Sell, 1)), Some(0.0));
165 }
166
167 #[test]
168 fn batch_equals_streaming() {
169 let trades: Vec<Trade> = (0..30)
170 .map(|i| {
171 let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
172 trade(1.0 + (i % 5) as f64, side, i)
173 })
174 .collect();
175 let mut a = TradeImbalance::new(5).unwrap();
176 let mut b = TradeImbalance::new(5).unwrap();
177 assert_eq!(
178 a.batch(&trades),
179 trades.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
180 );
181 }
182
183 #[test]
184 fn reset_clears_state() {
185 let mut ti = TradeImbalance::new(2).unwrap();
186 ti.update(trade(3.0, Side::Buy, 0));
187 ti.update(trade(1.0, Side::Sell, 1));
188 assert!(ti.is_ready());
189 ti.reset();
190 assert!(!ti.is_ready());
191 assert_eq!(ti.update(trade(2.0, Side::Buy, 2)), None);
192 }
193}