barter_data/exchange/bybit/trade.rs
1use crate::{
2 event::{MarketEvent, MarketIter},
3 exchange::bybit::message::BybitPayload,
4 subscription::trade::PublicTrade,
5};
6use barter_instrument::{Side, exchange::ExchangeId};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// Terse type alias for an [`BybitTrade`](BybitTradeInner) real-time trades WebSocket message.
11pub type BybitTrade = BybitPayload<Vec<BybitTradeInner>>;
12
13/// ### Raw Payload Examples
14/// See docs: <https://bybit-exchange.github.io/docs/v5/websocket/public/trade>
15/// Spot Side::Buy Trade
16///```json
17/// {
18/// "T": 1672304486865,
19/// "s": "BTCUSDT",
20/// "S": "Buy",
21/// "v": "0.001",
22/// "p": "16578.50",
23/// "L": "PlusTick",
24/// "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
25/// "BT": false
26/// }
27/// ```
28#[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)]
29pub struct BybitTradeInner {
30 #[serde(
31 alias = "T",
32 deserialize_with = "barter_integration::de::de_u64_epoch_ms_as_datetime_utc"
33 )]
34 pub time: DateTime<Utc>,
35
36 #[serde(rename = "s")]
37 pub market: String,
38
39 #[serde(rename = "S")]
40 pub side: Side,
41
42 #[serde(alias = "v", deserialize_with = "barter_integration::de::de_str")]
43 pub amount: f64,
44
45 #[serde(alias = "p", deserialize_with = "barter_integration::de::de_str")]
46 pub price: f64,
47
48 #[serde(rename = "i")]
49 pub id: String,
50}
51
52impl<InstrumentKey: Clone> From<(ExchangeId, InstrumentKey, BybitTrade)>
53 for MarketIter<InstrumentKey, PublicTrade>
54{
55 fn from((exchange, instrument, trades): (ExchangeId, InstrumentKey, BybitTrade)) -> Self {
56 Self(
57 trades
58 .data
59 .into_iter()
60 .map(|trade| {
61 Ok(MarketEvent {
62 time_exchange: trade.time,
63 time_received: Utc::now(),
64 exchange,
65 instrument: instrument.clone(),
66 kind: PublicTrade {
67 id: trade.id,
68 price: trade.price,
69 amount: trade.amount,
70 side: trade.side,
71 },
72 })
73 })
74 .collect(),
75 )
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 mod de {
84 use super::*;
85 use barter_integration::{
86 de::datetime_utc_from_epoch_duration, error::SocketError, subscription::SubscriptionId,
87 };
88 use smol_str::ToSmolStr;
89 use std::time::Duration;
90
91 #[test]
92 fn test_bybit_trade() {
93 struct TestCase {
94 input: &'static str,
95 expected: Result<BybitTradeInner, SocketError>,
96 }
97
98 let tests = vec![
99 // TC0: input BybitTradeInner is deserialised
100 TestCase {
101 input: r#"
102 {
103 "T": 1672304486865,
104 "s": "BTCUSDT",
105 "S": "Buy",
106 "v": "0.001",
107 "p": "16578.50",
108 "L": "PlusTick",
109 "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
110 "BT": false
111 }
112 "#,
113 expected: Ok(BybitTradeInner {
114 time: datetime_utc_from_epoch_duration(Duration::from_millis(
115 1672304486865,
116 )),
117 market: "BTCUSDT".to_string(),
118 side: Side::Buy,
119 amount: 0.001,
120 price: 16578.50,
121 id: "20f43950-d8dd-5b31-9112-a178eb6023af".to_string(),
122 }),
123 },
124 // TC1: input BybitTradeInner is deserialised
125 TestCase {
126 input: r#"
127 {
128 "T": 1672304486865,
129 "s": "BTCUSDT",
130 "S": "Sell",
131 "v": "0.001",
132 "p": "16578.50",
133 "L": "PlusTick",
134 "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
135 "BT": false
136 }
137 "#,
138 expected: Ok(BybitTradeInner {
139 time: datetime_utc_from_epoch_duration(Duration::from_millis(
140 1672304486865,
141 )),
142 market: "BTCUSDT".to_string(),
143 side: Side::Sell,
144 amount: 0.001,
145 price: 16578.50,
146 id: "20f43950-d8dd-5b31-9112-a178eb6023af".to_string(),
147 }),
148 },
149 // TC2: input BybitTradeInner is unable to be deserialised
150 TestCase {
151 input: r#"
152 {
153 "T": 1672304486865,
154 "s": "BTCUSDT",
155 "S": "Unknown",
156 "v": "0.001",
157 "p": "16578.50",
158 "L": "PlusTick",
159 "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
160 "BT": false
161 }
162 "#,
163 expected: Err(SocketError::Unsupported {
164 entity: "".to_string(),
165 item: "".to_string(),
166 }),
167 },
168 ];
169
170 for (index, test) in tests.into_iter().enumerate() {
171 let actual = serde_json::from_str::<BybitTradeInner>(test.input);
172 match (actual, test.expected) {
173 (Ok(actual), Ok(expected)) => {
174 assert_eq!(actual, expected, "TC{} failed", index)
175 }
176 (Err(_), Err(_)) => {
177 // Test passed
178 }
179 (actual, expected) => {
180 // Test failed
181 panic!(
182 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
183 );
184 }
185 }
186 }
187 }
188
189 #[test]
190 fn test_bybit_trade_payload() {
191 struct TestCase {
192 input: &'static str,
193 expected: Result<BybitTrade, SocketError>,
194 }
195
196 let tests = vec![
197 // TC0: input BybitTrade is deserialised
198 TestCase {
199 input: r#"
200 {
201 "topic": "publicTrade.BTCUSDT",
202 "type": "snapshot",
203 "ts": 1672304486868,
204 "data": [
205 {
206 "T": 1672304486865,
207 "s": "BTCUSDT",
208 "S": "Buy",
209 "v": "0.001",
210 "p": "16578.50",
211 "L": "PlusTick",
212 "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
213 "BT": false
214 },
215 {
216 "T": 1672304486865,
217 "s": "BTCUSDT",
218 "S": "Sell",
219 "v": "0.001",
220 "p": "16578.50",
221 "L": "PlusTick",
222 "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
223 "BT": false
224 }
225 ]
226 }
227 "#,
228 expected: Ok(BybitTrade {
229 subscription_id: SubscriptionId("publicTrade|BTCUSDT".to_smolstr()),
230 r#type: "snapshot".to_string(),
231 time: datetime_utc_from_epoch_duration(Duration::from_millis(
232 1672304486868,
233 )),
234 data: vec![
235 BybitTradeInner {
236 time: datetime_utc_from_epoch_duration(Duration::from_millis(
237 1672304486865,
238 )),
239 market: "BTCUSDT".to_string(),
240 side: Side::Buy,
241 amount: 0.001,
242 price: 16578.50,
243 id: "20f43950-d8dd-5b31-9112-a178eb6023af".to_string(),
244 },
245 BybitTradeInner {
246 time: datetime_utc_from_epoch_duration(Duration::from_millis(
247 1672304486865,
248 )),
249 market: "BTCUSDT".to_string(),
250 side: Side::Sell,
251 amount: 0.001,
252 price: 16578.50,
253 id: "20f43950-d8dd-5b31-9112-a178eb6023af".to_string(),
254 },
255 ],
256 }),
257 },
258 // TC1: input BybitTrade is invalid w/ no subscription_id
259 TestCase {
260 input: r#"
261 {
262 "data": [
263 {
264 "T": 1672304486865,
265 "s": "BTCUSDT",
266 "S": "Unknown",
267 "v": "0.001",
268 "p": "16578.50",
269 "L": "PlusTick",
270 "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
271 "BT": false
272 }
273 ]
274 }
275 "#,
276 expected: Err(SocketError::Unsupported {
277 entity: "".to_string(),
278 item: "".to_string(),
279 }),
280 },
281 // TC1: input BybitTrade is invalid w/ invalid subscription_id format
282 TestCase {
283 input: r#"
284 {
285 "topic": "publicTrade.BTCUSDT.should_not_be_present",
286 "type": "snapshot",
287 "ts": 1672304486868,
288 "data": [
289 {
290 "T": 1672304486865,
291 "s": "BTCUSDT",
292 "S": "Buy",
293 "v": "0.001",
294 "p": "16578.50",
295 "L": "PlusTick",
296 "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
297 "BT": false
298 },
299 {
300 "T": 1672304486865,
301 "s": "BTCUSDT",
302 "S": "Sell",
303 "v": "0.001",
304 "p": "16578.50",
305 "L": "PlusTick",
306 "i": "20f43950-d8dd-5b31-9112-a178eb6023af",
307 "BT": false
308 }
309 ]
310 }
311 "#,
312 expected: Err(SocketError::Unsupported {
313 entity: "".to_string(),
314 item: "".to_string(),
315 }),
316 },
317 ];
318
319 for (index, test) in tests.into_iter().enumerate() {
320 let actual = serde_json::from_str::<BybitTrade>(test.input);
321 match (actual, test.expected) {
322 (Ok(actual), Ok(expected)) => {
323 assert_eq!(actual, expected, "TC{} failed", index)
324 }
325 (Err(_), Err(_)) => {
326 // Test passed
327 }
328 (actual, expected) => {
329 // Test failed
330 panic!(
331 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
332 );
333 }
334 }
335 }
336 }
337 }
338}