Skip to main content

bybit/models/
order_book.rs

1use crate::prelude::*;
2
3/// Represents the order book for a trading pair.
4///
5/// Contains the current bid and ask levels, along with metadata like the update ID. Bots use this to analyze market depth and liquidity in perpetual futures.
6#[derive(Serialize, Deserialize, Clone, Debug)]
7pub struct OrderBook {
8    /// The trading pair symbol (e.g., "BTCUSDT").
9    ///
10    /// Confirms the trading pair for the order book. Bots should verify this matches the requested symbol.
11    #[serde(rename = "s")]
12    pub symbol: String,
13
14    /// A list of ask (sell) orders.
15    ///
16    /// Contains the current ask prices and quantities. Bots use this to assess selling pressure and determine resistance levels in perpetual futures.
17    #[serde(rename = "a")]
18    pub asks: Vec<Ask>,
19
20    /// A list of bid (buy) orders.
21    ///
22    /// Contains the current bid prices and quantities. Bots use this to assess buying support and determine support levels in perpetual futures.
23    #[serde(rename = "b")]
24    pub bids: Vec<Bid>,
25
26    /// The timestamp of the order book snapshot (Unix timestamp in milliseconds).
27    ///
28    /// Indicates when the order book data was captured. Bots should use this to ensure the data is recent, as stale order book data can lead to poor trading decisions.
29    #[serde(rename = "ts")]
30    pub timestamp: u64,
31
32    /// The update ID of the order book.
33    ///
34    /// A unique identifier for the order book snapshot. Bots can use this to track updates and ensure they’re processing the latest data, especially in WebSocket streams.
35    #[serde(rename = "u")]
36    pub update_id: u64,
37
38    /// Cross sequence.
39    ///
40    /// You can use this field to compare different levels orderbook data, and for the smaller seq,
41    /// then it means the data is generated earlier.
42    #[serde(rename = "seq")]
43    pub sequence: u64,
44
45    /// The timestamp from the matching engine when this orderbook data is produced.
46    /// It can be correlated with `T` from public trade channel.
47    #[serde(rename = "cts")]
48    pub matching_engine_timestamp: u64,
49}
50
51impl OrderBook {
52    /// Returns the best ask price (lowest ask).
53    pub fn best_ask(&self) -> Option<f64> {
54        self.asks.first().map(|ask| ask.price)
55    }
56
57    /// Returns the best bid price (highest bid).
58    pub fn best_bid(&self) -> Option<f64> {
59        self.bids.first().map(|bid| bid.price)
60    }
61
62    /// Returns the bid-ask spread.
63    pub fn spread(&self) -> Option<f64> {
64        match (self.best_bid(), self.best_ask()) {
65            (Some(bid), Some(ask)) => Some(ask - bid),
66            _ => None,
67        }
68    }
69
70    /// Returns the mid price (average of best bid and ask).
71    pub fn mid_price(&self) -> Option<f64> {
72        match (self.best_bid(), self.best_ask()) {
73            (Some(bid), Some(ask)) => Some((bid + ask) / 2.0),
74            _ => None,
75        }
76    }
77
78    /// Returns the spread as a percentage of mid price.
79    pub fn spread_percentage(&self) -> Option<f64> {
80        match (self.spread(), self.mid_price()) {
81            (Some(spread), Some(mid)) if mid != 0.0 => Some(spread / mid),
82            _ => None,
83        }
84    }
85
86    /// Returns the total quantity on the ask side.
87    pub fn total_ask_quantity(&self) -> f64 {
88        self.asks.iter().map(|ask| ask.qty).sum()
89    }
90
91    /// Returns the total quantity on the bid side.
92    pub fn total_bid_quantity(&self) -> f64 {
93        self.bids.iter().map(|bid| bid.qty).sum()
94    }
95
96    /// Returns the total quantity (bids + asks).
97    pub fn total_quantity(&self) -> f64 {
98        self.total_bid_quantity() + self.total_ask_quantity()
99    }
100
101    /// Returns the bid-ask quantity ratio.
102    pub fn bid_ask_quantity_ratio(&self) -> f64 {
103        let total_bid = self.total_bid_quantity();
104        let total_ask = self.total_ask_quantity();
105        if total_ask == 0.0 {
106            return 0.0;
107        }
108        total_bid / total_ask
109    }
110
111    /// Returns the order book imbalance (bid - ask) / (bid + ask).
112    pub fn order_book_imbalance(&self) -> f64 {
113        let total_bid = self.total_bid_quantity();
114        let total_ask = self.total_ask_quantity();
115        let total = total_bid + total_ask;
116        if total == 0.0 {
117            return 0.0;
118        }
119        (total_bid - total_ask) / total
120    }
121
122    /// Returns the weighted average price for a given quantity on the ask side.
123    pub fn weighted_average_ask_price(&self, target_quantity: f64) -> Option<f64> {
124        let mut remaining = target_quantity;
125        let mut total_value = 0.0;
126
127        for ask in &self.asks {
128            let qty_to_take = ask.qty.min(remaining);
129            total_value += qty_to_take * ask.price;
130            remaining -= qty_to_take;
131
132            if remaining <= 0.0 {
133                break;
134            }
135        }
136
137        if remaining > 0.0 {
138            // Not enough liquidity
139            None
140        } else {
141            Some(total_value / target_quantity)
142        }
143    }
144
145    /// Returns the weighted average price for a given quantity on the bid side.
146    pub fn weighted_average_bid_price(&self, target_quantity: f64) -> Option<f64> {
147        let mut remaining = target_quantity;
148        let mut total_value = 0.0;
149
150        for bid in &self.bids {
151            let qty_to_take = bid.qty.min(remaining);
152            total_value += qty_to_take * bid.price;
153            remaining -= qty_to_take;
154
155            if remaining <= 0.0 {
156                break;
157            }
158        }
159
160        if remaining > 0.0 {
161            // Not enough liquidity
162            None
163        } else {
164            Some(total_value / target_quantity)
165        }
166    }
167
168    /// Returns the price impact for a given quantity on the ask side.
169    pub fn ask_price_impact(&self, quantity: f64) -> Option<f64> {
170        let wap = self.weighted_average_ask_price(quantity)?;
171        let best_ask = self.best_ask()?;
172        Some((wap - best_ask) / best_ask)
173    }
174
175    /// Returns the price impact for a given quantity on the bid side.
176    pub fn bid_price_impact(&self, quantity: f64) -> Option<f64> {
177        let wap = self.weighted_average_bid_price(quantity)?;
178        let best_bid = self.best_bid()?;
179        Some((best_bid - wap) / best_bid)
180    }
181
182    /// Returns the cumulative quantity up to a given price level on the ask side.
183    pub fn cumulative_ask_quantity_to_price(&self, price: f64) -> f64 {
184        self.asks
185            .iter()
186            .take_while(|ask| ask.price <= price)
187            .map(|ask| ask.qty)
188            .sum()
189    }
190
191    /// Returns the cumulative quantity up to a given price level on the bid side.
192    pub fn cumulative_bid_quantity_to_price(&self, price: f64) -> f64 {
193        self.bids
194            .iter()
195            .take_while(|bid| bid.price >= price)
196            .map(|bid| bid.qty)
197            .sum()
198    }
199
200    /// Returns the price level that contains a given cumulative quantity on the ask side.
201    pub fn ask_price_for_cumulative_quantity(&self, target_quantity: f64) -> Option<f64> {
202        let mut cumulative = 0.0;
203        for ask in &self.asks {
204            cumulative += ask.qty;
205            if cumulative >= target_quantity {
206                return Some(ask.price);
207            }
208        }
209        None
210    }
211
212    /// Returns the price level that contains a given cumulative quantity on the bid side.
213    pub fn bid_price_for_cumulative_quantity(&self, target_quantity: f64) -> Option<f64> {
214        let mut cumulative = 0.0;
215        for bid in &self.bids {
216            cumulative += bid.qty;
217            if cumulative >= target_quantity {
218                return Some(bid.price);
219            }
220        }
221        None
222    }
223
224    /// Returns the market depth (quantity within a percentage range of mid price).
225    pub fn market_depth(&self, percentage_range: f64) -> (f64, f64) {
226        let mid = match self.mid_price() {
227            Some(mid) => mid,
228            None => return (0.0, 0.0),
229        };
230
231        let lower_bound = mid * (1.0 - percentage_range / 100.0);
232        let upper_bound = mid * (1.0 + percentage_range / 100.0);
233
234        let bid_depth = self
235            .bids
236            .iter()
237            .filter(|bid| bid.price >= lower_bound)
238            .map(|bid| bid.qty)
239            .sum();
240
241        let ask_depth = self
242            .asks
243            .iter()
244            .filter(|ask| ask.price <= upper_bound)
245            .map(|ask| ask.qty)
246            .sum();
247
248        (bid_depth, ask_depth)
249    }
250
251    /// Returns the liquidity score (higher is more liquid).
252    pub fn liquidity_score(&self) -> f64 {
253        let spread_score = match self.spread_percentage() {
254            Some(spread_pct) => 1.0 / (1.0 + spread_pct * 1000.0), // Normalize spread
255            None => 0.0,
256        };
257
258        let depth_score = {
259            let total_qty = self.total_quantity();
260            total_qty / (total_qty + 1000.0) // Normalize depth
261        };
262
263        let imbalance_score = 1.0 - self.order_book_imbalance().abs();
264
265        spread_score * 0.4 + depth_score * 0.4 + imbalance_score * 0.2
266    }
267
268    /// Returns the timestamp as a DateTime.
269    pub fn timestamp_datetime(&self) -> chrono::DateTime<chrono::Utc> {
270        chrono::DateTime::from_timestamp((self.timestamp / 1000) as i64, 0)
271            .unwrap_or_else(chrono::Utc::now)
272    }
273
274    /// Returns the matching engine timestamp as a DateTime.
275    pub fn matching_engine_timestamp_datetime(&self) -> chrono::DateTime<chrono::Utc> {
276        chrono::DateTime::from_timestamp((self.matching_engine_timestamp / 1000) as i64, 0)
277            .unwrap_or_else(chrono::Utc::now)
278    }
279
280    /// Returns the latency between system generation and matching engine.
281    pub fn processing_latency_ms(&self) -> i64 {
282        if self.matching_engine_timestamp > self.timestamp {
283            (self.matching_engine_timestamp - self.timestamp) as i64
284        } else {
285            (self.timestamp - self.matching_engine_timestamp) as i64
286        }
287    }
288
289    /// Returns the VWAP (Volume Weighted Average Price) for the visible order book.
290    pub fn vwap(&self) -> Option<f64> {
291        let total_bid_value: f64 = self.bids.iter().map(|bid| bid.price * bid.qty).sum();
292        let total_ask_value: f64 = self.asks.iter().map(|ask| ask.price * ask.qty).sum();
293        let total_bid_qty = self.total_bid_quantity();
294        let total_ask_qty = self.total_ask_quantity();
295
296        let total_value = total_bid_value + total_ask_value;
297        let total_qty = total_bid_qty + total_ask_qty;
298
299        if total_qty == 0.0 {
300            None
301        } else {
302            Some(total_value / total_qty)
303        }
304    }
305
306    /// Returns the microprice (weighted by inverse of spread).
307    pub fn microprice(&self) -> Option<f64> {
308        let best_bid = self.best_bid()?;
309        let best_ask = self.best_ask()?;
310        let bid_qty = self.bids.first().map(|b| b.qty).unwrap_or(0.0);
311        let ask_qty = self.asks.first().map(|a| a.qty).unwrap_or(0.0);
312
313        let total_qty = bid_qty + ask_qty;
314        if total_qty == 0.0 {
315            return None;
316        }
317
318        Some((best_bid * ask_qty + best_ask * bid_qty) / total_qty)
319    }
320
321    /// Returns the order book slope (price change per unit quantity).
322    pub fn order_book_slope(&self, side: OrderBookSide, levels: usize) -> Option<f64> {
323        if levels < 2 {
324            return None;
325        }
326
327        let prices: Vec<f64>;
328        let quantities: Vec<f64>;
329
330        match side {
331            OrderBookSide::Bid => {
332                if self.bids.len() < levels {
333                    return None;
334                }
335                prices = self.bids[..levels].iter().map(|b| b.price).collect();
336                quantities = self.bids[..levels].iter().map(|b| b.qty).collect();
337            }
338            OrderBookSide::Ask => {
339                if self.asks.len() < levels {
340                    return None;
341                }
342                prices = self.asks[..levels].iter().map(|a| a.price).collect();
343                quantities = self.asks[..levels].iter().map(|a| a.qty).collect();
344            }
345        }
346
347        // Simple linear regression for slope
348        let n = levels as f64;
349        let sum_x: f64 = quantities.iter().sum();
350        let sum_y: f64 = prices.iter().sum();
351        let sum_xy: f64 = quantities
352            .iter()
353            .zip(prices.iter())
354            .map(|(x, y)| x * y)
355            .sum();
356        let sum_x2: f64 = quantities.iter().map(|x| x * x).sum();
357
358        let denominator = n * sum_x2 - sum_x * sum_x;
359        if denominator == 0.0 {
360            return None;
361        }
362
363        Some((n * sum_xy - sum_x * sum_y) / denominator)
364    }
365
366    /// Returns the order book curvature (second derivative).
367    pub fn order_book_curvature(&self, side: OrderBookSide, levels: usize) -> Option<f64> {
368        if levels < 3 {
369            return None;
370        }
371
372        // Use finite difference method for curvature
373        let slope1 = self.order_book_slope(side, levels - 1)?;
374        let slope2 = self.order_book_slope(side, levels)?;
375
376        // Average quantity difference for normalization
377        let avg_qty = match side {
378            OrderBookSide::Bid => {
379                if self.bids.len() < levels {
380                    return None;
381                }
382                self.bids[..levels].iter().map(|b| b.qty).sum::<f64>() / levels as f64
383            }
384            OrderBookSide::Ask => {
385                if self.asks.len() < levels {
386                    return None;
387                }
388                self.asks[..levels].iter().map(|a| a.qty).sum::<f64>() / levels as f64
389            }
390        };
391
392        if avg_qty == 0.0 {
393            return None;
394        }
395
396        Some((slope2 - slope1) / avg_qty)
397    }
398
399    /// Returns the order book resilience (how quickly liquidity replenishes).
400    pub fn order_book_resilience(&self) -> f64 {
401        let bid_slope = self.order_book_slope(OrderBookSide::Bid, 5).unwrap_or(0.0);
402        let ask_slope = self.order_book_slope(OrderBookSide::Ask, 5).unwrap_or(0.0);
403
404        // Negative slopes indicate decreasing liquidity with price (normal)
405        // More negative = less resilient
406        let bid_resilience = 1.0 / (1.0 + bid_slope.abs());
407        let ask_resilience = 1.0 / (1.0 + ask_slope.abs());
408
409        (bid_resilience + ask_resilience) / 2.0
410    }
411
412    /// Returns the order book toxicity (probability of informed trading).
413    pub fn order_book_toxicity(&self) -> f64 {
414        let imbalance = self.order_book_imbalance().abs();
415        let spread_pct = self.spread_percentage().unwrap_or(0.0);
416
417        // Higher imbalance and wider spread indicate higher toxicity
418        (imbalance * 0.6 + spread_pct * 100.0 * 0.4).min(1.0)
419    }
420
421    /// Returns the effective cost of trading (spread + impact for a given quantity).
422    pub fn effective_cost(&self, quantity: f64) -> Option<f64> {
423        let spread_pct = self.spread_percentage()?;
424        let bid_impact = self.bid_price_impact(quantity).unwrap_or(0.0);
425        let ask_impact = self.ask_price_impact(quantity).unwrap_or(0.0);
426
427        Some(spread_pct + (bid_impact + ask_impact) / 2.0)
428    }
429
430    /// Returns the market impact cost for a round trip trade.
431    pub fn round_trip_cost(&self, quantity: f64) -> Option<f64> {
432        let bid_wap = self.weighted_average_bid_price(quantity)?;
433        let ask_wap = self.weighted_average_ask_price(quantity)?;
434
435        Some((ask_wap - bid_wap) / bid_wap)
436    }
437
438    /// Returns the optimal order size based on market impact.
439    pub fn optimal_order_size(&self, max_impact: f64) -> Option<f64> {
440        // Binary search for optimal size
441        let mut low = 0.0;
442        let mut high = self.total_quantity() * 0.1; // Don't exceed 10% of visible liquidity
443        let mut best_size = 0.0;
444
445        for _ in 0..20 {
446            // 20 iterations should be enough
447            let mid = (low + high) / 2.0;
448            let impact = self
449                .bid_price_impact(mid)
450                .unwrap_or(1.0)
451                .max(self.ask_price_impact(mid).unwrap_or(1.0));
452
453            if impact <= max_impact {
454                best_size = mid;
455                low = mid;
456            } else {
457                high = mid;
458            }
459        }
460
461        if best_size > 0.0 {
462            Some(best_size)
463        } else {
464            None
465        }
466    }
467}
468
469/// Enum representing order book sides.
470#[derive(Debug, Clone, Copy)]
471pub enum OrderBookSide {
472    Bid,
473    Ask,
474}