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;
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// `Serialize` is intentionally not derived: the `subscription_id` field is deserialized from the
57// wire `s` symbol via `de_trade_subscription_id`, so a derived `Serialize` would emit this struct's
58// own field shape and not round-trip — and nothing serializes these wire types.
59#[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize)]
60pub struct BinanceTrade {
61    #[serde(alias = "s", deserialize_with = "de_trade_subscription_id")]
62    pub subscription_id: SubscriptionId,
63    #[serde(
64        alias = "T",
65        deserialize_with = "rustrade_integration::serde::de::de_u64_epoch_ms_as_datetime_utc"
66    )]
67    pub time: DateTime<Utc>,
68    #[serde(alias = "t")]
69    pub id: u64,
70    #[serde(
71        alias = "p",
72        deserialize_with = "rustrade_integration::serde::de::de_str"
73    )]
74    pub price: Decimal,
75    #[serde(
76        alias = "q",
77        deserialize_with = "rustrade_integration::serde::de::de_str"
78    )]
79    pub amount: Decimal,
80    #[serde(alias = "m", deserialize_with = "de_side_from_buyer_is_maker")]
81    pub side: Side,
82}
83
84impl Identifier<Option<SubscriptionId>> for BinanceTrade {
85    fn id(&self) -> Option<SubscriptionId> {
86        Some(self.subscription_id.clone())
87    }
88}
89
90impl<InstrumentKey> From<(ExchangeId, InstrumentKey, BinanceTrade)>
91    for MarketIter<InstrumentKey, PublicTrade>
92{
93    fn from((exchange_id, instrument, trade): (ExchangeId, InstrumentKey, BinanceTrade)) -> Self {
94        Self(vec![Ok(MarketEvent {
95            time_exchange: trade.time,
96            time_received: Utc::now(),
97            exchange: exchange_id,
98            instrument,
99            kind: PublicTrade {
100                id: format_smolstr!("{}", trade.id),
101                price: trade.price,
102                amount: trade.amount,
103                side: Some(trade.side),
104            },
105        })])
106    }
107}
108
109/// Deserialize a [`BinanceTrade`] "s" (eg/ "BTCUSDT") as the associated [`SubscriptionId`]
110/// (eg/ "@trade|BTCUSDT").
111pub fn de_trade_subscription_id<'de, D>(deserializer: D) -> Result<SubscriptionId, D::Error>
112where
113    D: serde::de::Deserializer<'de>,
114{
115    <&str as Deserialize>::deserialize(deserializer)
116        .map(|market| ExchangeSub::from((BinanceChannel::TRADES, market)).id())
117}
118
119/// Deserialize a [`BinanceTrade`] "buyer_is_maker" boolean field to a Barter [`Side`].
120///
121/// Variants:
122/// buyer_is_maker => Side::Sell
123/// !buyer_is_maker => Side::Buy
124pub fn de_side_from_buyer_is_maker<'de, D>(deserializer: D) -> Result<Side, D::Error>
125where
126    D: serde::de::Deserializer<'de>,
127{
128    Deserialize::deserialize(deserializer).map(|buyer_is_maker| {
129        if buyer_is_maker {
130            Side::Sell
131        } else {
132            Side::Buy
133        }
134    })
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    mod de {
142        use std::time::Duration;
143
144        use super::*;
145        use rust_decimal_macros::dec;
146        use rustrade_integration::{
147            error::SocketError, serde::de::datetime_utc_from_epoch_duration,
148        };
149        use serde::de::Error;
150
151        #[test]
152        fn test_binance_trade() {
153            struct TestCase {
154                input: &'static str,
155                expected: Result<BinanceTrade, SocketError>,
156            }
157
158            let tests = vec![
159                TestCase {
160                    // TC0: Spot trade valid
161                    input: r#"
162                    {
163                        "e":"trade","E":1649324825173,"s":"ETHUSDT","t":1000000000,
164                        "p":"10000.19","q":"0.239000","b":10108767791,"a":10108764858,
165                        "T":1749354825200,"m":false,"M":true
166                    }
167                    "#,
168                    expected: Ok(BinanceTrade {
169                        subscription_id: SubscriptionId::from("@trade|ETHUSDT"),
170                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
171                            1749354825200,
172                        )),
173                        id: 1000000000,
174                        price: dec!(10000.19),
175                        amount: dec!(0.239000),
176                        side: Side::Buy,
177                    }),
178                },
179                TestCase {
180                    // TC1: Spot trade malformed w/ "yes" is_buyer_maker field
181                    input: r#"{
182                        "e":"trade","E":1649324825173,"s":"ETHUSDT","t":1000000000,
183                        "p":"10000.19000000","q":"0.239000","b":10108767791,"a":10108764858,
184                        "T":1649324825173,"m":"yes","M":true
185                    }"#,
186                    expected: Err(SocketError::Deserialise {
187                        error: serde_json::Error::custom(""),
188                        payload: "".to_owned(),
189                    }),
190                },
191                TestCase {
192                    // TC2: FuturePerpetual trade w/ type MARKET
193                    input: r#"
194                    {
195                        "e": "trade","E": 1649839266194,"T": 1749354825200,"s": "ETHUSDT",
196                        "t": 1000000000,"p":"10000.19","q":"0.239000","X": "MARKET","m": true
197                    }
198                    "#,
199                    expected: Ok(BinanceTrade {
200                        subscription_id: SubscriptionId::from("@trade|ETHUSDT"),
201                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
202                            1749354825200,
203                        )),
204                        id: 1000000000,
205                        price: dec!(10000.19),
206                        amount: dec!(0.239000),
207                        side: Side::Sell,
208                    }),
209                },
210                TestCase {
211                    // TC3: FuturePerpetual trade w/ type LIQUIDATION
212                    input: r#"
213                    {
214                        "e": "trade","E": 1649839266194,"T": 1749354825200,"s": "ETHUSDT",
215                        "t": 1000000000,"p":"10000.19","q":"0.239000","X": "LIQUIDATION","m": false
216                    }
217                    "#,
218                    expected: Ok(BinanceTrade {
219                        subscription_id: SubscriptionId::from("@trade|ETHUSDT"),
220                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
221                            1749354825200,
222                        )),
223                        id: 1000000000,
224                        price: dec!(10000.19),
225                        amount: dec!(0.239000),
226                        side: Side::Buy,
227                    }),
228                },
229                TestCase {
230                    // TC4: FuturePerpetual trade w/ type LIQUIDATION
231                    input: r#"{
232                        "e": "trade","E": 1649839266194,"T": 1749354825200,"s": "ETHUSDT",
233                        "t": 1000000000,"p":"10000.19","q":"0.239000","X": "INSURANCE_FUND","m": false
234                    }"#,
235                    expected: Ok(BinanceTrade {
236                        subscription_id: SubscriptionId::from("@trade|ETHUSDT"),
237                        time: datetime_utc_from_epoch_duration(Duration::from_millis(
238                            1749354825200,
239                        )),
240                        id: 1000000000,
241                        price: dec!(10000.19),
242                        amount: dec!(0.239000),
243                        side: Side::Buy,
244                    }),
245                },
246            ];
247
248            for (index, test) in tests.into_iter().enumerate() {
249                let actual = serde_json::from_str::<BinanceTrade>(test.input);
250                match (actual, test.expected) {
251                    (Ok(actual), Ok(expected)) => {
252                        assert_eq!(actual, expected, "TC{} failed", index)
253                    }
254                    (Err(_), Err(_)) => {
255                        // Test passed
256                    }
257                    (actual, expected) => {
258                        // Test failed
259                        panic!(
260                            "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
261                        );
262                    }
263                }
264            }
265        }
266    }
267}