1use serde::{Deserialize, Serialize};
4
5use crate::types::Category;
6
7
8#[derive(Debug, Clone, Serialize)]
10pub struct WsOperation {
11 pub op: String,
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub args: Option<Vec<String>>,
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub req_id: Option<String>,
19}
20
21impl WsOperation {
22 pub fn ping() -> Self {
24 Self {
25 op: "ping".to_string(),
26 args: None,
27 req_id: None,
28 }
29 }
30
31 pub fn subscribe(topics: Vec<String>) -> Self {
33 Self {
34 op: "subscribe".to_string(),
35 args: Some(topics),
36 req_id: None,
37 }
38 }
39
40 pub fn unsubscribe(topics: Vec<String>) -> Self {
42 Self {
43 op: "unsubscribe".to_string(),
44 args: Some(topics),
45 req_id: None,
46 }
47 }
48
49 pub fn auth(api_key: &str, expires: u64, signature: &str) -> Self {
51 Self {
52 op: "auth".to_string(),
53 args: Some(vec![
54 api_key.to_string(),
55 expires.to_string(),
56 signature.to_string(),
57 ]),
58 req_id: None,
59 }
60 }
61
62 pub fn with_req_id(mut self, req_id: impl Into<String>) -> Self {
64 self.req_id = Some(req_id.into());
65 self
66 }
67}
68
69#[derive(Debug, Clone, Deserialize)]
71pub struct WsOperationResponse {
72 pub success: bool,
74 #[serde(default)]
76 pub ret_msg: Option<String>,
77 #[serde(default)]
79 pub conn_id: Option<String>,
80 #[serde(default)]
82 pub req_id: Option<String>,
83 #[serde(default)]
85 pub op: Option<String>,
86}
87
88#[derive(Debug, Clone, Deserialize)]
90pub struct WsPong {
91 pub success: bool,
93 #[serde(default)]
95 pub ret_msg: Option<String>,
96 #[serde(default)]
98 pub conn_id: Option<String>,
99 #[serde(default)]
101 pub req_id: Option<String>,
102 pub op: String,
104}
105
106
107#[derive(Debug, Clone, Deserialize)]
109pub struct WsStreamMessage<T> {
110 pub topic: String,
112 #[serde(rename = "type")]
114 pub update_type: String,
115 pub ts: u64,
117 pub data: T,
119 #[serde(default)]
121 pub cts: Option<u64>,
122}
123
124#[derive(Debug, Clone, Deserialize)]
126pub struct OrderbookEntry {
127 #[serde(rename = "0")]
129 pub price: String,
130 #[serde(rename = "1")]
132 pub size: String,
133}
134
135#[derive(Debug, Clone, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct OrderbookData {
139 #[serde(rename = "s")]
141 pub symbol: String,
142 #[serde(rename = "b")]
144 pub bids: Vec<OrderbookEntry>,
145 #[serde(rename = "a")]
147 pub asks: Vec<OrderbookEntry>,
148 #[serde(rename = "u")]
150 pub update_id: u64,
151 #[serde(default)]
153 pub seq: Option<u64>,
154}
155
156#[derive(Debug, Clone, Deserialize)]
158pub struct TradeData {
159 #[serde(rename = "T")]
161 pub timestamp: u64,
162 #[serde(rename = "s")]
164 pub symbol: String,
165 #[serde(rename = "S")]
167 pub side: String,
168 #[serde(rename = "v")]
170 pub size: String,
171 #[serde(rename = "p")]
173 pub price: String,
174 #[serde(rename = "L")]
176 pub tick_direction: String,
177 #[serde(rename = "i")]
179 pub trade_id: String,
180 #[serde(rename = "BT")]
182 pub is_block_trade: bool,
183}
184
185#[derive(Debug, Clone, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct TickerData {
189 pub symbol: String,
191 #[serde(default)]
193 pub tick_direction: Option<String>,
194 #[serde(default)]
196 pub price24h_pcnt: Option<String>,
197 #[serde(default)]
199 pub last_price: Option<String>,
200 #[serde(default)]
202 pub prev_price24h: Option<String>,
203 #[serde(default)]
205 pub high_price24h: Option<String>,
206 #[serde(default)]
208 pub low_price24h: Option<String>,
209 #[serde(default)]
211 pub prev_price1h: Option<String>,
212 #[serde(default)]
214 pub mark_price: Option<String>,
215 #[serde(default)]
217 pub index_price: Option<String>,
218 #[serde(default)]
220 pub open_interest: Option<String>,
221 #[serde(default)]
223 pub open_interest_value: Option<String>,
224 #[serde(default)]
226 pub turnover24h: Option<String>,
227 #[serde(default)]
229 pub volume24h: Option<String>,
230 #[serde(default)]
232 pub next_funding_time: Option<String>,
233 #[serde(default)]
235 pub funding_rate: Option<String>,
236 #[serde(default)]
238 pub bid1_price: Option<String>,
239 #[serde(default)]
241 pub bid1_size: Option<String>,
242 #[serde(default)]
244 pub ask1_price: Option<String>,
245 #[serde(default)]
247 pub ask1_size: Option<String>,
248}
249
250#[derive(Debug, Clone, Deserialize)]
252pub struct KlineData {
253 pub start: u64,
255 pub end: u64,
257 pub interval: String,
259 pub open: String,
261 pub close: String,
263 pub high: String,
265 pub low: String,
267 pub volume: String,
269 pub turnover: String,
271 pub confirm: bool,
273 pub timestamp: u64,
275}
276
277#[derive(Debug, Clone, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct LiquidationData {
281 pub symbol: String,
283 pub side: String,
285 pub price: String,
287 pub size: String,
289 pub updated_time: u64,
291}
292
293
294#[derive(Debug, Clone, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct WsPrivateMessage<T> {
298 #[serde(default)]
300 pub id: Option<String>,
301 pub topic: String,
303 pub creation_time: u64,
305 pub data: T,
307}
308
309#[derive(Debug, Clone, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct PositionData {
313 pub category: String,
315 pub symbol: String,
317 pub side: String,
319 pub size: String,
321 #[serde(default)]
323 pub position_idx: i32,
324 #[serde(default)]
326 pub trade_mode: i32,
327 #[serde(default)]
329 pub position_value: Option<String>,
330 #[serde(default)]
332 pub risk_id: Option<i32>,
333 #[serde(default)]
335 pub risk_limit_value: Option<String>,
336 #[serde(default)]
338 pub entry_price: Option<String>,
339 #[serde(default)]
341 pub mark_price: Option<String>,
342 #[serde(default)]
344 pub leverage: Option<String>,
345 #[serde(default)]
347 pub position_balance: Option<String>,
348 #[serde(default)]
350 pub auto_add_margin: Option<i32>,
351 #[serde(default)]
353 pub position_m_m: Option<String>,
354 #[serde(default)]
356 pub position_i_m: Option<String>,
357 #[serde(default)]
359 pub liq_price: Option<String>,
360 #[serde(default)]
362 pub bust_price: Option<String>,
363 #[serde(default)]
365 pub tpsl_mode: Option<String>,
366 #[serde(default)]
368 pub take_profit: Option<String>,
369 #[serde(default)]
371 pub stop_loss: Option<String>,
372 #[serde(default)]
374 pub trailing_stop: Option<String>,
375 #[serde(default)]
377 pub unrealised_pnl: Option<String>,
378 #[serde(default)]
380 pub cur_realised_pnl: Option<String>,
381 #[serde(default)]
383 pub cum_realised_pnl: Option<String>,
384 #[serde(default)]
386 pub session_avg_price: Option<String>,
387 #[serde(default)]
389 pub position_status: Option<String>,
390 #[serde(default)]
392 pub adl_rank_indicator: Option<i32>,
393 #[serde(default)]
395 pub is_reduce_only: Option<bool>,
396 #[serde(default)]
398 pub created_time: Option<String>,
399 #[serde(default)]
401 pub updated_time: Option<String>,
402 #[serde(default)]
404 pub seq: Option<i64>,
405}
406
407#[derive(Debug, Clone, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct OrderData {
411 pub category: String,
413 pub order_id: String,
415 #[serde(default)]
417 pub order_link_id: Option<String>,
418 #[serde(default)]
420 pub is_leverage: Option<String>,
421 #[serde(default)]
423 pub block_trade_id: Option<String>,
424 pub symbol: String,
426 pub price: String,
428 pub qty: String,
430 pub side: String,
432 #[serde(default)]
434 pub position_idx: Option<i32>,
435 pub order_status: String,
437 #[serde(default)]
439 pub create_type: Option<String>,
440 #[serde(default)]
442 pub cancel_type: Option<String>,
443 #[serde(default)]
445 pub reject_reason: Option<String>,
446 #[serde(default)]
448 pub avg_price: Option<String>,
449 #[serde(default)]
451 pub leaves_qty: Option<String>,
452 #[serde(default)]
454 pub leaves_value: Option<String>,
455 #[serde(default)]
457 pub cum_exec_qty: Option<String>,
458 #[serde(default)]
460 pub cum_exec_value: Option<String>,
461 #[serde(default)]
463 pub cum_exec_fee: Option<String>,
464 #[serde(default)]
466 pub closed_pnl: Option<String>,
467 #[serde(default)]
469 pub fee_currency: Option<String>,
470 #[serde(default)]
472 pub time_in_force: Option<String>,
473 pub order_type: String,
475 #[serde(default)]
477 pub stop_order_type: Option<String>,
478 #[serde(default)]
480 pub trigger_price: Option<String>,
481 #[serde(default)]
483 pub take_profit: Option<String>,
484 #[serde(default)]
486 pub stop_loss: Option<String>,
487 #[serde(default)]
489 pub tpsl_mode: Option<String>,
490 #[serde(default)]
492 pub reduce_only: Option<bool>,
493 #[serde(default)]
495 pub close_on_trigger: Option<bool>,
496 #[serde(default)]
498 pub smp_type: Option<String>,
499 #[serde(default)]
501 pub smp_group: Option<i32>,
502 #[serde(default)]
504 pub smp_order_id: Option<String>,
505 #[serde(default)]
507 pub created_time: Option<String>,
508 #[serde(default)]
510 pub updated_time: Option<String>,
511}
512
513#[derive(Debug, Clone, Deserialize)]
515#[serde(rename_all = "camelCase")]
516pub struct ExecutionData {
517 pub category: String,
519 pub symbol: String,
521 #[serde(default)]
523 pub is_leverage: Option<String>,
524 pub order_id: String,
526 #[serde(default)]
528 pub order_link_id: Option<String>,
529 pub side: String,
531 #[serde(default)]
533 pub order_price: Option<String>,
534 #[serde(default)]
536 pub order_qty: Option<String>,
537 #[serde(default)]
539 pub leaves_qty: Option<String>,
540 #[serde(default)]
542 pub create_type: Option<String>,
543 #[serde(default)]
545 pub order_type: Option<String>,
546 #[serde(default)]
548 pub stop_order_type: Option<String>,
549 #[serde(default)]
551 pub exec_fee: Option<String>,
552 #[serde(default)]
554 pub fee_currency: Option<String>,
555 pub exec_id: String,
557 pub exec_price: String,
559 pub exec_qty: String,
561 #[serde(default)]
563 pub exec_pnl: Option<String>,
564 #[serde(default)]
566 pub exec_type: Option<String>,
567 #[serde(default)]
569 pub exec_value: Option<String>,
570 pub exec_time: String,
572 #[serde(default)]
574 pub is_maker: Option<bool>,
575 #[serde(default)]
577 pub fee_rate: Option<String>,
578 #[serde(default)]
580 pub mark_price: Option<String>,
581 #[serde(default)]
583 pub index_price: Option<String>,
584 #[serde(default)]
586 pub closed_size: Option<String>,
587 #[serde(default)]
589 pub seq: Option<i64>,
590}
591
592#[derive(Debug, Clone, Deserialize)]
594#[serde(rename_all = "camelCase")]
595pub struct ExecutionFastData {
596 pub category: String,
598 pub symbol: String,
600 pub exec_id: String,
602 pub exec_price: String,
604 pub exec_qty: String,
606 pub order_id: String,
608 #[serde(default)]
610 pub is_maker: Option<bool>,
611 #[serde(default)]
613 pub order_link_id: Option<String>,
614 pub side: String,
616 pub exec_time: String,
618 #[serde(default)]
620 pub seq: Option<i64>,
621}
622
623#[derive(Debug, Clone, Deserialize)]
625#[serde(rename_all = "camelCase")]
626pub struct CoinData {
627 pub coin: String,
629 #[serde(default)]
631 pub equity: Option<String>,
632 #[serde(default)]
634 pub usd_value: Option<String>,
635 #[serde(default)]
637 pub wallet_balance: Option<String>,
638 #[serde(default)]
640 pub free: Option<String>,
641 #[serde(default)]
643 pub locked: Option<String>,
644 #[serde(default)]
646 pub borrow_amount: Option<String>,
647 #[serde(default)]
649 pub available_to_borrow: Option<String>,
650 #[serde(default)]
652 pub available_to_withdraw: Option<String>,
653 #[serde(default)]
655 pub accrued_interest: Option<String>,
656 #[serde(default)]
658 pub total_order_i_m: Option<String>,
659 #[serde(default)]
661 pub total_position_i_m: Option<String>,
662 #[serde(default)]
664 pub total_position_m_m: Option<String>,
665 #[serde(default)]
667 pub unrealised_pnl: Option<String>,
668 #[serde(default)]
670 pub cum_realised_pnl: Option<String>,
671 #[serde(default)]
673 pub bonus: Option<String>,
674}
675
676#[derive(Debug, Clone, Deserialize)]
678#[serde(rename_all = "camelCase")]
679pub struct WalletData {
680 pub account_type: String,
682 #[serde(default)]
684 pub account_l_t_v: Option<String>,
685 #[serde(default)]
687 pub account_i_m_rate: Option<String>,
688 #[serde(default)]
690 pub account_m_m_rate: Option<String>,
691 #[serde(default)]
693 pub total_equity: Option<String>,
694 #[serde(default)]
696 pub total_wallet_balance: Option<String>,
697 #[serde(default)]
699 pub total_margin_balance: Option<String>,
700 #[serde(default)]
702 pub total_available_balance: Option<String>,
703 #[serde(default)]
705 pub total_perp_u_p_l: Option<String>,
706 #[serde(default)]
708 pub total_initial_margin: Option<String>,
709 #[serde(default)]
711 pub total_maintenance_margin: Option<String>,
712 #[serde(default)]
714 pub coin: Vec<CoinData>,
715}
716
717#[derive(Debug, Clone, Deserialize)]
719#[serde(rename_all = "camelCase")]
720pub struct GreeksData {
721 pub base_coin: String,
723 #[serde(default)]
725 pub total_delta: Option<String>,
726 #[serde(default)]
728 pub total_gamma: Option<String>,
729 #[serde(default)]
731 pub total_vega: Option<String>,
732 #[serde(default)]
734 pub total_theta: Option<String>,
735}
736
737
738#[derive(Debug, Clone)]
740pub enum WsMessage {
741 Pong(WsPong),
743 OperationResponse(WsOperationResponse),
745 Orderbook(Box<WsStreamMessage<OrderbookData>>),
747 Trade(Box<WsStreamMessage<Vec<TradeData>>>),
749 Ticker(Box<WsStreamMessage<TickerData>>),
751 Kline(Box<WsStreamMessage<Vec<KlineData>>>),
753 Liquidation(Box<WsStreamMessage<LiquidationData>>),
755 Position(Box<WsPrivateMessage<Vec<PositionData>>>),
757 Order(Box<WsPrivateMessage<Vec<OrderData>>>),
759 Execution(Box<WsPrivateMessage<Vec<ExecutionData>>>),
761 ExecutionFast(Box<WsPrivateMessage<Vec<ExecutionFastData>>>),
763 Wallet(Box<WsPrivateMessage<Vec<WalletData>>>),
765 Greeks(Box<WsPrivateMessage<Vec<GreeksData>>>),
767 Raw(String),
769}
770
771
772#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
774pub enum WsChannel {
775 PublicSpot,
777 PublicLinear,
779 PublicInverse,
781 PublicOption,
783 Private,
785 Trade,
787}
788
789impl WsChannel {
790 pub fn path(&self) -> &'static str {
792 match self {
793 WsChannel::PublicSpot => "/v5/public/spot",
794 WsChannel::PublicLinear => "/v5/public/linear",
795 WsChannel::PublicInverse => "/v5/public/inverse",
796 WsChannel::PublicOption => "/v5/public/option",
797 WsChannel::Private => "/v5/private",
798 WsChannel::Trade => "/v5/trade",
799 }
800 }
801
802 pub fn from_category(category: Category) -> Self {
804 match category {
805 Category::Spot => WsChannel::PublicSpot,
806 Category::Linear => WsChannel::PublicLinear,
807 Category::Inverse => WsChannel::PublicInverse,
808 Category::Option => WsChannel::PublicOption,
809 }
810 }
811
812 pub fn requires_auth(&self) -> bool {
814 matches!(self, WsChannel::Private | WsChannel::Trade)
815 }
816}
817
818#[derive(Debug, Clone, Copy, PartialEq, Eq)]
820pub enum ConnectionState {
821 Disconnected,
823 Connecting,
825 Connected,
827 Reconnecting,
829 Closed,
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn test_ws_operation_serialize() {
839 let op = WsOperation::subscribe(vec!["orderbook.50.BTCUSDT".to_string()]);
840 let json = match serde_json::to_string(&op) {
841 Ok(json) => json,
842 Err(err) => panic!("Failed to serialize operation: {}", err),
843 };
844 assert!(json.contains("\"op\":\"subscribe\""));
845 assert!(json.contains("orderbook.50.BTCUSDT"));
846 }
847
848 #[test]
849 fn test_ws_operation_ping() {
850 let op = WsOperation::ping();
851 let json = match serde_json::to_string(&op) {
852 Ok(json) => json,
853 Err(err) => panic!("Failed to serialize ping: {}", err),
854 };
855 assert_eq!(json, r#"{"op":"ping"}"#);
856 }
857
858 #[test]
859 fn test_ws_channel_path() {
860 assert_eq!(WsChannel::PublicLinear.path(), "/v5/public/linear");
861 assert_eq!(WsChannel::Private.path(), "/v5/private");
862 }
863
864 #[test]
865 fn test_orderbook_data_deserialize() {
866 let json = r#"{
867 "s": "BTCUSDT",
868 "b": [["50000", "1.5"], ["49999", "2.0"]],
869 "a": [["50001", "0.8"]],
870 "u": 12345
871 }"#;
872 let data: OrderbookData = match serde_json::from_str(json) {
873 Ok(data) => data,
874 Err(err) => panic!("Failed to parse orderbook data: {}", err),
875 };
876 assert_eq!(data.symbol, "BTCUSDT");
877 assert_eq!(data.bids.len(), 2);
878 assert_eq!(data.bids[0].price, "50000");
879 assert_eq!(data.bids[0].size, "1.5");
880 }
881
882 #[test]
883 fn test_position_data_deserialize() {
884 let json = r#"{
885 "category": "linear",
886 "symbol": "BTCUSDT",
887 "side": "Buy",
888 "size": "0.01",
889 "positionIdx": 0,
890 "tradeMode": 0,
891 "positionValue": "500.0",
892 "entryPrice": "50000",
893 "markPrice": "50100",
894 "leverage": "10",
895 "unrealisedPnl": "1.0",
896 "cumRealisedPnl": "100.0",
897 "positionStatus": "Normal",
898 "createdTime": "1658384314791",
899 "updatedTime": "1658384314792"
900 }"#;
901 let data: PositionData = match serde_json::from_str(json) {
902 Ok(data) => data,
903 Err(err) => panic!("Failed to parse position data: {}", err),
904 };
905 assert_eq!(data.category, "linear");
906 assert_eq!(data.symbol, "BTCUSDT");
907 assert_eq!(data.side, "Buy");
908 assert_eq!(data.size, "0.01");
909 assert_eq!(data.entry_price, Some("50000".to_string()));
910 }
911
912 #[test]
913 fn test_order_data_deserialize() {
914 let json = r#"{
915 "category": "linear",
916 "orderId": "order-123",
917 "orderLinkId": "my-order-1",
918 "symbol": "BTCUSDT",
919 "price": "50000",
920 "qty": "0.01",
921 "side": "Buy",
922 "orderStatus": "New",
923 "orderType": "Limit",
924 "timeInForce": "GTC",
925 "createdTime": "1658384314791",
926 "updatedTime": "1658384314792"
927 }"#;
928 let data: OrderData = match serde_json::from_str(json) {
929 Ok(data) => data,
930 Err(err) => panic!("Failed to parse order data: {}", err),
931 };
932 assert_eq!(data.category, "linear");
933 assert_eq!(data.order_id, "order-123");
934 assert_eq!(data.symbol, "BTCUSDT");
935 assert_eq!(data.side, "Buy");
936 assert_eq!(data.order_status, "New");
937 assert_eq!(data.order_type, "Limit");
938 }
939
940 #[test]
941 fn test_execution_data_deserialize() {
942 let json = r#"{
943 "category": "linear",
944 "symbol": "BTCUSDT",
945 "orderId": "order-123",
946 "side": "Buy",
947 "execId": "exec-456",
948 "execPrice": "50000",
949 "execQty": "0.01",
950 "execTime": "1658384314791",
951 "execType": "Trade",
952 "isMaker": false,
953 "feeRate": "0.0006"
954 }"#;
955 let data: ExecutionData = match serde_json::from_str(json) {
956 Ok(data) => data,
957 Err(err) => panic!("Failed to parse execution data: {}", err),
958 };
959 assert_eq!(data.category, "linear");
960 assert_eq!(data.symbol, "BTCUSDT");
961 assert_eq!(data.exec_id, "exec-456");
962 assert_eq!(data.exec_price, "50000");
963 assert_eq!(data.exec_qty, "0.01");
964 assert_eq!(data.is_maker, Some(false));
965 }
966
967 #[test]
968 fn test_wallet_data_deserialize() {
969 let json = r#"{
970 "accountType": "UNIFIED",
971 "totalEquity": "10000.0",
972 "totalWalletBalance": "9500.0",
973 "totalAvailableBalance": "8000.0",
974 "coin": [
975 {
976 "coin": "USDT",
977 "equity": "5000.0",
978 "walletBalance": "5000.0",
979 "availableToWithdraw": "4500.0"
980 },
981 {
982 "coin": "BTC",
983 "equity": "0.1",
984 "walletBalance": "0.1"
985 }
986 ]
987 }"#;
988 let data: WalletData = match serde_json::from_str(json) {
989 Ok(data) => data,
990 Err(err) => panic!("Failed to parse wallet data: {}", err),
991 };
992 assert_eq!(data.account_type, "UNIFIED");
993 assert_eq!(data.total_equity, Some("10000.0".to_string()));
994 assert_eq!(data.coin.len(), 2);
995 assert_eq!(data.coin[0].coin, "USDT");
996 assert_eq!(data.coin[1].coin, "BTC");
997 }
998
999 #[test]
1000 fn test_greeks_data_deserialize() {
1001 let json = r#"{
1002 "baseCoin": "BTC",
1003 "totalDelta": "0.5",
1004 "totalGamma": "0.001",
1005 "totalVega": "100.0",
1006 "totalTheta": "-50.0"
1007 }"#;
1008 let data: GreeksData = match serde_json::from_str(json) {
1009 Ok(data) => data,
1010 Err(err) => panic!("Failed to parse greeks data: {}", err),
1011 };
1012 assert_eq!(data.base_coin, "BTC");
1013 assert_eq!(data.total_delta, Some("0.5".to_string()));
1014 assert_eq!(data.total_gamma, Some("0.001".to_string()));
1015 }
1016
1017 #[test]
1018 fn test_private_message_deserialize() {
1019 let json = r#"{
1020 "topic": "position",
1021 "creationTime": 1658384314791,
1022 "data": [{
1023 "category": "linear",
1024 "symbol": "BTCUSDT",
1025 "side": "Buy",
1026 "size": "0.01"
1027 }]
1028 }"#;
1029 let msg: WsPrivateMessage<Vec<PositionData>> = match serde_json::from_str(json) {
1030 Ok(msg) => msg,
1031 Err(err) => panic!("Failed to parse private message: {}", err),
1032 };
1033 assert_eq!(msg.topic, "position");
1034 assert_eq!(msg.creation_time, 1658384314791);
1035 assert_eq!(msg.data.len(), 1);
1036 assert_eq!(msg.data[0].symbol, "BTCUSDT");
1037 }
1038}