1use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18use ustr::Ustr;
19
20use crate::{enums::Exchange, parse::deserialize_uppercase};
21
22#[derive(Debug, Clone, Deserialize, Serialize)]
24pub struct BookLevel {
25 pub price: f64,
27 pub amount: f64,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct BookChangeMsg {
35 #[serde(deserialize_with = "deserialize_uppercase")]
37 pub symbol: Ustr,
38 pub exchange: Exchange,
40 pub is_snapshot: bool,
42 pub bids: Vec<BookLevel>,
44 pub asks: Vec<BookLevel>,
46 pub timestamp: DateTime<Utc>,
48 pub local_timestamp: DateTime<Utc>,
50}
51
52#[derive(Debug, Clone, Deserialize, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct BookSnapshotMsg {
56 #[serde(deserialize_with = "deserialize_uppercase")]
58 pub symbol: Ustr,
59 pub exchange: Exchange,
61 pub name: String,
63 pub depth: u32,
65 pub interval: u32,
67 pub bids: Vec<BookLevel>,
69 pub asks: Vec<BookLevel>,
71 pub timestamp: DateTime<Utc>,
73 pub local_timestamp: DateTime<Utc>,
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize)]
79#[serde(tag = "type")]
80#[serde(rename_all = "camelCase")]
81pub struct TradeMsg {
82 #[serde(deserialize_with = "deserialize_uppercase")]
84 pub symbol: Ustr,
85 pub exchange: Exchange,
87 pub id: Option<String>,
89 pub price: f64,
91 pub amount: f64,
93 pub side: String,
95 pub timestamp: DateTime<Utc>,
97 pub local_timestamp: DateTime<Utc>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct DerivativeTickerMsg {
105 #[serde(deserialize_with = "deserialize_uppercase")]
107 pub symbol: Ustr,
108 pub exchange: Exchange,
110 pub last_price: Option<f64>,
112 pub open_interest: Option<f64>,
114 pub funding_rate: Option<f64>,
116 pub index_price: Option<f64>,
118 pub mark_price: Option<f64>,
120 pub timestamp: DateTime<Utc>,
122 pub local_timestamp: DateTime<Utc>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct BarMsg {
132 #[serde(deserialize_with = "deserialize_uppercase")]
134 pub symbol: Ustr,
135 pub exchange: Exchange,
137 pub name: String,
139 pub interval: u64,
141 pub open: f64,
143 pub high: f64,
145 pub low: f64,
147 pub close: f64,
149 pub volume: f64,
151 pub buy_volume: f64,
153 pub sell_volume: f64,
155 pub trades: u64,
157 pub vwap: f64,
159 pub open_timestamp: DateTime<Utc>,
161 pub close_timestamp: DateTime<Utc>,
163 pub timestamp: DateTime<Utc>,
165 pub local_timestamp: DateTime<Utc>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct DisconnectMsg {
174 pub exchange: Exchange,
176 pub local_timestamp: DateTime<Utc>,
178}
179
180#[allow(missing_docs)]
182#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case", tag = "type")]
184pub enum WsMessage {
185 BookChange(BookChangeMsg),
186 BookSnapshot(BookSnapshotMsg),
187 Trade(TradeMsg),
188 TradeBar(BarMsg),
189 DerivativeTicker(DerivativeTickerMsg),
190 Disconnect(DisconnectMsg),
191}
192
193#[cfg(test)]
197mod tests {
198 use rstest::rstest;
199
200 use super::*;
201 use crate::tests::load_test_json;
202
203 #[rstest]
204 fn test_parse_book_change_message() {
205 let json_data = load_test_json("book_change.json");
206 let message: BookChangeMsg = serde_json::from_str(&json_data).unwrap();
207
208 assert_eq!(message.symbol, "XBTUSD");
209 assert_eq!(message.exchange, Exchange::Bitmex);
210 assert!(!message.is_snapshot);
211 assert!(message.bids.is_empty());
212 assert_eq!(message.asks.len(), 1);
213 assert_eq!(message.asks[0].price, 7_985.0);
214 assert_eq!(message.asks[0].amount, 283_318.0);
215 assert_eq!(
216 message.timestamp,
217 DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
218 );
219 assert_eq!(
220 message.local_timestamp,
221 DateTime::parse_from_rfc3339("2019-10-23T11:29:53.469Z").unwrap()
222 );
223 }
224
225 #[rstest]
226 fn test_parse_book_snapshot_message() {
227 let json_data = load_test_json("book_snapshot.json");
228 let message: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
229
230 assert_eq!(message.symbol, "XBTUSD");
231 assert_eq!(message.exchange, Exchange::Bitmex);
232 assert_eq!(message.name, "book_snapshot_2_50ms");
233 assert_eq!(message.depth, 2);
234 assert_eq!(message.interval, 50);
235 assert_eq!(message.bids.len(), 2);
236 assert_eq!(message.asks.len(), 2);
237 assert_eq!(message.bids[0].price, 7_633.5);
238 assert_eq!(message.bids[0].amount, 1_906_067.0);
239 assert_eq!(message.asks[0].price, 7_634.0);
240 assert_eq!(message.asks[0].amount, 1_467_849.0);
241 assert_eq!(
242 message.timestamp,
243 DateTime::parse_from_rfc3339("2019-10-25T13:39:46.950Z").unwrap(),
244 );
245 assert_eq!(
246 message.local_timestamp,
247 DateTime::parse_from_rfc3339("2019-10-25T13:39:46.961Z").unwrap()
248 );
249 }
250
251 #[rstest]
252 fn test_parse_trade_message() {
253 let json_data = load_test_json("trade.json");
254 let message: TradeMsg = serde_json::from_str(&json_data).unwrap();
255
256 assert_eq!(message.symbol, "XBTUSD");
257 assert_eq!(message.exchange, Exchange::Bitmex);
258 assert_eq!(
259 message.id,
260 Some("282a0445-0e3a-abeb-f403-11003204ea1b".to_string())
261 );
262 assert_eq!(message.price, 7_996.0);
263 assert_eq!(message.amount, 50.0);
264 assert_eq!(message.side, "sell");
265 assert_eq!(
266 message.timestamp,
267 DateTime::parse_from_rfc3339("2019-10-23T10:32:49.669Z").unwrap()
268 );
269 assert_eq!(
270 message.local_timestamp,
271 DateTime::parse_from_rfc3339("2019-10-23T10:32:49.740Z").unwrap()
272 );
273 }
274
275 #[rstest]
276 fn test_parse_derivative_ticker_message() {
277 let json_data = load_test_json("derivative_ticker.json");
278 let message: DerivativeTickerMsg = serde_json::from_str(&json_data).unwrap();
279
280 assert_eq!(message.symbol, "BTC-PERPETUAL");
281 assert_eq!(message.exchange, Exchange::Deribit);
282 assert_eq!(message.last_price, Some(7_987.5));
283 assert_eq!(message.open_interest, Some(84_129_491.0));
284 assert_eq!(message.funding_rate, Some(-0.00001568));
285 assert_eq!(message.index_price, Some(7_989.28));
286 assert_eq!(message.mark_price, Some(7_987.56));
287 assert_eq!(
288 message.timestamp,
289 DateTime::parse_from_rfc3339("2019-10-23T11:34:29.302Z").unwrap()
290 );
291 assert_eq!(
292 message.local_timestamp,
293 DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
294 );
295 }
296
297 #[rstest]
298 fn test_parse_bar_message() {
299 let json_data = load_test_json("bar.json");
300 let message: BarMsg = serde_json::from_str(&json_data).unwrap();
301
302 assert_eq!(message.symbol, "XBTUSD");
303 assert_eq!(message.exchange, Exchange::Bitmex);
304 assert_eq!(message.name, "trade_bar_10000ms");
305 assert_eq!(message.interval, 10_000);
306 assert_eq!(message.open, 7_623.5);
307 assert_eq!(message.high, 7_623.5);
308 assert_eq!(message.low, 7_623.0);
309 assert_eq!(message.close, 7_623.5);
310 assert_eq!(message.volume, 37_034.0);
311 assert_eq!(message.buy_volume, 24_244.0);
312 assert_eq!(message.sell_volume, 12_790.0);
313 assert_eq!(message.trades, 9);
314 assert_eq!(message.vwap, 7_623.327320840309);
315 assert_eq!(
316 message.open_timestamp,
317 DateTime::parse_from_rfc3339("2019-10-25T13:11:31.574Z").unwrap()
318 );
319 assert_eq!(
320 message.close_timestamp,
321 DateTime::parse_from_rfc3339("2019-10-25T13:11:39.212Z").unwrap()
322 );
323 assert_eq!(
324 message.local_timestamp,
325 DateTime::parse_from_rfc3339("2019-10-25T13:11:40.369Z").unwrap()
326 );
327 assert_eq!(
328 message.timestamp,
329 DateTime::parse_from_rfc3339("2019-10-25T13:11:40.000Z").unwrap()
330 );
331 }
332
333 #[rstest]
334 fn test_parse_disconnect_message() {
335 let json_data = load_test_json("disconnect.json");
336 let message: DisconnectMsg = serde_json::from_str(&json_data).unwrap();
337
338 assert_eq!(message.exchange, Exchange::Deribit);
339 assert_eq!(
340 message.local_timestamp,
341 DateTime::parse_from_rfc3339("2019-10-23T11:34:29.416Z").unwrap()
342 );
343 }
344}