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}
387
388#[derive(PartialEq, Deserialize, Debug)]
389#[serde(rename_all = "camelCase")]
390pub struct PublicMsg<T> {
391 #[serde(default, deserialize_with = "empty_string_as_none")]
392 id: Option<String>,
393 #[serde(default, deserialize_with = "option_number")]
394 cs: Option<u64>,
395 ts: Timestamp,
396 data: T,
397}
398
399#[derive(PartialEq, Deserialize, Debug)]
400#[serde(rename_all = "camelCase")]
401pub struct PrivateMsg<T> {
402 pub id: String,
405 pub creation_time: Timestamp,
407 pub data: T,
408}
409
410#[derive(PartialEq, Deserialize, Debug, Clone)]
411#[serde(rename_all = "camelCase")]
412pub struct OrderMsg {
413 pub category: Category,
417 pub order_id: String,
419 #[serde(default, deserialize_with = "empty_string_as_none")]
421 pub order_link_id: Option<String>,
422 #[serde(default, deserialize_with = "string_to_option_bool")]
426 pub is_leverage: Option<bool>,
427 #[serde(default, deserialize_with = "empty_string_as_none")]
429 pub block_trade_id: Option<String>,
430 pub symbol: String,
432 pub price: Decimal,
434 #[serde(default, deserialize_with = "option_decimal")]
436 pub broker_order_price: Option<Decimal>,
437 pub qty: Decimal,
439 pub side: Side,
441 pub position_idx: PositionIdx,
443 pub order_status: OrderStatus,
445 #[serde(default, deserialize_with = "empty_string_as_none")]
449 pub create_type: Option<CreateType>,
450 pub cancel_type: CancelType,
452 pub reject_reason: RejectReason,
454 #[serde(default, deserialize_with = "option_decimal")]
458 pub avg_price: Option<Decimal>,
459 #[serde(default, deserialize_with = "option_decimal")]
461 pub leaves_qty: Option<Decimal>,
462 #[serde(default, deserialize_with = "option_decimal")]
464 pub leaves_value: Option<Decimal>,
465 pub cum_exec_qty: Decimal,
467 pub cum_exec_value: Decimal,
469 pub cum_exec_fee: Decimal,
473 pub cum_fee_detail: Option<serde_json::Value>,
475 pub closed_pnl: Decimal,
477 #[serde(deserialize_with = "option_decimal")]
479 pub fee_currency: Option<Decimal>,
480 pub time_in_force: TimeInForce,
482 pub order_type: OrderType,
484 #[serde(default, deserialize_with = "empty_string_as_none")]
486 pub stop_order_type: Option<StopOrderType>,
487 #[serde(default, deserialize_with = "empty_string_as_none")]
489 pub oco_trigger_by: Option<OcoTriggerBy>,
490 #[serde(deserialize_with = "option_decimal")]
492 pub order_iv: Option<Decimal>,
493 #[serde(default, deserialize_with = "empty_string_as_none")]
495 pub market_unit: Option<String>,
496 #[serde(default, deserialize_with = "empty_string_as_none")]
498 pub slippage_tolerance_type: Option<SlippageToleranceType>,
499 #[serde(default, deserialize_with = "option_decimal")]
501 pub slippage_tolerance: Option<Decimal>,
502 #[serde(deserialize_with = "option_decimal")]
504 pub trigger_price: Option<Decimal>,
505 #[serde(deserialize_with = "option_decimal")]
507 pub take_profit: Option<Decimal>,
508 #[serde(deserialize_with = "option_decimal")]
510 pub stop_loss: Option<Decimal>,
511 #[serde(default, deserialize_with = "empty_string_as_none")]
513 pub tpsl_mode: Option<TpslMode>,
514 #[serde(deserialize_with = "option_decimal")]
516 pub tp_limit_price: Option<Decimal>,
517 #[serde(deserialize_with = "option_decimal")]
519 pub sl_limit_price: Option<Decimal>,
520 #[serde(default, deserialize_with = "empty_string_as_none")]
522 pub tp_trigger_by: Option<TriggerBy>,
523 #[serde(default, deserialize_with = "empty_string_as_none")]
525 pub sl_trigger_by: Option<TriggerBy>,
526 pub trigger_direction: TriggerDirection,
528 #[serde(default, deserialize_with = "empty_string_as_none")]
530 pub trigger_by: Option<TriggerBy>,
531 #[serde(deserialize_with = "option_decimal")]
533 pub last_price_on_created: Option<Decimal>,
534 pub reduce_only: bool,
536 pub close_on_trigger: bool,
538 #[serde(default, deserialize_with = "empty_string_as_none")]
540 pub place_type: Option<PlaceType>,
541 pub smp_type: SmpType,
543 #[serde(deserialize_with = "number")]
545 pub smp_group: i64,
546 #[serde(default, deserialize_with = "empty_string_as_none")]
548 pub smp_order_id: Option<String>,
549 #[serde(deserialize_with = "number")]
551 pub created_time: Timestamp,
552 #[serde(deserialize_with = "number")]
554 pub updated_time: Timestamp,
555}
556
557#[derive(PartialEq, Deserialize, Debug, Clone)]
558#[serde(rename_all = "camelCase")]
559pub struct PositionMsg {
560 pub category: Category,
562 pub symbol: String,
564 #[serde(default, deserialize_with = "empty_string_as_none")]
568 pub side: Option<Side>,
569 pub size: Decimal,
571 pub position_idx: PositionIdx,
573 pub position_value: Decimal,
575 #[serde(deserialize_with = "number")]
578 pub risk_id: i64,
579 #[serde(default, deserialize_with = "option_decimal")]
582 pub risk_limit_value: Option<Decimal>,
583 pub entry_price: Decimal,
585 pub mark_price: Decimal,
587 pub leverage: Decimal,
590 #[serde(default, deserialize_with = "int_to_bool")]
592 pub auto_add_margin: bool,
593 #[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
596 pub position_im: Option<Decimal>,
597 #[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
600 pub position_mm: Option<Decimal>,
601 #[serde(
604 rename = "positionIMByMp",
605 default,
606 deserialize_with = "option_decimal"
607 )]
608 pub position_im_by_mp: Option<Decimal>,
609 #[serde(
612 rename = "positionMMByMp",
613 default,
614 deserialize_with = "option_decimal"
615 )]
616 pub position_mm_by_mp: Option<Decimal>,
617 #[serde(default, deserialize_with = "option_decimal")]
624 pub liq_price: Option<Decimal>,
625 pub take_profit: Decimal,
627 pub stop_loss: Decimal,
629 pub trailing_stop: Decimal,
631 pub unrealised_pnl: Decimal,
633 pub cur_realised_pnl: Decimal,
635 #[serde(default, deserialize_with = "option_decimal")]
637 pub session_avg_price: Option<Decimal>,
638 #[serde(default, deserialize_with = "empty_string_as_none")]
640 pub delta: Option<String>,
641 #[serde(default, deserialize_with = "empty_string_as_none")]
643 pub gamma: Option<String>,
644 #[serde(default, deserialize_with = "empty_string_as_none")]
646 pub vega: Option<String>,
647 #[serde(default, deserialize_with = "empty_string_as_none")]
649 pub theta: Option<String>,
650 pub cum_realised_pnl: Decimal,
654 pub position_status: PositionStatus,
656 pub adl_rank_indicator: AdlRankIndicator,
658 pub is_reduce_only: bool,
663 #[serde(deserialize_with = "option_number")]
670 pub mmr_sys_updated_time: Option<Timestamp>,
671 #[serde(deserialize_with = "option_number")]
678 pub leverage_sys_updated_time: Option<Timestamp>,
679 #[serde(deserialize_with = "number")]
681 pub created_time: Timestamp,
682 #[serde(deserialize_with = "number")]
684 pub updated_time: Timestamp,
685 pub seq: i64,
690}
691
692#[derive(PartialEq, Deserialize, Debug, Clone)]
693#[serde(rename_all = "camelCase")]
694pub struct WalletMsg {
695 pub account_type: AccountType,
700 #[serde(rename = "accountIMRate")]
707 pub account_im_rate: Decimal,
708 #[serde(rename = "accountMMRate")]
710 pub account_mm_rate: Decimal,
711 pub total_equity: Decimal,
713 pub total_wallet_balance: Decimal,
715 pub total_margin_balance: Decimal,
717 pub total_available_balance: Decimal,
719 #[serde(rename = "totalPerpUPL")]
721 pub total_perp_upl: Decimal,
722 pub total_initial_margin: Decimal,
724 pub total_maintenance_margin: Decimal,
726 #[serde(rename = "accountIMRateByMp")]
728 pub account_im_rate_by_mp: Decimal,
729 #[serde(rename = "accountMMRateByMp")]
731 pub account_mm_rate_by_mp: Decimal,
732 #[serde(rename = "totalInitialMarginByMp")]
734 pub total_initial_margin_by_mp: Decimal,
735 #[serde(rename = "totalMaintenanceMarginByMp")]
737 pub total_maintenance_margin_by_mp: Decimal,
738 #[serde(deserialize_with = "hash_map")]
739 pub coin: HashMap<String, WalletCoin>,
740}
741
742#[derive(PartialEq, Deserialize, Debug, Clone)]
743#[serde(rename_all = "camelCase")]
744pub struct ExecutionMsg {
745 pub category: Category,
747 pub symbol: String,
749 #[serde(default, deserialize_with = "string_to_bool")]
751 pub is_leverage: bool,
752 pub order_id: String,
754 #[serde(default, deserialize_with = "empty_string_as_none")]
756 pub order_link_id: Option<String>,
757 pub side: Side,
759 pub order_price: Decimal,
761 pub order_qty: Decimal,
763 pub leaves_qty: Decimal,
765 pub create_type: CreateType,
768 pub order_type: OrderType,
770 pub stop_order_type: StopOrderType,
772 pub exec_fee: Decimal,
774 pub exec_id: String,
776 pub exec_price: Decimal,
778 pub exec_qty: Decimal,
780 pub exec_pnl: Decimal,
782 pub exec_type: ExecType,
784 pub exec_value: Decimal,
786 #[serde(deserialize_with = "number")]
788 pub exec_time: Timestamp,
789 pub is_maker: bool,
791 pub fee_rate: Decimal,
793 #[serde(default, deserialize_with = "option_decimal")]
795 pub trade_iv: Option<Decimal>,
796 #[serde(default, deserialize_with = "option_decimal")]
798 pub mark_iv: Option<Decimal>,
799 pub mark_price: Decimal,
801 #[serde(default, deserialize_with = "option_decimal")]
803 pub index_price: Option<Decimal>,
804 #[serde(default, deserialize_with = "option_decimal")]
806 pub underlying_price: Option<Decimal>,
807 #[serde(default, deserialize_with = "empty_string_as_none")]
809 pub block_trade_id: Option<String>,
810 pub closed_size: Decimal,
812 pub extra_fees: Option<Vec<ExtraFee>>, pub seq: i64,
818 pub fee_currency: String,
820}
821
822#[derive(PartialEq, Deserialize, Debug, Clone)]
823#[serde(rename_all = "camelCase")]
824pub struct ExtraFee {
825 pub fee_coin: String,
826 pub fee_type: ExtraFeeType,
827 pub sub_fee_type: ExtraSubFeeType,
828 pub fee_rate: Decimal,
829 pub fee: Decimal,
830}
831
832#[cfg(test)]
833mod tests {
834 use rust_decimal::dec;
835
836 use crate::DepthLevel;
837 use crate::serde::{Unique, deserialize_json};
838
839 use super::*;
840
841 #[test]
842 fn deserialize_incoming_message_command_subscribe() {
843 let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"subscribe"}"#;
844 let expected = IncomingMessage::Command(CommandMsg::Subscribe {
845 req_id: None,
846 ret_msg: None,
847 conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
848 success: true,
849 });
850
851 let message = deserialize_json(json).unwrap();
852
853 assert_eq!(expected, message);
854 }
855
856 #[test]
857 fn deserialize_incoming_message_command_unsubscribe() {
858 let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"unsubscribe"}"#;
859 let expected = IncomingMessage::Command(CommandMsg::Unsubscribe {
860 req_id: None,
861 ret_msg: None,
862 conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
863 success: true,
864 });
865
866 let message = deserialize_json(json).unwrap();
867
868 assert_eq!(expected, message);
869 }
870
871 #[test]
872 fn deserialize_incoming_message_ticker_delta() {
873 let json = r#"{
874 "topic": "tickers.BTCUSDT",
875 "type": "delta",
876 "data": {
877 "symbol": "BTCUSDT",
878 "tickDirection": "PlusTick",
879 "price24hPcnt": "-0.015895",
880 "lastPrice": "63948.50",
881 "turnover24h": "6793884423.5518",
882 "volume24h": "105991.3760",
883 "bid1Price": "63948.40",
884 "bid1Size": "3.439",
885 "ask1Price": "63948.50",
886 "ask1Size": "2.566"
887 },
888 "cs": 195377749067,
889 "ts": 1718995014034
890 }"#;
891 let ticker_delta = TickerMsg::Delta {
892 topic: Topic::Ticker(String::from("BTCUSDT")),
893 cs: Some(195377749067),
894 ts: 1718995014034,
895 data: TickerDeltaMsg {
896 symbol: String::from("BTCUSDT"),
897 tick_direction: Some(TickDirection::PlusTick),
898 last_price: Some(dec!(63948.5)),
899 pre_open_price: None,
900 pre_qty: None,
901 cur_pre_listing_phase: None,
902 prev_price24h: None,
903 price24h_pcnt: Some(dec!(-0.015895)),
904 high_price24h: None,
905 low_price24h: None,
906 prev_price1h: None,
907 mark_price: None,
908 index_price: None,
909 open_interest: None,
910 open_interest_value: None,
911 turnover24h: Some(dec!(6793884423.5518)),
912 volume24h: Some(dec!(105991.376)),
913 funding_rate: None,
914 next_funding_time: None,
915 bid1_price: Some(dec!(63948.4)),
916 bid1_size: Some(dec!(3.439)),
917 ask1_price: Some(dec!(63948.5)),
918 ask1_size: Some(dec!(2.566)),
919 delivery_time: None,
920 basis_rate: None,
921 delivery_fee_rate: None,
922 predicted_delivery_price: None,
923 },
924 };
925 let expected = IncomingMessage::Ticker(Box::new(ticker_delta));
926
927 let message = deserialize_json(json).unwrap();
928
929 assert_eq!(expected, message);
930 }
931
932 #[test]
933 fn deserialize_incoming_message_ticker_snapshot() {
934 let json = r#"{
936 "topic": "tickers.BTCUSDT",
937 "type": "snapshot",
938 "data": {
939 "symbol":"BTCUSDT",
940 "tickDirection":"ZeroPlusTick",
941 "price24hPcnt":"-0.044555",
942 "lastPrice":"84594.40",
943 "prevPrice24h":"88539.30",
944 "highPrice24h":"89389.90",
945 "lowPrice24h":"82055.60",
946 "prevPrice1h":"84307.20",
947 "markPrice":"84594.00",
948 "indexPrice":"84650.47",
949 "openInterest":"52903.75",
950 "openInterestValue":"4475339827.50",
951 "turnover24h":"17166562011.6514",
952 "volume24h":"200176.9910",
953 "nextFundingTime":"1740643200000",
954 "fundingRate":"-0.00016974",
955 "bid1Price":"84594.30",
956 "bid1Size":"6.777",
957 "ask1Price":"84594.40",
958 "ask1Size":"0.660",
959 "preOpenPrice":"",
960 "preQty":"",
961 "curPreListingPhase":""
962 },
963 "cs": 337149693308,
964 "ts": 1740622194359
965 }"#;
966 let ticker_snapshot = TickerMsg::Snapshot {
967 topic: Topic::Ticker(String::from("BTCUSDT")),
968 cs: Some(337149693308),
969 ts: 1740622194359,
970 data: TickerSnapshotMsg {
971 symbol: String::from("BTCUSDT"),
972 tick_direction: TickDirection::ZeroPlusTick,
973 last_price: dec!(84594.40),
974 pre_open_price: None,
975 pre_qty: None,
976 cur_pre_listing_phase: None,
977 prev_price24h: dec!(88539.30),
978 price24h_pcnt: dec!(-0.044555),
979 high_price24h: dec!(89389.90),
980 low_price24h: dec!(82055.60),
981 prev_price1h: dec!(84307.20),
982 mark_price: dec!(84594.00),
983 index_price: dec!(84650.47),
984 open_interest: dec!(52903.75),
985 open_interest_value: dec!(4475339827.50),
986 turnover24h: dec!(17166562011.6514),
987 volume24h: dec!(200176.9910),
988 funding_rate: dec!(-0.00016974),
989 next_funding_time: 1740643200000,
990 bid1_price: dec!(84594.30),
991 bid1_size: dec!(6.777),
992 ask1_price: dec!(84594.40),
993 ask1_size: dec!(0.660),
994 delivery_time: None,
995 basis_rate: None,
996 delivery_fee_rate: None,
997 predicted_delivery_price: None,
998 },
999 };
1000 let expected = IncomingMessage::Ticker(Box::new(ticker_snapshot));
1001
1002 let message = deserialize_json(json).unwrap();
1003
1004 assert_eq!(expected, message);
1005 }
1006
1007 #[test]
1008 fn deserialize_incoming_message_trade_snapshot() {
1009 let json = r#"{
1011 "topic":"publicTrade.BTCUSDT",
1012 "type":"snapshot",
1013 "ts":1741433245359,
1014 "data":[
1015 {
1016 "T":1741433245357,
1017 "s":"BTCUSDT",
1018 "S":"Buy",
1019 "v":"0.007",
1020 "p":"85821.00",
1021 "L":"PlusTick",
1022 "i":"485eaa70-df6e-5260-bbef-4f7324e3c5d9",
1023 "BT":false
1024 }
1025 ]
1026 }"#;
1027 let expected = IncomingMessage::Trade(TradeMsg::Snapshot {
1028 id: None,
1029 topic: Topic::Trade(String::from("BTCUSDT")),
1030 ts: 1741433245359,
1031 data: vec![TradeSnapshotMsg {
1032 time: 1741433245357,
1033 symbol: String::from("BTCUSDT"),
1034 side: Side::Buy,
1035 size: dec!(0.007),
1036 price: dec!(85821.00),
1037 tick_direction: TickDirection::PlusTick,
1038 trade_id: String::from("485eaa70-df6e-5260-bbef-4f7324e3c5d9"),
1039 block_trade: false,
1040 rpi_trade: None,
1041 mark_price: None,
1042 index_price: None,
1043 mark_iv: None,
1044 iv: None,
1045 }],
1046 });
1047
1048 let message = deserialize_json(json).unwrap();
1049
1050 assert_eq!(expected, message);
1051 }
1052
1053 #[test]
1054 fn deserialize_incoming_message_orderbook_snapshot() {
1055 let json = r#"{
1057 "topic":"orderbook.50.BTCUSDT",
1058 "type":"snapshot",
1059 "ts":1672304484978,
1060 "data":{
1061 "s":"BTCUSDT",
1062 "b":[
1063 ["16493.50","0.006"],
1064 ["16493.00","0.100"]
1065 ],
1066 "a":[
1067 ["16611.00","0.029"],
1068 ["16612.00","0.213"]
1069 ],
1070 "u":18521288,
1071 "seq":7961638724
1072 },
1073 "cts":1672304484976
1074 }"#;
1075 let expected = IncomingMessage::Orderbook(OrderbookMsg::Snapshot {
1076 topic: Topic::Orderbook {
1077 symbol: String::from("BTCUSDT"),
1078 depth: DepthLevel::Level50,
1079 },
1080 ts: 1672304484978,
1081 data: OrderbookDataMsg {
1082 symbol: String::from("BTCUSDT"),
1083 bids: vec![
1084 OrderbookLevel {
1085 price: dec!(16493.50),
1086 size: dec!(0.006),
1087 },
1088 OrderbookLevel {
1089 price: dec!(16493.00),
1090 size: dec!(0.100),
1091 },
1092 ],
1093 asks: vec![
1094 OrderbookLevel {
1095 price: dec!(16611.00),
1096 size: dec!(0.029),
1097 },
1098 OrderbookLevel {
1099 price: dec!(16612.00),
1100 size: dec!(0.213),
1101 },
1102 ],
1103 update_id: 18521288,
1104 seq: 7961638724,
1105 },
1106 cts: 1672304484976,
1107 });
1108
1109 let message = deserialize_json(json).unwrap();
1110
1111 assert_eq!(expected, message);
1112 }
1113
1114 #[test]
1115 fn deserialize_incoming_message_orderbook_delta() {
1116 let json = r#"{
1118 "topic":"orderbook.50.BTCUSDT",
1119 "type":"delta",
1120 "ts":1687940967466,
1121 "data":{
1122 "s":"BTCUSDT",
1123 "b":[
1124 ["30247.20","30.028"],
1125 ["30245.40","0.224"],
1126 ["30242.10","0.000"]
1127 ],
1128 "a":[
1129 ["30248.67","0.033"],
1130 ["30249.20","0.000"]
1131 ],
1132 "u":177400507,
1133 "seq":66544703342
1134 },
1135 "cts":1687940967464
1136 }"#;
1137 let expected = IncomingMessage::Orderbook(OrderbookMsg::Delta {
1138 topic: Topic::Orderbook {
1139 symbol: String::from("BTCUSDT"),
1140 depth: DepthLevel::Level50,
1141 },
1142 ts: 1687940967466,
1143 data: OrderbookDataMsg {
1144 symbol: String::from("BTCUSDT"),
1145 bids: vec![
1146 OrderbookLevel {
1147 price: dec!(30247.20),
1148 size: dec!(30.028),
1149 },
1150 OrderbookLevel {
1151 price: dec!(30245.40),
1152 size: dec!(0.224),
1153 },
1154 OrderbookLevel {
1155 price: dec!(30242.10),
1156 size: dec!(0.000),
1157 },
1158 ],
1159 asks: vec![
1160 OrderbookLevel {
1161 price: dec!(30248.67),
1162 size: dec!(0.033),
1163 },
1164 OrderbookLevel {
1165 price: dec!(30249.20),
1166 size: dec!(0.000),
1167 },
1168 ],
1169 update_id: 177400507,
1170 seq: 66544703342,
1171 },
1172 cts: 1687940967464,
1173 });
1174
1175 let message = deserialize_json(json).unwrap();
1176
1177 assert_eq!(expected, message);
1178 }
1179
1180 #[test]
1181 fn deserialize_incoming_message_all_liquidation_snapshot() {
1182 let json = r#"{
1184 "topic":"allLiquidation.BTCUSDT",
1185 "type":"snapshot",
1186 "ts":1741450605553,
1187 "data":[
1188 {
1189 "T":1741450605236,
1190 "s":"BTCUSDT",
1191 "S":"Buy",
1192 "v":"0.001",
1193 "p":"85823.60"
1194 }
1195 ]
1196 }"#;
1197 let expected = AllLiquidationMsg::Snapshot {
1198 topic: Topic::AllLiquidation(String::from("BTCUSDT")),
1199 ts: 1741450605553,
1200 data: vec![AllLiquidationSnapshotMsg {
1201 time: 1741450605236,
1202 symbol: String::from("BTCUSDT"),
1203 side: Side::Buy,
1204 size: dec!(0.001),
1205 price: dec!(85823.60),
1206 }],
1207 };
1208
1209 let message = deserialize_json(json).unwrap();
1210
1211 assert_eq!(expected, message);
1212 }
1213
1214 #[test]
1215 fn deserialize_incoming_message_order() {
1216 let json = r#"{
1217 "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90",
1218 "topic": "order",
1219 "creationTime": 1672364262474,
1220 "data": [
1221 {
1222 "symbol": "ETH-30DEC22-1400-C",
1223 "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020",
1224 "side": "Sell",
1225 "orderType": "Market",
1226 "cancelType": "UNKNOWN",
1227 "price": "72.5",
1228 "qty": "1",
1229 "orderIv": "",
1230 "timeInForce": "IOC",
1231 "orderStatus": "Filled",
1232 "orderLinkId": "",
1233 "lastPriceOnCreated": "",
1234 "reduceOnly": false,
1235 "leavesQty": "",
1236 "leavesValue": "",
1237 "cumExecQty": "1",
1238 "cumExecValue": "75",
1239 "avgPrice": "75",
1240 "blockTradeId": "",
1241 "positionIdx": 0,
1242 "cumExecFee": "0.358635",
1243 "closedPnl": "0",
1244 "createdTime": "1672364262444",
1245 "updatedTime": "1672364262457",
1246 "rejectReason": "EC_NoError",
1247 "stopOrderType": "",
1248 "tpslMode": "",
1249 "triggerPrice": "",
1250 "takeProfit": "",
1251 "stopLoss": "",
1252 "tpTriggerBy": "",
1253 "slTriggerBy": "",
1254 "tpLimitPrice": "",
1255 "slLimitPrice": "",
1256 "triggerDirection": 0,
1257 "triggerBy": "",
1258 "closeOnTrigger": false,
1259 "category": "option",
1260 "placeType": "price",
1261 "smpType": "None",
1262 "smpGroup": 0,
1263 "smpOrderId": "",
1264 "feeCurrency": "",
1265 "cumFeeDetail": {
1266 "MNT": "0.00242968"
1267 }
1268 }
1269 ]
1270 }"#;
1271 let order = PrivateMsg {
1272 id: String::from("5923240c6880ab-c59f-420b-9adb-3639adc9dd90"),
1273 creation_time: 1672364262474,
1274 data: vec![OrderMsg {
1275 category: Category::Option,
1276 order_id: String::from("5cf98598-39a7-459e-97bf-76ca765ee020"),
1277 order_link_id: None,
1278 is_leverage: None,
1279 block_trade_id: None,
1280 symbol: String::from("ETH-30DEC22-1400-C"),
1281 price: dec!(72.5),
1282 broker_order_price: None,
1283 qty: dec!(1.0),
1284 side: Side::Sell,
1285 position_idx: PositionIdx::OneWay,
1286 order_status: OrderStatus::Filled,
1287 create_type: None,
1288 cancel_type: CancelType::UNKNOWN,
1289 reject_reason: RejectReason::EcNoError,
1290 avg_price: Some(dec!(75.0)),
1291 leaves_qty: None,
1292 leaves_value: None,
1293 cum_exec_qty: dec!(1.0),
1294 cum_exec_value: dec!(75.0),
1295 cum_exec_fee: dec!(0.358635),
1296 closed_pnl: dec!(0.0),
1297 fee_currency: None,
1298 time_in_force: TimeInForce::IOC,
1299 order_type: OrderType::Market,
1300 stop_order_type: None,
1301 oco_trigger_by: None,
1302 order_iv: None,
1303 market_unit: None,
1304 slippage_tolerance_type: None,
1305 slippage_tolerance: None,
1306 trigger_price: None,
1307 take_profit: None,
1308 stop_loss: None,
1309 tpsl_mode: None,
1310 tp_limit_price: None,
1311 sl_limit_price: None,
1312 tp_trigger_by: None,
1313 sl_trigger_by: None,
1314 trigger_direction: TriggerDirection::UNKNOWN,
1315 trigger_by: None,
1316 last_price_on_created: None,
1317 reduce_only: false,
1318 close_on_trigger: false,
1319 place_type: Some(PlaceType::Price),
1320 smp_type: SmpType::None,
1321 smp_group: 0,
1322 smp_order_id: None,
1323 created_time: 1672364262444,
1324 updated_time: 1672364262457,
1325 cum_fee_detail: Some(serde_json::from_str(r#"{"MNT": "0.00242968"}"#).unwrap()),
1326 }],
1327 };
1328 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1329
1330 let message = deserialize_json(json).unwrap();
1331
1332 assert_eq!(expected, message);
1333 }
1334
1335 #[test]
1336 fn deserialize_incoming_message_order2() {
1337 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":{}}]}"#;
1338 let order = PrivateMsg {
1339 id: String::from("108985347_ADAUSDT_140667095077548"),
1340 creation_time: 1766436947942,
1341 data: vec![OrderMsg {
1342 category: Category::Linear,
1343 order_id: String::from("ae802ad5-af70-4957-ba72-86ad7fc9c24d"),
1344 order_link_id: None,
1345 is_leverage: None,
1346 block_trade_id: None,
1347 symbol: String::from("ADAUSDT"),
1348 price: dec!(0.3862),
1349 broker_order_price: None,
1350 qty: dec!(15),
1351 side: Side::Buy,
1352 position_idx: PositionIdx::OneWay,
1353 order_status: OrderStatus::Filled,
1354 create_type: Some(CreateType::CreateByUser),
1355 cancel_type: CancelType::UNKNOWN,
1356 reject_reason: RejectReason::EcNoError,
1357 avg_price: Some(dec!(0.3679)),
1358 leaves_qty: Some(dec!(0)),
1359 leaves_value: Some(dec!(0)),
1360 cum_exec_qty: dec!(15),
1361 cum_exec_value: dec!(5.5185),
1362 cum_exec_fee: dec!(0.00303518),
1363 closed_pnl: dec!(0),
1364 fee_currency: None,
1365 time_in_force: TimeInForce::IOC,
1366 order_type: OrderType::Market,
1367 stop_order_type: None,
1368 oco_trigger_by: None,
1369 order_iv: None,
1370 market_unit: None,
1371 slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1372 slippage_tolerance: Some(dec!(0)),
1373 trigger_price: None,
1374 take_profit: None,
1375 stop_loss: None,
1376 tpsl_mode: Some(TpslMode::UNKNOWN),
1377 tp_limit_price: Some(dec!(0)),
1378 sl_limit_price: Some(dec!(0)),
1379 tp_trigger_by: None,
1380 sl_trigger_by: None,
1381 trigger_direction: TriggerDirection::UNKNOWN,
1382 trigger_by: None,
1383 last_price_on_created: Some(dec!(0.3679)),
1384 reduce_only: false,
1385 close_on_trigger: false,
1386 place_type: None,
1387 smp_type: SmpType::None,
1388 smp_group: 0,
1389 smp_order_id: None,
1390 created_time: 1766436947940,
1391 updated_time: 1766436947940,
1392 cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1393 }],
1394 };
1395 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1396
1397 let message = deserialize_json(json).unwrap();
1398
1399 assert_eq!(expected, message);
1400 }
1401
1402 #[test]
1403 fn deserialize_incoming_message_order3() {
1404 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":{}}]}"#;
1405 let order = PrivateMsg {
1406 id: String::from("108985347_ADAUSDT_140667102632416"),
1407 creation_time: 1766600379878,
1408 data: vec![OrderMsg {
1409 category: Category::Linear,
1410 order_id: String::from("f0468cbc-ed2f-4fd7-9620-998f3e9f387c"),
1411 order_link_id: Some(String::from("BOT_LINK_ID-1")),
1412 is_leverage: None,
1413 block_trade_id: None,
1414 symbol: String::from("ADAUSDT"),
1415 price: dec!(0.3539),
1416 broker_order_price: None,
1417 qty: dec!(15),
1418 side: Side::Buy,
1419 position_idx: PositionIdx::OneWay,
1420 order_status: OrderStatus::New,
1421 create_type: Some(CreateType::CreateByUser),
1422 cancel_type: CancelType::UNKNOWN,
1423 reject_reason: RejectReason::EcNoError,
1424 avg_price: None,
1425 leaves_qty: Some(dec!(15)),
1426 leaves_value: Some(dec!(5.3085)),
1427 cum_exec_qty: dec!(0),
1428 cum_exec_value: dec!(0),
1429 cum_exec_fee: dec!(0),
1430 closed_pnl: dec!(0),
1431 fee_currency: None,
1432 time_in_force: TimeInForce::GTC,
1433 order_type: OrderType::Limit,
1434 stop_order_type: None,
1435 oco_trigger_by: None,
1436 order_iv: None,
1437 market_unit: None,
1438 slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1439 slippage_tolerance: Some(dec!(0)),
1440 trigger_price: None,
1441 take_profit: None,
1442 stop_loss: None,
1443 tpsl_mode: Some(TpslMode::UNKNOWN),
1444 tp_limit_price: Some(dec!(0)),
1445 sl_limit_price: Some(dec!(0)),
1446 tp_trigger_by: None,
1447 sl_trigger_by: None,
1448 trigger_direction: TriggerDirection::UNKNOWN,
1449 trigger_by: None,
1450 last_price_on_created: Some(dec!(0.355)),
1451 reduce_only: false,
1452 close_on_trigger: false,
1453 place_type: None, smp_type: SmpType::None,
1455 smp_group: 0,
1456 smp_order_id: None,
1457 created_time: 1766600379876,
1458 updated_time: 1766600379876,
1459 cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1460 }],
1461 };
1462 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1463
1464 let message = deserialize_json(json).unwrap();
1465
1466 assert_eq!(expected, message);
1467 }
1468
1469 #[test]
1470 fn deserialize_incoming_message_position() {
1471 let json = r#"{
1472 "id": "108985347_position_1765659601915",
1473 "topic": "position",
1474 "creationTime": 1765659601915,
1475 "data": [
1476 {
1477 "positionIdx": 1,
1478 "tradeMode": 0,
1479 "riskId": 116,
1480 "riskLimitValue": "200000",
1481 "symbol": "ADAUSDT",
1482 "side": "Buy",
1483 "size": "18720",
1484 "entryPrice": "0.41160027",
1485 "sessionAvgPrice": "",
1486 "leverage": "75",
1487 "positionValue": "7705.157",
1488 "positionBalance": "0",
1489 "markPrice": "0.41",
1490 "positionIM": "106.51735757",
1491 "positionMM": "61.74535757",
1492 "positionIMByMp": "106.51735757",
1493 "positionMMByMp": "61.74535757",
1494 "takeProfit": "0.4321",
1495 "stopLoss": "0.3704",
1496 "trailingStop": "0",
1497 "unrealisedPnl": "-29.957",
1498 "cumRealisedPnl": "-6712.87804378",
1499 "curRealisedPnl": "-2.6317147",
1500 "createdTime": "1714594321840",
1501 "updatedTime": "1765645142548",
1502 "tpslMode": "Full",
1503 "liqPrice": "0.37000066",
1504 "bustPrice": "",
1505 "category": "linear",
1506 "positionStatus": "Normal",
1507 "adlRankIndicator": 2,
1508 "autoAddMargin": 0,
1509 "leverageSysUpdatedTime": "",
1510 "mmrSysUpdatedTime": "",
1511 "seq": 140667058318085,
1512 "isReduceOnly": false
1513 },
1514 {
1515 "positionIdx": 2,
1516 "tradeMode": 0,
1517 "riskId": 116,
1518 "riskLimitValue": "200000",
1519 "symbol": "ADAUSDT",
1520 "side": "",
1521 "size": "0",
1522 "entryPrice": "0",
1523 "sessionAvgPrice": "",
1524 "leverage": "75",
1525 "positionValue": "0",
1526 "positionBalance": "0",
1527 "markPrice": "0.41",
1528 "positionIM": "",
1529 "positionMM": "",
1530 "positionIMByMp": "",
1531 "positionMMByMp": "",
1532 "takeProfit": "0",
1533 "stopLoss": "0",
1534 "trailingStop": "0",
1535 "unrealisedPnl": "0",
1536 "cumRealisedPnl": "1618.30675974",
1537 "curRealisedPnl": "0",
1538 "createdTime": "1714594321840",
1539 "updatedTime": "1765046350698",
1540 "tpslMode": "Full",
1541 "liqPrice": "0",
1542 "bustPrice": "",
1543 "category": "linear",
1544 "positionStatus": "Normal",
1545 "adlRankIndicator": 0,
1546 "autoAddMargin": 0,
1547 "leverageSysUpdatedTime": "",
1548 "mmrSysUpdatedTime": "",
1549 "seq": 140667031311361,
1550 "isReduceOnly": false
1551 }
1552 ]
1553 }"#;
1554 let position = PrivateMsg {
1555 id: String::from("108985347_position_1765659601915"),
1556 creation_time: 1765659601915,
1557 data: vec![
1558 PositionMsg {
1559 category: Category::Linear,
1560 symbol: String::from("ADAUSDT"),
1561 side: Some(Side::Buy),
1562 size: dec!(18720),
1563 position_idx: PositionIdx::Buy,
1564 position_value: dec!(7705.157),
1565 risk_id: 116,
1566 risk_limit_value: Some(dec!(200000)),
1567 entry_price: dec!(0.41160027),
1568 mark_price: dec!(0.41),
1569 leverage: dec!(75),
1570 auto_add_margin: false,
1571 position_im: Some(dec!(106.51735757)),
1572 position_mm: Some(dec!(61.74535757)),
1573 position_im_by_mp: Some(dec!(106.51735757)),
1574 position_mm_by_mp: Some(dec!(61.74535757)),
1575 liq_price: Some(dec!(0.37000066)),
1576 take_profit: dec!(0.4321),
1577 stop_loss: dec!(0.3704),
1578 trailing_stop: dec!(0),
1579 unrealised_pnl: dec!(-29.957),
1580 cur_realised_pnl: dec!(-2.6317147),
1581 session_avg_price: None,
1582 delta: None,
1583 gamma: None,
1584 vega: None,
1585 theta: None,
1586 cum_realised_pnl: dec!(-6712.87804378),
1587 position_status: PositionStatus::Normal,
1588 adl_rank_indicator: AdlRankIndicator::Two,
1589 is_reduce_only: false,
1590 mmr_sys_updated_time: None,
1591 leverage_sys_updated_time: None,
1592 created_time: 1714594321840,
1593 updated_time: 1765645142548,
1594 seq: 140667058318085,
1595 },
1596 PositionMsg {
1597 category: Category::Linear,
1598 symbol: String::from("ADAUSDT"),
1599 side: None,
1600 size: dec!(0),
1601 position_idx: PositionIdx::Sell,
1602 position_value: dec!(0),
1603 risk_id: 116,
1604 risk_limit_value: Some(dec!(200000)),
1605 entry_price: dec!(0),
1606 mark_price: dec!(0.41),
1607 leverage: dec!(75),
1608 auto_add_margin: false,
1609 position_im: None,
1610 position_mm: None,
1611 position_im_by_mp: None,
1612 position_mm_by_mp: None,
1613 liq_price: Some(dec!(0)),
1614 take_profit: dec!(0),
1615 stop_loss: dec!(0),
1616 trailing_stop: dec!(0),
1617 unrealised_pnl: dec!(0),
1618 cur_realised_pnl: dec!(0),
1619 session_avg_price: None,
1620 delta: None,
1621 gamma: None,
1622 vega: None,
1623 theta: None,
1624 cum_realised_pnl: dec!(1618.30675974),
1625 position_status: PositionStatus::Normal,
1626 adl_rank_indicator: AdlRankIndicator::Zero,
1627 is_reduce_only: false,
1628 mmr_sys_updated_time: None,
1629 leverage_sys_updated_time: None,
1630 created_time: 1714594321840,
1631 updated_time: 1765046350698,
1632 seq: 140667031311361,
1633 },
1634 ],
1635 };
1636 let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1637
1638 let message = deserialize_json(json).unwrap();
1639
1640 assert_eq!(expected, message);
1641 }
1642
1643 #[test]
1644 fn deserialize_incoming_message_position2() {
1645 let json = r#"{
1646 "id":"108985347_position_1766316605952",
1647 "topic":"position",
1648 "creationTime":1766316605952,
1649 "data":[
1650 {
1651 "positionIdx":1,
1652 "tradeMode":0,
1653 "riskId":116,
1654 "riskLimitValue":"200000",
1655 "symbol":"ADAUSDT",
1656 "side":"Buy",
1657 "size":"43",
1658 "entryPrice":"0.37293023",
1659 "sessionAvgPrice":"",
1660 "leverage":"75",
1661 "positionValue":"16.036",
1662 "positionBalance":"0",
1663 "markPrice":"0.3702",
1664 "positionIM":"0.22095025",
1665 "positionMM":"0.12809175",
1666 "positionIMByMp":"0.22095025",
1667 "positionMMByMp":"0.12809175",
1668 "takeProfit":"0",
1669 "stopLoss":"0",
1670 "trailingStop":"0",
1671 "unrealisedPnl":"-0.1174",
1672 "cumRealisedPnl":"-7547.8530836",
1673 "curRealisedPnl":"-0.00465061",
1674 "createdTime":"1714594321840",
1675 "updatedTime":"1766313370061",
1676 "tpslMode":"Full",
1677 "liqPrice":"",
1678 "bustPrice":"",
1679 "category":"linear",
1680 "positionStatus":"Normal",
1681 "adlRankIndicator":2,
1682 "autoAddMargin":0,
1683 "leverageSysUpdatedTime":"",
1684 "mmrSysUpdatedTime":"",
1685 "seq":140667089523042,
1686 "isReduceOnly":false
1687 },
1688 {
1689 "positionIdx":2,
1690 "tradeMode":0,
1691 "riskId":116,
1692 "riskLimitValue":"200000",
1693 "symbol":"ADAUSDT",
1694 "side":"",
1695 "size":"0",
1696 "entryPrice":"0",
1697 "sessionAvgPrice":"",
1698 "leverage":"75",
1699 "positionValue":"0",
1700 "positionBalance":"0",
1701 "markPrice":"0.3702",
1702 "positionIM":"",
1703 "positionMM":"",
1704 "positionIMByMp":"",
1705 "positionMMByMp":"",
1706 "takeProfit":"0",
1707 "stopLoss":"0",
1708 "trailingStop":"0",
1709 "unrealisedPnl":"0",
1710 "cumRealisedPnl":"1618.30675974",
1711 "curRealisedPnl":"0",
1712 "createdTime":"1714594321840",
1713 "updatedTime":"1765046350698",
1714 "tpslMode":"Full",
1715 "liqPrice":"0",
1716 "bustPrice":"",
1717 "category":"linear",
1718 "positionStatus":"Normal",
1719 "adlRankIndicator":0,
1720 "autoAddMargin":0,
1721 "leverageSysUpdatedTime":"",
1722 "mmrSysUpdatedTime":"",
1723 "seq":140667031311361,
1724 "isReduceOnly":false
1725 }
1726 ]
1727 }"#;
1728 let position = PrivateMsg {
1729 id: String::from("108985347_position_1766316605952"),
1730 creation_time: 1766316605952,
1731 data: vec![
1732 PositionMsg {
1733 category: Category::Linear,
1734 symbol: String::from("ADAUSDT"),
1735 side: Some(Side::Buy),
1736 size: dec!(43),
1737 position_idx: PositionIdx::Buy,
1738 position_value: dec!(16.036),
1739 risk_id: 116,
1740 risk_limit_value: Some(dec!(200000)),
1741 entry_price: dec!(0.37293023),
1742 mark_price: dec!(0.3702),
1743 leverage: dec!(75),
1744 auto_add_margin: false,
1745 position_im: Some(dec!(0.22095025)),
1746 position_mm: Some(dec!(0.12809175)),
1747 position_im_by_mp: Some(dec!(0.22095025)),
1748 position_mm_by_mp: Some(dec!(0.12809175)),
1749 liq_price: None,
1750 take_profit: dec!(0),
1751 stop_loss: dec!(0),
1752 trailing_stop: dec!(0),
1753 unrealised_pnl: dec!(-0.1174),
1754 cur_realised_pnl: dec!(-0.00465061),
1755 session_avg_price: None,
1756 delta: None,
1757 gamma: None,
1758 vega: None,
1759 theta: None,
1760 cum_realised_pnl: dec!(-7547.8530836),
1761 position_status: PositionStatus::Normal,
1762 adl_rank_indicator: AdlRankIndicator::Two,
1763 is_reduce_only: false,
1764 mmr_sys_updated_time: None,
1765 leverage_sys_updated_time: None,
1766 created_time: 1714594321840,
1767 updated_time: 1766313370061,
1768 seq: 140667089523042,
1769 },
1770 PositionMsg {
1771 category: Category::Linear,
1772 symbol: String::from("ADAUSDT"),
1773 side: None,
1774 size: dec!(0),
1775 position_idx: PositionIdx::Sell,
1776 position_value: dec!(0),
1777 risk_id: 116,
1778 risk_limit_value: Some(dec!(200000)),
1779 entry_price: dec!(0),
1780 mark_price: dec!(0.3702),
1781 leverage: dec!(75),
1782 auto_add_margin: false,
1783 position_im: None,
1784 position_mm: None,
1785 position_im_by_mp: None,
1786 position_mm_by_mp: None,
1787 liq_price: Some(dec!(0)),
1788 take_profit: dec!(0),
1789 stop_loss: dec!(0),
1790 trailing_stop: dec!(0),
1791 unrealised_pnl: dec!(0),
1792 cur_realised_pnl: dec!(0),
1793 session_avg_price: None,
1794 delta: None,
1795 gamma: None,
1796 vega: None,
1797 theta: None,
1798 cum_realised_pnl: dec!(1618.30675974),
1799 position_status: PositionStatus::Normal,
1800 adl_rank_indicator: AdlRankIndicator::Zero,
1801 is_reduce_only: false,
1802 mmr_sys_updated_time: None,
1803 leverage_sys_updated_time: None,
1804 created_time: 1714594321840,
1805 updated_time: 1765046350698,
1806 seq: 140667031311361,
1807 },
1808 ],
1809 };
1810 let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1811
1812 let message = deserialize_json(json).unwrap();
1813
1814 assert_eq!(expected, message);
1815 }
1816
1817 #[test]
1818 fn deserialize_incoming_message_wallet() {
1819 let json = r#"{
1820 "id": "592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e",
1821 "topic": "wallet",
1822 "creationTime": 1700034722104,
1823 "data": [
1824 {
1825 "accountIMRate": "0",
1826 "accountIMRateByMp": "0",
1827 "accountMMRate": "0",
1828 "accountMMRateByMp": "0",
1829 "totalEquity": "10262.91335023",
1830 "totalWalletBalance": "9684.46297164",
1831 "totalMarginBalance": "9684.46297164",
1832 "totalAvailableBalance": "9556.6056555",
1833 "totalPerpUPL": "0",
1834 "totalInitialMargin": "0",
1835 "totalInitialMarginByMp": "0",
1836 "totalMaintenanceMargin": "0",
1837 "totalMaintenanceMarginByMp": "0",
1838 "coin": [
1839 {
1840 "coin": "BTC",
1841 "equity": "0.00102964",
1842 "usdValue": "36.70759517",
1843 "walletBalance": "0.00102964",
1844 "availableToWithdraw": "0.00102964",
1845 "availableToBorrow": "",
1846 "borrowAmount": "0",
1847 "accruedInterest": "0",
1848 "totalOrderIM": "",
1849 "totalPositionIM": "",
1850 "totalPositionMM": "",
1851 "unrealisedPnl": "0",
1852 "cumRealisedPnl": "-0.00000973",
1853 "bonus": "0",
1854 "collateralSwitch": true,
1855 "marginCollateral": true,
1856 "locked": "0",
1857 "spotHedgingQty": "0.01592413",
1858 "spotBorrow": "0"
1859 }
1860 ],
1861 "accountLTV": "0",
1862 "accountType": "UNIFIED"
1863 }
1864 ]
1865 }"#;
1866 let coin = WalletCoin {
1867 coin: String::from("BTC"),
1868 equity: dec!(0.00102964),
1869 usd_value: dec!(36.70759517),
1870 wallet_balance: dec!(0.00102964),
1871 locked: dec!(0),
1872 spot_hedging_qty: dec!(0.01592413),
1873 borrow_amount: dec!(0),
1874 accrued_interest: dec!(0),
1875 total_order_im: None,
1876 total_position_im: None,
1877 total_position_mm: None,
1878 unrealised_pnl: dec!(0),
1879 cum_realised_pnl: dec!(-0.00000973),
1880 bonus: dec!(0),
1881 collateral_switch: true,
1882 margin_collateral: true,
1883 spot_borrow: Some(dec!(0)),
1884 };
1885 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
1886 let wallet = PrivateMsg {
1887 id: String::from("592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e"),
1888 creation_time: 1700034722104,
1889 data: vec![WalletMsg {
1890 account_type: AccountType::UNIFIED,
1891 account_im_rate: dec!(0),
1892 account_im_rate_by_mp: dec!(0),
1893 account_mm_rate: dec!(0),
1894 account_mm_rate_by_mp: dec!(0),
1895 total_equity: dec!(10262.91335023),
1896 total_wallet_balance: dec!(9684.46297164),
1897 total_margin_balance: dec!(9684.46297164),
1898 total_available_balance: dec!(9556.6056555),
1899 total_perp_upl: dec!(0),
1900 total_initial_margin: dec!(0),
1901 total_initial_margin_by_mp: dec!(0),
1902 total_maintenance_margin: dec!(0),
1903 total_maintenance_margin_by_mp: dec!(0),
1904 coin,
1905 }],
1906 };
1907 let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
1908
1909 let message = deserialize_json(json).unwrap();
1910
1911 assert_eq!(expected, message);
1912 }
1913
1914 #[test]
1915 fn deserialize_incoming_message_wallet2() {
1916 let json = r#"{
1917 "id":"108985347_wallet_1766318882965",
1918 "topic":"wallet",
1919 "creationTime":1766318882964,
1920 "data":[
1921 {
1922 "accountIMRate":"0.0007",
1923 "accountMMRate":"0.0004",
1924 "accountIMRateByMp":"0.0007",
1925 "accountMMRateByMp":"0.0004",
1926 "totalEquity":"102.7094181",
1927 "totalWalletBalance":"102.16591975",
1928 "totalMarginBalance":"102.16591975",
1929 "totalAvailableBalance":"102.09402758",
1930 "totalPerpUPL":"0",
1931 "totalInitialMargin":"0.07189217",
1932 "totalMaintenanceMargin":"0.04166941",
1933 "totalInitialMarginByMp":"0.07189217",
1934 "totalMaintenanceMarginByMp":"0.04166941",
1935 "coin":[
1936 {
1937 "coin":"USDT",
1938 "equity":"75.5601152",
1939 "usdValue":"75.53450032",
1940 "walletBalance":"75.5601152",
1941 "availableToWithdraw":"",
1942 "availableToBorrow":"",
1943 "borrowAmount":"0",
1944 "accruedInterest":"0",
1945 "totalOrderIM":"0",
1946 "totalPositionIM":"0.07191655",
1947 "totalPositionMM":"0.04168355",
1948 "unrealisedPnl":"0",
1949 "cumRealisedPnl":"36163.8134634",
1950 "bonus":"0",
1951 "collateralSwitch":true,
1952 "marginCollateral":true,
1953 "locked":"0",
1954 "spotHedgingQty":"0"
1955 }
1956 ],
1957 "accountLTV":"0",
1958 "accountType":"UNIFIED"
1959 }
1960 ]
1961 }"#;
1962 let coin = WalletCoin {
1963 coin: String::from("USDT"),
1964 equity: dec!(75.5601152),
1965 usd_value: dec!(75.53450032),
1966 wallet_balance: dec!(75.5601152),
1967 locked: dec!(0),
1968 spot_hedging_qty: dec!(0),
1969 borrow_amount: dec!(0),
1970 accrued_interest: dec!(0),
1971 total_order_im: Some(dec!(0)),
1972 total_position_im: Some(dec!(0.07191655)),
1973 total_position_mm: Some(dec!(0.04168355)),
1974 unrealised_pnl: dec!(0),
1975 cum_realised_pnl: dec!(36163.8134634),
1976 bonus: dec!(0),
1977 collateral_switch: true,
1978 margin_collateral: true,
1979 spot_borrow: None,
1980 };
1981 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
1982 let wallet = PrivateMsg {
1983 id: String::from("108985347_wallet_1766318882965"),
1984 creation_time: 1766318882964,
1985 data: vec![WalletMsg {
1986 account_type: AccountType::UNIFIED,
1987 account_im_rate: dec!(0.0007),
1988 account_im_rate_by_mp: dec!(0.0007),
1989 account_mm_rate: dec!(0.0004),
1990 account_mm_rate_by_mp: dec!(0.0004),
1991 total_equity: dec!(102.7094181),
1992 total_wallet_balance: dec!(102.16591975),
1993 total_margin_balance: dec!(102.16591975),
1994 total_available_balance: dec!(102.09402758),
1995 total_perp_upl: dec!(0),
1996 total_initial_margin: dec!(0.07189217),
1997 total_initial_margin_by_mp: dec!(0.07189217),
1998 total_maintenance_margin: dec!(0.04166941),
1999 total_maintenance_margin_by_mp: dec!(0.04166941),
2000 coin,
2001 }],
2002 };
2003 let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
2004
2005 let message = deserialize_json(json).unwrap();
2006
2007 assert_eq!(expected, message);
2008 }
2009
2010 #[test]
2011 fn deserialize_incoming_message_execution() {
2012 let json = r#"{
2013 "topic": "execution",
2014 "id": "386825804_BTCUSDT_140612148849382",
2015 "creationTime": 1746270400355,
2016 "data": [
2017 {
2018 "category": "linear",
2019 "symbol": "BTCUSDT",
2020 "closedSize": "0.5",
2021 "execFee": "26.3725275",
2022 "execId": "0ab1bdf7-4219-438b-b30a-32ec863018f7",
2023 "execPrice": "95900.1",
2024 "execQty": "0.5",
2025 "execType": "Trade",
2026 "execValue": "47950.05",
2027 "feeRate": "0.00055",
2028 "tradeIv": "",
2029 "markIv": "",
2030 "blockTradeId": "",
2031 "markPrice": "95901.48",
2032 "indexPrice": "",
2033 "underlyingPrice": "",
2034 "leavesQty": "0",
2035 "orderId": "9aac161b-8ed6-450d-9cab-c5cc67c21784",
2036 "orderLinkId": "",
2037 "orderPrice": "94942.5",
2038 "orderQty": "0.5",
2039 "orderType": "Market",
2040 "stopOrderType": "UNKNOWN",
2041 "side": "Sell",
2042 "execTime": "1746270400353",
2043 "isLeverage": "0",
2044 "isMaker": false,
2045 "seq": 140612148849382,
2046 "marketUnit": "",
2047 "execPnl": "0.05",
2048 "createType": "CreateByUser",
2049 "extraFees":[{"feeCoin":"USDT","feeType":"GST","subFeeType":"IND_GST","feeRate":"0.0000675","fee":"0.006403779"}],
2050 "feeCurrency": "USDT"
2051 }
2052 ]
2053 }"#;
2054 let execution = PrivateMsg {
2055 id: String::from("386825804_BTCUSDT_140612148849382"),
2056 creation_time: 1746270400355,
2057 data: vec![ExecutionMsg {
2058 category: Category::Linear,
2059 symbol: String::from("BTCUSDT"),
2060 is_leverage: false,
2061 order_id: String::from("9aac161b-8ed6-450d-9cab-c5cc67c21784"),
2062 order_link_id: None,
2063 side: Side::Sell,
2064 order_price: dec!(94942.5),
2065 order_qty: dec!(0.5),
2066 leaves_qty: dec!(0),
2067 create_type: CreateType::CreateByUser,
2068 order_type: OrderType::Market,
2069 stop_order_type: StopOrderType::UNKNOWN,
2070 exec_fee: dec!(26.3725275),
2071 exec_id: String::from("0ab1bdf7-4219-438b-b30a-32ec863018f7"),
2072 exec_price: dec!(95900.1),
2073 exec_qty: dec!(0.5),
2074 exec_pnl: dec!(0.05),
2075 exec_type: ExecType::Trade,
2076 exec_value: dec!(47950.05),
2077 exec_time: 1746270400353,
2078 is_maker: false,
2079 fee_rate: dec!(0.00055),
2080 trade_iv: None,
2081 mark_iv: None,
2082 mark_price: dec!(95901.48),
2083 index_price: None,
2084 underlying_price: None,
2085 block_trade_id: None,
2086 closed_size: dec!(0.5),
2087 extra_fees: Some(vec![ExtraFee {
2088 fee_coin: String::from("USDT"),
2089 fee_type: ExtraFeeType::Gst,
2090 sub_fee_type: ExtraSubFeeType::IndGst,
2091 fee_rate: dec!(0.0000675),
2092 fee: dec!(0.006403779),
2093 }]),
2094 seq: 140612148849382,
2095 fee_currency: String::from("USDT"),
2096 }],
2097 };
2098 let expected = IncomingMessage::Topic(TopicMessage::Execution(execution));
2099
2100 let message = deserialize_json(json).unwrap();
2101
2102 assert_eq!(expected, message);
2103 }
2104}