bybit/models/
rpi_orderbook.rs1use crate::prelude::*;
2
3#[derive(Clone, Debug)]
8pub struct RPIOrderbookLevel {
9 pub price: f64,
11
12 pub non_rpi_size: f64,
17
18 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 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 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 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 pub fn total_size(&self) -> f64 {
73 self.non_rpi_size + self.rpi_size
74 }
75
76 pub fn has_rpi(&self) -> bool {
78 self.rpi_size > 0.0
79 }
80
81 pub fn has_non_rpi(&self) -> bool {
83 self.non_rpi_size > 0.0
84 }
85
86 pub fn notional_value(&self) -> f64 {
88 self.price * self.total_size()
89 }
90
91 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 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 pub fn effective_taker_price(&self, is_buy: bool) -> f64 {
111 if self.has_rpi() {
112 let improvement = if is_buy {
114 -self.price * 0.0001 } else {
117 self.price * 0.0001 };
120 self.price + improvement
121 } else {
122 self.price
123 }
124 }
125
126 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 pub fn provides_price_improvement(&self, reference_price: f64, is_buy: bool) -> bool {
136 if is_buy {
137 self.price < reference_price
139 } else {
140 self.price > reference_price
142 }
143 }
144
145 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 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 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 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 pub fn weighted_price_with_rpi_probability(&self, rpi_execution_probability: f64) -> f64 {
182 let rpi_price = self.effective_taker_price(true); self.price * (1.0 - rpi_execution_probability) + rpi_price * rpi_execution_probability
184 }
185}
186
187#[derive(Serialize, Deserialize, Clone, Debug)]
192pub struct RPIOrderbook {
193 #[serde(rename = "s")]
195 pub symbol: String,
196
197 #[serde(rename = "a")]
202 pub asks: Vec<RPIOrderbookLevel>,
203
204 #[serde(rename = "b")]
209 pub bids: Vec<RPIOrderbookLevel>,
210
211 #[serde(rename = "ts")]
213 pub timestamp: u64,
214
215 #[serde(rename = "u")]
217 pub update_id: u64,
218
219 #[serde(rename = "seq")]
224 pub sequence: u64,
225
226 #[serde(rename = "cts")]
229 pub matching_engine_timestamp: u64,
230}
231
232impl RPIOrderbook {
233 pub fn best_ask(&self) -> Option<f64> {
235 self.asks.first().map(|ask| ask.price)
236 }
237
238 pub fn best_bid(&self) -> Option<f64> {
240 self.bids.first().map(|bid| bid.price)
241 }
242
243 pub fn best_ask_with_rpi(&self) -> Option<&RPIOrderbookLevel> {
245 self.asks.first()
246 }
247
248 pub fn best_bid_with_rpi(&self) -> Option<&RPIOrderbookLevel> {
250 self.bids.first()
251 }
252
253 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 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 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 pub fn total_ask_rpi_size(&self) -> f64 {
279 self.asks.iter().map(|ask| ask.rpi_size).sum()
280 }
281
282 pub fn total_ask_non_rpi_size(&self) -> f64 {
284 self.asks.iter().map(|ask| ask.non_rpi_size).sum()
285 }
286
287 pub fn total_bid_rpi_size(&self) -> f64 {
289 self.bids.iter().map(|bid| bid.rpi_size).sum()
290 }
291
292 pub fn total_bid_non_rpi_size(&self) -> f64 {
294 self.bids.iter().map(|bid| bid.non_rpi_size).sum()
295 }
296
297 pub fn total_ask_size(&self) -> f64 {
299 self.asks.iter().map(|ask| ask.total_size()).sum()
300 }
301
302 pub fn total_bid_size(&self) -> f64 {
304 self.bids.iter().map(|bid| bid.total_size()).sum()
305 }
306
307 pub fn total_ask_notional(&self) -> f64 {
309 self.asks.iter().map(|ask| ask.notional_value()).sum()
310 }
311
312 pub fn total_bid_notional(&self) -> f64 {
314 self.bids.iter().map(|bid| bid.notional_value()).sum()
315 }
316
317 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 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 pub fn rpi_ratio_imbalance(&self) -> f64 {
337 self.average_bid_rpi_ratio() - self.average_ask_rpi_ratio()
338 }
339
340 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 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 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 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 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 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 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 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 Some((best_price - wap_with_rpi) / best_price)
430 } else {
431 Some((wap_with_rpi - best_price) / best_price)
433 }
434 }
435
436 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 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 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 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}