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::{OrderbookLevel, 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 Orderbook(OrderbookMsg),
32 AllLiquidation(AllLiquidationMsg),
33 Topic(TopicMessage),
34}
35
36impl IncomingMessage {
37 pub fn is_pong(&self) -> bool {
38 matches!(
39 self,
40 IncomingMessage::Command(CommandMsg::Pong {
41 req_id: _,
42 ret_msg: _,
43 conn_id: _,
44 args: _,
45 success: _,
46 })
47 )
48 }
49 pub fn is_ping(&self) -> bool {
50 matches!(
51 self,
52 IncomingMessage::Command(CommandMsg::Ping {
53 req_id: _,
54 ret_msg: _,
55 conn_id: _,
56 args: _,
57 success: _,
58 })
59 )
60 }
61}
62
63#[derive(PartialEq, Deserialize, Debug)]
64#[serde(tag = "op")]
65pub enum CommandMsg {
66 #[serde(rename = "subscribe")]
67 Subscribe {
68 #[serde(default, deserialize_with = "empty_string_as_none")]
69 req_id: Option<String>,
70 #[serde(default, deserialize_with = "empty_string_as_none")]
71 ret_msg: Option<String>,
72 conn_id: String,
73 success: bool,
74 },
75 #[serde(rename = "unsubscribe")]
76 Unsubscribe {
77 #[serde(default, deserialize_with = "empty_string_as_none")]
78 req_id: Option<String>,
79 #[serde(default, deserialize_with = "empty_string_as_none")]
80 ret_msg: Option<String>,
81 conn_id: String,
82 success: bool,
83 },
84 #[serde(rename = "auth")]
85 Auth {
86 #[serde(default, deserialize_with = "empty_string_as_none")]
87 req_id: Option<String>,
88 #[serde(default, deserialize_with = "empty_string_as_none")]
89 ret_msg: Option<String>,
90 conn_id: String,
91 success: bool,
92 },
93 #[serde(rename = "pong")]
94 Pong {
95 #[serde(default, deserialize_with = "empty_string_as_none")]
96 req_id: Option<String>,
97 #[serde(default, deserialize_with = "empty_string_as_none")]
98 ret_msg: Option<String>,
99 conn_id: String,
100 args: Option<Vec<String>>,
101 success: Option<bool>,
102 },
103 #[serde(rename = "ping")]
104 Ping {
105 #[serde(default, deserialize_with = "empty_string_as_none")]
106 req_id: Option<String>,
107 #[serde(default, deserialize_with = "empty_string_as_none")]
108 ret_msg: Option<String>,
109 conn_id: String,
110 args: Option<Vec<String>>,
111 success: bool,
112 },
113}
114
115#[derive(PartialEq, Deserialize, Debug)]
117#[serde(tag = "type")]
118pub enum TickerMsg {
119 #[serde(rename = "snapshot")]
120 Snapshot {
121 topic: Topic,
122 #[serde(default, deserialize_with = "option_number")]
123 cs: Option<u64>,
124 ts: Timestamp,
125 data: TickerSnapshotMsg,
126 },
127 #[serde(rename = "delta")]
128 Delta {
129 topic: Topic,
130 #[serde(default, deserialize_with = "option_number")]
131 cs: Option<u64>,
132 ts: Timestamp,
133 data: TickerDeltaMsg,
134 },
135}
136
137#[derive(PartialEq, Deserialize, Debug)]
138#[serde(rename_all = "camelCase")]
139pub struct TickerSnapshotMsg {
140 pub symbol: String,
141 pub tick_direction: TickDirection,
142 pub last_price: Decimal,
143 #[serde(default, deserialize_with = "option_decimal")]
144 pub pre_open_price: Option<Decimal>,
145 #[serde(default, deserialize_with = "option_decimal")]
146 pub pre_qty: Option<Decimal>,
147 #[serde(default, deserialize_with = "empty_string_as_none")]
148 pub cur_pre_listing_phase: Option<String>,
149 pub prev_price24h: Decimal,
150 pub price24h_pcnt: Decimal,
151 pub high_price24h: Decimal,
152 pub low_price24h: Decimal,
153 pub prev_price1h: Decimal,
154 pub mark_price: Decimal,
155 pub index_price: Decimal,
156 pub open_interest: Decimal,
157 pub open_interest_value: Decimal,
158 pub turnover24h: Decimal,
159 pub volume24h: Decimal,
160 pub funding_rate: Decimal,
161 #[serde(default, deserialize_with = "number")]
162 pub next_funding_time: Timestamp,
163 pub bid1_price: Decimal,
164 pub bid1_size: Decimal,
165 pub ask1_price: Decimal,
166 pub ask1_size: Decimal,
167 #[serde(default, deserialize_with = "option_number")]
168 pub delivery_time: Option<Timestamp>,
169 #[serde(default, deserialize_with = "option_decimal")]
170 pub basis_rate: Option<Decimal>,
171 #[serde(default, deserialize_with = "option_decimal")]
172 pub delivery_fee_rate: Option<Decimal>,
173 #[serde(default, deserialize_with = "option_decimal")]
174 pub predicted_delivery_price: Option<Decimal>,
175}
176
177#[derive(PartialEq, Deserialize, Debug)]
178#[serde(rename_all = "camelCase")]
179pub struct TickerDeltaMsg {
180 pub symbol: String,
181 #[serde(default, deserialize_with = "empty_string_as_none")]
182 pub tick_direction: Option<TickDirection>,
183 #[serde(default, deserialize_with = "option_decimal")]
184 pub last_price: Option<Decimal>,
185 #[serde(default, deserialize_with = "option_decimal")]
186 pub pre_open_price: Option<Decimal>,
187 #[serde(default, deserialize_with = "option_decimal")]
188 pub pre_qty: Option<Decimal>,
189 #[serde(default, deserialize_with = "empty_string_as_none")]
190 pub cur_pre_listing_phase: Option<String>,
191 #[serde(default, deserialize_with = "option_decimal")]
192 pub prev_price24h: Option<Decimal>,
193 #[serde(default, deserialize_with = "option_decimal")]
194 pub price24h_pcnt: Option<Decimal>,
195 #[serde(default, deserialize_with = "option_decimal")]
196 pub high_price24h: Option<Decimal>,
197 #[serde(default, deserialize_with = "option_decimal")]
198 pub low_price24h: Option<Decimal>,
199 #[serde(default, deserialize_with = "option_decimal")]
200 pub prev_price1h: Option<Decimal>,
201 #[serde(default, deserialize_with = "option_decimal")]
202 pub mark_price: Option<Decimal>,
203 #[serde(default, deserialize_with = "option_decimal")]
204 pub index_price: Option<Decimal>,
205 #[serde(default, deserialize_with = "option_decimal")]
206 pub open_interest: Option<Decimal>,
207 #[serde(default, deserialize_with = "option_decimal")]
208 pub open_interest_value: Option<Decimal>,
209 #[serde(default, deserialize_with = "option_decimal")]
210 pub turnover24h: Option<Decimal>,
211 #[serde(default, deserialize_with = "option_decimal")]
212 pub volume24h: Option<Decimal>,
213 #[serde(default, deserialize_with = "option_decimal")]
214 pub funding_rate: Option<Decimal>,
215 #[serde(default, deserialize_with = "option_decimal")]
216 pub next_funding_time: Option<Decimal>,
217 #[serde(default, deserialize_with = "option_decimal")]
218 pub bid1_price: Option<Decimal>,
219 #[serde(default, deserialize_with = "option_decimal")]
220 pub bid1_size: Option<Decimal>,
221 #[serde(default, deserialize_with = "option_decimal")]
222 pub ask1_price: Option<Decimal>,
223 #[serde(default, deserialize_with = "option_decimal")]
224 pub ask1_size: Option<Decimal>,
225 #[serde(default, deserialize_with = "option_number")]
226 pub delivery_time: Option<Timestamp>,
227 #[serde(default, deserialize_with = "option_decimal")]
228 pub basis_rate: Option<Decimal>,
229 #[serde(default, deserialize_with = "option_decimal")]
230 pub delivery_fee_rate: Option<Decimal>,
231 #[serde(default, deserialize_with = "option_decimal")]
232 pub predicted_delivery_price: Option<Decimal>,
233}
234
235#[derive(PartialEq, Deserialize, Debug)]
237#[serde(tag = "type")]
238pub enum TradeMsg {
239 #[serde(rename = "snapshot")]
240 Snapshot {
241 #[serde(default, deserialize_with = "empty_string_as_none")]
242 id: Option<String>,
243 topic: Topic,
244 ts: Timestamp,
245 data: Vec<TradeSnapshotMsg>,
246 },
247}
248
249#[derive(PartialEq, Deserialize, Debug)]
250pub struct TradeSnapshotMsg {
251 #[serde(rename = "T")]
252 pub time: Timestamp,
253 #[serde(rename = "s")]
254 pub symbol: String,
255 #[serde(rename = "S")]
256 pub side: Side,
257 #[serde(rename = "v")]
258 pub size: Decimal,
259 #[serde(rename = "p")]
260 pub price: Decimal,
261 #[serde(rename = "L")]
262 pub tick_direction: TickDirection,
263 #[serde(rename = "i")]
264 pub trade_id: String,
265 #[serde(rename = "BT")]
266 pub block_trade: bool,
267 #[serde(rename = "RPI")]
268 pub rpi_trade: Option<bool>,
269 #[serde(rename = "mP", default, deserialize_with = "empty_string_as_none")]
270 pub mark_price: Option<String>,
271 #[serde(rename = "iP", default, deserialize_with = "empty_string_as_none")]
272 pub index_price: Option<String>,
273 #[serde(rename = "mlv", default, deserialize_with = "empty_string_as_none")]
274 pub mark_iv: Option<String>,
275 #[serde(rename = "iv", default, deserialize_with = "empty_string_as_none")]
276 pub iv: Option<String>,
277}
278
279#[derive(PartialEq, Deserialize, Debug)]
281#[serde(tag = "type")]
282pub enum KLineMsg {
283 #[serde(rename = "snapshot")]
284 Snapshot {
285 topic: Topic,
286 ts: Timestamp,
287 data: Vec<KLineSnapshotMsg>,
288 },
289}
290
291#[derive(PartialEq, Deserialize, Debug)]
292pub struct KLineSnapshotMsg {
293 pub start: Timestamp,
294 pub end: Timestamp,
295 pub interval: Interval,
296 pub open: Decimal,
297 pub close: Decimal,
298 pub high: Decimal,
299 pub low: Decimal,
300 pub volume: Decimal,
301 pub turnover: Decimal,
302 pub confirm: bool,
303 pub timestamp: Timestamp,
304}
305
306#[derive(PartialEq, Deserialize, Debug)]
308#[serde(tag = "type")]
309pub enum OrderbookMsg {
310 #[serde(rename = "snapshot")]
311 Snapshot {
312 topic: Topic,
313 ts: Timestamp,
314 data: OrderbookDataMsg,
315 cts: Timestamp,
316 },
317 #[serde(rename = "delta")]
318 Delta {
319 topic: Topic,
320 ts: Timestamp,
321 data: OrderbookDataMsg,
322 cts: Timestamp,
323 },
324}
325
326#[derive(PartialEq, Deserialize, Debug)]
327pub struct OrderbookDataMsg {
328 #[serde(rename = "s")]
330 pub symbol: String,
331 #[serde(rename = "b")]
334 pub bids: Vec<OrderbookLevel>,
335 #[serde(rename = "a")]
338 pub asks: Vec<OrderbookLevel>,
339 #[serde(rename = "u")]
342 pub update_id: i64,
343 pub seq: i64,
346}
347
348#[derive(PartialEq, Deserialize, Debug)]
350#[serde(tag = "type")]
351pub enum AllLiquidationMsg {
352 #[serde(rename = "snapshot")]
353 Snapshot {
354 topic: Topic,
355 ts: Timestamp,
356 data: Vec<AllLiquidationSnapshotMsg>,
357 },
358}
359
360#[derive(PartialEq, Deserialize, Debug)]
361pub struct AllLiquidationSnapshotMsg {
362 #[serde(rename = "T")]
363 pub time: Timestamp,
364 #[serde(rename = "s")]
365 pub symbol: String,
366 #[serde(rename = "S")]
368 pub side: Side,
369 #[serde(rename = "v")]
370 pub size: Decimal,
371 #[serde(rename = "p")]
372 pub price: Decimal,
373}
374
375#[derive(PartialEq, Deserialize, Debug)]
376#[serde(tag = "topic")] pub enum TopicMessage {
378 #[serde(rename = "order")]
379 Order(PrivateMsg<Vec<OrderMsg>>),
380 #[serde(rename = "position")]
381 Position(PrivateMsg<Vec<PositionMsg>>),
382 #[serde(rename = "wallet")]
383 Wallet(PrivateMsg<Vec<WalletMsg>>),
384 #[serde(rename = "execution")]
385 Execution(PrivateMsg<Vec<ExecutionMsg>>),
386 #[serde(rename = "fastExecution")]
388 FastExecution(PrivateMsg<Vec<FastExecutionMsg>>),
389 #[serde(rename = "greeks")]
391 Greeks(PrivateMsg<Vec<GreekMsg>>),
392 #[serde(rename = "dcp")]
394 Dcp(PrivateMsg<DcpMsg>),
395}
396
397#[derive(PartialEq, Deserialize, Debug)]
398#[serde(rename_all = "camelCase")]
399pub struct PublicMsg<T> {
400 #[serde(default, deserialize_with = "empty_string_as_none")]
401 id: Option<String>,
402 #[serde(default, deserialize_with = "option_number")]
403 cs: Option<u64>,
404 ts: Timestamp,
405 data: T,
406}
407
408#[derive(PartialEq, Deserialize, Debug)]
409#[serde(rename_all = "camelCase")]
410pub struct PrivateMsg<T> {
411 pub id: String,
414 pub creation_time: Timestamp,
416 pub data: T,
417}
418
419#[derive(PartialEq, Deserialize, Debug, Clone)]
420#[serde(rename_all = "camelCase")]
421pub struct OrderMsg {
422 pub category: Category,
426 pub order_id: String,
428 #[serde(default, deserialize_with = "empty_string_as_none")]
430 pub order_link_id: Option<String>,
431 #[serde(default, deserialize_with = "string_to_option_bool")]
435 pub is_leverage: Option<bool>,
436 #[serde(default, deserialize_with = "empty_string_as_none")]
438 pub block_trade_id: Option<String>,
439 pub symbol: String,
441 pub price: Decimal,
443 #[serde(default, deserialize_with = "option_decimal")]
445 pub broker_order_price: Option<Decimal>,
446 pub qty: Decimal,
448 pub side: Side,
450 pub position_idx: PositionIdx,
452 pub order_status: OrderStatus,
454 #[serde(default, deserialize_with = "empty_string_as_none")]
458 pub create_type: Option<CreateType>,
459 pub cancel_type: CancelType,
461 pub reject_reason: RejectReason,
463 #[serde(default, deserialize_with = "option_decimal")]
467 pub avg_price: Option<Decimal>,
468 #[serde(default, deserialize_with = "option_decimal")]
470 pub leaves_qty: Option<Decimal>,
471 #[serde(default, deserialize_with = "option_decimal")]
473 pub leaves_value: Option<Decimal>,
474 pub cum_exec_qty: Decimal,
476 pub cum_exec_value: Decimal,
478 pub cum_exec_fee: Decimal,
482 pub cum_fee_detail: Option<serde_json::Value>,
484 pub closed_pnl: Decimal,
486 #[serde(deserialize_with = "option_decimal")]
488 pub fee_currency: Option<Decimal>,
489 pub time_in_force: TimeInForce,
491 pub order_type: OrderType,
493 #[serde(default, deserialize_with = "empty_string_as_none")]
495 pub stop_order_type: Option<StopOrderType>,
496 #[serde(default, deserialize_with = "empty_string_as_none")]
498 pub oco_trigger_by: Option<OcoTriggerBy>,
499 #[serde(deserialize_with = "option_decimal")]
501 pub order_iv: Option<Decimal>,
502 #[serde(default, deserialize_with = "empty_string_as_none")]
504 pub market_unit: Option<String>,
505 #[serde(default, deserialize_with = "empty_string_as_none")]
507 pub slippage_tolerance_type: Option<SlippageToleranceType>,
508 #[serde(default, deserialize_with = "option_decimal")]
510 pub slippage_tolerance: Option<Decimal>,
511 #[serde(deserialize_with = "option_decimal")]
513 pub trigger_price: Option<Decimal>,
514 #[serde(deserialize_with = "option_decimal")]
516 pub take_profit: Option<Decimal>,
517 #[serde(deserialize_with = "option_decimal")]
519 pub stop_loss: Option<Decimal>,
520 #[serde(default, deserialize_with = "empty_string_as_none")]
522 pub tpsl_mode: Option<TpslMode>,
523 #[serde(deserialize_with = "option_decimal")]
525 pub tp_limit_price: Option<Decimal>,
526 #[serde(deserialize_with = "option_decimal")]
528 pub sl_limit_price: Option<Decimal>,
529 #[serde(default, deserialize_with = "empty_string_as_none")]
531 pub tp_trigger_by: Option<TriggerBy>,
532 #[serde(default, deserialize_with = "empty_string_as_none")]
534 pub sl_trigger_by: Option<TriggerBy>,
535 pub trigger_direction: TriggerDirection,
537 #[serde(default, deserialize_with = "empty_string_as_none")]
539 pub trigger_by: Option<TriggerBy>,
540 #[serde(deserialize_with = "option_decimal")]
542 pub last_price_on_created: Option<Decimal>,
543 pub reduce_only: bool,
545 pub close_on_trigger: bool,
547 #[serde(default, deserialize_with = "empty_string_as_none")]
549 pub place_type: Option<PlaceType>,
550 pub smp_type: SmpType,
552 #[serde(deserialize_with = "number")]
554 pub smp_group: i64,
555 #[serde(default, deserialize_with = "empty_string_as_none")]
557 pub smp_order_id: Option<String>,
558 #[serde(deserialize_with = "number")]
560 pub created_time: Timestamp,
561 #[serde(deserialize_with = "number")]
563 pub updated_time: Timestamp,
564}
565
566#[derive(PartialEq, Deserialize, Debug, Clone)]
567#[serde(rename_all = "camelCase")]
568pub struct PositionMsg {
569 pub category: Category,
571 pub symbol: String,
573 #[serde(default, deserialize_with = "empty_string_as_none")]
577 pub side: Option<Side>,
578 pub size: Decimal,
580 pub position_idx: PositionIdx,
582 pub position_value: Decimal,
584 #[serde(deserialize_with = "number")]
587 pub risk_id: i64,
588 #[serde(default, deserialize_with = "option_decimal")]
591 pub risk_limit_value: Option<Decimal>,
592 pub entry_price: Decimal,
594 pub mark_price: Decimal,
596 pub leverage: Decimal,
599 #[serde(default, deserialize_with = "int_to_bool")]
601 pub auto_add_margin: bool,
602 #[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
605 pub position_im: Option<Decimal>,
606 #[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
609 pub position_mm: Option<Decimal>,
610 #[serde(
613 rename = "positionIMByMp",
614 default,
615 deserialize_with = "option_decimal"
616 )]
617 pub position_im_by_mp: Option<Decimal>,
618 #[serde(
621 rename = "positionMMByMp",
622 default,
623 deserialize_with = "option_decimal"
624 )]
625 pub position_mm_by_mp: Option<Decimal>,
626 #[serde(default, deserialize_with = "option_decimal")]
633 pub liq_price: Option<Decimal>,
634 pub take_profit: Decimal,
636 pub stop_loss: Decimal,
638 pub trailing_stop: Decimal,
640 pub unrealised_pnl: Decimal,
642 pub cur_realised_pnl: Decimal,
644 #[serde(default, deserialize_with = "option_decimal")]
646 pub session_avg_price: Option<Decimal>,
647 #[serde(default, deserialize_with = "empty_string_as_none")]
649 pub delta: Option<String>,
650 #[serde(default, deserialize_with = "empty_string_as_none")]
652 pub gamma: Option<String>,
653 #[serde(default, deserialize_with = "empty_string_as_none")]
655 pub vega: Option<String>,
656 #[serde(default, deserialize_with = "empty_string_as_none")]
658 pub theta: Option<String>,
659 pub cum_realised_pnl: Decimal,
663 pub position_status: PositionStatus,
665 pub adl_rank_indicator: AdlRankIndicator,
667 pub is_reduce_only: bool,
672 #[serde(deserialize_with = "option_number")]
679 pub mmr_sys_updated_time: Option<Timestamp>,
680 #[serde(deserialize_with = "option_number")]
687 pub leverage_sys_updated_time: Option<Timestamp>,
688 #[serde(deserialize_with = "number")]
690 pub created_time: Timestamp,
691 #[serde(deserialize_with = "number")]
693 pub updated_time: Timestamp,
694 pub seq: i64,
699}
700
701#[derive(PartialEq, Deserialize, Debug, Clone)]
702#[serde(rename_all = "camelCase")]
703pub struct WalletMsg {
704 pub account_type: AccountType,
709 #[serde(rename = "accountIMRate")]
716 pub account_im_rate: Decimal,
717 #[serde(rename = "accountMMRate")]
719 pub account_mm_rate: Decimal,
720 pub total_equity: Decimal,
722 pub total_wallet_balance: Decimal,
724 pub total_margin_balance: Decimal,
726 pub total_available_balance: Decimal,
728 #[serde(rename = "totalPerpUPL")]
730 pub total_perp_upl: Decimal,
731 pub total_initial_margin: Decimal,
733 pub total_maintenance_margin: Decimal,
735 #[serde(rename = "accountIMRateByMp")]
737 pub account_im_rate_by_mp: Decimal,
738 #[serde(rename = "accountMMRateByMp")]
740 pub account_mm_rate_by_mp: Decimal,
741 #[serde(rename = "totalInitialMarginByMp")]
743 pub total_initial_margin_by_mp: Decimal,
744 #[serde(rename = "totalMaintenanceMarginByMp")]
746 pub total_maintenance_margin_by_mp: Decimal,
747 #[serde(deserialize_with = "hash_map")]
748 pub coin: HashMap<String, WalletCoin>,
749}
750
751#[derive(PartialEq, Deserialize, Debug, Clone)]
752#[serde(rename_all = "camelCase")]
753pub struct ExecutionMsg {
754 pub category: Category,
756 pub symbol: String,
758 #[serde(default, deserialize_with = "string_to_bool")]
760 pub is_leverage: bool,
761 pub order_id: String,
763 #[serde(default, deserialize_with = "empty_string_as_none")]
765 pub order_link_id: Option<String>,
766 pub side: Side,
768 pub order_price: Decimal,
770 pub order_qty: Decimal,
772 pub leaves_qty: Decimal,
774 pub create_type: CreateType,
777 pub order_type: OrderType,
779 pub stop_order_type: StopOrderType,
781 pub exec_fee: Decimal,
783 pub exec_id: String,
785 pub exec_price: Decimal,
787 pub exec_qty: Decimal,
789 pub exec_pnl: Decimal,
791 pub exec_type: ExecType,
793 pub exec_value: Decimal,
795 #[serde(deserialize_with = "number")]
797 pub exec_time: Timestamp,
798 pub is_maker: bool,
800 pub fee_rate: Decimal,
802 #[serde(default, deserialize_with = "option_decimal")]
804 pub trade_iv: Option<Decimal>,
805 #[serde(default, deserialize_with = "option_decimal")]
807 pub mark_iv: Option<Decimal>,
808 pub mark_price: Decimal,
810 #[serde(default, deserialize_with = "option_decimal")]
812 pub index_price: Option<Decimal>,
813 #[serde(default, deserialize_with = "option_decimal")]
815 pub underlying_price: Option<Decimal>,
816 #[serde(default, deserialize_with = "empty_string_as_none")]
818 pub block_trade_id: Option<String>,
819 pub closed_size: Decimal,
821 pub extra_fees: Option<Vec<ExtraFee>>, pub seq: i64,
827 pub fee_currency: String,
829}
830
831#[derive(PartialEq, Deserialize, Debug, Clone)]
832#[serde(rename_all = "camelCase")]
833pub struct ExtraFee {
834 pub fee_coin: String,
835 pub fee_type: ExtraFeeType,
836 pub sub_fee_type: ExtraSubFeeType,
837 pub fee_rate: Decimal,
838 pub fee: Decimal,
839}
840
841#[derive(PartialEq, Deserialize, Debug, Clone)]
847#[serde(rename_all = "camelCase")]
848pub struct FastExecutionMsg {
849 pub category: Category,
850 pub symbol: String,
851 pub exec_id: String,
852 pub exec_price: Decimal,
853 pub exec_qty: Decimal,
854 pub side: Side,
855 pub order_qty: Decimal,
856 pub order_id: String,
857 #[serde(default, deserialize_with = "empty_string_as_none")]
858 pub order_link_id: Option<String>,
859 pub is_maker: bool,
860 pub fee_currency: String,
861 pub fee_rate: Decimal,
862 pub exec_fee: Decimal,
863 #[serde(default, deserialize_with = "option_decimal")]
864 pub trade_iv: Option<Decimal>,
865 #[serde(default, deserialize_with = "option_decimal")]
866 pub mark_iv: Option<Decimal>,
867 pub mark_price: Decimal,
868 #[serde(default, deserialize_with = "option_decimal")]
869 pub index_price: Option<Decimal>,
870 #[serde(default, deserialize_with = "option_decimal")]
871 pub underlying_price: Option<Decimal>,
872 #[serde(default, deserialize_with = "empty_string_as_none")]
873 pub block_trade_id: Option<String>,
874 #[serde(deserialize_with = "number")]
875 pub exec_time: Timestamp,
876}
877
878#[derive(PartialEq, Deserialize, Debug, Clone)]
883#[serde(rename_all = "camelCase")]
884pub struct GreekMsg {
885 pub base_coin: String,
886 pub total_delta: Decimal,
887 pub total_gamma: Decimal,
888 pub total_vega: Decimal,
889 pub total_theta: Decimal,
890}
891
892#[derive(PartialEq, Deserialize, Debug, Clone)]
895#[serde(rename_all = "camelCase")]
896pub struct DcpMsg {
897 pub product: String,
899 pub dcp_status: String,
901 pub time_window: u64,
903 #[serde(deserialize_with = "number")]
905 pub updated_at: Timestamp,
906}
907
908#[cfg(test)]
909mod tests {
910 use rust_decimal::dec;
911
912 use crate::DepthLevel;
913 use crate::serde::{Unique, deserialize_json};
914
915 use super::*;
916
917 #[test]
918 fn deserialize_incoming_message_command_subscribe() {
919 let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"subscribe"}"#;
920 let expected = IncomingMessage::Command(CommandMsg::Subscribe {
921 req_id: None,
922 ret_msg: None,
923 conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
924 success: true,
925 });
926
927 let message = deserialize_json(json).unwrap();
928
929 assert_eq!(expected, message);
930 }
931
932 #[test]
933 fn deserialize_incoming_message_command_unsubscribe() {
934 let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"unsubscribe"}"#;
935 let expected = IncomingMessage::Command(CommandMsg::Unsubscribe {
936 req_id: None,
937 ret_msg: None,
938 conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
939 success: true,
940 });
941
942 let message = deserialize_json(json).unwrap();
943
944 assert_eq!(expected, message);
945 }
946
947 #[test]
948 fn deserialize_incoming_message_ticker_delta() {
949 let json = r#"{
950 "topic": "tickers.BTCUSDT",
951 "type": "delta",
952 "data": {
953 "symbol": "BTCUSDT",
954 "tickDirection": "PlusTick",
955 "price24hPcnt": "-0.015895",
956 "lastPrice": "63948.50",
957 "turnover24h": "6793884423.5518",
958 "volume24h": "105991.3760",
959 "bid1Price": "63948.40",
960 "bid1Size": "3.439",
961 "ask1Price": "63948.50",
962 "ask1Size": "2.566"
963 },
964 "cs": 195377749067,
965 "ts": 1718995014034
966 }"#;
967 let ticker_delta = TickerMsg::Delta {
968 topic: Topic::Ticker(String::from("BTCUSDT")),
969 cs: Some(195377749067),
970 ts: 1718995014034,
971 data: TickerDeltaMsg {
972 symbol: String::from("BTCUSDT"),
973 tick_direction: Some(TickDirection::PlusTick),
974 last_price: Some(dec!(63948.5)),
975 pre_open_price: None,
976 pre_qty: None,
977 cur_pre_listing_phase: None,
978 prev_price24h: None,
979 price24h_pcnt: Some(dec!(-0.015895)),
980 high_price24h: None,
981 low_price24h: None,
982 prev_price1h: None,
983 mark_price: None,
984 index_price: None,
985 open_interest: None,
986 open_interest_value: None,
987 turnover24h: Some(dec!(6793884423.5518)),
988 volume24h: Some(dec!(105991.376)),
989 funding_rate: None,
990 next_funding_time: None,
991 bid1_price: Some(dec!(63948.4)),
992 bid1_size: Some(dec!(3.439)),
993 ask1_price: Some(dec!(63948.5)),
994 ask1_size: Some(dec!(2.566)),
995 delivery_time: None,
996 basis_rate: None,
997 delivery_fee_rate: None,
998 predicted_delivery_price: None,
999 },
1000 };
1001 let expected = IncomingMessage::Ticker(Box::new(ticker_delta));
1002
1003 let message = deserialize_json(json).unwrap();
1004
1005 assert_eq!(expected, message);
1006 }
1007
1008 #[test]
1009 fn deserialize_incoming_message_ticker_snapshot() {
1010 let json = r#"{
1012 "topic": "tickers.BTCUSDT",
1013 "type": "snapshot",
1014 "data": {
1015 "symbol":"BTCUSDT",
1016 "tickDirection":"ZeroPlusTick",
1017 "price24hPcnt":"-0.044555",
1018 "lastPrice":"84594.40",
1019 "prevPrice24h":"88539.30",
1020 "highPrice24h":"89389.90",
1021 "lowPrice24h":"82055.60",
1022 "prevPrice1h":"84307.20",
1023 "markPrice":"84594.00",
1024 "indexPrice":"84650.47",
1025 "openInterest":"52903.75",
1026 "openInterestValue":"4475339827.50",
1027 "turnover24h":"17166562011.6514",
1028 "volume24h":"200176.9910",
1029 "nextFundingTime":"1740643200000",
1030 "fundingRate":"-0.00016974",
1031 "bid1Price":"84594.30",
1032 "bid1Size":"6.777",
1033 "ask1Price":"84594.40",
1034 "ask1Size":"0.660",
1035 "preOpenPrice":"",
1036 "preQty":"",
1037 "curPreListingPhase":""
1038 },
1039 "cs": 337149693308,
1040 "ts": 1740622194359
1041 }"#;
1042 let ticker_snapshot = TickerMsg::Snapshot {
1043 topic: Topic::Ticker(String::from("BTCUSDT")),
1044 cs: Some(337149693308),
1045 ts: 1740622194359,
1046 data: TickerSnapshotMsg {
1047 symbol: String::from("BTCUSDT"),
1048 tick_direction: TickDirection::ZeroPlusTick,
1049 last_price: dec!(84594.40),
1050 pre_open_price: None,
1051 pre_qty: None,
1052 cur_pre_listing_phase: None,
1053 prev_price24h: dec!(88539.30),
1054 price24h_pcnt: dec!(-0.044555),
1055 high_price24h: dec!(89389.90),
1056 low_price24h: dec!(82055.60),
1057 prev_price1h: dec!(84307.20),
1058 mark_price: dec!(84594.00),
1059 index_price: dec!(84650.47),
1060 open_interest: dec!(52903.75),
1061 open_interest_value: dec!(4475339827.50),
1062 turnover24h: dec!(17166562011.6514),
1063 volume24h: dec!(200176.9910),
1064 funding_rate: dec!(-0.00016974),
1065 next_funding_time: 1740643200000,
1066 bid1_price: dec!(84594.30),
1067 bid1_size: dec!(6.777),
1068 ask1_price: dec!(84594.40),
1069 ask1_size: dec!(0.660),
1070 delivery_time: None,
1071 basis_rate: None,
1072 delivery_fee_rate: None,
1073 predicted_delivery_price: None,
1074 },
1075 };
1076 let expected = IncomingMessage::Ticker(Box::new(ticker_snapshot));
1077
1078 let message = deserialize_json(json).unwrap();
1079
1080 assert_eq!(expected, message);
1081 }
1082
1083 #[test]
1084 fn deserialize_incoming_message_trade_snapshot() {
1085 let json = r#"{
1087 "topic":"publicTrade.BTCUSDT",
1088 "type":"snapshot",
1089 "ts":1741433245359,
1090 "data":[
1091 {
1092 "T":1741433245357,
1093 "s":"BTCUSDT",
1094 "S":"Buy",
1095 "v":"0.007",
1096 "p":"85821.00",
1097 "L":"PlusTick",
1098 "i":"485eaa70-df6e-5260-bbef-4f7324e3c5d9",
1099 "BT":false
1100 }
1101 ]
1102 }"#;
1103 let expected = IncomingMessage::Trade(TradeMsg::Snapshot {
1104 id: None,
1105 topic: Topic::Trade(String::from("BTCUSDT")),
1106 ts: 1741433245359,
1107 data: vec![TradeSnapshotMsg {
1108 time: 1741433245357,
1109 symbol: String::from("BTCUSDT"),
1110 side: Side::Buy,
1111 size: dec!(0.007),
1112 price: dec!(85821.00),
1113 tick_direction: TickDirection::PlusTick,
1114 trade_id: String::from("485eaa70-df6e-5260-bbef-4f7324e3c5d9"),
1115 block_trade: false,
1116 rpi_trade: None,
1117 mark_price: None,
1118 index_price: None,
1119 mark_iv: None,
1120 iv: None,
1121 }],
1122 });
1123
1124 let message = deserialize_json(json).unwrap();
1125
1126 assert_eq!(expected, message);
1127 }
1128
1129 #[test]
1130 fn deserialize_incoming_message_orderbook_snapshot() {
1131 let json = r#"{
1133 "topic":"orderbook.50.BTCUSDT",
1134 "type":"snapshot",
1135 "ts":1672304484978,
1136 "data":{
1137 "s":"BTCUSDT",
1138 "b":[
1139 ["16493.50","0.006"],
1140 ["16493.00","0.100"]
1141 ],
1142 "a":[
1143 ["16611.00","0.029"],
1144 ["16612.00","0.213"]
1145 ],
1146 "u":18521288,
1147 "seq":7961638724
1148 },
1149 "cts":1672304484976
1150 }"#;
1151 let expected = IncomingMessage::Orderbook(OrderbookMsg::Snapshot {
1152 topic: Topic::Orderbook {
1153 symbol: String::from("BTCUSDT"),
1154 depth: DepthLevel::Level50,
1155 },
1156 ts: 1672304484978,
1157 data: OrderbookDataMsg {
1158 symbol: String::from("BTCUSDT"),
1159 bids: vec![
1160 OrderbookLevel {
1161 price: dec!(16493.50),
1162 size: dec!(0.006),
1163 },
1164 OrderbookLevel {
1165 price: dec!(16493.00),
1166 size: dec!(0.100),
1167 },
1168 ],
1169 asks: vec![
1170 OrderbookLevel {
1171 price: dec!(16611.00),
1172 size: dec!(0.029),
1173 },
1174 OrderbookLevel {
1175 price: dec!(16612.00),
1176 size: dec!(0.213),
1177 },
1178 ],
1179 update_id: 18521288,
1180 seq: 7961638724,
1181 },
1182 cts: 1672304484976,
1183 });
1184
1185 let message = deserialize_json(json).unwrap();
1186
1187 assert_eq!(expected, message);
1188 }
1189
1190 #[test]
1191 fn deserialize_incoming_message_orderbook_delta() {
1192 let json = r#"{
1194 "topic":"orderbook.50.BTCUSDT",
1195 "type":"delta",
1196 "ts":1687940967466,
1197 "data":{
1198 "s":"BTCUSDT",
1199 "b":[
1200 ["30247.20","30.028"],
1201 ["30245.40","0.224"],
1202 ["30242.10","0.000"]
1203 ],
1204 "a":[
1205 ["30248.67","0.033"],
1206 ["30249.20","0.000"]
1207 ],
1208 "u":177400507,
1209 "seq":66544703342
1210 },
1211 "cts":1687940967464
1212 }"#;
1213 let expected = IncomingMessage::Orderbook(OrderbookMsg::Delta {
1214 topic: Topic::Orderbook {
1215 symbol: String::from("BTCUSDT"),
1216 depth: DepthLevel::Level50,
1217 },
1218 ts: 1687940967466,
1219 data: OrderbookDataMsg {
1220 symbol: String::from("BTCUSDT"),
1221 bids: vec![
1222 OrderbookLevel {
1223 price: dec!(30247.20),
1224 size: dec!(30.028),
1225 },
1226 OrderbookLevel {
1227 price: dec!(30245.40),
1228 size: dec!(0.224),
1229 },
1230 OrderbookLevel {
1231 price: dec!(30242.10),
1232 size: dec!(0.000),
1233 },
1234 ],
1235 asks: vec![
1236 OrderbookLevel {
1237 price: dec!(30248.67),
1238 size: dec!(0.033),
1239 },
1240 OrderbookLevel {
1241 price: dec!(30249.20),
1242 size: dec!(0.000),
1243 },
1244 ],
1245 update_id: 177400507,
1246 seq: 66544703342,
1247 },
1248 cts: 1687940967464,
1249 });
1250
1251 let message = deserialize_json(json).unwrap();
1252
1253 assert_eq!(expected, message);
1254 }
1255
1256 #[test]
1257 fn deserialize_incoming_message_all_liquidation_snapshot() {
1258 let json = r#"{
1260 "topic":"allLiquidation.BTCUSDT",
1261 "type":"snapshot",
1262 "ts":1741450605553,
1263 "data":[
1264 {
1265 "T":1741450605236,
1266 "s":"BTCUSDT",
1267 "S":"Buy",
1268 "v":"0.001",
1269 "p":"85823.60"
1270 }
1271 ]
1272 }"#;
1273 let expected = AllLiquidationMsg::Snapshot {
1274 topic: Topic::AllLiquidation(String::from("BTCUSDT")),
1275 ts: 1741450605553,
1276 data: vec![AllLiquidationSnapshotMsg {
1277 time: 1741450605236,
1278 symbol: String::from("BTCUSDT"),
1279 side: Side::Buy,
1280 size: dec!(0.001),
1281 price: dec!(85823.60),
1282 }],
1283 };
1284
1285 let message = deserialize_json(json).unwrap();
1286
1287 assert_eq!(expected, message);
1288 }
1289
1290 #[test]
1291 fn deserialize_incoming_message_order() {
1292 let json = r#"{
1293 "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90",
1294 "topic": "order",
1295 "creationTime": 1672364262474,
1296 "data": [
1297 {
1298 "symbol": "ETH-30DEC22-1400-C",
1299 "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020",
1300 "side": "Sell",
1301 "orderType": "Market",
1302 "cancelType": "UNKNOWN",
1303 "price": "72.5",
1304 "qty": "1",
1305 "orderIv": "",
1306 "timeInForce": "IOC",
1307 "orderStatus": "Filled",
1308 "orderLinkId": "",
1309 "lastPriceOnCreated": "",
1310 "reduceOnly": false,
1311 "leavesQty": "",
1312 "leavesValue": "",
1313 "cumExecQty": "1",
1314 "cumExecValue": "75",
1315 "avgPrice": "75",
1316 "blockTradeId": "",
1317 "positionIdx": 0,
1318 "cumExecFee": "0.358635",
1319 "closedPnl": "0",
1320 "createdTime": "1672364262444",
1321 "updatedTime": "1672364262457",
1322 "rejectReason": "EC_NoError",
1323 "stopOrderType": "",
1324 "tpslMode": "",
1325 "triggerPrice": "",
1326 "takeProfit": "",
1327 "stopLoss": "",
1328 "tpTriggerBy": "",
1329 "slTriggerBy": "",
1330 "tpLimitPrice": "",
1331 "slLimitPrice": "",
1332 "triggerDirection": 0,
1333 "triggerBy": "",
1334 "closeOnTrigger": false,
1335 "category": "option",
1336 "placeType": "price",
1337 "smpType": "None",
1338 "smpGroup": 0,
1339 "smpOrderId": "",
1340 "feeCurrency": "",
1341 "cumFeeDetail": {
1342 "MNT": "0.00242968"
1343 }
1344 }
1345 ]
1346 }"#;
1347 let order = PrivateMsg {
1348 id: String::from("5923240c6880ab-c59f-420b-9adb-3639adc9dd90"),
1349 creation_time: 1672364262474,
1350 data: vec![OrderMsg {
1351 category: Category::Option,
1352 order_id: String::from("5cf98598-39a7-459e-97bf-76ca765ee020"),
1353 order_link_id: None,
1354 is_leverage: None,
1355 block_trade_id: None,
1356 symbol: String::from("ETH-30DEC22-1400-C"),
1357 price: dec!(72.5),
1358 broker_order_price: None,
1359 qty: dec!(1.0),
1360 side: Side::Sell,
1361 position_idx: PositionIdx::OneWay,
1362 order_status: OrderStatus::Filled,
1363 create_type: None,
1364 cancel_type: CancelType::UNKNOWN,
1365 reject_reason: RejectReason::EcNoError,
1366 avg_price: Some(dec!(75.0)),
1367 leaves_qty: None,
1368 leaves_value: None,
1369 cum_exec_qty: dec!(1.0),
1370 cum_exec_value: dec!(75.0),
1371 cum_exec_fee: dec!(0.358635),
1372 closed_pnl: dec!(0.0),
1373 fee_currency: None,
1374 time_in_force: TimeInForce::IOC,
1375 order_type: OrderType::Market,
1376 stop_order_type: None,
1377 oco_trigger_by: None,
1378 order_iv: None,
1379 market_unit: None,
1380 slippage_tolerance_type: None,
1381 slippage_tolerance: None,
1382 trigger_price: None,
1383 take_profit: None,
1384 stop_loss: None,
1385 tpsl_mode: None,
1386 tp_limit_price: None,
1387 sl_limit_price: None,
1388 tp_trigger_by: None,
1389 sl_trigger_by: None,
1390 trigger_direction: TriggerDirection::UNKNOWN,
1391 trigger_by: None,
1392 last_price_on_created: None,
1393 reduce_only: false,
1394 close_on_trigger: false,
1395 place_type: Some(PlaceType::Price),
1396 smp_type: SmpType::None,
1397 smp_group: 0,
1398 smp_order_id: None,
1399 created_time: 1672364262444,
1400 updated_time: 1672364262457,
1401 cum_fee_detail: Some(serde_json::from_str(r#"{"MNT": "0.00242968"}"#).unwrap()),
1402 }],
1403 };
1404 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1405
1406 let message = deserialize_json(json).unwrap();
1407
1408 assert_eq!(expected, message);
1409 }
1410
1411 #[test]
1412 fn deserialize_incoming_message_order2() {
1413 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":{}}]}"#;
1414 let order = PrivateMsg {
1415 id: String::from("108985347_ADAUSDT_140667095077548"),
1416 creation_time: 1766436947942,
1417 data: vec![OrderMsg {
1418 category: Category::Linear,
1419 order_id: String::from("ae802ad5-af70-4957-ba72-86ad7fc9c24d"),
1420 order_link_id: None,
1421 is_leverage: None,
1422 block_trade_id: None,
1423 symbol: String::from("ADAUSDT"),
1424 price: dec!(0.3862),
1425 broker_order_price: None,
1426 qty: dec!(15),
1427 side: Side::Buy,
1428 position_idx: PositionIdx::OneWay,
1429 order_status: OrderStatus::Filled,
1430 create_type: Some(CreateType::CreateByUser),
1431 cancel_type: CancelType::UNKNOWN,
1432 reject_reason: RejectReason::EcNoError,
1433 avg_price: Some(dec!(0.3679)),
1434 leaves_qty: Some(dec!(0)),
1435 leaves_value: Some(dec!(0)),
1436 cum_exec_qty: dec!(15),
1437 cum_exec_value: dec!(5.5185),
1438 cum_exec_fee: dec!(0.00303518),
1439 closed_pnl: dec!(0),
1440 fee_currency: None,
1441 time_in_force: TimeInForce::IOC,
1442 order_type: OrderType::Market,
1443 stop_order_type: None,
1444 oco_trigger_by: None,
1445 order_iv: None,
1446 market_unit: None,
1447 slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1448 slippage_tolerance: Some(dec!(0)),
1449 trigger_price: None,
1450 take_profit: None,
1451 stop_loss: None,
1452 tpsl_mode: Some(TpslMode::UNKNOWN),
1453 tp_limit_price: Some(dec!(0)),
1454 sl_limit_price: Some(dec!(0)),
1455 tp_trigger_by: None,
1456 sl_trigger_by: None,
1457 trigger_direction: TriggerDirection::UNKNOWN,
1458 trigger_by: None,
1459 last_price_on_created: Some(dec!(0.3679)),
1460 reduce_only: false,
1461 close_on_trigger: false,
1462 place_type: None,
1463 smp_type: SmpType::None,
1464 smp_group: 0,
1465 smp_order_id: None,
1466 created_time: 1766436947940,
1467 updated_time: 1766436947940,
1468 cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1469 }],
1470 };
1471 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1472
1473 let message = deserialize_json(json).unwrap();
1474
1475 assert_eq!(expected, message);
1476 }
1477
1478 #[test]
1479 fn deserialize_incoming_message_order3() {
1480 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":{}}]}"#;
1481 let order = PrivateMsg {
1482 id: String::from("108985347_ADAUSDT_140667102632416"),
1483 creation_time: 1766600379878,
1484 data: vec![OrderMsg {
1485 category: Category::Linear,
1486 order_id: String::from("f0468cbc-ed2f-4fd7-9620-998f3e9f387c"),
1487 order_link_id: Some(String::from("BOT_LINK_ID-1")),
1488 is_leverage: None,
1489 block_trade_id: None,
1490 symbol: String::from("ADAUSDT"),
1491 price: dec!(0.3539),
1492 broker_order_price: None,
1493 qty: dec!(15),
1494 side: Side::Buy,
1495 position_idx: PositionIdx::OneWay,
1496 order_status: OrderStatus::New,
1497 create_type: Some(CreateType::CreateByUser),
1498 cancel_type: CancelType::UNKNOWN,
1499 reject_reason: RejectReason::EcNoError,
1500 avg_price: None,
1501 leaves_qty: Some(dec!(15)),
1502 leaves_value: Some(dec!(5.3085)),
1503 cum_exec_qty: dec!(0),
1504 cum_exec_value: dec!(0),
1505 cum_exec_fee: dec!(0),
1506 closed_pnl: dec!(0),
1507 fee_currency: None,
1508 time_in_force: TimeInForce::GTC,
1509 order_type: OrderType::Limit,
1510 stop_order_type: None,
1511 oco_trigger_by: None,
1512 order_iv: None,
1513 market_unit: None,
1514 slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1515 slippage_tolerance: Some(dec!(0)),
1516 trigger_price: None,
1517 take_profit: None,
1518 stop_loss: None,
1519 tpsl_mode: Some(TpslMode::UNKNOWN),
1520 tp_limit_price: Some(dec!(0)),
1521 sl_limit_price: Some(dec!(0)),
1522 tp_trigger_by: None,
1523 sl_trigger_by: None,
1524 trigger_direction: TriggerDirection::UNKNOWN,
1525 trigger_by: None,
1526 last_price_on_created: Some(dec!(0.355)),
1527 reduce_only: false,
1528 close_on_trigger: false,
1529 place_type: None, smp_type: SmpType::None,
1531 smp_group: 0,
1532 smp_order_id: None,
1533 created_time: 1766600379876,
1534 updated_time: 1766600379876,
1535 cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1536 }],
1537 };
1538 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1539
1540 let message = deserialize_json(json).unwrap();
1541
1542 assert_eq!(expected, message);
1543 }
1544
1545 #[test]
1546 fn deserialize_incoming_message_position() {
1547 let json = r#"{
1548 "id": "108985347_position_1765659601915",
1549 "topic": "position",
1550 "creationTime": 1765659601915,
1551 "data": [
1552 {
1553 "positionIdx": 1,
1554 "tradeMode": 0,
1555 "riskId": 116,
1556 "riskLimitValue": "200000",
1557 "symbol": "ADAUSDT",
1558 "side": "Buy",
1559 "size": "18720",
1560 "entryPrice": "0.41160027",
1561 "sessionAvgPrice": "",
1562 "leverage": "75",
1563 "positionValue": "7705.157",
1564 "positionBalance": "0",
1565 "markPrice": "0.41",
1566 "positionIM": "106.51735757",
1567 "positionMM": "61.74535757",
1568 "positionIMByMp": "106.51735757",
1569 "positionMMByMp": "61.74535757",
1570 "takeProfit": "0.4321",
1571 "stopLoss": "0.3704",
1572 "trailingStop": "0",
1573 "unrealisedPnl": "-29.957",
1574 "cumRealisedPnl": "-6712.87804378",
1575 "curRealisedPnl": "-2.6317147",
1576 "createdTime": "1714594321840",
1577 "updatedTime": "1765645142548",
1578 "tpslMode": "Full",
1579 "liqPrice": "0.37000066",
1580 "bustPrice": "",
1581 "category": "linear",
1582 "positionStatus": "Normal",
1583 "adlRankIndicator": 2,
1584 "autoAddMargin": 0,
1585 "leverageSysUpdatedTime": "",
1586 "mmrSysUpdatedTime": "",
1587 "seq": 140667058318085,
1588 "isReduceOnly": false
1589 },
1590 {
1591 "positionIdx": 2,
1592 "tradeMode": 0,
1593 "riskId": 116,
1594 "riskLimitValue": "200000",
1595 "symbol": "ADAUSDT",
1596 "side": "",
1597 "size": "0",
1598 "entryPrice": "0",
1599 "sessionAvgPrice": "",
1600 "leverage": "75",
1601 "positionValue": "0",
1602 "positionBalance": "0",
1603 "markPrice": "0.41",
1604 "positionIM": "",
1605 "positionMM": "",
1606 "positionIMByMp": "",
1607 "positionMMByMp": "",
1608 "takeProfit": "0",
1609 "stopLoss": "0",
1610 "trailingStop": "0",
1611 "unrealisedPnl": "0",
1612 "cumRealisedPnl": "1618.30675974",
1613 "curRealisedPnl": "0",
1614 "createdTime": "1714594321840",
1615 "updatedTime": "1765046350698",
1616 "tpslMode": "Full",
1617 "liqPrice": "0",
1618 "bustPrice": "",
1619 "category": "linear",
1620 "positionStatus": "Normal",
1621 "adlRankIndicator": 0,
1622 "autoAddMargin": 0,
1623 "leverageSysUpdatedTime": "",
1624 "mmrSysUpdatedTime": "",
1625 "seq": 140667031311361,
1626 "isReduceOnly": false
1627 }
1628 ]
1629 }"#;
1630 let position = PrivateMsg {
1631 id: String::from("108985347_position_1765659601915"),
1632 creation_time: 1765659601915,
1633 data: vec![
1634 PositionMsg {
1635 category: Category::Linear,
1636 symbol: String::from("ADAUSDT"),
1637 side: Some(Side::Buy),
1638 size: dec!(18720),
1639 position_idx: PositionIdx::Buy,
1640 position_value: dec!(7705.157),
1641 risk_id: 116,
1642 risk_limit_value: Some(dec!(200000)),
1643 entry_price: dec!(0.41160027),
1644 mark_price: dec!(0.41),
1645 leverage: dec!(75),
1646 auto_add_margin: false,
1647 position_im: Some(dec!(106.51735757)),
1648 position_mm: Some(dec!(61.74535757)),
1649 position_im_by_mp: Some(dec!(106.51735757)),
1650 position_mm_by_mp: Some(dec!(61.74535757)),
1651 liq_price: Some(dec!(0.37000066)),
1652 take_profit: dec!(0.4321),
1653 stop_loss: dec!(0.3704),
1654 trailing_stop: dec!(0),
1655 unrealised_pnl: dec!(-29.957),
1656 cur_realised_pnl: dec!(-2.6317147),
1657 session_avg_price: None,
1658 delta: None,
1659 gamma: None,
1660 vega: None,
1661 theta: None,
1662 cum_realised_pnl: dec!(-6712.87804378),
1663 position_status: PositionStatus::Normal,
1664 adl_rank_indicator: AdlRankIndicator::Two,
1665 is_reduce_only: false,
1666 mmr_sys_updated_time: None,
1667 leverage_sys_updated_time: None,
1668 created_time: 1714594321840,
1669 updated_time: 1765645142548,
1670 seq: 140667058318085,
1671 },
1672 PositionMsg {
1673 category: Category::Linear,
1674 symbol: String::from("ADAUSDT"),
1675 side: None,
1676 size: dec!(0),
1677 position_idx: PositionIdx::Sell,
1678 position_value: dec!(0),
1679 risk_id: 116,
1680 risk_limit_value: Some(dec!(200000)),
1681 entry_price: dec!(0),
1682 mark_price: dec!(0.41),
1683 leverage: dec!(75),
1684 auto_add_margin: false,
1685 position_im: None,
1686 position_mm: None,
1687 position_im_by_mp: None,
1688 position_mm_by_mp: None,
1689 liq_price: Some(dec!(0)),
1690 take_profit: dec!(0),
1691 stop_loss: dec!(0),
1692 trailing_stop: dec!(0),
1693 unrealised_pnl: dec!(0),
1694 cur_realised_pnl: dec!(0),
1695 session_avg_price: None,
1696 delta: None,
1697 gamma: None,
1698 vega: None,
1699 theta: None,
1700 cum_realised_pnl: dec!(1618.30675974),
1701 position_status: PositionStatus::Normal,
1702 adl_rank_indicator: AdlRankIndicator::Zero,
1703 is_reduce_only: false,
1704 mmr_sys_updated_time: None,
1705 leverage_sys_updated_time: None,
1706 created_time: 1714594321840,
1707 updated_time: 1765046350698,
1708 seq: 140667031311361,
1709 },
1710 ],
1711 };
1712 let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1713
1714 let message = deserialize_json(json).unwrap();
1715
1716 assert_eq!(expected, message);
1717 }
1718
1719 #[test]
1720 fn deserialize_incoming_message_position2() {
1721 let json = r#"{
1722 "id":"108985347_position_1766316605952",
1723 "topic":"position",
1724 "creationTime":1766316605952,
1725 "data":[
1726 {
1727 "positionIdx":1,
1728 "tradeMode":0,
1729 "riskId":116,
1730 "riskLimitValue":"200000",
1731 "symbol":"ADAUSDT",
1732 "side":"Buy",
1733 "size":"43",
1734 "entryPrice":"0.37293023",
1735 "sessionAvgPrice":"",
1736 "leverage":"75",
1737 "positionValue":"16.036",
1738 "positionBalance":"0",
1739 "markPrice":"0.3702",
1740 "positionIM":"0.22095025",
1741 "positionMM":"0.12809175",
1742 "positionIMByMp":"0.22095025",
1743 "positionMMByMp":"0.12809175",
1744 "takeProfit":"0",
1745 "stopLoss":"0",
1746 "trailingStop":"0",
1747 "unrealisedPnl":"-0.1174",
1748 "cumRealisedPnl":"-7547.8530836",
1749 "curRealisedPnl":"-0.00465061",
1750 "createdTime":"1714594321840",
1751 "updatedTime":"1766313370061",
1752 "tpslMode":"Full",
1753 "liqPrice":"",
1754 "bustPrice":"",
1755 "category":"linear",
1756 "positionStatus":"Normal",
1757 "adlRankIndicator":2,
1758 "autoAddMargin":0,
1759 "leverageSysUpdatedTime":"",
1760 "mmrSysUpdatedTime":"",
1761 "seq":140667089523042,
1762 "isReduceOnly":false
1763 },
1764 {
1765 "positionIdx":2,
1766 "tradeMode":0,
1767 "riskId":116,
1768 "riskLimitValue":"200000",
1769 "symbol":"ADAUSDT",
1770 "side":"",
1771 "size":"0",
1772 "entryPrice":"0",
1773 "sessionAvgPrice":"",
1774 "leverage":"75",
1775 "positionValue":"0",
1776 "positionBalance":"0",
1777 "markPrice":"0.3702",
1778 "positionIM":"",
1779 "positionMM":"",
1780 "positionIMByMp":"",
1781 "positionMMByMp":"",
1782 "takeProfit":"0",
1783 "stopLoss":"0",
1784 "trailingStop":"0",
1785 "unrealisedPnl":"0",
1786 "cumRealisedPnl":"1618.30675974",
1787 "curRealisedPnl":"0",
1788 "createdTime":"1714594321840",
1789 "updatedTime":"1765046350698",
1790 "tpslMode":"Full",
1791 "liqPrice":"0",
1792 "bustPrice":"",
1793 "category":"linear",
1794 "positionStatus":"Normal",
1795 "adlRankIndicator":0,
1796 "autoAddMargin":0,
1797 "leverageSysUpdatedTime":"",
1798 "mmrSysUpdatedTime":"",
1799 "seq":140667031311361,
1800 "isReduceOnly":false
1801 }
1802 ]
1803 }"#;
1804 let position = PrivateMsg {
1805 id: String::from("108985347_position_1766316605952"),
1806 creation_time: 1766316605952,
1807 data: vec![
1808 PositionMsg {
1809 category: Category::Linear,
1810 symbol: String::from("ADAUSDT"),
1811 side: Some(Side::Buy),
1812 size: dec!(43),
1813 position_idx: PositionIdx::Buy,
1814 position_value: dec!(16.036),
1815 risk_id: 116,
1816 risk_limit_value: Some(dec!(200000)),
1817 entry_price: dec!(0.37293023),
1818 mark_price: dec!(0.3702),
1819 leverage: dec!(75),
1820 auto_add_margin: false,
1821 position_im: Some(dec!(0.22095025)),
1822 position_mm: Some(dec!(0.12809175)),
1823 position_im_by_mp: Some(dec!(0.22095025)),
1824 position_mm_by_mp: Some(dec!(0.12809175)),
1825 liq_price: None,
1826 take_profit: dec!(0),
1827 stop_loss: dec!(0),
1828 trailing_stop: dec!(0),
1829 unrealised_pnl: dec!(-0.1174),
1830 cur_realised_pnl: dec!(-0.00465061),
1831 session_avg_price: None,
1832 delta: None,
1833 gamma: None,
1834 vega: None,
1835 theta: None,
1836 cum_realised_pnl: dec!(-7547.8530836),
1837 position_status: PositionStatus::Normal,
1838 adl_rank_indicator: AdlRankIndicator::Two,
1839 is_reduce_only: false,
1840 mmr_sys_updated_time: None,
1841 leverage_sys_updated_time: None,
1842 created_time: 1714594321840,
1843 updated_time: 1766313370061,
1844 seq: 140667089523042,
1845 },
1846 PositionMsg {
1847 category: Category::Linear,
1848 symbol: String::from("ADAUSDT"),
1849 side: None,
1850 size: dec!(0),
1851 position_idx: PositionIdx::Sell,
1852 position_value: dec!(0),
1853 risk_id: 116,
1854 risk_limit_value: Some(dec!(200000)),
1855 entry_price: dec!(0),
1856 mark_price: dec!(0.3702),
1857 leverage: dec!(75),
1858 auto_add_margin: false,
1859 position_im: None,
1860 position_mm: None,
1861 position_im_by_mp: None,
1862 position_mm_by_mp: None,
1863 liq_price: Some(dec!(0)),
1864 take_profit: dec!(0),
1865 stop_loss: dec!(0),
1866 trailing_stop: dec!(0),
1867 unrealised_pnl: dec!(0),
1868 cur_realised_pnl: dec!(0),
1869 session_avg_price: None,
1870 delta: None,
1871 gamma: None,
1872 vega: None,
1873 theta: None,
1874 cum_realised_pnl: dec!(1618.30675974),
1875 position_status: PositionStatus::Normal,
1876 adl_rank_indicator: AdlRankIndicator::Zero,
1877 is_reduce_only: false,
1878 mmr_sys_updated_time: None,
1879 leverage_sys_updated_time: None,
1880 created_time: 1714594321840,
1881 updated_time: 1765046350698,
1882 seq: 140667031311361,
1883 },
1884 ],
1885 };
1886 let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1887
1888 let message = deserialize_json(json).unwrap();
1889
1890 assert_eq!(expected, message);
1891 }
1892
1893 #[test]
1894 fn deserialize_incoming_message_wallet() {
1895 let json = r#"{
1896 "id": "592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e",
1897 "topic": "wallet",
1898 "creationTime": 1700034722104,
1899 "data": [
1900 {
1901 "accountIMRate": "0",
1902 "accountIMRateByMp": "0",
1903 "accountMMRate": "0",
1904 "accountMMRateByMp": "0",
1905 "totalEquity": "10262.91335023",
1906 "totalWalletBalance": "9684.46297164",
1907 "totalMarginBalance": "9684.46297164",
1908 "totalAvailableBalance": "9556.6056555",
1909 "totalPerpUPL": "0",
1910 "totalInitialMargin": "0",
1911 "totalInitialMarginByMp": "0",
1912 "totalMaintenanceMargin": "0",
1913 "totalMaintenanceMarginByMp": "0",
1914 "coin": [
1915 {
1916 "coin": "BTC",
1917 "equity": "0.00102964",
1918 "usdValue": "36.70759517",
1919 "walletBalance": "0.00102964",
1920 "availableToWithdraw": "0.00102964",
1921 "availableToBorrow": "",
1922 "borrowAmount": "0",
1923 "accruedInterest": "0",
1924 "totalOrderIM": "",
1925 "totalPositionIM": "",
1926 "totalPositionMM": "",
1927 "unrealisedPnl": "0",
1928 "cumRealisedPnl": "-0.00000973",
1929 "bonus": "0",
1930 "collateralSwitch": true,
1931 "marginCollateral": true,
1932 "locked": "0",
1933 "spotHedgingQty": "0.01592413",
1934 "spotBorrow": "0"
1935 }
1936 ],
1937 "accountLTV": "0",
1938 "accountType": "UNIFIED"
1939 }
1940 ]
1941 }"#;
1942 let coin = WalletCoin {
1943 coin: String::from("BTC"),
1944 equity: dec!(0.00102964),
1945 usd_value: dec!(36.70759517),
1946 wallet_balance: dec!(0.00102964),
1947 locked: dec!(0),
1948 spot_hedging_qty: dec!(0.01592413),
1949 borrow_amount: dec!(0),
1950 accrued_interest: dec!(0),
1951 total_order_im: None,
1952 total_position_im: None,
1953 total_position_mm: None,
1954 unrealised_pnl: dec!(0),
1955 cum_realised_pnl: dec!(-0.00000973),
1956 bonus: dec!(0),
1957 collateral_switch: true,
1958 margin_collateral: true,
1959 spot_borrow: Some(dec!(0)),
1960 };
1961 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
1962 let wallet = PrivateMsg {
1963 id: String::from("592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e"),
1964 creation_time: 1700034722104,
1965 data: vec![WalletMsg {
1966 account_type: AccountType::UNIFIED,
1967 account_im_rate: dec!(0),
1968 account_im_rate_by_mp: dec!(0),
1969 account_mm_rate: dec!(0),
1970 account_mm_rate_by_mp: dec!(0),
1971 total_equity: dec!(10262.91335023),
1972 total_wallet_balance: dec!(9684.46297164),
1973 total_margin_balance: dec!(9684.46297164),
1974 total_available_balance: dec!(9556.6056555),
1975 total_perp_upl: dec!(0),
1976 total_initial_margin: dec!(0),
1977 total_initial_margin_by_mp: dec!(0),
1978 total_maintenance_margin: dec!(0),
1979 total_maintenance_margin_by_mp: dec!(0),
1980 coin,
1981 }],
1982 };
1983 let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
1984
1985 let message = deserialize_json(json).unwrap();
1986
1987 assert_eq!(expected, message);
1988 }
1989
1990 #[test]
1991 fn deserialize_incoming_message_wallet2() {
1992 let json = r#"{
1993 "id":"108985347_wallet_1766318882965",
1994 "topic":"wallet",
1995 "creationTime":1766318882964,
1996 "data":[
1997 {
1998 "accountIMRate":"0.0007",
1999 "accountMMRate":"0.0004",
2000 "accountIMRateByMp":"0.0007",
2001 "accountMMRateByMp":"0.0004",
2002 "totalEquity":"102.7094181",
2003 "totalWalletBalance":"102.16591975",
2004 "totalMarginBalance":"102.16591975",
2005 "totalAvailableBalance":"102.09402758",
2006 "totalPerpUPL":"0",
2007 "totalInitialMargin":"0.07189217",
2008 "totalMaintenanceMargin":"0.04166941",
2009 "totalInitialMarginByMp":"0.07189217",
2010 "totalMaintenanceMarginByMp":"0.04166941",
2011 "coin":[
2012 {
2013 "coin":"USDT",
2014 "equity":"75.5601152",
2015 "usdValue":"75.53450032",
2016 "walletBalance":"75.5601152",
2017 "availableToWithdraw":"",
2018 "availableToBorrow":"",
2019 "borrowAmount":"0",
2020 "accruedInterest":"0",
2021 "totalOrderIM":"0",
2022 "totalPositionIM":"0.07191655",
2023 "totalPositionMM":"0.04168355",
2024 "unrealisedPnl":"0",
2025 "cumRealisedPnl":"36163.8134634",
2026 "bonus":"0",
2027 "collateralSwitch":true,
2028 "marginCollateral":true,
2029 "locked":"0",
2030 "spotHedgingQty":"0"
2031 }
2032 ],
2033 "accountLTV":"0",
2034 "accountType":"UNIFIED"
2035 }
2036 ]
2037 }"#;
2038 let coin = WalletCoin {
2039 coin: String::from("USDT"),
2040 equity: dec!(75.5601152),
2041 usd_value: dec!(75.53450032),
2042 wallet_balance: dec!(75.5601152),
2043 locked: dec!(0),
2044 spot_hedging_qty: dec!(0),
2045 borrow_amount: dec!(0),
2046 accrued_interest: dec!(0),
2047 total_order_im: Some(dec!(0)),
2048 total_position_im: Some(dec!(0.07191655)),
2049 total_position_mm: Some(dec!(0.04168355)),
2050 unrealised_pnl: dec!(0),
2051 cum_realised_pnl: dec!(36163.8134634),
2052 bonus: dec!(0),
2053 collateral_switch: true,
2054 margin_collateral: true,
2055 spot_borrow: None,
2056 };
2057 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
2058 let wallet = PrivateMsg {
2059 id: String::from("108985347_wallet_1766318882965"),
2060 creation_time: 1766318882964,
2061 data: vec![WalletMsg {
2062 account_type: AccountType::UNIFIED,
2063 account_im_rate: dec!(0.0007),
2064 account_im_rate_by_mp: dec!(0.0007),
2065 account_mm_rate: dec!(0.0004),
2066 account_mm_rate_by_mp: dec!(0.0004),
2067 total_equity: dec!(102.7094181),
2068 total_wallet_balance: dec!(102.16591975),
2069 total_margin_balance: dec!(102.16591975),
2070 total_available_balance: dec!(102.09402758),
2071 total_perp_upl: dec!(0),
2072 total_initial_margin: dec!(0.07189217),
2073 total_initial_margin_by_mp: dec!(0.07189217),
2074 total_maintenance_margin: dec!(0.04166941),
2075 total_maintenance_margin_by_mp: dec!(0.04166941),
2076 coin,
2077 }],
2078 };
2079 let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
2080
2081 let message = deserialize_json(json).unwrap();
2082
2083 assert_eq!(expected, message);
2084 }
2085
2086 #[test]
2087 fn deserialize_incoming_message_execution() {
2088 let json = r#"{
2089 "topic": "execution",
2090 "id": "386825804_BTCUSDT_140612148849382",
2091 "creationTime": 1746270400355,
2092 "data": [
2093 {
2094 "category": "linear",
2095 "symbol": "BTCUSDT",
2096 "closedSize": "0.5",
2097 "execFee": "26.3725275",
2098 "execId": "0ab1bdf7-4219-438b-b30a-32ec863018f7",
2099 "execPrice": "95900.1",
2100 "execQty": "0.5",
2101 "execType": "Trade",
2102 "execValue": "47950.05",
2103 "feeRate": "0.00055",
2104 "tradeIv": "",
2105 "markIv": "",
2106 "blockTradeId": "",
2107 "markPrice": "95901.48",
2108 "indexPrice": "",
2109 "underlyingPrice": "",
2110 "leavesQty": "0",
2111 "orderId": "9aac161b-8ed6-450d-9cab-c5cc67c21784",
2112 "orderLinkId": "",
2113 "orderPrice": "94942.5",
2114 "orderQty": "0.5",
2115 "orderType": "Market",
2116 "stopOrderType": "UNKNOWN",
2117 "side": "Sell",
2118 "execTime": "1746270400353",
2119 "isLeverage": "0",
2120 "isMaker": false,
2121 "seq": 140612148849382,
2122 "marketUnit": "",
2123 "execPnl": "0.05",
2124 "createType": "CreateByUser",
2125 "extraFees":[{"feeCoin":"USDT","feeType":"GST","subFeeType":"IND_GST","feeRate":"0.0000675","fee":"0.006403779"}],
2126 "feeCurrency": "USDT"
2127 }
2128 ]
2129 }"#;
2130 let execution = PrivateMsg {
2131 id: String::from("386825804_BTCUSDT_140612148849382"),
2132 creation_time: 1746270400355,
2133 data: vec![ExecutionMsg {
2134 category: Category::Linear,
2135 symbol: String::from("BTCUSDT"),
2136 is_leverage: false,
2137 order_id: String::from("9aac161b-8ed6-450d-9cab-c5cc67c21784"),
2138 order_link_id: None,
2139 side: Side::Sell,
2140 order_price: dec!(94942.5),
2141 order_qty: dec!(0.5),
2142 leaves_qty: dec!(0),
2143 create_type: CreateType::CreateByUser,
2144 order_type: OrderType::Market,
2145 stop_order_type: StopOrderType::UNKNOWN,
2146 exec_fee: dec!(26.3725275),
2147 exec_id: String::from("0ab1bdf7-4219-438b-b30a-32ec863018f7"),
2148 exec_price: dec!(95900.1),
2149 exec_qty: dec!(0.5),
2150 exec_pnl: dec!(0.05),
2151 exec_type: ExecType::Trade,
2152 exec_value: dec!(47950.05),
2153 exec_time: 1746270400353,
2154 is_maker: false,
2155 fee_rate: dec!(0.00055),
2156 trade_iv: None,
2157 mark_iv: None,
2158 mark_price: dec!(95901.48),
2159 index_price: None,
2160 underlying_price: None,
2161 block_trade_id: None,
2162 closed_size: dec!(0.5),
2163 extra_fees: Some(vec![ExtraFee {
2164 fee_coin: String::from("USDT"),
2165 fee_type: ExtraFeeType::Gst,
2166 sub_fee_type: ExtraSubFeeType::IndGst,
2167 fee_rate: dec!(0.0000675),
2168 fee: dec!(0.006403779),
2169 }]),
2170 seq: 140612148849382,
2171 fee_currency: String::from("USDT"),
2172 }],
2173 };
2174 let expected = IncomingMessage::Topic(TopicMessage::Execution(execution));
2175
2176 let message = deserialize_json(json).unwrap();
2177
2178 assert_eq!(expected, message);
2179 }
2180}