1use crate::prelude::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum OrderBookSide {
6 Bid,
8 Ask,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum TradeSide {
15 Buy,
17 Sell,
19}
20
21#[derive(Debug, Clone)]
23pub struct DepthProfile {
24 pub min_price: f64,
26 pub max_price: f64,
28 pub bucket_size: f64,
30 pub ask_buckets: Vec<f64>,
32 pub bid_buckets: Vec<f64>,
34}
35
36#[derive(Serialize, Deserialize, Debug, Clone)]
40#[serde(rename_all = "camelCase")]
41pub struct WsOrderBook {
42 #[serde(rename = "s")]
46 pub symbol: String,
47
48 #[serde(rename = "a")]
52 pub asks: Vec<Ask>,
53
54 #[serde(rename = "b")]
58 pub bids: Vec<Bid>,
59
60 #[serde(rename = "u")]
64 pub update_id: u64,
65
66 pub seq: u64,
70}
71
72impl WsOrderBook {
73 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 pub fn best_ask(&self) -> Option<f64> {
86 self.asks.first().map(|ask| ask.price)
87 }
88
89 pub fn best_ask_quantity(&self) -> Option<f64> {
91 self.asks.first().map(|ask| ask.qty)
92 }
93
94 pub fn best_bid(&self) -> Option<f64> {
96 self.bids.first().map(|bid| bid.price)
97 }
98
99 pub fn best_bid_quantity(&self) -> Option<f64> {
101 self.bids.first().map(|bid| bid.qty)
102 }
103
104 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 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 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 pub fn total_ask_quantity(&self) -> f64 {
130 self.asks.iter().map(|ask| ask.qty).sum()
131 }
132
133 pub fn total_bid_quantity(&self) -> f64 {
135 self.bids.iter().map(|bid| bid.qty).sum()
136 }
137
138 pub fn total_ask_value(&self) -> f64 {
140 self.asks.iter().map(|ask| ask.price * ask.qty).sum()
141 }
142
143 pub fn total_bid_value(&self) -> f64 {
145 self.bids.iter().map(|bid| bid.price * bid.qty).sum()
146 }
147
148 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Some(spread * (ask_weight + bid_weight) / 2.0)
394 } else {
395 None
396 }
397 }
398
399 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; depth_ratio / (1.0 + spread_ratio)
407 }
408
409 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 imbalance / (1.0 + spread)
417 }
418
419 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 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 pub fn to_json(&self) -> String {
456 serde_json::to_string(self).unwrap_or_default()
457 }
458
459 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; }
469
470 self.update_id = update.update_id;
472 self.seq = update.seq;
473
474 self.asks = update.asks.clone();
477 self.bids = update.bids.clone();
478
479 true
480 }
481
482 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 pub fn is_stale(&self, current_seq: u64, max_age: u64) -> bool {
493 self.age(current_seq) > max_age
494 }
495
496 pub fn market_quality_score(&self) -> f64 {
499 let mut score = 0.0;
500 let mut weight_sum = 0.0;
501
502 if let Some(spread_pct) = self.spread_percentage() {
504 let spread_score = 1.0 / (1.0 + spread_pct * 10.0); score += spread_score * 0.4;
506 weight_sum += 0.4;
507 }
508
509 let total_depth = self.total_bid_quantity() + self.total_ask_quantity();
511 let depth_score = (total_depth / 1000.0).min(1.0); score += depth_score * 0.3;
513 weight_sum += 0.3;
514
515 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 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 } else {
530 0.0
531 }
532 }
533
534 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 Some((buy_impact + sell_impact) / 2.0 + spread_cost / 2.0)
545 }
546
547 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 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 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 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 pub fn volatility_estimate(&self) -> Option<f64> {
618 let mid_price = self.mid_price()?;
619 let price_range = 0.01; 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 Some(1.0 - (total_liquidity / max_possible_liquidity))
632 } else {
633 None
634 }
635 }
636
637 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct OrderBookSnapshot {
843 pub symbol: String,
845 pub bids: Vec<Bid>,
847 pub asks: Vec<Ask>,
849 pub update_id: u64,
851 pub seq: u64,
853 pub timestamp: u64,
855}
856
857impl OrderBookSnapshot {
858 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 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 pub fn is_stale(&self, max_age_ms: u64) -> bool {
882 self.age_ms() > max_age_ms
883 }
884
885 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}