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    /// Fast execution: fills arrive immediately, before settlement completes.
387    #[serde(rename = "fastExecution")]
388    FastExecution(PrivateMsg<Vec<FastExecutionMsg>>),
389    /// Option greek updates.
390    #[serde(rename = "greeks")]
391    Greeks(PrivateMsg<Vec<GreekMsg>>),
392    /// Disconnection and Cancellation Protection status update.
393    #[serde(rename = "dcp")]
394    Dcp(PrivateMsg<DcpMsg>),
395}
396
397#[derive(PartialEq, Deserialize, Debug)]
398#[serde(rename_all = "camelCase")]
399pub struct PublicMsg<T> {
400    #[serde(default, deserialize_with = "empty_string_as_none")]
401    id: Option<String>,
402    #[serde(default, deserialize_with = "option_number")]
403    cs: Option<u64>,
404    ts: Timestamp,
405    data: T,
406}
407
408#[derive(PartialEq, Deserialize, Debug)]
409#[serde(rename_all = "camelCase")]
410pub struct PrivateMsg<T> {
411    // TODO: pub topic: Topic, /// Topic name
412    /// Message ID
413    pub id: String,
414    /// Data created timestamp (ms)
415    pub creation_time: Timestamp,
416    pub data: T,
417}
418
419#[derive(PartialEq, Deserialize, Debug, Clone)]
420#[serde(rename_all = "camelCase")]
421pub struct OrderMsg {
422    /// Product type
423    /// UTA2.0, UTA1.0: spot, linear, inverse, option
424    /// Classic account: spot, linear, inverse.
425    pub category: Category,
426    /// Order ID
427    pub order_id: String,
428    /// User customized order ID
429    #[serde(default, deserialize_with = "empty_string_as_none")]
430    pub order_link_id: Option<String>,
431    /// Whether to borrow.
432    /// Unified spot only. 0: false, 1: true.
433    /// Classic spot is not supported, always 0
434    #[serde(default, deserialize_with = "string_to_option_bool")]
435    pub is_leverage: Option<bool>,
436    /// Block trade ID
437    #[serde(default, deserialize_with = "empty_string_as_none")]
438    pub block_trade_id: Option<String>,
439    /// Symbol name
440    pub symbol: String,
441    /// Order price
442    pub price: Decimal,
443    /// Dedicated field for EU liquidity provider
444    #[serde(default, deserialize_with = "option_decimal")]
445    pub broker_order_price: Option<Decimal>,
446    /// Order qty
447    pub qty: Decimal,
448    /// Side. Buy,Sell
449    pub side: Side,
450    /// Position index. Used to identify positions in different position modes.
451    pub position_idx: PositionIdx,
452    /// Order status
453    pub order_status: OrderStatus,
454    /// Order create type
455    /// Only for category=linear or inverse
456    /// Spot, Option do not have this key
457    #[serde(default, deserialize_with = "empty_string_as_none")]
458    pub create_type: Option<CreateType>,
459    /// Cancel type
460    pub cancel_type: CancelType,
461    /// Reject reason. Classic spot is not supported
462    pub reject_reason: RejectReason,
463    /// Average filled price
464    /// returns "" for those orders without avg price, and also for those classic account orders have partilly filled but cancelled at the end
465    /// Classic Spot: not supported, always ""
466    #[serde(default, deserialize_with = "option_decimal")]
467    pub avg_price: Option<Decimal>,
468    /// The remaining qty not executed. Classic spot is not supported
469    #[serde(default, deserialize_with = "option_decimal")]
470    pub leaves_qty: Option<Decimal>,
471    /// The estimated value not executed. Classic spot is not supported
472    #[serde(default, deserialize_with = "option_decimal")]
473    pub leaves_value: Option<Decimal>,
474    /// Cumulative executed order qty
475    pub cum_exec_qty: Decimal,
476    /// Cumulative executed order value.
477    pub cum_exec_value: Decimal,
478    /// Cumulative executed trading fee.
479    /// Classic spot: it is the latest execution fee for order.
480    /// After upgraded to the Unified account, you can use execFee for each fill in Execution topic
481    pub cum_exec_fee: Decimal,
482    /// linear, spot: Cumulative trading fee details instead of cumExecFee
483    pub cum_fee_detail: Option<serde_json::Value>,
484    /// Closed profit and loss for each close position order. The figure is the same as "closedPnl" from Get Closed PnL
485    pub closed_pnl: Decimal,
486    /// Trading fee currency for Spot only. Please understand Spot trading fee currency here
487    #[serde(deserialize_with = "option_decimal")]
488    pub fee_currency: Option<Decimal>,
489    /// Time in force
490    pub time_in_force: TimeInForce,
491    /// Order type. Market,Limit. For TP/SL order, it means the order type after triggered
492    pub order_type: OrderType,
493    /// Stop order type
494    #[serde(default, deserialize_with = "empty_string_as_none")]
495    pub stop_order_type: Option<StopOrderType>,
496    /// The trigger type of Spot OCO order.OcoTriggerByUnknown, OcoTriggerByTp, OcoTriggerByBySl. Classic spot is not supported
497    #[serde(default, deserialize_with = "empty_string_as_none")]
498    pub oco_trigger_by: Option<OcoTriggerBy>,
499    /// Implied volatility
500    #[serde(deserialize_with = "option_decimal")]
501    pub order_iv: Option<Decimal>,
502    /// The unit for qty when create Spot market orders for UTA account. baseCoin, quoteCoin
503    #[serde(default, deserialize_with = "empty_string_as_none")]
504    pub market_unit: Option<String>,
505    /// Spot and Futures market order slippage tolerance type TickSize, Percent, UNKNOWN(default)
506    #[serde(default, deserialize_with = "empty_string_as_none")]
507    pub slippage_tolerance_type: Option<SlippageToleranceType>,
508    /// Slippage tolerance value
509    #[serde(default, deserialize_with = "option_decimal")]
510    pub slippage_tolerance: Option<Decimal>,
511    /// Trigger price. If stopOrderType=TrailingStop, it is activate price. Otherwise, it is trigger price
512    #[serde(deserialize_with = "option_decimal")]
513    pub trigger_price: Option<Decimal>,
514    /// Take profit price
515    #[serde(deserialize_with = "option_decimal")]
516    pub take_profit: Option<Decimal>,
517    /// Stop loss price
518    #[serde(deserialize_with = "option_decimal")]
519    pub stop_loss: Option<Decimal>,
520    /// TP/SL mode, Full: entire position for TP/SL. Partial: partial position tp/sl. Spot does not have this field, and Option returns always ""
521    #[serde(default, deserialize_with = "empty_string_as_none")]
522    pub tpsl_mode: Option<TpslMode>,
523    /// The limit order price when take profit price is triggered
524    #[serde(deserialize_with = "option_decimal")]
525    pub tp_limit_price: Option<Decimal>,
526    /// The limit order price when stop loss price is triggered
527    #[serde(deserialize_with = "option_decimal")]
528    pub sl_limit_price: Option<Decimal>,
529    /// The price type to trigger take profit
530    #[serde(default, deserialize_with = "empty_string_as_none")]
531    pub tp_trigger_by: Option<TriggerBy>,
532    /// The price type to trigger stop loss
533    #[serde(default, deserialize_with = "empty_string_as_none")]
534    pub sl_trigger_by: Option<TriggerBy>,
535    /// Trigger direction. 1: rise, 2: fall
536    pub trigger_direction: TriggerDirection,
537    /// The price type of trigger price
538    #[serde(default, deserialize_with = "empty_string_as_none")]
539    pub trigger_by: Option<TriggerBy>,
540    /// Last price when place the order, Spot is not applicable
541    #[serde(deserialize_with = "option_decimal")]
542    pub last_price_on_created: Option<Decimal>,
543    /// Reduce only. true means reduce position size
544    pub reduce_only: bool,
545    /// Close on trigger.
546    pub close_on_trigger: bool,
547    /// Place type, option used. iv, price
548    #[serde(default, deserialize_with = "empty_string_as_none")]
549    pub place_type: Option<PlaceType>,
550    /// SMP execution type
551    pub smp_type: SmpType,
552    /// Smp group ID. If the UID has no group, it is 0 by default
553    #[serde(deserialize_with = "number")]
554    pub smp_group: i64,
555    /// The counterparty's orderID which triggers this SMP execution
556    #[serde(default, deserialize_with = "empty_string_as_none")]
557    pub smp_order_id: Option<String>,
558    /// Order created timestamp (ms)
559    #[serde(deserialize_with = "number")]
560    pub created_time: Timestamp,
561    /// Order updated timestamp (ms)
562    #[serde(deserialize_with = "number")]
563    pub updated_time: Timestamp,
564}
565
566#[derive(PartialEq, Deserialize, Debug, Clone)]
567#[serde(rename_all = "camelCase")]
568pub struct PositionMsg {
569    /// Product type
570    pub category: Category,
571    /// Symbol name
572    pub symbol: String,
573    /// Position side. Buy: long, Sell: short
574    /// one-way mode: classic & UTA1.0(inverse), an empty position returns None.
575    /// UTA2.0(linear, inverse) & UTA1.0(linear): either one-way or hedge mode returns an empty string "" for an empty position.
576    #[serde(default, deserialize_with = "empty_string_as_none")]
577    pub side: Option<Side>,
578    /// Position size
579    pub size: Decimal,
580    /// Used to identify positions in different position modes
581    pub position_idx: PositionIdx,
582    /// Position value
583    pub position_value: Decimal,
584    /// Risk tier ID
585    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
586    #[serde(deserialize_with = "number")]
587    pub risk_id: i64,
588    /// Risk limit value
589    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
590    #[serde(default, deserialize_with = "option_decimal")]
591    pub risk_limit_value: Option<Decimal>,
592    /// Entry price
593    pub entry_price: Decimal,
594    /// Mark price
595    pub mark_price: Decimal,
596    /// Position leverage
597    /// for portfolio margin mode, this field returns "", which means leverage rules are invalid
598    pub leverage: Decimal,
599    /// Whether to add margin automatically. 0: false, 1: true. For UTA, it is meaningful only when UTA enables ISOLATED_MARGIN
600    #[serde(default, deserialize_with = "int_to_bool")]
601    pub auto_add_margin: bool,
602    /// Initial margin, the same value as positionIMByMp, please note this change The New Margin Calculation: Adjustments and Implications
603    /// Portfolio margin mode: returns ""
604    #[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
605    pub position_im: Option<Decimal>,
606    /// Maintenance margin, the same value as positionMMByMp
607    /// Portfolio margin mode: returns ""
608    #[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
609    pub position_mm: Option<Decimal>,
610    /// Initial margin calculated by mark price, the same value as positionIM
611    /// Portfolio margin mode: returns ""
612    #[serde(
613        rename = "positionIMByMp",
614        default,
615        deserialize_with = "option_decimal"
616    )]
617    pub position_im_by_mp: Option<Decimal>,
618    /// Maintenance margin calculated by mark price, the same value as positionMM
619    /// Portfolio margin mode: returns ""
620    #[serde(
621        rename = "positionMMByMp",
622        default,
623        deserialize_with = "option_decimal"
624    )]
625    pub position_mm_by_mp: Option<Decimal>,
626    /// Position liquidation price
627    /// Isolated margin:
628    /// it is the real price for isolated and cross positions, and keeps "" when liqPrice <= minPrice or liqPrice >= maxPrice
629    /// Cross margin:
630    /// 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
631    /// this field is empty for Portfolio Margin Mode, and no liquidation price will be provided
632    #[serde(default, deserialize_with = "option_decimal")]
633    pub liq_price: Option<Decimal>,
634    /// Take profit price
635    pub take_profit: Decimal,
636    /// Stop loss price
637    pub stop_loss: Decimal,
638    /// Trailing stop
639    pub trailing_stop: Decimal,
640    /// Unrealised profit and loss
641    pub unrealised_pnl: Decimal,
642    /// The realised PnL for the current holding position
643    pub cur_realised_pnl: Decimal,
644    /// USDC contract session avg price, it is the same figure as avg entry price shown in the web UI
645    #[serde(default, deserialize_with = "option_decimal")]
646    pub session_avg_price: Option<Decimal>,
647    /// Delta
648    #[serde(default, deserialize_with = "empty_string_as_none")]
649    pub delta: Option<String>,
650    /// Gamma
651    #[serde(default, deserialize_with = "empty_string_as_none")]
652    pub gamma: Option<String>,
653    /// Vega
654    #[serde(default, deserialize_with = "empty_string_as_none")]
655    pub vega: Option<String>,
656    /// Theta
657    #[serde(default, deserialize_with = "empty_string_as_none")]
658    pub theta: Option<String>,
659    /// Cumulative realised pnl
660    /// Futures & Perp: it is the all time cumulative realised P&L
661    /// Option: it is the realised P&L when you hold that position
662    pub cum_realised_pnl: Decimal,
663    /// Position status. Normal, Liq, Adl
664    pub position_status: PositionStatus,
665    /// Auto-deleverage rank indicator. What is Auto-Deleveraging?
666    pub adl_rank_indicator: AdlRankIndicator,
667    /// Useful when Bybit lower the risk limit
668    /// 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
669    /// false: There is no restriction, and it means your position is under the risk when the risk limit is systematically adjusted
670    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
671    pub is_reduce_only: bool,
672    /// Useful when Bybit lower the risk limit
673    /// When isReduceOnly=true: the timestamp (ms) when the MMR will be forcibly adjusted by the system
674    /// When isReduceOnly=false: the timestamp when the MMR had been adjusted by system
675    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
676    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
677    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
678    #[serde(deserialize_with = "option_number")]
679    pub mmr_sys_updated_time: Option<Timestamp>,
680    /// Useful when Bybit lower the risk limit
681    /// When isReduceOnly=true: the timestamp (ms) when the leverage will be forcibly adjusted by the system
682    /// When isReduceOnly=false: the timestamp when the leverage had been adjusted by system
683    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
684    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
685    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
686    #[serde(deserialize_with = "option_number")]
687    pub leverage_sys_updated_time: Option<Timestamp>,
688    /// Timestamp of the first time a position was created on this symbol (ms)
689    #[serde(deserialize_with = "number")]
690    pub created_time: Timestamp,
691    /// Position data updated timestamp (ms)
692    #[serde(deserialize_with = "number")]
693    pub updated_time: Timestamp,
694    /// Cross sequence, used to associate each fill and each position update
695    /// Different symbols may have the same seq, please use seq + symbol to check unique
696    /// Returns "-1" if the symbol has never been traded
697    /// Returns the seq updated by the last transaction when there are setting like leverage, risk limit
698    pub seq: i64,
699}
700
701#[derive(PartialEq, Deserialize, Debug, Clone)]
702#[serde(rename_all = "camelCase")]
703pub struct WalletMsg {
704    /// Account type.
705    /// UTA2.0: UNIFIED
706    /// UTA1.0: UNIFIED (spot/linear/options), CONTRACT(inverse)
707    /// Classic: CONTRACT, SPOT
708    pub account_type: AccountType,
709    /// Account IM rate
710    /// You can refer to this Glossary to understand the below fields calculation and mearning
711    /// All below account wide fields are not applicable to
712    /// UTA2.0(isolated margin),
713    /// UTA1.0(isolated margin), UTA1.0(CONTRACT),
714    /// classic account(SPOT, CONTRACT)
715    #[serde(rename = "accountIMRate")]
716    pub account_im_rate: Decimal,
717    /// Account MM rate
718    #[serde(rename = "accountMMRate")]
719    pub account_mm_rate: Decimal,
720    /// Account total equity (USD)
721    pub total_equity: Decimal,
722    /// Account wallet balance (USD): ∑Asset Wallet Balance By USD value of each asset
723    pub total_wallet_balance: Decimal,
724    /// Account margin balance (USD): totalWalletBalance + totalPerpUPL
725    pub total_margin_balance: Decimal,
726    /// Account available balance (USD), Cross Margin: totalMarginBalance - totalInitialMargin
727    pub total_available_balance: Decimal,
728    /// Account Perps and Futures unrealised p&l (USD): ∑Each Perp and USDC Futures upl by base coin
729    #[serde(rename = "totalPerpUPL")]
730    pub total_perp_upl: Decimal,
731    /// Account initial margin (USD): ∑Asset Total Initial Margin Base Coin
732    pub total_initial_margin: Decimal,
733    /// Account maintenance margin (USD): ∑ Asset Total Maintenance Margin Base Coin
734    pub total_maintenance_margin: Decimal,
735    /// You can ignore this field, and refer to accountIMRate, which has the same calculation
736    #[serde(rename = "accountIMRateByMp")]
737    pub account_im_rate_by_mp: Decimal,
738    /// You can ignore this field, and refer to accountMMRate, which has the same calculation
739    #[serde(rename = "accountMMRateByMp")]
740    pub account_mm_rate_by_mp: Decimal,
741    /// You can ignore this field, and refer to totalInitialMargin, which has the same calculation
742    #[serde(rename = "totalInitialMarginByMp")]
743    pub total_initial_margin_by_mp: Decimal,
744    /// You can ignore this field, and refer to totalMaintenanceMargin, which has the same calculation
745    #[serde(rename = "totalMaintenanceMarginByMp")]
746    pub total_maintenance_margin_by_mp: Decimal,
747    #[serde(deserialize_with = "hash_map")]
748    pub coin: HashMap<String, WalletCoin>,
749}
750
751#[derive(PartialEq, Deserialize, Debug, Clone)]
752#[serde(rename_all = "camelCase")]
753pub struct ExecutionMsg {
754    /// Product type spot, linear, inverse, option
755    pub category: Category,
756    /// Symbol name
757    pub symbol: String,
758    /// Whether to borrow. 0: false, 1: true
759    #[serde(default, deserialize_with = "string_to_bool")]
760    pub is_leverage: bool,
761    /// Order ID
762    pub order_id: String,
763    /// User customized order ID
764    #[serde(default, deserialize_with = "empty_string_as_none")]
765    pub order_link_id: Option<String>,
766    /// Side. Buy,Sell
767    pub side: Side,
768    /// Order price
769    pub order_price: Decimal,
770    /// Order qty
771    pub order_qty: Decimal,
772    /// The remaining qty not executed
773    pub leaves_qty: Decimal,
774    /// Order create type
775    /// Spot, Option do not have this key
776    pub create_type: CreateType,
777    /// Order type. Market,Limit
778    pub order_type: OrderType,
779    /// Stop order type. If the order is not stop order, any type is not returned
780    pub stop_order_type: StopOrderType,
781    /// Executed trading fee. You can get spot fee currency instruction here
782    pub exec_fee: Decimal,
783    /// Execution ID
784    pub exec_id: String,
785    /// Execution price
786    pub exec_price: Decimal,
787    /// Execution qty
788    pub exec_qty: Decimal,
789    /// Profit and Loss for each close position execution. The value keeps consistent with the field "cashFlow" in the Get Transaction Log
790    pub exec_pnl: Decimal,
791    /// Executed type
792    pub exec_type: ExecType,
793    /// Executed order value
794    pub exec_value: Decimal,
795    /// Executed timestamp (ms)
796    #[serde(deserialize_with = "number")]
797    pub exec_time: Timestamp,
798    /// Is maker order. true: maker, false: taker
799    pub is_maker: bool,
800    /// Trading fee rate
801    pub fee_rate: Decimal,
802    /// Implied volatility. valid for option
803    #[serde(default, deserialize_with = "option_decimal")]
804    pub trade_iv: Option<Decimal>,
805    /// Implied volatility of mark price. valid for option
806    #[serde(default, deserialize_with = "option_decimal")]
807    pub mark_iv: Option<Decimal>,
808    /// The mark price of the symbol when executing. valid for option
809    pub mark_price: Decimal,
810    /// The index price of the symbol when executing. valid for option
811    #[serde(default, deserialize_with = "option_decimal")]
812    pub index_price: Option<Decimal>,
813    /// The underlying price of the symbol when executing. valid for option
814    #[serde(default, deserialize_with = "option_decimal")]
815    pub underlying_price: Option<Decimal>,
816    /// Paradigm block trade ID
817    #[serde(default, deserialize_with = "empty_string_as_none")]
818    pub block_trade_id: Option<String>,
819    /// Closed position size
820    pub closed_size: Decimal,
821    /// 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
822    pub extra_fees: Option<Vec<ExtraFee>>, // TODO: !!! ignore if empty string !!!
823    /// Cross sequence, used to associate each fill and each position update
824    /// The seq will be the same when conclude multiple transactions at the same time
825    /// Different symbols may have the same seq, please use seq + symbol to check unique
826    pub seq: i64,
827    /// Trading fee currency
828    pub fee_currency: String,
829}
830
831#[derive(PartialEq, Deserialize, Debug, Clone)]
832#[serde(rename_all = "camelCase")]
833pub struct ExtraFee {
834    pub fee_coin: String,
835    pub fee_type: ExtraFeeType,
836    pub sub_fee_type: ExtraSubFeeType,
837    pub fee_rate: Decimal,
838    pub fee: Decimal,
839}
840
841/// A fill from the `fastExecution` private WS topic.
842///
843/// This is a condensed view of an execution that arrives immediately when an
844/// order is matched, before the normal settlement cycle that drives
845/// [`ExecutionMsg`]. The field set is smaller than [`ExecutionMsg`].
846#[derive(PartialEq, Deserialize, Debug, Clone)]
847#[serde(rename_all = "camelCase")]
848pub struct FastExecutionMsg {
849    pub category: Category,
850    pub symbol: String,
851    pub exec_id: String,
852    pub exec_price: Decimal,
853    pub exec_qty: Decimal,
854    pub side: Side,
855    pub order_qty: Decimal,
856    pub order_id: String,
857    #[serde(default, deserialize_with = "empty_string_as_none")]
858    pub order_link_id: Option<String>,
859    pub is_maker: bool,
860    pub fee_currency: String,
861    pub fee_rate: Decimal,
862    pub exec_fee: Decimal,
863    #[serde(default, deserialize_with = "option_decimal")]
864    pub trade_iv: Option<Decimal>,
865    #[serde(default, deserialize_with = "option_decimal")]
866    pub mark_iv: Option<Decimal>,
867    pub mark_price: Decimal,
868    #[serde(default, deserialize_with = "option_decimal")]
869    pub index_price: Option<Decimal>,
870    #[serde(default, deserialize_with = "option_decimal")]
871    pub underlying_price: Option<Decimal>,
872    #[serde(default, deserialize_with = "empty_string_as_none")]
873    pub block_trade_id: Option<String>,
874    #[serde(deserialize_with = "number")]
875    pub exec_time: Timestamp,
876}
877
878/// A data point from the `greeks` private WS topic.
879///
880/// Sent once per second when there is an option position. Contains the
881/// aggregate net greeks across all option positions for the base coin.
882#[derive(PartialEq, Deserialize, Debug, Clone)]
883#[serde(rename_all = "camelCase")]
884pub struct GreekMsg {
885    pub base_coin: String,
886    pub total_delta: Decimal,
887    pub total_gamma: Decimal,
888    pub total_vega: Decimal,
889    pub total_theta: Decimal,
890}
891
892/// Data from the `dcp` private WS topic (Disconnection and Cancellation
893/// Protection status update).
894#[derive(PartialEq, Deserialize, Debug, Clone)]
895#[serde(rename_all = "camelCase")]
896pub struct DcpMsg {
897    /// Product class affected: e.g. `"OPTIONS"`, `"DERIVATIVES"`.
898    pub product: String,
899    /// Current DCP status. `"ON"` or `"OFF"`.
900    pub dcp_status: String,
901    /// The time window (seconds) configured for DCP.
902    pub time_window: u64,
903    /// Timestamp of the last status change (ms).
904    #[serde(deserialize_with = "number")]
905    pub updated_at: Timestamp,
906}
907
908#[cfg(test)]
909mod tests {
910    use rust_decimal::dec;
911
912    use crate::DepthLevel;
913    use crate::serde::{Unique, deserialize_json};
914
915    use super::*;
916
917    #[test]
918    fn deserialize_incoming_message_command_subscribe() {
919        let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"subscribe"}"#;
920        let expected = IncomingMessage::Command(CommandMsg::Subscribe {
921            req_id: None,
922            ret_msg: None,
923            conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
924            success: true,
925        });
926
927        let message = deserialize_json(json).unwrap();
928
929        assert_eq!(expected, message);
930    }
931
932    #[test]
933    fn deserialize_incoming_message_command_unsubscribe() {
934        let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"unsubscribe"}"#;
935        let expected = IncomingMessage::Command(CommandMsg::Unsubscribe {
936            req_id: None,
937            ret_msg: None,
938            conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
939            success: true,
940        });
941
942        let message = deserialize_json(json).unwrap();
943
944        assert_eq!(expected, message);
945    }
946
947    #[test]
948    fn deserialize_incoming_message_ticker_delta() {
949        let json = r#"{
950		    "topic": "tickers.BTCUSDT",
951		    "type": "delta",
952		    "data": {
953		        "symbol": "BTCUSDT",
954		        "tickDirection": "PlusTick",
955		        "price24hPcnt": "-0.015895",
956		        "lastPrice": "63948.50",
957		        "turnover24h": "6793884423.5518",
958		        "volume24h": "105991.3760",
959		        "bid1Price": "63948.40",
960		        "bid1Size": "3.439",
961		        "ask1Price": "63948.50",
962		        "ask1Size": "2.566"
963		    },
964		    "cs": 195377749067,
965		    "ts": 1718995014034
966		}"#;
967        let ticker_delta = TickerMsg::Delta {
968            topic: Topic::Ticker(String::from("BTCUSDT")),
969            cs: Some(195377749067),
970            ts: 1718995014034,
971            data: TickerDeltaMsg {
972                symbol: String::from("BTCUSDT"),
973                tick_direction: Some(TickDirection::PlusTick),
974                last_price: Some(dec!(63948.5)),
975                pre_open_price: None,
976                pre_qty: None,
977                cur_pre_listing_phase: None,
978                prev_price24h: None,
979                price24h_pcnt: Some(dec!(-0.015895)),
980                high_price24h: None,
981                low_price24h: None,
982                prev_price1h: None,
983                mark_price: None,
984                index_price: None,
985                open_interest: None,
986                open_interest_value: None,
987                turnover24h: Some(dec!(6793884423.5518)),
988                volume24h: Some(dec!(105991.376)),
989                funding_rate: None,
990                next_funding_time: None,
991                bid1_price: Some(dec!(63948.4)),
992                bid1_size: Some(dec!(3.439)),
993                ask1_price: Some(dec!(63948.5)),
994                ask1_size: Some(dec!(2.566)),
995                delivery_time: None,
996                basis_rate: None,
997                delivery_fee_rate: None,
998                predicted_delivery_price: None,
999            },
1000        };
1001        let expected = IncomingMessage::Ticker(Box::new(ticker_delta));
1002
1003        let message = deserialize_json(json).unwrap();
1004
1005        assert_eq!(expected, message);
1006    }
1007
1008    #[test]
1009    fn deserialize_incoming_message_ticker_snapshot() {
1010        // Category: linear.
1011        let json = r#"{
1012		    "topic": "tickers.BTCUSDT",
1013		    "type": "snapshot",
1014		    "data": {
1015                "symbol":"BTCUSDT",
1016                "tickDirection":"ZeroPlusTick",
1017                "price24hPcnt":"-0.044555",
1018                "lastPrice":"84594.40",
1019                "prevPrice24h":"88539.30",
1020                "highPrice24h":"89389.90",
1021                "lowPrice24h":"82055.60",
1022                "prevPrice1h":"84307.20",
1023                "markPrice":"84594.00",
1024                "indexPrice":"84650.47",
1025                "openInterest":"52903.75",
1026                "openInterestValue":"4475339827.50",
1027                "turnover24h":"17166562011.6514",
1028                "volume24h":"200176.9910",
1029                "nextFundingTime":"1740643200000",
1030                "fundingRate":"-0.00016974",
1031                "bid1Price":"84594.30",
1032                "bid1Size":"6.777",
1033                "ask1Price":"84594.40",
1034                "ask1Size":"0.660",
1035                "preOpenPrice":"",
1036                "preQty":"",
1037                "curPreListingPhase":""
1038		    },
1039		    "cs": 337149693308,
1040		    "ts": 1740622194359
1041		}"#;
1042        let ticker_snapshot = TickerMsg::Snapshot {
1043            topic: Topic::Ticker(String::from("BTCUSDT")),
1044            cs: Some(337149693308),
1045            ts: 1740622194359,
1046            data: TickerSnapshotMsg {
1047                symbol: String::from("BTCUSDT"),
1048                tick_direction: TickDirection::ZeroPlusTick,
1049                last_price: dec!(84594.40),
1050                pre_open_price: None,
1051                pre_qty: None,
1052                cur_pre_listing_phase: None,
1053                prev_price24h: dec!(88539.30),
1054                price24h_pcnt: dec!(-0.044555),
1055                high_price24h: dec!(89389.90),
1056                low_price24h: dec!(82055.60),
1057                prev_price1h: dec!(84307.20),
1058                mark_price: dec!(84594.00),
1059                index_price: dec!(84650.47),
1060                open_interest: dec!(52903.75),
1061                open_interest_value: dec!(4475339827.50),
1062                turnover24h: dec!(17166562011.6514),
1063                volume24h: dec!(200176.9910),
1064                funding_rate: dec!(-0.00016974),
1065                next_funding_time: 1740643200000,
1066                bid1_price: dec!(84594.30),
1067                bid1_size: dec!(6.777),
1068                ask1_price: dec!(84594.40),
1069                ask1_size: dec!(0.660),
1070                delivery_time: None,
1071                basis_rate: None,
1072                delivery_fee_rate: None,
1073                predicted_delivery_price: None,
1074            },
1075        };
1076        let expected = IncomingMessage::Ticker(Box::new(ticker_snapshot));
1077
1078        let message = deserialize_json(json).unwrap();
1079
1080        assert_eq!(expected, message);
1081    }
1082
1083    #[test]
1084    fn deserialize_incoming_message_trade_snapshot() {
1085        // Category: linear.
1086        let json = r#"{
1087            "topic":"publicTrade.BTCUSDT",
1088            "type":"snapshot",
1089            "ts":1741433245359,
1090            "data":[
1091                {
1092                    "T":1741433245357,
1093                    "s":"BTCUSDT",
1094                    "S":"Buy",
1095                    "v":"0.007",
1096                    "p":"85821.00",
1097                    "L":"PlusTick",
1098                    "i":"485eaa70-df6e-5260-bbef-4f7324e3c5d9",
1099                    "BT":false
1100                }
1101            ]
1102        }"#;
1103        let expected = IncomingMessage::Trade(TradeMsg::Snapshot {
1104            id: None,
1105            topic: Topic::Trade(String::from("BTCUSDT")),
1106            ts: 1741433245359,
1107            data: vec![TradeSnapshotMsg {
1108                time: 1741433245357,
1109                symbol: String::from("BTCUSDT"),
1110                side: Side::Buy,
1111                size: dec!(0.007),
1112                price: dec!(85821.00),
1113                tick_direction: TickDirection::PlusTick,
1114                trade_id: String::from("485eaa70-df6e-5260-bbef-4f7324e3c5d9"),
1115                block_trade: false,
1116                rpi_trade: None,
1117                mark_price: None,
1118                index_price: None,
1119                mark_iv: None,
1120                iv: None,
1121            }],
1122        });
1123
1124        let message = deserialize_json(json).unwrap();
1125
1126        assert_eq!(expected, message);
1127    }
1128
1129    #[test]
1130    fn deserialize_incoming_message_orderbook_snapshot() {
1131        // Category: linear.
1132        let json = r#"{
1133            "topic":"orderbook.50.BTCUSDT",
1134            "type":"snapshot",
1135            "ts":1672304484978,
1136            "data":{
1137                "s":"BTCUSDT",
1138                "b":[
1139                    ["16493.50","0.006"],
1140                    ["16493.00","0.100"]
1141                ],
1142                "a":[
1143                    ["16611.00","0.029"],
1144                    ["16612.00","0.213"]
1145                ],
1146                "u":18521288,
1147                "seq":7961638724
1148            },
1149            "cts":1672304484976
1150        }"#;
1151        let expected = IncomingMessage::Orderbook(OrderbookMsg::Snapshot {
1152            topic: Topic::Orderbook {
1153                symbol: String::from("BTCUSDT"),
1154                depth: DepthLevel::Level50,
1155            },
1156            ts: 1672304484978,
1157            data: OrderbookDataMsg {
1158                symbol: String::from("BTCUSDT"),
1159                bids: vec![
1160                    OrderbookLevel {
1161                        price: dec!(16493.50),
1162                        size: dec!(0.006),
1163                    },
1164                    OrderbookLevel {
1165                        price: dec!(16493.00),
1166                        size: dec!(0.100),
1167                    },
1168                ],
1169                asks: vec![
1170                    OrderbookLevel {
1171                        price: dec!(16611.00),
1172                        size: dec!(0.029),
1173                    },
1174                    OrderbookLevel {
1175                        price: dec!(16612.00),
1176                        size: dec!(0.213),
1177                    },
1178                ],
1179                update_id: 18521288,
1180                seq: 7961638724,
1181            },
1182            cts: 1672304484976,
1183        });
1184
1185        let message = deserialize_json(json).unwrap();
1186
1187        assert_eq!(expected, message);
1188    }
1189
1190    #[test]
1191    fn deserialize_incoming_message_orderbook_delta() {
1192        // Category: linear.
1193        let json = r#"{
1194            "topic":"orderbook.50.BTCUSDT",
1195            "type":"delta",
1196            "ts":1687940967466,
1197            "data":{
1198                "s":"BTCUSDT",
1199                "b":[
1200                    ["30247.20","30.028"],
1201                    ["30245.40","0.224"],
1202                    ["30242.10","0.000"]
1203                ],
1204                "a":[
1205                    ["30248.67","0.033"],
1206                    ["30249.20","0.000"]
1207                ],
1208                "u":177400507,
1209                "seq":66544703342
1210            },
1211            "cts":1687940967464
1212        }"#;
1213        let expected = IncomingMessage::Orderbook(OrderbookMsg::Delta {
1214            topic: Topic::Orderbook {
1215                symbol: String::from("BTCUSDT"),
1216                depth: DepthLevel::Level50,
1217            },
1218            ts: 1687940967466,
1219            data: OrderbookDataMsg {
1220                symbol: String::from("BTCUSDT"),
1221                bids: vec![
1222                    OrderbookLevel {
1223                        price: dec!(30247.20),
1224                        size: dec!(30.028),
1225                    },
1226                    OrderbookLevel {
1227                        price: dec!(30245.40),
1228                        size: dec!(0.224),
1229                    },
1230                    OrderbookLevel {
1231                        price: dec!(30242.10),
1232                        size: dec!(0.000),
1233                    },
1234                ],
1235                asks: vec![
1236                    OrderbookLevel {
1237                        price: dec!(30248.67),
1238                        size: dec!(0.033),
1239                    },
1240                    OrderbookLevel {
1241                        price: dec!(30249.20),
1242                        size: dec!(0.000),
1243                    },
1244                ],
1245                update_id: 177400507,
1246                seq: 66544703342,
1247            },
1248            cts: 1687940967464,
1249        });
1250
1251        let message = deserialize_json(json).unwrap();
1252
1253        assert_eq!(expected, message);
1254    }
1255
1256    #[test]
1257    fn deserialize_incoming_message_all_liquidation_snapshot() {
1258        // Category: linear.
1259        let json = r#"{
1260            "topic":"allLiquidation.BTCUSDT",
1261            "type":"snapshot",
1262            "ts":1741450605553,
1263            "data":[
1264                {
1265                    "T":1741450605236,
1266                    "s":"BTCUSDT",
1267                    "S":"Buy",
1268                    "v":"0.001",
1269                    "p":"85823.60"
1270                }
1271            ]
1272        }"#;
1273        let expected = AllLiquidationMsg::Snapshot {
1274            topic: Topic::AllLiquidation(String::from("BTCUSDT")),
1275            ts: 1741450605553,
1276            data: vec![AllLiquidationSnapshotMsg {
1277                time: 1741450605236,
1278                symbol: String::from("BTCUSDT"),
1279                side: Side::Buy,
1280                size: dec!(0.001),
1281                price: dec!(85823.60),
1282            }],
1283        };
1284
1285        let message = deserialize_json(json).unwrap();
1286
1287        assert_eq!(expected, message);
1288    }
1289
1290    #[test]
1291    fn deserialize_incoming_message_order() {
1292        let json = r#"{
1293            "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90",
1294            "topic": "order",
1295            "creationTime": 1672364262474,
1296            "data": [
1297                {
1298                    "symbol": "ETH-30DEC22-1400-C",
1299                    "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020",
1300                    "side": "Sell",
1301                    "orderType": "Market",
1302                    "cancelType": "UNKNOWN",
1303                    "price": "72.5",
1304                    "qty": "1",
1305                    "orderIv": "",
1306                    "timeInForce": "IOC",
1307                    "orderStatus": "Filled",
1308                    "orderLinkId": "",
1309                    "lastPriceOnCreated": "",
1310                    "reduceOnly": false,
1311                    "leavesQty": "",
1312                    "leavesValue": "",
1313                    "cumExecQty": "1",
1314                    "cumExecValue": "75",
1315                    "avgPrice": "75",
1316                    "blockTradeId": "",
1317                    "positionIdx": 0,
1318                    "cumExecFee": "0.358635",
1319                    "closedPnl": "0",
1320                    "createdTime": "1672364262444",
1321                    "updatedTime": "1672364262457",
1322                    "rejectReason": "EC_NoError",
1323                    "stopOrderType": "",
1324                    "tpslMode": "",
1325                    "triggerPrice": "",
1326                    "takeProfit": "",
1327                    "stopLoss": "",
1328                    "tpTriggerBy": "",
1329                    "slTriggerBy": "",
1330                    "tpLimitPrice": "",
1331                    "slLimitPrice": "",
1332                    "triggerDirection": 0,
1333                    "triggerBy": "",
1334                    "closeOnTrigger": false,
1335                    "category": "option",
1336                    "placeType": "price",
1337                    "smpType": "None",
1338                    "smpGroup": 0,
1339                    "smpOrderId": "",
1340                    "feeCurrency": "",
1341                    "cumFeeDetail": {
1342                        "MNT": "0.00242968"
1343                    }
1344                }
1345            ]
1346        }"#;
1347        let order = PrivateMsg {
1348            id: String::from("5923240c6880ab-c59f-420b-9adb-3639adc9dd90"),
1349            creation_time: 1672364262474,
1350            data: vec![OrderMsg {
1351                category: Category::Option,
1352                order_id: String::from("5cf98598-39a7-459e-97bf-76ca765ee020"),
1353                order_link_id: None,
1354                is_leverage: None,
1355                block_trade_id: None,
1356                symbol: String::from("ETH-30DEC22-1400-C"),
1357                price: dec!(72.5),
1358                broker_order_price: None,
1359                qty: dec!(1.0),
1360                side: Side::Sell,
1361                position_idx: PositionIdx::OneWay,
1362                order_status: OrderStatus::Filled,
1363                create_type: None,
1364                cancel_type: CancelType::UNKNOWN,
1365                reject_reason: RejectReason::EcNoError,
1366                avg_price: Some(dec!(75.0)),
1367                leaves_qty: None,
1368                leaves_value: None,
1369                cum_exec_qty: dec!(1.0),
1370                cum_exec_value: dec!(75.0),
1371                cum_exec_fee: dec!(0.358635),
1372                closed_pnl: dec!(0.0),
1373                fee_currency: None,
1374                time_in_force: TimeInForce::IOC,
1375                order_type: OrderType::Market,
1376                stop_order_type: None,
1377                oco_trigger_by: None,
1378                order_iv: None,
1379                market_unit: None,
1380                slippage_tolerance_type: None,
1381                slippage_tolerance: None,
1382                trigger_price: None,
1383                take_profit: None,
1384                stop_loss: None,
1385                tpsl_mode: None,
1386                tp_limit_price: None,
1387                sl_limit_price: None,
1388                tp_trigger_by: None,
1389                sl_trigger_by: None,
1390                trigger_direction: TriggerDirection::UNKNOWN,
1391                trigger_by: None,
1392                last_price_on_created: None,
1393                reduce_only: false,
1394                close_on_trigger: false,
1395                place_type: Some(PlaceType::Price),
1396                smp_type: SmpType::None,
1397                smp_group: 0,
1398                smp_order_id: None,
1399                created_time: 1672364262444,
1400                updated_time: 1672364262457,
1401                cum_fee_detail: Some(serde_json::from_str(r#"{"MNT": "0.00242968"}"#).unwrap()),
1402            }],
1403        };
1404        let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1405
1406        let message = deserialize_json(json).unwrap();
1407
1408        assert_eq!(expected, message);
1409    }
1410
1411    #[test]
1412    fn deserialize_incoming_message_order2() {
1413        let json = r#"{"topic":"order","id":"108985347_ADAUSDT_140667095077548","creationTime":1766436947942,"data":[{"category":"linear","symbol":"ADAUSDT","orderId":"ae802ad5-af70-4957-ba72-86ad7fc9c24d","orderLinkId":"","blockTradeId":"","side":"Buy","positionIdx":0,"orderStatus":"Filled","cancelType":"UNKNOWN","rejectReason":"EC_NoError","timeInForce":"IOC","isLeverage":"","price":"0.3862","qty":"15","avgPrice":"0.3679","leavesQty":"0","leavesValue":"0","cumExecQty":"15","cumExecValue":"5.5185","cumExecFee":"0.00303518","orderType":"Market","stopOrderType":"","orderIv":"","triggerPrice":"","takeProfit":"","stopLoss":"","triggerBy":"","tpTriggerBy":"","slTriggerBy":"","triggerDirection":0,"placeType":"","lastPriceOnCreated":"0.3679","closeOnTrigger":false,"reduceOnly":false,"smpGroup":0,"smpType":"None","smpOrderId":"","slLimitPrice":"0","tpLimitPrice":"0","tpslMode":"UNKNOWN","createType":"CreateByUser","marketUnit":"","createdTime":"1766436947940","updatedTime":"1766436947940","feeCurrency":"","closedPnl":"0","slippageTolerance":"0","slippageToleranceType":"UNKNOWN","cumFeeDetail":{}}]}"#;
1414        let order = PrivateMsg {
1415            id: String::from("108985347_ADAUSDT_140667095077548"),
1416            creation_time: 1766436947942,
1417            data: vec![OrderMsg {
1418                category: Category::Linear,
1419                order_id: String::from("ae802ad5-af70-4957-ba72-86ad7fc9c24d"),
1420                order_link_id: None,
1421                is_leverage: None,
1422                block_trade_id: None,
1423                symbol: String::from("ADAUSDT"),
1424                price: dec!(0.3862),
1425                broker_order_price: None,
1426                qty: dec!(15),
1427                side: Side::Buy,
1428                position_idx: PositionIdx::OneWay,
1429                order_status: OrderStatus::Filled,
1430                create_type: Some(CreateType::CreateByUser),
1431                cancel_type: CancelType::UNKNOWN,
1432                reject_reason: RejectReason::EcNoError,
1433                avg_price: Some(dec!(0.3679)),
1434                leaves_qty: Some(dec!(0)),
1435                leaves_value: Some(dec!(0)),
1436                cum_exec_qty: dec!(15),
1437                cum_exec_value: dec!(5.5185),
1438                cum_exec_fee: dec!(0.00303518),
1439                closed_pnl: dec!(0),
1440                fee_currency: None,
1441                time_in_force: TimeInForce::IOC,
1442                order_type: OrderType::Market,
1443                stop_order_type: None,
1444                oco_trigger_by: None,
1445                order_iv: None,
1446                market_unit: None,
1447                slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1448                slippage_tolerance: Some(dec!(0)),
1449                trigger_price: None,
1450                take_profit: None,
1451                stop_loss: None,
1452                tpsl_mode: Some(TpslMode::UNKNOWN),
1453                tp_limit_price: Some(dec!(0)),
1454                sl_limit_price: Some(dec!(0)),
1455                tp_trigger_by: None,
1456                sl_trigger_by: None,
1457                trigger_direction: TriggerDirection::UNKNOWN,
1458                trigger_by: None,
1459                last_price_on_created: Some(dec!(0.3679)),
1460                reduce_only: false,
1461                close_on_trigger: false,
1462                place_type: None,
1463                smp_type: SmpType::None,
1464                smp_group: 0,
1465                smp_order_id: None,
1466                created_time: 1766436947940,
1467                updated_time: 1766436947940,
1468                cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1469            }],
1470        };
1471        let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1472
1473        let message = deserialize_json(json).unwrap();
1474
1475        assert_eq!(expected, message);
1476    }
1477
1478    #[test]
1479    fn deserialize_incoming_message_order3() {
1480        let json = r#"{"topic":"order","id":"108985347_ADAUSDT_140667102632416","creationTime":1766600379878,"data":[{"category":"linear","symbol":"ADAUSDT","orderId":"f0468cbc-ed2f-4fd7-9620-998f3e9f387c","orderLinkId":"BOT_LINK_ID-1","blockTradeId":"","side":"Buy","positionIdx":0,"orderStatus":"New","cancelType":"UNKNOWN","rejectReason":"EC_NoError","timeInForce":"GTC","isLeverage":"","price":"0.3539","qty":"15","avgPrice":"","leavesQty":"15","leavesValue":"5.3085","cumExecQty":"0","cumExecValue":"0","cumExecFee":"0","orderType":"Limit","stopOrderType":"","orderIv":"","triggerPrice":"","takeProfit":"","stopLoss":"","triggerBy":"","tpTriggerBy":"","slTriggerBy":"","triggerDirection":0,"placeType":"","lastPriceOnCreated":"0.355","closeOnTrigger":false,"reduceOnly":false,"smpGroup":0,"smpType":"None","smpOrderId":"","slLimitPrice":"0","tpLimitPrice":"0","tpslMode":"UNKNOWN","createType":"CreateByUser","marketUnit":"","createdTime":"1766600379876","updatedTime":"1766600379876","feeCurrency":"","closedPnl":"0","slippageTolerance":"0","slippageToleranceType":"UNKNOWN","cumFeeDetail":{}}]}"#;
1481        let order = PrivateMsg {
1482            id: String::from("108985347_ADAUSDT_140667102632416"),
1483            creation_time: 1766600379878,
1484            data: vec![OrderMsg {
1485                category: Category::Linear,
1486                order_id: String::from("f0468cbc-ed2f-4fd7-9620-998f3e9f387c"),
1487                order_link_id: Some(String::from("BOT_LINK_ID-1")),
1488                is_leverage: None,
1489                block_trade_id: None,
1490                symbol: String::from("ADAUSDT"),
1491                price: dec!(0.3539),
1492                broker_order_price: None,
1493                qty: dec!(15),
1494                side: Side::Buy,
1495                position_idx: PositionIdx::OneWay,
1496                order_status: OrderStatus::New,
1497                create_type: Some(CreateType::CreateByUser),
1498                cancel_type: CancelType::UNKNOWN,
1499                reject_reason: RejectReason::EcNoError,
1500                avg_price: None,
1501                leaves_qty: Some(dec!(15)),
1502                leaves_value: Some(dec!(5.3085)),
1503                cum_exec_qty: dec!(0),
1504                cum_exec_value: dec!(0),
1505                cum_exec_fee: dec!(0),
1506                closed_pnl: dec!(0),
1507                fee_currency: None,
1508                time_in_force: TimeInForce::GTC,
1509                order_type: OrderType::Limit,
1510                stop_order_type: None,
1511                oco_trigger_by: None,
1512                order_iv: None,
1513                market_unit: None,
1514                slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1515                slippage_tolerance: Some(dec!(0)),
1516                trigger_price: None,
1517                take_profit: None,
1518                stop_loss: None,
1519                tpsl_mode: Some(TpslMode::UNKNOWN),
1520                tp_limit_price: Some(dec!(0)),
1521                sl_limit_price: Some(dec!(0)),
1522                tp_trigger_by: None,
1523                sl_trigger_by: None,
1524                trigger_direction: TriggerDirection::UNKNOWN,
1525                trigger_by: None,
1526                last_price_on_created: Some(dec!(0.355)),
1527                reduce_only: false,
1528                close_on_trigger: false,
1529                place_type: None, // "smpGroup":0,"smpType":"None","smpOrderId":"",
1530                smp_type: SmpType::None,
1531                smp_group: 0,
1532                smp_order_id: None,
1533                created_time: 1766600379876,
1534                updated_time: 1766600379876,
1535                cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1536            }],
1537        };
1538        let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1539
1540        let message = deserialize_json(json).unwrap();
1541
1542        assert_eq!(expected, message);
1543    }
1544
1545    #[test]
1546    fn deserialize_incoming_message_position() {
1547        let json = r#"{
1548            "id": "108985347_position_1765659601915",
1549            "topic": "position",
1550            "creationTime": 1765659601915,
1551            "data": [
1552                {
1553                    "positionIdx": 1,
1554                    "tradeMode": 0,
1555                    "riskId": 116,
1556                    "riskLimitValue": "200000",
1557                    "symbol": "ADAUSDT",
1558                    "side": "Buy",
1559                    "size": "18720",
1560                    "entryPrice": "0.41160027",
1561                    "sessionAvgPrice": "",
1562                    "leverage": "75",
1563                    "positionValue": "7705.157",
1564                    "positionBalance": "0",
1565                    "markPrice": "0.41",
1566                    "positionIM": "106.51735757",
1567                    "positionMM": "61.74535757",
1568                    "positionIMByMp": "106.51735757",
1569                    "positionMMByMp": "61.74535757",
1570                    "takeProfit": "0.4321",
1571                    "stopLoss": "0.3704",
1572                    "trailingStop": "0",
1573                    "unrealisedPnl": "-29.957",
1574                    "cumRealisedPnl": "-6712.87804378",
1575                    "curRealisedPnl": "-2.6317147",
1576                    "createdTime": "1714594321840",
1577                    "updatedTime": "1765645142548",
1578                    "tpslMode": "Full",
1579                    "liqPrice": "0.37000066",
1580                    "bustPrice": "",
1581                    "category": "linear",
1582                    "positionStatus": "Normal",
1583                    "adlRankIndicator": 2,
1584                    "autoAddMargin": 0,
1585                    "leverageSysUpdatedTime": "",
1586                    "mmrSysUpdatedTime": "",
1587                    "seq": 140667058318085,
1588                    "isReduceOnly": false
1589                },
1590                {
1591                    "positionIdx": 2,
1592                    "tradeMode": 0,
1593                    "riskId": 116,
1594                    "riskLimitValue": "200000",
1595                    "symbol": "ADAUSDT",
1596                    "side": "",
1597                    "size": "0",
1598                    "entryPrice": "0",
1599                    "sessionAvgPrice": "",
1600                    "leverage": "75",
1601                    "positionValue": "0",
1602                    "positionBalance": "0",
1603                    "markPrice": "0.41",
1604                    "positionIM": "",
1605                    "positionMM": "",
1606                    "positionIMByMp": "",
1607                    "positionMMByMp": "",
1608                    "takeProfit": "0",
1609                    "stopLoss": "0",
1610                    "trailingStop": "0",
1611                    "unrealisedPnl": "0",
1612                    "cumRealisedPnl": "1618.30675974",
1613                    "curRealisedPnl": "0",
1614                    "createdTime": "1714594321840",
1615                    "updatedTime": "1765046350698",
1616                    "tpslMode": "Full",
1617                    "liqPrice": "0",
1618                    "bustPrice": "",
1619                    "category": "linear",
1620                    "positionStatus": "Normal",
1621                    "adlRankIndicator": 0,
1622                    "autoAddMargin": 0,
1623                    "leverageSysUpdatedTime": "",
1624                    "mmrSysUpdatedTime": "",
1625                    "seq": 140667031311361,
1626                    "isReduceOnly": false
1627                }
1628            ]
1629        }"#;
1630        let position = PrivateMsg {
1631            id: String::from("108985347_position_1765659601915"),
1632            creation_time: 1765659601915,
1633            data: vec![
1634                PositionMsg {
1635                    category: Category::Linear,
1636                    symbol: String::from("ADAUSDT"),
1637                    side: Some(Side::Buy),
1638                    size: dec!(18720),
1639                    position_idx: PositionIdx::Buy,
1640                    position_value: dec!(7705.157),
1641                    risk_id: 116,
1642                    risk_limit_value: Some(dec!(200000)),
1643                    entry_price: dec!(0.41160027),
1644                    mark_price: dec!(0.41),
1645                    leverage: dec!(75),
1646                    auto_add_margin: false,
1647                    position_im: Some(dec!(106.51735757)),
1648                    position_mm: Some(dec!(61.74535757)),
1649                    position_im_by_mp: Some(dec!(106.51735757)),
1650                    position_mm_by_mp: Some(dec!(61.74535757)),
1651                    liq_price: Some(dec!(0.37000066)),
1652                    take_profit: dec!(0.4321),
1653                    stop_loss: dec!(0.3704),
1654                    trailing_stop: dec!(0),
1655                    unrealised_pnl: dec!(-29.957),
1656                    cur_realised_pnl: dec!(-2.6317147),
1657                    session_avg_price: None,
1658                    delta: None,
1659                    gamma: None,
1660                    vega: None,
1661                    theta: None,
1662                    cum_realised_pnl: dec!(-6712.87804378),
1663                    position_status: PositionStatus::Normal,
1664                    adl_rank_indicator: AdlRankIndicator::Two,
1665                    is_reduce_only: false,
1666                    mmr_sys_updated_time: None,
1667                    leverage_sys_updated_time: None,
1668                    created_time: 1714594321840,
1669                    updated_time: 1765645142548,
1670                    seq: 140667058318085,
1671                },
1672                PositionMsg {
1673                    category: Category::Linear,
1674                    symbol: String::from("ADAUSDT"),
1675                    side: None,
1676                    size: dec!(0),
1677                    position_idx: PositionIdx::Sell,
1678                    position_value: dec!(0),
1679                    risk_id: 116,
1680                    risk_limit_value: Some(dec!(200000)),
1681                    entry_price: dec!(0),
1682                    mark_price: dec!(0.41),
1683                    leverage: dec!(75),
1684                    auto_add_margin: false,
1685                    position_im: None,
1686                    position_mm: None,
1687                    position_im_by_mp: None,
1688                    position_mm_by_mp: None,
1689                    liq_price: Some(dec!(0)),
1690                    take_profit: dec!(0),
1691                    stop_loss: dec!(0),
1692                    trailing_stop: dec!(0),
1693                    unrealised_pnl: dec!(0),
1694                    cur_realised_pnl: dec!(0),
1695                    session_avg_price: None,
1696                    delta: None,
1697                    gamma: None,
1698                    vega: None,
1699                    theta: None,
1700                    cum_realised_pnl: dec!(1618.30675974),
1701                    position_status: PositionStatus::Normal,
1702                    adl_rank_indicator: AdlRankIndicator::Zero,
1703                    is_reduce_only: false,
1704                    mmr_sys_updated_time: None,
1705                    leverage_sys_updated_time: None,
1706                    created_time: 1714594321840,
1707                    updated_time: 1765046350698,
1708                    seq: 140667031311361,
1709                },
1710            ],
1711        };
1712        let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1713
1714        let message = deserialize_json(json).unwrap();
1715
1716        assert_eq!(expected, message);
1717    }
1718
1719    #[test]
1720    fn deserialize_incoming_message_position2() {
1721        let json = r#"{
1722            "id":"108985347_position_1766316605952",
1723            "topic":"position",
1724            "creationTime":1766316605952,
1725            "data":[
1726                {
1727                "positionIdx":1,
1728                "tradeMode":0,
1729                "riskId":116,
1730                "riskLimitValue":"200000",
1731                "symbol":"ADAUSDT",
1732                "side":"Buy",
1733                "size":"43",
1734                "entryPrice":"0.37293023",
1735                "sessionAvgPrice":"",
1736                "leverage":"75",
1737                "positionValue":"16.036",
1738                "positionBalance":"0",
1739                "markPrice":"0.3702",
1740                "positionIM":"0.22095025",
1741                "positionMM":"0.12809175",
1742                "positionIMByMp":"0.22095025",
1743                "positionMMByMp":"0.12809175",
1744                "takeProfit":"0",
1745                "stopLoss":"0",
1746                "trailingStop":"0",
1747                "unrealisedPnl":"-0.1174",
1748                "cumRealisedPnl":"-7547.8530836",
1749                "curRealisedPnl":"-0.00465061",
1750                "createdTime":"1714594321840",
1751                "updatedTime":"1766313370061",
1752                "tpslMode":"Full",
1753                "liqPrice":"",
1754                "bustPrice":"",
1755                "category":"linear",
1756                "positionStatus":"Normal",
1757                "adlRankIndicator":2,
1758                "autoAddMargin":0,
1759                "leverageSysUpdatedTime":"",
1760                "mmrSysUpdatedTime":"",
1761                "seq":140667089523042,
1762                "isReduceOnly":false
1763                },
1764                {
1765                "positionIdx":2,
1766                "tradeMode":0,
1767                "riskId":116,
1768                "riskLimitValue":"200000",
1769                "symbol":"ADAUSDT",
1770                "side":"",
1771                "size":"0",
1772                "entryPrice":"0",
1773                "sessionAvgPrice":"",
1774                "leverage":"75",
1775                "positionValue":"0",
1776                "positionBalance":"0",
1777                "markPrice":"0.3702",
1778                "positionIM":"",
1779                "positionMM":"",
1780                "positionIMByMp":"",
1781                "positionMMByMp":"",
1782                "takeProfit":"0",
1783                "stopLoss":"0",
1784                "trailingStop":"0",
1785                "unrealisedPnl":"0",
1786                "cumRealisedPnl":"1618.30675974",
1787                "curRealisedPnl":"0",
1788                "createdTime":"1714594321840",
1789                "updatedTime":"1765046350698",
1790                "tpslMode":"Full",
1791                "liqPrice":"0",
1792                "bustPrice":"",
1793                "category":"linear",
1794                "positionStatus":"Normal",
1795                "adlRankIndicator":0,
1796                "autoAddMargin":0,
1797                "leverageSysUpdatedTime":"",
1798                "mmrSysUpdatedTime":"",
1799                "seq":140667031311361,
1800                "isReduceOnly":false
1801                }
1802            ]
1803        }"#;
1804        let position = PrivateMsg {
1805            id: String::from("108985347_position_1766316605952"),
1806            creation_time: 1766316605952,
1807            data: vec![
1808                PositionMsg {
1809                    category: Category::Linear,
1810                    symbol: String::from("ADAUSDT"),
1811                    side: Some(Side::Buy),
1812                    size: dec!(43),
1813                    position_idx: PositionIdx::Buy,
1814                    position_value: dec!(16.036),
1815                    risk_id: 116,
1816                    risk_limit_value: Some(dec!(200000)),
1817                    entry_price: dec!(0.37293023),
1818                    mark_price: dec!(0.3702),
1819                    leverage: dec!(75),
1820                    auto_add_margin: false,
1821                    position_im: Some(dec!(0.22095025)),
1822                    position_mm: Some(dec!(0.12809175)),
1823                    position_im_by_mp: Some(dec!(0.22095025)),
1824                    position_mm_by_mp: Some(dec!(0.12809175)),
1825                    liq_price: None,
1826                    take_profit: dec!(0),
1827                    stop_loss: dec!(0),
1828                    trailing_stop: dec!(0),
1829                    unrealised_pnl: dec!(-0.1174),
1830                    cur_realised_pnl: dec!(-0.00465061),
1831                    session_avg_price: None,
1832                    delta: None,
1833                    gamma: None,
1834                    vega: None,
1835                    theta: None,
1836                    cum_realised_pnl: dec!(-7547.8530836),
1837                    position_status: PositionStatus::Normal,
1838                    adl_rank_indicator: AdlRankIndicator::Two,
1839                    is_reduce_only: false,
1840                    mmr_sys_updated_time: None,
1841                    leverage_sys_updated_time: None,
1842                    created_time: 1714594321840,
1843                    updated_time: 1766313370061,
1844                    seq: 140667089523042,
1845                },
1846                PositionMsg {
1847                    category: Category::Linear,
1848                    symbol: String::from("ADAUSDT"),
1849                    side: None,
1850                    size: dec!(0),
1851                    position_idx: PositionIdx::Sell,
1852                    position_value: dec!(0),
1853                    risk_id: 116,
1854                    risk_limit_value: Some(dec!(200000)),
1855                    entry_price: dec!(0),
1856                    mark_price: dec!(0.3702),
1857                    leverage: dec!(75),
1858                    auto_add_margin: false,
1859                    position_im: None,
1860                    position_mm: None,
1861                    position_im_by_mp: None,
1862                    position_mm_by_mp: None,
1863                    liq_price: Some(dec!(0)),
1864                    take_profit: dec!(0),
1865                    stop_loss: dec!(0),
1866                    trailing_stop: dec!(0),
1867                    unrealised_pnl: dec!(0),
1868                    cur_realised_pnl: dec!(0),
1869                    session_avg_price: None,
1870                    delta: None,
1871                    gamma: None,
1872                    vega: None,
1873                    theta: None,
1874                    cum_realised_pnl: dec!(1618.30675974),
1875                    position_status: PositionStatus::Normal,
1876                    adl_rank_indicator: AdlRankIndicator::Zero,
1877                    is_reduce_only: false,
1878                    mmr_sys_updated_time: None,
1879                    leverage_sys_updated_time: None,
1880                    created_time: 1714594321840,
1881                    updated_time: 1765046350698,
1882                    seq: 140667031311361,
1883                },
1884            ],
1885        };
1886        let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1887
1888        let message = deserialize_json(json).unwrap();
1889
1890        assert_eq!(expected, message);
1891    }
1892
1893    #[test]
1894    fn deserialize_incoming_message_wallet() {
1895        let json = r#"{
1896            "id": "592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e",
1897            "topic": "wallet",
1898            "creationTime": 1700034722104,
1899            "data": [
1900                {
1901                    "accountIMRate": "0",
1902                    "accountIMRateByMp": "0",
1903                    "accountMMRate": "0",
1904                    "accountMMRateByMp": "0",
1905                    "totalEquity": "10262.91335023",
1906                    "totalWalletBalance": "9684.46297164",
1907                    "totalMarginBalance": "9684.46297164",
1908                    "totalAvailableBalance": "9556.6056555",
1909                    "totalPerpUPL": "0",
1910                    "totalInitialMargin": "0",
1911                    "totalInitialMarginByMp": "0",
1912                    "totalMaintenanceMargin": "0",
1913                    "totalMaintenanceMarginByMp": "0",
1914                    "coin": [
1915                        {
1916                            "coin": "BTC",
1917                            "equity": "0.00102964",
1918                            "usdValue": "36.70759517",
1919                            "walletBalance": "0.00102964",
1920                            "availableToWithdraw": "0.00102964",
1921                            "availableToBorrow": "",
1922                            "borrowAmount": "0",
1923                            "accruedInterest": "0",
1924                            "totalOrderIM": "",
1925                            "totalPositionIM": "",
1926                            "totalPositionMM": "",
1927                            "unrealisedPnl": "0",
1928                            "cumRealisedPnl": "-0.00000973",
1929                            "bonus": "0",
1930                            "collateralSwitch": true,
1931                            "marginCollateral": true,
1932                            "locked": "0",
1933                            "spotHedgingQty": "0.01592413",
1934                            "spotBorrow": "0"
1935                        }
1936                    ],
1937                    "accountLTV": "0",
1938                    "accountType": "UNIFIED"
1939                }
1940            ]
1941        }"#;
1942        let coin = WalletCoin {
1943            coin: String::from("BTC"),
1944            equity: dec!(0.00102964),
1945            usd_value: dec!(36.70759517),
1946            wallet_balance: dec!(0.00102964),
1947            locked: dec!(0),
1948            spot_hedging_qty: dec!(0.01592413),
1949            borrow_amount: dec!(0),
1950            accrued_interest: dec!(0),
1951            total_order_im: None,
1952            total_position_im: None,
1953            total_position_mm: None,
1954            unrealised_pnl: dec!(0),
1955            cum_realised_pnl: dec!(-0.00000973),
1956            bonus: dec!(0),
1957            collateral_switch: true,
1958            margin_collateral: true,
1959            spot_borrow: Some(dec!(0)),
1960        };
1961        let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
1962        let wallet = PrivateMsg {
1963            id: String::from("592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e"),
1964            creation_time: 1700034722104,
1965            data: vec![WalletMsg {
1966                account_type: AccountType::UNIFIED,
1967                account_im_rate: dec!(0),
1968                account_im_rate_by_mp: dec!(0),
1969                account_mm_rate: dec!(0),
1970                account_mm_rate_by_mp: dec!(0),
1971                total_equity: dec!(10262.91335023),
1972                total_wallet_balance: dec!(9684.46297164),
1973                total_margin_balance: dec!(9684.46297164),
1974                total_available_balance: dec!(9556.6056555),
1975                total_perp_upl: dec!(0),
1976                total_initial_margin: dec!(0),
1977                total_initial_margin_by_mp: dec!(0),
1978                total_maintenance_margin: dec!(0),
1979                total_maintenance_margin_by_mp: dec!(0),
1980                coin,
1981            }],
1982        };
1983        let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
1984
1985        let message = deserialize_json(json).unwrap();
1986
1987        assert_eq!(expected, message);
1988    }
1989
1990    #[test]
1991    fn deserialize_incoming_message_wallet2() {
1992        let json = r#"{
1993            "id":"108985347_wallet_1766318882965",
1994            "topic":"wallet",
1995            "creationTime":1766318882964,
1996            "data":[
1997                {
1998                "accountIMRate":"0.0007",
1999                "accountMMRate":"0.0004",
2000                "accountIMRateByMp":"0.0007",
2001                "accountMMRateByMp":"0.0004",
2002                "totalEquity":"102.7094181",
2003                "totalWalletBalance":"102.16591975",
2004                "totalMarginBalance":"102.16591975",
2005                "totalAvailableBalance":"102.09402758",
2006                "totalPerpUPL":"0",
2007                "totalInitialMargin":"0.07189217",
2008                "totalMaintenanceMargin":"0.04166941",
2009                "totalInitialMarginByMp":"0.07189217",
2010                "totalMaintenanceMarginByMp":"0.04166941",
2011                "coin":[
2012                    {
2013                    "coin":"USDT",
2014                    "equity":"75.5601152",
2015                    "usdValue":"75.53450032",
2016                    "walletBalance":"75.5601152",
2017                    "availableToWithdraw":"",
2018                    "availableToBorrow":"",
2019                    "borrowAmount":"0",
2020                    "accruedInterest":"0",
2021                    "totalOrderIM":"0",
2022                    "totalPositionIM":"0.07191655",
2023                    "totalPositionMM":"0.04168355",
2024                    "unrealisedPnl":"0",
2025                    "cumRealisedPnl":"36163.8134634",
2026                    "bonus":"0",
2027                    "collateralSwitch":true,
2028                    "marginCollateral":true,
2029                    "locked":"0",
2030                    "spotHedgingQty":"0"
2031                    }
2032                ],
2033                "accountLTV":"0",
2034                "accountType":"UNIFIED"
2035                }
2036            ]
2037            }"#;
2038        let coin = WalletCoin {
2039            coin: String::from("USDT"),
2040            equity: dec!(75.5601152),
2041            usd_value: dec!(75.53450032),
2042            wallet_balance: dec!(75.5601152),
2043            locked: dec!(0),
2044            spot_hedging_qty: dec!(0),
2045            borrow_amount: dec!(0),
2046            accrued_interest: dec!(0),
2047            total_order_im: Some(dec!(0)),
2048            total_position_im: Some(dec!(0.07191655)),
2049            total_position_mm: Some(dec!(0.04168355)),
2050            unrealised_pnl: dec!(0),
2051            cum_realised_pnl: dec!(36163.8134634),
2052            bonus: dec!(0),
2053            collateral_switch: true,
2054            margin_collateral: true,
2055            spot_borrow: None,
2056        };
2057        let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
2058        let wallet = PrivateMsg {
2059            id: String::from("108985347_wallet_1766318882965"),
2060            creation_time: 1766318882964,
2061            data: vec![WalletMsg {
2062                account_type: AccountType::UNIFIED,
2063                account_im_rate: dec!(0.0007),
2064                account_im_rate_by_mp: dec!(0.0007),
2065                account_mm_rate: dec!(0.0004),
2066                account_mm_rate_by_mp: dec!(0.0004),
2067                total_equity: dec!(102.7094181),
2068                total_wallet_balance: dec!(102.16591975),
2069                total_margin_balance: dec!(102.16591975),
2070                total_available_balance: dec!(102.09402758),
2071                total_perp_upl: dec!(0),
2072                total_initial_margin: dec!(0.07189217),
2073                total_initial_margin_by_mp: dec!(0.07189217),
2074                total_maintenance_margin: dec!(0.04166941),
2075                total_maintenance_margin_by_mp: dec!(0.04166941),
2076                coin,
2077            }],
2078        };
2079        let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
2080
2081        let message = deserialize_json(json).unwrap();
2082
2083        assert_eq!(expected, message);
2084    }
2085
2086    #[test]
2087    fn deserialize_incoming_message_execution() {
2088        let json = r#"{
2089            "topic": "execution",
2090            "id": "386825804_BTCUSDT_140612148849382",
2091            "creationTime": 1746270400355,
2092            "data": [
2093                {
2094                    "category": "linear",
2095                    "symbol": "BTCUSDT",
2096                    "closedSize": "0.5",
2097                    "execFee": "26.3725275",
2098                    "execId": "0ab1bdf7-4219-438b-b30a-32ec863018f7",
2099                    "execPrice": "95900.1",
2100                    "execQty": "0.5",
2101                    "execType": "Trade",
2102                    "execValue": "47950.05",
2103                    "feeRate": "0.00055",
2104                    "tradeIv": "",
2105                    "markIv": "",
2106                    "blockTradeId": "",
2107                    "markPrice": "95901.48",
2108                    "indexPrice": "",
2109                    "underlyingPrice": "",
2110                    "leavesQty": "0",
2111                    "orderId": "9aac161b-8ed6-450d-9cab-c5cc67c21784",
2112                    "orderLinkId": "",
2113                    "orderPrice": "94942.5",
2114                    "orderQty": "0.5",
2115                    "orderType": "Market",
2116                    "stopOrderType": "UNKNOWN",
2117                    "side": "Sell",
2118                    "execTime": "1746270400353",
2119                    "isLeverage": "0",
2120                    "isMaker": false,
2121                    "seq": 140612148849382,
2122                    "marketUnit": "",
2123                    "execPnl": "0.05",
2124                    "createType": "CreateByUser",
2125                    "extraFees":[{"feeCoin":"USDT","feeType":"GST","subFeeType":"IND_GST","feeRate":"0.0000675","fee":"0.006403779"}],
2126                    "feeCurrency": "USDT"
2127                }
2128            ]
2129        }"#;
2130        let execution = PrivateMsg {
2131            id: String::from("386825804_BTCUSDT_140612148849382"),
2132            creation_time: 1746270400355,
2133            data: vec![ExecutionMsg {
2134                category: Category::Linear,
2135                symbol: String::from("BTCUSDT"),
2136                is_leverage: false,
2137                order_id: String::from("9aac161b-8ed6-450d-9cab-c5cc67c21784"),
2138                order_link_id: None,
2139                side: Side::Sell,
2140                order_price: dec!(94942.5),
2141                order_qty: dec!(0.5),
2142                leaves_qty: dec!(0),
2143                create_type: CreateType::CreateByUser,
2144                order_type: OrderType::Market,
2145                stop_order_type: StopOrderType::UNKNOWN,
2146                exec_fee: dec!(26.3725275),
2147                exec_id: String::from("0ab1bdf7-4219-438b-b30a-32ec863018f7"),
2148                exec_price: dec!(95900.1),
2149                exec_qty: dec!(0.5),
2150                exec_pnl: dec!(0.05),
2151                exec_type: ExecType::Trade,
2152                exec_value: dec!(47950.05),
2153                exec_time: 1746270400353,
2154                is_maker: false,
2155                fee_rate: dec!(0.00055),
2156                trade_iv: None,
2157                mark_iv: None,
2158                mark_price: dec!(95901.48),
2159                index_price: None,
2160                underlying_price: None,
2161                block_trade_id: None,
2162                closed_size: dec!(0.5),
2163                extra_fees: Some(vec![ExtraFee {
2164                    fee_coin: String::from("USDT"),
2165                    fee_type: ExtraFeeType::Gst,
2166                    sub_fee_type: ExtraSubFeeType::IndGst,
2167                    fee_rate: dec!(0.0000675),
2168                    fee: dec!(0.006403779),
2169                }]),
2170                seq: 140612148849382,
2171                fee_currency: String::from("USDT"),
2172            }],
2173        };
2174        let expected = IncomingMessage::Topic(TopicMessage::Execution(execution));
2175
2176        let message = deserialize_json(json).unwrap();
2177
2178        assert_eq!(expected, message);
2179    }
2180}