Skip to main content

bybit/ws/
incoming_message.rs

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    // TickerMsg is 584 bytes (TickerDeltaMsg alone is 528 bytes — 24 optional Decimals × 16 bytes
25    // each, plus TickerSnapshotMsg at 448 bytes). Without Box the entire IncomingMessage enum
26    // would be 584 bytes on every allocation, including the tiny Command/Trade/Topic variants that
27    // flow through the mpsc channel far more frequently. Box keeps IncomingMessage at 104 bytes.
28    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// TODO: Use PublicMsg<T>
116#[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// TODO: Use PublicMsg<T>
236#[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// TODO: Use PublicMsg<T>
280#[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// TODO: Use PublicMsg<T>
307#[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    /// Symbol name
329    #[serde(rename = "s")]
330    pub symbol: String,
331    /// Bid, buy side. For snapshot, sorted by price in descending order.
332    /// For delta, size 0 means delete the price level
333    #[serde(rename = "b")]
334    pub bids: Vec<OrderbookLevel>,
335    /// Ask, sell side. For snapshot, sorted by price in ascending order.
336    /// For delta, size 0 means delete the price level
337    #[serde(rename = "a")]
338    pub asks: Vec<OrderbookLevel>,
339    /// Update ID, is a sequence. Occasionally, you'll receive "u"=1, which is a snapshot
340    /// data due to the restart of the websocket service. So please overwrite your local orderbook
341    #[serde(rename = "u")]
342    pub update_id: i64,
343    /// Cross sequence. You can use this field to compare different levels orderbook data,
344    /// and for the smaller seq, then it means the data is generated earlier
345    pub seq: i64,
346}
347
348// TODO: Use PublicMsg<T>
349#[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    /// When you receive a Buy update, this means that a long position has been liquidated
367    #[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")] // TODO: Use field topic
377pub 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    // TODO: pub topic: Topic, /// Topic name
403    /// Message ID
404    pub id: String,
405    /// Data created timestamp (ms)
406    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    /// Product type
414    /// UTA2.0, UTA1.0: spot, linear, inverse, option
415    /// Classic account: spot, linear, inverse.
416    pub category: Category,
417    /// Order ID
418    pub order_id: String,
419    /// User customized order ID
420    #[serde(default, deserialize_with = "empty_string_as_none")]
421    pub order_link_id: Option<String>,
422    /// Whether to borrow.
423    /// Unified spot only. 0: false, 1: true.
424    /// Classic spot is not supported, always 0
425    #[serde(default, deserialize_with = "string_to_option_bool")]
426    pub is_leverage: Option<bool>,
427    /// Block trade ID
428    #[serde(default, deserialize_with = "empty_string_as_none")]
429    pub block_trade_id: Option<String>,
430    /// Symbol name
431    pub symbol: String,
432    /// Order price
433    pub price: Decimal,
434    /// Dedicated field for EU liquidity provider
435    #[serde(default, deserialize_with = "option_decimal")]
436    pub broker_order_price: Option<Decimal>,
437    /// Order qty
438    pub qty: Decimal,
439    /// Side. Buy,Sell
440    pub side: Side,
441    /// Position index. Used to identify positions in different position modes.
442    pub position_idx: PositionIdx,
443    /// Order status
444    pub order_status: OrderStatus,
445    /// Order create type
446    /// Only for category=linear or inverse
447    /// Spot, Option do not have this key
448    #[serde(default, deserialize_with = "empty_string_as_none")]
449    pub create_type: Option<CreateType>,
450    /// Cancel type
451    pub cancel_type: CancelType,
452    /// Reject reason. Classic spot is not supported
453    pub reject_reason: RejectReason,
454    /// Average filled price
455    /// returns "" for those orders without avg price, and also for those classic account orders have partilly filled but cancelled at the end
456    /// Classic Spot: not supported, always ""
457    #[serde(default, deserialize_with = "option_decimal")]
458    pub avg_price: Option<Decimal>,
459    /// The remaining qty not executed. Classic spot is not supported
460    #[serde(default, deserialize_with = "option_decimal")]
461    pub leaves_qty: Option<Decimal>,
462    /// The estimated value not executed. Classic spot is not supported
463    #[serde(default, deserialize_with = "option_decimal")]
464    pub leaves_value: Option<Decimal>,
465    /// Cumulative executed order qty
466    pub cum_exec_qty: Decimal,
467    /// Cumulative executed order value.
468    pub cum_exec_value: Decimal,
469    /// Cumulative executed trading fee.
470    /// Classic spot: it is the latest execution fee for order.
471    /// After upgraded to the Unified account, you can use execFee for each fill in Execution topic
472    pub cum_exec_fee: Decimal,
473    /// linear, spot: Cumulative trading fee details instead of cumExecFee
474    pub cum_fee_detail: Option<serde_json::Value>,
475    /// Closed profit and loss for each close position order. The figure is the same as "closedPnl" from Get Closed PnL
476    pub closed_pnl: Decimal,
477    /// Trading fee currency for Spot only. Please understand Spot trading fee currency here
478    #[serde(deserialize_with = "option_decimal")]
479    pub fee_currency: Option<Decimal>,
480    /// Time in force
481    pub time_in_force: TimeInForce,
482    /// Order type. Market,Limit. For TP/SL order, it means the order type after triggered
483    pub order_type: OrderType,
484    /// Stop order type
485    #[serde(default, deserialize_with = "empty_string_as_none")]
486    pub stop_order_type: Option<StopOrderType>,
487    /// The trigger type of Spot OCO order.OcoTriggerByUnknown, OcoTriggerByTp, OcoTriggerByBySl. Classic spot is not supported
488    #[serde(default, deserialize_with = "empty_string_as_none")]
489    pub oco_trigger_by: Option<OcoTriggerBy>,
490    /// Implied volatility
491    #[serde(deserialize_with = "option_decimal")]
492    pub order_iv: Option<Decimal>,
493    /// The unit for qty when create Spot market orders for UTA account. baseCoin, quoteCoin
494    #[serde(default, deserialize_with = "empty_string_as_none")]
495    pub market_unit: Option<String>,
496    /// Spot and Futures market order slippage tolerance type TickSize, Percent, UNKNOWN(default)
497    #[serde(default, deserialize_with = "empty_string_as_none")]
498    pub slippage_tolerance_type: Option<SlippageToleranceType>,
499    /// Slippage tolerance value
500    #[serde(default, deserialize_with = "option_decimal")]
501    pub slippage_tolerance: Option<Decimal>,
502    /// Trigger price. If stopOrderType=TrailingStop, it is activate price. Otherwise, it is trigger price
503    #[serde(deserialize_with = "option_decimal")]
504    pub trigger_price: Option<Decimal>,
505    /// Take profit price
506    #[serde(deserialize_with = "option_decimal")]
507    pub take_profit: Option<Decimal>,
508    /// Stop loss price
509    #[serde(deserialize_with = "option_decimal")]
510    pub stop_loss: Option<Decimal>,
511    /// TP/SL mode, Full: entire position for TP/SL. Partial: partial position tp/sl. Spot does not have this field, and Option returns always ""
512    #[serde(default, deserialize_with = "empty_string_as_none")]
513    pub tpsl_mode: Option<TpslMode>,
514    /// The limit order price when take profit price is triggered
515    #[serde(deserialize_with = "option_decimal")]
516    pub tp_limit_price: Option<Decimal>,
517    /// The limit order price when stop loss price is triggered
518    #[serde(deserialize_with = "option_decimal")]
519    pub sl_limit_price: Option<Decimal>,
520    /// The price type to trigger take profit
521    #[serde(default, deserialize_with = "empty_string_as_none")]
522    pub tp_trigger_by: Option<TriggerBy>,
523    /// The price type to trigger stop loss
524    #[serde(default, deserialize_with = "empty_string_as_none")]
525    pub sl_trigger_by: Option<TriggerBy>,
526    /// Trigger direction. 1: rise, 2: fall
527    pub trigger_direction: TriggerDirection,
528    /// The price type of trigger price
529    #[serde(default, deserialize_with = "empty_string_as_none")]
530    pub trigger_by: Option<TriggerBy>,
531    /// Last price when place the order, Spot is not applicable
532    #[serde(deserialize_with = "option_decimal")]
533    pub last_price_on_created: Option<Decimal>,
534    /// Reduce only. true means reduce position size
535    pub reduce_only: bool,
536    /// Close on trigger.
537    pub close_on_trigger: bool,
538    /// Place type, option used. iv, price
539    #[serde(default, deserialize_with = "empty_string_as_none")]
540    pub place_type: Option<PlaceType>,
541    /// SMP execution type
542    pub smp_type: SmpType,
543    /// Smp group ID. If the UID has no group, it is 0 by default
544    #[serde(deserialize_with = "number")]
545    pub smp_group: i64,
546    /// The counterparty's orderID which triggers this SMP execution
547    #[serde(default, deserialize_with = "empty_string_as_none")]
548    pub smp_order_id: Option<String>,
549    /// Order created timestamp (ms)
550    #[serde(deserialize_with = "number")]
551    pub created_time: Timestamp,
552    /// Order updated timestamp (ms)
553    #[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    /// Product type
561    pub category: Category,
562    /// Symbol name
563    pub symbol: String,
564    /// Position side. Buy: long, Sell: short
565    /// one-way mode: classic & UTA1.0(inverse), an empty position returns None.
566    /// UTA2.0(linear, inverse) & UTA1.0(linear): either one-way or hedge mode returns an empty string "" for an empty position.
567    #[serde(default, deserialize_with = "empty_string_as_none")]
568    pub side: Option<Side>,
569    /// Position size
570    pub size: Decimal,
571    /// Used to identify positions in different position modes
572    pub position_idx: PositionIdx,
573    /// Position value
574    pub position_value: Decimal,
575    /// Risk tier ID
576    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
577    #[serde(deserialize_with = "number")]
578    pub risk_id: i64,
579    /// Risk limit value
580    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
581    #[serde(default, deserialize_with = "option_decimal")]
582    pub risk_limit_value: Option<Decimal>,
583    /// Entry price
584    pub entry_price: Decimal,
585    /// Mark price
586    pub mark_price: Decimal,
587    /// Position leverage
588    /// for portfolio margin mode, this field returns "", which means leverage rules are invalid
589    pub leverage: Decimal,
590    /// Whether to add margin automatically. 0: false, 1: true. For UTA, it is meaningful only when UTA enables ISOLATED_MARGIN
591    #[serde(default, deserialize_with = "int_to_bool")]
592    pub auto_add_margin: bool,
593    /// Initial margin, the same value as positionIMByMp, please note this change The New Margin Calculation: Adjustments and Implications
594    /// Portfolio margin mode: returns ""
595    #[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
596    pub position_im: Option<Decimal>,
597    /// Maintenance margin, the same value as positionMMByMp
598    /// Portfolio margin mode: returns ""
599    #[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
600    pub position_mm: Option<Decimal>,
601    /// Initial margin calculated by mark price, the same value as positionIM
602    /// Portfolio margin mode: returns ""
603    #[serde(
604        rename = "positionIMByMp",
605        default,
606        deserialize_with = "option_decimal"
607    )]
608    pub position_im_by_mp: Option<Decimal>,
609    /// Maintenance margin calculated by mark price, the same value as positionMM
610    /// Portfolio margin mode: returns ""
611    #[serde(
612        rename = "positionMMByMp",
613        default,
614        deserialize_with = "option_decimal"
615    )]
616    pub position_mm_by_mp: Option<Decimal>,
617    /// Position liquidation price
618    /// Isolated margin:
619    /// it is the real price for isolated and cross positions, and keeps "" when liqPrice <= minPrice or liqPrice >= maxPrice
620    /// Cross margin:
621    /// it is an estimated price for cross positions(because the unified mode controls the risk rate according to the account), and keeps "" when liqPrice <= minPrice or liqPrice >= maxPrice
622    /// this field is empty for Portfolio Margin Mode, and no liquidation price will be provided
623    #[serde(default, deserialize_with = "option_decimal")]
624    pub liq_price: Option<Decimal>,
625    /// Take profit price
626    pub take_profit: Decimal,
627    /// Stop loss price
628    pub stop_loss: Decimal,
629    /// Trailing stop
630    pub trailing_stop: Decimal,
631    /// Unrealised profit and loss
632    pub unrealised_pnl: Decimal,
633    /// The realised PnL for the current holding position
634    pub cur_realised_pnl: Decimal,
635    /// USDC contract session avg price, it is the same figure as avg entry price shown in the web UI
636    #[serde(default, deserialize_with = "option_decimal")]
637    pub session_avg_price: Option<Decimal>,
638    /// Delta
639    #[serde(default, deserialize_with = "empty_string_as_none")]
640    pub delta: Option<String>,
641    /// Gamma
642    #[serde(default, deserialize_with = "empty_string_as_none")]
643    pub gamma: Option<String>,
644    /// Vega
645    #[serde(default, deserialize_with = "empty_string_as_none")]
646    pub vega: Option<String>,
647    /// Theta
648    #[serde(default, deserialize_with = "empty_string_as_none")]
649    pub theta: Option<String>,
650    /// Cumulative realised pnl
651    /// Futures & Perp: it is the all time cumulative realised P&L
652    /// Option: it is the realised P&L when you hold that position
653    pub cum_realised_pnl: Decimal,
654    /// Position status. Normal, Liq, Adl
655    pub position_status: PositionStatus,
656    /// Auto-deleverage rank indicator. What is Auto-Deleveraging?
657    pub adl_rank_indicator: AdlRankIndicator,
658    /// Useful when Bybit lower the risk limit
659    /// true: Only allowed to reduce the position. You can consider a series of measures, e.g., lower the risk limit, decrease leverage or reduce the position, add margin, or cancel orders, after these operations, you can call confirm new risk limit endpoint to check if your position can be removed the reduceOnly mark
660    /// false: There is no restriction, and it means your position is under the risk when the risk limit is systematically adjusted
661    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
662    pub is_reduce_only: bool,
663    /// Useful when Bybit lower the risk limit
664    /// When isReduceOnly=true: the timestamp (ms) when the MMR will be forcibly adjusted by the system
665    /// When isReduceOnly=false: the timestamp when the MMR had been adjusted by system
666    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
667    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
668    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
669    #[serde(deserialize_with = "option_number")]
670    pub mmr_sys_updated_time: Option<Timestamp>,
671    /// Useful when Bybit lower the risk limit
672    /// When isReduceOnly=true: the timestamp (ms) when the leverage will be forcibly adjusted by the system
673    /// When isReduceOnly=false: the timestamp when the leverage had been adjusted by system
674    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
675    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
676    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
677    #[serde(deserialize_with = "option_number")]
678    pub leverage_sys_updated_time: Option<Timestamp>,
679    /// Timestamp of the first time a position was created on this symbol (ms)
680    #[serde(deserialize_with = "number")]
681    pub created_time: Timestamp,
682    /// Position data updated timestamp (ms)
683    #[serde(deserialize_with = "number")]
684    pub updated_time: Timestamp,
685    /// Cross sequence, used to associate each fill and each position update
686    /// Different symbols may have the same seq, please use seq + symbol to check unique
687    /// Returns "-1" if the symbol has never been traded
688    /// Returns the seq updated by the last transaction when there are setting like leverage, risk limit
689    pub seq: i64,
690}
691
692#[derive(PartialEq, Deserialize, Debug, Clone)]
693#[serde(rename_all = "camelCase")]
694pub struct WalletMsg {
695    /// Account type.
696    /// UTA2.0: UNIFIED
697    /// UTA1.0: UNIFIED (spot/linear/options), CONTRACT(inverse)
698    /// Classic: CONTRACT, SPOT
699    pub account_type: AccountType,
700    /// Account IM rate
701    /// You can refer to this Glossary to understand the below fields calculation and mearning
702    /// All below account wide fields are not applicable to
703    /// UTA2.0(isolated margin),
704    /// UTA1.0(isolated margin), UTA1.0(CONTRACT),
705    /// classic account(SPOT, CONTRACT)
706    #[serde(rename = "accountIMRate")]
707    pub account_im_rate: Decimal,
708    /// Account MM rate
709    #[serde(rename = "accountMMRate")]
710    pub account_mm_rate: Decimal,
711    /// Account total equity (USD)
712    pub total_equity: Decimal,
713    /// Account wallet balance (USD): ∑Asset Wallet Balance By USD value of each asset
714    pub total_wallet_balance: Decimal,
715    /// Account margin balance (USD): totalWalletBalance + totalPerpUPL
716    pub total_margin_balance: Decimal,
717    /// Account available balance (USD), Cross Margin: totalMarginBalance - totalInitialMargin
718    pub total_available_balance: Decimal,
719    /// Account Perps and Futures unrealised p&l (USD): ∑Each Perp and USDC Futures upl by base coin
720    #[serde(rename = "totalPerpUPL")]
721    pub total_perp_upl: Decimal,
722    /// Account initial margin (USD): ∑Asset Total Initial Margin Base Coin
723    pub total_initial_margin: Decimal,
724    /// Account maintenance margin (USD): ∑ Asset Total Maintenance Margin Base Coin
725    pub total_maintenance_margin: Decimal,
726    /// You can ignore this field, and refer to accountIMRate, which has the same calculation
727    #[serde(rename = "accountIMRateByMp")]
728    pub account_im_rate_by_mp: Decimal,
729    /// You can ignore this field, and refer to accountMMRate, which has the same calculation
730    #[serde(rename = "accountMMRateByMp")]
731    pub account_mm_rate_by_mp: Decimal,
732    /// You can ignore this field, and refer to totalInitialMargin, which has the same calculation
733    #[serde(rename = "totalInitialMarginByMp")]
734    pub total_initial_margin_by_mp: Decimal,
735    /// You can ignore this field, and refer to totalMaintenanceMargin, which has the same calculation
736    #[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    /// Product type spot, linear, inverse, option
746    pub category: Category,
747    /// Symbol name
748    pub symbol: String,
749    /// Whether to borrow. 0: false, 1: true
750    #[serde(default, deserialize_with = "string_to_bool")]
751    pub is_leverage: bool,
752    /// Order ID
753    pub order_id: String,
754    /// User customized order ID
755    #[serde(default, deserialize_with = "empty_string_as_none")]
756    pub order_link_id: Option<String>,
757    /// Side. Buy,Sell
758    pub side: Side,
759    /// Order price
760    pub order_price: Decimal,
761    /// Order qty
762    pub order_qty: Decimal,
763    /// The remaining qty not executed
764    pub leaves_qty: Decimal,
765    /// Order create type
766    /// Spot, Option do not have this key
767    pub create_type: CreateType,
768    /// Order type. Market,Limit
769    pub order_type: OrderType,
770    /// Stop order type. If the order is not stop order, any type is not returned
771    pub stop_order_type: StopOrderType,
772    /// Executed trading fee. You can get spot fee currency instruction here
773    pub exec_fee: Decimal,
774    /// Execution ID
775    pub exec_id: String,
776    /// Execution price
777    pub exec_price: Decimal,
778    /// Execution qty
779    pub exec_qty: Decimal,
780    /// Profit and Loss for each close position execution. The value keeps consistent with the field "cashFlow" in the Get Transaction Log
781    pub exec_pnl: Decimal,
782    /// Executed type
783    pub exec_type: ExecType,
784    /// Executed order value
785    pub exec_value: Decimal,
786    /// Executed timestamp (ms)
787    #[serde(deserialize_with = "number")]
788    pub exec_time: Timestamp,
789    /// Is maker order. true: maker, false: taker
790    pub is_maker: bool,
791    /// Trading fee rate
792    pub fee_rate: Decimal,
793    /// Implied volatility. valid for option
794    #[serde(default, deserialize_with = "option_decimal")]
795    pub trade_iv: Option<Decimal>,
796    /// Implied volatility of mark price. valid for option
797    #[serde(default, deserialize_with = "option_decimal")]
798    pub mark_iv: Option<Decimal>,
799    /// The mark price of the symbol when executing. valid for option
800    pub mark_price: Decimal,
801    /// The index price of the symbol when executing. valid for option
802    #[serde(default, deserialize_with = "option_decimal")]
803    pub index_price: Option<Decimal>,
804    /// The underlying price of the symbol when executing. valid for option
805    #[serde(default, deserialize_with = "option_decimal")]
806    pub underlying_price: Option<Decimal>,
807    /// Paradigm block trade ID
808    #[serde(default, deserialize_with = "empty_string_as_none")]
809    pub block_trade_id: Option<String>,
810    /// Closed position size
811    pub closed_size: Decimal,
812    /// Extra trading fee information. Currently, this data is returned only for kyc=Indian user or spot orders placed on the Indonesian site or spot fiat currency orders placed on the EU site. In other cases, an empty string is returned. Enum: feeType, subFeeType
813    pub extra_fees: Option<Vec<ExtraFee>>, // TODO: !!! ignore if empty string !!!
814    /// Cross sequence, used to associate each fill and each position update
815    /// The seq will be the same when conclude multiple transactions at the same time
816    /// Different symbols may have the same seq, please use seq + symbol to check unique
817    pub seq: i64,
818    /// Trading fee currency
819    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        // Category: linear.
935        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        // Category: linear.
1010        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        // Category: linear.
1056        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        // Category: linear.
1117        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        // Category: linear.
1183        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, // "smpGroup":0,"smpType":"None","smpOrderId":"",
1454                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}