1mod account;
2mod asset;
3mod client;
4mod common;
5mod market;
6mod orders;
7mod positions;
8mod rate_limiter;
9mod spot_lever_token;
10mod spot_margin_trade;
11mod user;
12
13pub use account::*;
14pub use asset::*;
15pub use client::*;
16pub use common::*;
17pub use market::*;
18pub use orders::*;
19pub use positions::*;
20pub use rate_limiter::{BucketLimit, RateLimitKey, RateLimiterConfig};
21pub use spot_lever_token::*;
22pub use spot_margin_trade::*;
23pub use user::*;
24
25#[cfg(test)]
26mod tests {
27 use std::collections::HashMap;
28
29 use rust_decimal::dec;
30
31 use crate::{
32 AccountType, AdlRankIndicator, CancelType, CreateType, MarginMode, OrderStatus, OrderType,
33 PositionIdx, PositionStatus, RejectReason, Side, SmpType, SpotHedgingStatus, TimeInForce,
34 TpslMode, TriggerBy, TriggerDirection, UnifiedMarginStatus,
35 enums::{Category, StopOrderType},
36 serde::{Unique, deserialize_json},
37 };
38
39 use super::*;
40
41 #[test]
42 fn deserialize_response_kline_inverse() {
43 let json = r#"{
44 "retCode": 0,
45 "retMsg": "OK",
46 "result": {
47 "symbol": "BTCUSD",
48 "category": "inverse",
49 "list": [
50 [
51 "1670608800000",
52 "17071",
53 "17073",
54 "17027",
55 "17055.5",
56 "268611",
57 "15.74462667"
58 ],
59 [
60 "1670605200000",
61 "17071.5",
62 "17071.5",
63 "17061",
64 "17071",
65 "4177",
66 "0.24469757"
67 ],
68 [
69 "1670601600000",
70 "17086.5",
71 "17088",
72 "16978",
73 "17071.5",
74 "6356",
75 "0.37288112"
76 ]
77 ]
78 },
79 "retExtInfo": {},
80 "time": 1672025956592
81 }"#;
82 let expected = Resp {
83 ret_code: 0,
84 ret_msg: String::from("OK"),
85 result: KLine::Inverse {
86 symbol: String::from("BTCUSD"),
87 list: vec![
88 KLineRow {
89 start_time: 1670608800000,
90 open_price: dec!(17071),
91 high_price: dec!(17073),
92 low_price: dec!(17027),
93 close_price: dec!(17055.5),
94 volume: dec!(268611),
95 turnover: dec!(15.74462667),
96 },
97 KLineRow {
98 start_time: 1670605200000,
99 open_price: dec!(17071.5),
100 high_price: dec!(17071.5),
101 low_price: dec!(17061),
102 close_price: dec!(17071),
103 volume: dec!(4177),
104 turnover: dec!(0.24469757),
105 },
106 KLineRow {
107 start_time: 1670601600000,
108 open_price: dec!(17086.5),
109 high_price: dec!(17088),
110 low_price: dec!(16978),
111 close_price: dec!(17071.5),
112 volume: dec!(6356),
113 turnover: dec!(0.37288112),
114 },
115 ],
116 },
117 time: Some(1672025956592),
118 ret_ext_info: Some(RetExtInfo::default()),
119 };
120
121 let message = deserialize_json(json).unwrap();
122
123 assert_eq!(expected, message);
124 }
125
126 #[test]
127 fn deserialize_response_ticker_inverse() {
128 let json = r#"{
129 "retCode": 0,
130 "retMsg": "OK",
131 "result": {
132 "category": "inverse",
133 "list": [
134 {
135 "symbol": "BTCUSD",
136 "lastPrice": "16597.00",
137 "indexPrice": "16598.54",
138 "markPrice": "16596.00",
139 "prevPrice24h": "16464.50",
140 "price24hPcnt": "0.008047",
141 "highPrice24h": "30912.50",
142 "lowPrice24h": "15700.00",
143 "prevPrice1h": "16595.50",
144 "openInterest": "373504107",
145 "openInterestValue": "22505.67",
146 "turnover24h": "2352.94950046",
147 "volume24h": "49337318",
148 "fundingRate": "-0.001034",
149 "nextFundingTime": "1672387200000",
150 "predictedDeliveryPrice": "",
151 "basisRate": "",
152 "deliveryFeeRate": "",
153 "deliveryTime": "0",
154 "ask1Size": "1",
155 "bid1Price": "16596.00",
156 "ask1Price": "16597.50",
157 "bid1Size": "1",
158 "basis": ""
159 }
160 ]
161 },
162 "retExtInfo": {},
163 "time": 1672376496682
164 }"#;
165 let expected = Resp {
166 ret_code: 0,
167 ret_msg: String::from("OK"),
168 result: Ticker::Inverse {
169 list: vec![LinearInverseTicker {
170 symbol: String::from("BTCUSD"),
171 last_price: dec!(16597.00),
172 mark_price: dec!(16596.00),
173 index_price: dec!(16598.54),
174 prev_price24h: dec!(16464.50),
175 price24h_pcnt: dec!(0.008047),
176 high_price24h: dec!(30912.50),
177 low_price24h: dec!(15700.00),
178 prev_price1h: dec!(16595.50),
179 open_interest: dec!(373504107),
180 open_interest_value: dec!(22505.67),
181 turnover24h: dec!(2352.94950046),
182 volume24h: dec!(49337318),
183 funding_rate: Some(dec!(-0.001034)),
184 next_funding_time: 1672387200000,
185 predicted_delivery_price: None,
186 basis_rate: None,
187 basis: None,
188 delivery_fee_rate: None,
189 delivery_time: Some(0),
190 bid1_price: dec!(16596.00),
191 bid1_size: dec!(1),
192 ask1_price: dec!(16597.50),
193 ask1_size: dec!(1),
194 pre_open_price: None,
195 pre_qty: None,
196 cur_pre_listing_phase: None,
197 }],
198 },
199 time: Some(1672376496682),
200 ret_ext_info: Some(RetExtInfo::default()),
201 };
202
203 let message = deserialize_json(json).unwrap();
204
205 assert_eq!(expected, message);
206 }
207
208 #[test]
209 fn deserialize_response_trad_spot() {
210 let json = r#"{
211 "retCode": 0,
212 "retMsg": "OK",
213 "result": {
214 "category": "spot",
215 "list": [
216 {
217 "execId": "2100000000007764263",
218 "symbol": "BTCUSDT",
219 "price": "16618.49",
220 "size": "0.00012",
221 "side": "Buy",
222 "time": "1672052955758",
223 "isBlockTrade": false,
224 "isRPITrade": true
225 }
226 ]
227 },
228 "retExtInfo": {},
229 "time": 1672053054358
230 }"#;
231 let expected = Resp {
232 ret_code: 0,
233 ret_msg: String::from("OK"),
234 result: Trade::Spot {
235 list: vec![InverseLinearSpotTrade {
236 exec_id: String::from("2100000000007764263"),
237 symbol: String::from("BTCUSDT"),
238 price: dec!(16618.49),
239 size: dec!(0.00012),
240 side: Side::Buy,
241 time: 1672052955758,
242 is_block_trade: false,
243 is_rpi_trade: true,
244 }],
245 },
246 time: Some(1672053054358),
247 ret_ext_info: Some(RetExtInfo::default()),
248 };
249
250 let message = deserialize_json(json).unwrap();
251
252 assert_eq!(expected, message);
253 }
254
255 #[test]
256 fn deserialize_response_orderbook_linear() {
257 let json = r#"{
258 "retCode": 0,
259 "retMsg": "OK",
260 "result": {
261 "s": "BTCUSDT",
262 "b": [
263 ["30190.65", "0.165956"],
264 ["30190.10", "0.020000"]
265 ],
266 "a": [
267 ["30201.42", "1.038467"],
268 ["30201.50", "0.500000"]
269 ],
270 "ts": 1675418560614,
271 "u": 177400507,
272 "seq": 66544703342
273 },
274 "retExtInfo": {},
275 "time": 1675418560633
276 }"#;
277 let expected = Resp {
278 ret_code: 0,
279 ret_msg: String::from("OK"),
280 result: Orderbook {
281 symbol: String::from("BTCUSDT"),
282 bids: vec![
283 OrderbookLevel {
284 price: dec!(30190.65),
285 size: dec!(0.165956),
286 },
287 OrderbookLevel {
288 price: dec!(30190.10),
289 size: dec!(0.020000),
290 },
291 ],
292 asks: vec![
293 OrderbookLevel {
294 price: dec!(30201.42),
295 size: dec!(1.038467),
296 },
297 OrderbookLevel {
298 price: dec!(30201.50),
299 size: dec!(0.500000),
300 },
301 ],
302 ts: 1675418560614,
303 update_id: 177400507,
304 seq: 66544703342,
305 cts: None,
306 },
307 time: Some(1675418560633),
308 ret_ext_info: Some(RetExtInfo::default()),
309 };
310
311 let message = deserialize_json(json).unwrap();
312
313 assert_eq!(expected, message);
314 }
315
316 #[test]
317 fn deserialize_response_get_open_closed_orders_linear() {
318 let json = r#"{
319 "retCode": 0,
320 "retMsg": "OK",
321 "result": {
322 "list": [
323 {
324 "orderId": "fd4300ae-7847-404e-b947-b46980a4d140",
325 "orderLinkId": "test-000005",
326 "blockTradeId": "",
327 "symbol": "ETHUSDT",
328 "price": "1600.00",
329 "qty": "0.10",
330 "side": "Buy",
331 "isLeverage": "",
332 "positionIdx": 1,
333 "orderStatus": "New",
334 "cancelType": "UNKNOWN",
335 "rejectReason": "EC_NoError",
336 "avgPrice": "0",
337 "leavesQty": "0.10",
338 "leavesValue": "160",
339 "cumExecQty": "0.00",
340 "cumExecValue": "0",
341 "cumExecFee": "0",
342 "timeInForce": "GTC",
343 "orderType": "Limit",
344 "stopOrderType": "UNKNOWN",
345 "orderIv": "",
346 "triggerPrice": "0.00",
347 "takeProfit": "2500.00",
348 "stopLoss": "1500.00",
349 "tpTriggerBy": "LastPrice",
350 "slTriggerBy": "LastPrice",
351 "triggerDirection": 0,
352 "triggerBy": "UNKNOWN",
353 "lastPriceOnCreated": "",
354 "reduceOnly": false,
355 "closeOnTrigger": false,
356 "smpType": "None",
357 "smpGroup": 0,
358 "smpOrderId": "",
359 "tpslMode": "Full",
360 "tpLimitPrice": "",
361 "slLimitPrice": "",
362 "placeType": "",
363 "createdTime": "1684738540559",
364 "updatedTime": "1684738540561"
365 }
366 ],
367 "nextPageCursor": "page_args%3Dfd4300ae-7847-404e-b947-b46980a4d140%26symbol%3D6%26",
368 "category": "linear"
369 },
370 "retExtInfo": {},
371 "time": 1684765770483
372 }"#;
373 let expected = Resp {
374 ret_code: 0,
375 ret_msg: String::from("OK"),
376 result: CursorPagination {
377 category: Some(Category::Linear),
378 next_page_cursor: Some(String::from(
379 "page_args%3Dfd4300ae-7847-404e-b947-b46980a4d140%26symbol%3D6%26",
380 )),
381 list: vec![Order {
382 order_id: String::from("fd4300ae-7847-404e-b947-b46980a4d140"),
383 order_link_id: Some(String::from("test-000005")),
384 block_trade_id: None,
385 symbol: String::from("ETHUSDT"),
386 price: dec!(1600.00),
387 qty: dec!(0.10),
388 side: Side::Buy,
389 is_leverage: None,
390 position_idx: PositionIdx::Buy,
391 order_status: OrderStatus::New,
392 create_type: None,
393 cancel_type: CancelType::UNKNOWN,
394 reject_reason: RejectReason::EcNoError,
395 avg_price: Some(dec!(0.0)),
396 leaves_qty: dec!(0.10),
397 leaves_value: dec!(160),
398 cum_exec_qty: dec!(0.00),
399 cum_exec_value: dec!(0),
400 cum_exec_fee: dec!(0),
401 time_in_force: TimeInForce::GTC,
402 order_type: OrderType::Limit,
403 stop_order_type: Some(StopOrderType::UNKNOWN),
404 order_iv: None,
405 market_unit: None,
406 trigger_price: Some(dec!(0.00)),
407 take_profit: Some(dec!(2500.00)),
408 stop_loss: Some(dec!(1500.00)),
409 tpsl_mode: Some(TpslMode::Full),
410 oco_trigger_by: None,
411 tp_limit_price: None,
412 sl_limit_price: None,
413 tp_trigger_by: Some(TriggerBy::LastPrice),
414 sl_trigger_by: Some(TriggerBy::LastPrice),
415 trigger_direction: TriggerDirection::UNKNOWN,
416 trigger_by: Some(TriggerBy::UNKNOWN),
417 last_price_on_created: None,
418 base_price: None,
419 reduce_only: false,
420 close_on_trigger: false,
421 place_type: None,
422 smp_type: SmpType::None,
423 smp_group: 0,
424 smp_order_id: None,
425 created_time: 1684738540559,
426 updated_time: 1684738540561,
427 }],
428 },
429 time: Some(1684765770483),
430 ret_ext_info: Some(RetExtInfo::default()),
431 };
432
433 let message = deserialize_json(json).unwrap();
434
435 assert_eq!(expected, message);
436 }
437
438 #[test]
439 fn deserialize_response_get_open_closed_orders_linear2() {
440 let json = r#"{
441 "retCode":0,
442 "retMsg":"OK",
443 "result":{
444 "nextPageCursor":"aed77e97-492f-45be-8ada-4ff350ec07a5%3A1762701687113%2Caed77e97-492f-45be-8ada-4ff350ec07a5%3A1762701687113",
445 "category":"linear",
446 "list":[
447 {
448 "symbol":"BTCUSDT",
449 "orderType":"Limit",
450 "orderLinkId":"",
451 "slLimitPrice":"0",
452 "orderId":"aed77e97-492f-45be-8ada-4ff350ec07a5",
453 "cancelType":"UNKNOWN",
454 "avgPrice":"",
455 "stopOrderType":"",
456 "lastPriceOnCreated":"103550",
457 "orderStatus":"New",
458 "createType":"CreateByUser",
459 "takeProfit":"",
460 "cumExecValue":"0",
461 "tpslMode":"",
462 "smpType":"None",
463 "triggerDirection":0,
464 "blockTradeId":"",
465 "isLeverage":"",
466 "rejectReason":"EC_NoError",
467 "price":"103000",
468 "orderIv":"",
469 "createdTime":"1762701687113",
470 "tpTriggerBy":"",
471 "positionIdx":1,
472 "timeInForce":"GTC",
473 "leavesValue":"1030",
474 "updatedTime":"1762701687113",
475 "side":"Buy",
476 "smpGroup":0,
477 "triggerPrice":"",
478 "tpLimitPrice":"0",
479 "cumExecFee":"0",
480 "leavesQty":"0.01",
481 "slTriggerBy":"",
482 "closeOnTrigger":false,
483 "placeType":"",
484 "cumExecQty":"0",
485 "reduceOnly":false,
486 "qty":"0.01",
487 "stopLoss":"",
488 "marketUnit":"",
489 "smpOrderId":"",
490 "triggerBy":""
491 }
492 ]
493 },
494 "retExtInfo":{},
495 "time":1762711342768
496 }"#;
497 let expected = Resp {
498 ret_code: 0,
499 ret_msg: String::from("OK"),
500 result: CursorPagination {
501 category: Some(Category::Linear),
502 next_page_cursor: Some(String::from(
503 "aed77e97-492f-45be-8ada-4ff350ec07a5%3A1762701687113%2Caed77e97-492f-45be-8ada-4ff350ec07a5%3A1762701687113",
504 )),
505 list: vec![Order {
506 order_id: String::from("aed77e97-492f-45be-8ada-4ff350ec07a5"),
507 order_link_id: None,
508 block_trade_id: None,
509 symbol: String::from("BTCUSDT"),
510 price: dec!(103000),
511 qty: dec!(0.01),
512 side: Side::Buy,
513 is_leverage: None,
514 position_idx: PositionIdx::Buy,
515 order_status: OrderStatus::New,
516 create_type: Some(CreateType::CreateByUser),
517 cancel_type: CancelType::UNKNOWN,
518 reject_reason: RejectReason::EcNoError,
519 avg_price: None,
520 leaves_qty: dec!(0.01),
521 leaves_value: dec!(1030),
522 cum_exec_qty: dec!(0),
523 cum_exec_value: dec!(0),
524 cum_exec_fee: dec!(0),
525 time_in_force: TimeInForce::GTC,
526 order_type: OrderType::Limit,
527 stop_order_type: None,
528 order_iv: None,
529 market_unit: None,
530 trigger_price: None,
531 take_profit: None,
532 stop_loss: None,
533 tpsl_mode: None,
534 oco_trigger_by: None,
535 tp_limit_price: Some(dec!(0)),
536 sl_limit_price: Some(dec!(0)),
537 tp_trigger_by: None,
538 sl_trigger_by: None,
539 trigger_direction: TriggerDirection::UNKNOWN,
540 trigger_by: None,
541 last_price_on_created: Some(dec!(103550)),
542 base_price: None,
543 reduce_only: false,
544 close_on_trigger: false,
545 place_type: None,
546 smp_type: SmpType::None,
547 smp_group: 0,
548 smp_order_id: None,
549 created_time: 1762701687113,
550 updated_time: 1762701687113,
551 }],
552 },
553 time: Some(1762711342768),
554 ret_ext_info: Some(RetExtInfo::default()),
555 };
556
557 let message = deserialize_json(json).unwrap();
558
559 assert_eq!(expected, message);
560 }
561
562 #[test]
563 fn deserialize_response_get_position_info_inverse() {
564 let json = r#"{
565 "retCode": 0,
566 "retMsg": "OK",
567 "result": {
568 "list": [
569 {
570 "positionIdx": 0,
571 "riskId": 1,
572 "riskLimitValue": "150",
573 "symbol": "BTCUSD",
574 "side": "Sell",
575 "size": "300",
576 "avgPrice": "27464.50441675",
577 "positionValue": "0.01092319",
578 "tradeMode": 0,
579 "positionStatus": "Normal",
580 "autoAddMargin": 1,
581 "adlRankIndicator": 2,
582 "leverage": "10",
583 "positionBalance": "0.00139186",
584 "markPrice": "28224.50",
585 "liqPrice": "",
586 "bustPrice": "999999.00",
587 "positionMM": "0.0000015",
588 "positionMMByMp": "0.0000015",
589 "positionIM": "0.00010923",
590 "positionIMByMp": "0.00010923",
591 "tpslMode": "Full",
592 "takeProfit": "0.00",
593 "stopLoss": "0.00",
594 "trailingStop": "0.00",
595 "unrealisedPnl": "-0.00029413",
596 "curRealisedPnl": "0.00013123",
597 "cumRealisedPnl": "-0.00096902",
598 "seq": 5723621632,
599 "isReduceOnly": false,
600 "mmrSysUpdatedTime": "",
601 "leverageSysUpdatedTime": "",
602 "sessionAvgPrice": "",
603 "createdTime": "1676538056258",
604 "updatedTime": "1697673600012"
605 }
606 ],
607 "nextPageCursor": "",
608 "category": "inverse"
609 },
610 "retExtInfo": {},
611 "time": 1697684980172
612 }"#;
613 let expected = Resp {
614 ret_code: 0,
615 ret_msg: String::from("OK"),
616 result: CursorPagination {
617 category: Some(Category::Inverse),
618 next_page_cursor: None,
619 list: vec![Position {
620 position_idx: PositionIdx::OneWay,
621 risk_id: 1,
622 risk_limit_value: Some(dec!(150)),
623 symbol: String::from("BTCUSD"),
624 side: Some(Side::Sell),
625 size: dec!(300),
626 avg_price: dec!(27464.50441675),
627 position_value: Some(dec!(0.01092319)),
628 auto_add_margin: true,
629 position_status: Some(PositionStatus::Normal),
630 leverage: dec!(10),
631 mark_price: dec!(28224.50),
632 liq_price: None,
633 position_im: Some(dec!(0.00010923)),
634 position_im_by_mp: Some(dec!(0.00010923)),
635 position_mm: Some(dec!(0.0000015)),
636 position_mm_by_mp: Some(dec!(0.0000015)),
637 take_profit: Some(dec!(0.00)),
638 stop_loss: Some(dec!(0.00)),
639 trailing_stop: Some(dec!(0.00)),
640 session_avg_price: None,
641 break_even_price: None,
642 net_delta_ratio: None,
643 open_time: None,
644 delta: None,
645 gamma: None,
646 vega: None,
647 theta: None,
648 unrealised_pnl: Some(dec!(-0.00029413)),
649 cur_realised_pnl: dec!(0.00013123),
650 cum_realised_pnl: dec!(-0.00096902),
651 adl_rank_indicator: AdlRankIndicator::Two,
652 created_time: 1676538056258,
653 updated_time: 1697673600012,
654 seq: 5723621632,
655 is_reduce_only: false,
656 mmr_sys_updated_time: None,
657 leverage_sys_updated_time: None,
658 }],
659 },
660 time: Some(1697684980172),
661 ret_ext_info: Some(RetExtInfo::default()),
662 };
663
664 let message: Resp<CursorPagination<Position>> = deserialize_json(json).unwrap();
665
666 assert_eq!(message, expected);
667 }
668
669 #[test]
670 fn deserialize_response_get_wallet_balance() {
671 let json = r#"{
672 "retCode": 0,
673 "retMsg": "OK",
674 "result": {
675 "list": [
676 {
677 "totalEquity": "3.31216591",
678 "accountIMRate": "0",
679 "accountIMRateByMp": "0",
680 "totalMarginBalance": "3.00326056",
681 "totalInitialMargin": "0",
682 "totalInitialMarginByMp": "0",
683 "accountType": "UNIFIED",
684 "totalAvailableBalance": "3.00326056",
685 "accountMMRate": "0",
686 "accountMMRateByMp": "0",
687 "totalPerpUPL": "0",
688 "totalWalletBalance": "3.00326056",
689 "accountLTV": "0",
690 "totalMaintenanceMargin": "0",
691 "totalMaintenanceMarginByMp": "0",
692 "coin": [
693 {
694 "availableToBorrow": "3",
695 "bonus": "0",
696 "accruedInterest": "0",
697 "availableToWithdraw": "0",
698 "totalOrderIM": "0",
699 "equity": "0",
700 "totalPositionMM": "0",
701 "usdValue": "0",
702 "spotHedgingQty": "0.01592413",
703 "unrealisedPnl": "0",
704 "collateralSwitch": true,
705 "borrowAmount": "0.0",
706 "totalPositionIM": "0",
707 "walletBalance": "0",
708 "cumRealisedPnl": "0",
709 "locked": "0",
710 "marginCollateral": true,
711 "coin": "BTC",
712 "spotBorrow": "0"
713 }
714 ]
715 }
716 ]
717 },
718 "retExtInfo": {},
719 "time": 1690872862481
720 }"#;
721 let coin = WalletCoin {
722 coin: String::from("BTC"),
723 equity: dec!(0),
724 usd_value: dec!(0),
725 wallet_balance: dec!(0),
726 locked: dec!(0),
727 spot_hedging_qty: dec!(0.01592413),
728 borrow_amount: dec!(0.0),
729 accrued_interest: dec!(0),
730 total_order_im: Some(dec!(0)),
731 total_position_im: Some(dec!(0)),
732 total_position_mm: Some(dec!(0)),
733 unrealised_pnl: dec!(0),
734 cum_realised_pnl: dec!(0),
735 bonus: dec!(0),
736 margin_collateral: true,
737 collateral_switch: true,
738 spot_borrow: Some(dec!(0)),
739 };
740 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
741 let expected = Resp {
742 ret_code: 0,
743 ret_msg: String::from("OK"),
744 result: List {
745 list: vec![WalletBalance {
746 account_type: AccountType::UNIFIED,
747 account_im_rate: dec!(0),
748 account_im_rate_by_mp: dec!(0),
749 account_mm_rate: dec!(0),
750 account_mm_rate_by_mp: dec!(0),
751 total_equity: dec!(3.31216591),
752 total_wallet_balance: dec!(3.00326056),
753 total_margin_balance: dec!(3.00326056),
754 total_available_balance: dec!(3.00326056),
755 total_perp_upl: dec!(0),
756 total_initial_margin: dec!(0),
757 total_initial_margin_by_mp: dec!(0),
758 total_maintenance_margin: dec!(0),
759 total_maintenance_margin_by_mp: dec!(0),
760 coin,
761 }],
762 },
763 time: Some(1690872862481),
764 ret_ext_info: Some(RetExtInfo::default()),
765 };
766
767 let message = deserialize_json(json).unwrap();
768
769 assert_eq!(expected, message);
770 }
771
772 #[test]
773 fn deserialize_response_get_wallet_balance_2() {
774 let json = r#"{
775 "retCode":0,
776 "retMsg":"OK",
777 "result":{
778 "list":[
779 {
780 "totalEquity":"36.42053792",
781 "accountIMRate":"0",
782 "accountIMRateByMp":"0",
783 "totalMarginBalance":"36.42053792",
784 "totalInitialMargin":"0",
785 "totalInitialMarginByMp":"0",
786 "accountType":"UNIFIED",
787 "totalAvailableBalance":"36.42053792",
788 "accountMMRate":"0",
789 "accountMMRateByMp":"0",
790 "totalPerpUPL":"0",
791 "totalWalletBalance":"36.42053792",
792 "accountLTV":"0",
793 "totalMaintenanceMargin":"0",
794 "totalMaintenanceMarginByMp":"0",
795 "coin":[
796 {
797 "availableToBorrow":"",
798 "bonus":"0",
799 "accruedInterest":"0",
800 "availableToWithdraw":"",
801 "totalOrderIM":"0",
802 "equity":"36.4061211",
803 "totalPositionMM":"0",
804 "usdValue":"36.42053792",
805 "unrealisedPnl":"0",
806 "collateralSwitch":true,
807 "spotHedgingQty":"0",
808 "borrowAmount":"0.000000000000000000",
809 "totalPositionIM":"0",
810 "walletBalance":"36.4061211",
811 "cumRealisedPnl":"-2084.9938789",
812 "locked":"0",
813 "marginCollateral":true,
814 "coin":"USDT",
815 "spotBorrow": "0"
816 }
817 ]
818 }
819 ]
820 },
821 "retExtInfo":{},
822 "time":1751570498412
823 }"#;
824 let coin = WalletCoin {
825 coin: String::from("USDT"),
826 equity: dec!(36.4061211),
827 usd_value: dec!(36.42053792),
828 wallet_balance: dec!(36.4061211),
829 locked: dec!(0),
830 spot_hedging_qty: dec!(0),
831 borrow_amount: dec!(0.000000000000000000),
832 accrued_interest: dec!(0),
833 total_order_im: Some(dec!(0)),
834 total_position_im: Some(dec!(0)),
835 total_position_mm: Some(dec!(0)),
836 unrealised_pnl: dec!(0),
837 cum_realised_pnl: dec!(-2084.9938789),
838 bonus: dec!(0),
839 margin_collateral: true,
840 collateral_switch: true,
841 spot_borrow: Some(dec!(0)),
842 };
843 let coin = HashMap::from([(Unique::unique_key(&coin), coin)]);
844 let expected = Resp {
845 ret_code: 0,
846 ret_msg: String::from("OK"),
847 result: List {
848 list: vec![WalletBalance {
849 account_type: AccountType::UNIFIED,
850 account_im_rate: dec!(0),
851 account_im_rate_by_mp: dec!(0),
852 account_mm_rate: dec!(0),
853 account_mm_rate_by_mp: dec!(0),
854 total_equity: dec!(36.42053792),
855 total_wallet_balance: dec!(36.42053792),
856 total_margin_balance: dec!(36.42053792),
857 total_available_balance: dec!(36.42053792),
858 total_perp_upl: dec!(0),
859 total_initial_margin: dec!(0),
860 total_initial_margin_by_mp: dec!(0),
861 total_maintenance_margin: dec!(0),
862 total_maintenance_margin_by_mp: dec!(0),
863 coin,
864 }],
865 },
866 time: Some(1751570498412),
867 ret_ext_info: Some(RetExtInfo::default()),
868 };
869
870 let message = deserialize_json(json).unwrap();
871
872 assert_eq!(expected, message);
873 }
874
875 #[test]
876 fn deserialize_response_get_account_info() {
877 let json = r#"{
878 "retCode": 0,
879 "retMsg": "OK",
880 "result": {
881 "marginMode": "REGULAR_MARGIN",
882 "updatedTime": "1697078946000",
883 "unifiedMarginStatus": 4,
884 "dcpStatus": "OFF",
885 "timeWindow": 10,
886 "smpGroup": 0,
887 "isMasterTrader": false,
888 "spotHedgingStatus": "OFF"
889 }
890 }"#;
891 let expected = Resp {
892 ret_code: 0,
893 ret_msg: String::from("OK"),
894 result: AccountInfo {
895 unified_margin_status: UnifiedMarginStatus::UnifiedTradingAccount1Pro,
896 margin_mode: MarginMode::RegularMargin,
897 is_master_trader: false,
898 spot_hedging_status: SpotHedgingStatus::Off,
899 updated_time: 1697078946000,
900 },
901 time: None,
902 ret_ext_info: None,
903 };
904
905 let message = deserialize_json(json).unwrap();
906
907 assert_eq!(expected, message);
908 }
909
910 #[test]
911 fn deserialize_position_break_even_price_and_open_time() {
912 let json = r#"{
913 "retCode": 0,
914 "retMsg": "OK",
915 "result": {
916 "list": [
917 {
918 "breakEvenPrice": "26500.5",
919 "netDeltaRatio": "",
920 "openTime": "1697673600000",
921 "positionIdx": 0,
922 "riskId": 1,
923 "riskLimitValue": "150",
924 "symbol": "BTCUSD",
925 "side": "Sell",
926 "size": "300",
927 "avgPrice": "27464.50441675",
928 "positionValue": "0.01092319",
929 "tradeMode": 0,
930 "positionStatus": "Normal",
931 "autoAddMargin": 1,
932 "adlRankIndicator": 2,
933 "leverage": "10",
934 "positionBalance": "0.00139186",
935 "markPrice": "28224.50",
936 "liqPrice": "",
937 "bustPrice": "999999.00",
938 "positionMM": "0.0000015",
939 "positionMMByMp": "0.0000015",
940 "positionIM": "0.00010923",
941 "positionIMByMp": "0.00010923",
942 "tpslMode": "Full",
943 "takeProfit": "0.00",
944 "stopLoss": "0.00",
945 "trailingStop": "0.00",
946 "unrealisedPnl": "-0.00029413",
947 "curRealisedPnl": "0.00013123",
948 "cumRealisedPnl": "-0.00096902",
949 "seq": 5723621632,
950 "isReduceOnly": false,
951 "mmrSysUpdatedTime": "",
952 "leverageSysUpdatedTime": "",
953 "sessionAvgPrice": "",
954 "createdTime": "1676538056258",
955 "updatedTime": "1697673600012"
956 }
957 ],
958 "nextPageCursor": "",
959 "category": "inverse"
960 },
961 "retExtInfo": {},
962 "time": 1697684980172
963 }"#;
964
965 let response: Resp<CursorPagination<Position>> = deserialize_json(json).unwrap();
966
967 let position = &response.result.list[0];
968 assert_eq!(position.break_even_price, Some(rust_decimal::dec!(26500.5)));
969 assert_eq!(position.net_delta_ratio, None);
970 assert_eq!(position.open_time, Some(1697673600000));
971 }
972
973 #[test]
974 fn deserialize_position_placeholder_without_a_position() {
975 let json = r#"{"retCode":0,"retMsg":"OK","result":{"nextPageCursor":"","category":"inverse","list":[{"symbol":"BTCUSD","leverage":"10","autoAddMargin":0,"avgPrice":"","liqPrice":"","riskLimitValue":"150","takeProfit":"","positionValue":"","tpslMode":"","isReduceOnly":false,"riskId":1,"trailingStop":"","unrealisedPnl":"","markPrice":"","adlRankIndicator":0,"cumRealisedPnl":"","positionMM":"","createdTime":"","positionIdx":0,"positionIM":"","seq":-1,"updatedTime":"","side":"","bustPrice":"","positionBalance":"","leverageSysUpdatedTime":"","curRealisedPnl":"","size":"0","positionStatus":"","mmrSysUpdatedTime":"","stopLoss":"","tradeMode":0,"sessionAvgPrice":""}]},"retExtInfo":{},"time":1791225338294}"#;
977
978 let response: Resp<CursorPagination<Position>> = deserialize_json(json).unwrap();
979
980 let position = &response.result.list[0];
981 assert!(position.size.is_zero());
982 assert!(position.avg_price.is_zero());
983 assert_eq!(position.position_status, None);
984 assert_eq!(position.created_time, 0);
985 }
986}