1use super::{Level, OrderBookSnapshot, TradeSide, TradeTick, decimal_to_f64_or_nan};
15use rust_decimal::Decimal;
16use rust_decimal_macros::dec;
17use smallvec::SmallVec;
18use std::collections::VecDeque;
19
20#[inline(always)]
24pub fn calculate_weighted_order_imbalance(
25 bids: &SmallVec<[Level; 25]>,
26 asks: &SmallVec<[Level; 25]>,
27 depth: usize,
28) -> f64 {
29 let depth = depth.min(bids.len()).min(asks.len());
30 if depth == 0 {
31 return f64::NAN;
32 }
33
34 let mut bid_qty_sum = Decimal::ZERO;
35 let mut ask_qty_sum = Decimal::ZERO;
36 let mut bid_weighted = Decimal::ZERO;
37 let mut ask_weighted = Decimal::ZERO;
38
39 for i in 0..depth {
40 let weight = Decimal::from((depth - i) as u64);
41
42 if i < bids.len() {
43 bid_qty_sum += bids[i].quantity;
44 bid_weighted += bids[i].quantity * weight;
45 }
46
47 if i < asks.len() {
48 ask_qty_sum += asks[i].quantity;
49 ask_weighted += asks[i].quantity * weight;
50 }
51 }
52
53 let total_sum = bid_qty_sum + ask_qty_sum;
54 if total_sum == Decimal::ZERO {
55 return 0.0;
56 }
57
58 decimal_to_f64_or_nan((bid_weighted - ask_weighted) / total_sum)
59}
60
61#[inline(always)]
65pub fn calculate_relative_spread(
66 bids: &SmallVec<[Level; 25]>,
67 asks: &SmallVec<[Level; 25]>,
68) -> f64 {
69 if bids.is_empty() || asks.is_empty() {
70 return f64::NAN;
71 }
72
73 let best_bid = bids[0].price;
74 let best_ask = asks[0].price;
75 let mid_price = (best_bid + best_ask) / dec!(2);
76
77 if mid_price == Decimal::ZERO {
78 return f64::NAN;
79 }
80
81 decimal_to_f64_or_nan((best_ask - best_bid) / mid_price)
82}
83
84pub fn calculate_volume_weighted_ofi(
88 bids: &SmallVec<[Level; 25]>,
89 asks: &SmallVec<[Level; 25]>,
90 depth: usize,
91) -> f64 {
92 let depth = depth.min(bids.len()).min(asks.len());
93 if depth == 0 {
94 return f64::NAN;
95 }
96
97 let mut bid_vwap = Decimal::ZERO;
98 let mut ask_vwap = Decimal::ZERO;
99 let mut bid_qty_sum = Decimal::ZERO;
100 let mut ask_qty_sum = Decimal::ZERO;
101
102 for i in 0..depth.min(bids.len()) {
104 bid_qty_sum += bids[i].quantity;
105 }
106
107 if bid_qty_sum > Decimal::ZERO {
108 for i in 0..depth.min(bids.len()) {
109 bid_vwap += bids[i].price * bids[i].quantity / bid_qty_sum;
110 }
111 }
112
113 for i in 0..depth.min(asks.len()) {
115 ask_qty_sum += asks[i].quantity;
116 }
117
118 if ask_qty_sum > Decimal::ZERO {
119 for i in 0..depth.min(asks.len()) {
120 ask_vwap += asks[i].price * asks[i].quantity / ask_qty_sum;
121 }
122 }
123
124 decimal_to_f64_or_nan(bid_vwap - ask_vwap)
125}
126
127#[inline(always)]
131pub fn calculate_depth_weighted_ofi(
132 bids: &SmallVec<[Level; 25]>,
133 asks: &SmallVec<[Level; 25]>,
134 depth: usize,
135) -> f64 {
136 let depth = depth.min(bids.len()).min(asks.len());
137 if depth == 0 {
138 return f64::NAN;
139 }
140
141 let mut bid_depth_weighted = Decimal::ZERO;
142 let mut ask_depth_weighted = Decimal::ZERO;
143
144 for i in 0..depth {
145 let weight = Decimal::from((depth - i) as u64);
146
147 if i < bids.len() {
148 bid_depth_weighted += bids[i].price * weight;
149 }
150
151 if i < asks.len() {
152 ask_depth_weighted += asks[i].price * weight;
153 }
154 }
155
156 decimal_to_f64_or_nan(bid_depth_weighted - ask_depth_weighted)
157}
158
159#[inline(always)]
163pub fn calculate_liquidity_shock_ratio(
164 bids: &SmallVec<[Level; 25]>,
165 asks: &SmallVec<[Level; 25]>,
166 top_levels: usize,
167) -> f64 {
168 if bids.is_empty() && asks.is_empty() {
169 return f64::NAN;
170 }
171
172 let mut total_qty = Decimal::ZERO;
173 let mut top_qty = Decimal::ZERO;
174
175 for bid in bids {
177 total_qty += bid.quantity;
178 }
179 for ask in asks {
180 total_qty += ask.quantity;
181 }
182
183 for i in 0..top_levels.min(bids.len()) {
185 top_qty += bids[i].quantity;
186 }
187 for i in 0..top_levels.min(asks.len()) {
188 top_qty += asks[i].quantity;
189 }
190
191 if total_qty == Decimal::ZERO {
192 return f64::NAN;
193 }
194
195 decimal_to_f64_or_nan(top_qty / total_qty)
196}
197
198pub struct PriceRunCalculator {
202 price_changes: VecDeque<bool>, window_size: usize,
204 last_mid_price: Option<Decimal>,
205}
206
207impl PriceRunCalculator {
208 #[must_use]
210 pub fn new(window_size: usize) -> Self {
211 Self {
212 price_changes: VecDeque::with_capacity(window_size),
213 window_size,
214 last_mid_price: None,
215 }
216 }
217
218 #[must_use]
220 pub fn update(&mut self, snapshot: &OrderBookSnapshot) -> f64 {
221 let mid_price = snapshot.mid_price();
222
223 if let Some(last_price) = self.last_mid_price {
224 if mid_price > last_price {
225 self.price_changes.push_back(true);
226 } else if mid_price < last_price {
227 self.price_changes.push_back(false);
228 }
229 if self.price_changes.len() > self.window_size {
232 self.price_changes.pop_front();
233 }
234 }
235
236 self.last_mid_price = Some(mid_price);
237
238 if self.price_changes.is_empty() {
239 return f64::NAN;
240 }
241
242 let increases = self.price_changes.iter().filter(|&&x| x).count();
243 increases as f64 / self.price_changes.len() as f64
244 }
245}
246
247pub struct AdvancedVPINCalculator {
252 bucket_size: Decimal,
253 buckets: VecDeque<VolumeBucket>,
254 current_bucket: VolumeBucket,
255 window_size: usize,
256}
257
258#[derive(Debug, Clone)]
259struct VolumeBucket {
260 buy_volume: Decimal,
261 sell_volume: Decimal,
262 total_volume: Decimal,
263}
264
265impl VolumeBucket {
266 const fn new() -> Self {
267 Self {
268 buy_volume: Decimal::ZERO,
269 sell_volume: Decimal::ZERO,
270 total_volume: Decimal::ZERO,
271 }
272 }
273
274 fn add_trade(&mut self, side: TradeSide, quantity: Decimal) {
275 match side {
276 TradeSide::Buy => self.buy_volume += quantity,
277 TradeSide::Sell => self.sell_volume += quantity,
278 }
279 self.total_volume += quantity;
280 }
281
282 fn is_full(&self, bucket_size: Decimal) -> bool {
283 self.total_volume >= bucket_size
284 }
285
286 fn vpin(&self) -> f64 {
287 if self.total_volume == Decimal::ZERO {
288 return 0.0;
289 }
290
291 let buy_ratio = decimal_to_f64_or_nan(self.buy_volume / self.total_volume);
292 let sell_ratio = decimal_to_f64_or_nan(self.sell_volume / self.total_volume);
293
294 (buy_ratio - sell_ratio).abs()
295 }
296}
297
298impl AdvancedVPINCalculator {
299 #[must_use]
301 pub fn new(bucket_size: Decimal, window_size: usize) -> Self {
302 Self {
303 bucket_size,
304 buckets: VecDeque::with_capacity(window_size),
305 current_bucket: VolumeBucket::new(),
306 window_size,
307 }
308 }
309
310 pub fn add_trade(&mut self, trade: &TradeTick) {
312 let mut remaining_qty = trade.quantity;
313
314 while remaining_qty > Decimal::ZERO {
315 let space_in_bucket = self.bucket_size - self.current_bucket.total_volume;
316 let qty_to_add = remaining_qty.min(space_in_bucket);
317
318 self.current_bucket.add_trade(trade.side, qty_to_add);
319 remaining_qty -= qty_to_add;
320
321 if self.current_bucket.is_full(self.bucket_size) {
322 self.buckets.push_back(self.current_bucket.clone());
324
325 if self.buckets.len() > self.window_size {
326 self.buckets.pop_front();
327 }
328
329 self.current_bucket = VolumeBucket::new();
330 }
331 }
332 }
333
334 #[must_use]
336 pub fn calculate(&self) -> f64 {
337 if self.buckets.is_empty() {
338 return 0.0;
339 }
340
341 let sum_vpin: f64 = self.buckets.iter().map(|bucket| bucket.vpin()).sum();
342
343 sum_vpin / self.buckets.len() as f64
344 }
345}
346
347#[inline(always)]
351pub fn calculate_order_cancel_rate(
352 bids: &SmallVec<[Level; 25]>,
353 asks: &SmallVec<[Level; 25]>,
354) -> f64 {
355 let mut ask_sum = Decimal::ZERO;
356 let mut bid_sum = Decimal::ZERO;
357
358 for ask in asks {
359 ask_sum += ask.quantity;
360 }
361
362 for bid in bids {
363 bid_sum += bid.quantity;
364 }
365
366 let total_sum = ask_sum + bid_sum;
367 if total_sum == Decimal::ZERO {
368 return f64::NAN;
369 }
370
371 decimal_to_f64_or_nan(ask_sum / total_sum)
372}
373
374pub struct RelativeTickVolumeCalculator {
378 volumes: VecDeque<Decimal>,
379 window_size: usize,
380}
381
382impl RelativeTickVolumeCalculator {
383 #[must_use]
385 pub fn new(window_size: usize) -> Self {
386 Self {
387 volumes: VecDeque::with_capacity(window_size),
388 window_size,
389 }
390 }
391
392 #[must_use]
394 pub fn add_trade(&mut self, trade: &TradeTick) -> f64 {
395 self.volumes.push_back(trade.quantity);
396
397 if self.volumes.len() > self.window_size {
398 self.volumes.pop_front();
399 }
400
401 if self.volumes.is_empty() {
402 return 1.0;
403 }
404
405 let sum: Decimal = self.volumes.iter().sum();
406 let avg = sum / Decimal::from(self.volumes.len() as u64);
407
408 if avg == Decimal::ZERO {
409 return f64::NAN;
410 }
411
412 decimal_to_f64_or_nan(trade.quantity / avg)
413 }
414}
415
416#[inline(always)]
420pub fn calculate_order_book_depth(
421 bids: &SmallVec<[Level; 25]>,
422 asks: &SmallVec<[Level; 25]>,
423) -> Decimal {
424 let mut total = Decimal::ZERO;
425
426 for bid in bids {
427 total += bid.quantity;
428 }
429
430 for ask in asks {
431 total += ask.quantity;
432 }
433
434 total
435}
436
437#[inline(always)]
441pub fn calculate_order_book_slope(
442 bids: &SmallVec<[Level; 25]>,
443 asks: &SmallVec<[Level; 25]>,
444) -> f64 {
445 let mut ask_sum = Decimal::ZERO;
446 let mut bid_sum = Decimal::ZERO;
447
448 for ask in asks {
449 ask_sum += ask.quantity;
450 }
451
452 for bid in bids {
453 bid_sum += bid.quantity;
454 }
455
456 if bid_sum == Decimal::ZERO {
457 return f64::NAN;
458 }
459
460 decimal_to_f64_or_nan(ask_sum / bid_sum)
461}
462
463pub struct ExponentialDecayCalculator {
467 alpha: f64,
468 ewm_mean: f64,
469 ewm_var: f64,
470 ewm_std: f64,
471 count: usize,
472 last_value: Option<f64>,
473}
474
475impl ExponentialDecayCalculator {
476 #[must_use]
478 pub fn new(span: usize) -> Self {
479 let alpha = 2.0 / (span as f64 + 1.0);
480 Self {
481 alpha,
482 ewm_mean: 0.0,
483 ewm_var: 0.0,
484 ewm_std: 0.0,
485 count: 0,
486 last_value: None,
487 }
488 }
489
490 pub fn update(&mut self, value: f64) {
492 if self.count == 0 {
493 self.ewm_mean = value;
494 self.ewm_var = 0.0;
495 self.ewm_std = 0.0;
496 } else {
497 let delta = value - self.ewm_mean;
498 self.ewm_mean += self.alpha * delta;
499 self.ewm_var = (1.0 - self.alpha) * (self.ewm_var + self.alpha * delta * delta);
500 self.ewm_std = self.ewm_var.sqrt();
501 }
502
503 self.count += 1;
504 self.last_value = Some(value);
505 }
506 pub const fn get_mean(&self) -> f64 {
508 self.ewm_mean
509 }
510 pub const fn get_std(&self) -> f64 {
512 self.ewm_std
513 }
514 pub const fn get_var(&self) -> f64 {
516 self.ewm_var
517 }
518}
519
520pub struct RollingPriceImpactCalculator {
524 price_window: VecDeque<Decimal>,
525 volume_window: VecDeque<Decimal>,
526 window_size: usize,
527}
528
529impl RollingPriceImpactCalculator {
530 #[must_use]
532 pub fn new(window_size: usize) -> Self {
533 Self {
534 price_window: VecDeque::with_capacity(window_size),
535 volume_window: VecDeque::with_capacity(window_size),
536 window_size,
537 }
538 }
539
540 #[must_use]
542 pub fn add_trade(&mut self, trade: &TradeTick) -> f64 {
543 self.price_window.push_back(trade.price);
544 self.volume_window.push_back(trade.quantity);
545
546 if self.price_window.len() > self.window_size {
547 self.price_window.pop_front();
548 self.volume_window.pop_front();
549 }
550
551 if self.price_window.len() < 2 {
552 return f64::NAN;
553 }
554
555 let prices: Vec<f64> = self
557 .price_window
558 .iter()
559 .map(|&p| decimal_to_f64_or_nan(p))
560 .collect();
561 let mean = prices.iter().sum::<f64>() / prices.len() as f64;
562 let variance =
563 prices.iter().map(|&p| (p - mean).powi(2)).sum::<f64>() / prices.len() as f64;
564 let price_std = variance.sqrt();
565
566 let total_volume = self.volume_window.iter().sum::<Decimal>();
568
569 if price_std == 0.0 {
570 return f64::NAN;
571 }
572
573 decimal_to_f64_or_nan(total_volume) / price_std
574 }
575}
576
577pub struct VolumePriceSensitivityCalculator {
581 price_changes: VecDeque<f64>,
582 volume_changes: VecDeque<f64>,
583 window_size: usize,
584 last_price: Option<Decimal>,
585 last_volume: Option<Decimal>,
586}
587
588impl VolumePriceSensitivityCalculator {
589 #[must_use]
591 pub fn new(window_size: usize) -> Self {
592 Self {
593 price_changes: VecDeque::with_capacity(window_size),
594 volume_changes: VecDeque::with_capacity(window_size),
595 window_size,
596 last_price: None,
597 last_volume: None,
598 }
599 }
600
601 #[must_use]
603 pub fn add_trade(&mut self, trade: &TradeTick) -> f64 {
604 if let (Some(last_p), Some(last_v)) = (self.last_price, self.last_volume) {
605 let price_change = decimal_to_f64_or_nan((trade.price - last_p).abs());
606 let volume_change = decimal_to_f64_or_nan((trade.quantity - last_v).abs());
607
608 self.price_changes.push_back(price_change);
609 self.volume_changes.push_back(volume_change);
610
611 if self.price_changes.len() > self.window_size {
612 self.price_changes.pop_front();
613 self.volume_changes.pop_front();
614 }
615 }
616
617 self.last_price = Some(trade.price);
618 self.last_volume = Some(trade.quantity);
619
620 if self.price_changes.is_empty() {
621 return f64::NAN;
622 }
623
624 let avg_price_change =
625 self.price_changes.iter().sum::<f64>() / self.price_changes.len() as f64;
626 let avg_volume_change =
627 self.volume_changes.iter().sum::<f64>() / self.volume_changes.len() as f64;
628
629 if avg_price_change == 0.0 {
630 return f64::NAN;
631 }
632
633 avg_volume_change / avg_price_change
634 }
635}
636
637pub struct PriceEntropyCalculator {
641 price_changes: VecDeque<i8>, window_size: usize,
643 last_price: Option<Decimal>,
644}
645
646impl PriceEntropyCalculator {
647 #[must_use]
649 pub fn new(window_size: usize) -> Self {
650 Self {
651 price_changes: VecDeque::with_capacity(window_size),
652 window_size,
653 last_price: None,
654 }
655 }
656
657 #[must_use]
659 pub fn update(&mut self, price: Decimal) -> f64 {
660 if let Some(last) = self.last_price {
661 let change = if price > last {
662 1
663 } else if price < last {
664 -1
665 } else {
666 0
667 };
668
669 self.price_changes.push_back(change);
670
671 if self.price_changes.len() > self.window_size {
672 self.price_changes.pop_front();
673 }
674 }
675
676 self.last_price = Some(price);
677
678 if self.price_changes.len() < 10 {
679 return f64::NAN;
680 }
681
682 let mut counts = [0u32; 3]; for &change in &self.price_changes {
685 counts[(change + 1) as usize] += 1;
686 }
687
688 let total = self.price_changes.len() as f64;
690 let mut entropy = 0.0;
691
692 for &count in &counts {
693 if count > 0 {
694 let prob = count as f64 / total;
695 entropy -= prob * prob.log2();
696 }
697 }
698
699 entropy
700 }
701}
702
703pub fn calculate_multi_level_ofi_detailed(
707 prev_bids: &SmallVec<[Level; 25]>,
708 prev_asks: &SmallVec<[Level; 25]>,
709 curr_bids: &SmallVec<[Level; 25]>,
710 curr_asks: &SmallVec<[Level; 25]>,
711) -> f64 {
712 let mut ofi = Decimal::ZERO;
713
714 for prev_bid in prev_bids {
716 let matching_bid = curr_bids.iter().find(|b| b.price == prev_bid.price);
718
719 if let Some(curr_bid) = matching_bid {
720 let delta_vol = curr_bid.quantity - prev_bid.quantity;
721 ofi += delta_vol * curr_bid.price;
722 }
723 }
724
725 for prev_ask in prev_asks {
727 let matching_ask = curr_asks.iter().find(|a| a.price == prev_ask.price);
729
730 if let Some(curr_ask) = matching_ask {
731 let delta_vol = curr_ask.quantity - prev_ask.quantity;
732 ofi -= delta_vol * curr_ask.price;
733 }
734 }
735
736 decimal_to_f64_or_nan(ofi)
737}
738
739pub struct TradingBurstDetector {
743 volumes: VecDeque<Decimal>,
744 window_size: usize,
745}
746
747impl TradingBurstDetector {
748 #[must_use]
750 pub fn new(window_size: usize) -> Self {
751 Self {
752 volumes: VecDeque::with_capacity(window_size),
753 window_size,
754 }
755 }
756
757 #[must_use]
759 pub fn add_trade(&mut self, trade: &TradeTick) -> f64 {
760 self.volumes.push_back(trade.quantity);
761
762 if self.volumes.len() > self.window_size {
763 self.volumes.pop_front();
764 }
765
766 if self.volumes.len() < 2 {
767 return f64::NAN;
768 }
769
770 let volumes_f64: Vec<f64> = self
772 .volumes
773 .iter()
774 .map(|&v| decimal_to_f64_or_nan(v))
775 .collect();
776
777 let mean = volumes_f64.iter().sum::<f64>() / volumes_f64.len() as f64;
778 let variance =
779 volumes_f64.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / volumes_f64.len() as f64;
780
781 variance.sqrt()
782 }
783}
784
785#[inline(always)]
789pub fn calculate_multi_level_queue_imbalance(
790 bids: &SmallVec<[Level; 25]>,
791 asks: &SmallVec<[Level; 25]>,
792 levels: usize,
793) -> f64 {
794 let mut ask_sum = Decimal::ZERO;
795 let mut bid_sum = Decimal::ZERO;
796
797 for i in 0..levels.min(asks.len()) {
798 ask_sum += asks[i].quantity;
799 }
800
801 for i in 0..levels.min(bids.len()) {
802 bid_sum += bids[i].quantity;
803 }
804
805 let total = ask_sum + bid_sum;
806 if total == Decimal::ZERO {
807 return f64::NAN;
808 }
809
810 decimal_to_f64_or_nan((ask_sum - bid_sum) / total)
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816
817 fn create_test_levels(prices: &[f64], quantities: &[f64]) -> SmallVec<[Level; 25]> {
818 prices
819 .iter()
820 .zip(quantities.iter())
821 .map(|(&p, &q)| Level {
822 price: Decimal::from_f64_retain(p).unwrap(),
823 quantity: Decimal::from_f64_retain(q).unwrap(),
824 order_count: 1,
825 })
826 .collect()
827 }
828
829 #[test]
830 fn test_weighted_order_imbalance() {
831 let bids = create_test_levels(&[100.0, 99.5, 99.0], &[10.0, 20.0, 30.0]);
832 let asks = create_test_levels(&[100.5, 101.0, 101.5], &[15.0, 25.0, 35.0]);
833
834 let imbalance = calculate_weighted_order_imbalance(&bids, &asks, 3);
835 assert!((-1.0..=1.0).contains(&imbalance));
836 }
837
838 #[test]
839 fn test_relative_spread() {
840 let bids = create_test_levels(&[100.0], &[10.0]);
841 let asks = create_test_levels(&[100.5], &[10.0]);
842
843 let spread = calculate_relative_spread(&bids, &asks);
844 assert!((spread - 0.004988).abs() < 0.000001); }
846
847 #[test]
848 fn test_advanced_vpin() {
849 let mut calculator = AdvancedVPINCalculator::new(dec!(100), 10);
850
851 calculator.add_trade(&TradeTick {
853 timestamp_ns: 1000,
854 symbol: "TEST".to_string(),
855 side: TradeSide::Buy,
856 price: dec!(100),
857 quantity: dec!(60),
858 });
859
860 calculator.add_trade(&TradeTick {
861 timestamp_ns: 2000,
862 symbol: "TEST".to_string(),
863 side: TradeSide::Sell,
864 price: dec!(100),
865 quantity: dec!(40),
866 });
867
868 let vpin = calculator.calculate();
869 assert!((0.0..=1.0).contains(&vpin));
870 }
871
872 #[test]
873 fn test_price_run() {
874 let mut calculator = PriceRunCalculator::new(5);
875
876 for i in 0..10 {
878 let price = 100.0 + i as f64;
879 let snapshot = OrderBookSnapshot {
880 timestamp_ns: i * 1000,
881 symbol: "TEST".to_string(),
882 bids: create_test_levels(&[price - 0.5], &[10.0]),
883 asks: create_test_levels(&[price + 0.5], &[10.0]),
884 };
885
886 let run = calculator.update(&snapshot);
887 if i > 0 {
888 assert!(!run.is_nan());
889 }
890 }
891 }
892
893 #[test]
894 fn test_liquidity_shock_ratio() {
895 let bids = create_test_levels(
896 &[100.0, 99.5, 99.0, 98.5, 98.0],
897 &[10.0, 20.0, 30.0, 40.0, 50.0],
898 );
899 let asks = create_test_levels(
900 &[100.5, 101.0, 101.5, 102.0, 102.5],
901 &[15.0, 25.0, 35.0, 45.0, 55.0],
902 );
903
904 let ratio = calculate_liquidity_shock_ratio(&bids, &asks, 2);
905 assert!((0.0..=1.0).contains(&ratio));
906 }
907
908 #[test]
909 fn test_exponential_decay_calculator() {
910 let mut calculator = ExponentialDecayCalculator::new(10);
911
912 for i in 1..=20 {
914 calculator.update(i as f64);
915 }
916
917 assert!(calculator.get_mean() > 0.0);
919 assert!(calculator.get_std() >= 0.0);
920 assert!(calculator.get_var() >= 0.0);
921 }
922
923 #[test]
924 fn test_rolling_price_impact() {
925 let mut calculator = RollingPriceImpactCalculator::new(5);
926
927 for i in 1..=10 {
928 let trade = TradeTick {
929 timestamp_ns: i * 1000,
930 symbol: "TEST".to_string(),
931 side: TradeSide::Buy,
932 price: dec!(100) + Decimal::from(i),
933 quantity: dec!(10) + Decimal::from(i),
934 };
935
936 let impact = calculator.add_trade(&trade);
937 if i > 2 {
938 assert!(!impact.is_nan());
939 }
940 }
941 }
942
943 #[test]
944 fn test_volume_price_sensitivity() {
945 let mut calculator = VolumePriceSensitivityCalculator::new(5);
946
947 for i in 1..=10 {
948 let trade = TradeTick {
949 timestamp_ns: i * 1000,
950 symbol: "TEST".to_string(),
951 side: TradeSide::Buy,
952 price: dec!(100) + Decimal::from(i % 3), quantity: dec!(10) + Decimal::from(i),
954 };
955
956 let sensitivity = calculator.add_trade(&trade);
957 if i > 1 {
958 println!("Sensitivity at {i}: {sensitivity}");
960 }
961 }
962 }
963
964 #[test]
965 fn test_price_entropy() {
966 let mut calculator = PriceEntropyCalculator::new(20);
967
968 for i in 1..=50 {
970 let price = dec!(100) + Decimal::from(i % 5); let entropy = calculator.update(price);
972
973 if i > 15 {
974 assert!(!entropy.is_nan());
975 assert!(entropy >= 0.0);
976 }
977 }
978 }
979
980 #[test]
981 fn test_multi_level_ofi_detailed() {
982 let prev_bids = create_test_levels(&[100.0, 99.5, 99.0], &[10.0, 20.0, 30.0]);
983 let prev_asks = create_test_levels(&[100.5, 101.0, 101.5], &[15.0, 25.0, 35.0]);
984
985 let curr_bids = create_test_levels(&[100.0, 99.5, 99.0], &[12.0, 18.0, 32.0]);
987 let curr_asks = create_test_levels(&[100.5, 101.0, 101.5], &[13.0, 27.0, 33.0]);
988
989 let ofi =
990 calculate_multi_level_ofi_detailed(&prev_bids, &prev_asks, &curr_bids, &curr_asks);
991 assert!(!ofi.is_nan());
992 }
993
994 #[test]
995 fn test_trading_burst_detector() {
996 let mut detector = TradingBurstDetector::new(5);
997
998 for i in 1..=10 {
999 let trade = TradeTick {
1000 timestamp_ns: i * 1000,
1001 symbol: "TEST".to_string(),
1002 side: TradeSide::Buy,
1003 price: dec!(100),
1004 quantity: dec!(10) + Decimal::from(i * i), };
1006
1007 let burst = detector.add_trade(&trade);
1008 if i > 2 {
1009 assert!(!burst.is_nan());
1010 assert!(burst >= 0.0);
1011 }
1012 }
1013 }
1014}