Skip to main content

rustrade_data/exchange/binance/
trade.rs

1use super::BinanceChannel;
2use crate::{
3    Identifier,
4    event::{MarketEvent, MarketIter},
5    exchange::ExchangeSub,
6    subscription::trade::PublicTrade,
7};
8use chrono::{DateTime, Utc};
9use rustrade_instrument::{Side, exchange::ExchangeId};
10use rustrade_integration::subscription::SubscriptionId;
11use serde::{Deserialize, Serialize};
12use smol_str::format_smolstr;
13
14/// Binance real-time trade message.
15///
16/// Note:
17/// For [`BinanceFuturesUsd`](super::futures::BinanceFuturesUsd) this real-time stream is
18/// undocumented.
19///
20/// See discord: <https://discord.com/channels/910237311332151317/923160222711812126/975712874582388757>
21///
22/// ### Raw Payload Examples
23/// See docs: <https://binance-docs.github.io/apidocs/spot/en/#trade-streams>
24/// #### Spot Side::Buy Trade
25/// ```json
26/// {
27///     "e":"trade",
28///     "E":1649324825173,
29///     "s":"ETHUSDT",
30///     "t":1000000000,
31///     "p":"10000.19",
32///     "q":"0.239000",
33///     "b":10108767791,
34///     "a":10108764858,
35///     "T":1749354825200,
36///     "m":false,
37///     "M":true
38/// }
39/// ```
40///
41/// #### FuturePerpetual Side::Sell Trade
42/// ```json
43/// {
44///     "e": "trade",
45///     "E": 1649839266194,
46///     "T": 1749354825200,
47///     "s": "ETHUSDT",
48///     "t": 1000000000,
49///     "p":"10000.19",
50///     "q":"0.239000",
51///     "X": "MARKET",
52///     "m": true
53/// }
54/// ```
55#[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)]
56pub struct BinanceTrade {
57    #[serde(alias = "s", deserialize_with = "de_trade_subscription_id")]
58    pub subscription_id: SubscriptionId,
59    #[serde(
60        alias = "T",
61        deserialize_with = "rustrade_integration::serde::de::de_u64_epoch_ms_as_datetime_utc"
62    )]
63    pub time: DateTime<Utc>,
64    #[serde(alias = "t")]
65    pub id: u64,
66    #[serde(
67        alias = "p",
68        deserialize_with = "rustrade_integration::serde::de::de_str"
69    )]
70    pub price: f64,
71    #[serde(
72        alias = "q",
73        deserialize_with = "rustrade_integration::serde::de::de_str"
74    )]
75    pub amount: f64,
76    #[serde(alias = "m", deserialize_with = "de_side_from_buyer_is_maker")]
77    pub side: Side,
78}
79
80impl Identifier<Option<SubscriptionId>> for BinanceTrade {
81    fn id(&self) -> Option<SubscriptionId> {
82        Some(self.subscription_id.clone())
83    }
84}
85
86impl<InstrumentKey> From<(ExchangeId, InstrumentKey, BinanceTrade)>
87    for MarketIter<InstrumentKey, PublicTrade>
88{
89    fn from((exchange_id, instrument, trade): (ExchangeId, InstrumentKey, BinanceTrade)) -> Self {
90        Self(vec![Ok(MarketEvent {
91            time_exchange: trade.time,
92            time_received: Utc::now(),
93            exchange: exchange_id,
94            instrument,
95            kind: PublicTrade {
96                id: format_smolstr!("{}", trade.id),
97                price: trade.price,
98                amount: trade.amount,
99                side: trade.side,
100            },
101        })])
102    }
103}
104
105/// Deserialize a [`BinanceTrade`] "s" (eg/ "BTCUSDT") as the associated [`SubscriptionId`]
106/// (eg/ "@trade|BTCUSDT").
107pub fn de_trade_subscription_id<'de, D>(deserializer: D) -> Result<SubscriptionId, D::Error>
108where
109    D: serde::de::Deserializer<'de>,
110{
111    <&str as Deserialize>::deserialize(deserializer)
112        .map(|market| ExchangeSub::from((BinanceChannel::TRADES, market)).id())
113}
114
115/// Deserialize a [`BinanceTrade`] "buyer_is_maker" boolean field to a Barter [`Side`].
116///
117/// Variants:
118/// buyer_is_maker => Side::Sell
119/// !buyer_is_maker => Side::Buy
120pub fn de_side_from_buyer_is_maker<'de, D>(deserializer: D) -> Result<Side, D::Error>
121where
122    D: serde::de::Deserializer<'de>,
123{
124    Deserialize::deserialize(deserializer).map(|buyer_is_maker| {
125        if buyer_is_maker {
126            Side::Sell
127        } else {
128            Side::Buy
129        }
130    })
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    mod de {
138        use std::time::Duration;
139
140        use super::*;
141        use rustrade_integration::{
142            error::SocketError, serde::de::datetime_utc_from_epoch_duration,
143        };
144        use serde::de::Error;
145
146        #[test]
147        fn test_binance_trade() {
148            struct TestCase {
149                input: &'static str,
150                expected: Result<BinanceTrade, SocketError>,
151            }
152
153            let tests = vec![
154                TestCase {
155                    // TC0: Spot trade valid
156                    input: r#"
157                    {
158                        "e":"trade","E":1649324825173,"s":"ETHUSDT","t":1000000000,
159                        "p":"10000.19","q":"0.239000","b":10108767791,"a":10108764858,
160                        "T":1749354825200,"m":false,"M":true
161                    }
162                    "#,
163                    expected: Ok(BinanceTrade {
164                        subscription_id: SubscriptionId::from("@trade|ETHUSDT"),
165                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
166                            1749354825200,
167                        )),
168                        id: 1000000000,
169                        price: 10000.19,
170                        amount: 0.239000,
171                        side: Side::Buy,
172                    }),
173                },
174                TestCase {
175                    // TC1: Spot trade malformed w/ "yes" is_buyer_maker field
176                    input: r#"{
177                        "e":"trade","E":1649324825173,"s":"ETHUSDT","t":1000000000,
178                        "p":"10000.19000000","q":"0.239000","b":10108767791,"a":10108764858,
179                        "T":1649324825173,"m":"yes","M":true
180                    }"#,
181                    expected: Err(SocketError::Deserialise {
182                        error: serde_json::Error::custom(""),
183                        payload: "".to_owned(),
184                    }),
185                },
186                TestCase {
187                    // TC2: FuturePerpetual trade w/ type MARKET
188                    input: r#"
189                    {
190                        "e": "trade","E": 1649839266194,"T": 1749354825200,"s": "ETHUSDT",
191                        "t": 1000000000,"p":"10000.19","q":"0.239000","X": "MARKET","m": true
192                    }
193                    "#,
194                    expected: Ok(BinanceTrade {
195                        subscription_id: SubscriptionId::from("@trade|ETHUSDT"),
196                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
197                            1749354825200,
198                        )),
199                        id: 1000000000,
200                        price: 10000.19,
201                        amount: 0.239000,
202                        side: Side::Sell,
203                    }),
204                },
205                TestCase {
206                    // TC3: FuturePerpetual trade w/ type LIQUIDATION
207                    input: r#"
208                    {
209                        "e": "trade","E": 1649839266194,"T": 1749354825200,"s": "ETHUSDT",
210                        "t": 1000000000,"p":"10000.19","q":"0.239000","X": "LIQUIDATION","m": false
211                    }
212                    "#,
213                    expected: Ok(BinanceTrade {
214                        subscription_id: SubscriptionId::from("@trade|ETHUSDT"),
215                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
216                            1749354825200,
217                        )),
218                        id: 1000000000,
219                        price: 10000.19,
220                        amount: 0.239000,
221                        side: Side::Buy,
222                    }),
223                },
224                TestCase {
225                    // TC4: FuturePerpetual trade w/ type LIQUIDATION
226                    input: r#"{
227                        "e": "trade","E": 1649839266194,"T": 1749354825200,"s": "ETHUSDT",
228                        "t": 1000000000,"p":"10000.19","q":"0.239000","X": "INSURANCE_FUND","m": false
229                    }"#,
230                    expected: Ok(BinanceTrade {
231                        subscription_id: SubscriptionId::from("@trade|ETHUSDT"),
232                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
233                            1749354825200,
234                        )),
235                        id: 1000000000,
236                        price: 10000.19,
237                        amount: 0.239000,
238                        side: Side::Buy,
239                    }),
240                },
241            ];
242
243            for (index, test) in tests.into_iter().enumerate() {
244                let actual = serde_json::from_str::<BinanceTrade>(test.input);
245                match (actual, test.expected) {
246                    (Ok(actual), Ok(expected)) => {
247                        assert_eq!(actual, expected, "TC{} failed", index)
248                    }
249                    (Err(_), Err(_)) => {
250                        // Test passed
251                    }
252                    (actual, expected) => {
253                        // Test failed
254                        panic!(
255                            "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
256                        );
257                    }
258                }
259            }
260        }
261    }
262}