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