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///     "e":"bookTicker",
35///     "u":22606535573,
36///     "s":"ETHUSDT",
37///     "b":"1215.27000000",
38///     "B":"32.49110000",
39///     "a":"1215.28000000",
40///     "A":"13.93900000",
41///     "T":1671621244670,
42///     "E":1671621244673
43/// }
44/// ```
45// `Serialize` is intentionally not derived: the `subscription_id` field is deserialized from the
46// wire `s` symbol via `de_ob_l1_subscription_id`, so a derived `Serialize` would emit this struct's
47// own field shape and not round-trip — and nothing serializes these wire types.
48#[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize)]
49pub struct BinanceOrderBookL1 {
50    #[serde(alias = "s", deserialize_with = "de_ob_l1_subscription_id")]
51    pub subscription_id: SubscriptionId,
52    /// The venue's transaction time (`T`) **where Binance sends one**, else the host clock at
53    /// deserialisation.
54    ///
55    /// The two Binance surfaces this type serves differ, and the payload examples above show it:
56    /// USD-M futures `bookTicker` carries `T`, Spot `bookTicker` carries only `{u, s, b, B, a, A}`.
57    /// So on Spot the `default` fires on every message and this is a **local** instant, not a venue
58    /// one. It is propagated to both `MarketEvent::time_exchange` and
59    /// [`OrderBookL1::last_update_time`], which keeps those two consistent as that field's contract
60    /// requires — but it means neither can be ranked against a Binance *trade*'s timestamp, which
61    /// is the venue's. See `OrderBookL1::last_update_time` and, for the consumer side,
62    /// `DefaultInstrumentMarketData::price` in the `rustrade` crate.
63    ///
64    /// `Utc::now` rather than an `Option`: `time_exchange` is not optional, so the receipt instant
65    /// is the only available answer, and it is the correct ordering key for a live stream where
66    /// receipt order *is* arrival order. Recording such a feed to a file bakes the recorder's clock
67    /// in permanently, which is worth knowing before replaying one against venue-stamped data.
68    #[serde(
69        alias = "T",
70        deserialize_with = "rustrade_integration::serde::de::de_u64_epoch_ms_as_datetime_utc",
71        default = "Utc::now"
72    )]
73    pub time: DateTime<Utc>,
74    #[serde(alias = "b", with = "rust_decimal::serde::str")]
75    pub best_bid_price: Decimal,
76    #[serde(alias = "B", with = "rust_decimal::serde::str")]
77    pub best_bid_amount: Decimal,
78    #[serde(alias = "a", with = "rust_decimal::serde::str")]
79    pub best_ask_price: Decimal,
80    #[serde(alias = "A", with = "rust_decimal::serde::str")]
81    pub best_ask_amount: Decimal,
82}
83
84impl Identifier<Option<SubscriptionId>> for BinanceOrderBookL1 {
85    fn id(&self) -> Option<SubscriptionId> {
86        Some(self.subscription_id.clone())
87    }
88}
89
90impl<InstrumentKey> From<(ExchangeId, InstrumentKey, BinanceOrderBookL1)>
91    for MarketIter<InstrumentKey, OrderBookL1>
92{
93    fn from(
94        (exchange_id, instrument, book): (ExchangeId, InstrumentKey, BinanceOrderBookL1),
95    ) -> Self {
96        let best_ask = if book.best_ask_price.is_zero() {
97            None
98        } else {
99            Some(Level::new(book.best_ask_price, book.best_ask_amount))
100        };
101
102        let best_bid = if book.best_bid_price.is_zero() {
103            None
104        } else {
105            Some(Level::new(book.best_bid_price, book.best_bid_amount))
106        };
107
108        Self(vec![Ok(MarketEvent {
109            time_exchange: book.time,
110            time_received: Utc::now(),
111            exchange: exchange_id,
112            instrument,
113            kind: OrderBookL1 {
114                last_update_time: book.time,
115                best_bid,
116                best_ask,
117            },
118        })])
119    }
120}
121
122/// Deserialize a [`BinanceOrderBookL1`] "s" (eg/ "BTCUSDT") as the associated [`SubscriptionId`].
123///
124/// eg/ "@bookTicker|BTCUSDT"
125pub fn de_ob_l1_subscription_id<'de, D>(deserializer: D) -> Result<SubscriptionId, D::Error>
126where
127    D: serde::de::Deserializer<'de>,
128{
129    <&str as Deserialize>::deserialize(deserializer)
130        .map(|market| ExchangeSub::from((BinanceChannel::ORDER_BOOK_L1, market)).id())
131}
132
133#[cfg(test)]
134#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
135mod tests {
136    use super::*;
137
138    mod de {
139        use super::*;
140        use rust_decimal_macros::dec;
141
142        #[test]
143        fn test_binance_order_book_l1() {
144            struct TestCase {
145                input: &'static str,
146                expected: BinanceOrderBookL1,
147            }
148
149            let time = Utc::now();
150
151            let tests = vec![
152                TestCase {
153                    // TC0: valid Spot BinanceOrderBookL1
154                    input: r#"
155                    {
156                        "u":22606535573,
157                        "s":"ETHUSDT",
158                        "b":"1215.27000000",
159                        "B":"32.49110000",
160                        "a":"1215.28000000",
161                        "A":"13.93900000"
162                    }
163                "#,
164                    expected: BinanceOrderBookL1 {
165                        subscription_id: SubscriptionId::from("@bookTicker|ETHUSDT"),
166                        time,
167                        best_bid_price: dec!(1215.27000000),
168                        best_bid_amount: dec!(32.49110000),
169                        best_ask_price: dec!(1215.28000000),
170                        best_ask_amount: dec!(13.93900000),
171                    },
172                },
173                TestCase {
174                    // TC1: valid FuturePerpetual BinanceOrderBookL1
175                    input: r#"
176                    {
177                        "e":"bookTicker",
178                        "u":2286618712950,
179                        "s":"BTCUSDT",
180                        "b":"16858.90",
181                        "B":"13.692",
182                        "a":"16859.00",
183                        "A":"30.219",
184                        "T":1671621244670,
185                        "E":1671621244673
186                    }"#,
187                    expected: BinanceOrderBookL1 {
188                        subscription_id: SubscriptionId::from("@bookTicker|BTCUSDT"),
189                        time,
190                        best_bid_price: dec!(16858.90),
191                        best_bid_amount: dec!(13.692),
192                        best_ask_price: dec!(16859.00),
193                        best_ask_amount: dec!(30.219),
194                    },
195                },
196            ];
197
198            for (index, test) in tests.into_iter().enumerate() {
199                let actual = serde_json::from_str::<BinanceOrderBookL1>(test.input).unwrap();
200                let actual = BinanceOrderBookL1 { time, ..actual };
201                assert_eq!(actual, test.expected, "TC{} failed", index);
202            }
203        }
204    }
205}