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