Skip to main content

bybit/models/
ws_order_book.rs

1use crate::prelude::*;
2
3/// Enum representing order book sides.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum OrderBookSide {
6    /// Bid side (buy orders)
7    Bid,
8    /// Ask side (sell orders)
9    Ask,
10}
11
12/// Enum representing trade sides.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum TradeSide {
15    /// Buy trade
16    Buy,
17    /// Sell trade
18    Sell,
19}
20
21/// Structure representing order book depth profile.
22#[derive(Debug, Clone)]
23pub struct DepthProfile {
24    /// Minimum price in the profile
25    pub min_price: f64,
26    /// Maximum price in the profile
27    pub max_price: f64,
28    /// Size of each price bucket
29    pub bucket_size: f64,
30    /// Quantity in each ask bucket
31    pub ask_buckets: Vec<f64>,
32    /// Quantity in each bid bucket
33    pub bid_buckets: Vec<f64>,
34}
35
36/// Structure for WebSocket order book data.
37///
38/// Contains the bids, asks, and sequence numbers for a trading pair’s order book. Bots use this to maintain an up-to-date view of market depth and liquidity.
39#[derive(Serialize, Deserialize, Debug, Clone)]
40#[serde(rename_all = "camelCase")]
41pub struct WsOrderBook {
42    /// The trading pair symbol (e.g., "BTCUSDT").
43    ///
44    /// Identifies the perpetual futures contract for the order book. Bots use this to verify the correct market.
45    #[serde(rename = "s")]
46    pub symbol: String,
47
48    /// A list of ask prices and quantities.
49    ///
50    /// Contains the current ask levels in the order book, sorted by price. Bots use this to assess selling pressure and liquidity on the ask side.
51    #[serde(rename = "a")]
52    pub asks: Vec<Ask>,
53
54    /// A list of bid prices and quantities.
55    ///
56    /// Contains the current bid levels in the order book, sorted by price. Bots use this to assess buying pressure and liquidity on the bid side.
57    #[serde(rename = "b")]
58    pub bids: Vec<Bid>,
59
60    /// The update ID for the order book.
61    ///
62    /// A unique identifier for the order book update. Bots use this to ensure updates are processed in the correct order.
63    #[serde(rename = "u")]
64    pub update_id: u64,
65
66    /// The sequence number for the update.
67    ///
68    /// A monotonically increasing number for ordering updates. Bots use this to detect missing or out-of-order updates and maintain order book consistency.
69    pub seq: u64,
70}
71
72impl WsOrderBook {
73    /// Creates a new WsOrderBook instance.
74    pub fn new(symbol: &str, asks: Vec<Ask>, bids: Vec<Bid>, update_id: u64, seq: u64) -> Self {
75        Self {
76            symbol: symbol.to_string(),
77            asks,
78            bids,
79            update_id,
80            seq,
81        }
82    }
83
84    /// Returns the best ask price (lowest ask).
85    pub fn best_ask(&self) -> Option<f64> {
86        self.asks.first().map(|ask| ask.price)
87    }
88
89    /// Returns the best ask quantity.
90    pub fn best_ask_quantity(&self) -> Option<f64> {
91        self.asks.first().map(|ask| ask.qty)
92    }
93
94    /// Returns the best bid price (highest bid).
95    pub fn best_bid(&self) -> Option<f64> {
96        self.bids.first().map(|bid| bid.price)
97    }
98
99    /// Returns the best bid quantity.
100    pub fn best_bid_quantity(&self) -> Option<f64> {
101        self.bids.first().map(|bid| bid.qty)
102    }
103
104    /// Returns the mid price (average of best bid and best ask).
105    pub fn mid_price(&self) -> Option<f64> {
106        match (self.best_bid(), self.best_ask()) {
107            (Some(bid), Some(ask)) => Some((bid + ask) / 2.0),
108            _ => None,
109        }
110    }
111
112    /// Returns the bid-ask spread.
113    pub fn spread(&self) -> Option<f64> {
114        match (self.best_ask(), self.best_bid()) {
115            (Some(ask), Some(bid)) => Some(ask - bid),
116            _ => None,
117        }
118    }
119
120    /// Returns the spread as a percentage of the mid price.
121    pub fn spread_percentage(&self) -> Option<f64> {
122        match (self.spread(), self.mid_price()) {
123            (Some(spread), Some(mid)) if mid > 0.0 => Some((spread / mid) * 100.0),
124            _ => None,
125        }
126    }
127
128    /// Returns the total quantity available at the ask side.
129    pub fn total_ask_quantity(&self) -> f64 {
130        self.asks.iter().map(|ask| ask.qty).sum()
131    }
132
133    /// Returns the total quantity available at the bid side.
134    pub fn total_bid_quantity(&self) -> f64 {
135        self.bids.iter().map(|bid| bid.qty).sum()
136    }
137
138    /// Returns the total value available at the ask side.
139    pub fn total_ask_value(&self) -> f64 {
140        self.asks.iter().map(|ask| ask.price * ask.qty).sum()
141    }
142
143    /// Returns the total value available at the bid side.
144    pub fn total_bid_value(&self) -> f64 {
145        self.bids.iter().map(|bid| bid.price * bid.qty).sum()
146    }
147
148    /// Returns the order book imbalance.
149    /// Positive values indicate more buying pressure, negative values indicate more selling pressure.
150    pub fn imbalance(&self) -> Option<f64> {
151        let total_bid = self.total_bid_quantity();
152        let total_ask = self.total_ask_quantity();
153        let total = total_bid + total_ask;
154
155        if total > 0.0 {
156            Some((total_bid - total_ask) / total)
157        } else {
158            None
159        }
160    }
161
162    /// Returns the volume-weighted average price (VWAP) for asks.
163    pub fn ask_vwap(&self) -> Option<f64> {
164        let total_value = self.total_ask_value();
165        let total_quantity = self.total_ask_quantity();
166
167        if total_quantity > 0.0 {
168            Some(total_value / total_quantity)
169        } else {
170            None
171        }
172    }
173
174    /// Returns the volume-weighted average price (VWAP) for bids.
175    pub fn bid_vwap(&self) -> Option<f64> {
176        let total_value = self.total_bid_value();
177        let total_quantity = self.total_bid_quantity();
178
179        if total_quantity > 0.0 {
180            Some(total_value / total_quantity)
181        } else {
182            None
183        }
184    }
185
186    /// Returns the market depth at a given price level.
187    /// Returns (bid_quantity, ask_quantity) at the specified price level.
188    pub fn depth_at_price(&self, price: f64, tolerance: f64) -> (f64, f64) {
189        let bid_quantity = self
190            .bids
191            .iter()
192            .filter(|bid| (bid.price - price).abs() <= tolerance)
193            .map(|bid| bid.qty)
194            .sum();
195
196        let ask_quantity = self
197            .asks
198            .iter()
199            .filter(|ask| (ask.price - price).abs() <= tolerance)
200            .map(|ask| ask.qty)
201            .sum();
202
203        (bid_quantity, ask_quantity)
204    }
205
206    /// Returns the cumulative order book quantities up to a given price level.
207    pub fn cumulative_depth(&self, price_limit: f64, side: OrderBookSide) -> f64 {
208        match side {
209            OrderBookSide::Bid => self
210                .bids
211                .iter()
212                .filter(|bid| bid.price >= price_limit)
213                .map(|bid| bid.qty)
214                .sum(),
215            OrderBookSide::Ask => self
216                .asks
217                .iter()
218                .filter(|ask| ask.price <= price_limit)
219                .map(|ask| ask.qty)
220                .sum(),
221        }
222    }
223
224    /// Returns the price levels within a given percentage range from the mid price.
225    pub fn price_levels_in_range(&self, percentage_range: f64) -> (Vec<&Ask>, Vec<&Bid>) {
226        if let Some(mid_price) = self.mid_price() {
227            let price_range = mid_price * percentage_range / 100.0;
228            let min_price = mid_price - price_range;
229            let max_price = mid_price + price_range;
230
231            let filtered_asks = self
232                .asks
233                .iter()
234                .filter(|ask| ask.price <= max_price)
235                .collect();
236
237            let filtered_bids = self
238                .bids
239                .iter()
240                .filter(|bid| bid.price >= min_price)
241                .collect();
242
243            (filtered_asks, filtered_bids)
244        } else {
245            (vec![], vec![])
246        }
247    }
248
249    /// Returns the order book liquidity in a given price range.
250    pub fn liquidity_in_range(&self, min_price: f64, max_price: f64) -> (f64, f64) {
251        let ask_liquidity = self
252            .asks
253            .iter()
254            .filter(|ask| ask.price >= min_price && ask.price <= max_price)
255            .map(|ask| ask.qty)
256            .sum();
257
258        let bid_liquidity = self
259            .bids
260            .iter()
261            .filter(|bid| bid.price >= min_price && bid.price <= max_price)
262            .map(|bid| bid.qty)
263            .sum();
264
265        (ask_liquidity, bid_liquidity)
266    }
267
268    /// Returns the price impact for a given trade size.
269    /// Estimates how much the price would move if a trade of the given size were executed.
270    pub fn price_impact(&self, trade_size: f64, side: TradeSide) -> Option<f64> {
271        match side {
272            TradeSide::Buy => self.price_impact_for_buy(trade_size),
273            TradeSide::Sell => self.price_impact_for_sell(trade_size),
274        }
275    }
276
277    /// Returns the price impact for a buy trade.
278    fn price_impact_for_buy(&self, trade_size: f64) -> Option<f64> {
279        let reference_price = self.best_ask()?;
280        let mut remaining_size = trade_size;
281        let mut total_cost = 0.0;
282        let mut executed_quantity = 0.0;
283
284        for ask in &self.asks {
285            let quantity_to_take = remaining_size.min(ask.qty);
286            total_cost += quantity_to_take * ask.price;
287            executed_quantity += quantity_to_take;
288            remaining_size -= quantity_to_take;
289
290            if remaining_size <= 0.0 {
291                break;
292            }
293        }
294
295        if executed_quantity > 0.0 {
296            let average_price = total_cost / executed_quantity;
297            Some((average_price - reference_price) / reference_price * 100.0)
298        } else {
299            None
300        }
301    }
302
303    /// Returns the price impact for a sell trade.
304    fn price_impact_for_sell(&self, trade_size: f64) -> Option<f64> {
305        let reference_price = self.best_bid()?;
306        let mut remaining_size = trade_size;
307        let mut total_cost = 0.0;
308        let mut executed_quantity = 0.0;
309
310        for bid in &self.bids {
311            let quantity_to_take = remaining_size.min(bid.qty);
312            total_cost += quantity_to_take * bid.price;
313            executed_quantity += quantity_to_take;
314            remaining_size -= quantity_to_take;
315
316            if remaining_size <= 0.0 {
317                break;
318            }
319        }
320
321        if executed_quantity > 0.0 {
322            let average_price = total_cost / executed_quantity;
323            Some((average_price - reference_price) / reference_price * 100.0)
324        } else {
325            None
326        }
327    }
328
329    /// Returns the market depth profile.
330    /// Groups order book levels into price buckets for analysis.
331    pub fn depth_profile(&self, num_buckets: usize) -> DepthProfile {
332        let (min_price, max_price) = self.price_range();
333        let price_range = max_price - min_price;
334        let bucket_size = price_range / num_buckets as f64;
335
336        let mut ask_buckets = vec![0.0; num_buckets];
337        let mut bid_buckets = vec![0.0; num_buckets];
338
339        // Fill ask buckets
340        for ask in &self.asks {
341            let bucket_index = ((ask.price - min_price) / bucket_size).floor() as usize;
342            if bucket_index < num_buckets {
343                ask_buckets[bucket_index] += ask.qty;
344            }
345        }
346
347        // Fill bid buckets
348        for bid in &self.bids {
349            let bucket_index = ((bid.price - min_price) / bucket_size).floor() as usize;
350            if bucket_index < num_buckets {
351                bid_buckets[bucket_index] += bid.qty;
352            }
353        }
354
355        DepthProfile {
356            min_price,
357            max_price,
358            bucket_size,
359            ask_buckets,
360            bid_buckets,
361        }
362    }
363
364    /// Returns the order book's price range.
365    pub fn price_range(&self) -> (f64, f64) {
366        let min_price = self
367            .bids
368            .last()
369            .map(|bid| bid.price)
370            .unwrap_or_else(|| self.asks.first().map(|ask| ask.price).unwrap_or(0.0));
371
372        let max_price = self
373            .asks
374            .last()
375            .map(|ask| ask.price)
376            .unwrap_or_else(|| self.bids.first().map(|bid| bid.price).unwrap_or(0.0));
377
378        (min_price, max_price)
379    }
380
381    /// Returns the order book's quantity-weighted average spread.
382    pub fn weighted_spread(&self) -> Option<f64> {
383        let best_ask_quantity = self.best_ask_quantity()?;
384        let best_bid_quantity = self.best_bid_quantity()?;
385        let spread = self.spread()?;
386
387        let total_quantity = best_ask_quantity + best_bid_quantity;
388        if total_quantity > 0.0 {
389            let ask_weight = best_ask_quantity / total_quantity;
390            let bid_weight = best_bid_quantity / total_quantity;
391
392            // Weighted spread gives more weight to the side with more liquidity
393            Some(spread * (ask_weight + bid_weight) / 2.0)
394        } else {
395            None
396        }
397    }
398
399    /// Returns the order book's resilience.
400    /// Measures how quickly the order book recovers after a trade.
401    pub fn resilience(&self) -> f64 {
402        let depth_ratio = self.total_bid_quantity() / self.total_ask_quantity().max(1.0);
403        let spread_ratio = self.spread_percentage().unwrap_or(0.0) / 0.1; // Normalize to 0.1% spread
404
405        // Higher depth ratio and lower spread indicate more resilience
406        depth_ratio / (1.0 + spread_ratio)
407    }
408
409    /// Returns the order book's toxicity.
410    /// Measures the likelihood of adverse selection.
411    pub fn toxicity(&self) -> f64 {
412        let imbalance = self.imbalance().unwrap_or(0.0).abs();
413        let spread = self.spread_percentage().unwrap_or(0.0);
414
415        // Higher imbalance and lower spread indicate higher toxicity
416        imbalance / (1.0 + spread)
417    }
418
419    /// Returns true if the order book is valid for trading.
420    pub fn is_valid(&self) -> bool {
421        !self.symbol.is_empty()
422            && !self.asks.is_empty()
423            && !self.bids.is_empty()
424            && self.best_ask().is_some()
425            && self.best_bid().is_some()
426            && self.best_ask().unwrap_or(0.0) > 0.0
427            && self.best_bid().unwrap_or(0.0) > 0.0
428            && self.best_ask().unwrap_or(f64::MAX) > self.best_bid().unwrap_or(0.0)
429    }
430
431    /// Returns a summary string for this order book.
432    pub fn to_summary_string(&self) -> String {
433        let best_bid = self.best_bid().unwrap_or(0.0);
434        let best_ask = self.best_ask().unwrap_or(0.0);
435        let spread = self.spread().unwrap_or(0.0);
436        let spread_pct = self.spread_percentage().unwrap_or(0.0);
437        let mid_price = self.mid_price().unwrap_or(0.0);
438        let imbalance = self.imbalance().unwrap_or(0.0);
439
440        format!(
441            "{}: Bid={:.2}, Ask={:.2}, Spread={:.4} ({:.4}%), Mid={:.2}, Imbalance={:.2}, Levels={}/{}",
442            self.symbol,
443            best_bid,
444            best_ask,
445            spread,
446            spread_pct,
447            mid_price,
448            imbalance,
449            self.bids.len(),
450            self.asks.len()
451        )
452    }
453
454    /// Returns the order book snapshot as a JSON string.
455    pub fn to_json(&self) -> String {
456        serde_json::to_string(self).unwrap_or_default()
457    }
458
459    /// Merges this order book with another order book update.
460    /// Used for incremental updates.
461    pub fn merge(&mut self, update: &WsOrderBook) -> bool {
462        if self.symbol != update.symbol {
463            return false;
464        }
465
466        if update.seq <= self.seq {
467            return false; // Old update
468        }
469
470        // Update sequence numbers
471        self.update_id = update.update_id;
472        self.seq = update.seq;
473
474        // For simplicity, replace the entire order book
475        // In a real implementation, you would apply incremental updates
476        self.asks = update.asks.clone();
477        self.bids = update.bids.clone();
478
479        true
480    }
481
482    /// Returns the order book age based on sequence numbers.
483    pub fn age(&self, current_seq: u64) -> u64 {
484        if current_seq >= self.seq {
485            current_seq - self.seq
486        } else {
487            0
488        }
489    }
490
491    /// Returns true if the order book is stale.
492    pub fn is_stale(&self, current_seq: u64, max_age: u64) -> bool {
493        self.age(current_seq) > max_age
494    }
495
496    /// Returns the order book's market quality score.
497    /// Higher scores indicate better market quality (more liquidity, tighter spreads).
498    pub fn market_quality_score(&self) -> f64 {
499        let mut score = 0.0;
500        let mut weight_sum = 0.0;
501
502        // Spread component (tighter spreads are better)
503        if let Some(spread_pct) = self.spread_percentage() {
504            let spread_score = 1.0 / (1.0 + spread_pct * 10.0); // Normalize
505            score += spread_score * 0.4;
506            weight_sum += 0.4;
507        }
508
509        // Depth component (more depth is better)
510        let total_depth = self.total_bid_quantity() + self.total_ask_quantity();
511        let depth_score = (total_depth / 1000.0).min(1.0); // Normalize to 1000 units
512        score += depth_score * 0.3;
513        weight_sum += 0.3;
514
515        // Imbalance component (balanced order book is better)
516        if let Some(imbalance) = self.imbalance() {
517            let imbalance_score = 1.0 - imbalance.abs();
518            score += imbalance_score * 0.2;
519            weight_sum += 0.2;
520        }
521
522        // Resilience component
523        let resilience_score = self.resilience().min(1.0);
524        score += resilience_score * 0.1;
525        weight_sum += 0.1;
526
527        if weight_sum > 0.0 {
528            score / weight_sum * 100.0 // Scale to 0-100
529        } else {
530            0.0
531        }
532    }
533
534    /// Returns the order book's estimated transaction cost.
535    /// This includes both the spread and potential price impact.
536    pub fn estimated_transaction_cost(&self, trade_size: f64) -> Option<f64> {
537        let spread_cost = self.spread_percentage()?;
538        let buy_impact = self.price_impact(trade_size, TradeSide::Buy).unwrap_or(0.0);
539        let sell_impact = self
540            .price_impact(trade_size, TradeSide::Sell)
541            .unwrap_or(0.0);
542
543        // Average of buy and sell impact plus half the spread (one-way cost)
544        Some((buy_impact + sell_impact) / 2.0 + spread_cost / 2.0)
545    }
546
547    /// Returns the optimal trade size based on market conditions.
548    /// Considers price impact and available liquidity.
549    pub fn optimal_trade_size(&self, max_price_impact: f64) -> Option<f64> {
550        let mut size = 0.0;
551        let mut step = self.total_bid_quantity().min(self.total_ask_quantity()) / 10.0;
552
553        // Binary search for optimal size
554        for _ in 0..10 {
555            let buy_impact = self
556                .price_impact(size + step, TradeSide::Buy)
557                .unwrap_or(0.0);
558            let sell_impact = self
559                .price_impact(size + step, TradeSide::Sell)
560                .unwrap_or(0.0);
561            let avg_impact = (buy_impact + sell_impact) / 2.0;
562
563            if avg_impact <= max_price_impact {
564                size += step;
565            } else {
566                step /= 2.0;
567            }
568        }
569
570        if size > 0.0 {
571            Some(size)
572        } else {
573            None
574        }
575    }
576
577    /// Returns the order book's support and resistance levels.
578    /// Based on significant accumulation of orders.
579    pub fn support_resistance_levels(&self, threshold_multiplier: f64) -> (Vec<f64>, Vec<f64>) {
580        let avg_quantity = (self.total_bid_quantity() + self.total_ask_quantity())
581            / (self.bids.len() + self.asks.len()) as f64;
582        let threshold = avg_quantity * threshold_multiplier;
583
584        let support_levels: Vec<f64> = self
585            .bids
586            .iter()
587            .filter(|bid| bid.qty >= threshold)
588            .map(|bid| bid.price)
589            .collect();
590
591        let resistance_levels: Vec<f64> = self
592            .asks
593            .iter()
594            .filter(|ask| ask.qty >= threshold)
595            .map(|ask| ask.price)
596            .collect();
597
598        (support_levels, resistance_levels)
599    }
600
601    /// Returns the order book's momentum indicator.
602    /// Positive values indicate buying pressure, negative values indicate selling pressure.
603    pub fn momentum_indicator(&self) -> f64 {
604        let top_bid_quantity = self.bids.first().map(|b| b.qty).unwrap_or(0.0);
605        let top_ask_quantity = self.asks.first().map(|a| a.qty).unwrap_or(0.0);
606        let total_top_quantity = top_bid_quantity + top_ask_quantity;
607
608        if total_top_quantity > 0.0 {
609            (top_bid_quantity - top_ask_quantity) / total_top_quantity
610        } else {
611            0.0
612        }
613    }
614
615    /// Returns the order book's volatility estimate.
616    /// Based on the density of orders around the mid price.
617    pub fn volatility_estimate(&self) -> Option<f64> {
618        let mid_price = self.mid_price()?;
619        let price_range = 0.01; // 1% price range
620
621        let (ask_liquidity, bid_liquidity) = self.liquidity_in_range(
622            mid_price * (1.0 - price_range),
623            mid_price * (1.0 + price_range),
624        );
625
626        let total_liquidity = ask_liquidity + bid_liquidity;
627        let max_possible_liquidity = self.total_ask_quantity() + self.total_bid_quantity();
628
629        if max_possible_liquidity > 0.0 {
630            // Lower concentration around mid price indicates higher volatility
631            Some(1.0 - (total_liquidity / max_possible_liquidity))
632        } else {
633            None
634        }
635    }
636
637    /// Returns the order book's efficiency metric.
638    /// Measures how well the order book facilitates trading.
639    pub fn efficiency_metric(&self) -> Option<f64> {
640        let spread_pct = self.spread_percentage()?;
641        let depth_ratio = self.total_bid_quantity() / self.total_ask_quantity().max(1.0);
642        let imbalance = self.imbalance()?.abs();
643
644        // Efficiency increases with lower spread, balanced depth, and lower imbalance
645        let spread_component = 1.0 / (1.0 + spread_pct * 100.0);
646        let depth_component = 2.0 * depth_ratio.min(1.0) / (1.0 + depth_ratio);
647        let imbalance_component = 1.0 - imbalance;
648
649        Some((spread_component + depth_component + imbalance_component) / 3.0 * 100.0)
650    }
651
652    /// Returns the order book's fair value estimate.
653    /// Based on volume-weighted average prices and order book imbalance.
654    pub fn fair_value_estimate(&self) -> Option<f64> {
655        let bid_vwap = self.bid_vwap()?;
656        let ask_vwap = self.ask_vwap()?;
657        let imbalance = self.imbalance()?;
658
659        // Weighted average based on order book imbalance
660        let bid_weight = (1.0 + imbalance) / 2.0;
661        let ask_weight = (1.0 - imbalance) / 2.0;
662
663        Some(bid_vwap * bid_weight + ask_vwap * ask_weight)
664    }
665
666    /// Returns the order book's arbitrage opportunity.
667    /// Difference between fair value and mid price as percentage.
668    pub fn arbitrage_opportunity(&self) -> Option<f64> {
669        let fair_value = self.fair_value_estimate()?;
670        let mid_price = self.mid_price()?;
671
672        if mid_price > 0.0 {
673            Some((fair_value - mid_price) / mid_price * 100.0)
674        } else {
675            None
676        }
677    }
678
679    /// Returns the order book's market impact profile.
680    /// Shows how price impact changes with trade size.
681    pub fn market_impact_profile(&self, max_trade_size: f64, steps: usize) -> Vec<(f64, f64)> {
682        let mut profile = Vec::with_capacity(steps);
683        let step_size = max_trade_size / steps as f64;
684
685        for i in 1..=steps {
686            let trade_size = step_size * i as f64;
687            if let Some(impact) = self.estimated_transaction_cost(trade_size) {
688                profile.push((trade_size, impact));
689            }
690        }
691
692        profile
693    }
694
695    /// Returns the order book's snapshot for persistence.
696    pub fn snapshot(&self) -> OrderBookSnapshot {
697        OrderBookSnapshot {
698            symbol: self.symbol.clone(),
699            bids: self.bids.clone(),
700            asks: self.asks.clone(),
701            update_id: self.update_id,
702            seq: self.seq,
703            timestamp: chrono::Utc::now().timestamp_millis() as u64,
704        }
705    }
706
707    /// Returns a comprehensive analysis report.
708    pub fn analysis_report(&self) -> String {
709        let mut report = String::new();
710
711        report.push_str(&format!("Order Book Analysis: {}\n", self.symbol));
712        report.push_str(&format!("================================\n"));
713
714        // Basic metrics
715        if let (Some(bid), Some(ask)) = (self.best_bid(), self.best_ask()) {
716            report.push_str(&format!("Best Bid: {:.8}\n", bid));
717            report.push_str(&format!("Best Ask: {:.8}\n", ask));
718        }
719
720        if let Some(spread) = self.spread() {
721            report.push_str(&format!("Spread: {:.8}\n", spread));
722        }
723
724        if let Some(spread_pct) = self.spread_percentage() {
725            report.push_str(&format!("Spread %: {:.4}%\n", spread_pct));
726        }
727
728        if let Some(mid) = self.mid_price() {
729            report.push_str(&format!("Mid Price: {:.8}\n", mid));
730        }
731
732        // Liquidity metrics
733        report.push_str(&format!("Bid Levels: {}\n", self.bids.len()));
734        report.push_str(&format!("Ask Levels: {}\n", self.asks.len()));
735        report.push_str(&format!(
736            "Total Bid Qty: {:.8}\n",
737            self.total_bid_quantity()
738        ));
739        report.push_str(&format!(
740            "Total Ask Qty: {:.8}\n",
741            self.total_ask_quantity()
742        ));
743        report.push_str(&format!("Total Bid Value: {:.8}\n", self.total_bid_value()));
744        report.push_str(&format!("Total Ask Value: {:.8}\n", self.total_ask_value()));
745
746        // Advanced metrics
747        if let Some(imbalance) = self.imbalance() {
748            report.push_str(&format!("Order Book Imbalance: {:.2}\n", imbalance));
749        }
750
751        if let Some(bid_vwap) = self.bid_vwap() {
752            report.push_str(&format!("Bid VWAP: {:.8}\n", bid_vwap));
753        }
754
755        if let Some(ask_vwap) = self.ask_vwap() {
756            report.push_str(&format!("Ask VWAP: {:.8}\n", ask_vwap));
757        }
758
759        let momentum = self.momentum_indicator();
760        report.push_str(&format!("Momentum Indicator: {:.4}\n", momentum));
761
762        let resilience = self.resilience();
763        report.push_str(&format!("Resilience Score: {:.4}\n", resilience));
764
765        let toxicity = self.toxicity();
766        report.push_str(&format!("Toxicity Score: {:.4}\n", toxicity));
767
768        let quality_score = self.market_quality_score();
769        report.push_str(&format!("Market Quality Score: {:.1}/100\n", quality_score));
770
771        if let Some(efficiency) = self.efficiency_metric() {
772            report.push_str(&format!("Efficiency Metric: {:.1}/100\n", efficiency));
773        }
774
775        if let Some(fair_value) = self.fair_value_estimate() {
776            report.push_str(&format!("Fair Value Estimate: {:.8}\n", fair_value));
777        }
778
779        if let Some(arb_opp) = self.arbitrage_opportunity() {
780            report.push_str(&format!("Arbitrage Opportunity: {:.4}%\n", arb_opp));
781        }
782
783        // Price impact examples
784        report.push_str("\nPrice Impact Analysis:\n");
785        for &size in &[0.1, 1.0, 10.0, 100.0] {
786            if let Some(cost) = self.estimated_transaction_cost(size) {
787                report.push_str(&format!("  Size {:.1}: {:.4}% cost\n", size, cost));
788            }
789        }
790
791        // Support and resistance
792        let (support, resistance) = self.support_resistance_levels(2.0);
793        if !support.is_empty() {
794            report.push_str(&format!("\nSupport Levels ({}):\n", support.len()));
795            for &level in &support[..support.len().min(5)] {
796                report.push_str(&format!("  {:.8}\n", level));
797            }
798        }
799
800        if !resistance.is_empty() {
801            report.push_str(&format!("\nResistance Levels ({}):\n", resistance.len()));
802            for &level in &resistance[..resistance.len().min(5)] {
803                report.push_str(&format!("  {:.8}\n", level));
804            }
805        }
806
807        // Recommendations
808        report.push_str("\nRecommendations:\n");
809        if quality_score >= 70.0 {
810            report.push_str("  ✅ Good market conditions for trading\n");
811        } else if quality_score >= 40.0 {
812            report.push_str("  ⚠️  Moderate market conditions\n");
813        } else {
814            report.push_str("  ❌ Poor market conditions\n");
815        }
816
817        if let Some(spread_pct) = self.spread_percentage() {
818            if spread_pct < 0.1 {
819                report.push_str("  ✅ Tight spreads\n");
820            } else if spread_pct < 0.5 {
821                report.push_str("  ⚠️  Moderate spreads\n");
822            } else {
823                report.push_str("  ❌ Wide spreads\n");
824            }
825        }
826
827        let imbalance = self.imbalance().unwrap_or(0.0);
828        if imbalance.abs() < 0.1 {
829            report.push_str("  ✅ Balanced order book\n");
830        } else if imbalance.abs() < 0.3 {
831            report.push_str("  ⚠️  Moderate imbalance\n");
832        } else {
833            report.push_str("  ❌ Significant imbalance\n");
834        }
835
836        report
837    }
838}
839
840/// Structure for persisting order book snapshots.
841#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct OrderBookSnapshot {
843    /// Trading symbol
844    pub symbol: String,
845    /// Bid levels
846    pub bids: Vec<Bid>,
847    /// Ask levels
848    pub asks: Vec<Ask>,
849    /// Update ID
850    pub update_id: u64,
851    /// Sequence number
852    pub seq: u64,
853    /// Timestamp when snapshot was taken
854    pub timestamp: u64,
855}
856
857impl OrderBookSnapshot {
858    /// Creates a new OrderBookSnapshot from a WsOrderBook.
859    pub fn from_order_book(order_book: &WsOrderBook) -> Self {
860        Self {
861            symbol: order_book.symbol.clone(),
862            bids: order_book.bids.clone(),
863            asks: order_book.asks.clone(),
864            update_id: order_book.update_id,
865            seq: order_book.seq,
866            timestamp: chrono::Utc::now().timestamp_millis() as u64,
867        }
868    }
869
870    /// Returns the age of the snapshot in milliseconds.
871    pub fn age_ms(&self) -> u64 {
872        let now = chrono::Utc::now().timestamp_millis() as u64;
873        if now > self.timestamp {
874            now - self.timestamp
875        } else {
876            0
877        }
878    }
879
880    /// Returns true if the snapshot is stale.
881    pub fn is_stale(&self, max_age_ms: u64) -> bool {
882        self.age_ms() > max_age_ms
883    }
884
885    /// Converts the snapshot back to a WsOrderBook.
886    pub fn to_order_book(&self) -> WsOrderBook {
887        WsOrderBook {
888            symbol: self.symbol.clone(),
889            asks: self.asks.clone(),
890            bids: self.bids.clone(),
891            update_id: self.update_id,
892            seq: self.seq,
893        }
894    }
895}