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}