rusty_backtest/features/
liquidity.rs

1//! Liquidity Analysis - Market depth and liquidity metrics
2//!
3//! Implements liquidity-based microstructural features for market analysis.
4
5use super::{Level, OrderBookSnapshot, decimal_to_f64_or_nan};
6use smallvec::SmallVec;
7
8/// Calculate total liquidity within N levels
9pub 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
30/// Calculate liquidity imbalance
31pub 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
57/// Detect liquidity shocks (sudden changes in market depth)
58pub 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
73/// Liquidity analyzer for comprehensive liquidity metrics
74pub struct LiquidityAnalyzer {
75    liquidity_history: Vec<f64>,
76    shock_history: Vec<f64>,
77    window_size: usize,
78}
79
80impl LiquidityAnalyzer {
81    /// Create new liquidity analyzer
82    #[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    /// Process order book update
92    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        // Maintain window size
97        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    /// Add liquidity shock measurement
104    pub fn add_shock(&mut self, shock: f64) {
105        self.shock_history.push(shock);
106
107        // Maintain window size
108        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    /// Get average liquidity over window
115    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    /// Get liquidity volatility
128    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    /// Get shock frequency (proportion of significant shocks)
149    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); // 10+8+6+12+9+7 = 52
191    }
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); // (25-15)/(25+15) = 0.25
200    }
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); // (60-40)/40 = 0.5 (50% increase)
224    }
225}