Skip to main content

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