rusty_backtest/features/
liquidity.rs1use super::{Level, OrderBookSnapshot, decimal_to_f64_or_nan};
6use smallvec::SmallVec;
7
8pub fn calculate_total_liquidity(
10 bids: &SmallVec<[Level; 25]>,
11 asks: &SmallVec<[Level; 25]>,
12 max_levels: usize,
13) -> f64 {
14 let levels = max_levels.min(bids.len()).min(asks.len());
15
16 let bid_liquidity: f64 = bids
17 .iter()
18 .take(levels)
19 .map(|l| decimal_to_f64_or_nan(l.quantity))
20 .sum();
21 let ask_liquidity: f64 = asks
22 .iter()
23 .take(levels)
24 .map(|l| decimal_to_f64_or_nan(l.quantity))
25 .sum();
26
27 bid_liquidity + ask_liquidity
28}
29
30pub fn calculate_liquidity_imbalance(
32 bids: &SmallVec<[Level; 25]>,
33 asks: &SmallVec<[Level; 25]>,
34 max_levels: usize,
35) -> f64 {
36 let levels = max_levels.min(bids.len()).min(asks.len());
37
38 let bid_liquidity: f64 = bids
39 .iter()
40 .take(levels)
41 .map(|l| decimal_to_f64_or_nan(l.quantity))
42 .sum();
43 let ask_liquidity: f64 = asks
44 .iter()
45 .take(levels)
46 .map(|l| decimal_to_f64_or_nan(l.quantity))
47 .sum();
48 let total_liquidity = bid_liquidity + ask_liquidity;
49
50 if total_liquidity > 0.0 {
51 (bid_liquidity - ask_liquidity) / total_liquidity
52 } else {
53 0.0
54 }
55}
56
57pub fn calculate_liquidity_shocks(
59 prev_book: &OrderBookSnapshot,
60 curr_book: &OrderBookSnapshot,
61 max_levels: usize,
62) -> f64 {
63 let prev_liquidity = calculate_total_liquidity(&prev_book.bids, &prev_book.asks, max_levels);
64 let curr_liquidity = calculate_total_liquidity(&curr_book.bids, &curr_book.asks, max_levels);
65
66 if prev_liquidity > 0.0 {
67 (curr_liquidity - prev_liquidity) / prev_liquidity
68 } else {
69 0.0
70 }
71}
72
73pub struct LiquidityAnalyzer {
75 liquidity_history: Vec<f64>,
76 shock_history: Vec<f64>,
77 window_size: usize,
78}
79
80impl LiquidityAnalyzer {
81 #[must_use]
83 pub fn new(window_size: usize) -> Self {
84 Self {
85 liquidity_history: Vec::with_capacity(window_size * 2),
86 shock_history: Vec::with_capacity(window_size * 2),
87 window_size,
88 }
89 }
90
91 pub fn process_orderbook_update(&mut self, book: &OrderBookSnapshot, max_levels: usize) {
93 let total_liquidity = calculate_total_liquidity(&book.bids, &book.asks, max_levels);
94 self.liquidity_history.push(total_liquidity);
95
96 if self.liquidity_history.len() > self.window_size * 2 {
98 let drain_count = self.window_size;
99 self.liquidity_history.drain(0..drain_count);
100 }
101 }
102
103 pub fn add_shock(&mut self, shock: f64) {
105 self.shock_history.push(shock);
106
107 if self.shock_history.len() > self.window_size * 2 {
109 let drain_count = self.window_size;
110 self.shock_history.drain(0..drain_count);
111 }
112 }
113
114 pub fn get_average_liquidity(&self) -> f64 {
116 if self.liquidity_history.is_empty() {
117 return 0.0;
118 }
119
120 let recent_window = &self.liquidity_history[self
121 .liquidity_history
122 .len()
123 .saturating_sub(self.window_size)..];
124 recent_window.iter().sum::<f64>() / recent_window.len() as f64
125 }
126
127 pub fn get_liquidity_volatility(&self) -> f64 {
129 if self.liquidity_history.len() < 2 {
130 return 0.0;
131 }
132
133 let recent_window = &self.liquidity_history[self
134 .liquidity_history
135 .len()
136 .saturating_sub(self.window_size)..];
137 let mean = self.get_average_liquidity();
138
139 let variance = recent_window
140 .iter()
141 .map(|&x| (x - mean).powi(2))
142 .sum::<f64>()
143 / recent_window.len() as f64;
144
145 variance.sqrt()
146 }
147
148 pub fn get_shock_frequency(&self, threshold: f64) -> f64 {
150 if self.shock_history.is_empty() {
151 return 0.0;
152 }
153
154 let recent_window =
155 &self.shock_history[self.shock_history.len().saturating_sub(self.window_size)..];
156
157 let significant_shocks = recent_window
158 .iter()
159 .filter(|&&shock| shock.abs() > threshold)
160 .count();
161
162 significant_shocks as f64 / recent_window.len() as f64
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use rust_decimal::{Decimal, prelude::FromPrimitive};
170 use rust_decimal_macros::dec;
171
172 fn create_test_levels(quantities: &[f64]) -> SmallVec<[Level; 25]> {
173 quantities
174 .iter()
175 .enumerate()
176 .map(|(i, &qty)| Level {
177 price: dec!(50000) + rust_decimal::Decimal::from(i),
178 quantity: Decimal::from_f64(qty).unwrap_or(Decimal::ZERO),
179 order_count: 1,
180 })
181 .collect()
182 }
183
184 #[test]
185 fn test_total_liquidity() {
186 let bids = create_test_levels(&[10.0, 8.0, 6.0]);
187 let asks = create_test_levels(&[12.0, 9.0, 7.0]);
188
189 let total_liquidity = calculate_total_liquidity(&bids, &asks, 3);
190 assert_eq!(total_liquidity, 52.0); }
192
193 #[test]
194 fn test_liquidity_imbalance() {
195 let bids = create_test_levels(&[15.0, 10.0]);
196 let asks = create_test_levels(&[5.0, 10.0]);
197
198 let imbalance = calculate_liquidity_imbalance(&bids, &asks, 2);
199 assert_eq!(imbalance, 0.25); }
201
202 #[test]
203 fn test_liquidity_shock() {
204 let prev_bids = create_test_levels(&[10.0, 10.0]);
205 let prev_asks = create_test_levels(&[10.0, 10.0]);
206 let prev_book = OrderBookSnapshot {
207 timestamp_ns: 1000000000,
208 symbol: "BTC-USD".into(),
209 bids: prev_bids,
210 asks: prev_asks,
211 };
212
213 let curr_bids = create_test_levels(&[15.0, 15.0]);
214 let curr_asks = create_test_levels(&[15.0, 15.0]);
215 let curr_book = OrderBookSnapshot {
216 timestamp_ns: 1000000001,
217 symbol: "BTC-USD".into(),
218 bids: curr_bids,
219 asks: curr_asks,
220 };
221
222 let shock = calculate_liquidity_shocks(&prev_book, &curr_book, 2);
223 assert_eq!(shock, 0.5); }
225}