Skip to main content

bybit/models/
rpi_orderbook.rs

1use crate::prelude::*;
2
3/// Represents a single RPI (Real-time Price Improvement) order book level.
4///
5/// Each level contains the price, non-RPI size, and RPI size for either bids or asks.
6/// RPI orders are special orders that can improve prices for takers.
7#[derive(Clone, Debug)]
8pub struct RPIOrderbookLevel {
9    /// The price level.
10    pub price: f64,
11
12    /// The non-RPI size at this price level.
13    ///
14    /// This represents the regular order quantity at this price.
15    /// When delta data has size=0, it means all quotations for this price have been filled or cancelled.
16    pub non_rpi_size: f64,
17
18    /// The RPI size at this price level.
19    ///
20    /// This represents the RPI (Real-time Price Improvement) order quantity at this price.
21    /// When a bid RPI order crosses with a non-RPI ask price, the quantity of the bid RPI becomes invalid and is hidden.
22    /// When an ask RPI order crosses with a non-RPI bid price, the quantity of the ask RPI becomes invalid and is hidden.
23    pub rpi_size: f64,
24}
25
26impl<'de> Deserialize<'de> for RPIOrderbookLevel {
27    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
28    where
29        D: serde::Deserializer<'de>,
30    {
31        // Deserialize as an array of 3 strings
32        let arr: [String; 3] = Deserialize::deserialize(deserializer)?;
33
34        let price = arr[0].parse::<f64>().map_err(serde::de::Error::custom)?;
35        let non_rpi_size = arr[1].parse::<f64>().map_err(serde::de::Error::custom)?;
36        let rpi_size = arr[2].parse::<f64>().map_err(serde::de::Error::custom)?;
37
38        Ok(Self {
39            price,
40            non_rpi_size,
41            rpi_size,
42        })
43    }
44}
45
46impl Serialize for RPIOrderbookLevel {
47    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
48    where
49        S: serde::Serializer,
50    {
51        // Serialize as an array of 3 strings
52        let arr = [
53            self.price.to_string(),
54            self.non_rpi_size.to_string(),
55            self.rpi_size.to_string(),
56        ];
57        arr.serialize(serializer)
58    }
59}
60
61impl RPIOrderbookLevel {
62    /// Constructs a new RPIOrderbookLevel with specified price, non-RPI size, and RPI size.
63    pub fn new(price: f64, non_rpi_size: f64, rpi_size: f64) -> Self {
64        Self {
65            price,
66            non_rpi_size,
67            rpi_size,
68        }
69    }
70
71    /// Returns the total size (non-RPI + RPI) at this price level.
72    pub fn total_size(&self) -> f64 {
73        self.non_rpi_size + self.rpi_size
74    }
75
76    /// Returns true if this level has any RPI size.
77    pub fn has_rpi(&self) -> bool {
78        self.rpi_size > 0.0
79    }
80
81    /// Returns true if this level has any non-RPI size.
82    pub fn has_non_rpi(&self) -> bool {
83        self.non_rpi_size > 0.0
84    }
85
86    /// Returns the notional value (price × total size).
87    pub fn notional_value(&self) -> f64 {
88        self.price * self.total_size()
89    }
90
91    /// Returns the RPI ratio (RPI size / total size).
92    pub fn rpi_ratio(&self) -> f64 {
93        let total = self.total_size();
94        if total == 0.0 {
95            return 0.0;
96        }
97        self.rpi_size / total
98    }
99
100    /// Returns the non-RPI ratio (non-RPI size / total size).
101    pub fn non_rpi_ratio(&self) -> f64 {
102        let total = self.total_size();
103        if total == 0.0 {
104            return 0.0;
105        }
106        self.non_rpi_size / total
107    }
108
109    /// Returns the effective price for takers (considering RPI improvement).
110    pub fn effective_taker_price(&self, is_buy: bool) -> f64 {
111        if self.has_rpi() {
112            // RPI orders can provide price improvement
113            let improvement = if is_buy {
114                // For buy orders, RPI ask orders might provide better prices
115                -self.price * 0.0001 // 0.01% improvement estimate
116            } else {
117                // For sell orders, RPI bid orders might provide better prices
118                self.price * 0.0001 // 0.01% improvement estimate
119            };
120            self.price + improvement
121        } else {
122            self.price
123        }
124    }
125
126    /// Returns the price impact if this level were consumed.
127    pub fn price_impact(&self, reference_price: f64) -> f64 {
128        if reference_price == 0.0 {
129            return 0.0;
130        }
131        (self.price - reference_price).abs() / reference_price
132    }
133
134    /// Returns whether this level provides price improvement over a reference price.
135    pub fn provides_price_improvement(&self, reference_price: f64, is_buy: bool) -> bool {
136        if is_buy {
137            // For buy orders, lower price is better
138            self.price < reference_price
139        } else {
140            // For sell orders, higher price is better
141            self.price > reference_price
142        }
143    }
144
145    /// Returns the improvement amount over a reference price.
146    pub fn improvement_amount(&self, reference_price: f64, is_buy: bool) -> f64 {
147        if self.provides_price_improvement(reference_price, is_buy) {
148            if is_buy {
149                reference_price - self.price
150            } else {
151                self.price - reference_price
152            }
153        } else {
154            0.0
155        }
156    }
157
158    /// Returns the improvement percentage over a reference price.
159    pub fn improvement_percentage(&self, reference_price: f64, is_buy: bool) -> f64 {
160        if reference_price == 0.0 {
161            return 0.0;
162        }
163        self.improvement_amount(reference_price, is_buy) / reference_price
164    }
165
166    /// Returns a scaled version of this level.
167    pub fn scaled(&self, factor: f64) -> Self {
168        Self {
169            price: self.price,
170            non_rpi_size: self.non_rpi_size * factor,
171            rpi_size: self.rpi_size * factor,
172        }
173    }
174
175    /// Returns whether this level is valid (positive price and at least one size > 0).
176    pub fn is_valid(&self) -> bool {
177        self.price > 0.0 && (self.non_rpi_size > 0.0 || self.rpi_size > 0.0)
178    }
179
180    /// Returns the weighted average price considering RPI probability.
181    pub fn weighted_price_with_rpi_probability(&self, rpi_execution_probability: f64) -> f64 {
182        let rpi_price = self.effective_taker_price(true); // Using buy side for calculation
183        self.price * (1.0 - rpi_execution_probability) + rpi_price * rpi_execution_probability
184    }
185}
186
187/// Represents the RPI (Real-time Price Improvement) order book for a trading pair.
188///
189/// Contains the current bid and ask levels with RPI information, along with metadata.
190/// RPI order books show both regular orders and RPI orders, which can provide price improvement.
191#[derive(Serialize, Deserialize, Clone, Debug)]
192pub struct RPIOrderbook {
193    /// The trading pair symbol (e.g., "BTCUSDT").
194    #[serde(rename = "s")]
195    pub symbol: String,
196
197    /// A list of ask (sell) orders with RPI information.
198    ///
199    /// Each element is an array of [price, non-RPI size, RPI size].
200    /// Sorted by price in ascending order.
201    #[serde(rename = "a")]
202    pub asks: Vec<RPIOrderbookLevel>,
203
204    /// A list of bid (buy) orders with RPI information.
205    ///
206    /// Each element is an array of [price, non-RPI size, RPI size].
207    /// Sorted by price in descending order.
208    #[serde(rename = "b")]
209    pub bids: Vec<RPIOrderbookLevel>,
210
211    /// The timestamp (ms) that the system generates the data.
212    #[serde(rename = "ts")]
213    pub timestamp: u64,
214
215    /// Update ID, is always in sequence corresponds to `u` in the 50-level WebSocket RPI orderbook stream.
216    #[serde(rename = "u")]
217    pub update_id: u64,
218
219    /// Cross sequence.
220    ///
221    /// You can use this field to compare different levels orderbook data, and for the smaller seq,
222    /// then it means the data is generated earlier.
223    #[serde(rename = "seq")]
224    pub sequence: u64,
225
226    /// The timestamp from the matching engine when this orderbook data is produced.
227    /// It can be correlated with `T` from public trade channel.
228    #[serde(rename = "cts")]
229    pub matching_engine_timestamp: u64,
230}
231
232impl RPIOrderbook {
233    /// Returns the best ask price (lowest ask).
234    pub fn best_ask(&self) -> Option<f64> {
235        self.asks.first().map(|ask| ask.price)
236    }
237
238    /// Returns the best bid price (highest bid).
239    pub fn best_bid(&self) -> Option<f64> {
240        self.bids.first().map(|bid| bid.price)
241    }
242
243    /// Returns the best ask with RPI information.
244    pub fn best_ask_with_rpi(&self) -> Option<&RPIOrderbookLevel> {
245        self.asks.first()
246    }
247
248    /// Returns the best bid with RPI information.
249    pub fn best_bid_with_rpi(&self) -> Option<&RPIOrderbookLevel> {
250        self.bids.first()
251    }
252
253    /// Returns the bid-ask spread.
254    pub fn spread(&self) -> Option<f64> {
255        match (self.best_bid(), self.best_ask()) {
256            (Some(bid), Some(ask)) => Some(ask - bid),
257            _ => None,
258        }
259    }
260
261    /// Returns the mid price (average of best bid and ask).
262    pub fn mid_price(&self) -> Option<f64> {
263        match (self.best_bid(), self.best_ask()) {
264            (Some(bid), Some(ask)) => Some((bid + ask) / 2.0),
265            _ => None,
266        }
267    }
268
269    /// Returns the spread as a percentage of mid price.
270    pub fn spread_percentage(&self) -> Option<f64> {
271        match (self.spread(), self.mid_price()) {
272            (Some(spread), Some(mid)) if mid != 0.0 => Some(spread / mid),
273            _ => None,
274        }
275    }
276
277    /// Returns the total RPI size on the ask side.
278    pub fn total_ask_rpi_size(&self) -> f64 {
279        self.asks.iter().map(|ask| ask.rpi_size).sum()
280    }
281
282    /// Returns the total non-RPI size on the ask side.
283    pub fn total_ask_non_rpi_size(&self) -> f64 {
284        self.asks.iter().map(|ask| ask.non_rpi_size).sum()
285    }
286
287    /// Returns the total RPI size on the bid side.
288    pub fn total_bid_rpi_size(&self) -> f64 {
289        self.bids.iter().map(|bid| bid.rpi_size).sum()
290    }
291
292    /// Returns the total non-RPI size on the bid side.
293    pub fn total_bid_non_rpi_size(&self) -> f64 {
294        self.bids.iter().map(|bid| bid.non_rpi_size).sum()
295    }
296
297    /// Returns the total size (RPI + non-RPI) on the ask side.
298    pub fn total_ask_size(&self) -> f64 {
299        self.asks.iter().map(|ask| ask.total_size()).sum()
300    }
301
302    /// Returns the total size (RPI + non-RPI) on the bid side.
303    pub fn total_bid_size(&self) -> f64 {
304        self.bids.iter().map(|bid| bid.total_size()).sum()
305    }
306
307    /// Returns the total notional value on the ask side.
308    pub fn total_ask_notional(&self) -> f64 {
309        self.asks.iter().map(|ask| ask.notional_value()).sum()
310    }
311
312    /// Returns the total notional value on the bid side.
313    pub fn total_bid_notional(&self) -> f64 {
314        self.bids.iter().map(|bid| bid.notional_value()).sum()
315    }
316
317    /// Returns the average RPI ratio on the ask side.
318    pub fn average_ask_rpi_ratio(&self) -> f64 {
319        let total_ask_size = self.total_ask_size();
320        if total_ask_size == 0.0 {
321            return 0.0;
322        }
323        self.total_ask_rpi_size() / total_ask_size
324    }
325
326    /// Returns the average RPI ratio on the bid side.
327    pub fn average_bid_rpi_ratio(&self) -> f64 {
328        let total_bid_size = self.total_bid_size();
329        if total_bid_size == 0.0 {
330            return 0.0;
331        }
332        self.total_bid_rpi_size() / total_bid_size
333    }
334
335    /// Returns the bid-ask RPI ratio difference.
336    pub fn rpi_ratio_imbalance(&self) -> f64 {
337        self.average_bid_rpi_ratio() - self.average_ask_rpi_ratio()
338    }
339
340    /// Returns the order book imbalance considering RPI sizes.
341    pub fn order_book_imbalance_with_rpi(&self) -> f64 {
342        let total_bid = self.total_bid_size();
343        let total_ask = self.total_ask_size();
344        let total = total_bid + total_ask;
345        if total == 0.0 {
346            return 0.0;
347        }
348        (total_bid - total_ask) / total
349    }
350
351    /// Returns the weighted average ask price considering RPI improvement.
352    pub fn weighted_average_ask_price_with_rpi(&self, target_quantity: f64) -> Option<f64> {
353        let mut remaining = target_quantity;
354        let mut total_value = 0.0;
355
356        for ask in &self.asks {
357            let qty_to_take = ask.total_size().min(remaining);
358            // Use effective price considering RPI improvement for takers
359            let effective_price = ask.effective_taker_price(false);
360            total_value += qty_to_take * effective_price;
361            remaining -= qty_to_take;
362
363            if remaining <= 0.0 {
364                break;
365            }
366        }
367
368        if remaining > 0.0 {
369            None
370        } else {
371            Some(total_value / target_quantity)
372        }
373    }
374
375    /// Returns the weighted average bid price considering RPI improvement.
376    pub fn weighted_average_bid_price_with_rpi(&self, target_quantity: f64) -> Option<f64> {
377        let mut remaining = target_quantity;
378        let mut total_value = 0.0;
379
380        for bid in &self.bids {
381            let qty_to_take = bid.total_size().min(remaining);
382            // Use effective price considering RPI improvement for takers
383            let effective_price = bid.effective_taker_price(true);
384            total_value += qty_to_take * effective_price;
385            remaining -= qty_to_take;
386
387            if remaining <= 0.0 {
388                break;
389            }
390        }
391
392        if remaining > 0.0 {
393            None
394        } else {
395            Some(total_value / target_quantity)
396        }
397    }
398
399    /// Returns the price impact for a given quantity considering RPI.
400    pub fn ask_price_impact_with_rpi(&self, quantity: f64) -> Option<f64> {
401        let wap = self.weighted_average_ask_price_with_rpi(quantity)?;
402        let best_ask = self.best_ask()?;
403        Some((wap - best_ask) / best_ask)
404    }
405
406    /// Returns the price impact for a given quantity considering RPI.
407    pub fn bid_price_impact_with_rpi(&self, quantity: f64) -> Option<f64> {
408        let wap = self.weighted_average_bid_price_with_rpi(quantity)?;
409        let best_bid = self.best_bid()?;
410        Some((best_bid - wap) / best_bid)
411    }
412
413    /// Returns the expected price improvement for takers.
414    pub fn expected_taker_improvement(&self, is_buy: bool, quantity: f64) -> Option<f64> {
415        let (wap_with_rpi, best_price) = if is_buy {
416            (
417                self.weighted_average_ask_price_with_rpi(quantity)?,
418                self.best_ask()?,
419            )
420        } else {
421            (
422                self.weighted_average_bid_price_with_rpi(quantity)?,
423                self.best_bid()?,
424            )
425        };
426
427        if is_buy {
428            // For buy orders, lower price is better
429            Some((best_price - wap_with_rpi) / best_price)
430        } else {
431            // For sell orders, higher price is better
432            Some((wap_with_rpi - best_price) / best_price)
433        }
434    }
435
436    /// Returns the liquidity score considering RPI availability.
437    pub fn liquidity_score_with_rpi(&self) -> f64 {
438        let spread_score = match self.spread_percentage() {
439            Some(spread_pct) => 1.0 / (1.0 + spread_pct * 1000.0),
440            None => 0.0,
441        };
442
443        let depth_score = {
444            let total_qty = self.total_ask_size() + self.total_bid_size();
445            total_qty / (total_qty + 1000.0)
446        };
447
448        let rpi_score = {
449            let avg_rpi_ratio = (self.average_ask_rpi_ratio() + self.average_bid_rpi_ratio()) / 2.0;
450            avg_rpi_ratio
451        };
452
453        spread_score * 0.3 + depth_score * 0.3 + rpi_score * 0.4
454    }
455
456    /// Returns the timestamp as a DateTime.
457    pub fn timestamp_datetime(&self) -> chrono::DateTime<chrono::Utc> {
458        chrono::DateTime::from_timestamp((self.timestamp / 1000) as i64, 0)
459            .unwrap_or_else(chrono::Utc::now)
460    }
461
462    /// Returns the matching engine timestamp as a DateTime.
463    pub fn matching_engine_timestamp_datetime(&self) -> chrono::DateTime<chrono::Utc> {
464        chrono::DateTime::from_timestamp((self.matching_engine_timestamp / 1000) as i64, 0)
465            .unwrap_or_else(chrono::Utc::now)
466    }
467
468    /// Returns the processing latency.
469    pub fn processing_latency_ms(&self) -> i64 {
470        if self.matching_engine_timestamp > self.timestamp {
471            (self.matching_engine_timestamp - self.timestamp) as i64
472        } else {
473            (self.timestamp - self.matching_engine_timestamp) as i64
474        }
475    }
476}