Skip to main content

rustrade_data/exchange/binance/book/
l1.rs

1use crate::{
2    Identifier,
3    books::Level,
4    event::{MarketEvent, MarketIter},
5    exchange::{binance::channel::BinanceChannel, subscription::ExchangeSub},
6    subscription::book::OrderBookL1,
7};
8use chrono::{DateTime, Utc};
9use rust_decimal::Decimal;
10use rustrade_instrument::exchange::ExchangeId;
11use rustrade_integration::subscription::SubscriptionId;
12use serde::Deserialize;
13
14/// [`Binance`](super::super::Binance) real-time OrderBook Level1 (top of books) message.
15///
16/// ### Raw Payload Examples
17/// #### BinanceSpot OrderBookL1
18/// See docs: <https://binance-docs.github.io/apidocs/spot/en/#individual-symbol-book-ticker-streams>
19/// ```json
20/// {
21///     "u":22606535573,
22///     "s":"ETHUSDT",
23///     "b":"1215.27000000",
24///     "B":"32.49110000",
25///     "a":"1215.28000000",
26///     "A":"13.93900000"
27/// }
28/// ```
29///
30/// #### BinanceFuturesUsd OrderBookL1
31/// See docs: <https://binance-docs.github.io/apidocs/futures/en/#individual-symbol-book-ticker-streams>
32/// ```json
33/// {
34///     "u":22606535573,
35///     "s":"ETHUSDT",
36///     "b":"1215.27000000",
37///     "B":"32.49110000",
38///     "a":"1215.28000000",
39///     "A":"13.93900000"
40/// }
41/// ```
42// `Serialize` is intentionally not derived: the `subscription_id` field is deserialized from the
43// wire `s` symbol via `de_ob_l1_subscription_id`, so a derived `Serialize` would emit this struct's
44// own field shape and not round-trip — and nothing serializes these wire types.
45#[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize)]
46pub struct BinanceOrderBookL1 {
47    #[serde(alias = "s", deserialize_with = "de_ob_l1_subscription_id")]
48    pub subscription_id: SubscriptionId,
49    #[serde(
50        alias = "T",
51        deserialize_with = "rustrade_integration::serde::de::de_u64_epoch_ms_as_datetime_utc",
52        default = "Utc::now"
53    )]
54    pub time: DateTime<Utc>,
55    #[serde(alias = "b", with = "rust_decimal::serde::str")]
56    pub best_bid_price: Decimal,
57    #[serde(alias = "B", with = "rust_decimal::serde::str")]
58    pub best_bid_amount: Decimal,
59    #[serde(alias = "a", with = "rust_decimal::serde::str")]
60    pub best_ask_price: Decimal,
61    #[serde(alias = "A", with = "rust_decimal::serde::str")]
62    pub best_ask_amount: Decimal,
63}
64
65impl Identifier<Option<SubscriptionId>> for BinanceOrderBookL1 {
66    fn id(&self) -> Option<SubscriptionId> {
67        Some(self.subscription_id.clone())
68    }
69}
70
71impl<InstrumentKey> From<(ExchangeId, InstrumentKey, BinanceOrderBookL1)>
72    for MarketIter<InstrumentKey, OrderBookL1>
73{
74    fn from(
75        (exchange_id, instrument, book): (ExchangeId, InstrumentKey, BinanceOrderBookL1),
76    ) -> Self {
77        let best_ask = if book.best_ask_price.is_zero() {
78            None
79        } else {
80            Some(Level::new(book.best_ask_price, book.best_ask_amount))
81        };
82
83        let best_bid = if book.best_bid_price.is_zero() {
84            None
85        } else {
86            Some(Level::new(book.best_bid_price, book.best_bid_amount))
87        };
88
89        Self(vec![Ok(MarketEvent {
90            time_exchange: book.time,
91            time_received: Utc::now(),
92            exchange: exchange_id,
93            instrument,
94            kind: OrderBookL1 {
95                last_update_time: book.time,
96                best_bid,
97                best_ask,
98            },
99        })])
100    }
101}
102
103/// Deserialize a [`BinanceOrderBookL1`] "s" (eg/ "BTCUSDT") as the associated [`SubscriptionId`].
104///
105/// eg/ "@bookTicker|BTCUSDT"
106pub fn de_ob_l1_subscription_id<'de, D>(deserializer: D) -> Result<SubscriptionId, D::Error>
107where
108    D: serde::de::Deserializer<'de>,
109{
110    <&str as Deserialize>::deserialize(deserializer)
111        .map(|market| ExchangeSub::from((BinanceChannel::ORDER_BOOK_L1, market)).id())
112}
113
114#[cfg(test)]
115#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
116mod tests {
117    use super::*;
118
119    mod de {
120        use super::*;
121        use rust_decimal_macros::dec;
122
123        #[test]
124        fn test_binance_order_book_l1() {
125            struct TestCase {
126                input: &'static str,
127                expected: BinanceOrderBookL1,
128            }
129
130            let time = Utc::now();
131
132            let tests = vec![
133                TestCase {
134                    // TC0: valid Spot BinanceOrderBookL1
135                    input: r#"
136                    {
137                        "u":22606535573,
138                        "s":"ETHUSDT",
139                        "b":"1215.27000000",
140                        "B":"32.49110000",
141                        "a":"1215.28000000",
142                        "A":"13.93900000"
143                    }
144                "#,
145                    expected: BinanceOrderBookL1 {
146                        subscription_id: SubscriptionId::from("@bookTicker|ETHUSDT"),
147                        time,
148                        best_bid_price: dec!(1215.27000000),
149                        best_bid_amount: dec!(32.49110000),
150                        best_ask_price: dec!(1215.28000000),
151                        best_ask_amount: dec!(13.93900000),
152                    },
153                },
154                TestCase {
155                    // TC1: valid FuturePerpetual BinanceOrderBookL1
156                    input: r#"
157                    {
158                        "e":"bookTicker",
159                        "u":2286618712950,
160                        "s":"BTCUSDT",
161                        "b":"16858.90",
162                        "B":"13.692",
163                        "a":"16859.00",
164                        "A":"30.219",
165                        "T":1671621244670,
166                        "E":1671621244673
167                    }"#,
168                    expected: BinanceOrderBookL1 {
169                        subscription_id: SubscriptionId::from("@bookTicker|BTCUSDT"),
170                        time,
171                        best_bid_price: dec!(16858.90),
172                        best_bid_amount: dec!(13.692),
173                        best_ask_price: dec!(16859.00),
174                        best_ask_amount: dec!(30.219),
175                    },
176                },
177            ];
178
179            for (index, test) in tests.into_iter().enumerate() {
180                let actual = serde_json::from_str::<BinanceOrderBookL1>(test.input).unwrap();
181                let actual = BinanceOrderBookL1 { time, ..actual };
182                assert_eq!(actual, test.expected, "TC{} failed", index);
183            }
184        }
185    }
186}