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