rusty_backtest/features/
tardis_features.rs

1//! Advanced Microstructural Features from Binance Tardis L2 Structure
2//!
3//! This module implements comprehensive microstructural features based on
4//! the Binance Tardis L2 data structure, including:
5//! - Weighted Order Imbalance
6//! - Relative Spread
7//! - Volume-Weighted OFI
8//! - Depth-Weighted OFI
9//! - Queue Imbalance with multi-level support
10//! - Liquidity Shocks
11//! - Price Run
12//! - Advanced VPIN calculations
13
14use 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/// Weighted Order Imbalance
21///
22/// Calculates imbalance using weighted volumes where closer levels have higher weights
23#[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/// Relative Spread
62///
63/// Spread normalized by mid price
64#[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
84/// Volume-Weighted OFI
85///
86/// Calculates order flow imbalance using volume-weighted average prices
87pub 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    // Calculate bid VWAP
103    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    // Calculate ask VWAP
114    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/// Depth-Weighted OFI
128///
129/// Order flow imbalance weighted by level depth
130#[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/// Liquidity Shocks Detector
160///
161/// Detects abrupt changes in liquidity by comparing top levels to total depth
162#[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    // Sum all quantities
176    for bid in bids {
177        total_qty += bid.quantity;
178    }
179    for ask in asks {
180        total_qty += ask.quantity;
181    }
182
183    // Sum top level quantities
184    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
198/// Price Run Calculator
199///
200/// Calculates the proportion of successive price increases over a window
201pub struct PriceRunCalculator {
202    price_changes: VecDeque<bool>, // true for increase, false for decrease
203    window_size: usize,
204    last_mid_price: Option<Decimal>,
205}
206
207impl PriceRunCalculator {
208    /// Create a new price run calculator with specified window size
209    #[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    /// Update with new orderbook snapshot and return the price run ratio
219    #[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            // Equal prices don't count as changes
230
231            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
247/// Advanced VPIN Calculator with Volume Buckets
248///
249/// Implements Volume-Synchronized Probability of Informed Trading
250/// with proper volume bucketing
251pub 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    /// Create a new advanced VPIN calculator with specified bucket size and window
300    #[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    /// Add a trade to the VPIN calculation
311    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                // Complete the bucket
323                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    /// Calculate the average VPIN across all buckets
335    #[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/// Order Cancel Estimated Rate
348///
349/// Estimates the rate of order cancellations based on ask/bid imbalance
350#[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
374/// Relative Tick Volume Calculator
375///
376/// Normalizes volume relative to recent average
377pub struct RelativeTickVolumeCalculator {
378    volumes: VecDeque<Decimal>,
379    window_size: usize,
380}
381
382impl RelativeTickVolumeCalculator {
383    /// Create a new relative tick volume calculator
384    #[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    /// Add a trade and return the relative volume compared to recent average
393    #[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/// Order Book Depth Calculator
417///
418/// Calculates total depth (sum of all bid and ask quantities)
419#[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/// Order Book Slope Calculator
438///
439/// Measures the ratio of total ask to bid quantities
440#[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
463/// Exponential Decay Rate Calculator
464///
465/// Computes exponential weighted moving statistics for price movements
466pub 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    /// Create a new exponential decay calculator with the given span
477    #[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    /// Update the calculator with a new value
491    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    /// Get the current exponentially weighted mean
507    pub const fn get_mean(&self) -> f64 {
508        self.ewm_mean
509    }
510    /// Get the current exponentially weighted standard deviation
511    pub const fn get_std(&self) -> f64 {
512        self.ewm_std
513    }
514    /// Get the current exponentially weighted variance
515    pub const fn get_var(&self) -> f64 {
516        self.ewm_var
517    }
518}
519
520/// Rolling Price Impact Calculator
521///
522/// Measures how volume moves price over a rolling window
523pub struct RollingPriceImpactCalculator {
524    price_window: VecDeque<Decimal>,
525    volume_window: VecDeque<Decimal>,
526    window_size: usize,
527}
528
529impl RollingPriceImpactCalculator {
530    /// Create a new rolling price impact calculator
531    #[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    /// Add a trade and calculate Kyle's lambda (price impact coefficient)
541    #[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        // Calculate price standard deviation
556        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        // Calculate total volume
567        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
577/// Volume-to-Price Sensitivity Calculator
578///
579/// Measures how volume changes relative to price changes
580pub 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    /// Create a new volume-price sensitivity calculator
590    #[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    /// Add a trade and calculate the volume-price sensitivity ratio
602    #[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
637/// Entropy of Price Changes Calculator
638///
639/// Measures the randomness or predictability of price movements
640pub struct PriceEntropyCalculator {
641    price_changes: VecDeque<i8>, // -1, 0, 1 for down, no change, up
642    window_size: usize,
643    last_price: Option<Decimal>,
644}
645
646impl PriceEntropyCalculator {
647    /// Create a new price entropy calculator
648    #[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    /// Update with new price and calculate Shannon entropy
658    #[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        // Count occurrences
683        let mut counts = [0u32; 3]; // for -1, 0, 1
684        for &change in &self.price_changes {
685            counts[(change + 1) as usize] += 1;
686        }
687
688        // Calculate entropy
689        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
703/// Multi-Level OFI with Price Matching
704///
705/// Detailed OFI calculation that tracks volume changes at specific price levels
706pub 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    // Process bid side
715    for prev_bid in prev_bids {
716        // Find matching price in current bids
717        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    // Process ask side
726    for prev_ask in prev_asks {
727        // Find matching price in current asks
728        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
739/// Bursts in Trading Activity Calculator
740///
741/// Detects periods of heightened trading activity using rolling variance
742pub struct TradingBurstDetector {
743    volumes: VecDeque<Decimal>,
744    window_size: usize,
745}
746
747impl TradingBurstDetector {
748    /// Create a new trading burst detector
749    #[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    /// Add a trade and return the relative volume compared to recent average
758    #[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        // Calculate variance
771        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/// Queue Imbalance at Multiple Levels
786///
787/// Calculates normalized difference between top bid and ask queues
788#[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); // 0.5 / 100.25 ≈ 0.004988
845    }
846
847    #[test]
848    fn test_advanced_vpin() {
849        let mut calculator = AdvancedVPINCalculator::new(dec!(100), 10);
850
851        // Add trades
852        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        // Create snapshots with increasing prices
877        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        // Add some values
913        for i in 1..=20 {
914            calculator.update(i as f64);
915        }
916
917        // Check that calculations are reasonable
918        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), // Some price variation
953                quantity: dec!(10) + Decimal::from(i),
954            };
955
956            let sensitivity = calculator.add_trade(&trade);
957            if i > 1 {
958                // Should eventually get a valid value
959                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        // Add prices with some pattern
969        for i in 1..=50 {
970            let price = dec!(100) + Decimal::from(i % 5); // Cyclical pattern
971            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        // Modified volumes
986        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), // Increasing variance
1005            };
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}