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::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    AllLiquidation(AllLiquidationMsg),
32    Topic(TopicMessage),
33}
34
35impl IncomingMessage {
36    pub fn is_pong(&self) -> bool {
37        matches!(
38            self,
39            IncomingMessage::Command(CommandMsg::Pong {
40                req_id: _,
41                ret_msg: _,
42                conn_id: _,
43                args: _,
44                success: _,
45            })
46        )
47    }
48    pub fn is_ping(&self) -> bool {
49        matches!(
50            self,
51            IncomingMessage::Command(CommandMsg::Ping {
52                req_id: _,
53                ret_msg: _,
54                conn_id: _,
55                args: _,
56                success: _,
57            })
58        )
59    }
60}
61
62#[derive(PartialEq, Deserialize, Debug)]
63#[serde(tag = "op")]
64pub enum CommandMsg {
65    #[serde(rename = "subscribe")]
66    Subscribe {
67        #[serde(default, deserialize_with = "empty_string_as_none")]
68        req_id: Option<String>,
69        #[serde(default, deserialize_with = "empty_string_as_none")]
70        ret_msg: Option<String>,
71        conn_id: String,
72        success: bool,
73    },
74    #[serde(rename = "unsubscribe")]
75    Unsubscribe {
76        #[serde(default, deserialize_with = "empty_string_as_none")]
77        req_id: Option<String>,
78        #[serde(default, deserialize_with = "empty_string_as_none")]
79        ret_msg: Option<String>,
80        conn_id: String,
81        success: bool,
82    },
83    #[serde(rename = "auth")]
84    Auth {
85        #[serde(default, deserialize_with = "empty_string_as_none")]
86        req_id: Option<String>,
87        #[serde(default, deserialize_with = "empty_string_as_none")]
88        ret_msg: Option<String>,
89        conn_id: String,
90        success: bool,
91    },
92    #[serde(rename = "pong")]
93    Pong {
94        #[serde(default, deserialize_with = "empty_string_as_none")]
95        req_id: Option<String>,
96        #[serde(default, deserialize_with = "empty_string_as_none")]
97        ret_msg: Option<String>,
98        conn_id: String,
99        args: Option<Vec<String>>,
100        success: Option<bool>,
101    },
102    #[serde(rename = "ping")]
103    Ping {
104        #[serde(default, deserialize_with = "empty_string_as_none")]
105        req_id: Option<String>,
106        #[serde(default, deserialize_with = "empty_string_as_none")]
107        ret_msg: Option<String>,
108        conn_id: String,
109        args: Option<Vec<String>>,
110        success: bool,
111    },
112}
113
114// TODO: Use PublicMsg<T>
115#[derive(PartialEq, Deserialize, Debug)]
116#[serde(tag = "type")]
117pub enum TickerMsg {
118    #[serde(rename = "snapshot")]
119    Snapshot {
120        topic: Topic,
121        #[serde(default, deserialize_with = "option_number")]
122        cs: Option<u64>,
123        ts: Timestamp,
124        data: TickerSnapshotMsg,
125    },
126    #[serde(rename = "delta")]
127    Delta {
128        topic: Topic,
129        #[serde(default, deserialize_with = "option_number")]
130        cs: Option<u64>,
131        ts: Timestamp,
132        data: TickerDeltaMsg,
133    },
134}
135
136#[derive(PartialEq, Deserialize, Debug)]
137#[serde(rename_all = "camelCase")]
138pub struct TickerSnapshotMsg {
139    pub symbol: String,
140    pub tick_direction: TickDirection,
141    pub last_price: Decimal,
142    #[serde(default, deserialize_with = "option_decimal")]
143    pub pre_open_price: Option<Decimal>,
144    #[serde(default, deserialize_with = "option_decimal")]
145    pub pre_qty: Option<Decimal>,
146    #[serde(default, deserialize_with = "empty_string_as_none")]
147    pub cur_pre_listing_phase: Option<String>,
148    pub prev_price24h: Decimal,
149    pub price24h_pcnt: Decimal,
150    pub high_price24h: Decimal,
151    pub low_price24h: Decimal,
152    pub prev_price1h: Decimal,
153    pub mark_price: Decimal,
154    pub index_price: Decimal,
155    pub open_interest: Decimal,
156    pub open_interest_value: Decimal,
157    pub turnover24h: Decimal,
158    pub volume24h: Decimal,
159    pub funding_rate: Decimal,
160    #[serde(default, deserialize_with = "number")]
161    pub next_funding_time: Timestamp,
162    pub bid1_price: Decimal,
163    pub bid1_size: Decimal,
164    pub ask1_price: Decimal,
165    pub ask1_size: Decimal,
166    #[serde(default, deserialize_with = "option_number")]
167    pub delivery_time: Option<Timestamp>,
168    #[serde(default, deserialize_with = "option_decimal")]
169    pub basis_rate: Option<Decimal>,
170    #[serde(default, deserialize_with = "option_decimal")]
171    pub delivery_fee_rate: Option<Decimal>,
172    #[serde(default, deserialize_with = "option_decimal")]
173    pub predicted_delivery_price: Option<Decimal>,
174}
175
176#[derive(PartialEq, Deserialize, Debug)]
177#[serde(rename_all = "camelCase")]
178pub struct TickerDeltaMsg {
179    pub symbol: String,
180    #[serde(default, deserialize_with = "empty_string_as_none")]
181    pub tick_direction: Option<TickDirection>,
182    #[serde(default, deserialize_with = "option_decimal")]
183    pub last_price: Option<Decimal>,
184    #[serde(default, deserialize_with = "option_decimal")]
185    pub pre_open_price: Option<Decimal>,
186    #[serde(default, deserialize_with = "option_decimal")]
187    pub pre_qty: Option<Decimal>,
188    #[serde(default, deserialize_with = "empty_string_as_none")]
189    pub cur_pre_listing_phase: Option<String>,
190    #[serde(default, deserialize_with = "option_decimal")]
191    pub prev_price24h: Option<Decimal>,
192    #[serde(default, deserialize_with = "option_decimal")]
193    pub price24h_pcnt: Option<Decimal>,
194    #[serde(default, deserialize_with = "option_decimal")]
195    pub high_price24h: Option<Decimal>,
196    #[serde(default, deserialize_with = "option_decimal")]
197    pub low_price24h: Option<Decimal>,
198    #[serde(default, deserialize_with = "option_decimal")]
199    pub prev_price1h: Option<Decimal>,
200    #[serde(default, deserialize_with = "option_decimal")]
201    pub mark_price: Option<Decimal>,
202    #[serde(default, deserialize_with = "option_decimal")]
203    pub index_price: Option<Decimal>,
204    #[serde(default, deserialize_with = "option_decimal")]
205    pub open_interest: Option<Decimal>,
206    #[serde(default, deserialize_with = "option_decimal")]
207    pub open_interest_value: Option<Decimal>,
208    #[serde(default, deserialize_with = "option_decimal")]
209    pub turnover24h: Option<Decimal>,
210    #[serde(default, deserialize_with = "option_decimal")]
211    pub volume24h: Option<Decimal>,
212    #[serde(default, deserialize_with = "option_decimal")]
213    pub funding_rate: Option<Decimal>,
214    #[serde(default, deserialize_with = "option_decimal")]
215    pub next_funding_time: Option<Decimal>,
216    #[serde(default, deserialize_with = "option_decimal")]
217    pub bid1_price: Option<Decimal>,
218    #[serde(default, deserialize_with = "option_decimal")]
219    pub bid1_size: Option<Decimal>,
220    #[serde(default, deserialize_with = "option_decimal")]
221    pub ask1_price: Option<Decimal>,
222    #[serde(default, deserialize_with = "option_decimal")]
223    pub ask1_size: Option<Decimal>,
224    #[serde(default, deserialize_with = "option_number")]
225    pub delivery_time: Option<Timestamp>,
226    #[serde(default, deserialize_with = "option_decimal")]
227    pub basis_rate: Option<Decimal>,
228    #[serde(default, deserialize_with = "option_decimal")]
229    pub delivery_fee_rate: Option<Decimal>,
230    #[serde(default, deserialize_with = "option_decimal")]
231    pub predicted_delivery_price: Option<Decimal>,
232}
233
234// TODO: Use PublicMsg<T>
235#[derive(PartialEq, Deserialize, Debug)]
236#[serde(tag = "type")]
237pub enum TradeMsg {
238    #[serde(rename = "snapshot")]
239    Snapshot {
240        #[serde(default, deserialize_with = "empty_string_as_none")]
241        id: Option<String>,
242        topic: Topic,
243        ts: Timestamp,
244        data: Vec<TradeSnapshotMsg>,
245    },
246}
247
248#[derive(PartialEq, Deserialize, Debug)]
249pub struct TradeSnapshotMsg {
250    #[serde(rename = "T")]
251    pub time: Timestamp,
252    #[serde(rename = "s")]
253    pub symbol: String,
254    #[serde(rename = "S")]
255    pub side: Side,
256    #[serde(rename = "v")]
257    pub size: Decimal,
258    #[serde(rename = "p")]
259    pub price: Decimal,
260    #[serde(rename = "L")]
261    pub tick_direction: TickDirection,
262    #[serde(rename = "i")]
263    pub trade_id: String,
264    #[serde(rename = "BT")]
265    pub block_trade: bool,
266    #[serde(rename = "RPI")]
267    pub rpi_trade: Option<bool>,
268    #[serde(rename = "mP", default, deserialize_with = "empty_string_as_none")]
269    pub mark_price: Option<String>,
270    #[serde(rename = "iP", default, deserialize_with = "empty_string_as_none")]
271    pub index_price: Option<String>,
272    #[serde(rename = "mlv", default, deserialize_with = "empty_string_as_none")]
273    pub mark_iv: Option<String>,
274    #[serde(rename = "iv", default, deserialize_with = "empty_string_as_none")]
275    pub iv: Option<String>,
276}
277
278// TODO: Use PublicMsg<T>
279#[derive(PartialEq, Deserialize, Debug)]
280#[serde(tag = "type")]
281pub enum KLineMsg {
282    #[serde(rename = "snapshot")]
283    Snapshot {
284        topic: Topic,
285        ts: Timestamp,
286        data: Vec<KLineSnapshotMsg>,
287    },
288}
289
290#[derive(PartialEq, Deserialize, Debug)]
291pub struct KLineSnapshotMsg {
292    pub start: Timestamp,
293    pub end: Timestamp,
294    pub interval: Interval,
295    pub open: Decimal,
296    pub close: Decimal,
297    pub high: Decimal,
298    pub low: Decimal,
299    pub volume: Decimal,
300    pub turnover: Decimal,
301    pub confirm: bool,
302    pub timestamp: Timestamp,
303}
304
305// TODO: Use PublicMsg<T>
306#[derive(PartialEq, Deserialize, Debug)]
307#[serde(tag = "type")]
308pub enum AllLiquidationMsg {
309    #[serde(rename = "snapshot")]
310    Snapshot {
311        topic: Topic,
312        ts: Timestamp,
313        data: Vec<AllLiquidationSnapshotMsg>,
314    },
315}
316
317#[derive(PartialEq, Deserialize, Debug)]
318pub struct AllLiquidationSnapshotMsg {
319    #[serde(rename = "T")]
320    pub time: Timestamp,
321    #[serde(rename = "s")]
322    pub symbol: String,
323    /// When you receive a Buy update, this means that a long position has been liquidated
324    #[serde(rename = "S")]
325    pub side: Side,
326    #[serde(rename = "v")]
327    pub size: Decimal,
328    #[serde(rename = "p")]
329    pub price: Decimal,
330}
331
332#[derive(PartialEq, Deserialize, Debug)]
333#[serde(tag = "topic")] // TODO: Use field topic
334pub enum TopicMessage {
335    #[serde(rename = "order")]
336    Order(PrivateMsg<Vec<OrderMsg>>),
337    #[serde(rename = "position")]
338    Position(PrivateMsg<Vec<PositionMsg>>),
339    #[serde(rename = "wallet")]
340    Wallet(PrivateMsg<Vec<WalletMsg>>),
341    #[serde(rename = "execution")]
342    Execution(PrivateMsg<Vec<ExecutionMsg>>),
343}
344
345#[derive(PartialEq, Deserialize, Debug)]
346#[serde(rename_all = "camelCase")]
347pub struct PublicMsg<T> {
348    #[serde(default, deserialize_with = "empty_string_as_none")]
349    id: Option<String>,
350    #[serde(default, deserialize_with = "option_number")]
351    cs: Option<u64>,
352    ts: Timestamp,
353    data: T,
354}
355
356#[derive(PartialEq, Deserialize, Debug)]
357#[serde(rename_all = "camelCase")]
358pub struct PrivateMsg<T> {
359    // TODO: pub topic: Topic, /// Topic name
360    /// Message ID
361    pub id: String,
362    /// Data created timestamp (ms)
363    pub creation_time: Timestamp,
364    pub data: T,
365}
366
367#[derive(PartialEq, Deserialize, Debug, Clone)]
368#[serde(rename_all = "camelCase")]
369pub struct OrderMsg {
370    /// Product type
371    /// UTA2.0, UTA1.0: spot, linear, inverse, option
372    /// Classic account: spot, linear, inverse.
373    pub category: Category,
374    /// Order ID
375    pub order_id: String,
376    /// User customized order ID
377    #[serde(default, deserialize_with = "empty_string_as_none")]
378    pub order_link_id: Option<String>,
379    /// Whether to borrow.
380    /// Unified spot only. 0: false, 1: true.
381    /// Classic spot is not supported, always 0
382    #[serde(default, deserialize_with = "string_to_option_bool")]
383    pub is_leverage: Option<bool>,
384    /// Block trade ID
385    #[serde(default, deserialize_with = "empty_string_as_none")]
386    pub block_trade_id: Option<String>,
387    /// Symbol name
388    pub symbol: String,
389    /// Order price
390    pub price: Decimal,
391    /// Dedicated field for EU liquidity provider
392    #[serde(default, deserialize_with = "option_decimal")]
393    pub broker_order_price: Option<Decimal>,
394    /// Order qty
395    pub qty: Decimal,
396    /// Side. Buy,Sell
397    pub side: Side,
398    /// Position index. Used to identify positions in different position modes.
399    pub position_idx: PositionIdx,
400    /// Order status
401    pub order_status: OrderStatus,
402    /// Order create type
403    /// Only for category=linear or inverse
404    /// Spot, Option do not have this key
405    #[serde(default, deserialize_with = "empty_string_as_none")]
406    pub create_type: Option<CreateType>,
407    /// Cancel type
408    pub cancel_type: CancelType,
409    /// Reject reason. Classic spot is not supported
410    pub reject_reason: RejectReason,
411    /// Average filled price
412    /// returns "" for those orders without avg price, and also for those classic account orders have partilly filled but cancelled at the end
413    /// Classic Spot: not supported, always ""
414    #[serde(default, deserialize_with = "option_decimal")]
415    pub avg_price: Option<Decimal>,
416    /// The remaining qty not executed. Classic spot is not supported
417    #[serde(default, deserialize_with = "option_decimal")]
418    pub leaves_qty: Option<Decimal>,
419    /// The estimated value not executed. Classic spot is not supported
420    #[serde(default, deserialize_with = "option_decimal")]
421    pub leaves_value: Option<Decimal>,
422    /// Cumulative executed order qty
423    pub cum_exec_qty: Decimal,
424    /// Cumulative executed order value.
425    pub cum_exec_value: Decimal,
426    /// Cumulative executed trading fee.
427    /// Classic spot: it is the latest execution fee for order.
428    /// After upgraded to the Unified account, you can use execFee for each fill in Execution topic
429    pub cum_exec_fee: Decimal,
430    /// linear, spot: Cumulative trading fee details instead of cumExecFee
431    pub cum_fee_detail: Option<serde_json::Value>,
432    /// Closed profit and loss for each close position order. The figure is the same as "closedPnl" from Get Closed PnL
433    pub closed_pnl: Decimal,
434    /// Trading fee currency for Spot only. Please understand Spot trading fee currency here
435    #[serde(deserialize_with = "option_decimal")]
436    pub fee_currency: Option<Decimal>,
437    /// Time in force
438    pub time_in_force: TimeInForce,
439    /// Order type. Market,Limit. For TP/SL order, it means the order type after triggered
440    pub order_type: OrderType,
441    /// Stop order type
442    #[serde(default, deserialize_with = "empty_string_as_none")]
443    pub stop_order_type: Option<StopOrderType>,
444    /// The trigger type of Spot OCO order.OcoTriggerByUnknown, OcoTriggerByTp, OcoTriggerByBySl. Classic spot is not supported
445    #[serde(default, deserialize_with = "empty_string_as_none")]
446    pub oco_trigger_by: Option<OcoTriggerBy>,
447    /// Implied volatility
448    #[serde(deserialize_with = "option_decimal")]
449    pub order_iv: Option<Decimal>,
450    /// The unit for qty when create Spot market orders for UTA account. baseCoin, quoteCoin
451    #[serde(default, deserialize_with = "empty_string_as_none")]
452    pub market_unit: Option<String>,
453    /// Spot and Futures market order slippage tolerance type TickSize, Percent, UNKNOWN(default)
454    #[serde(default, deserialize_with = "empty_string_as_none")]
455    pub slippage_tolerance_type: Option<SlippageToleranceType>,
456    /// Slippage tolerance value
457    #[serde(default, deserialize_with = "option_decimal")]
458    pub slippage_tolerance: Option<Decimal>,
459    /// Trigger price. If stopOrderType=TrailingStop, it is activate price. Otherwise, it is trigger price
460    #[serde(deserialize_with = "option_decimal")]
461    pub trigger_price: Option<Decimal>,
462    /// Take profit price
463    #[serde(deserialize_with = "option_decimal")]
464    pub take_profit: Option<Decimal>,
465    /// Stop loss price
466    #[serde(deserialize_with = "option_decimal")]
467    pub stop_loss: Option<Decimal>,
468    /// TP/SL mode, Full: entire position for TP/SL. Partial: partial position tp/sl. Spot does not have this field, and Option returns always ""
469    #[serde(default, deserialize_with = "empty_string_as_none")]
470    pub tpsl_mode: Option<TpslMode>,
471    /// The limit order price when take profit price is triggered
472    #[serde(deserialize_with = "option_decimal")]
473    pub tp_limit_price: Option<Decimal>,
474    /// The limit order price when stop loss price is triggered
475    #[serde(deserialize_with = "option_decimal")]
476    pub sl_limit_price: Option<Decimal>,
477    /// The price type to trigger take profit
478    #[serde(default, deserialize_with = "empty_string_as_none")]
479    pub tp_trigger_by: Option<TriggerBy>,
480    /// The price type to trigger stop loss
481    #[serde(default, deserialize_with = "empty_string_as_none")]
482    pub sl_trigger_by: Option<TriggerBy>,
483    /// Trigger direction. 1: rise, 2: fall
484    pub trigger_direction: TriggerDirection,
485    /// The price type of trigger price
486    #[serde(default, deserialize_with = "empty_string_as_none")]
487    pub trigger_by: Option<TriggerBy>,
488    /// Last price when place the order, Spot is not applicable
489    #[serde(deserialize_with = "option_decimal")]
490    pub last_price_on_created: Option<Decimal>,
491    /// Reduce only. true means reduce position size
492    pub reduce_only: bool,
493    /// Close on trigger.
494    pub close_on_trigger: bool,
495    /// Place type, option used. iv, price
496    #[serde(default, deserialize_with = "empty_string_as_none")]
497    pub place_type: Option<PlaceType>,
498    /// SMP execution type
499    pub smp_type: SmpType,
500    /// Smp group ID. If the UID has no group, it is 0 by default
501    #[serde(deserialize_with = "number")]
502    pub smp_group: i64,
503    /// The counterparty's orderID which triggers this SMP execution
504    #[serde(default, deserialize_with = "empty_string_as_none")]
505    pub smp_order_id: Option<String>,
506    /// Order created timestamp (ms)
507    #[serde(deserialize_with = "number")]
508    pub created_time: Timestamp,
509    /// Order updated timestamp (ms)
510    #[serde(deserialize_with = "number")]
511    pub updated_time: Timestamp,
512}
513
514#[derive(PartialEq, Deserialize, Debug, Clone)]
515#[serde(rename_all = "camelCase")]
516pub struct PositionMsg {
517    /// Product type
518    pub category: Category,
519    /// Symbol name
520    pub symbol: String,
521    /// Position side. Buy: long, Sell: short
522    /// one-way mode: classic & UTA1.0(inverse), an empty position returns None.
523    /// UTA2.0(linear, inverse) & UTA1.0(linear): either one-way or hedge mode returns an empty string "" for an empty position.
524    #[serde(default, deserialize_with = "empty_string_as_none")]
525    pub side: Option<Side>,
526    /// Position size
527    pub size: Decimal,
528    /// Used to identify positions in different position modes
529    pub position_idx: PositionIdx,
530    /// Position value
531    pub position_value: Decimal,
532    /// Risk tier ID
533    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
534    #[serde(deserialize_with = "number")]
535    pub risk_id: i64,
536    /// Risk limit value
537    /// for portfolio margin mode, this field returns 0, which means risk limit rules are invalid
538    #[serde(default, deserialize_with = "option_decimal")]
539    pub risk_limit_value: Option<Decimal>,
540    /// Entry price
541    pub entry_price: Decimal,
542    /// Mark price
543    pub mark_price: Decimal,
544    /// Position leverage
545    /// for portfolio margin mode, this field returns "", which means leverage rules are invalid
546    pub leverage: Decimal,
547    /// Whether to add margin automatically. 0: false, 1: true. For UTA, it is meaningful only when UTA enables ISOLATED_MARGIN
548    #[serde(default, deserialize_with = "int_to_bool")]
549    pub auto_add_margin: bool,
550    /// Initial margin, the same value as positionIMByMp, please note this change The New Margin Calculation: Adjustments and Implications
551    /// Portfolio margin mode: returns ""
552    #[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
553    pub position_im: Option<Decimal>,
554    /// Maintenance margin, the same value as positionMMByMp
555    /// Portfolio margin mode: returns ""
556    #[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
557    pub position_mm: Option<Decimal>,
558    /// Initial margin calculated by mark price, the same value as positionIM
559    /// Portfolio margin mode: returns ""
560    #[serde(
561        rename = "positionIMByMp",
562        default,
563        deserialize_with = "option_decimal"
564    )]
565    pub position_im_by_mp: Option<Decimal>,
566    /// Maintenance margin calculated by mark price, the same value as positionMM
567    /// Portfolio margin mode: returns ""
568    #[serde(
569        rename = "positionMMByMp",
570        default,
571        deserialize_with = "option_decimal"
572    )]
573    pub position_mm_by_mp: Option<Decimal>,
574    /// Position liquidation price
575    /// Isolated margin:
576    /// it is the real price for isolated and cross positions, and keeps "" when liqPrice <= minPrice or liqPrice >= maxPrice
577    /// Cross margin:
578    /// 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
579    /// this field is empty for Portfolio Margin Mode, and no liquidation price will be provided
580    #[serde(default, deserialize_with = "option_decimal")]
581    pub liq_price: Option<Decimal>,
582    /// Take profit price
583    pub take_profit: Decimal,
584    /// Stop loss price
585    pub stop_loss: Decimal,
586    /// Trailing stop
587    pub trailing_stop: Decimal,
588    /// Unrealised profit and loss
589    pub unrealised_pnl: Decimal,
590    /// The realised PnL for the current holding position
591    pub cur_realised_pnl: Decimal,
592    /// USDC contract session avg price, it is the same figure as avg entry price shown in the web UI
593    #[serde(default, deserialize_with = "option_decimal")]
594    pub session_avg_price: Option<Decimal>,
595    /// Delta
596    #[serde(default, deserialize_with = "empty_string_as_none")]
597    pub delta: Option<String>,
598    /// Gamma
599    #[serde(default, deserialize_with = "empty_string_as_none")]
600    pub gamma: Option<String>,
601    /// Vega
602    #[serde(default, deserialize_with = "empty_string_as_none")]
603    pub vega: Option<String>,
604    /// Theta
605    #[serde(default, deserialize_with = "empty_string_as_none")]
606    pub theta: Option<String>,
607    /// Cumulative realised pnl
608    /// Futures & Perp: it is the all time cumulative realised P&L
609    /// Option: it is the realised P&L when you hold that position
610    pub cum_realised_pnl: Decimal,
611    /// Position status. Normal, Liq, Adl
612    pub position_status: PositionStatus,
613    /// Auto-deleverage rank indicator. What is Auto-Deleveraging?
614    pub adl_rank_indicator: AdlRankIndicator,
615    /// Useful when Bybit lower the risk limit
616    /// 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
617    /// false: There is no restriction, and it means your position is under the risk when the risk limit is systematically adjusted
618    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
619    pub is_reduce_only: bool,
620    /// Useful when Bybit lower the risk limit
621    /// When isReduceOnly=true: the timestamp (ms) when the MMR will be forcibly adjusted by the system
622    /// When isReduceOnly=false: the timestamp when the MMR had been adjusted by system
623    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
624    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
625    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
626    #[serde(deserialize_with = "option_number")]
627    pub mmr_sys_updated_time: Option<Timestamp>,
628    /// Useful when Bybit lower the risk limit
629    /// When isReduceOnly=true: the timestamp (ms) when the leverage will be forcibly adjusted by the system
630    /// When isReduceOnly=false: the timestamp when the leverage had been adjusted by system
631    /// It returns the timestamp when the system operates, and if you manually operate, there is no timestamp
632    /// Keeps "" by default, if there was a lower risk limit system adjustment previously, it shows that system operation timestamp
633    /// Only meaningful for isolated margin & cross margin of USDT Perp, USDC Perp, USDC Futures, Inverse Perp and Inverse Futures, meaningless for others
634    #[serde(deserialize_with = "option_number")]
635    pub leverage_sys_updated_time: Option<Timestamp>,
636    /// Timestamp of the first time a position was created on this symbol (ms)
637    #[serde(deserialize_with = "number")]
638    pub created_time: Timestamp,
639    /// Position data updated timestamp (ms)
640    #[serde(deserialize_with = "number")]
641    pub updated_time: Timestamp,
642    /// Cross sequence, used to associate each fill and each position update
643    /// Different symbols may have the same seq, please use seq + symbol to check unique
644    /// Returns "-1" if the symbol has never been traded
645    /// Returns the seq updated by the last transaction when there are setting like leverage, risk limit
646    pub seq: i64,
647}
648
649#[derive(PartialEq, Deserialize, Debug, Clone)]
650#[serde(rename_all = "camelCase")]
651pub struct WalletMsg {
652    /// Account type.
653    /// UTA2.0: UNIFIED
654    /// UTA1.0: UNIFIED (spot/linear/options), CONTRACT(inverse)
655    /// Classic: CONTRACT, SPOT
656    pub account_type: AccountType,
657    /// Account IM rate
658    /// You can refer to this Glossary to understand the below fields calculation and mearning
659    /// All below account wide fields are not applicable to
660    /// UTA2.0(isolated margin),
661    /// UTA1.0(isolated margin), UTA1.0(CONTRACT),
662    /// classic account(SPOT, CONTRACT)
663    #[serde(rename = "accountIMRate")]
664    pub account_im_rate: Decimal,
665    /// Account MM rate
666    #[serde(rename = "accountMMRate")]
667    pub account_mm_rate: Decimal,
668    /// Account total equity (USD)
669    pub total_equity: Decimal,
670    /// Account wallet balance (USD): ∑Asset Wallet Balance By USD value of each asset
671    pub total_wallet_balance: Decimal,
672    /// Account margin balance (USD): totalWalletBalance + totalPerpUPL
673    pub total_margin_balance: Decimal,
674    /// Account available balance (USD), Cross Margin: totalMarginBalance - totalInitialMargin
675    pub total_available_balance: Decimal,
676    /// Account Perps and Futures unrealised p&l (USD): ∑Each Perp and USDC Futures upl by base coin
677    #[serde(rename = "totalPerpUPL")]
678    pub total_perp_upl: Decimal,
679    /// Account initial margin (USD): ∑Asset Total Initial Margin Base Coin
680    pub total_initial_margin: Decimal,
681    /// Account maintenance margin (USD): ∑ Asset Total Maintenance Margin Base Coin
682    pub total_maintenance_margin: Decimal,
683    /// You can ignore this field, and refer to accountIMRate, which has the same calculation
684    #[serde(rename = "accountIMRateByMp")]
685    pub account_im_rate_by_mp: Decimal,
686    /// You can ignore this field, and refer to accountMMRate, which has the same calculation
687    #[serde(rename = "accountMMRateByMp")]
688    pub account_mm_rate_by_mp: Decimal,
689    /// You can ignore this field, and refer to totalInitialMargin, which has the same calculation
690    #[serde(rename = "totalInitialMarginByMp")]
691    pub total_initial_margin_by_mp: Decimal,
692    /// You can ignore this field, and refer to totalMaintenanceMargin, which has the same calculation
693    #[serde(rename = "totalMaintenanceMarginByMp")]
694    pub total_maintenance_margin_by_mp: Decimal,
695    #[serde(deserialize_with = "hash_map")]
696    pub coin: HashMap<String, WalletCoin>,
697}
698
699#[derive(PartialEq, Deserialize, Debug, Clone)]
700#[serde(rename_all = "camelCase")]
701pub struct ExecutionMsg {
702    /// Product type spot, linear, inverse, option
703    pub category: Category,
704    /// Symbol name
705    pub symbol: String,
706    /// Whether to borrow. 0: false, 1: true
707    #[serde(default, deserialize_with = "string_to_bool")]
708    pub is_leverage: bool,
709    /// Order ID
710    pub order_id: String,
711    /// User customized order ID
712    #[serde(default, deserialize_with = "empty_string_as_none")]
713    pub order_link_id: Option<String>,
714    /// Side. Buy,Sell
715    pub side: Side,
716    /// Order price
717    pub order_price: Decimal,
718    /// Order qty
719    pub order_qty: Decimal,
720    /// The remaining qty not executed
721    pub leaves_qty: Decimal,
722    /// Order create type
723    /// Spot, Option do not have this key
724    pub create_type: CreateType,
725    /// Order type. Market,Limit
726    pub order_type: OrderType,
727    /// Stop order type. If the order is not stop order, any type is not returned
728    pub stop_order_type: StopOrderType,
729    /// Executed trading fee. You can get spot fee currency instruction here
730    pub exec_fee: Decimal,
731    /// Execution ID
732    pub exec_id: String,
733    /// Execution price
734    pub exec_price: Decimal,
735    /// Execution qty
736    pub exec_qty: Decimal,
737    /// Profit and Loss for each close position execution. The value keeps consistent with the field "cashFlow" in the Get Transaction Log
738    pub exec_pnl: Decimal,
739    /// Executed type
740    pub exec_type: ExecType,
741    /// Executed order value
742    pub exec_value: Decimal,
743    /// Executed timestamp (ms)
744    #[serde(deserialize_with = "number")]
745    pub exec_time: Timestamp,
746    /// Is maker order. true: maker, false: taker
747    pub is_maker: bool,
748    /// Trading fee rate
749    pub fee_rate: Decimal,
750    /// Implied volatility. valid for option
751    #[serde(default, deserialize_with = "option_decimal")]
752    pub trade_iv: Option<Decimal>,
753    /// Implied volatility of mark price. valid for option
754    #[serde(default, deserialize_with = "option_decimal")]
755    pub mark_iv: Option<Decimal>,
756    /// The mark price of the symbol when executing. valid for option
757    pub mark_price: Decimal,
758    /// The index price of the symbol when executing. valid for option
759    #[serde(default, deserialize_with = "option_decimal")]
760    pub index_price: Option<Decimal>,
761    /// The underlying price of the symbol when executing. valid for option
762    #[serde(default, deserialize_with = "option_decimal")]
763    pub underlying_price: Option<Decimal>,
764    /// Paradigm block trade ID
765    #[serde(default, deserialize_with = "empty_string_as_none")]
766    pub block_trade_id: Option<String>,
767    /// Closed position size
768    pub closed_size: Decimal,
769    /// 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
770    pub extra_fees: Option<Vec<ExtraFee>>, // TODO: !!! ignore if empty string !!!
771    /// Cross sequence, used to associate each fill and each position update
772    /// The seq will be the same when conclude multiple transactions at the same time
773    /// Different symbols may have the same seq, please use seq + symbol to check unique
774    pub seq: i64,
775    /// Trading fee currency
776    pub fee_currency: String,
777}
778
779#[derive(PartialEq, Deserialize, Debug, Clone)]
780#[serde(rename_all = "camelCase")]
781pub struct ExtraFee {
782    pub fee_coin: String,
783    pub fee_type: ExtraFeeType,
784    pub sub_fee_type: ExtraSubFeeType,
785    pub fee_rate: Decimal,
786    pub fee: Decimal,
787}
788
789#[cfg(test)]
790mod tests {
791    use rust_decimal::dec;
792
793    use crate::serde::{Unique, deserialize_json};
794
795    use super::*;
796
797    #[test]
798    fn deserialize_incoming_message_command_subscribe() {
799        let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"subscribe"}"#;
800        let expected = IncomingMessage::Command(CommandMsg::Subscribe {
801            req_id: None,
802            ret_msg: None,
803            conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
804            success: true,
805        });
806
807        let message = deserialize_json(json).unwrap();
808
809        assert_eq!(expected, message);
810    }
811
812    #[test]
813    fn deserialize_incoming_message_command_unsubscribe() {
814        let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"unsubscribe"}"#;
815        let expected = IncomingMessage::Command(CommandMsg::Unsubscribe {
816            req_id: None,
817            ret_msg: None,
818            conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
819            success: true,
820        });
821
822        let message = deserialize_json(json).unwrap();
823
824        assert_eq!(expected, message);
825    }
826
827    #[test]
828    fn deserialize_incoming_message_ticker_delta() {
829        let json = r#"{
830		    "topic": "tickers.BTCUSDT",
831		    "type": "delta",
832		    "data": {
833		        "symbol": "BTCUSDT",
834		        "tickDirection": "PlusTick",
835		        "price24hPcnt": "-0.015895",
836		        "lastPrice": "63948.50",
837		        "turnover24h": "6793884423.5518",
838		        "volume24h": "105991.3760",
839		        "bid1Price": "63948.40",
840		        "bid1Size": "3.439",
841		        "ask1Price": "63948.50",
842		        "ask1Size": "2.566"
843		    },
844		    "cs": 195377749067,
845		    "ts": 1718995014034
846		}"#;
847        let ticker_delta = TickerMsg::Delta {
848            topic: Topic::Ticker(String::from("BTCUSDT")),
849            cs: Some(195377749067),
850            ts: 1718995014034,
851            data: TickerDeltaMsg {
852                symbol: String::from("BTCUSDT"),
853                tick_direction: Some(TickDirection::PlusTick),
854                last_price: Some(dec!(63948.5)),
855                pre_open_price: None,
856                pre_qty: None,
857                cur_pre_listing_phase: None,
858                prev_price24h: None,
859                price24h_pcnt: Some(dec!(-0.015895)),
860                high_price24h: None,
861                low_price24h: None,
862                prev_price1h: None,
863                mark_price: None,
864                index_price: None,
865                open_interest: None,
866                open_interest_value: None,
867                turnover24h: Some(dec!(6793884423.5518)),
868                volume24h: Some(dec!(105991.376)),
869                funding_rate: None,
870                next_funding_time: None,
871                bid1_price: Some(dec!(63948.4)),
872                bid1_size: Some(dec!(3.439)),
873                ask1_price: Some(dec!(63948.5)),
874                ask1_size: Some(dec!(2.566)),
875                delivery_time: None,
876                basis_rate: None,
877                delivery_fee_rate: None,
878                predicted_delivery_price: None,
879            },
880        };
881        let expected = IncomingMessage::Ticker(Box::new(ticker_delta));
882
883        let message = deserialize_json(json).unwrap();
884
885        assert_eq!(expected, message);
886    }
887
888    #[test]
889    fn deserialize_incoming_message_ticker_snapshot() {
890        // Category: linear.
891        let json = r#"{
892		    "topic": "tickers.BTCUSDT",
893		    "type": "snapshot",
894		    "data": {
895                "symbol":"BTCUSDT",
896                "tickDirection":"ZeroPlusTick",
897                "price24hPcnt":"-0.044555",
898                "lastPrice":"84594.40",
899                "prevPrice24h":"88539.30",
900                "highPrice24h":"89389.90",
901                "lowPrice24h":"82055.60",
902                "prevPrice1h":"84307.20",
903                "markPrice":"84594.00",
904                "indexPrice":"84650.47",
905                "openInterest":"52903.75",
906                "openInterestValue":"4475339827.50",
907                "turnover24h":"17166562011.6514",
908                "volume24h":"200176.9910",
909                "nextFundingTime":"1740643200000",
910                "fundingRate":"-0.00016974",
911                "bid1Price":"84594.30",
912                "bid1Size":"6.777",
913                "ask1Price":"84594.40",
914                "ask1Size":"0.660",
915                "preOpenPrice":"",
916                "preQty":"",
917                "curPreListingPhase":""
918		    },
919		    "cs": 337149693308,
920		    "ts": 1740622194359
921		}"#;
922        let ticker_snapshot = TickerMsg::Snapshot {
923            topic: Topic::Ticker(String::from("BTCUSDT")),
924            cs: Some(337149693308),
925            ts: 1740622194359,
926            data: TickerSnapshotMsg {
927                symbol: String::from("BTCUSDT"),
928                tick_direction: TickDirection::ZeroPlusTick,
929                last_price: dec!(84594.40),
930                pre_open_price: None,
931                pre_qty: None,
932                cur_pre_listing_phase: None,
933                prev_price24h: dec!(88539.30),
934                price24h_pcnt: dec!(-0.044555),
935                high_price24h: dec!(89389.90),
936                low_price24h: dec!(82055.60),
937                prev_price1h: dec!(84307.20),
938                mark_price: dec!(84594.00),
939                index_price: dec!(84650.47),
940                open_interest: dec!(52903.75),
941                open_interest_value: dec!(4475339827.50),
942                turnover24h: dec!(17166562011.6514),
943                volume24h: dec!(200176.9910),
944                funding_rate: dec!(-0.00016974),
945                next_funding_time: 1740643200000,
946                bid1_price: dec!(84594.30),
947                bid1_size: dec!(6.777),
948                ask1_price: dec!(84594.40),
949                ask1_size: dec!(0.660),
950                delivery_time: None,
951                basis_rate: None,
952                delivery_fee_rate: None,
953                predicted_delivery_price: None,
954            },
955        };
956        let expected = IncomingMessage::Ticker(Box::new(ticker_snapshot));
957
958        let message = deserialize_json(json).unwrap();
959
960        assert_eq!(expected, message);
961    }
962
963    #[test]
964    fn deserialize_incoming_message_trade_snapshot() {
965        // Category: linear.
966        let json = r#"{
967            "topic":"publicTrade.BTCUSDT",
968            "type":"snapshot",
969            "ts":1741433245359,
970            "data":[
971                {
972                    "T":1741433245357,
973                    "s":"BTCUSDT",
974                    "S":"Buy",
975                    "v":"0.007",
976                    "p":"85821.00",
977                    "L":"PlusTick",
978                    "i":"485eaa70-df6e-5260-bbef-4f7324e3c5d9",
979                    "BT":false
980                }
981            ]
982        }"#;
983        let expected = IncomingMessage::Trade(TradeMsg::Snapshot {
984            id: None,
985            topic: Topic::Trade(String::from("BTCUSDT")),
986            ts: 1741433245359,
987            data: vec![TradeSnapshotMsg {
988                time: 1741433245357,
989                symbol: String::from("BTCUSDT"),
990                side: Side::Buy,
991                size: dec!(0.007),
992                price: dec!(85821.00),
993                tick_direction: TickDirection::PlusTick,
994                trade_id: String::from("485eaa70-df6e-5260-bbef-4f7324e3c5d9"),
995                block_trade: false,
996                rpi_trade: None,
997                mark_price: None,
998                index_price: None,
999                mark_iv: None,
1000                iv: None,
1001            }],
1002        });
1003
1004        let message = deserialize_json(json).unwrap();
1005
1006        assert_eq!(expected, message);
1007    }
1008
1009    #[test]
1010    fn deserialize_incoming_message_all_liquidation_snapshot() {
1011        // Category: linear.
1012        let json = r#"{
1013            "topic":"allLiquidation.BTCUSDT",
1014            "type":"snapshot",
1015            "ts":1741450605553,
1016            "data":[
1017                {
1018                    "T":1741450605236,
1019                    "s":"BTCUSDT",
1020                    "S":"Buy",
1021                    "v":"0.001",
1022                    "p":"85823.60"
1023                }
1024            ]
1025        }"#;
1026        let expected = AllLiquidationMsg::Snapshot {
1027            topic: Topic::AllLiquidation(String::from("BTCUSDT")),
1028            ts: 1741450605553,
1029            data: vec![AllLiquidationSnapshotMsg {
1030                time: 1741450605236,
1031                symbol: String::from("BTCUSDT"),
1032                side: Side::Buy,
1033                size: dec!(0.001),
1034                price: dec!(85823.60),
1035            }],
1036        };
1037
1038        let message = deserialize_json(json).unwrap();
1039
1040        assert_eq!(expected, message);
1041    }
1042
1043    #[test]
1044    fn deserialize_incoming_message_order() {
1045        let json = r#"{
1046            "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90",
1047            "topic": "order",
1048            "creationTime": 1672364262474,
1049            "data": [
1050                {
1051                    "symbol": "ETH-30DEC22-1400-C",
1052                    "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020",
1053                    "side": "Sell",
1054                    "orderType": "Market",
1055                    "cancelType": "UNKNOWN",
1056                    "price": "72.5",
1057                    "qty": "1",
1058                    "orderIv": "",
1059                    "timeInForce": "IOC",
1060                    "orderStatus": "Filled",
1061                    "orderLinkId": "",
1062                    "lastPriceOnCreated": "",
1063                    "reduceOnly": false,
1064                    "leavesQty": "",
1065                    "leavesValue": "",
1066                    "cumExecQty": "1",
1067                    "cumExecValue": "75",
1068                    "avgPrice": "75",
1069                    "blockTradeId": "",
1070                    "positionIdx": 0,
1071                    "cumExecFee": "0.358635",
1072                    "closedPnl": "0",
1073                    "createdTime": "1672364262444",
1074                    "updatedTime": "1672364262457",
1075                    "rejectReason": "EC_NoError",
1076                    "stopOrderType": "",
1077                    "tpslMode": "",
1078                    "triggerPrice": "",
1079                    "takeProfit": "",
1080                    "stopLoss": "",
1081                    "tpTriggerBy": "",
1082                    "slTriggerBy": "",
1083                    "tpLimitPrice": "",
1084                    "slLimitPrice": "",
1085                    "triggerDirection": 0,
1086                    "triggerBy": "",
1087                    "closeOnTrigger": false,
1088                    "category": "option",
1089                    "placeType": "price",
1090                    "smpType": "None",
1091                    "smpGroup": 0,
1092                    "smpOrderId": "",
1093                    "feeCurrency": "",
1094                    "cumFeeDetail": {
1095                        "MNT": "0.00242968"
1096                    }
1097                }
1098            ]
1099        }"#;
1100        let order = PrivateMsg {
1101            id: String::from("5923240c6880ab-c59f-420b-9adb-3639adc9dd90"),
1102            creation_time: 1672364262474,
1103            data: vec![OrderMsg {
1104                category: Category::Option,
1105                order_id: String::from("5cf98598-39a7-459e-97bf-76ca765ee020"),
1106                order_link_id: None,
1107                is_leverage: None,
1108                block_trade_id: None,
1109                symbol: String::from("ETH-30DEC22-1400-C"),
1110                price: dec!(72.5),
1111                broker_order_price: None,
1112                qty: dec!(1.0),
1113                side: Side::Sell,
1114                position_idx: PositionIdx::OneWay,
1115                order_status: OrderStatus::Filled,
1116                create_type: None,
1117                cancel_type: CancelType::UNKNOWN,
1118                reject_reason: RejectReason::EcNoError,
1119                avg_price: Some(dec!(75.0)),
1120                leaves_qty: None,
1121                leaves_value: None,
1122                cum_exec_qty: dec!(1.0),
1123                cum_exec_value: dec!(75.0),
1124                cum_exec_fee: dec!(0.358635),
1125                closed_pnl: dec!(0.0),
1126                fee_currency: None,
1127                time_in_force: TimeInForce::IOC,
1128                order_type: OrderType::Market,
1129                stop_order_type: None,
1130                oco_trigger_by: None,
1131                order_iv: None,
1132                market_unit: None,
1133                slippage_tolerance_type: None,
1134                slippage_tolerance: None,
1135                trigger_price: None,
1136                take_profit: None,
1137                stop_loss: None,
1138                tpsl_mode: None,
1139                tp_limit_price: None,
1140                sl_limit_price: None,
1141                tp_trigger_by: None,
1142                sl_trigger_by: None,
1143                trigger_direction: TriggerDirection::UNKNOWN,
1144                trigger_by: None,
1145                last_price_on_created: None,
1146                reduce_only: false,
1147                close_on_trigger: false,
1148                place_type: Some(PlaceType::Price),
1149                smp_type: SmpType::None,
1150                smp_group: 0,
1151                smp_order_id: None,
1152                created_time: 1672364262444,
1153                updated_time: 1672364262457,
1154                cum_fee_detail: Some(serde_json::from_str(r#"{"MNT": "0.00242968"}"#).unwrap()),
1155            }],
1156        };
1157        let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1158
1159        let message = deserialize_json(json).unwrap();
1160
1161        assert_eq!(expected, message);
1162    }
1163
1164    #[test]
1165    fn deserialize_incoming_message_order2() {
1166        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":{}}]}"#;
1167        let order = PrivateMsg {
1168            id: String::from("108985347_ADAUSDT_140667095077548"),
1169            creation_time: 1766436947942,
1170            data: vec![OrderMsg {
1171                category: Category::Linear,
1172                order_id: String::from("ae802ad5-af70-4957-ba72-86ad7fc9c24d"),
1173                order_link_id: None,
1174                is_leverage: None,
1175                block_trade_id: None,
1176                symbol: String::from("ADAUSDT"),
1177                price: dec!(0.3862),
1178                broker_order_price: None,
1179                qty: dec!(15),
1180                side: Side::Buy,
1181                position_idx: PositionIdx::OneWay,
1182                order_status: OrderStatus::Filled,
1183                create_type: Some(CreateType::CreateByUser),
1184                cancel_type: CancelType::UNKNOWN,
1185                reject_reason: RejectReason::EcNoError,
1186                avg_price: Some(dec!(0.3679)),
1187                leaves_qty: Some(dec!(0)),
1188                leaves_value: Some(dec!(0)),
1189                cum_exec_qty: dec!(15),
1190                cum_exec_value: dec!(5.5185),
1191                cum_exec_fee: dec!(0.00303518),
1192                closed_pnl: dec!(0),
1193                fee_currency: None,
1194                time_in_force: TimeInForce::IOC,
1195                order_type: OrderType::Market,
1196                stop_order_type: None,
1197                oco_trigger_by: None,
1198                order_iv: None,
1199                market_unit: None,
1200                slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1201                slippage_tolerance: Some(dec!(0)),
1202                trigger_price: None,
1203                take_profit: None,
1204                stop_loss: None,
1205                tpsl_mode: Some(TpslMode::UNKNOWN),
1206                tp_limit_price: Some(dec!(0)),
1207                sl_limit_price: Some(dec!(0)),
1208                tp_trigger_by: None,
1209                sl_trigger_by: None,
1210                trigger_direction: TriggerDirection::UNKNOWN,
1211                trigger_by: None,
1212                last_price_on_created: Some(dec!(0.3679)),
1213                reduce_only: false,
1214                close_on_trigger: false,
1215                place_type: None,
1216                smp_type: SmpType::None,
1217                smp_group: 0,
1218                smp_order_id: None,
1219                created_time: 1766436947940,
1220                updated_time: 1766436947940,
1221                cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1222            }],
1223        };
1224        let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1225
1226        let message = deserialize_json(json).unwrap();
1227
1228        assert_eq!(expected, message);
1229    }
1230
1231    #[test]
1232    fn deserialize_incoming_message_order3() {
1233        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":{}}]}"#;
1234        let order = PrivateMsg {
1235            id: String::from("108985347_ADAUSDT_140667102632416"),
1236            creation_time: 1766600379878,
1237            data: vec![OrderMsg {
1238                category: Category::Linear,
1239                order_id: String::from("f0468cbc-ed2f-4fd7-9620-998f3e9f387c"),
1240                order_link_id: Some(String::from("BOT_LINK_ID-1")),
1241                is_leverage: None,
1242                block_trade_id: None,
1243                symbol: String::from("ADAUSDT"),
1244                price: dec!(0.3539),
1245                broker_order_price: None,
1246                qty: dec!(15),
1247                side: Side::Buy,
1248                position_idx: PositionIdx::OneWay,
1249                order_status: OrderStatus::New,
1250                create_type: Some(CreateType::CreateByUser),
1251                cancel_type: CancelType::UNKNOWN,
1252                reject_reason: RejectReason::EcNoError,
1253                avg_price: None,
1254                leaves_qty: Some(dec!(15)),
1255                leaves_value: Some(dec!(5.3085)),
1256                cum_exec_qty: dec!(0),
1257                cum_exec_value: dec!(0),
1258                cum_exec_fee: dec!(0),
1259                closed_pnl: dec!(0),
1260                fee_currency: None,
1261                time_in_force: TimeInForce::GTC,
1262                order_type: OrderType::Limit,
1263                stop_order_type: None,
1264                oco_trigger_by: None,
1265                order_iv: None,
1266                market_unit: None,
1267                slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1268                slippage_tolerance: Some(dec!(0)),
1269                trigger_price: None,
1270                take_profit: None,
1271                stop_loss: None,
1272                tpsl_mode: Some(TpslMode::UNKNOWN),
1273                tp_limit_price: Some(dec!(0)),
1274                sl_limit_price: Some(dec!(0)),
1275                tp_trigger_by: None,
1276                sl_trigger_by: None,
1277                trigger_direction: TriggerDirection::UNKNOWN,
1278                trigger_by: None,
1279                last_price_on_created: Some(dec!(0.355)),
1280                reduce_only: false,
1281                close_on_trigger: false,
1282                place_type: None, // "smpGroup":0,"smpType":"None","smpOrderId":"",
1283                smp_type: SmpType::None,
1284                smp_group: 0,
1285                smp_order_id: None,
1286                created_time: 1766600379876,
1287                updated_time: 1766600379876,
1288                cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1289            }],
1290        };
1291        let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1292
1293        let message = deserialize_json(json).unwrap();
1294
1295        assert_eq!(expected, message);
1296    }
1297
1298    #[test]
1299    fn deserialize_incoming_message_position() {
1300        let json = r#"{
1301            "id": "108985347_position_1765659601915",
1302            "topic": "position",
1303            "creationTime": 1765659601915,
1304            "data": [
1305                {
1306                    "positionIdx": 1,
1307                    "tradeMode": 0,
1308                    "riskId": 116,
1309                    "riskLimitValue": "200000",
1310                    "symbol": "ADAUSDT",
1311                    "side": "Buy",
1312                    "size": "18720",
1313                    "entryPrice": "0.41160027",
1314                    "sessionAvgPrice": "",
1315                    "leverage": "75",
1316                    "positionValue": "7705.157",
1317                    "positionBalance": "0",
1318                    "markPrice": "0.41",
1319                    "positionIM": "106.51735757",
1320                    "positionMM": "61.74535757",
1321                    "positionIMByMp": "106.51735757",
1322                    "positionMMByMp": "61.74535757",
1323                    "takeProfit": "0.4321",
1324                    "stopLoss": "0.3704",
1325                    "trailingStop": "0",
1326                    "unrealisedPnl": "-29.957",
1327                    "cumRealisedPnl": "-6712.87804378",
1328                    "curRealisedPnl": "-2.6317147",
1329                    "createdTime": "1714594321840",
1330                    "updatedTime": "1765645142548",
1331                    "tpslMode": "Full",
1332                    "liqPrice": "0.37000066",
1333                    "bustPrice": "",
1334                    "category": "linear",
1335                    "positionStatus": "Normal",
1336                    "adlRankIndicator": 2,
1337                    "autoAddMargin": 0,
1338                    "leverageSysUpdatedTime": "",
1339                    "mmrSysUpdatedTime": "",
1340                    "seq": 140667058318085,
1341                    "isReduceOnly": false
1342                },
1343                {
1344                    "positionIdx": 2,
1345                    "tradeMode": 0,
1346                    "riskId": 116,
1347                    "riskLimitValue": "200000",
1348                    "symbol": "ADAUSDT",
1349                    "side": "",
1350                    "size": "0",
1351                    "entryPrice": "0",
1352                    "sessionAvgPrice": "",
1353                    "leverage": "75",
1354                    "positionValue": "0",
1355                    "positionBalance": "0",
1356                    "markPrice": "0.41",
1357                    "positionIM": "",
1358                    "positionMM": "",
1359                    "positionIMByMp": "",
1360                    "positionMMByMp": "",
1361                    "takeProfit": "0",
1362                    "stopLoss": "0",
1363                    "trailingStop": "0",
1364                    "unrealisedPnl": "0",
1365                    "cumRealisedPnl": "1618.30675974",
1366                    "curRealisedPnl": "0",
1367                    "createdTime": "1714594321840",
1368                    "updatedTime": "1765046350698",
1369                    "tpslMode": "Full",
1370                    "liqPrice": "0",
1371                    "bustPrice": "",
1372                    "category": "linear",
1373                    "positionStatus": "Normal",
1374                    "adlRankIndicator": 0,
1375                    "autoAddMargin": 0,
1376                    "leverageSysUpdatedTime": "",
1377                    "mmrSysUpdatedTime": "",
1378                    "seq": 140667031311361,
1379                    "isReduceOnly": false
1380                }
1381            ]
1382        }"#;
1383        let position = PrivateMsg {
1384            id: String::from("108985347_position_1765659601915"),
1385            creation_time: 1765659601915,
1386            data: vec![
1387                PositionMsg {
1388                    category: Category::Linear,
1389                    symbol: String::from("ADAUSDT"),
1390                    side: Some(Side::Buy),
1391                    size: dec!(18720),
1392                    position_idx: PositionIdx::Buy,
1393                    position_value: dec!(7705.157),
1394                    risk_id: 116,
1395                    risk_limit_value: Some(dec!(200000)),
1396                    entry_price: dec!(0.41160027),
1397                    mark_price: dec!(0.41),
1398                    leverage: dec!(75),
1399                    auto_add_margin: false,
1400                    position_im: Some(dec!(106.51735757)),
1401                    position_mm: Some(dec!(61.74535757)),
1402                    position_im_by_mp: Some(dec!(106.51735757)),
1403                    position_mm_by_mp: Some(dec!(61.74535757)),
1404                    liq_price: Some(dec!(0.37000066)),
1405                    take_profit: dec!(0.4321),
1406                    stop_loss: dec!(0.3704),
1407                    trailing_stop: dec!(0),
1408                    unrealised_pnl: dec!(-29.957),
1409                    cur_realised_pnl: dec!(-2.6317147),
1410                    session_avg_price: None,
1411                    delta: None,
1412                    gamma: None,
1413                    vega: None,
1414                    theta: None,
1415                    cum_realised_pnl: dec!(-6712.87804378),
1416                    position_status: PositionStatus::Normal,
1417                    adl_rank_indicator: AdlRankIndicator::Two,
1418                    is_reduce_only: false,
1419                    mmr_sys_updated_time: None,
1420                    leverage_sys_updated_time: None,
1421                    created_time: 1714594321840,
1422                    updated_time: 1765645142548,
1423                    seq: 140667058318085,
1424                },
1425                PositionMsg {
1426                    category: Category::Linear,
1427                    symbol: String::from("ADAUSDT"),
1428                    side: None,
1429                    size: dec!(0),
1430                    position_idx: PositionIdx::Sell,
1431                    position_value: dec!(0),
1432                    risk_id: 116,
1433                    risk_limit_value: Some(dec!(200000)),
1434                    entry_price: dec!(0),
1435                    mark_price: dec!(0.41),
1436                    leverage: dec!(75),
1437                    auto_add_margin: false,
1438                    position_im: None,
1439                    position_mm: None,
1440                    position_im_by_mp: None,
1441                    position_mm_by_mp: None,
1442                    liq_price: Some(dec!(0)),
1443                    take_profit: dec!(0),
1444                    stop_loss: dec!(0),
1445                    trailing_stop: dec!(0),
1446                    unrealised_pnl: dec!(0),
1447                    cur_realised_pnl: dec!(0),
1448                    session_avg_price: None,
1449                    delta: None,
1450                    gamma: None,
1451                    vega: None,
1452                    theta: None,
1453                    cum_realised_pnl: dec!(1618.30675974),
1454                    position_status: PositionStatus::Normal,
1455                    adl_rank_indicator: AdlRankIndicator::Zero,
1456                    is_reduce_only: false,
1457                    mmr_sys_updated_time: None,
1458                    leverage_sys_updated_time: None,
1459                    created_time: 1714594321840,
1460                    updated_time: 1765046350698,
1461                    seq: 140667031311361,
1462                },
1463            ],
1464        };
1465        let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1466
1467        let message = deserialize_json(json).unwrap();
1468
1469        assert_eq!(expected, message);
1470    }
1471
1472    #[test]
1473    fn deserialize_incoming_message_position2() {
1474        let json = r#"{
1475            "id":"108985347_position_1766316605952",
1476            "topic":"position",
1477            "creationTime":1766316605952,
1478            "data":[
1479                {
1480                "positionIdx":1,
1481                "tradeMode":0,
1482                "riskId":116,
1483                "riskLimitValue":"200000",
1484                "symbol":"ADAUSDT",
1485                "side":"Buy",
1486                "size":"43",
1487                "entryPrice":"0.37293023",
1488                "sessionAvgPrice":"",
1489                "leverage":"75",
1490                "positionValue":"16.036",
1491                "positionBalance":"0",
1492                "markPrice":"0.3702",
1493                "positionIM":"0.22095025",
1494                "positionMM":"0.12809175",
1495                "positionIMByMp":"0.22095025",
1496                "positionMMByMp":"0.12809175",
1497                "takeProfit":"0",
1498                "stopLoss":"0",
1499                "trailingStop":"0",
1500                "unrealisedPnl":"-0.1174",
1501                "cumRealisedPnl":"-7547.8530836",
1502                "curRealisedPnl":"-0.00465061",
1503                "createdTime":"1714594321840",
1504                "updatedTime":"1766313370061",
1505                "tpslMode":"Full",
1506                "liqPrice":"",
1507                "bustPrice":"",
1508                "category":"linear",
1509                "positionStatus":"Normal",
1510                "adlRankIndicator":2,
1511                "autoAddMargin":0,
1512                "leverageSysUpdatedTime":"",
1513                "mmrSysUpdatedTime":"",
1514                "seq":140667089523042,
1515                "isReduceOnly":false
1516                },
1517                {
1518                "positionIdx":2,
1519                "tradeMode":0,
1520                "riskId":116,
1521                "riskLimitValue":"200000",
1522                "symbol":"ADAUSDT",
1523                "side":"",
1524                "size":"0",
1525                "entryPrice":"0",
1526                "sessionAvgPrice":"",
1527                "leverage":"75",
1528                "positionValue":"0",
1529                "positionBalance":"0",
1530                "markPrice":"0.3702",
1531                "positionIM":"",
1532                "positionMM":"",
1533                "positionIMByMp":"",
1534                "positionMMByMp":"",
1535                "takeProfit":"0",
1536                "stopLoss":"0",
1537                "trailingStop":"0",
1538                "unrealisedPnl":"0",
1539                "cumRealisedPnl":"1618.30675974",
1540                "curRealisedPnl":"0",
1541                "createdTime":"1714594321840",
1542                "updatedTime":"1765046350698",
1543                "tpslMode":"Full",
1544                "liqPrice":"0",
1545                "bustPrice":"",
1546                "category":"linear",
1547                "positionStatus":"Normal",
1548                "adlRankIndicator":0,
1549                "autoAddMargin":0,
1550                "leverageSysUpdatedTime":"",
1551                "mmrSysUpdatedTime":"",
1552                "seq":140667031311361,
1553                "isReduceOnly":false
1554                }
1555            ]
1556        }"#;
1557        let position = PrivateMsg {
1558            id: String::from("108985347_position_1766316605952"),
1559            creation_time: 1766316605952,
1560            data: vec![
1561                PositionMsg {
1562                    category: Category::Linear,
1563                    symbol: String::from("ADAUSDT"),
1564                    side: Some(Side::Buy),
1565                    size: dec!(43),
1566                    position_idx: PositionIdx::Buy,
1567                    position_value: dec!(16.036),
1568                    risk_id: 116,
1569                    risk_limit_value: Some(dec!(200000)),
1570                    entry_price: dec!(0.37293023),
1571                    mark_price: dec!(0.3702),
1572                    leverage: dec!(75),
1573                    auto_add_margin: false,
1574                    position_im: Some(dec!(0.22095025)),
1575                    position_mm: Some(dec!(0.12809175)),
1576                    position_im_by_mp: Some(dec!(0.22095025)),
1577                    position_mm_by_mp: Some(dec!(0.12809175)),
1578                    liq_price: None,
1579                    take_profit: dec!(0),
1580                    stop_loss: dec!(0),
1581                    trailing_stop: dec!(0),
1582                    unrealised_pnl: dec!(-0.1174),
1583                    cur_realised_pnl: dec!(-0.00465061),
1584                    session_avg_price: None,
1585                    delta: None,
1586                    gamma: None,
1587                    vega: None,
1588                    theta: None,
1589                    cum_realised_pnl: dec!(-7547.8530836),
1590                    position_status: PositionStatus::Normal,
1591                    adl_rank_indicator: AdlRankIndicator::Two,
1592                    is_reduce_only: false,
1593                    mmr_sys_updated_time: None,
1594                    leverage_sys_updated_time: None,
1595                    created_time: 1714594321840,
1596                    updated_time: 1766313370061,
1597                    seq: 140667089523042,
1598                },
1599                PositionMsg {
1600                    category: Category::Linear,
1601                    symbol: String::from("ADAUSDT"),
1602                    side: None,
1603                    size: dec!(0),
1604                    position_idx: PositionIdx::Sell,
1605                    position_value: dec!(0),
1606                    risk_id: 116,
1607                    risk_limit_value: Some(dec!(200000)),
1608                    entry_price: dec!(0),
1609                    mark_price: dec!(0.3702),
1610                    leverage: dec!(75),
1611                    auto_add_margin: false,
1612                    position_im: None,
1613                    position_mm: None,
1614                    position_im_by_mp: None,
1615                    position_mm_by_mp: None,
1616                    liq_price: Some(dec!(0)),
1617                    take_profit: dec!(0),
1618                    stop_loss: dec!(0),
1619                    trailing_stop: dec!(0),
1620                    unrealised_pnl: dec!(0),
1621                    cur_realised_pnl: dec!(0),
1622                    session_avg_price: None,
1623                    delta: None,
1624                    gamma: None,
1625                    vega: None,
1626                    theta: None,
1627                    cum_realised_pnl: dec!(1618.30675974),
1628                    position_status: PositionStatus::Normal,
1629                    adl_rank_indicator: AdlRankIndicator::Zero,
1630                    is_reduce_only: false,
1631                    mmr_sys_updated_time: None,
1632                    leverage_sys_updated_time: None,
1633                    created_time: 1714594321840,
1634                    updated_time: 1765046350698,
1635                    seq: 140667031311361,
1636                },
1637            ],
1638        };
1639        let expected = IncomingMessage::Topic(TopicMessage::Position(position));
1640
1641        let message = deserialize_json(json).unwrap();
1642
1643        assert_eq!(expected, message);
1644    }
1645
1646    #[test]
1647    fn deserialize_incoming_message_wallet() {
1648        let json = r#"{
1649            "id": "592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e",
1650            "topic": "wallet",
1651            "creationTime": 1700034722104,
1652            "data": [
1653                {
1654                    "accountIMRate": "0",
1655                    "accountIMRateByMp": "0",
1656                    "accountMMRate": "0",
1657                    "accountMMRateByMp": "0",
1658                    "totalEquity": "10262.91335023",
1659                    "totalWalletBalance": "9684.46297164",
1660                    "totalMarginBalance": "9684.46297164",
1661                    "totalAvailableBalance": "9556.6056555",
1662                    "totalPerpUPL": "0",
1663                    "totalInitialMargin": "0",
1664                    "totalInitialMarginByMp": "0",
1665                    "totalMaintenanceMargin": "0",
1666                    "totalMaintenanceMarginByMp": "0",
1667                    "coin": [
1668                        {
1669                            "coin": "BTC",
1670                            "equity": "0.00102964",
1671                            "usdValue": "36.70759517",
1672                            "walletBalance": "0.00102964",
1673                            "availableToWithdraw": "0.00102964",
1674                            "availableToBorrow": "",
1675                            "borrowAmount": "0",
1676                            "accruedInterest": "0",
1677                            "totalOrderIM": "",
1678                            "totalPositionIM": "",
1679                            "totalPositionMM": "",
1680                            "unrealisedPnl": "0",
1681                            "cumRealisedPnl": "-0.00000973",
1682                            "bonus": "0",
1683                            "collateralSwitch": true,
1684                            "marginCollateral": true,
1685                            "locked": "0",
1686                            "spotHedgingQty": "0.01592413",
1687                            "spotBorrow": "0"
1688                        }
1689                    ],
1690                    "accountLTV": "0",
1691                    "accountType": "UNIFIED"
1692                }
1693            ]
1694        }"#;
1695        let coin = WalletCoin {
1696            coin: String::from("BTC"),
1697            equity: dec!(0.00102964),
1698            usd_value: dec!(36.70759517),
1699            wallet_balance: dec!(0.00102964),
1700            locked: dec!(0),
1701            spot_hedging_qty: dec!(0.01592413),
1702            borrow_amount: dec!(0),
1703            accrued_interest: dec!(0),
1704            total_order_im: None,
1705            total_position_im: None,
1706            total_position_mm: None,
1707            unrealised_pnl: dec!(0),
1708            cum_realised_pnl: dec!(-0.00000973),
1709            bonus: dec!(0),
1710            collateral_switch: true,
1711            margin_collateral: true,
1712            spot_borrow: Some(dec!(0)),
1713        };
1714        let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
1715        let wallet = PrivateMsg {
1716            id: String::from("592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e"),
1717            creation_time: 1700034722104,
1718            data: vec![WalletMsg {
1719                account_type: AccountType::UNIFIED,
1720                account_im_rate: dec!(0),
1721                account_im_rate_by_mp: dec!(0),
1722                account_mm_rate: dec!(0),
1723                account_mm_rate_by_mp: dec!(0),
1724                total_equity: dec!(10262.91335023),
1725                total_wallet_balance: dec!(9684.46297164),
1726                total_margin_balance: dec!(9684.46297164),
1727                total_available_balance: dec!(9556.6056555),
1728                total_perp_upl: dec!(0),
1729                total_initial_margin: dec!(0),
1730                total_initial_margin_by_mp: dec!(0),
1731                total_maintenance_margin: dec!(0),
1732                total_maintenance_margin_by_mp: dec!(0),
1733                coin,
1734            }],
1735        };
1736        let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
1737
1738        let message = deserialize_json(json).unwrap();
1739
1740        assert_eq!(expected, message);
1741    }
1742
1743    #[test]
1744    fn deserialize_incoming_message_wallet2() {
1745        let json = r#"{
1746            "id":"108985347_wallet_1766318882965",
1747            "topic":"wallet",
1748            "creationTime":1766318882964,
1749            "data":[
1750                {
1751                "accountIMRate":"0.0007",
1752                "accountMMRate":"0.0004",
1753                "accountIMRateByMp":"0.0007",
1754                "accountMMRateByMp":"0.0004",
1755                "totalEquity":"102.7094181",
1756                "totalWalletBalance":"102.16591975",
1757                "totalMarginBalance":"102.16591975",
1758                "totalAvailableBalance":"102.09402758",
1759                "totalPerpUPL":"0",
1760                "totalInitialMargin":"0.07189217",
1761                "totalMaintenanceMargin":"0.04166941",
1762                "totalInitialMarginByMp":"0.07189217",
1763                "totalMaintenanceMarginByMp":"0.04166941",
1764                "coin":[
1765                    {
1766                    "coin":"USDT",
1767                    "equity":"75.5601152",
1768                    "usdValue":"75.53450032",
1769                    "walletBalance":"75.5601152",
1770                    "availableToWithdraw":"",
1771                    "availableToBorrow":"",
1772                    "borrowAmount":"0",
1773                    "accruedInterest":"0",
1774                    "totalOrderIM":"0",
1775                    "totalPositionIM":"0.07191655",
1776                    "totalPositionMM":"0.04168355",
1777                    "unrealisedPnl":"0",
1778                    "cumRealisedPnl":"36163.8134634",
1779                    "bonus":"0",
1780                    "collateralSwitch":true,
1781                    "marginCollateral":true,
1782                    "locked":"0",
1783                    "spotHedgingQty":"0"
1784                    }
1785                ],
1786                "accountLTV":"0",
1787                "accountType":"UNIFIED"
1788                }
1789            ]
1790            }"#;
1791        let coin = WalletCoin {
1792            coin: String::from("USDT"),
1793            equity: dec!(75.5601152),
1794            usd_value: dec!(75.53450032),
1795            wallet_balance: dec!(75.5601152),
1796            locked: dec!(0),
1797            spot_hedging_qty: dec!(0),
1798            borrow_amount: dec!(0),
1799            accrued_interest: dec!(0),
1800            total_order_im: Some(dec!(0)),
1801            total_position_im: Some(dec!(0.07191655)),
1802            total_position_mm: Some(dec!(0.04168355)),
1803            unrealised_pnl: dec!(0),
1804            cum_realised_pnl: dec!(36163.8134634),
1805            bonus: dec!(0),
1806            collateral_switch: true,
1807            margin_collateral: true,
1808            spot_borrow: None,
1809        };
1810        let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
1811        let wallet = PrivateMsg {
1812            id: String::from("108985347_wallet_1766318882965"),
1813            creation_time: 1766318882964,
1814            data: vec![WalletMsg {
1815                account_type: AccountType::UNIFIED,
1816                account_im_rate: dec!(0.0007),
1817                account_im_rate_by_mp: dec!(0.0007),
1818                account_mm_rate: dec!(0.0004),
1819                account_mm_rate_by_mp: dec!(0.0004),
1820                total_equity: dec!(102.7094181),
1821                total_wallet_balance: dec!(102.16591975),
1822                total_margin_balance: dec!(102.16591975),
1823                total_available_balance: dec!(102.09402758),
1824                total_perp_upl: dec!(0),
1825                total_initial_margin: dec!(0.07189217),
1826                total_initial_margin_by_mp: dec!(0.07189217),
1827                total_maintenance_margin: dec!(0.04166941),
1828                total_maintenance_margin_by_mp: dec!(0.04166941),
1829                coin,
1830            }],
1831        };
1832        let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
1833
1834        let message = deserialize_json(json).unwrap();
1835
1836        assert_eq!(expected, message);
1837    }
1838
1839    #[test]
1840    fn deserialize_incoming_message_execution() {
1841        let json = r#"{
1842            "topic": "execution",
1843            "id": "386825804_BTCUSDT_140612148849382",
1844            "creationTime": 1746270400355,
1845            "data": [
1846                {
1847                    "category": "linear",
1848                    "symbol": "BTCUSDT",
1849                    "closedSize": "0.5",
1850                    "execFee": "26.3725275",
1851                    "execId": "0ab1bdf7-4219-438b-b30a-32ec863018f7",
1852                    "execPrice": "95900.1",
1853                    "execQty": "0.5",
1854                    "execType": "Trade",
1855                    "execValue": "47950.05",
1856                    "feeRate": "0.00055",
1857                    "tradeIv": "",
1858                    "markIv": "",
1859                    "blockTradeId": "",
1860                    "markPrice": "95901.48",
1861                    "indexPrice": "",
1862                    "underlyingPrice": "",
1863                    "leavesQty": "0",
1864                    "orderId": "9aac161b-8ed6-450d-9cab-c5cc67c21784",
1865                    "orderLinkId": "",
1866                    "orderPrice": "94942.5",
1867                    "orderQty": "0.5",
1868                    "orderType": "Market",
1869                    "stopOrderType": "UNKNOWN",
1870                    "side": "Sell",
1871                    "execTime": "1746270400353",
1872                    "isLeverage": "0",
1873                    "isMaker": false,
1874                    "seq": 140612148849382,
1875                    "marketUnit": "",
1876                    "execPnl": "0.05",
1877                    "createType": "CreateByUser",
1878                    "extraFees":[{"feeCoin":"USDT","feeType":"GST","subFeeType":"IND_GST","feeRate":"0.0000675","fee":"0.006403779"}],
1879                    "feeCurrency": "USDT"
1880                }
1881            ]
1882        }"#;
1883        let execution = PrivateMsg {
1884            id: String::from("386825804_BTCUSDT_140612148849382"),
1885            creation_time: 1746270400355,
1886            data: vec![ExecutionMsg {
1887                category: Category::Linear,
1888                symbol: String::from("BTCUSDT"),
1889                is_leverage: false,
1890                order_id: String::from("9aac161b-8ed6-450d-9cab-c5cc67c21784"),
1891                order_link_id: None,
1892                side: Side::Sell,
1893                order_price: dec!(94942.5),
1894                order_qty: dec!(0.5),
1895                leaves_qty: dec!(0),
1896                create_type: CreateType::CreateByUser,
1897                order_type: OrderType::Market,
1898                stop_order_type: StopOrderType::UNKNOWN,
1899                exec_fee: dec!(26.3725275),
1900                exec_id: String::from("0ab1bdf7-4219-438b-b30a-32ec863018f7"),
1901                exec_price: dec!(95900.1),
1902                exec_qty: dec!(0.5),
1903                exec_pnl: dec!(0.05),
1904                exec_type: ExecType::Trade,
1905                exec_value: dec!(47950.05),
1906                exec_time: 1746270400353,
1907                is_maker: false,
1908                fee_rate: dec!(0.00055),
1909                trade_iv: None,
1910                mark_iv: None,
1911                mark_price: dec!(95901.48),
1912                index_price: None,
1913                underlying_price: None,
1914                block_trade_id: None,
1915                closed_size: dec!(0.5),
1916                extra_fees: Some(vec![ExtraFee {
1917                    fee_coin: String::from("USDT"),
1918                    fee_type: ExtraFeeType::Gst,
1919                    sub_fee_type: ExtraSubFeeType::IndGst,
1920                    fee_rate: dec!(0.0000675),
1921                    fee: dec!(0.006403779),
1922                }]),
1923                seq: 140612148849382,
1924                fee_currency: String::from("USDT"),
1925            }],
1926        };
1927        let expected = IncomingMessage::Topic(TopicMessage::Execution(execution));
1928
1929        let message = deserialize_json(json).unwrap();
1930
1931        assert_eq!(expected, message);
1932    }
1933}