bybit/models/
order_book.rs1use crate::prelude::*;
2
3#[derive(Serialize, Deserialize, Clone, Debug)]
7pub struct OrderBook {
8 #[serde(rename = "s")]
12 pub symbol: String,
13
14 #[serde(rename = "a")]
18 pub asks: Vec<Ask>,
19
20 #[serde(rename = "b")]
24 pub bids: Vec<Bid>,
25
26 #[serde(rename = "ts")]
30 pub timestamp: u64,
31
32 #[serde(rename = "u")]
36 pub update_id: u64,
37
38 #[serde(rename = "seq")]
43 pub sequence: u64,
44
45 #[serde(rename = "cts")]
48 pub matching_engine_timestamp: u64,
49}
50
51impl OrderBook {
52 pub fn best_ask(&self) -> Option<f64> {
54 self.asks.first().map(|ask| ask.price)
55 }
56
57 pub fn best_bid(&self) -> Option<f64> {
59 self.bids.first().map(|bid| bid.price)
60 }
61
62 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 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 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 pub fn total_ask_quantity(&self) -> f64 {
88 self.asks.iter().map(|ask| ask.qty).sum()
89 }
90
91 pub fn total_bid_quantity(&self) -> f64 {
93 self.bids.iter().map(|bid| bid.qty).sum()
94 }
95
96 pub fn total_quantity(&self) -> f64 {
98 self.total_bid_quantity() + self.total_ask_quantity()
99 }
100
101 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 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 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 None
140 } else {
141 Some(total_value / target_quantity)
142 }
143 }
144
145 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 None
163 } else {
164 Some(total_value / target_quantity)
165 }
166 }
167
168 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 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 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 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 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 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 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 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), None => 0.0,
256 };
257
258 let depth_score = {
259 let total_qty = self.total_quantity();
260 total_qty / (total_qty + 1000.0) };
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 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 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 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 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 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 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 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 pub fn order_book_curvature(&self, side: OrderBookSide, levels: usize) -> Option<f64> {
368 if levels < 3 {
369 return None;
370 }
371
372 let slope1 = self.order_book_slope(side, levels - 1)?;
374 let slope2 = self.order_book_slope(side, levels)?;
375
376 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 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 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 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 (imbalance * 0.6 + spread_pct * 100.0 * 0.4).min(1.0)
419 }
420
421 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 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 pub fn optimal_order_size(&self, max_impact: f64) -> Option<f64> {
440 let mut low = 0.0;
442 let mut high = self.total_quantity() * 0.1; let mut best_size = 0.0;
444
445 for _ in 0..20 {
446 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#[derive(Debug, Clone, Copy)]
471pub enum OrderBookSide {
472 Bid,
473 Ask,
474}