1use std::collections::HashMap;
2
3use crate::{
4 AccountType, AdlRankIndicator, CancelType, Category, CreateType, ExecType, ExtraFeeType,
5 ExtraSubFeeType, Interval, OcoTriggerBy, OrderStatus, OrderType, PlaceType, PositionIdx,
6 PositionStatus, RejectReason, Side, SlippageToleranceType, SmpType, StopOrderType,
7 TickDirection, TimeInForce, Timestamp, Topic, TpslMode, TriggerBy, TriggerDirection,
8 http::WalletCoin,
9 serde::hash_map,
10 serde::{empty_string_as_none, int_to_bool, string_to_bool, string_to_option_bool},
11};
12
13use rust_decimal::{Decimal, serde::str_option::deserialize as option_decimal};
14use serde::Deserialize;
15use serde_aux::prelude::{
16 deserialize_number_from_string as number,
17 deserialize_option_number_from_string as option_number,
18};
19
20#[derive(PartialEq, Deserialize, Debug)]
21#[serde(untagged)]
22pub enum IncomingMessage {
23 Command(CommandMsg),
24 Ticker(Box<TickerMsg>),
29 Trade(TradeMsg),
30 KLine(KLineMsg),
31 AllLiquidation(AllLiquidationMsg),
32 Topic(TopicMessage),
33}
34
35impl IncomingMessage {
36 pub fn is_pong(&self) -> bool {
37 matches!(
38 self,
39 IncomingMessage::Command(CommandMsg::Pong {
40 req_id: _,
41 ret_msg: _,
42 conn_id: _,
43 args: _,
44 success: _,
45 })
46 )
47 }
48 pub fn is_ping(&self) -> bool {
49 matches!(
50 self,
51 IncomingMessage::Command(CommandMsg::Ping {
52 req_id: _,
53 ret_msg: _,
54 conn_id: _,
55 args: _,
56 success: _,
57 })
58 )
59 }
60}
61
62#[derive(PartialEq, Deserialize, Debug)]
63#[serde(tag = "op")]
64pub enum CommandMsg {
65 #[serde(rename = "subscribe")]
66 Subscribe {
67 #[serde(default, deserialize_with = "empty_string_as_none")]
68 req_id: Option<String>,
69 #[serde(default, deserialize_with = "empty_string_as_none")]
70 ret_msg: Option<String>,
71 conn_id: String,
72 success: bool,
73 },
74 #[serde(rename = "unsubscribe")]
75 Unsubscribe {
76 #[serde(default, deserialize_with = "empty_string_as_none")]
77 req_id: Option<String>,
78 #[serde(default, deserialize_with = "empty_string_as_none")]
79 ret_msg: Option<String>,
80 conn_id: String,
81 success: bool,
82 },
83 #[serde(rename = "auth")]
84 Auth {
85 #[serde(default, deserialize_with = "empty_string_as_none")]
86 req_id: Option<String>,
87 #[serde(default, deserialize_with = "empty_string_as_none")]
88 ret_msg: Option<String>,
89 conn_id: String,
90 success: bool,
91 },
92 #[serde(rename = "pong")]
93 Pong {
94 #[serde(default, deserialize_with = "empty_string_as_none")]
95 req_id: Option<String>,
96 #[serde(default, deserialize_with = "empty_string_as_none")]
97 ret_msg: Option<String>,
98 conn_id: String,
99 args: Option<Vec<String>>,
100 success: Option<bool>,
101 },
102 #[serde(rename = "ping")]
103 Ping {
104 #[serde(default, deserialize_with = "empty_string_as_none")]
105 req_id: Option<String>,
106 #[serde(default, deserialize_with = "empty_string_as_none")]
107 ret_msg: Option<String>,
108 conn_id: String,
109 args: Option<Vec<String>>,
110 success: bool,
111 },
112}
113
114#[derive(PartialEq, Deserialize, Debug)]
116#[serde(tag = "type")]
117pub enum TickerMsg {
118 #[serde(rename = "snapshot")]
119 Snapshot {
120 topic: Topic,
121 #[serde(default, deserialize_with = "option_number")]
122 cs: Option<u64>,
123 ts: Timestamp,
124 data: TickerSnapshotMsg,
125 },
126 #[serde(rename = "delta")]
127 Delta {
128 topic: Topic,
129 #[serde(default, deserialize_with = "option_number")]
130 cs: Option<u64>,
131 ts: Timestamp,
132 data: TickerDeltaMsg,
133 },
134}
135
136#[derive(PartialEq, Deserialize, Debug)]
137#[serde(rename_all = "camelCase")]
138pub struct TickerSnapshotMsg {
139 pub symbol: String,
140 pub tick_direction: TickDirection,
141 pub last_price: Decimal,
142 #[serde(default, deserialize_with = "option_decimal")]
143 pub pre_open_price: Option<Decimal>,
144 #[serde(default, deserialize_with = "option_decimal")]
145 pub pre_qty: Option<Decimal>,
146 #[serde(default, deserialize_with = "empty_string_as_none")]
147 pub cur_pre_listing_phase: Option<String>,
148 pub prev_price24h: Decimal,
149 pub price24h_pcnt: Decimal,
150 pub high_price24h: Decimal,
151 pub low_price24h: Decimal,
152 pub prev_price1h: Decimal,
153 pub mark_price: Decimal,
154 pub index_price: Decimal,
155 pub open_interest: Decimal,
156 pub open_interest_value: Decimal,
157 pub turnover24h: Decimal,
158 pub volume24h: Decimal,
159 pub funding_rate: Decimal,
160 #[serde(default, deserialize_with = "number")]
161 pub next_funding_time: Timestamp,
162 pub bid1_price: Decimal,
163 pub bid1_size: Decimal,
164 pub ask1_price: Decimal,
165 pub ask1_size: Decimal,
166 #[serde(default, deserialize_with = "option_number")]
167 pub delivery_time: Option<Timestamp>,
168 #[serde(default, deserialize_with = "option_decimal")]
169 pub basis_rate: Option<Decimal>,
170 #[serde(default, deserialize_with = "option_decimal")]
171 pub delivery_fee_rate: Option<Decimal>,
172 #[serde(default, deserialize_with = "option_decimal")]
173 pub predicted_delivery_price: Option<Decimal>,
174}
175
176#[derive(PartialEq, Deserialize, Debug)]
177#[serde(rename_all = "camelCase")]
178pub struct TickerDeltaMsg {
179 pub symbol: String,
180 #[serde(default, deserialize_with = "empty_string_as_none")]
181 pub tick_direction: Option<TickDirection>,
182 #[serde(default, deserialize_with = "option_decimal")]
183 pub last_price: Option<Decimal>,
184 #[serde(default, deserialize_with = "option_decimal")]
185 pub pre_open_price: Option<Decimal>,
186 #[serde(default, deserialize_with = "option_decimal")]
187 pub pre_qty: Option<Decimal>,
188 #[serde(default, deserialize_with = "empty_string_as_none")]
189 pub cur_pre_listing_phase: Option<String>,
190 #[serde(default, deserialize_with = "option_decimal")]
191 pub prev_price24h: Option<Decimal>,
192 #[serde(default, deserialize_with = "option_decimal")]
193 pub price24h_pcnt: Option<Decimal>,
194 #[serde(default, deserialize_with = "option_decimal")]
195 pub high_price24h: Option<Decimal>,
196 #[serde(default, deserialize_with = "option_decimal")]
197 pub low_price24h: Option<Decimal>,
198 #[serde(default, deserialize_with = "option_decimal")]
199 pub prev_price1h: Option<Decimal>,
200 #[serde(default, deserialize_with = "option_decimal")]
201 pub mark_price: Option<Decimal>,
202 #[serde(default, deserialize_with = "option_decimal")]
203 pub index_price: Option<Decimal>,
204 #[serde(default, deserialize_with = "option_decimal")]
205 pub open_interest: Option<Decimal>,
206 #[serde(default, deserialize_with = "option_decimal")]
207 pub open_interest_value: Option<Decimal>,
208 #[serde(default, deserialize_with = "option_decimal")]
209 pub turnover24h: Option<Decimal>,
210 #[serde(default, deserialize_with = "option_decimal")]
211 pub volume24h: Option<Decimal>,
212 #[serde(default, deserialize_with = "option_decimal")]
213 pub funding_rate: Option<Decimal>,
214 #[serde(default, deserialize_with = "option_decimal")]
215 pub next_funding_time: Option<Decimal>,
216 #[serde(default, deserialize_with = "option_decimal")]
217 pub bid1_price: Option<Decimal>,
218 #[serde(default, deserialize_with = "option_decimal")]
219 pub bid1_size: Option<Decimal>,
220 #[serde(default, deserialize_with = "option_decimal")]
221 pub ask1_price: Option<Decimal>,
222 #[serde(default, deserialize_with = "option_decimal")]
223 pub ask1_size: Option<Decimal>,
224 #[serde(default, deserialize_with = "option_number")]
225 pub delivery_time: Option<Timestamp>,
226 #[serde(default, deserialize_with = "option_decimal")]
227 pub basis_rate: Option<Decimal>,
228 #[serde(default, deserialize_with = "option_decimal")]
229 pub delivery_fee_rate: Option<Decimal>,
230 #[serde(default, deserialize_with = "option_decimal")]
231 pub predicted_delivery_price: Option<Decimal>,
232}
233
234#[derive(PartialEq, Deserialize, Debug)]
236#[serde(tag = "type")]
237pub enum TradeMsg {
238 #[serde(rename = "snapshot")]
239 Snapshot {
240 #[serde(default, deserialize_with = "empty_string_as_none")]
241 id: Option<String>,
242 topic: Topic,
243 ts: Timestamp,
244 data: Vec<TradeSnapshotMsg>,
245 },
246}
247
248#[derive(PartialEq, Deserialize, Debug)]
249pub struct TradeSnapshotMsg {
250 #[serde(rename = "T")]
251 pub time: Timestamp,
252 #[serde(rename = "s")]
253 pub symbol: String,
254 #[serde(rename = "S")]
255 pub side: Side,
256 #[serde(rename = "v")]
257 pub size: Decimal,
258 #[serde(rename = "p")]
259 pub price: Decimal,
260 #[serde(rename = "L")]
261 pub tick_direction: TickDirection,
262 #[serde(rename = "i")]
263 pub trade_id: String,
264 #[serde(rename = "BT")]
265 pub block_trade: bool,
266 #[serde(rename = "RPI")]
267 pub rpi_trade: Option<bool>,
268 #[serde(rename = "mP", default, deserialize_with = "empty_string_as_none")]
269 pub mark_price: Option<String>,
270 #[serde(rename = "iP", default, deserialize_with = "empty_string_as_none")]
271 pub index_price: Option<String>,
272 #[serde(rename = "mlv", default, deserialize_with = "empty_string_as_none")]
273 pub mark_iv: Option<String>,
274 #[serde(rename = "iv", default, deserialize_with = "empty_string_as_none")]
275 pub iv: Option<String>,
276}
277
278#[derive(PartialEq, Deserialize, Debug)]
280#[serde(tag = "type")]
281pub enum KLineMsg {
282 #[serde(rename = "snapshot")]
283 Snapshot {
284 topic: Topic,
285 ts: Timestamp,
286 data: Vec<KLineSnapshotMsg>,
287 },
288}
289
290#[derive(PartialEq, Deserialize, Debug)]
291pub struct KLineSnapshotMsg {
292 pub start: Timestamp,
293 pub end: Timestamp,
294 pub interval: Interval,
295 pub open: Decimal,
296 pub close: Decimal,
297 pub high: Decimal,
298 pub low: Decimal,
299 pub volume: Decimal,
300 pub turnover: Decimal,
301 pub confirm: bool,
302 pub timestamp: Timestamp,
303}
304
305#[derive(PartialEq, Deserialize, Debug)]
307#[serde(tag = "type")]
308pub enum AllLiquidationMsg {
309 #[serde(rename = "snapshot")]
310 Snapshot {
311 topic: Topic,
312 ts: Timestamp,
313 data: Vec<AllLiquidationSnapshotMsg>,
314 },
315}
316
317#[derive(PartialEq, Deserialize, Debug)]
318pub struct AllLiquidationSnapshotMsg {
319 #[serde(rename = "T")]
320 pub time: Timestamp,
321 #[serde(rename = "s")]
322 pub symbol: String,
323 #[serde(rename = "S")]
325 pub side: Side,
326 #[serde(rename = "v")]
327 pub size: Decimal,
328 #[serde(rename = "p")]
329 pub price: Decimal,
330}
331
332#[derive(PartialEq, Deserialize, Debug)]
333#[serde(tag = "topic")] pub enum TopicMessage {
335 #[serde(rename = "order")]
336 Order(PrivateMsg<Vec<OrderMsg>>),
337 #[serde(rename = "position")]
338 Position(PrivateMsg<Vec<PositionMsg>>),
339 #[serde(rename = "wallet")]
340 Wallet(PrivateMsg<Vec<WalletMsg>>),
341 #[serde(rename = "execution")]
342 Execution(PrivateMsg<Vec<ExecutionMsg>>),
343}
344
345#[derive(PartialEq, Deserialize, Debug)]
346#[serde(rename_all = "camelCase")]
347pub struct PublicMsg<T> {
348 #[serde(default, deserialize_with = "empty_string_as_none")]
349 id: Option<String>,
350 #[serde(default, deserialize_with = "option_number")]
351 cs: Option<u64>,
352 ts: Timestamp,
353 data: T,
354}
355
356#[derive(PartialEq, Deserialize, Debug)]
357#[serde(rename_all = "camelCase")]
358pub struct PrivateMsg<T> {
359 pub id: String,
362 pub creation_time: Timestamp,
364 pub data: T,
365}
366
367#[derive(PartialEq, Deserialize, Debug, Clone)]
368#[serde(rename_all = "camelCase")]
369pub struct OrderMsg {
370 pub category: Category,
374 pub order_id: String,
376 #[serde(default, deserialize_with = "empty_string_as_none")]
378 pub order_link_id: Option<String>,
379 #[serde(default, deserialize_with = "string_to_option_bool")]
383 pub is_leverage: Option<bool>,
384 #[serde(default, deserialize_with = "empty_string_as_none")]
386 pub block_trade_id: Option<String>,
387 pub symbol: String,
389 pub price: Decimal,
391 #[serde(default, deserialize_with = "option_decimal")]
393 pub broker_order_price: Option<Decimal>,
394 pub qty: Decimal,
396 pub side: Side,
398 pub position_idx: PositionIdx,
400 pub order_status: OrderStatus,
402 #[serde(default, deserialize_with = "empty_string_as_none")]
406 pub create_type: Option<CreateType>,
407 pub cancel_type: CancelType,
409 pub reject_reason: RejectReason,
411 #[serde(default, deserialize_with = "option_decimal")]
415 pub avg_price: Option<Decimal>,
416 #[serde(default, deserialize_with = "option_decimal")]
418 pub leaves_qty: Option<Decimal>,
419 #[serde(default, deserialize_with = "option_decimal")]
421 pub leaves_value: Option<Decimal>,
422 pub cum_exec_qty: Decimal,
424 pub cum_exec_value: Decimal,
426 pub cum_exec_fee: Decimal,
430 pub cum_fee_detail: Option<serde_json::Value>,
432 pub closed_pnl: Decimal,
434 #[serde(deserialize_with = "option_decimal")]
436 pub fee_currency: Option<Decimal>,
437 pub time_in_force: TimeInForce,
439 pub order_type: OrderType,
441 #[serde(default, deserialize_with = "empty_string_as_none")]
443 pub stop_order_type: Option<StopOrderType>,
444 #[serde(default, deserialize_with = "empty_string_as_none")]
446 pub oco_trigger_by: Option<OcoTriggerBy>,
447 #[serde(deserialize_with = "option_decimal")]
449 pub order_iv: Option<Decimal>,
450 #[serde(default, deserialize_with = "empty_string_as_none")]
452 pub market_unit: Option<String>,
453 #[serde(default, deserialize_with = "empty_string_as_none")]
455 pub slippage_tolerance_type: Option<SlippageToleranceType>,
456 #[serde(default, deserialize_with = "option_decimal")]
458 pub slippage_tolerance: Option<Decimal>,
459 #[serde(deserialize_with = "option_decimal")]
461 pub trigger_price: Option<Decimal>,
462 #[serde(deserialize_with = "option_decimal")]
464 pub take_profit: Option<Decimal>,
465 #[serde(deserialize_with = "option_decimal")]
467 pub stop_loss: Option<Decimal>,
468 #[serde(default, deserialize_with = "empty_string_as_none")]
470 pub tpsl_mode: Option<TpslMode>,
471 #[serde(deserialize_with = "option_decimal")]
473 pub tp_limit_price: Option<Decimal>,
474 #[serde(deserialize_with = "option_decimal")]
476 pub sl_limit_price: Option<Decimal>,
477 #[serde(default, deserialize_with = "empty_string_as_none")]
479 pub tp_trigger_by: Option<TriggerBy>,
480 #[serde(default, deserialize_with = "empty_string_as_none")]
482 pub sl_trigger_by: Option<TriggerBy>,
483 pub trigger_direction: TriggerDirection,
485 #[serde(default, deserialize_with = "empty_string_as_none")]
487 pub trigger_by: Option<TriggerBy>,
488 #[serde(deserialize_with = "option_decimal")]
490 pub last_price_on_created: Option<Decimal>,
491 pub reduce_only: bool,
493 pub close_on_trigger: bool,
495 #[serde(default, deserialize_with = "empty_string_as_none")]
497 pub place_type: Option<PlaceType>,
498 pub smp_type: SmpType,
500 #[serde(deserialize_with = "number")]
502 pub smp_group: i64,
503 #[serde(default, deserialize_with = "empty_string_as_none")]
505 pub smp_order_id: Option<String>,
506 #[serde(deserialize_with = "number")]
508 pub created_time: Timestamp,
509 #[serde(deserialize_with = "number")]
511 pub updated_time: Timestamp,
512}
513
514#[derive(PartialEq, Deserialize, Debug, Clone)]
515#[serde(rename_all = "camelCase")]
516pub struct PositionMsg {
517 pub category: Category,
519 pub symbol: String,
521 #[serde(default, deserialize_with = "empty_string_as_none")]
525 pub side: Option<Side>,
526 pub size: Decimal,
528 pub position_idx: PositionIdx,
530 pub position_value: Decimal,
532 #[serde(deserialize_with = "number")]
535 pub risk_id: i64,
536 #[serde(default, deserialize_with = "option_decimal")]
539 pub risk_limit_value: Option<Decimal>,
540 pub entry_price: Decimal,
542 pub mark_price: Decimal,
544 pub leverage: Decimal,
547 #[serde(default, deserialize_with = "int_to_bool")]
549 pub auto_add_margin: bool,
550 #[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
553 pub position_im: Option<Decimal>,
554 #[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
557 pub position_mm: Option<Decimal>,
558 #[serde(
561 rename = "positionIMByMp",
562 default,
563 deserialize_with = "option_decimal"
564 )]
565 pub position_im_by_mp: Option<Decimal>,
566 #[serde(
569 rename = "positionMMByMp",
570 default,
571 deserialize_with = "option_decimal"
572 )]
573 pub position_mm_by_mp: Option<Decimal>,
574 #[serde(default, deserialize_with = "option_decimal")]
581 pub liq_price: Option<Decimal>,
582 pub take_profit: Decimal,
584 pub stop_loss: Decimal,
586 pub trailing_stop: Decimal,
588 pub unrealised_pnl: Decimal,
590 pub cur_realised_pnl: Decimal,
592 #[serde(default, deserialize_with = "option_decimal")]
594 pub session_avg_price: Option<Decimal>,
595 #[serde(default, deserialize_with = "empty_string_as_none")]
597 pub delta: Option<String>,
598 #[serde(default, deserialize_with = "empty_string_as_none")]
600 pub gamma: Option<String>,
601 #[serde(default, deserialize_with = "empty_string_as_none")]
603 pub vega: Option<String>,
604 #[serde(default, deserialize_with = "empty_string_as_none")]
606 pub theta: Option<String>,
607 pub cum_realised_pnl: Decimal,
611 pub position_status: PositionStatus,
613 pub adl_rank_indicator: AdlRankIndicator,
615 pub is_reduce_only: bool,
620 #[serde(deserialize_with = "option_number")]
627 pub mmr_sys_updated_time: Option<Timestamp>,
628 #[serde(deserialize_with = "option_number")]
635 pub leverage_sys_updated_time: Option<Timestamp>,
636 #[serde(deserialize_with = "number")]
638 pub created_time: Timestamp,
639 #[serde(deserialize_with = "number")]
641 pub updated_time: Timestamp,
642 pub seq: i64,
647}
648
649#[derive(PartialEq, Deserialize, Debug, Clone)]
650#[serde(rename_all = "camelCase")]
651pub struct WalletMsg {
652 pub account_type: AccountType,
657 #[serde(rename = "accountIMRate")]
664 pub account_im_rate: Decimal,
665 #[serde(rename = "accountMMRate")]
667 pub account_mm_rate: Decimal,
668 pub total_equity: Decimal,
670 pub total_wallet_balance: Decimal,
672 pub total_margin_balance: Decimal,
674 pub total_available_balance: Decimal,
676 #[serde(rename = "totalPerpUPL")]
678 pub total_perp_upl: Decimal,
679 pub total_initial_margin: Decimal,
681 pub total_maintenance_margin: Decimal,
683 #[serde(rename = "accountIMRateByMp")]
685 pub account_im_rate_by_mp: Decimal,
686 #[serde(rename = "accountMMRateByMp")]
688 pub account_mm_rate_by_mp: Decimal,
689 #[serde(rename = "totalInitialMarginByMp")]
691 pub total_initial_margin_by_mp: Decimal,
692 #[serde(rename = "totalMaintenanceMarginByMp")]
694 pub total_maintenance_margin_by_mp: Decimal,
695 #[serde(deserialize_with = "hash_map")]
696 pub coin: HashMap<String, WalletCoin>,
697}
698
699#[derive(PartialEq, Deserialize, Debug, Clone)]
700#[serde(rename_all = "camelCase")]
701pub struct ExecutionMsg {
702 pub category: Category,
704 pub symbol: String,
706 #[serde(default, deserialize_with = "string_to_bool")]
708 pub is_leverage: bool,
709 pub order_id: String,
711 #[serde(default, deserialize_with = "empty_string_as_none")]
713 pub order_link_id: Option<String>,
714 pub side: Side,
716 pub order_price: Decimal,
718 pub order_qty: Decimal,
720 pub leaves_qty: Decimal,
722 pub create_type: CreateType,
725 pub order_type: OrderType,
727 pub stop_order_type: StopOrderType,
729 pub exec_fee: Decimal,
731 pub exec_id: String,
733 pub exec_price: Decimal,
735 pub exec_qty: Decimal,
737 pub exec_pnl: Decimal,
739 pub exec_type: ExecType,
741 pub exec_value: Decimal,
743 #[serde(deserialize_with = "number")]
745 pub exec_time: Timestamp,
746 pub is_maker: bool,
748 pub fee_rate: Decimal,
750 #[serde(default, deserialize_with = "option_decimal")]
752 pub trade_iv: Option<Decimal>,
753 #[serde(default, deserialize_with = "option_decimal")]
755 pub mark_iv: Option<Decimal>,
756 pub mark_price: Decimal,
758 #[serde(default, deserialize_with = "option_decimal")]
760 pub index_price: Option<Decimal>,
761 #[serde(default, deserialize_with = "option_decimal")]
763 pub underlying_price: Option<Decimal>,
764 #[serde(default, deserialize_with = "empty_string_as_none")]
766 pub block_trade_id: Option<String>,
767 pub closed_size: Decimal,
769 pub extra_fees: Option<Vec<ExtraFee>>, pub seq: i64,
775 pub fee_currency: String,
777}
778
779#[derive(PartialEq, Deserialize, Debug, Clone)]
780#[serde(rename_all = "camelCase")]
781pub struct ExtraFee {
782 pub fee_coin: String,
783 pub fee_type: ExtraFeeType,
784 pub sub_fee_type: ExtraSubFeeType,
785 pub fee_rate: Decimal,
786 pub fee: Decimal,
787}
788
789#[cfg(test)]
790mod tests {
791 use rust_decimal::dec;
792
793 use crate::serde::{Unique, deserialize_json};
794
795 use super::*;
796
797 #[test]
798 fn deserialize_incoming_message_command_subscribe() {
799 let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"subscribe"}"#;
800 let expected = IncomingMessage::Command(CommandMsg::Subscribe {
801 req_id: None,
802 ret_msg: None,
803 conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
804 success: true,
805 });
806
807 let message = deserialize_json(json).unwrap();
808
809 assert_eq!(expected, message);
810 }
811
812 #[test]
813 fn deserialize_incoming_message_command_unsubscribe() {
814 let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"unsubscribe"}"#;
815 let expected = IncomingMessage::Command(CommandMsg::Unsubscribe {
816 req_id: None,
817 ret_msg: None,
818 conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
819 success: true,
820 });
821
822 let message = deserialize_json(json).unwrap();
823
824 assert_eq!(expected, message);
825 }
826
827 #[test]
828 fn deserialize_incoming_message_ticker_delta() {
829 let json = r#"{
830 "topic": "tickers.BTCUSDT",
831 "type": "delta",
832 "data": {
833 "symbol": "BTCUSDT",
834 "tickDirection": "PlusTick",
835 "price24hPcnt": "-0.015895",
836 "lastPrice": "63948.50",
837 "turnover24h": "6793884423.5518",
838 "volume24h": "105991.3760",
839 "bid1Price": "63948.40",
840 "bid1Size": "3.439",
841 "ask1Price": "63948.50",
842 "ask1Size": "2.566"
843 },
844 "cs": 195377749067,
845 "ts": 1718995014034
846 }"#;
847 let ticker_delta = TickerMsg::Delta {
848 topic: Topic::Ticker(String::from("BTCUSDT")),
849 cs: Some(195377749067),
850 ts: 1718995014034,
851 data: TickerDeltaMsg {
852 symbol: String::from("BTCUSDT"),
853 tick_direction: Some(TickDirection::PlusTick),
854 last_price: Some(dec!(63948.5)),
855 pre_open_price: None,
856 pre_qty: None,
857 cur_pre_listing_phase: None,
858 prev_price24h: None,
859 price24h_pcnt: Some(dec!(-0.015895)),
860 high_price24h: None,
861 low_price24h: None,
862 prev_price1h: None,
863 mark_price: None,
864 index_price: None,
865 open_interest: None,
866 open_interest_value: None,
867 turnover24h: Some(dec!(6793884423.5518)),
868 volume24h: Some(dec!(105991.376)),
869 funding_rate: None,
870 next_funding_time: None,
871 bid1_price: Some(dec!(63948.4)),
872 bid1_size: Some(dec!(3.439)),
873 ask1_price: Some(dec!(63948.5)),
874 ask1_size: Some(dec!(2.566)),
875 delivery_time: None,
876 basis_rate: None,
877 delivery_fee_rate: None,
878 predicted_delivery_price: None,
879 },
880 };
881 let expected = IncomingMessage::Ticker(Box::new(ticker_delta));
882
883 let message = deserialize_json(json).unwrap();
884
885 assert_eq!(expected, message);
886 }
887
888 #[test]
889 fn deserialize_incoming_message_ticker_snapshot() {
890 let json = r#"{
892 "topic": "tickers.BTCUSDT",
893 "type": "snapshot",
894 "data": {
895 "symbol":"BTCUSDT",
896 "tickDirection":"ZeroPlusTick",
897 "price24hPcnt":"-0.044555",
898 "lastPrice":"84594.40",
899 "prevPrice24h":"88539.30",
900 "highPrice24h":"89389.90",
901 "lowPrice24h":"82055.60",
902 "prevPrice1h":"84307.20",
903 "markPrice":"84594.00",
904 "indexPrice":"84650.47",
905 "openInterest":"52903.75",
906 "openInterestValue":"4475339827.50",
907 "turnover24h":"17166562011.6514",
908 "volume24h":"200176.9910",
909 "nextFundingTime":"1740643200000",
910 "fundingRate":"-0.00016974",
911 "bid1Price":"84594.30",
912 "bid1Size":"6.777",
913 "ask1Price":"84594.40",
914 "ask1Size":"0.660",
915 "preOpenPrice":"",
916 "preQty":"",
917 "curPreListingPhase":""
918 },
919 "cs": 337149693308,
920 "ts": 1740622194359
921 }"#;
922 let ticker_snapshot = TickerMsg::Snapshot {
923 topic: Topic::Ticker(String::from("BTCUSDT")),
924 cs: Some(337149693308),
925 ts: 1740622194359,
926 data: TickerSnapshotMsg {
927 symbol: String::from("BTCUSDT"),
928 tick_direction: TickDirection::ZeroPlusTick,
929 last_price: dec!(84594.40),
930 pre_open_price: None,
931 pre_qty: None,
932 cur_pre_listing_phase: None,
933 prev_price24h: dec!(88539.30),
934 price24h_pcnt: dec!(-0.044555),
935 high_price24h: dec!(89389.90),
936 low_price24h: dec!(82055.60),
937 prev_price1h: dec!(84307.20),
938 mark_price: dec!(84594.00),
939 index_price: dec!(84650.47),
940 open_interest: dec!(52903.75),
941 open_interest_value: dec!(4475339827.50),
942 turnover24h: dec!(17166562011.6514),
943 volume24h: dec!(200176.9910),
944 funding_rate: dec!(-0.00016974),
945 next_funding_time: 1740643200000,
946 bid1_price: dec!(84594.30),
947 bid1_size: dec!(6.777),
948 ask1_price: dec!(84594.40),
949 ask1_size: dec!(0.660),
950 delivery_time: None,
951 basis_rate: None,
952 delivery_fee_rate: None,
953 predicted_delivery_price: None,
954 },
955 };
956 let expected = IncomingMessage::Ticker(Box::new(ticker_snapshot));
957
958 let message = deserialize_json(json).unwrap();
959
960 assert_eq!(expected, message);
961 }
962
963 #[test]
964 fn deserialize_incoming_message_trade_snapshot() {
965 let json = r#"{
967 "topic":"publicTrade.BTCUSDT",
968 "type":"snapshot",
969 "ts":1741433245359,
970 "data":[
971 {
972 "T":1741433245357,
973 "s":"BTCUSDT",
974 "S":"Buy",
975 "v":"0.007",
976 "p":"85821.00",
977 "L":"PlusTick",
978 "i":"485eaa70-df6e-5260-bbef-4f7324e3c5d9",
979 "BT":false
980 }
981 ]
982 }"#;
983 let expected = IncomingMessage::Trade(TradeMsg::Snapshot {
984 id: None,
985 topic: Topic::Trade(String::from("BTCUSDT")),
986 ts: 1741433245359,
987 data: vec![TradeSnapshotMsg {
988 time: 1741433245357,
989 symbol: String::from("BTCUSDT"),
990 side: Side::Buy,
991 size: dec!(0.007),
992 price: dec!(85821.00),
993 tick_direction: TickDirection::PlusTick,
994 trade_id: String::from("485eaa70-df6e-5260-bbef-4f7324e3c5d9"),
995 block_trade: false,
996 rpi_trade: None,
997 mark_price: None,
998 index_price: None,
999 mark_iv: None,
1000 iv: None,
1001 }],
1002 });
1003
1004 let message = deserialize_json(json).unwrap();
1005
1006 assert_eq!(expected, message);
1007 }
1008
1009 #[test]
1010 fn deserialize_incoming_message_all_liquidation_snapshot() {
1011 let json = r#"{
1013 "topic":"allLiquidation.BTCUSDT",
1014 "type":"snapshot",
1015 "ts":1741450605553,
1016 "data":[
1017 {
1018 "T":1741450605236,
1019 "s":"BTCUSDT",
1020 "S":"Buy",
1021 "v":"0.001",
1022 "p":"85823.60"
1023 }
1024 ]
1025 }"#;
1026 let expected = AllLiquidationMsg::Snapshot {
1027 topic: Topic::AllLiquidation(String::from("BTCUSDT")),
1028 ts: 1741450605553,
1029 data: vec![AllLiquidationSnapshotMsg {
1030 time: 1741450605236,
1031 symbol: String::from("BTCUSDT"),
1032 side: Side::Buy,
1033 size: dec!(0.001),
1034 price: dec!(85823.60),
1035 }],
1036 };
1037
1038 let message = deserialize_json(json).unwrap();
1039
1040 assert_eq!(expected, message);
1041 }
1042
1043 #[test]
1044 fn deserialize_incoming_message_order() {
1045 let json = r#"{
1046 "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90",
1047 "topic": "order",
1048 "creationTime": 1672364262474,
1049 "data": [
1050 {
1051 "symbol": "ETH-30DEC22-1400-C",
1052 "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020",
1053 "side": "Sell",
1054 "orderType": "Market",
1055 "cancelType": "UNKNOWN",
1056 "price": "72.5",
1057 "qty": "1",
1058 "orderIv": "",
1059 "timeInForce": "IOC",
1060 "orderStatus": "Filled",
1061 "orderLinkId": "",
1062 "lastPriceOnCreated": "",
1063 "reduceOnly": false,
1064 "leavesQty": "",
1065 "leavesValue": "",
1066 "cumExecQty": "1",
1067 "cumExecValue": "75",
1068 "avgPrice": "75",
1069 "blockTradeId": "",
1070 "positionIdx": 0,
1071 "cumExecFee": "0.358635",
1072 "closedPnl": "0",
1073 "createdTime": "1672364262444",
1074 "updatedTime": "1672364262457",
1075 "rejectReason": "EC_NoError",
1076 "stopOrderType": "",
1077 "tpslMode": "",
1078 "triggerPrice": "",
1079 "takeProfit": "",
1080 "stopLoss": "",
1081 "tpTriggerBy": "",
1082 "slTriggerBy": "",
1083 "tpLimitPrice": "",
1084 "slLimitPrice": "",
1085 "triggerDirection": 0,
1086 "triggerBy": "",
1087 "closeOnTrigger": false,
1088 "category": "option",
1089 "placeType": "price",
1090 "smpType": "None",
1091 "smpGroup": 0,
1092 "smpOrderId": "",
1093 "feeCurrency": "",
1094 "cumFeeDetail": {
1095 "MNT": "0.00242968"
1096 }
1097 }
1098 ]
1099 }"#;
1100 let order = PrivateMsg {
1101 id: String::from("5923240c6880ab-c59f-420b-9adb-3639adc9dd90"),
1102 creation_time: 1672364262474,
1103 data: vec![OrderMsg {
1104 category: Category::Option,
1105 order_id: String::from("5cf98598-39a7-459e-97bf-76ca765ee020"),
1106 order_link_id: None,
1107 is_leverage: None,
1108 block_trade_id: None,
1109 symbol: String::from("ETH-30DEC22-1400-C"),
1110 price: dec!(72.5),
1111 broker_order_price: None,
1112 qty: dec!(1.0),
1113 side: Side::Sell,
1114 position_idx: PositionIdx::OneWay,
1115 order_status: OrderStatus::Filled,
1116 create_type: None,
1117 cancel_type: CancelType::UNKNOWN,
1118 reject_reason: RejectReason::EcNoError,
1119 avg_price: Some(dec!(75.0)),
1120 leaves_qty: None,
1121 leaves_value: None,
1122 cum_exec_qty: dec!(1.0),
1123 cum_exec_value: dec!(75.0),
1124 cum_exec_fee: dec!(0.358635),
1125 closed_pnl: dec!(0.0),
1126 fee_currency: None,
1127 time_in_force: TimeInForce::IOC,
1128 order_type: OrderType::Market,
1129 stop_order_type: None,
1130 oco_trigger_by: None,
1131 order_iv: None,
1132 market_unit: None,
1133 slippage_tolerance_type: None,
1134 slippage_tolerance: None,
1135 trigger_price: None,
1136 take_profit: None,
1137 stop_loss: None,
1138 tpsl_mode: None,
1139 tp_limit_price: None,
1140 sl_limit_price: None,
1141 tp_trigger_by: None,
1142 sl_trigger_by: None,
1143 trigger_direction: TriggerDirection::UNKNOWN,
1144 trigger_by: None,
1145 last_price_on_created: None,
1146 reduce_only: false,
1147 close_on_trigger: false,
1148 place_type: Some(PlaceType::Price),
1149 smp_type: SmpType::None,
1150 smp_group: 0,
1151 smp_order_id: None,
1152 created_time: 1672364262444,
1153 updated_time: 1672364262457,
1154 cum_fee_detail: Some(serde_json::from_str(r#"{"MNT": "0.00242968"}"#).unwrap()),
1155 }],
1156 };
1157 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1158
1159 let message = deserialize_json(json).unwrap();
1160
1161 assert_eq!(expected, message);
1162 }
1163
1164 #[test]
1165 fn deserialize_incoming_message_order2() {
1166 let json = r#"{"topic":"order","id":"108985347_ADAUSDT_140667095077548","creationTime":1766436947942,"data":[{"category":"linear","symbol":"ADAUSDT","orderId":"ae802ad5-af70-4957-ba72-86ad7fc9c24d","orderLinkId":"","blockTradeId":"","side":"Buy","positionIdx":0,"orderStatus":"Filled","cancelType":"UNKNOWN","rejectReason":"EC_NoError","timeInForce":"IOC","isLeverage":"","price":"0.3862","qty":"15","avgPrice":"0.3679","leavesQty":"0","leavesValue":"0","cumExecQty":"15","cumExecValue":"5.5185","cumExecFee":"0.00303518","orderType":"Market","stopOrderType":"","orderIv":"","triggerPrice":"","takeProfit":"","stopLoss":"","triggerBy":"","tpTriggerBy":"","slTriggerBy":"","triggerDirection":0,"placeType":"","lastPriceOnCreated":"0.3679","closeOnTrigger":false,"reduceOnly":false,"smpGroup":0,"smpType":"None","smpOrderId":"","slLimitPrice":"0","tpLimitPrice":"0","tpslMode":"UNKNOWN","createType":"CreateByUser","marketUnit":"","createdTime":"1766436947940","updatedTime":"1766436947940","feeCurrency":"","closedPnl":"0","slippageTolerance":"0","slippageToleranceType":"UNKNOWN","cumFeeDetail":{}}]}"#;
1167 let order = PrivateMsg {
1168 id: String::from("108985347_ADAUSDT_140667095077548"),
1169 creation_time: 1766436947942,
1170 data: vec![OrderMsg {
1171 category: Category::Linear,
1172 order_id: String::from("ae802ad5-af70-4957-ba72-86ad7fc9c24d"),
1173 order_link_id: None,
1174 is_leverage: None,
1175 block_trade_id: None,
1176 symbol: String::from("ADAUSDT"),
1177 price: dec!(0.3862),
1178 broker_order_price: None,
1179 qty: dec!(15),
1180 side: Side::Buy,
1181 position_idx: PositionIdx::OneWay,
1182 order_status: OrderStatus::Filled,
1183 create_type: Some(CreateType::CreateByUser),
1184 cancel_type: CancelType::UNKNOWN,
1185 reject_reason: RejectReason::EcNoError,
1186 avg_price: Some(dec!(0.3679)),
1187 leaves_qty: Some(dec!(0)),
1188 leaves_value: Some(dec!(0)),
1189 cum_exec_qty: dec!(15),
1190 cum_exec_value: dec!(5.5185),
1191 cum_exec_fee: dec!(0.00303518),
1192 closed_pnl: dec!(0),
1193 fee_currency: None,
1194 time_in_force: TimeInForce::IOC,
1195 order_type: OrderType::Market,
1196 stop_order_type: None,
1197 oco_trigger_by: None,
1198 order_iv: None,
1199 market_unit: None,
1200 slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1201 slippage_tolerance: Some(dec!(0)),
1202 trigger_price: None,
1203 take_profit: None,
1204 stop_loss: None,
1205 tpsl_mode: Some(TpslMode::UNKNOWN),
1206 tp_limit_price: Some(dec!(0)),
1207 sl_limit_price: Some(dec!(0)),
1208 tp_trigger_by: None,
1209 sl_trigger_by: None,
1210 trigger_direction: TriggerDirection::UNKNOWN,
1211 trigger_by: None,
1212 last_price_on_created: Some(dec!(0.3679)),
1213 reduce_only: false,
1214 close_on_trigger: false,
1215 place_type: None,
1216 smp_type: SmpType::None,
1217 smp_group: 0,
1218 smp_order_id: None,
1219 created_time: 1766436947940,
1220 updated_time: 1766436947940,
1221 cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1222 }],
1223 };
1224 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1225
1226 let message = deserialize_json(json).unwrap();
1227
1228 assert_eq!(expected, message);
1229 }
1230
1231 #[test]
1232 fn deserialize_incoming_message_order3() {
1233 let json = r#"{"topic":"order","id":"108985347_ADAUSDT_140667102632416","creationTime":1766600379878,"data":[{"category":"linear","symbol":"ADAUSDT","orderId":"f0468cbc-ed2f-4fd7-9620-998f3e9f387c","orderLinkId":"BOT_LINK_ID-1","blockTradeId":"","side":"Buy","positionIdx":0,"orderStatus":"New","cancelType":"UNKNOWN","rejectReason":"EC_NoError","timeInForce":"GTC","isLeverage":"","price":"0.3539","qty":"15","avgPrice":"","leavesQty":"15","leavesValue":"5.3085","cumExecQty":"0","cumExecValue":"0","cumExecFee":"0","orderType":"Limit","stopOrderType":"","orderIv":"","triggerPrice":"","takeProfit":"","stopLoss":"","triggerBy":"","tpTriggerBy":"","slTriggerBy":"","triggerDirection":0,"placeType":"","lastPriceOnCreated":"0.355","closeOnTrigger":false,"reduceOnly":false,"smpGroup":0,"smpType":"None","smpOrderId":"","slLimitPrice":"0","tpLimitPrice":"0","tpslMode":"UNKNOWN","createType":"CreateByUser","marketUnit":"","createdTime":"1766600379876","updatedTime":"1766600379876","feeCurrency":"","closedPnl":"0","slippageTolerance":"0","slippageToleranceType":"UNKNOWN","cumFeeDetail":{}}]}"#;
1234 let order = PrivateMsg {
1235 id: String::from("108985347_ADAUSDT_140667102632416"),
1236 creation_time: 1766600379878,
1237 data: vec![OrderMsg {
1238 category: Category::Linear,
1239 order_id: String::from("f0468cbc-ed2f-4fd7-9620-998f3e9f387c"),
1240 order_link_id: Some(String::from("BOT_LINK_ID-1")),
1241 is_leverage: None,
1242 block_trade_id: None,
1243 symbol: String::from("ADAUSDT"),
1244 price: dec!(0.3539),
1245 broker_order_price: None,
1246 qty: dec!(15),
1247 side: Side::Buy,
1248 position_idx: PositionIdx::OneWay,
1249 order_status: OrderStatus::New,
1250 create_type: Some(CreateType::CreateByUser),
1251 cancel_type: CancelType::UNKNOWN,
1252 reject_reason: RejectReason::EcNoError,
1253 avg_price: None,
1254 leaves_qty: Some(dec!(15)),
1255 leaves_value: Some(dec!(5.3085)),
1256 cum_exec_qty: dec!(0),
1257 cum_exec_value: dec!(0),
1258 cum_exec_fee: dec!(0),
1259 closed_pnl: dec!(0),
1260 fee_currency: None,
1261 time_in_force: TimeInForce::GTC,
1262 order_type: OrderType::Limit,
1263 stop_order_type: None,
1264 oco_trigger_by: None,
1265 order_iv: None,
1266 market_unit: None,
1267 slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1268 slippage_tolerance: Some(dec!(0)),
1269 trigger_price: None,
1270 take_profit: None,
1271 stop_loss: None,
1272 tpsl_mode: Some(TpslMode::UNKNOWN),
1273 tp_limit_price: Some(dec!(0)),
1274 sl_limit_price: Some(dec!(0)),
1275 tp_trigger_by: None,
1276 sl_trigger_by: None,
1277 trigger_direction: TriggerDirection::UNKNOWN,
1278 trigger_by: None,
1279 last_price_on_created: Some(dec!(0.355)),
1280 reduce_only: false,
1281 close_on_trigger: false,
1282 place_type: None, smp_type: SmpType::None,
1284 smp_group: 0,
1285 smp_order_id: None,
1286 created_time: 1766600379876,
1287 updated_time: 1766600379876,
1288 cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1289 }],
1290 };
1291 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1292
1293 let message = deserialize_json(json).unwrap();
1294
1295 assert_eq!(expected, message);
1296 }
1297
1298 #[test]
1299 fn deserialize_incoming_message_position() {
1300 let json = r#"{
1301 "id": "108985347_position_1765659601915",
1302 "topic": "position",
1303 "creationTime": 1765659601915,
1304 "data": [
1305 {
1306 "positionIdx": 1,
1307 "tradeMode": 0,
1308 "riskId": 116,
1309 "riskLimitValue": "200000",
1310 "symbol": "ADAUSDT",
1311 "side": "Buy",
1312 "size": "18720",
1313 "entryPrice": "0.41160027",
1314 "sessionAvgPrice": "",
1315 "leverage": "75",
1316 "positionValue": "7705.157",
1317 "positionBalance": "0",
1318 "markPrice": "0.41",
1319 "positionIM": "106.51735757",
1320 "positionMM": "61.74535757",
1321 "positionIMByMp": "106.51735757",
1322 "positionMMByMp": "61.74535757",
1323 "takeProfit": "0.4321",
1324 "stopLoss": "0.3704",
1325 "trailingStop": "0",
1326 "unrealisedPnl": "-29.957",
1327 "cumRealisedPnl": "-6712.87804378",
1328 "curRealisedPnl": "-2.6317147",
1329 "createdTime": "1714594321840",
1330 "updatedTime": "1765645142548",
1331 "tpslMode": "Full",
1332 "liqPrice": "0.37000066",
1333 "bustPrice": "",
1334 "category": "linear",
1335 "positionStatus": "Normal",
1336 "adlRankIndicator": 2,
1337 "autoAddMargin": 0,
1338 "leverageSysUpdatedTime": "",
1339 "mmrSysUpdatedTime": "",
1340 "seq": 140667058318085,
1341 "isReduceOnly": false
1342 },
1343 {
1344 "positionIdx": 2,
1345 "tradeMode": 0,
1346 "riskId": 116,
1347 "riskLimitValue": "200000",
1348 "symbol": "ADAUSDT",
1349 "side": "",
1350 "size": "0",
1351 "entryPrice": "0",
1352 "sessionAvgPrice": "",
1353 "leverage": "75",
1354 "positionValue": "0",
1355 "positionBalance": "0",
1356 "markPrice": "0.41",
1357 "positionIM": "",
1358 "positionMM": "",
1359 "positionIMByMp": "",
1360 "positionMMByMp": "",
1361 "takeProfit": "0",
1362 "stopLoss": "0",
1363 "trailingStop": "0",
1364 "unrealisedPnl": "0",
1365 "cumRealisedPnl": "1618.30675974",
1366 "curRealisedPnl": "0",
1367 "createdTime": "1714594321840",
1368 "updatedTime": "1765046350698",
1369 "tpslMode": "Full",
1370 "liqPrice": "0",
1371 "bustPrice": "",
1372 "category": "linear",
1373 "positionStatus": "Normal",
1374 "adlRankIndicator": 0,
1375 "autoAddMargin": 0,
1376 "leverageSysUpdatedTime": "",
1377 "mmrSysUpdatedTime": "",
1378 "seq": 140667031311361,
1379 "isReduceOnly": false
1380 }
1381 ]
1382 }"#;
1383 let position = PrivateMsg {
1384 id: String::from("108985347_position_1765659601915"),
1385 creation_time: 1765659601915,
1386 data: vec![
1387 PositionMsg {
1388 category: Category::Linear,
1389 symbol: String::from("ADAUSDT"),
1390 side: Some(Side::Buy),
1391 size: dec!(18720),
1392 position_idx: PositionIdx::Buy,
1393 position_value: dec!(7705.157),
1394 risk_id: 116,
1395 risk_limit_value: Some(dec!(200000)),
1396 entry_price: dec!(0.41160027),
1397 mark_price: dec!(0.41),
1398 leverage: dec!(75),
1399 auto_add_margin: false,
1400 position_im: Some(dec!(106.51735757)),
1401 position_mm: Some(dec!(61.74535757)),
1402 position_im_by_mp: Some(dec!(106.51735757)),
1403 position_mm_by_mp: Some(dec!(61.74535757)),
1404 liq_price: Some(dec!(0.37000066)),
1405 take_profit: dec!(0.4321),
1406 stop_loss: dec!(0.3704),
1407 trailing_stop: dec!(0),
1408 unrealised_pnl: dec!(-29.957),
1409 cur_realised_pnl: dec!(-2.6317147),
1410 session_avg_price: None,
1411 delta: None,
1412 gamma: None,
1413 vega: None,
1414 theta: None,
1415 cum_realised_pnl: dec!(-6712.87804378),
1416 position_status: PositionStatus::Normal,
1417 adl_rank_indicator: AdlRankIndicator::Two,
1418 is_reduce_only: false,
1419 mmr_sys_updated_time: None,
1420 leverage_sys_updated_time: None,
1421 created_time: 1714594321840,
1422 updated_time: 1765645142548,
1423 seq: 140667058318085,
1424 },
1425 PositionMsg {
1426 category: Category::Linear,
1427 symbol: String::from("ADAUSDT"),
1428 side: None,
1429 size: dec!(0),
1430 position_idx: PositionIdx::Sell,
1431 position_value: dec!(0),
1432 risk_id: 116,
1433 risk_limit_value: Some(dec!(200000)),
1434 entry_price: dec!(0),
1435 mark_price: dec!(0.41),
1436 leverage: dec!(75),
1437 auto_add_margin: false,
1438 position_im: None,
1439 position_mm: None,
1440 position_im_by_mp: None,
1441 position_mm_by_mp: None,
1442 liq_price: Some(dec!(0)),
1443 take_profit: dec!(0),
1444 stop_loss: dec!(0),
1445 trailing_stop: dec!(0),
1446 unrealised_pnl: dec!(0),
1447 cur_realised_pnl: dec!(0),
1448 session_avg_price: None,
1449 delta: None,
1450 gamma: None,
1451 vega: None,
1452 theta: None,
1453 cum_realised_pnl: dec!(1618.30675974),
1454 position_status: PositionStatus::Normal,
1455 adl_rank_indicator: AdlRankIndicator::Zero,
1456 is_reduce_only: false,
1457 mmr_sys_updated_time: None,
1458 leverage_sys_updated_time: None,
1459 created_time: 1714594321840,
1460 updated_time: 1765046350698,
1461 seq: 140667031311361,
1462 },
1463 ],
1464 };
1465 let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1466
1467 let message = deserialize_json(json).unwrap();
1468
1469 assert_eq!(expected, message);
1470 }
1471
1472 #[test]
1473 fn deserialize_incoming_message_position2() {
1474 let json = r#"{
1475 "id":"108985347_position_1766316605952",
1476 "topic":"position",
1477 "creationTime":1766316605952,
1478 "data":[
1479 {
1480 "positionIdx":1,
1481 "tradeMode":0,
1482 "riskId":116,
1483 "riskLimitValue":"200000",
1484 "symbol":"ADAUSDT",
1485 "side":"Buy",
1486 "size":"43",
1487 "entryPrice":"0.37293023",
1488 "sessionAvgPrice":"",
1489 "leverage":"75",
1490 "positionValue":"16.036",
1491 "positionBalance":"0",
1492 "markPrice":"0.3702",
1493 "positionIM":"0.22095025",
1494 "positionMM":"0.12809175",
1495 "positionIMByMp":"0.22095025",
1496 "positionMMByMp":"0.12809175",
1497 "takeProfit":"0",
1498 "stopLoss":"0",
1499 "trailingStop":"0",
1500 "unrealisedPnl":"-0.1174",
1501 "cumRealisedPnl":"-7547.8530836",
1502 "curRealisedPnl":"-0.00465061",
1503 "createdTime":"1714594321840",
1504 "updatedTime":"1766313370061",
1505 "tpslMode":"Full",
1506 "liqPrice":"",
1507 "bustPrice":"",
1508 "category":"linear",
1509 "positionStatus":"Normal",
1510 "adlRankIndicator":2,
1511 "autoAddMargin":0,
1512 "leverageSysUpdatedTime":"",
1513 "mmrSysUpdatedTime":"",
1514 "seq":140667089523042,
1515 "isReduceOnly":false
1516 },
1517 {
1518 "positionIdx":2,
1519 "tradeMode":0,
1520 "riskId":116,
1521 "riskLimitValue":"200000",
1522 "symbol":"ADAUSDT",
1523 "side":"",
1524 "size":"0",
1525 "entryPrice":"0",
1526 "sessionAvgPrice":"",
1527 "leverage":"75",
1528 "positionValue":"0",
1529 "positionBalance":"0",
1530 "markPrice":"0.3702",
1531 "positionIM":"",
1532 "positionMM":"",
1533 "positionIMByMp":"",
1534 "positionMMByMp":"",
1535 "takeProfit":"0",
1536 "stopLoss":"0",
1537 "trailingStop":"0",
1538 "unrealisedPnl":"0",
1539 "cumRealisedPnl":"1618.30675974",
1540 "curRealisedPnl":"0",
1541 "createdTime":"1714594321840",
1542 "updatedTime":"1765046350698",
1543 "tpslMode":"Full",
1544 "liqPrice":"0",
1545 "bustPrice":"",
1546 "category":"linear",
1547 "positionStatus":"Normal",
1548 "adlRankIndicator":0,
1549 "autoAddMargin":0,
1550 "leverageSysUpdatedTime":"",
1551 "mmrSysUpdatedTime":"",
1552 "seq":140667031311361,
1553 "isReduceOnly":false
1554 }
1555 ]
1556 }"#;
1557 let position = PrivateMsg {
1558 id: String::from("108985347_position_1766316605952"),
1559 creation_time: 1766316605952,
1560 data: vec![
1561 PositionMsg {
1562 category: Category::Linear,
1563 symbol: String::from("ADAUSDT"),
1564 side: Some(Side::Buy),
1565 size: dec!(43),
1566 position_idx: PositionIdx::Buy,
1567 position_value: dec!(16.036),
1568 risk_id: 116,
1569 risk_limit_value: Some(dec!(200000)),
1570 entry_price: dec!(0.37293023),
1571 mark_price: dec!(0.3702),
1572 leverage: dec!(75),
1573 auto_add_margin: false,
1574 position_im: Some(dec!(0.22095025)),
1575 position_mm: Some(dec!(0.12809175)),
1576 position_im_by_mp: Some(dec!(0.22095025)),
1577 position_mm_by_mp: Some(dec!(0.12809175)),
1578 liq_price: None,
1579 take_profit: dec!(0),
1580 stop_loss: dec!(0),
1581 trailing_stop: dec!(0),
1582 unrealised_pnl: dec!(-0.1174),
1583 cur_realised_pnl: dec!(-0.00465061),
1584 session_avg_price: None,
1585 delta: None,
1586 gamma: None,
1587 vega: None,
1588 theta: None,
1589 cum_realised_pnl: dec!(-7547.8530836),
1590 position_status: PositionStatus::Normal,
1591 adl_rank_indicator: AdlRankIndicator::Two,
1592 is_reduce_only: false,
1593 mmr_sys_updated_time: None,
1594 leverage_sys_updated_time: None,
1595 created_time: 1714594321840,
1596 updated_time: 1766313370061,
1597 seq: 140667089523042,
1598 },
1599 PositionMsg {
1600 category: Category::Linear,
1601 symbol: String::from("ADAUSDT"),
1602 side: None,
1603 size: dec!(0),
1604 position_idx: PositionIdx::Sell,
1605 position_value: dec!(0),
1606 risk_id: 116,
1607 risk_limit_value: Some(dec!(200000)),
1608 entry_price: dec!(0),
1609 mark_price: dec!(0.3702),
1610 leverage: dec!(75),
1611 auto_add_margin: false,
1612 position_im: None,
1613 position_mm: None,
1614 position_im_by_mp: None,
1615 position_mm_by_mp: None,
1616 liq_price: Some(dec!(0)),
1617 take_profit: dec!(0),
1618 stop_loss: dec!(0),
1619 trailing_stop: dec!(0),
1620 unrealised_pnl: dec!(0),
1621 cur_realised_pnl: dec!(0),
1622 session_avg_price: None,
1623 delta: None,
1624 gamma: None,
1625 vega: None,
1626 theta: None,
1627 cum_realised_pnl: dec!(1618.30675974),
1628 position_status: PositionStatus::Normal,
1629 adl_rank_indicator: AdlRankIndicator::Zero,
1630 is_reduce_only: false,
1631 mmr_sys_updated_time: None,
1632 leverage_sys_updated_time: None,
1633 created_time: 1714594321840,
1634 updated_time: 1765046350698,
1635 seq: 140667031311361,
1636 },
1637 ],
1638 };
1639 let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1640
1641 let message = deserialize_json(json).unwrap();
1642
1643 assert_eq!(expected, message);
1644 }
1645
1646 #[test]
1647 fn deserialize_incoming_message_wallet() {
1648 let json = r#"{
1649 "id": "592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e",
1650 "topic": "wallet",
1651 "creationTime": 1700034722104,
1652 "data": [
1653 {
1654 "accountIMRate": "0",
1655 "accountIMRateByMp": "0",
1656 "accountMMRate": "0",
1657 "accountMMRateByMp": "0",
1658 "totalEquity": "10262.91335023",
1659 "totalWalletBalance": "9684.46297164",
1660 "totalMarginBalance": "9684.46297164",
1661 "totalAvailableBalance": "9556.6056555",
1662 "totalPerpUPL": "0",
1663 "totalInitialMargin": "0",
1664 "totalInitialMarginByMp": "0",
1665 "totalMaintenanceMargin": "0",
1666 "totalMaintenanceMarginByMp": "0",
1667 "coin": [
1668 {
1669 "coin": "BTC",
1670 "equity": "0.00102964",
1671 "usdValue": "36.70759517",
1672 "walletBalance": "0.00102964",
1673 "availableToWithdraw": "0.00102964",
1674 "availableToBorrow": "",
1675 "borrowAmount": "0",
1676 "accruedInterest": "0",
1677 "totalOrderIM": "",
1678 "totalPositionIM": "",
1679 "totalPositionMM": "",
1680 "unrealisedPnl": "0",
1681 "cumRealisedPnl": "-0.00000973",
1682 "bonus": "0",
1683 "collateralSwitch": true,
1684 "marginCollateral": true,
1685 "locked": "0",
1686 "spotHedgingQty": "0.01592413",
1687 "spotBorrow": "0"
1688 }
1689 ],
1690 "accountLTV": "0",
1691 "accountType": "UNIFIED"
1692 }
1693 ]
1694 }"#;
1695 let coin = WalletCoin {
1696 coin: String::from("BTC"),
1697 equity: dec!(0.00102964),
1698 usd_value: dec!(36.70759517),
1699 wallet_balance: dec!(0.00102964),
1700 locked: dec!(0),
1701 spot_hedging_qty: dec!(0.01592413),
1702 borrow_amount: dec!(0),
1703 accrued_interest: dec!(0),
1704 total_order_im: None,
1705 total_position_im: None,
1706 total_position_mm: None,
1707 unrealised_pnl: dec!(0),
1708 cum_realised_pnl: dec!(-0.00000973),
1709 bonus: dec!(0),
1710 collateral_switch: true,
1711 margin_collateral: true,
1712 spot_borrow: Some(dec!(0)),
1713 };
1714 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
1715 let wallet = PrivateMsg {
1716 id: String::from("592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e"),
1717 creation_time: 1700034722104,
1718 data: vec![WalletMsg {
1719 account_type: AccountType::UNIFIED,
1720 account_im_rate: dec!(0),
1721 account_im_rate_by_mp: dec!(0),
1722 account_mm_rate: dec!(0),
1723 account_mm_rate_by_mp: dec!(0),
1724 total_equity: dec!(10262.91335023),
1725 total_wallet_balance: dec!(9684.46297164),
1726 total_margin_balance: dec!(9684.46297164),
1727 total_available_balance: dec!(9556.6056555),
1728 total_perp_upl: dec!(0),
1729 total_initial_margin: dec!(0),
1730 total_initial_margin_by_mp: dec!(0),
1731 total_maintenance_margin: dec!(0),
1732 total_maintenance_margin_by_mp: dec!(0),
1733 coin,
1734 }],
1735 };
1736 let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
1737
1738 let message = deserialize_json(json).unwrap();
1739
1740 assert_eq!(expected, message);
1741 }
1742
1743 #[test]
1744 fn deserialize_incoming_message_wallet2() {
1745 let json = r#"{
1746 "id":"108985347_wallet_1766318882965",
1747 "topic":"wallet",
1748 "creationTime":1766318882964,
1749 "data":[
1750 {
1751 "accountIMRate":"0.0007",
1752 "accountMMRate":"0.0004",
1753 "accountIMRateByMp":"0.0007",
1754 "accountMMRateByMp":"0.0004",
1755 "totalEquity":"102.7094181",
1756 "totalWalletBalance":"102.16591975",
1757 "totalMarginBalance":"102.16591975",
1758 "totalAvailableBalance":"102.09402758",
1759 "totalPerpUPL":"0",
1760 "totalInitialMargin":"0.07189217",
1761 "totalMaintenanceMargin":"0.04166941",
1762 "totalInitialMarginByMp":"0.07189217",
1763 "totalMaintenanceMarginByMp":"0.04166941",
1764 "coin":[
1765 {
1766 "coin":"USDT",
1767 "equity":"75.5601152",
1768 "usdValue":"75.53450032",
1769 "walletBalance":"75.5601152",
1770 "availableToWithdraw":"",
1771 "availableToBorrow":"",
1772 "borrowAmount":"0",
1773 "accruedInterest":"0",
1774 "totalOrderIM":"0",
1775 "totalPositionIM":"0.07191655",
1776 "totalPositionMM":"0.04168355",
1777 "unrealisedPnl":"0",
1778 "cumRealisedPnl":"36163.8134634",
1779 "bonus":"0",
1780 "collateralSwitch":true,
1781 "marginCollateral":true,
1782 "locked":"0",
1783 "spotHedgingQty":"0"
1784 }
1785 ],
1786 "accountLTV":"0",
1787 "accountType":"UNIFIED"
1788 }
1789 ]
1790 }"#;
1791 let coin = WalletCoin {
1792 coin: String::from("USDT"),
1793 equity: dec!(75.5601152),
1794 usd_value: dec!(75.53450032),
1795 wallet_balance: dec!(75.5601152),
1796 locked: dec!(0),
1797 spot_hedging_qty: dec!(0),
1798 borrow_amount: dec!(0),
1799 accrued_interest: dec!(0),
1800 total_order_im: Some(dec!(0)),
1801 total_position_im: Some(dec!(0.07191655)),
1802 total_position_mm: Some(dec!(0.04168355)),
1803 unrealised_pnl: dec!(0),
1804 cum_realised_pnl: dec!(36163.8134634),
1805 bonus: dec!(0),
1806 collateral_switch: true,
1807 margin_collateral: true,
1808 spot_borrow: None,
1809 };
1810 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
1811 let wallet = PrivateMsg {
1812 id: String::from("108985347_wallet_1766318882965"),
1813 creation_time: 1766318882964,
1814 data: vec![WalletMsg {
1815 account_type: AccountType::UNIFIED,
1816 account_im_rate: dec!(0.0007),
1817 account_im_rate_by_mp: dec!(0.0007),
1818 account_mm_rate: dec!(0.0004),
1819 account_mm_rate_by_mp: dec!(0.0004),
1820 total_equity: dec!(102.7094181),
1821 total_wallet_balance: dec!(102.16591975),
1822 total_margin_balance: dec!(102.16591975),
1823 total_available_balance: dec!(102.09402758),
1824 total_perp_upl: dec!(0),
1825 total_initial_margin: dec!(0.07189217),
1826 total_initial_margin_by_mp: dec!(0.07189217),
1827 total_maintenance_margin: dec!(0.04166941),
1828 total_maintenance_margin_by_mp: dec!(0.04166941),
1829 coin,
1830 }],
1831 };
1832 let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
1833
1834 let message = deserialize_json(json).unwrap();
1835
1836 assert_eq!(expected, message);
1837 }
1838
1839 #[test]
1840 fn deserialize_incoming_message_execution() {
1841 let json = r#"{
1842 "topic": "execution",
1843 "id": "386825804_BTCUSDT_140612148849382",
1844 "creationTime": 1746270400355,
1845 "data": [
1846 {
1847 "category": "linear",
1848 "symbol": "BTCUSDT",
1849 "closedSize": "0.5",
1850 "execFee": "26.3725275",
1851 "execId": "0ab1bdf7-4219-438b-b30a-32ec863018f7",
1852 "execPrice": "95900.1",
1853 "execQty": "0.5",
1854 "execType": "Trade",
1855 "execValue": "47950.05",
1856 "feeRate": "0.00055",
1857 "tradeIv": "",
1858 "markIv": "",
1859 "blockTradeId": "",
1860 "markPrice": "95901.48",
1861 "indexPrice": "",
1862 "underlyingPrice": "",
1863 "leavesQty": "0",
1864 "orderId": "9aac161b-8ed6-450d-9cab-c5cc67c21784",
1865 "orderLinkId": "",
1866 "orderPrice": "94942.5",
1867 "orderQty": "0.5",
1868 "orderType": "Market",
1869 "stopOrderType": "UNKNOWN",
1870 "side": "Sell",
1871 "execTime": "1746270400353",
1872 "isLeverage": "0",
1873 "isMaker": false,
1874 "seq": 140612148849382,
1875 "marketUnit": "",
1876 "execPnl": "0.05",
1877 "createType": "CreateByUser",
1878 "extraFees":[{"feeCoin":"USDT","feeType":"GST","subFeeType":"IND_GST","feeRate":"0.0000675","fee":"0.006403779"}],
1879 "feeCurrency": "USDT"
1880 }
1881 ]
1882 }"#;
1883 let execution = PrivateMsg {
1884 id: String::from("386825804_BTCUSDT_140612148849382"),
1885 creation_time: 1746270400355,
1886 data: vec![ExecutionMsg {
1887 category: Category::Linear,
1888 symbol: String::from("BTCUSDT"),
1889 is_leverage: false,
1890 order_id: String::from("9aac161b-8ed6-450d-9cab-c5cc67c21784"),
1891 order_link_id: None,
1892 side: Side::Sell,
1893 order_price: dec!(94942.5),
1894 order_qty: dec!(0.5),
1895 leaves_qty: dec!(0),
1896 create_type: CreateType::CreateByUser,
1897 order_type: OrderType::Market,
1898 stop_order_type: StopOrderType::UNKNOWN,
1899 exec_fee: dec!(26.3725275),
1900 exec_id: String::from("0ab1bdf7-4219-438b-b30a-32ec863018f7"),
1901 exec_price: dec!(95900.1),
1902 exec_qty: dec!(0.5),
1903 exec_pnl: dec!(0.05),
1904 exec_type: ExecType::Trade,
1905 exec_value: dec!(47950.05),
1906 exec_time: 1746270400353,
1907 is_maker: false,
1908 fee_rate: dec!(0.00055),
1909 trade_iv: None,
1910 mark_iv: None,
1911 mark_price: dec!(95901.48),
1912 index_price: None,
1913 underlying_price: None,
1914 block_trade_id: None,
1915 closed_size: dec!(0.5),
1916 extra_fees: Some(vec![ExtraFee {
1917 fee_coin: String::from("USDT"),
1918 fee_type: ExtraFeeType::Gst,
1919 sub_fee_type: ExtraSubFeeType::IndGst,
1920 fee_rate: dec!(0.0000675),
1921 fee: dec!(0.006403779),
1922 }]),
1923 seq: 140612148849382,
1924 fee_currency: String::from("USDT"),
1925 }],
1926 };
1927 let expected = IncomingMessage::Topic(TopicMessage::Execution(execution));
1928
1929 let message = deserialize_json(json).unwrap();
1930
1931 assert_eq!(expected, message);
1932 }
1933}