rusty_backtest/features/
queue.rs

1//! Queue Analysis - Queue imbalance and position modeling
2//!
3//! Implements queue-based microstructural features for order book analysis.
4
5use super::{Level, decimal_to_f64_or_nan};
6use rust_decimal::prelude::*;
7use smallvec::SmallVec;
8
9/// Calculate basic queue imbalance at best bid/ask
10///
11/// Returns normalized imbalance (-1.0 to 1.0) where:
12/// - Positive values indicate bid quantity dominance
13/// - Negative values indicate ask quantity dominance
14#[inline(always)]
15pub fn calculate_queue_imbalance(
16    bids: &SmallVec<[Level; 25]>,
17    asks: &SmallVec<[Level; 25]>,
18) -> f64 {
19    if let (Some(best_bid), Some(best_ask)) = (bids.first(), asks.first()) {
20        let total_qty = best_bid.quantity + best_ask.quantity;
21        if total_qty > Decimal::ZERO {
22            decimal_to_f64_or_nan((best_bid.quantity - best_ask.quantity) / total_qty)
23        } else {
24            0.0
25        }
26    } else {
27        0.0
28    }
29}
30
31/// Calculate weighted queue imbalance across multiple levels
32///
33/// Uses distance-based weighting where closer levels have higher impact.
34#[inline(always)]
35pub fn calculate_weighted_queue_imbalance(
36    bids: &SmallVec<[Level; 25]>,
37    asks: &SmallVec<[Level; 25]>,
38    max_levels: usize,
39) -> f64 {
40    let levels = max_levels.min(bids.len()).min(asks.len());
41    if levels == 0 {
42        return 0.0;
43    }
44
45    let mut weighted_bid_qty = 0.0;
46    let mut weighted_ask_qty = 0.0;
47    let mut _total_weight = 0.0;
48
49    for i in 0..levels {
50        let weight = 1.0 / (i as f64 + 1.0); // Decreasing weight by distance
51        weighted_bid_qty += decimal_to_f64_or_nan(bids[i].quantity) * weight;
52        weighted_ask_qty += decimal_to_f64_or_nan(asks[i].quantity) * weight;
53        _total_weight += weight;
54    }
55
56    let total_weighted_qty = weighted_bid_qty + weighted_ask_qty;
57    if total_weighted_qty > 0.0 {
58        (weighted_bid_qty - weighted_ask_qty) / total_weighted_qty
59    } else {
60        0.0
61    }
62}
63
64/// Queue Analyzer for comprehensive queue metrics
65pub struct QueueAnalyzer {
66    imbalance_history: Vec<f64>,
67    window_size: usize,
68}
69
70impl QueueAnalyzer {
71    /// Create new queue analyzer
72    #[must_use]
73    pub fn new(window_size: usize) -> Self {
74        Self {
75            imbalance_history: Vec::with_capacity(window_size * 2),
76            window_size,
77        }
78    }
79
80    /// Add queue imbalance measurement
81    pub fn add_measurement(&mut self, imbalance: f64) {
82        self.imbalance_history.push(imbalance);
83
84        // Maintain window size
85        if self.imbalance_history.len() > self.window_size * 2 {
86            let drain_count = self.window_size;
87            self.imbalance_history.drain(0..drain_count);
88        }
89    }
90
91    /// Get average imbalance over window
92    pub fn get_average_imbalance(&self) -> f64 {
93        if self.imbalance_history.is_empty() {
94            return 0.0;
95        }
96
97        let recent_window = &self.imbalance_history[self
98            .imbalance_history
99            .len()
100            .saturating_sub(self.window_size)..];
101        recent_window.iter().sum::<f64>() / recent_window.len() as f64
102    }
103
104    /// Get imbalance volatility (standard deviation)
105    pub fn get_imbalance_volatility(&self) -> f64 {
106        if self.imbalance_history.len() < 2 {
107            return 0.0;
108        }
109
110        let recent_window = &self.imbalance_history[self
111            .imbalance_history
112            .len()
113            .saturating_sub(self.window_size)..];
114        let mean = self.get_average_imbalance();
115
116        let variance = recent_window
117            .iter()
118            .map(|&x| (x - mean).powi(2))
119            .sum::<f64>()
120            / recent_window.len() as f64;
121
122        variance.sqrt()
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use rust_decimal_macros::dec;
130
131    #[test]
132    fn test_queue_imbalance_balanced() {
133        let bids = smallvec::smallvec![Level {
134            price: dec!(49999),
135            quantity: dec!(10.0),
136            order_count: 1
137        }];
138        let asks = smallvec::smallvec![Level {
139            price: dec!(50001),
140            quantity: dec!(10.0),
141            order_count: 1
142        }];
143
144        let imbalance = calculate_queue_imbalance(&bids, &asks);
145        assert_eq!(imbalance, 0.0); // Balanced should give 0
146    }
147
148    #[test]
149    fn test_queue_imbalance_bid_heavy() {
150        let bids = smallvec::smallvec![Level {
151            price: dec!(49999),
152            quantity: dec!(15.0),
153            order_count: 1
154        }];
155        let asks = smallvec::smallvec![Level {
156            price: dec!(50001),
157            quantity: dec!(5.0),
158            order_count: 1
159        }];
160
161        let imbalance = calculate_queue_imbalance(&bids, &asks);
162        assert_eq!(imbalance, 0.5); // (15-5)/(15+5) = 0.5
163    }
164
165    #[test]
166    fn test_queue_imbalance_ask_heavy() {
167        let bids = smallvec::smallvec![Level {
168            price: dec!(49999),
169            quantity: dec!(5.0),
170            order_count: 1
171        }];
172        let asks = smallvec::smallvec![Level {
173            price: dec!(50001),
174            quantity: dec!(15.0),
175            order_count: 1
176        }];
177
178        let imbalance = calculate_queue_imbalance(&bids, &asks);
179        assert_eq!(imbalance, -0.5); // (5-15)/(5+15) = -0.5
180    }
181
182    #[test]
183    fn test_weighted_queue_imbalance_single_level() {
184        let bids = smallvec::smallvec![Level {
185            price: dec!(49999),
186            quantity: dec!(10.0),
187            order_count: 1
188        }];
189        let asks = smallvec::smallvec![Level {
190            price: dec!(50001),
191            quantity: dec!(10.0),
192            order_count: 1
193        }];
194
195        let imbalance = calculate_weighted_queue_imbalance(&bids, &asks, 1);
196        assert_eq!(imbalance, 0.0); // Balanced at first level
197    }
198
199    #[test]
200    fn test_weighted_queue_imbalance_multi_level() {
201        let bids = smallvec::smallvec![
202            Level {
203                price: dec!(49999),
204                quantity: dec!(10.0),
205                order_count: 1
206            },
207            Level {
208                price: dec!(49998),
209                quantity: dec!(20.0),
210                order_count: 2
211            },
212            Level {
213                price: dec!(49997),
214                quantity: dec!(30.0),
215                order_count: 3
216            }
217        ];
218        let asks = smallvec::smallvec![
219            Level {
220                price: dec!(50001),
221                quantity: dec!(10.0),
222                order_count: 1
223            },
224            Level {
225                price: dec!(50002),
226                quantity: dec!(20.0),
227                order_count: 2
228            },
229            Level {
230                price: dec!(50003),
231                quantity: dec!(30.0),
232                order_count: 3
233            }
234        ];
235
236        let imbalance = calculate_weighted_queue_imbalance(&bids, &asks, 3);
237        assert_eq!(imbalance, 0.0); // Symmetric book should be balanced
238    }
239
240    #[test]
241    fn test_weighted_queue_imbalance_bid_heavy() {
242        let bids = smallvec::smallvec![
243            Level {
244                price: dec!(49999),
245                quantity: dec!(20.0),
246                order_count: 1
247            },
248            Level {
249                price: dec!(49998),
250                quantity: dec!(30.0),
251                order_count: 2
252            }
253        ];
254        let asks = smallvec::smallvec![
255            Level {
256                price: dec!(50001),
257                quantity: dec!(10.0),
258                order_count: 1
259            },
260            Level {
261                price: dec!(50002),
262                quantity: dec!(15.0),
263                order_count: 2
264            }
265        ];
266
267        let imbalance = calculate_weighted_queue_imbalance(&bids, &asks, 2);
268        // Weight 1: 20 * 1.0 = 20, 10 * 1.0 = 10
269        // Weight 0.5: 30 * 0.5 = 15, 15 * 0.5 = 7.5
270        // Total bid: 35, Total ask: 17.5
271        // (35 - 17.5) / (35 + 17.5) = 17.5 / 52.5 = 0.333...
272        assert!((imbalance - 0.333_333_333_333_333_3).abs() < 1e-10);
273    }
274
275    #[test]
276    fn test_weighted_queue_imbalance_empty_book() {
277        let bids: SmallVec<[Level; 25]> = smallvec::smallvec![];
278        let asks: SmallVec<[Level; 25]> = smallvec::smallvec![];
279
280        let imbalance = calculate_weighted_queue_imbalance(&bids, &asks, 5);
281        assert_eq!(imbalance, 0.0); // Empty book returns 0
282    }
283
284    #[test]
285    fn test_weighted_queue_imbalance_one_sided() {
286        let bids = smallvec::smallvec![Level {
287            price: dec!(49999),
288            quantity: dec!(10.0),
289            order_count: 1
290        }];
291        let asks: SmallVec<[Level; 25]> = smallvec::smallvec![];
292
293        let imbalance = calculate_weighted_queue_imbalance(&bids, &asks, 1);
294        assert_eq!(imbalance, 0.0); // One-sided book returns 0
295    }
296
297    #[test]
298    fn test_weighted_queue_imbalance_max_levels_exceeds_book() {
299        let bids = smallvec::smallvec![Level {
300            price: dec!(49999),
301            quantity: dec!(10.0),
302            order_count: 1
303        }];
304        let asks = smallvec::smallvec![Level {
305            price: dec!(50001),
306            quantity: dec!(20.0),
307            order_count: 1
308        }];
309
310        // Request 10 levels but only have 1
311        let imbalance = calculate_weighted_queue_imbalance(&bids, &asks, 10);
312        // Should only use available level: (10 - 20) / (10 + 20) = -10/30 = -0.333...
313        assert!((imbalance - (-0.333_333_333_333_333_3)).abs() < 1e-10);
314    }
315}