1use std::{borrow::Cow, collections::HashMap};
2
3use crate::{
4 AccountType, AdlRankIndicator, CancelType, Category, CreateType, ExecType, ExtraFeeType,
5 ExtraSubFeeType, Interval, OcoTriggerBy, OrderStatus, OrderType, PlaceType, PositionIdx,
6 PositionStatus, RejectReason, Side, SlippageToleranceType, SmpType, StopOrderType,
7 TickDirection, TimeInForce, Timestamp, Topic, TpslMode, TriggerBy, TriggerDirection,
8 http::{OrderbookLevel, WalletCoin},
9 serde::{deserialize_json, hash_map},
10 serde::{
11 empty_string_as_none, empty_string_or_vec, int_to_bool, string_to_bool,
12 string_to_option_bool,
13 },
14};
15
16use rust_decimal::{Decimal, serde::str_option::deserialize as option_decimal};
17use serde::Deserialize;
18use serde_aux::prelude::{
19 deserialize_number_from_string as number,
20 deserialize_option_number_from_string as option_number,
21};
22
23#[derive(PartialEq, Deserialize, Debug)]
25#[serde(untagged)]
26pub enum IncomingMessage {
27 Command(CommandMsg),
29 TradeReply(Box<TradeReply>),
31 Ticker(Box<TickerMsg>),
37 Trade(TradeMsg),
39 KLine(KLineMsg),
41 Orderbook(OrderbookMsg),
43 AllLiquidation(AllLiquidationMsg),
45 Topic(TopicMessage),
47}
48
49#[derive(Deserialize)]
51struct Envelope<'a> {
52 #[serde(borrow, default)]
53 op: Option<Cow<'a, str>>,
54 #[serde(borrow, default)]
55 topic: Option<Cow<'a, str>>,
56 #[serde(borrow, default, rename = "type")]
57 kind: Option<Cow<'a, str>>,
58 #[serde(default, rename = "retCode")]
60 ret_code: Option<i64>,
61}
62
63#[derive(Deserialize)]
67struct Frame<D> {
68 #[serde(default, deserialize_with = "empty_string_as_none")]
69 id: Option<String>,
70 topic: Topic,
71 #[serde(default, deserialize_with = "option_number")]
72 cs: Option<u64>,
73 ts: Timestamp,
74 data: D,
75}
76
77#[derive(Deserialize)]
79struct BookFrame {
80 topic: Topic,
81 ts: Timestamp,
82 data: OrderbookDataMsg,
83 cts: Timestamp,
84}
85
86impl IncomingMessage {
87 pub fn from_json(json: &str) -> Result<Self, serde_path_to_error::Error<serde_json::Error>> {
95 let envelope: Envelope = deserialize_json(json)?;
96
97 if let Some(op) = &envelope.op {
98 if envelope.ret_code.is_some() && op != "pong" && op != "ping" {
101 return deserialize_json(json).map(|reply| Self::TradeReply(Box::new(reply)));
102 }
103 return deserialize_json(json).map(Self::Command);
104 }
105 let Some(topic) = envelope.topic else {
106 return deserialize_json(json);
107 };
108 let (kind, rest) = topic.split_once('.').unwrap_or((&topic, ""));
109
110 match kind {
111 "tickers" => match envelope.kind.as_deref() {
112 Some("snapshot") => deserialize_json::<Frame<TickerSnapshotMsg>>(json).map(|f| {
113 Self::Ticker(Box::new(TickerMsg::Snapshot {
114 topic: f.topic,
115 cs: f.cs,
116 ts: f.ts,
117 data: f.data,
118 }))
119 }),
120 Some("delta") => deserialize_json::<Frame<TickerDeltaMsg>>(json).map(|f| {
121 Self::Ticker(Box::new(TickerMsg::Delta {
122 topic: f.topic,
123 cs: f.cs,
124 ts: f.ts,
125 data: f.data,
126 }))
127 }),
128 _ => deserialize_json(json).map(|msg| Self::Ticker(Box::new(msg))),
129 },
130 "publicTrade" => match envelope.kind.as_deref() {
131 Some("snapshot") => {
132 deserialize_json::<Frame<Vec<TradeSnapshotMsg>>>(json).map(|f| {
133 Self::Trade(TradeMsg::Snapshot {
134 id: f.id,
135 topic: f.topic,
136 ts: f.ts,
137 data: f.data,
138 })
139 })
140 }
141 _ => deserialize_json(json).map(Self::Trade),
142 },
143 "kline" => match envelope.kind.as_deref() {
144 Some("snapshot") => {
145 deserialize_json::<Frame<Vec<KLineSnapshotMsg>>>(json).map(|f| {
146 Self::KLine(KLineMsg::Snapshot {
147 topic: f.topic,
148 ts: f.ts,
149 data: f.data,
150 })
151 })
152 }
153 _ => deserialize_json(json).map(Self::KLine),
154 },
155 "orderbook" => {
156 let book = |f: BookFrame| (f.topic, f.ts, f.data, f.cts);
157 match envelope.kind.as_deref() {
158 Some("snapshot") => deserialize_json::<BookFrame>(json).map(book).map(
159 |(topic, ts, data, cts)| {
160 Self::Orderbook(OrderbookMsg::Snapshot {
161 topic,
162 ts,
163 data,
164 cts,
165 })
166 },
167 ),
168 Some("delta") => deserialize_json::<BookFrame>(json).map(book).map(
169 |(topic, ts, data, cts)| {
170 Self::Orderbook(OrderbookMsg::Delta {
171 topic,
172 ts,
173 data,
174 cts,
175 })
176 },
177 ),
178 _ => deserialize_json(json).map(Self::Orderbook),
179 }
180 }
181 "allLiquidation" => match envelope.kind.as_deref() {
182 Some("snapshot") => deserialize_json::<Frame<Vec<AllLiquidationSnapshotMsg>>>(json)
183 .map(|f| {
184 Self::AllLiquidation(AllLiquidationMsg::Snapshot {
185 topic: f.topic,
186 ts: f.ts,
187 data: f.data,
188 })
189 }),
190 _ => deserialize_json(json).map(Self::AllLiquidation),
191 },
192 "order" => deserialize_json(json)
193 .map(TopicMessage::Order)
194 .map(Self::Topic),
195 "position" => deserialize_json(json)
196 .map(TopicMessage::Position)
197 .map(Self::Topic),
198 "wallet" => deserialize_json(json)
199 .map(TopicMessage::Wallet)
200 .map(Self::Topic),
201 "execution" if rest == "fast" || rest.starts_with("fast.") => deserialize_json(json)
202 .map(TopicMessage::FastExecution)
203 .map(Self::Topic),
204 "execution" => deserialize_json(json)
205 .map(TopicMessage::Execution)
206 .map(Self::Topic),
207 "fastExecution" => deserialize_json(json)
208 .map(TopicMessage::FastExecution)
209 .map(Self::Topic),
210 "greeks" => deserialize_json(json)
211 .map(TopicMessage::Greeks)
212 .map(Self::Topic),
213 "dcp" => deserialize_json(json)
214 .map(TopicMessage::Dcp)
215 .map(Self::Topic),
216 _ => deserialize_json(json),
218 }
219 }
220
221 pub fn is_pong(&self) -> bool {
223 matches!(self, IncomingMessage::Command(CommandMsg::Pong { .. }))
224 }
225 pub fn is_ping(&self) -> bool {
227 matches!(self, IncomingMessage::Command(CommandMsg::Ping { .. }))
228 }
229}
230
231#[derive(PartialEq, Deserialize, Debug, Clone)]
236#[serde(rename_all = "camelCase")]
237pub struct TradeReply {
238 #[serde(default)]
240 pub req_id: Option<String>,
241 pub ret_code: i64,
243 #[serde(default)]
245 pub ret_msg: String,
246 pub op: String,
248 #[serde(default)]
250 pub data: Option<serde_json::Value>,
251 #[serde(default)]
253 pub ret_ext_info: Option<serde_json::Value>,
254 #[serde(default)]
256 pub header: Option<TradeReplyHeader>,
257 #[serde(default)]
259 pub conn_id: Option<String>,
260}
261
262#[derive(PartialEq, Deserialize, Debug, Clone)]
264pub struct TradeReplyHeader {
265 #[serde(rename = "X-Bapi-Limit", default, deserialize_with = "option_number")]
267 pub limit: Option<u64>,
268 #[serde(
270 rename = "X-Bapi-Limit-Status",
271 default,
272 deserialize_with = "option_number"
273 )]
274 pub limit_status: Option<u64>,
275 #[serde(
277 rename = "X-Bapi-Limit-Reset-Timestamp",
278 default,
279 deserialize_with = "option_number"
280 )]
281 pub limit_reset_timestamp: Option<Timestamp>,
282 #[serde(rename = "Traceid", default)]
284 pub trace_id: Option<String>,
285 #[serde(rename = "Timenow", default, deserialize_with = "option_number")]
287 pub time_now: Option<Timestamp>,
288}
289
290#[derive(PartialEq, Deserialize, Debug)]
292#[serde(tag = "op")]
293pub enum CommandMsg {
294 #[serde(rename = "subscribe")]
296 Subscribe {
297 #[serde(default, deserialize_with = "empty_string_as_none")]
299 req_id: Option<String>,
300 #[serde(default, deserialize_with = "empty_string_as_none")]
302 ret_msg: Option<String>,
303 conn_id: String,
305 success: bool,
307 },
308 #[serde(rename = "unsubscribe")]
310 Unsubscribe {
311 #[serde(default, deserialize_with = "empty_string_as_none")]
313 req_id: Option<String>,
314 #[serde(default, deserialize_with = "empty_string_as_none")]
316 ret_msg: Option<String>,
317 conn_id: String,
319 success: bool,
321 },
322 #[serde(rename = "auth")]
324 Auth {
325 #[serde(default, deserialize_with = "empty_string_as_none")]
327 req_id: Option<String>,
328 #[serde(default, deserialize_with = "empty_string_as_none")]
330 ret_msg: Option<String>,
331 conn_id: String,
333 success: bool,
335 },
336 #[serde(rename = "pong")]
340 Pong {
341 #[serde(default, alias = "reqId", deserialize_with = "empty_string_as_none")]
343 req_id: Option<String>,
344 #[serde(default, deserialize_with = "empty_string_as_none")]
346 ret_msg: Option<String>,
347 #[serde(default, alias = "connId")]
349 conn_id: Option<String>,
350 #[serde(default)]
352 args: Option<Vec<String>>,
353 #[serde(default)]
355 success: Option<bool>,
356 },
357 #[serde(rename = "ping")]
360 Ping {
361 #[serde(default, alias = "reqId", deserialize_with = "empty_string_as_none")]
363 req_id: Option<String>,
364 #[serde(default, deserialize_with = "empty_string_as_none")]
366 ret_msg: Option<String>,
367 #[serde(default, alias = "connId")]
369 conn_id: Option<String>,
370 #[serde(default)]
372 args: Option<Vec<String>>,
373 #[serde(default)]
375 success: Option<bool>,
376 },
377}
378
379#[derive(PartialEq, Deserialize, Debug)]
382#[serde(tag = "type")]
383pub enum TickerMsg {
384 #[serde(rename = "snapshot")]
386 Snapshot {
387 topic: Topic,
389 #[serde(default, deserialize_with = "option_number")]
391 cs: Option<u64>,
392 ts: Timestamp,
394 data: TickerSnapshotMsg,
396 },
397 #[serde(rename = "delta")]
399 Delta {
400 topic: Topic,
402 #[serde(default, deserialize_with = "option_number")]
404 cs: Option<u64>,
405 ts: Timestamp,
407 data: TickerDeltaMsg,
409 },
410}
411
412#[derive(PartialEq, Deserialize, Debug)]
414#[serde(rename_all = "camelCase")]
415pub struct TickerSnapshotMsg {
416 pub symbol: String,
418 pub tick_direction: TickDirection,
420 pub last_price: Decimal,
422 #[serde(default, deserialize_with = "option_decimal")]
424 pub pre_open_price: Option<Decimal>,
425 #[serde(default, deserialize_with = "option_decimal")]
427 pub pre_qty: Option<Decimal>,
428 #[serde(default, deserialize_with = "empty_string_as_none")]
430 pub cur_pre_listing_phase: Option<String>,
431 pub prev_price24h: Decimal,
433 pub price24h_pcnt: Decimal,
435 pub high_price24h: Decimal,
437 pub low_price24h: Decimal,
439 pub prev_price1h: Decimal,
441 pub mark_price: Decimal,
443 pub index_price: Decimal,
445 pub open_interest: Decimal,
447 pub open_interest_value: Decimal,
449 pub turnover24h: Decimal,
451 pub volume24h: Decimal,
453 pub funding_rate: Decimal,
455 #[serde(default, deserialize_with = "number")]
457 pub next_funding_time: Timestamp,
458 pub bid1_price: Decimal,
460 pub bid1_size: Decimal,
462 pub ask1_price: Decimal,
464 pub ask1_size: Decimal,
466 #[serde(default, deserialize_with = "option_number")]
468 pub delivery_time: Option<Timestamp>,
469 #[serde(default, deserialize_with = "option_decimal")]
471 pub basis_rate: Option<Decimal>,
472 #[serde(default, deserialize_with = "option_decimal")]
474 pub delivery_fee_rate: Option<Decimal>,
475 #[serde(default, deserialize_with = "option_decimal")]
477 pub predicted_delivery_price: Option<Decimal>,
478}
479
480#[derive(PartialEq, Deserialize, Debug)]
482#[serde(rename_all = "camelCase")]
483pub struct TickerDeltaMsg {
484 pub symbol: String,
486 #[serde(default, deserialize_with = "empty_string_as_none")]
488 pub tick_direction: Option<TickDirection>,
489 #[serde(default, deserialize_with = "option_decimal")]
491 pub last_price: Option<Decimal>,
492 #[serde(default, deserialize_with = "option_decimal")]
494 pub pre_open_price: Option<Decimal>,
495 #[serde(default, deserialize_with = "option_decimal")]
497 pub pre_qty: Option<Decimal>,
498 #[serde(default, deserialize_with = "empty_string_as_none")]
500 pub cur_pre_listing_phase: Option<String>,
501 #[serde(default, deserialize_with = "option_decimal")]
503 pub prev_price24h: Option<Decimal>,
504 #[serde(default, deserialize_with = "option_decimal")]
506 pub price24h_pcnt: Option<Decimal>,
507 #[serde(default, deserialize_with = "option_decimal")]
509 pub high_price24h: Option<Decimal>,
510 #[serde(default, deserialize_with = "option_decimal")]
512 pub low_price24h: Option<Decimal>,
513 #[serde(default, deserialize_with = "option_decimal")]
515 pub prev_price1h: Option<Decimal>,
516 #[serde(default, deserialize_with = "option_decimal")]
518 pub mark_price: Option<Decimal>,
519 #[serde(default, deserialize_with = "option_decimal")]
521 pub index_price: Option<Decimal>,
522 #[serde(default, deserialize_with = "option_decimal")]
524 pub open_interest: Option<Decimal>,
525 #[serde(default, deserialize_with = "option_decimal")]
527 pub open_interest_value: Option<Decimal>,
528 #[serde(default, deserialize_with = "option_decimal")]
530 pub turnover24h: Option<Decimal>,
531 #[serde(default, deserialize_with = "option_decimal")]
533 pub volume24h: Option<Decimal>,
534 #[serde(default, deserialize_with = "option_decimal")]
536 pub funding_rate: Option<Decimal>,
537 #[serde(default, deserialize_with = "option_decimal")]
539 pub next_funding_time: Option<Decimal>,
540 #[serde(default, deserialize_with = "option_decimal")]
542 pub bid1_price: Option<Decimal>,
543 #[serde(default, deserialize_with = "option_decimal")]
545 pub bid1_size: Option<Decimal>,
546 #[serde(default, deserialize_with = "option_decimal")]
548 pub ask1_price: Option<Decimal>,
549 #[serde(default, deserialize_with = "option_decimal")]
551 pub ask1_size: Option<Decimal>,
552 #[serde(default, deserialize_with = "option_number")]
554 pub delivery_time: Option<Timestamp>,
555 #[serde(default, deserialize_with = "option_decimal")]
557 pub basis_rate: Option<Decimal>,
558 #[serde(default, deserialize_with = "option_decimal")]
560 pub delivery_fee_rate: Option<Decimal>,
561 #[serde(default, deserialize_with = "option_decimal")]
563 pub predicted_delivery_price: Option<Decimal>,
564}
565
566#[derive(PartialEq, Deserialize, Debug)]
569#[serde(tag = "type")]
570pub enum TradeMsg {
571 #[serde(rename = "snapshot")]
573 Snapshot {
574 #[serde(default, deserialize_with = "empty_string_as_none")]
576 id: Option<String>,
577 topic: Topic,
579 ts: Timestamp,
581 data: Vec<TradeSnapshotMsg>,
583 },
584}
585
586#[derive(PartialEq, Deserialize, Debug)]
588pub struct TradeSnapshotMsg {
589 #[serde(rename = "T")]
591 pub time: Timestamp,
592 #[serde(rename = "s")]
594 pub symbol: String,
595 #[serde(rename = "S")]
597 pub side: Side,
598 #[serde(rename = "v")]
600 pub size: Decimal,
601 #[serde(rename = "p")]
603 pub price: Decimal,
604 #[serde(rename = "L")]
606 pub tick_direction: TickDirection,
607 #[serde(rename = "i")]
609 pub trade_id: String,
610 #[serde(rename = "BT")]
612 pub block_trade: bool,
613 #[serde(rename = "RPI")]
615 pub rpi_trade: Option<bool>,
616 #[serde(rename = "mP", default, deserialize_with = "empty_string_as_none")]
618 pub mark_price: Option<String>,
619 #[serde(rename = "iP", default, deserialize_with = "empty_string_as_none")]
621 pub index_price: Option<String>,
622 #[serde(rename = "mlv", default, deserialize_with = "empty_string_as_none")]
624 pub mark_iv: Option<String>,
625 #[serde(rename = "iv", default, deserialize_with = "empty_string_as_none")]
627 pub iv: Option<String>,
628}
629
630#[derive(PartialEq, Deserialize, Debug)]
633#[serde(tag = "type")]
634pub enum KLineMsg {
635 #[serde(rename = "snapshot")]
637 Snapshot {
638 topic: Topic,
640 ts: Timestamp,
642 data: Vec<KLineSnapshotMsg>,
644 },
645}
646
647#[derive(PartialEq, Deserialize, Debug)]
649pub struct KLineSnapshotMsg {
650 pub start: Timestamp,
652 pub end: Timestamp,
654 pub interval: Interval,
656 pub open: Decimal,
658 pub close: Decimal,
660 pub high: Decimal,
662 pub low: Decimal,
664 pub volume: Decimal,
666 pub turnover: Decimal,
668 pub confirm: bool,
670 pub timestamp: Timestamp,
672}
673
674#[derive(PartialEq, Deserialize, Debug)]
677#[serde(tag = "type")]
678pub enum OrderbookMsg {
679 #[serde(rename = "snapshot")]
681 Snapshot {
682 topic: Topic,
684 ts: Timestamp,
686 data: OrderbookDataMsg,
688 cts: Timestamp,
690 },
691 #[serde(rename = "delta")]
693 Delta {
694 topic: Topic,
696 ts: Timestamp,
698 data: OrderbookDataMsg,
700 cts: Timestamp,
702 },
703}
704
705#[derive(PartialEq, Deserialize, Debug)]
707pub struct OrderbookDataMsg {
708 #[serde(rename = "s")]
710 pub symbol: String,
711 #[serde(rename = "b")]
714 pub bids: Vec<OrderbookLevel>,
715 #[serde(rename = "a")]
718 pub asks: Vec<OrderbookLevel>,
719 #[serde(rename = "u")]
722 pub update_id: i64,
723 pub seq: i64,
726}
727
728#[derive(PartialEq, Deserialize, Debug)]
731#[serde(tag = "type")]
732pub enum AllLiquidationMsg {
733 #[serde(rename = "snapshot")]
735 Snapshot {
736 topic: Topic,
738 ts: Timestamp,
740 data: Vec<AllLiquidationSnapshotMsg>,
742 },
743}
744
745#[derive(PartialEq, Deserialize, Debug)]
747pub struct AllLiquidationSnapshotMsg {
748 #[serde(rename = "T")]
750 pub time: Timestamp,
751 #[serde(rename = "s")]
753 pub symbol: String,
754 #[serde(rename = "S")]
756 pub side: Side,
757 #[serde(rename = "v")]
759 pub size: Decimal,
760 #[serde(rename = "p")]
762 pub price: Decimal,
763}
764
765#[derive(PartialEq, Deserialize, Debug)]
766#[serde(tag = "topic")]
770pub enum TopicMessage {
771 #[serde(rename = "order")]
773 Order(PrivateMsg<Vec<OrderMsg>>),
774 #[serde(rename = "position")]
776 Position(PrivateMsg<Vec<PositionMsg>>),
777 #[serde(rename = "wallet")]
779 Wallet(PrivateMsg<Vec<WalletMsg>>),
780 #[serde(rename = "execution")]
782 Execution(PrivateMsg<Vec<ExecutionMsg>>),
783 #[serde(rename = "fastExecution")]
785 FastExecution(PrivateMsg<Vec<FastExecutionMsg>>),
786 #[serde(rename = "greeks")]
788 Greeks(PrivateMsg<Vec<GreekMsg>>),
789 #[serde(rename = "dcp")]
791 Dcp(PrivateMsg<DcpMsg>),
792}
793
794#[derive(PartialEq, Deserialize, Debug)]
796#[serde(rename_all = "camelCase")]
797pub struct PublicMsg<T> {
798 #[serde(default, deserialize_with = "empty_string_as_none")]
799 id: Option<String>,
800 #[serde(default, deserialize_with = "option_number")]
801 cs: Option<u64>,
802 ts: Timestamp,
803 data: T,
804}
805
806#[derive(PartialEq, Deserialize, Debug)]
808#[serde(rename_all = "camelCase")]
809pub struct PrivateMsg<T> {
810 pub id: String,
812 pub creation_time: Timestamp,
814 pub data: T,
816}
817
818#[derive(PartialEq, Deserialize, Debug, Clone)]
820#[serde(rename_all = "camelCase")]
821pub struct OrderMsg {
822 pub category: Category,
826 pub order_id: String,
828 #[serde(default, deserialize_with = "empty_string_as_none")]
830 pub order_link_id: Option<String>,
831 #[serde(default, deserialize_with = "string_to_option_bool")]
835 pub is_leverage: Option<bool>,
836 #[serde(default, deserialize_with = "empty_string_as_none")]
838 pub block_trade_id: Option<String>,
839 pub symbol: String,
841 pub price: Decimal,
843 #[serde(default, deserialize_with = "option_decimal")]
845 pub broker_order_price: Option<Decimal>,
846 pub qty: Decimal,
848 pub side: Side,
850 pub position_idx: PositionIdx,
852 pub order_status: OrderStatus,
854 #[serde(default, deserialize_with = "empty_string_as_none")]
858 pub create_type: Option<CreateType>,
859 pub cancel_type: CancelType,
861 pub reject_reason: RejectReason,
863 #[serde(default, deserialize_with = "option_decimal")]
867 pub avg_price: Option<Decimal>,
868 #[serde(default, deserialize_with = "option_decimal")]
870 pub leaves_qty: Option<Decimal>,
871 #[serde(default, deserialize_with = "option_decimal")]
873 pub leaves_value: Option<Decimal>,
874 pub cum_exec_qty: Decimal,
876 pub cum_exec_value: Decimal,
878 pub cum_exec_fee: Decimal,
882 pub cum_fee_detail: Option<serde_json::Value>,
884 pub closed_pnl: Decimal,
886 #[serde(deserialize_with = "option_decimal")]
888 pub fee_currency: Option<Decimal>,
889 pub time_in_force: TimeInForce,
891 pub order_type: OrderType,
893 #[serde(default, deserialize_with = "empty_string_as_none")]
895 pub stop_order_type: Option<StopOrderType>,
896 #[serde(default, deserialize_with = "empty_string_as_none")]
898 pub oco_trigger_by: Option<OcoTriggerBy>,
899 #[serde(deserialize_with = "option_decimal")]
901 pub order_iv: Option<Decimal>,
902 #[serde(default, deserialize_with = "empty_string_as_none")]
904 pub market_unit: Option<String>,
905 #[serde(default, deserialize_with = "empty_string_as_none")]
907 pub slippage_tolerance_type: Option<SlippageToleranceType>,
908 #[serde(default, deserialize_with = "option_decimal")]
910 pub slippage_tolerance: Option<Decimal>,
911 #[serde(deserialize_with = "option_decimal")]
913 pub trigger_price: Option<Decimal>,
914 #[serde(deserialize_with = "option_decimal")]
916 pub take_profit: Option<Decimal>,
917 #[serde(deserialize_with = "option_decimal")]
919 pub stop_loss: Option<Decimal>,
920 #[serde(default, deserialize_with = "empty_string_as_none")]
922 pub tpsl_mode: Option<TpslMode>,
923 #[serde(deserialize_with = "option_decimal")]
925 pub tp_limit_price: Option<Decimal>,
926 #[serde(deserialize_with = "option_decimal")]
928 pub sl_limit_price: Option<Decimal>,
929 #[serde(default, deserialize_with = "empty_string_as_none")]
931 pub tp_trigger_by: Option<TriggerBy>,
932 #[serde(default, deserialize_with = "empty_string_as_none")]
934 pub sl_trigger_by: Option<TriggerBy>,
935 pub trigger_direction: TriggerDirection,
937 #[serde(default, deserialize_with = "empty_string_as_none")]
939 pub trigger_by: Option<TriggerBy>,
940 #[serde(deserialize_with = "option_decimal")]
942 pub last_price_on_created: Option<Decimal>,
943 pub reduce_only: bool,
945 pub close_on_trigger: bool,
947 #[serde(default, deserialize_with = "empty_string_as_none")]
949 pub place_type: Option<PlaceType>,
950 pub smp_type: SmpType,
952 #[serde(deserialize_with = "number")]
954 pub smp_group: i64,
955 #[serde(default, deserialize_with = "empty_string_as_none")]
957 pub smp_order_id: Option<String>,
958 #[serde(deserialize_with = "number")]
960 pub created_time: Timestamp,
961 #[serde(deserialize_with = "number")]
963 pub updated_time: Timestamp,
964}
965
966#[derive(PartialEq, Deserialize, Debug, Clone)]
968#[serde(rename_all = "camelCase")]
969pub struct PositionMsg {
970 pub category: Category,
972 pub symbol: String,
974 #[serde(default, deserialize_with = "empty_string_as_none")]
978 pub side: Option<Side>,
979 pub size: Decimal,
981 pub position_idx: PositionIdx,
983 pub position_value: Decimal,
985 #[serde(deserialize_with = "number")]
988 pub risk_id: i64,
989 #[serde(default, deserialize_with = "option_decimal")]
992 pub risk_limit_value: Option<Decimal>,
993 pub entry_price: Decimal,
995 pub mark_price: Decimal,
997 pub leverage: Decimal,
1000 #[serde(default, deserialize_with = "int_to_bool")]
1002 pub auto_add_margin: bool,
1003 #[serde(rename = "positionIM", default, deserialize_with = "option_decimal")]
1006 pub position_im: Option<Decimal>,
1007 #[serde(rename = "positionMM", default, deserialize_with = "option_decimal")]
1010 pub position_mm: Option<Decimal>,
1011 #[serde(
1014 rename = "positionIMByMp",
1015 default,
1016 deserialize_with = "option_decimal"
1017 )]
1018 pub position_im_by_mp: Option<Decimal>,
1019 #[serde(
1022 rename = "positionMMByMp",
1023 default,
1024 deserialize_with = "option_decimal"
1025 )]
1026 pub position_mm_by_mp: Option<Decimal>,
1027 #[serde(default, deserialize_with = "option_decimal")]
1034 pub liq_price: Option<Decimal>,
1035 pub take_profit: Decimal,
1037 pub stop_loss: Decimal,
1039 pub trailing_stop: Decimal,
1041 pub unrealised_pnl: Decimal,
1043 pub cur_realised_pnl: Decimal,
1045 #[serde(default, deserialize_with = "option_decimal")]
1047 pub session_avg_price: Option<Decimal>,
1048 #[serde(default, deserialize_with = "empty_string_as_none")]
1050 pub delta: Option<String>,
1051 #[serde(default, deserialize_with = "empty_string_as_none")]
1053 pub gamma: Option<String>,
1054 #[serde(default, deserialize_with = "empty_string_as_none")]
1056 pub vega: Option<String>,
1057 #[serde(default, deserialize_with = "empty_string_as_none")]
1059 pub theta: Option<String>,
1060 pub cum_realised_pnl: Decimal,
1064 pub position_status: PositionStatus,
1066 pub adl_rank_indicator: AdlRankIndicator,
1068 pub is_reduce_only: bool,
1073 #[serde(deserialize_with = "option_number")]
1080 pub mmr_sys_updated_time: Option<Timestamp>,
1081 #[serde(deserialize_with = "option_number")]
1088 pub leverage_sys_updated_time: Option<Timestamp>,
1089 #[serde(deserialize_with = "number")]
1091 pub created_time: Timestamp,
1092 #[serde(deserialize_with = "number")]
1094 pub updated_time: Timestamp,
1095 pub seq: i64,
1100}
1101
1102#[derive(PartialEq, Deserialize, Debug, Clone)]
1104#[serde(rename_all = "camelCase")]
1105pub struct WalletMsg {
1106 pub account_type: AccountType,
1111 #[serde(rename = "accountIMRate")]
1118 pub account_im_rate: Decimal,
1119 #[serde(rename = "accountMMRate")]
1121 pub account_mm_rate: Decimal,
1122 pub total_equity: Decimal,
1124 pub total_wallet_balance: Decimal,
1126 pub total_margin_balance: Decimal,
1128 pub total_available_balance: Decimal,
1130 #[serde(rename = "totalPerpUPL")]
1132 pub total_perp_upl: Decimal,
1133 pub total_initial_margin: Decimal,
1135 pub total_maintenance_margin: Decimal,
1137 #[serde(rename = "accountIMRateByMp")]
1139 pub account_im_rate_by_mp: Decimal,
1140 #[serde(rename = "accountMMRateByMp")]
1142 pub account_mm_rate_by_mp: Decimal,
1143 #[serde(rename = "totalInitialMarginByMp")]
1145 pub total_initial_margin_by_mp: Decimal,
1146 #[serde(rename = "totalMaintenanceMarginByMp")]
1148 pub total_maintenance_margin_by_mp: Decimal,
1149 #[serde(deserialize_with = "hash_map")]
1151 pub coin: HashMap<String, WalletCoin>,
1152}
1153
1154#[derive(PartialEq, Deserialize, Debug, Clone)]
1156#[serde(rename_all = "camelCase")]
1157pub struct ExecutionMsg {
1158 pub category: Category,
1160 pub symbol: String,
1162 #[serde(default, deserialize_with = "string_to_bool")]
1164 pub is_leverage: bool,
1165 pub order_id: String,
1167 #[serde(default, deserialize_with = "empty_string_as_none")]
1169 pub order_link_id: Option<String>,
1170 pub side: Side,
1172 pub order_price: Decimal,
1174 pub order_qty: Decimal,
1176 pub leaves_qty: Decimal,
1178 pub create_type: CreateType,
1181 pub order_type: OrderType,
1183 pub stop_order_type: StopOrderType,
1185 pub exec_fee: Decimal,
1187 pub exec_id: String,
1189 pub exec_price: Decimal,
1191 pub exec_qty: Decimal,
1193 pub exec_pnl: Decimal,
1195 pub exec_type: ExecType,
1197 pub exec_value: Decimal,
1199 #[serde(deserialize_with = "number")]
1201 pub exec_time: Timestamp,
1202 pub is_maker: bool,
1204 pub fee_rate: Decimal,
1206 #[serde(default, deserialize_with = "option_decimal")]
1208 pub trade_iv: Option<Decimal>,
1209 #[serde(default, deserialize_with = "option_decimal")]
1211 pub mark_iv: Option<Decimal>,
1212 pub mark_price: Decimal,
1214 #[serde(default, deserialize_with = "option_decimal")]
1216 pub index_price: Option<Decimal>,
1217 #[serde(default, deserialize_with = "option_decimal")]
1219 pub underlying_price: Option<Decimal>,
1220 #[serde(default, deserialize_with = "empty_string_as_none")]
1222 pub block_trade_id: Option<String>,
1223 pub closed_size: Decimal,
1225 #[serde(default, deserialize_with = "empty_string_or_vec")]
1227 pub extra_fees: Option<Vec<ExtraFee>>,
1228 pub seq: i64,
1232 pub fee_currency: String,
1234}
1235
1236#[derive(PartialEq, Deserialize, Debug, Clone)]
1238#[serde(rename_all = "camelCase")]
1239pub struct ExtraFee {
1240 pub fee_coin: String,
1242 pub fee_type: ExtraFeeType,
1244 pub sub_fee_type: ExtraSubFeeType,
1246 pub fee_rate: Decimal,
1248 pub fee: Decimal,
1250}
1251
1252#[derive(PartialEq, Deserialize, Debug, Clone)]
1258#[serde(rename_all = "camelCase")]
1259pub struct FastExecutionMsg {
1260 pub category: Category,
1262 pub symbol: String,
1264 pub exec_id: String,
1266 pub exec_price: Decimal,
1268 pub exec_qty: Decimal,
1270 pub side: Side,
1272 pub order_qty: Decimal,
1274 pub order_id: String,
1276 #[serde(default, deserialize_with = "empty_string_as_none")]
1278 pub order_link_id: Option<String>,
1279 pub is_maker: bool,
1281 pub fee_currency: String,
1283 pub fee_rate: Decimal,
1285 pub exec_fee: Decimal,
1287 #[serde(default, deserialize_with = "option_decimal")]
1289 pub trade_iv: Option<Decimal>,
1290 #[serde(default, deserialize_with = "option_decimal")]
1292 pub mark_iv: Option<Decimal>,
1293 pub mark_price: Decimal,
1295 #[serde(default, deserialize_with = "option_decimal")]
1297 pub index_price: Option<Decimal>,
1298 #[serde(default, deserialize_with = "option_decimal")]
1300 pub underlying_price: Option<Decimal>,
1301 #[serde(default, deserialize_with = "empty_string_as_none")]
1303 pub block_trade_id: Option<String>,
1304 #[serde(deserialize_with = "number")]
1306 pub exec_time: Timestamp,
1307}
1308
1309#[derive(PartialEq, Deserialize, Debug, Clone)]
1314#[serde(rename_all = "camelCase")]
1315pub struct GreekMsg {
1316 pub base_coin: String,
1318 pub total_delta: Decimal,
1320 pub total_gamma: Decimal,
1322 pub total_vega: Decimal,
1324 pub total_theta: Decimal,
1326}
1327
1328#[derive(PartialEq, Deserialize, Debug, Clone)]
1331#[serde(rename_all = "camelCase")]
1332pub struct DcpMsg {
1333 pub product: String,
1335 pub dcp_status: String,
1337 pub time_window: u64,
1339 #[serde(deserialize_with = "number")]
1341 pub updated_at: Timestamp,
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346 use rust_decimal::dec;
1347
1348 use crate::DepthLevel;
1349 use crate::serde::{Unique, deserialize_json};
1350
1351 fn parse(json: &str) -> IncomingMessage {
1354 let fast = IncomingMessage::from_json(json).unwrap();
1355 let untagged: IncomingMessage = deserialize_json(json).unwrap();
1356 assert_eq!(fast, untagged);
1357 fast
1358 }
1359
1360 use super::*;
1361
1362 #[test]
1363 fn deserialize_incoming_message_command_subscribe() {
1364 let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"subscribe"}"#;
1365 let expected = IncomingMessage::Command(CommandMsg::Subscribe {
1366 req_id: None,
1367 ret_msg: None,
1368 conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
1369 success: true,
1370 });
1371
1372 let message = parse(json);
1373
1374 assert_eq!(expected, message);
1375 }
1376
1377 #[test]
1378 fn deserialize_incoming_message_command_unsubscribe() {
1379 let json = r#"{"success":true,"ret_msg":"","conn_id":"c0c928a4-daab-460d-b186-45e90a10a3d4","req_id":"","op":"unsubscribe"}"#;
1380 let expected = IncomingMessage::Command(CommandMsg::Unsubscribe {
1381 req_id: None,
1382 ret_msg: None,
1383 conn_id: String::from("c0c928a4-daab-460d-b186-45e90a10a3d4"),
1384 success: true,
1385 });
1386
1387 let message = parse(json);
1388
1389 assert_eq!(expected, message);
1390 }
1391
1392 #[test]
1393 fn deserialize_incoming_message_ticker_delta() {
1394 let json = r#"{
1395 "topic": "tickers.BTCUSDT",
1396 "type": "delta",
1397 "data": {
1398 "symbol": "BTCUSDT",
1399 "tickDirection": "PlusTick",
1400 "price24hPcnt": "-0.015895",
1401 "lastPrice": "63948.50",
1402 "turnover24h": "6793884423.5518",
1403 "volume24h": "105991.3760",
1404 "bid1Price": "63948.40",
1405 "bid1Size": "3.439",
1406 "ask1Price": "63948.50",
1407 "ask1Size": "2.566"
1408 },
1409 "cs": 195377749067,
1410 "ts": 1718995014034
1411 }"#;
1412 let ticker_delta = TickerMsg::Delta {
1413 topic: Topic::Ticker(String::from("BTCUSDT")),
1414 cs: Some(195377749067),
1415 ts: 1718995014034,
1416 data: TickerDeltaMsg {
1417 symbol: String::from("BTCUSDT"),
1418 tick_direction: Some(TickDirection::PlusTick),
1419 last_price: Some(dec!(63948.5)),
1420 pre_open_price: None,
1421 pre_qty: None,
1422 cur_pre_listing_phase: None,
1423 prev_price24h: None,
1424 price24h_pcnt: Some(dec!(-0.015895)),
1425 high_price24h: None,
1426 low_price24h: None,
1427 prev_price1h: None,
1428 mark_price: None,
1429 index_price: None,
1430 open_interest: None,
1431 open_interest_value: None,
1432 turnover24h: Some(dec!(6793884423.5518)),
1433 volume24h: Some(dec!(105991.376)),
1434 funding_rate: None,
1435 next_funding_time: None,
1436 bid1_price: Some(dec!(63948.4)),
1437 bid1_size: Some(dec!(3.439)),
1438 ask1_price: Some(dec!(63948.5)),
1439 ask1_size: Some(dec!(2.566)),
1440 delivery_time: None,
1441 basis_rate: None,
1442 delivery_fee_rate: None,
1443 predicted_delivery_price: None,
1444 },
1445 };
1446 let expected = IncomingMessage::Ticker(Box::new(ticker_delta));
1447
1448 let message = parse(json);
1449
1450 assert_eq!(expected, message);
1451 }
1452
1453 #[test]
1454 fn deserialize_incoming_message_ticker_snapshot() {
1455 let json = r#"{
1457 "topic": "tickers.BTCUSDT",
1458 "type": "snapshot",
1459 "data": {
1460 "symbol":"BTCUSDT",
1461 "tickDirection":"ZeroPlusTick",
1462 "price24hPcnt":"-0.044555",
1463 "lastPrice":"84594.40",
1464 "prevPrice24h":"88539.30",
1465 "highPrice24h":"89389.90",
1466 "lowPrice24h":"82055.60",
1467 "prevPrice1h":"84307.20",
1468 "markPrice":"84594.00",
1469 "indexPrice":"84650.47",
1470 "openInterest":"52903.75",
1471 "openInterestValue":"4475339827.50",
1472 "turnover24h":"17166562011.6514",
1473 "volume24h":"200176.9910",
1474 "nextFundingTime":"1740643200000",
1475 "fundingRate":"-0.00016974",
1476 "bid1Price":"84594.30",
1477 "bid1Size":"6.777",
1478 "ask1Price":"84594.40",
1479 "ask1Size":"0.660",
1480 "preOpenPrice":"",
1481 "preQty":"",
1482 "curPreListingPhase":""
1483 },
1484 "cs": 337149693308,
1485 "ts": 1740622194359
1486 }"#;
1487 let ticker_snapshot = TickerMsg::Snapshot {
1488 topic: Topic::Ticker(String::from("BTCUSDT")),
1489 cs: Some(337149693308),
1490 ts: 1740622194359,
1491 data: TickerSnapshotMsg {
1492 symbol: String::from("BTCUSDT"),
1493 tick_direction: TickDirection::ZeroPlusTick,
1494 last_price: dec!(84594.40),
1495 pre_open_price: None,
1496 pre_qty: None,
1497 cur_pre_listing_phase: None,
1498 prev_price24h: dec!(88539.30),
1499 price24h_pcnt: dec!(-0.044555),
1500 high_price24h: dec!(89389.90),
1501 low_price24h: dec!(82055.60),
1502 prev_price1h: dec!(84307.20),
1503 mark_price: dec!(84594.00),
1504 index_price: dec!(84650.47),
1505 open_interest: dec!(52903.75),
1506 open_interest_value: dec!(4475339827.50),
1507 turnover24h: dec!(17166562011.6514),
1508 volume24h: dec!(200176.9910),
1509 funding_rate: dec!(-0.00016974),
1510 next_funding_time: 1740643200000,
1511 bid1_price: dec!(84594.30),
1512 bid1_size: dec!(6.777),
1513 ask1_price: dec!(84594.40),
1514 ask1_size: dec!(0.660),
1515 delivery_time: None,
1516 basis_rate: None,
1517 delivery_fee_rate: None,
1518 predicted_delivery_price: None,
1519 },
1520 };
1521 let expected = IncomingMessage::Ticker(Box::new(ticker_snapshot));
1522
1523 let message = parse(json);
1524
1525 assert_eq!(expected, message);
1526 }
1527
1528 #[test]
1529 fn deserialize_incoming_message_trade_snapshot() {
1530 let json = r#"{
1532 "topic":"publicTrade.BTCUSDT",
1533 "type":"snapshot",
1534 "ts":1741433245359,
1535 "data":[
1536 {
1537 "T":1741433245357,
1538 "s":"BTCUSDT",
1539 "S":"Buy",
1540 "v":"0.007",
1541 "p":"85821.00",
1542 "L":"PlusTick",
1543 "i":"485eaa70-df6e-5260-bbef-4f7324e3c5d9",
1544 "BT":false
1545 }
1546 ]
1547 }"#;
1548 let expected = IncomingMessage::Trade(TradeMsg::Snapshot {
1549 id: None,
1550 topic: Topic::Trade(String::from("BTCUSDT")),
1551 ts: 1741433245359,
1552 data: vec![TradeSnapshotMsg {
1553 time: 1741433245357,
1554 symbol: String::from("BTCUSDT"),
1555 side: Side::Buy,
1556 size: dec!(0.007),
1557 price: dec!(85821.00),
1558 tick_direction: TickDirection::PlusTick,
1559 trade_id: String::from("485eaa70-df6e-5260-bbef-4f7324e3c5d9"),
1560 block_trade: false,
1561 rpi_trade: None,
1562 mark_price: None,
1563 index_price: None,
1564 mark_iv: None,
1565 iv: None,
1566 }],
1567 });
1568
1569 let message = parse(json);
1570
1571 assert_eq!(expected, message);
1572 }
1573
1574 #[test]
1575 fn deserialize_incoming_message_orderbook_snapshot() {
1576 let json = r#"{
1578 "topic":"orderbook.50.BTCUSDT",
1579 "type":"snapshot",
1580 "ts":1672304484978,
1581 "data":{
1582 "s":"BTCUSDT",
1583 "b":[
1584 ["16493.50","0.006"],
1585 ["16493.00","0.100"]
1586 ],
1587 "a":[
1588 ["16611.00","0.029"],
1589 ["16612.00","0.213"]
1590 ],
1591 "u":18521288,
1592 "seq":7961638724
1593 },
1594 "cts":1672304484976
1595 }"#;
1596 let expected = IncomingMessage::Orderbook(OrderbookMsg::Snapshot {
1597 topic: Topic::Orderbook {
1598 symbol: String::from("BTCUSDT"),
1599 depth: DepthLevel::Level50,
1600 },
1601 ts: 1672304484978,
1602 data: OrderbookDataMsg {
1603 symbol: String::from("BTCUSDT"),
1604 bids: vec![
1605 OrderbookLevel {
1606 price: dec!(16493.50),
1607 size: dec!(0.006),
1608 },
1609 OrderbookLevel {
1610 price: dec!(16493.00),
1611 size: dec!(0.100),
1612 },
1613 ],
1614 asks: vec![
1615 OrderbookLevel {
1616 price: dec!(16611.00),
1617 size: dec!(0.029),
1618 },
1619 OrderbookLevel {
1620 price: dec!(16612.00),
1621 size: dec!(0.213),
1622 },
1623 ],
1624 update_id: 18521288,
1625 seq: 7961638724,
1626 },
1627 cts: 1672304484976,
1628 });
1629
1630 let message = parse(json);
1631
1632 assert_eq!(expected, message);
1633 }
1634
1635 #[test]
1636 fn deserialize_incoming_message_orderbook_delta() {
1637 let json = r#"{
1639 "topic":"orderbook.50.BTCUSDT",
1640 "type":"delta",
1641 "ts":1687940967466,
1642 "data":{
1643 "s":"BTCUSDT",
1644 "b":[
1645 ["30247.20","30.028"],
1646 ["30245.40","0.224"],
1647 ["30242.10","0.000"]
1648 ],
1649 "a":[
1650 ["30248.67","0.033"],
1651 ["30249.20","0.000"]
1652 ],
1653 "u":177400507,
1654 "seq":66544703342
1655 },
1656 "cts":1687940967464
1657 }"#;
1658 let expected = IncomingMessage::Orderbook(OrderbookMsg::Delta {
1659 topic: Topic::Orderbook {
1660 symbol: String::from("BTCUSDT"),
1661 depth: DepthLevel::Level50,
1662 },
1663 ts: 1687940967466,
1664 data: OrderbookDataMsg {
1665 symbol: String::from("BTCUSDT"),
1666 bids: vec![
1667 OrderbookLevel {
1668 price: dec!(30247.20),
1669 size: dec!(30.028),
1670 },
1671 OrderbookLevel {
1672 price: dec!(30245.40),
1673 size: dec!(0.224),
1674 },
1675 OrderbookLevel {
1676 price: dec!(30242.10),
1677 size: dec!(0.000),
1678 },
1679 ],
1680 asks: vec![
1681 OrderbookLevel {
1682 price: dec!(30248.67),
1683 size: dec!(0.033),
1684 },
1685 OrderbookLevel {
1686 price: dec!(30249.20),
1687 size: dec!(0.000),
1688 },
1689 ],
1690 update_id: 177400507,
1691 seq: 66544703342,
1692 },
1693 cts: 1687940967464,
1694 });
1695
1696 let message = parse(json);
1697
1698 assert_eq!(expected, message);
1699 }
1700
1701 #[test]
1702 fn deserialize_incoming_message_all_liquidation_snapshot() {
1703 let json = r#"{
1705 "topic":"allLiquidation.BTCUSDT",
1706 "type":"snapshot",
1707 "ts":1741450605553,
1708 "data":[
1709 {
1710 "T":1741450605236,
1711 "s":"BTCUSDT",
1712 "S":"Buy",
1713 "v":"0.001",
1714 "p":"85823.60"
1715 }
1716 ]
1717 }"#;
1718 let expected = AllLiquidationMsg::Snapshot {
1719 topic: Topic::AllLiquidation(String::from("BTCUSDT")),
1720 ts: 1741450605553,
1721 data: vec![AllLiquidationSnapshotMsg {
1722 time: 1741450605236,
1723 symbol: String::from("BTCUSDT"),
1724 side: Side::Buy,
1725 size: dec!(0.001),
1726 price: dec!(85823.60),
1727 }],
1728 };
1729
1730 let message = parse(json);
1731
1732 assert_eq!(IncomingMessage::AllLiquidation(expected), message);
1733 }
1734
1735 #[test]
1736 fn deserialize_incoming_message_order() {
1737 let json = r#"{
1738 "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90",
1739 "topic": "order",
1740 "creationTime": 1672364262474,
1741 "data": [
1742 {
1743 "symbol": "ETH-30DEC22-1400-C",
1744 "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020",
1745 "side": "Sell",
1746 "orderType": "Market",
1747 "cancelType": "UNKNOWN",
1748 "price": "72.5",
1749 "qty": "1",
1750 "orderIv": "",
1751 "timeInForce": "IOC",
1752 "orderStatus": "Filled",
1753 "orderLinkId": "",
1754 "lastPriceOnCreated": "",
1755 "reduceOnly": false,
1756 "leavesQty": "",
1757 "leavesValue": "",
1758 "cumExecQty": "1",
1759 "cumExecValue": "75",
1760 "avgPrice": "75",
1761 "blockTradeId": "",
1762 "positionIdx": 0,
1763 "cumExecFee": "0.358635",
1764 "closedPnl": "0",
1765 "createdTime": "1672364262444",
1766 "updatedTime": "1672364262457",
1767 "rejectReason": "EC_NoError",
1768 "stopOrderType": "",
1769 "tpslMode": "",
1770 "triggerPrice": "",
1771 "takeProfit": "",
1772 "stopLoss": "",
1773 "tpTriggerBy": "",
1774 "slTriggerBy": "",
1775 "tpLimitPrice": "",
1776 "slLimitPrice": "",
1777 "triggerDirection": 0,
1778 "triggerBy": "",
1779 "closeOnTrigger": false,
1780 "category": "option",
1781 "placeType": "price",
1782 "smpType": "None",
1783 "smpGroup": 0,
1784 "smpOrderId": "",
1785 "feeCurrency": "",
1786 "cumFeeDetail": {
1787 "MNT": "0.00242968"
1788 }
1789 }
1790 ]
1791 }"#;
1792 let order = PrivateMsg {
1793 id: String::from("5923240c6880ab-c59f-420b-9adb-3639adc9dd90"),
1794 creation_time: 1672364262474,
1795 data: vec![OrderMsg {
1796 category: Category::Option,
1797 order_id: String::from("5cf98598-39a7-459e-97bf-76ca765ee020"),
1798 order_link_id: None,
1799 is_leverage: None,
1800 block_trade_id: None,
1801 symbol: String::from("ETH-30DEC22-1400-C"),
1802 price: dec!(72.5),
1803 broker_order_price: None,
1804 qty: dec!(1.0),
1805 side: Side::Sell,
1806 position_idx: PositionIdx::OneWay,
1807 order_status: OrderStatus::Filled,
1808 create_type: None,
1809 cancel_type: CancelType::UNKNOWN,
1810 reject_reason: RejectReason::EcNoError,
1811 avg_price: Some(dec!(75.0)),
1812 leaves_qty: None,
1813 leaves_value: None,
1814 cum_exec_qty: dec!(1.0),
1815 cum_exec_value: dec!(75.0),
1816 cum_exec_fee: dec!(0.358635),
1817 closed_pnl: dec!(0.0),
1818 fee_currency: None,
1819 time_in_force: TimeInForce::IOC,
1820 order_type: OrderType::Market,
1821 stop_order_type: None,
1822 oco_trigger_by: None,
1823 order_iv: None,
1824 market_unit: None,
1825 slippage_tolerance_type: None,
1826 slippage_tolerance: None,
1827 trigger_price: None,
1828 take_profit: None,
1829 stop_loss: None,
1830 tpsl_mode: None,
1831 tp_limit_price: None,
1832 sl_limit_price: None,
1833 tp_trigger_by: None,
1834 sl_trigger_by: None,
1835 trigger_direction: TriggerDirection::UNKNOWN,
1836 trigger_by: None,
1837 last_price_on_created: None,
1838 reduce_only: false,
1839 close_on_trigger: false,
1840 place_type: Some(PlaceType::Price),
1841 smp_type: SmpType::None,
1842 smp_group: 0,
1843 smp_order_id: None,
1844 created_time: 1672364262444,
1845 updated_time: 1672364262457,
1846 cum_fee_detail: Some(serde_json::from_str(r#"{"MNT": "0.00242968"}"#).unwrap()),
1847 }],
1848 };
1849 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1850
1851 let message = parse(json);
1852
1853 assert_eq!(expected, message);
1854 }
1855
1856 #[test]
1857 fn deserialize_incoming_message_order2() {
1858 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":{}}]}"#;
1859 let order = PrivateMsg {
1860 id: String::from("108985347_ADAUSDT_140667095077548"),
1861 creation_time: 1766436947942,
1862 data: vec![OrderMsg {
1863 category: Category::Linear,
1864 order_id: String::from("ae802ad5-af70-4957-ba72-86ad7fc9c24d"),
1865 order_link_id: None,
1866 is_leverage: None,
1867 block_trade_id: None,
1868 symbol: String::from("ADAUSDT"),
1869 price: dec!(0.3862),
1870 broker_order_price: None,
1871 qty: dec!(15),
1872 side: Side::Buy,
1873 position_idx: PositionIdx::OneWay,
1874 order_status: OrderStatus::Filled,
1875 create_type: Some(CreateType::CreateByUser),
1876 cancel_type: CancelType::UNKNOWN,
1877 reject_reason: RejectReason::EcNoError,
1878 avg_price: Some(dec!(0.3679)),
1879 leaves_qty: Some(dec!(0)),
1880 leaves_value: Some(dec!(0)),
1881 cum_exec_qty: dec!(15),
1882 cum_exec_value: dec!(5.5185),
1883 cum_exec_fee: dec!(0.00303518),
1884 closed_pnl: dec!(0),
1885 fee_currency: None,
1886 time_in_force: TimeInForce::IOC,
1887 order_type: OrderType::Market,
1888 stop_order_type: None,
1889 oco_trigger_by: None,
1890 order_iv: None,
1891 market_unit: None,
1892 slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1893 slippage_tolerance: Some(dec!(0)),
1894 trigger_price: None,
1895 take_profit: None,
1896 stop_loss: None,
1897 tpsl_mode: Some(TpslMode::UNKNOWN),
1898 tp_limit_price: Some(dec!(0)),
1899 sl_limit_price: Some(dec!(0)),
1900 tp_trigger_by: None,
1901 sl_trigger_by: None,
1902 trigger_direction: TriggerDirection::UNKNOWN,
1903 trigger_by: None,
1904 last_price_on_created: Some(dec!(0.3679)),
1905 reduce_only: false,
1906 close_on_trigger: false,
1907 place_type: None,
1908 smp_type: SmpType::None,
1909 smp_group: 0,
1910 smp_order_id: None,
1911 created_time: 1766436947940,
1912 updated_time: 1766436947940,
1913 cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1914 }],
1915 };
1916 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1917
1918 let message = parse(json);
1919
1920 assert_eq!(expected, message);
1921 }
1922
1923 #[test]
1924 fn deserialize_incoming_message_order3() {
1925 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":{}}]}"#;
1926 let order = PrivateMsg {
1927 id: String::from("108985347_ADAUSDT_140667102632416"),
1928 creation_time: 1766600379878,
1929 data: vec![OrderMsg {
1930 category: Category::Linear,
1931 order_id: String::from("f0468cbc-ed2f-4fd7-9620-998f3e9f387c"),
1932 order_link_id: Some(String::from("BOT_LINK_ID-1")),
1933 is_leverage: None,
1934 block_trade_id: None,
1935 symbol: String::from("ADAUSDT"),
1936 price: dec!(0.3539),
1937 broker_order_price: None,
1938 qty: dec!(15),
1939 side: Side::Buy,
1940 position_idx: PositionIdx::OneWay,
1941 order_status: OrderStatus::New,
1942 create_type: Some(CreateType::CreateByUser),
1943 cancel_type: CancelType::UNKNOWN,
1944 reject_reason: RejectReason::EcNoError,
1945 avg_price: None,
1946 leaves_qty: Some(dec!(15)),
1947 leaves_value: Some(dec!(5.3085)),
1948 cum_exec_qty: dec!(0),
1949 cum_exec_value: dec!(0),
1950 cum_exec_fee: dec!(0),
1951 closed_pnl: dec!(0),
1952 fee_currency: None,
1953 time_in_force: TimeInForce::GTC,
1954 order_type: OrderType::Limit,
1955 stop_order_type: None,
1956 oco_trigger_by: None,
1957 order_iv: None,
1958 market_unit: None,
1959 slippage_tolerance_type: Some(SlippageToleranceType::UNKNOWN),
1960 slippage_tolerance: Some(dec!(0)),
1961 trigger_price: None,
1962 take_profit: None,
1963 stop_loss: None,
1964 tpsl_mode: Some(TpslMode::UNKNOWN),
1965 tp_limit_price: Some(dec!(0)),
1966 sl_limit_price: Some(dec!(0)),
1967 tp_trigger_by: None,
1968 sl_trigger_by: None,
1969 trigger_direction: TriggerDirection::UNKNOWN,
1970 trigger_by: None,
1971 last_price_on_created: Some(dec!(0.355)),
1972 reduce_only: false,
1973 close_on_trigger: false,
1974 place_type: None, smp_type: SmpType::None,
1976 smp_group: 0,
1977 smp_order_id: None,
1978 created_time: 1766600379876,
1979 updated_time: 1766600379876,
1980 cum_fee_detail: Some(serde_json::from_str(r#"{}"#).unwrap()),
1981 }],
1982 };
1983 let expected = IncomingMessage::Topic(TopicMessage::Order(order));
1984
1985 let message = parse(json);
1986
1987 assert_eq!(expected, message);
1988 }
1989
1990 #[test]
1991 fn deserialize_incoming_message_position() {
1992 let json = r#"{
1993 "id": "108985347_position_1765659601915",
1994 "topic": "position",
1995 "creationTime": 1765659601915,
1996 "data": [
1997 {
1998 "positionIdx": 1,
1999 "tradeMode": 0,
2000 "riskId": 116,
2001 "riskLimitValue": "200000",
2002 "symbol": "ADAUSDT",
2003 "side": "Buy",
2004 "size": "18720",
2005 "entryPrice": "0.41160027",
2006 "sessionAvgPrice": "",
2007 "leverage": "75",
2008 "positionValue": "7705.157",
2009 "positionBalance": "0",
2010 "markPrice": "0.41",
2011 "positionIM": "106.51735757",
2012 "positionMM": "61.74535757",
2013 "positionIMByMp": "106.51735757",
2014 "positionMMByMp": "61.74535757",
2015 "takeProfit": "0.4321",
2016 "stopLoss": "0.3704",
2017 "trailingStop": "0",
2018 "unrealisedPnl": "-29.957",
2019 "cumRealisedPnl": "-6712.87804378",
2020 "curRealisedPnl": "-2.6317147",
2021 "createdTime": "1714594321840",
2022 "updatedTime": "1765645142548",
2023 "tpslMode": "Full",
2024 "liqPrice": "0.37000066",
2025 "bustPrice": "",
2026 "category": "linear",
2027 "positionStatus": "Normal",
2028 "adlRankIndicator": 2,
2029 "autoAddMargin": 0,
2030 "leverageSysUpdatedTime": "",
2031 "mmrSysUpdatedTime": "",
2032 "seq": 140667058318085,
2033 "isReduceOnly": false
2034 },
2035 {
2036 "positionIdx": 2,
2037 "tradeMode": 0,
2038 "riskId": 116,
2039 "riskLimitValue": "200000",
2040 "symbol": "ADAUSDT",
2041 "side": "",
2042 "size": "0",
2043 "entryPrice": "0",
2044 "sessionAvgPrice": "",
2045 "leverage": "75",
2046 "positionValue": "0",
2047 "positionBalance": "0",
2048 "markPrice": "0.41",
2049 "positionIM": "",
2050 "positionMM": "",
2051 "positionIMByMp": "",
2052 "positionMMByMp": "",
2053 "takeProfit": "0",
2054 "stopLoss": "0",
2055 "trailingStop": "0",
2056 "unrealisedPnl": "0",
2057 "cumRealisedPnl": "1618.30675974",
2058 "curRealisedPnl": "0",
2059 "createdTime": "1714594321840",
2060 "updatedTime": "1765046350698",
2061 "tpslMode": "Full",
2062 "liqPrice": "0",
2063 "bustPrice": "",
2064 "category": "linear",
2065 "positionStatus": "Normal",
2066 "adlRankIndicator": 0,
2067 "autoAddMargin": 0,
2068 "leverageSysUpdatedTime": "",
2069 "mmrSysUpdatedTime": "",
2070 "seq": 140667031311361,
2071 "isReduceOnly": false
2072 }
2073 ]
2074 }"#;
2075 let position = PrivateMsg {
2076 id: String::from("108985347_position_1765659601915"),
2077 creation_time: 1765659601915,
2078 data: vec![
2079 PositionMsg {
2080 category: Category::Linear,
2081 symbol: String::from("ADAUSDT"),
2082 side: Some(Side::Buy),
2083 size: dec!(18720),
2084 position_idx: PositionIdx::Buy,
2085 position_value: dec!(7705.157),
2086 risk_id: 116,
2087 risk_limit_value: Some(dec!(200000)),
2088 entry_price: dec!(0.41160027),
2089 mark_price: dec!(0.41),
2090 leverage: dec!(75),
2091 auto_add_margin: false,
2092 position_im: Some(dec!(106.51735757)),
2093 position_mm: Some(dec!(61.74535757)),
2094 position_im_by_mp: Some(dec!(106.51735757)),
2095 position_mm_by_mp: Some(dec!(61.74535757)),
2096 liq_price: Some(dec!(0.37000066)),
2097 take_profit: dec!(0.4321),
2098 stop_loss: dec!(0.3704),
2099 trailing_stop: dec!(0),
2100 unrealised_pnl: dec!(-29.957),
2101 cur_realised_pnl: dec!(-2.6317147),
2102 session_avg_price: None,
2103 delta: None,
2104 gamma: None,
2105 vega: None,
2106 theta: None,
2107 cum_realised_pnl: dec!(-6712.87804378),
2108 position_status: PositionStatus::Normal,
2109 adl_rank_indicator: AdlRankIndicator::Two,
2110 is_reduce_only: false,
2111 mmr_sys_updated_time: None,
2112 leverage_sys_updated_time: None,
2113 created_time: 1714594321840,
2114 updated_time: 1765645142548,
2115 seq: 140667058318085,
2116 },
2117 PositionMsg {
2118 category: Category::Linear,
2119 symbol: String::from("ADAUSDT"),
2120 side: None,
2121 size: dec!(0),
2122 position_idx: PositionIdx::Sell,
2123 position_value: dec!(0),
2124 risk_id: 116,
2125 risk_limit_value: Some(dec!(200000)),
2126 entry_price: dec!(0),
2127 mark_price: dec!(0.41),
2128 leverage: dec!(75),
2129 auto_add_margin: false,
2130 position_im: None,
2131 position_mm: None,
2132 position_im_by_mp: None,
2133 position_mm_by_mp: None,
2134 liq_price: Some(dec!(0)),
2135 take_profit: dec!(0),
2136 stop_loss: dec!(0),
2137 trailing_stop: dec!(0),
2138 unrealised_pnl: dec!(0),
2139 cur_realised_pnl: dec!(0),
2140 session_avg_price: None,
2141 delta: None,
2142 gamma: None,
2143 vega: None,
2144 theta: None,
2145 cum_realised_pnl: dec!(1618.30675974),
2146 position_status: PositionStatus::Normal,
2147 adl_rank_indicator: AdlRankIndicator::Zero,
2148 is_reduce_only: false,
2149 mmr_sys_updated_time: None,
2150 leverage_sys_updated_time: None,
2151 created_time: 1714594321840,
2152 updated_time: 1765046350698,
2153 seq: 140667031311361,
2154 },
2155 ],
2156 };
2157 let expected = IncomingMessage::Topic(TopicMessage::Position(position));
2158
2159 let message = parse(json);
2160
2161 assert_eq!(expected, message);
2162 }
2163
2164 #[test]
2165 fn deserialize_incoming_message_position2() {
2166 let json = r#"{
2167 "id":"108985347_position_1766316605952",
2168 "topic":"position",
2169 "creationTime":1766316605952,
2170 "data":[
2171 {
2172 "positionIdx":1,
2173 "tradeMode":0,
2174 "riskId":116,
2175 "riskLimitValue":"200000",
2176 "symbol":"ADAUSDT",
2177 "side":"Buy",
2178 "size":"43",
2179 "entryPrice":"0.37293023",
2180 "sessionAvgPrice":"",
2181 "leverage":"75",
2182 "positionValue":"16.036",
2183 "positionBalance":"0",
2184 "markPrice":"0.3702",
2185 "positionIM":"0.22095025",
2186 "positionMM":"0.12809175",
2187 "positionIMByMp":"0.22095025",
2188 "positionMMByMp":"0.12809175",
2189 "takeProfit":"0",
2190 "stopLoss":"0",
2191 "trailingStop":"0",
2192 "unrealisedPnl":"-0.1174",
2193 "cumRealisedPnl":"-7547.8530836",
2194 "curRealisedPnl":"-0.00465061",
2195 "createdTime":"1714594321840",
2196 "updatedTime":"1766313370061",
2197 "tpslMode":"Full",
2198 "liqPrice":"",
2199 "bustPrice":"",
2200 "category":"linear",
2201 "positionStatus":"Normal",
2202 "adlRankIndicator":2,
2203 "autoAddMargin":0,
2204 "leverageSysUpdatedTime":"",
2205 "mmrSysUpdatedTime":"",
2206 "seq":140667089523042,
2207 "isReduceOnly":false
2208 },
2209 {
2210 "positionIdx":2,
2211 "tradeMode":0,
2212 "riskId":116,
2213 "riskLimitValue":"200000",
2214 "symbol":"ADAUSDT",
2215 "side":"",
2216 "size":"0",
2217 "entryPrice":"0",
2218 "sessionAvgPrice":"",
2219 "leverage":"75",
2220 "positionValue":"0",
2221 "positionBalance":"0",
2222 "markPrice":"0.3702",
2223 "positionIM":"",
2224 "positionMM":"",
2225 "positionIMByMp":"",
2226 "positionMMByMp":"",
2227 "takeProfit":"0",
2228 "stopLoss":"0",
2229 "trailingStop":"0",
2230 "unrealisedPnl":"0",
2231 "cumRealisedPnl":"1618.30675974",
2232 "curRealisedPnl":"0",
2233 "createdTime":"1714594321840",
2234 "updatedTime":"1765046350698",
2235 "tpslMode":"Full",
2236 "liqPrice":"0",
2237 "bustPrice":"",
2238 "category":"linear",
2239 "positionStatus":"Normal",
2240 "adlRankIndicator":0,
2241 "autoAddMargin":0,
2242 "leverageSysUpdatedTime":"",
2243 "mmrSysUpdatedTime":"",
2244 "seq":140667031311361,
2245 "isReduceOnly":false
2246 }
2247 ]
2248 }"#;
2249 let position = PrivateMsg {
2250 id: String::from("108985347_position_1766316605952"),
2251 creation_time: 1766316605952,
2252 data: vec![
2253 PositionMsg {
2254 category: Category::Linear,
2255 symbol: String::from("ADAUSDT"),
2256 side: Some(Side::Buy),
2257 size: dec!(43),
2258 position_idx: PositionIdx::Buy,
2259 position_value: dec!(16.036),
2260 risk_id: 116,
2261 risk_limit_value: Some(dec!(200000)),
2262 entry_price: dec!(0.37293023),
2263 mark_price: dec!(0.3702),
2264 leverage: dec!(75),
2265 auto_add_margin: false,
2266 position_im: Some(dec!(0.22095025)),
2267 position_mm: Some(dec!(0.12809175)),
2268 position_im_by_mp: Some(dec!(0.22095025)),
2269 position_mm_by_mp: Some(dec!(0.12809175)),
2270 liq_price: None,
2271 take_profit: dec!(0),
2272 stop_loss: dec!(0),
2273 trailing_stop: dec!(0),
2274 unrealised_pnl: dec!(-0.1174),
2275 cur_realised_pnl: dec!(-0.00465061),
2276 session_avg_price: None,
2277 delta: None,
2278 gamma: None,
2279 vega: None,
2280 theta: None,
2281 cum_realised_pnl: dec!(-7547.8530836),
2282 position_status: PositionStatus::Normal,
2283 adl_rank_indicator: AdlRankIndicator::Two,
2284 is_reduce_only: false,
2285 mmr_sys_updated_time: None,
2286 leverage_sys_updated_time: None,
2287 created_time: 1714594321840,
2288 updated_time: 1766313370061,
2289 seq: 140667089523042,
2290 },
2291 PositionMsg {
2292 category: Category::Linear,
2293 symbol: String::from("ADAUSDT"),
2294 side: None,
2295 size: dec!(0),
2296 position_idx: PositionIdx::Sell,
2297 position_value: dec!(0),
2298 risk_id: 116,
2299 risk_limit_value: Some(dec!(200000)),
2300 entry_price: dec!(0),
2301 mark_price: dec!(0.3702),
2302 leverage: dec!(75),
2303 auto_add_margin: false,
2304 position_im: None,
2305 position_mm: None,
2306 position_im_by_mp: None,
2307 position_mm_by_mp: None,
2308 liq_price: Some(dec!(0)),
2309 take_profit: dec!(0),
2310 stop_loss: dec!(0),
2311 trailing_stop: dec!(0),
2312 unrealised_pnl: dec!(0),
2313 cur_realised_pnl: dec!(0),
2314 session_avg_price: None,
2315 delta: None,
2316 gamma: None,
2317 vega: None,
2318 theta: None,
2319 cum_realised_pnl: dec!(1618.30675974),
2320 position_status: PositionStatus::Normal,
2321 adl_rank_indicator: AdlRankIndicator::Zero,
2322 is_reduce_only: false,
2323 mmr_sys_updated_time: None,
2324 leverage_sys_updated_time: None,
2325 created_time: 1714594321840,
2326 updated_time: 1765046350698,
2327 seq: 140667031311361,
2328 },
2329 ],
2330 };
2331 let expected = IncomingMessage::Topic(TopicMessage::Position(position));
2332
2333 let message = parse(json);
2334
2335 assert_eq!(expected, message);
2336 }
2337
2338 #[test]
2339 fn deserialize_incoming_message_wallet() {
2340 let json = r#"{
2341 "id": "592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e",
2342 "topic": "wallet",
2343 "creationTime": 1700034722104,
2344 "data": [
2345 {
2346 "accountIMRate": "0",
2347 "accountIMRateByMp": "0",
2348 "accountMMRate": "0",
2349 "accountMMRateByMp": "0",
2350 "totalEquity": "10262.91335023",
2351 "totalWalletBalance": "9684.46297164",
2352 "totalMarginBalance": "9684.46297164",
2353 "totalAvailableBalance": "9556.6056555",
2354 "totalPerpUPL": "0",
2355 "totalInitialMargin": "0",
2356 "totalInitialMarginByMp": "0",
2357 "totalMaintenanceMargin": "0",
2358 "totalMaintenanceMarginByMp": "0",
2359 "coin": [
2360 {
2361 "coin": "BTC",
2362 "equity": "0.00102964",
2363 "usdValue": "36.70759517",
2364 "walletBalance": "0.00102964",
2365 "availableToWithdraw": "0.00102964",
2366 "availableToBorrow": "",
2367 "borrowAmount": "0",
2368 "accruedInterest": "0",
2369 "totalOrderIM": "",
2370 "totalPositionIM": "",
2371 "totalPositionMM": "",
2372 "unrealisedPnl": "0",
2373 "cumRealisedPnl": "-0.00000973",
2374 "bonus": "0",
2375 "collateralSwitch": true,
2376 "marginCollateral": true,
2377 "locked": "0",
2378 "spotHedgingQty": "0.01592413",
2379 "spotBorrow": "0"
2380 }
2381 ],
2382 "accountLTV": "0",
2383 "accountType": "UNIFIED"
2384 }
2385 ]
2386 }"#;
2387 let coin = WalletCoin {
2388 coin: String::from("BTC"),
2389 equity: dec!(0.00102964),
2390 usd_value: dec!(36.70759517),
2391 wallet_balance: dec!(0.00102964),
2392 locked: dec!(0),
2393 spot_hedging_qty: dec!(0.01592413),
2394 borrow_amount: dec!(0),
2395 accrued_interest: dec!(0),
2396 total_order_im: None,
2397 total_position_im: None,
2398 total_position_mm: None,
2399 unrealised_pnl: dec!(0),
2400 cum_realised_pnl: dec!(-0.00000973),
2401 bonus: dec!(0),
2402 collateral_switch: true,
2403 margin_collateral: true,
2404 spot_borrow: Some(dec!(0)),
2405 };
2406 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
2407 let wallet = PrivateMsg {
2408 id: String::from("592324d2bce751-ad38-48eb-8f42-4671d1fb4d4e"),
2409 creation_time: 1700034722104,
2410 data: vec![WalletMsg {
2411 account_type: AccountType::UNIFIED,
2412 account_im_rate: dec!(0),
2413 account_im_rate_by_mp: dec!(0),
2414 account_mm_rate: dec!(0),
2415 account_mm_rate_by_mp: dec!(0),
2416 total_equity: dec!(10262.91335023),
2417 total_wallet_balance: dec!(9684.46297164),
2418 total_margin_balance: dec!(9684.46297164),
2419 total_available_balance: dec!(9556.6056555),
2420 total_perp_upl: dec!(0),
2421 total_initial_margin: dec!(0),
2422 total_initial_margin_by_mp: dec!(0),
2423 total_maintenance_margin: dec!(0),
2424 total_maintenance_margin_by_mp: dec!(0),
2425 coin,
2426 }],
2427 };
2428 let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
2429
2430 let message = parse(json);
2431
2432 assert_eq!(expected, message);
2433 }
2434
2435 #[test]
2436 fn deserialize_incoming_message_wallet2() {
2437 let json = r#"{
2438 "id":"108985347_wallet_1766318882965",
2439 "topic":"wallet",
2440 "creationTime":1766318882964,
2441 "data":[
2442 {
2443 "accountIMRate":"0.0007",
2444 "accountMMRate":"0.0004",
2445 "accountIMRateByMp":"0.0007",
2446 "accountMMRateByMp":"0.0004",
2447 "totalEquity":"102.7094181",
2448 "totalWalletBalance":"102.16591975",
2449 "totalMarginBalance":"102.16591975",
2450 "totalAvailableBalance":"102.09402758",
2451 "totalPerpUPL":"0",
2452 "totalInitialMargin":"0.07189217",
2453 "totalMaintenanceMargin":"0.04166941",
2454 "totalInitialMarginByMp":"0.07189217",
2455 "totalMaintenanceMarginByMp":"0.04166941",
2456 "coin":[
2457 {
2458 "coin":"USDT",
2459 "equity":"75.5601152",
2460 "usdValue":"75.53450032",
2461 "walletBalance":"75.5601152",
2462 "availableToWithdraw":"",
2463 "availableToBorrow":"",
2464 "borrowAmount":"0",
2465 "accruedInterest":"0",
2466 "totalOrderIM":"0",
2467 "totalPositionIM":"0.07191655",
2468 "totalPositionMM":"0.04168355",
2469 "unrealisedPnl":"0",
2470 "cumRealisedPnl":"36163.8134634",
2471 "bonus":"0",
2472 "collateralSwitch":true,
2473 "marginCollateral":true,
2474 "locked":"0",
2475 "spotHedgingQty":"0"
2476 }
2477 ],
2478 "accountLTV":"0",
2479 "accountType":"UNIFIED"
2480 }
2481 ]
2482 }"#;
2483 let coin = WalletCoin {
2484 coin: String::from("USDT"),
2485 equity: dec!(75.5601152),
2486 usd_value: dec!(75.53450032),
2487 wallet_balance: dec!(75.5601152),
2488 locked: dec!(0),
2489 spot_hedging_qty: dec!(0),
2490 borrow_amount: dec!(0),
2491 accrued_interest: dec!(0),
2492 total_order_im: Some(dec!(0)),
2493 total_position_im: Some(dec!(0.07191655)),
2494 total_position_mm: Some(dec!(0.04168355)),
2495 unrealised_pnl: dec!(0),
2496 cum_realised_pnl: dec!(36163.8134634),
2497 bonus: dec!(0),
2498 collateral_switch: true,
2499 margin_collateral: true,
2500 spot_borrow: None,
2501 };
2502 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
2503 let wallet = PrivateMsg {
2504 id: String::from("108985347_wallet_1766318882965"),
2505 creation_time: 1766318882964,
2506 data: vec![WalletMsg {
2507 account_type: AccountType::UNIFIED,
2508 account_im_rate: dec!(0.0007),
2509 account_im_rate_by_mp: dec!(0.0007),
2510 account_mm_rate: dec!(0.0004),
2511 account_mm_rate_by_mp: dec!(0.0004),
2512 total_equity: dec!(102.7094181),
2513 total_wallet_balance: dec!(102.16591975),
2514 total_margin_balance: dec!(102.16591975),
2515 total_available_balance: dec!(102.09402758),
2516 total_perp_upl: dec!(0),
2517 total_initial_margin: dec!(0.07189217),
2518 total_initial_margin_by_mp: dec!(0.07189217),
2519 total_maintenance_margin: dec!(0.04166941),
2520 total_maintenance_margin_by_mp: dec!(0.04166941),
2521 coin,
2522 }],
2523 };
2524 let expected = IncomingMessage::Topic(TopicMessage::Wallet(wallet));
2525
2526 let message = parse(json);
2527
2528 assert_eq!(expected, message);
2529 }
2530
2531 #[test]
2532 fn deserialize_incoming_message_execution() {
2533 let json = r#"{
2534 "topic": "execution",
2535 "id": "386825804_BTCUSDT_140612148849382",
2536 "creationTime": 1746270400355,
2537 "data": [
2538 {
2539 "category": "linear",
2540 "symbol": "BTCUSDT",
2541 "closedSize": "0.5",
2542 "execFee": "26.3725275",
2543 "execId": "0ab1bdf7-4219-438b-b30a-32ec863018f7",
2544 "execPrice": "95900.1",
2545 "execQty": "0.5",
2546 "execType": "Trade",
2547 "execValue": "47950.05",
2548 "feeRate": "0.00055",
2549 "tradeIv": "",
2550 "markIv": "",
2551 "blockTradeId": "",
2552 "markPrice": "95901.48",
2553 "indexPrice": "",
2554 "underlyingPrice": "",
2555 "leavesQty": "0",
2556 "orderId": "9aac161b-8ed6-450d-9cab-c5cc67c21784",
2557 "orderLinkId": "",
2558 "orderPrice": "94942.5",
2559 "orderQty": "0.5",
2560 "orderType": "Market",
2561 "stopOrderType": "UNKNOWN",
2562 "side": "Sell",
2563 "execTime": "1746270400353",
2564 "isLeverage": "0",
2565 "isMaker": false,
2566 "seq": 140612148849382,
2567 "marketUnit": "",
2568 "execPnl": "0.05",
2569 "createType": "CreateByUser",
2570 "extraFees":[{"feeCoin":"USDT","feeType":"GST","subFeeType":"IND_GST","feeRate":"0.0000675","fee":"0.006403779"}],
2571 "feeCurrency": "USDT"
2572 }
2573 ]
2574 }"#;
2575 let execution = PrivateMsg {
2576 id: String::from("386825804_BTCUSDT_140612148849382"),
2577 creation_time: 1746270400355,
2578 data: vec![ExecutionMsg {
2579 category: Category::Linear,
2580 symbol: String::from("BTCUSDT"),
2581 is_leverage: false,
2582 order_id: String::from("9aac161b-8ed6-450d-9cab-c5cc67c21784"),
2583 order_link_id: None,
2584 side: Side::Sell,
2585 order_price: dec!(94942.5),
2586 order_qty: dec!(0.5),
2587 leaves_qty: dec!(0),
2588 create_type: CreateType::CreateByUser,
2589 order_type: OrderType::Market,
2590 stop_order_type: StopOrderType::UNKNOWN,
2591 exec_fee: dec!(26.3725275),
2592 exec_id: String::from("0ab1bdf7-4219-438b-b30a-32ec863018f7"),
2593 exec_price: dec!(95900.1),
2594 exec_qty: dec!(0.5),
2595 exec_pnl: dec!(0.05),
2596 exec_type: ExecType::Trade,
2597 exec_value: dec!(47950.05),
2598 exec_time: 1746270400353,
2599 is_maker: false,
2600 fee_rate: dec!(0.00055),
2601 trade_iv: None,
2602 mark_iv: None,
2603 mark_price: dec!(95901.48),
2604 index_price: None,
2605 underlying_price: None,
2606 block_trade_id: None,
2607 closed_size: dec!(0.5),
2608 extra_fees: Some(vec![ExtraFee {
2609 fee_coin: String::from("USDT"),
2610 fee_type: ExtraFeeType::Gst,
2611 sub_fee_type: ExtraSubFeeType::IndGst,
2612 fee_rate: dec!(0.0000675),
2613 fee: dec!(0.006403779),
2614 }]),
2615 seq: 140612148849382,
2616 fee_currency: String::from("USDT"),
2617 }],
2618 };
2619 let expected = IncomingMessage::Topic(TopicMessage::Execution(execution));
2620
2621 let message = parse(json);
2622
2623 assert_eq!(expected, message);
2624 }
2625
2626 #[test]
2627 fn from_json_recognizes_topic_with_category_suffix() {
2628 let json = r#"{
2629 "id": "5923240c6880ab-c59f-420b-9adb-3639adc9dd90",
2630 "topic": "order.option",
2631 "creationTime": 1672364262474,
2632 "data": [
2633 {
2634 "symbol": "ETH-30DEC22-1400-C",
2635 "orderId": "5cf98598-39a7-459e-97bf-76ca765ee020",
2636 "side": "Sell",
2637 "orderType": "Market",
2638 "cancelType": "UNKNOWN",
2639 "price": "72.5",
2640 "qty": "1",
2641 "orderIv": "",
2642 "timeInForce": "IOC",
2643 "orderStatus": "Filled",
2644 "orderLinkId": "",
2645 "lastPriceOnCreated": "",
2646 "reduceOnly": false,
2647 "leavesQty": "",
2648 "leavesValue": "",
2649 "cumExecQty": "1",
2650 "cumExecValue": "75",
2651 "avgPrice": "75",
2652 "blockTradeId": "",
2653 "positionIdx": 0,
2654 "cumExecFee": "0.358635",
2655 "closedPnl": "0",
2656 "createdTime": "1672364262444",
2657 "updatedTime": "1672364262457",
2658 "rejectReason": "EC_NoError",
2659 "stopOrderType": "",
2660 "tpslMode": "",
2661 "triggerPrice": "",
2662 "takeProfit": "",
2663 "stopLoss": "",
2664 "tpTriggerBy": "",
2665 "slTriggerBy": "",
2666 "tpLimitPrice": "",
2667 "slLimitPrice": "",
2668 "triggerDirection": 0,
2669 "triggerBy": "",
2670 "closeOnTrigger": false,
2671 "category": "option",
2672 "placeType": "price",
2673 "smpType": "None",
2674 "smpGroup": 0,
2675 "smpOrderId": "",
2676 "feeCurrency": "",
2677 "cumFeeDetail": {
2678 "MNT": "0.00242968"
2679 }
2680 }
2681 ]
2682 }"#;
2683
2684 let message = IncomingMessage::from_json(json).unwrap();
2685
2686 assert!(matches!(
2687 message,
2688 IncomingMessage::Topic(TopicMessage::Order(PrivateMsg { ref data, .. })) if data.len() == 1
2689 ));
2690 }
2691
2692 #[test]
2693 fn from_json_error_describes_the_problem_instead_of_untagged_mismatch() {
2694 let json = r#"{
2695 "topic":"orderbook.50.BTCUSDT",
2696 "type":"delta",
2697 "ts":1687940967466,
2698 "data":{"s":"BTCUSDT","b":[["abc","30.028"]],"a":[],"u":1,"seq":1},
2699 "cts":1687940967464
2700 }"#;
2701
2702 let fast = IncomingMessage::from_json(json).unwrap_err().to_string();
2703 let untagged = deserialize_json::<IncomingMessage>(json)
2704 .unwrap_err()
2705 .to_string();
2706
2707 assert!(untagged.contains("did not match any variant"), "{untagged}");
2708 assert!(!fast.contains("did not match any variant"), "{fast}");
2709 }
2710
2711 #[test]
2712 fn from_json_error_points_at_the_offending_field() {
2713 let json = r#"{
2714 "topic":"orderbook.50.BTCUSDT",
2715 "type":"delta",
2716 "ts":1687940967466,
2717 "data":{"s":"BTCUSDT","b":[["30247.20","30.028"],["abc","1"]],"a":[],"u":1,"seq":1},
2718 "cts":1687940967464
2719 }"#;
2720
2721 let err = IncomingMessage::from_json(json).unwrap_err();
2722
2723 assert!(
2724 err.path().to_string().starts_with("data.b[1]"),
2725 "unexpected path: {}",
2726 err.path()
2727 );
2728 }
2729
2730 #[test]
2731 fn execution_with_empty_extra_fees_string() {
2732 let json = r#"{
2733 "topic": "execution",
2734 "id": "386825804_BTCUSDT_140612148849382",
2735 "creationTime": 1746270400355,
2736 "data": [
2737 {
2738 "category": "linear",
2739 "symbol": "BTCUSDT",
2740 "closedSize": "0.5",
2741 "execFee": "26.3725275",
2742 "execId": "0ab1bdf7-4219-438b-b30a-32ec863018f7",
2743 "execPrice": "95900.1",
2744 "execQty": "0.5",
2745 "execType": "Trade",
2746 "execValue": "47950.05",
2747 "feeRate": "0.00055",
2748 "tradeIv": "",
2749 "markIv": "",
2750 "blockTradeId": "",
2751 "markPrice": "95901.48",
2752 "indexPrice": "",
2753 "underlyingPrice": "",
2754 "leavesQty": "0",
2755 "orderId": "9aac161b-8ed6-450d-9cab-c5cc67c21784",
2756 "orderLinkId": "",
2757 "orderPrice": "94942.5",
2758 "orderQty": "0.5",
2759 "orderType": "Market",
2760 "stopOrderType": "UNKNOWN",
2761 "side": "Sell",
2762 "execTime": "1746270400353",
2763 "isLeverage": "0",
2764 "isMaker": false,
2765 "seq": 140612148849382,
2766 "marketUnit": "",
2767 "execPnl": "0.05",
2768 "createType": "CreateByUser",
2769 "extraFees":"",
2770 "feeCurrency": "USDT"
2771 }
2772 ]
2773 }"#;
2774
2775 let message = parse(json);
2776
2777 let IncomingMessage::Topic(TopicMessage::Execution(msg)) = message else {
2778 panic!("not an execution: {message:?}");
2779 };
2780 assert_eq!(msg.data[0].extra_fees, None);
2781 }
2782
2783 #[test]
2784 fn heartbeat_replies_of_every_stream_are_recognized() {
2785 let replies = [
2786 r#"{"success":true,"ret_msg":"pong","conn_id":"0970e817","op":"ping"}"#,
2788 r#"{"args":["1672916271846"],"op":"pong"}"#,
2790 r#"{"req_id":"test","op":"pong","args":["1675418560633"],"conn_id":"cfcb4ocs"}"#,
2792 r#"{"reqId":"1","retCode":0,"retMsg":"OK","op":"pong","connId":"cnt5leec"}"#,
2794 ];
2795
2796 for json in replies {
2797 let message = IncomingMessage::from_json(json).unwrap();
2798 assert!(message.is_ping() || message.is_pong(), "{json}");
2799 }
2800 }
2801
2802 #[test]
2803 fn order_entry_replies_are_trade_replies() {
2804 let auth = r#"{"retCode":0,"retMsg":"OK","op":"auth","connId":"cnt5leec0hvan15eukcg-2t"}"#;
2805 let order = r#"{
2806 "reqId":"test-005","retCode":0,"retMsg":"OK","op":"order.create",
2807 "data":{"orderId":"a4c1718e-fe53-4659-a118-1f6ecce04ad9","orderLinkId":""},
2808 "retExtInfo":{},
2809 "header":{"X-Bapi-Limit":"10","X-Bapi-Limit-Status":"9",
2810 "X-Bapi-Limit-Reset-Timestamp":"1711001595208",
2811 "Traceid":"38b7977b430f9bd228f4b19724794dfd","Timenow":"1711001595209"},
2812 "connId":"cnt5leec0hvan15eukcg-2v"
2813 }"#;
2814
2815 let IncomingMessage::TradeReply(auth) = IncomingMessage::from_json(auth).unwrap() else {
2816 panic!("auth reply is not a TradeReply");
2817 };
2818 assert_eq!((auth.op.as_str(), auth.ret_code), ("auth", 0));
2819
2820 let IncomingMessage::TradeReply(order) = IncomingMessage::from_json(order).unwrap() else {
2821 panic!("order reply is not a TradeReply");
2822 };
2823 assert_eq!(order.req_id.as_deref(), Some("test-005"));
2824 assert_eq!(
2825 order.data.unwrap()["orderId"],
2826 "a4c1718e-fe53-4659-a118-1f6ecce04ad9"
2827 );
2828 let header = order.header.unwrap();
2829 assert_eq!((header.limit, header.limit_status), (Some(10), Some(9)));
2830 assert_eq!(header.time_now, Some(1711001595209));
2831 }
2832}