1use super::{OrderBookSnapshot, decimal_to_f64_or_nan};
7
8#[derive(Debug, Clone)]
13pub struct AsymmetryIndexCalculator {
14 max_levels: usize,
16 #[allow(dead_code)]
18 volume_weighted: bool,
19 distance_decay: f64,
21 window_size: usize,
23 history: Vec<f64>,
25 buffer_position: usize,
27 samples_count: usize,
29}
30
31impl AsymmetryIndexCalculator {
32 #[must_use]
40 pub fn new(
41 max_levels: usize,
42 volume_weighted: bool,
43 distance_decay: f64,
44 window_size: usize,
45 ) -> Self {
46 Self {
47 max_levels,
48 volume_weighted,
49 distance_decay: distance_decay.clamp(0.0, 1.0),
50 window_size,
51 history: vec![0.0; window_size],
52 buffer_position: 0,
53 samples_count: 0,
54 }
55 }
56
57 pub fn calculate(&mut self, snapshot: &OrderBookSnapshot) -> AsymmetryMetrics {
59 let basic_asymmetry = self.calculate_basic_asymmetry(snapshot);
61
62 let weighted_asymmetry = self.calculate_weighted_asymmetry(snapshot);
64
65 let volume_asymmetry = self.calculate_volume_asymmetry(snapshot);
67
68 let order_count_asymmetry = self.calculate_order_count_asymmetry(snapshot);
70
71 self.history[self.buffer_position] = weighted_asymmetry;
73 self.buffer_position = (self.buffer_position + 1) % self.window_size;
74 self.samples_count = self.samples_count.saturating_add(1).min(self.window_size);
75
76 let smoothed_asymmetry = if self.samples_count > 0 {
78 self.history.iter().take(self.samples_count).sum::<f64>() / self.samples_count as f64
79 } else {
80 weighted_asymmetry
81 };
82
83 AsymmetryMetrics {
84 basic_asymmetry,
85 weighted_asymmetry,
86 volume_asymmetry,
87 order_count_asymmetry,
88 smoothed_asymmetry,
89 directional_pressure: self.calculate_directional_pressure(smoothed_asymmetry),
90 }
91 }
92
93 fn calculate_basic_asymmetry(&self, snapshot: &OrderBookSnapshot) -> f64 {
95 let levels = self
96 .max_levels
97 .min(snapshot.bids.len())
98 .min(snapshot.asks.len());
99 if levels == 0 {
100 return 0.0;
101 }
102
103 let mid_price = decimal_to_f64_or_nan(snapshot.mid_price());
104 if mid_price <= 0.0 {
105 return 0.0;
106 }
107
108 let mut bid_score = 0.0;
109 let mut ask_score = 0.0;
110
111 for i in 0..levels {
112 let bid_price = decimal_to_f64_or_nan(snapshot.bids[i].price);
113 let ask_price = decimal_to_f64_or_nan(snapshot.asks[i].price);
114
115 let bid_distance = (mid_price - bid_price).abs() / mid_price;
117 let ask_distance = (ask_price - mid_price).abs() / mid_price;
118
119 bid_score += 1.0 / (1.0 + bid_distance);
121 ask_score += 1.0 / (1.0 + ask_distance);
122 }
123
124 (bid_score - ask_score) / (bid_score + ask_score)
126 }
127
128 fn calculate_weighted_asymmetry(&self, snapshot: &OrderBookSnapshot) -> f64 {
130 let levels = self
131 .max_levels
132 .min(snapshot.bids.len())
133 .min(snapshot.asks.len());
134 if levels == 0 {
135 return 0.0;
136 }
137
138 let mid_price = decimal_to_f64_or_nan(snapshot.mid_price());
139 if mid_price <= 0.0 {
140 return 0.0;
141 }
142
143 let mut bid_weighted_volume = 0.0;
144 let mut ask_weighted_volume = 0.0;
145
146 for i in 0..levels {
147 let bid = &snapshot.bids[i];
148 let ask = &snapshot.asks[i];
149
150 let bid_price = decimal_to_f64_or_nan(bid.price);
151 let ask_price = decimal_to_f64_or_nan(ask.price);
152
153 let bid_distance = ((mid_price - bid_price) / mid_price).abs();
155 let ask_distance = ((ask_price - mid_price) / mid_price).abs();
156
157 let bid_weight = self.distance_decay.powf(bid_distance * 100.0);
159 let ask_weight = self.distance_decay.powf(ask_distance * 100.0);
160
161 bid_weighted_volume += decimal_to_f64_or_nan(bid.quantity) * bid_weight;
162 ask_weighted_volume += decimal_to_f64_or_nan(ask.quantity) * ask_weight;
163 }
164
165 if bid_weighted_volume + ask_weighted_volume == 0.0 {
166 return 0.0;
167 }
168
169 (bid_weighted_volume - ask_weighted_volume) / (bid_weighted_volume + ask_weighted_volume)
171 }
172
173 fn calculate_volume_asymmetry(&self, snapshot: &OrderBookSnapshot) -> f64 {
175 let levels = self
176 .max_levels
177 .min(snapshot.bids.len())
178 .min(snapshot.asks.len());
179 if levels == 0 {
180 return 0.0;
181 }
182
183 let mut total_bid_volume = 0.0;
184 let mut total_ask_volume = 0.0;
185
186 for i in 0..levels {
187 total_bid_volume += decimal_to_f64_or_nan(snapshot.bids[i].quantity);
188 total_ask_volume += decimal_to_f64_or_nan(snapshot.asks[i].quantity);
189 }
190
191 if total_bid_volume + total_ask_volume == 0.0 {
192 return 0.0;
193 }
194
195 (total_bid_volume - total_ask_volume) / (total_bid_volume + total_ask_volume)
196 }
197
198 fn calculate_order_count_asymmetry(&self, snapshot: &OrderBookSnapshot) -> f64 {
200 let levels = self
201 .max_levels
202 .min(snapshot.bids.len())
203 .min(snapshot.asks.len());
204 if levels == 0 {
205 return 0.0;
206 }
207
208 let mut total_bid_orders = 0u32;
209 let mut total_ask_orders = 0u32;
210
211 for i in 0..levels {
212 total_bid_orders += snapshot.bids[i].order_count;
213 total_ask_orders += snapshot.asks[i].order_count;
214 }
215
216 if total_bid_orders + total_ask_orders == 0 {
217 return 0.0;
218 }
219
220 let bid_orders = total_bid_orders as f64;
221 let ask_orders = total_ask_orders as f64;
222
223 (bid_orders - ask_orders) / (bid_orders + ask_orders)
224 }
225
226 fn calculate_directional_pressure(&self, smoothed_asymmetry: f64) -> DirectionalPressure {
228 if smoothed_asymmetry > 0.3 {
229 DirectionalPressure::StrongBullish
230 } else if smoothed_asymmetry > 0.1 {
231 DirectionalPressure::WeakBullish
232 } else if smoothed_asymmetry < -0.3 {
233 DirectionalPressure::StrongBearish
234 } else if smoothed_asymmetry < -0.1 {
235 DirectionalPressure::WeakBearish
236 } else {
237 DirectionalPressure::Neutral
238 }
239 }
240
241 pub fn reset(&mut self) {
243 self.history.fill(0.0);
244 self.buffer_position = 0;
245 self.samples_count = 0;
246 }
247}
248
249#[derive(Debug, Clone, Copy)]
251pub struct AsymmetryMetrics {
252 pub basic_asymmetry: f64,
254 pub weighted_asymmetry: f64,
256 pub volume_asymmetry: f64,
258 pub order_count_asymmetry: f64,
260 pub smoothed_asymmetry: f64,
262 pub directional_pressure: DirectionalPressure,
264}
265
266#[derive(Debug, Clone, Copy, PartialEq)]
268pub enum DirectionalPressure {
269 StrongBullish,
271 WeakBullish,
273 Neutral,
275 WeakBearish,
277 StrongBearish,
279}
280
281#[derive(Debug, Clone)]
283pub struct MultiTimeframeAsymmetry {
284 short_term: AsymmetryIndexCalculator,
286 medium_term: AsymmetryIndexCalculator,
288 long_term: AsymmetryIndexCalculator,
290}
291
292impl MultiTimeframeAsymmetry {
293 #[must_use]
295 pub fn new(max_levels: usize, distance_decay: f64) -> Self {
296 Self {
297 short_term: AsymmetryIndexCalculator::new(max_levels, true, distance_decay, 10),
298 medium_term: AsymmetryIndexCalculator::new(max_levels, true, distance_decay, 50),
299 long_term: AsymmetryIndexCalculator::new(max_levels, true, distance_decay, 200),
300 }
301 }
302
303 pub fn update(&mut self, snapshot: &OrderBookSnapshot) -> MultiTimeframeMetrics {
305 let short_metrics = self.short_term.calculate(snapshot);
306 let medium_metrics = self.medium_term.calculate(snapshot);
307 let long_metrics = self.long_term.calculate(snapshot);
308
309 let short_medium_divergence =
311 (short_metrics.smoothed_asymmetry - medium_metrics.smoothed_asymmetry).abs();
312 let medium_long_divergence =
313 (medium_metrics.smoothed_asymmetry - long_metrics.smoothed_asymmetry).abs();
314
315 let trend_aligned = short_metrics.smoothed_asymmetry.signum()
317 == medium_metrics.smoothed_asymmetry.signum()
318 && medium_metrics.smoothed_asymmetry.signum()
319 == long_metrics.smoothed_asymmetry.signum();
320
321 MultiTimeframeMetrics {
322 short_term: short_metrics,
323 medium_term: medium_metrics,
324 long_term: long_metrics,
325 short_medium_divergence,
326 medium_long_divergence,
327 trend_aligned,
328 composite_signal: self.calculate_composite_signal(
329 &short_metrics,
330 &medium_metrics,
331 &long_metrics,
332 ),
333 }
334 }
335
336 fn calculate_composite_signal(
338 &self,
339 short: &AsymmetryMetrics,
340 medium: &AsymmetryMetrics,
341 long: &AsymmetryMetrics,
342 ) -> f64 {
343 0.5 * short.smoothed_asymmetry
345 + 0.3 * medium.smoothed_asymmetry
346 + 0.2 * long.smoothed_asymmetry
347 }
348}
349
350#[derive(Debug, Clone)]
352pub struct MultiTimeframeMetrics {
353 pub short_term: AsymmetryMetrics,
355 pub medium_term: AsymmetryMetrics,
357 pub long_term: AsymmetryMetrics,
359 pub short_medium_divergence: f64,
361 pub medium_long_divergence: f64,
363 pub trend_aligned: bool,
365 pub composite_signal: f64,
367}
368
369pub fn calculate_asymmetry_index(snapshot: &OrderBookSnapshot, levels: usize) -> f64 {
371 let n_levels = levels.min(snapshot.bids.len()).min(snapshot.asks.len());
372 if n_levels == 0 {
373 return 0.0;
374 }
375
376 let mut bid_volume = 0.0;
377 let mut ask_volume = 0.0;
378
379 for i in 0..n_levels {
380 bid_volume += decimal_to_f64_or_nan(snapshot.bids[i].quantity);
381 ask_volume += decimal_to_f64_or_nan(snapshot.asks[i].quantity);
382 }
383
384 if bid_volume + ask_volume == 0.0 {
385 return 0.0;
386 }
387
388 (bid_volume - ask_volume) / (bid_volume + ask_volume)
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::features::Level;
395 use rust_decimal_macros::dec;
396 use smallvec::smallvec;
397
398 fn create_test_snapshot() -> OrderBookSnapshot {
399 OrderBookSnapshot {
400 symbol: "TEST".into(),
401 timestamp_ns: 1_000_000_000,
402 bids: smallvec![
403 Level {
404 price: dec!(99.9),
405 quantity: dec!(100.0),
406 order_count: 5
407 },
408 Level {
409 price: dec!(99.8),
410 quantity: dec!(150.0),
411 order_count: 7
412 },
413 Level {
414 price: dec!(99.7),
415 quantity: dec!(200.0),
416 order_count: 10
417 },
418 ],
419 asks: smallvec![
420 Level {
421 price: dec!(100.1),
422 quantity: dec!(80.0),
423 order_count: 4
424 },
425 Level {
426 price: dec!(100.2),
427 quantity: dec!(120.0),
428 order_count: 6
429 },
430 Level {
431 price: dec!(100.3),
432 quantity: dec!(180.0),
433 order_count: 9
434 },
435 ],
436 }
437 }
438
439 #[test]
440 fn test_basic_asymmetry() {
441 let mut calculator = AsymmetryIndexCalculator::new(3, false, 0.95, 10);
442 let snapshot = create_test_snapshot();
443
444 let metrics = calculator.calculate(&snapshot);
445
446 assert!(metrics.volume_asymmetry > 0.0);
448
449 assert!(metrics.order_count_asymmetry > 0.0);
451 }
452
453 #[test]
454 fn test_directional_pressure() {
455 let mut calculator = AsymmetryIndexCalculator::new(3, true, 0.95, 10);
456
457 let mut snapshot = create_test_snapshot();
459 snapshot.bids[0].quantity = dec!(500.0);
460 snapshot.bids[1].quantity = dec!(400.0);
461
462 let metrics = calculator.calculate(&snapshot);
463 assert!(matches!(
464 metrics.directional_pressure,
465 DirectionalPressure::StrongBullish
466 ));
467 }
468
469 #[test]
470 fn test_multi_timeframe() {
471 let mut analyzer = MultiTimeframeAsymmetry::new(3, 0.95);
472 let snapshot = create_test_snapshot();
473
474 let mtf_metrics = analyzer.update(&snapshot);
475
476 assert!(
478 (mtf_metrics.short_term.volume_asymmetry - mtf_metrics.medium_term.volume_asymmetry)
479 .abs()
480 < 0.1
481 );
482 }
483
484 #[test]
485 fn test_simple_asymmetry_function() {
486 let snapshot = create_test_snapshot();
487 let asymmetry = calculate_asymmetry_index(&snapshot, 3);
488
489 assert!((asymmetry - 0.084).abs() < 0.01);
492 }
493}