Skip to main content

bybit/http/
mod.rs

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        // A symbol without a position: Bybit sends most fields as "".
976        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}