Skip to main content

rustrade_data/exchange/bybit/book/
mod.rs

1use crate::{
2    books::{Level, OrderBook, OrderBookTimes},
3    event::{MarketEvent, MarketIter},
4    subscription::book::OrderBookEvent,
5};
6use chrono::Utc;
7use rust_decimal::Decimal;
8use rustrade_instrument::exchange::ExchangeId;
9use serde::{Deserialize, Serialize};
10
11use super::message::{BybitPayload, BybitPayloadKind};
12
13/// Level 1 OrderBook types.
14pub mod l1;
15
16/// Level 2 OrderBook types.
17pub mod l2;
18
19/// Terse type alias for an [`BybitOrderBookMessage`] OrderBook WebSocket message.
20pub type BybitOrderBookMessage = BybitPayload<BybitOrderBookInner>;
21
22#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
23pub struct BybitOrderBookInner {
24    #[serde(rename = "b")]
25    pub bids: Vec<BybitLevel>,
26
27    #[serde(rename = "a")]
28    pub asks: Vec<BybitLevel>,
29
30    #[serde(rename = "u")]
31    pub update_id: u64,
32
33    #[serde(rename = "seq")]
34    pub sequence: u64,
35}
36
37impl<InstrumentKey> From<(ExchangeId, InstrumentKey, BybitOrderBookMessage)>
38    for MarketIter<InstrumentKey, OrderBookEvent>
39{
40    fn from(
41        (exchange, instrument, message): (ExchangeId, InstrumentKey, BybitOrderBookMessage),
42    ) -> Self {
43        let time_received = Utc::now();
44        // Bybit's `ts` broadcast time feeds both the book's `time_exchange` and the
45        // `MarketEvent` envelope's `time_exchange` below — bind once so the shared
46        // source is explicit.
47        let time_exchange = message.time;
48        let orderbook = OrderBook::new(
49            message.data.sequence,
50            // `time_exchange` is Bybit's `ts` broadcast time → `time_exchange` only.
51            // Bybit sends no matching-engine time, so `time_engine` is `None`.
52            OrderBookTimes {
53                time_engine: None,
54                time_exchange: Some(time_exchange),
55                time_received,
56            },
57            message.data.bids,
58            message.data.asks,
59        );
60
61        let kind = match message.kind {
62            BybitPayloadKind::Snapshot => OrderBookEvent::Snapshot(orderbook),
63            BybitPayloadKind::Delta => OrderBookEvent::Update(orderbook),
64        };
65
66        Self(vec![Ok(MarketEvent {
67            time_exchange,
68            time_received,
69            exchange,
70            instrument,
71            kind,
72        })])
73    }
74}
75
76/// [`Bybit`](super::Bybit) OrderBook level.
77///
78/// #### Raw Payload Examples
79/// See docs: <https://bybit-exchange.github.io/docs/v5/websocket/public/orderbook#response-parameters>
80///
81/// ```json
82/// ["16493.50", "0.006"]
83/// ```
84#[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Deserialize, Serialize)]
85pub struct BybitLevel {
86    #[serde(with = "rust_decimal::serde::str")]
87    pub price: Decimal,
88    #[serde(with = "rust_decimal::serde::str")]
89    pub amount: Decimal,
90}
91
92impl From<BybitLevel> for Level {
93    fn from(level: BybitLevel) -> Self {
94        Self {
95            price: level.price,
96            amount: level.amount,
97        }
98    }
99}
100
101#[cfg(test)]
102#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
103mod tests {
104    use super::*;
105
106    mod de {
107        use super::*;
108        use rust_decimal_macros::dec;
109
110        #[test]
111        fn test_bybit_level() {
112            let input = r#"["16493.50", "0.006"]"#;
113            assert_eq!(
114                serde_json::from_str::<BybitLevel>(input).unwrap(),
115                BybitLevel {
116                    price: dec!(16493.50),
117                    amount: dec!(0.006)
118                },
119            )
120        }
121    }
122}