patisson-bybit-sdk 0.2.0

Unofficial Rust SDK for the Bybit exchange API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
use crate::{
    Error, Timestamp,
    crypto::{SensitiveString, Signer},
    http::{
        APIErrorResponse, APIKeyInformation, AccountInfo, AmendOrderBatchRequest,
        AmendOrderBatchResult, AmendOrderRequest, AmendOrderResponse, CancelAllOrdersRequest,
        CancelAllOrdersResponse, CancelOrderBatchRequest, CancelOrderBatchResult,
        CancelOrderRequest, CancelOrderResponse, ClosedPnl, CursorPagination, EmptyResult,
        ExecutionEntry, GetClosedPnlParams, GetExecutionListParams, GetInstrumentsInfoParams,
        GetKLinesParams, GetOpenClosedOrdersParams, GetOrderHistoryParams, GetPositionInfoParams,
        GetTickersParams, GetTradesParams, GetTransactionLogParams, GetWalletBalanceParams,
        Headers, InstrumentsInfo, KLine, List, Order, PlaceOrderBatchRequest,
        PlaceOrderBatchResult, PlaceOrderRequest, PlaceOrderResponse, Position, Resp, Response,
        ServerTime, SetAutoAddMarginRequest, SetLeverageRequest, SetRiskLimitRequest,
        SetRiskLimitResponse, SetTradingStopRequest, SwitchCrossIsolatedMarginRequest,
        SwitchPositionModeRequest, Ticker, Trade, TransactionLog, WalletBalance,
    },
    serde::{deserialize_json, serialize_json, serialize_query},
    url::*,
};
use reqwest::{self, Method, RequestBuilder, header::HeaderMap};

pub struct Config {
    pub base_url: String,
    pub api_key: Option<SensitiveString>,
    pub api_secret: Option<SensitiveString>,
    /// Milliseconds.
    pub recv_window: Timestamp,
    /// HTTP the header for broker users only.
    pub referer: Option<String>,
}

// TODO: use proxy
#[derive(Debug)]
pub struct Client {
    base_url: String,
    headers: HeaderMap,
    client: reqwest::Client,
    signer: Option<Signer>,
}

impl Client {
    pub fn new(cfg: Config) -> Result<Self, Error> {
        let mut headers = HeaderMap::new();

        if let Some(api_key) = cfg.api_key.as_ref() {
            let api_key = api_key.expose().parse()?;
            headers.append(HEADER_X_BAPI_API_KEY, api_key);
        }

        let recv_window = cfg.recv_window.to_string().parse()?;
        headers.append(HEADER_X_BAPI_RECV_WINDOW, recv_window);

        if let Some(referer) = cfg.referer {
            let referer = referer.parse()?;
            headers.append(HEADER_X_REFERER, referer);
        }

        let signer = cfg
            .api_secret
            .map(|api_secret| -> Result<Signer, Error> {
                let api_key = cfg
                    .api_key
                    .ok_or_else(|| Error::from("api_key is required when api_secret is set"))?;
                Ok(Signer::new(api_key, api_secret, cfg.recv_window, None))
            })
            .transpose()?;

        Ok(Self {
            base_url: cfg.base_url,
            headers,
            client: reqwest::Client::builder().build()?,
            signer,
        })
    }

    fn get_signed_headers(&self, s: &str) -> HeaderMap {
        let mut headers = self.headers.clone();

        let (signature, timestamp) = self.signer.as_ref().unwrap().sign(s);
        let signature = signature.parse().unwrap();
        headers.append(HEADER_X_BAPI_SIGN, signature);
        let timestamp = timestamp.parse().unwrap();
        headers.append(HEADER_X_BAPI_TIMESTAMP, timestamp);

        headers
    }
}

// Market.
impl Client {
    #[tracing::instrument(skip(self), err)]
    pub async fn get_server_time(&self) -> Result<Response<ServerTime>, Error> {
        let url = format!("{}{}", self.base_url, Path::MarketServerTime);

        let request = self.client.request(Method::GET, url);

        let response = send(request).await?;
        Ok(response)
    }

    #[tracing::instrument(skip(self), err)]
    pub async fn get_kline(&self, params: GetKLinesParams) -> Result<Response<KLine>, Error> {
        let url = format!("{}{}", self.base_url, Path::MarketKline);

        let request = self.client.request(Method::GET, url).query(&params);

        let response = send(request).await?;
        Ok(response)
    }

    /// Get Tickers
    /// Query for the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours.
    /// If category=option, symbol or baseCoin must be passed.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_tickers(&self, params: GetTickersParams) -> Result<Response<Ticker>, Error> {
        let url = format!("{}{}", self.base_url, Path::MarketTickers);

        let request = self.client.request(Method::GET, url).query(&params);

        let response = send(request).await?;
        Ok(response)
    }

    #[tracing::instrument(skip(self), err)]
    pub async fn get_instruments_info(
        &self,
        params: GetInstrumentsInfoParams,
    ) -> Result<Response<InstrumentsInfo>, Error> {
        let url = format!("{}{}", self.base_url, Path::MarketInstrumentsInfo);

        let request = self.client.request(Method::GET, url).query(&params);

        let response = send(request).await?;
        Ok(response)
    }

    #[tracing::instrument(skip(self), err)]
    pub async fn get_public_recent_trading_history(
        &self,
        params: GetTradesParams,
    ) -> Result<Response<Trade>, Error> {
        let url = format!("{}{}", self.base_url, Path::MarketRecentTrade);

        let request = self.client.request(Method::GET, url).query(&params);

        let response = send(request).await?;
        Ok(response)
    }
}

// Trade.
impl Client {
    /// Place Order
    /// This endpoint supports to create the order for Spot, Margin trading, USDT perpetual, USDT futures, USDC perpetual, USDC futures, Inverse Futures and Options.
    ///
    /// INFO:
    /// Supported order type (orderType):
    /// Limit order: orderType=Limit, it is necessary to specify order qty and price.
    ///
    /// Market order: orderType=Market, execute at the best price in the Bybit market until the transaction is completed. When selecting a market order, the "price" can be empty. In the futures trading system, in order to protect traders against the serious slippage of the Market order, Bybit trading engine will convert the market order into an IOC limit order for matching. If there are no orderbook entries within price slippage limit, the order will not be executed. If there is insufficient liquidity, the order will be cancelled. The slippage threshold refers to the percentage that the order price deviates from the mark price. You can learn more here: Adjustments to Bybit's Derivative Trading Price Limit Mechanism
    /// Supported timeInForce strategy:
    /// GTC
    /// IOC
    /// FOK
    /// PostOnly: If the order would be filled immediately when submitted, it will be cancelled. The purpose of this is to protect your order during the submission process. If the matching system cannot entrust the order to the order book due to price changes on the market, it will be cancelled.
    /// RPI: Retail Price Improvement order. Assigned market maker can place this kind of order, and it is a post only order, only match with the order from Web or APP.
    ///
    /// How to create a conditional order:
    /// When submitting an order, if triggerPrice is set, the order will be automatically converted into a conditional order. In addition, the conditional order does not occupy the margin. If the margin is insufficient after the conditional order is triggered, the order will be cancelled.
    ///
    /// Take profit / Stop loss: You can set TP/SL while placing orders. Besides, you could modify the position's TP/SL.
    ///
    /// Order quantity: The quantity of perpetual contracts you are going to buy/sell. For the order quantity, Bybit only supports positive number at present.
    ///
    /// Order price: Place a limit order, this parameter is required. If you have position, the price should be higher than the liquidation price. For the minimum unit of the price change, please refer to the priceFilter > tickSize field in the instruments-info endpoint.
    ///
    /// orderLinkId: You can customize the active order ID. We can link this ID to the order ID in the system. Once the active order is successfully created, we will send the unique order ID in the system to you. Then, you can use this order ID to cancel active orders, and if both orderId and orderLinkId are entered in the parameter input, Bybit will prioritize the orderId to process the corresponding order. Meanwhile, your customized order ID should be no longer than 36 characters and should be unique.
    ///
    /// Open orders up limit:
    /// Perps & Futures:
    /// a) Each account can hold a maximum of 500 active orders simultaneously per symbol.
    /// b) conditional orders: each account can hold a maximum of 10 active orders simultaneously per symbol.
    /// Spot: 500 orders in total, including a maximum of 30 open TP/SL orders, a maximum of 30 open conditional orders for each symbol per account
    /// Option: a maximum of 50 open orders per account
    ///
    /// Rate limit:
    /// Please refer to rate limit table. If you need to raise the rate limit, please contact your client manager or submit an application via here
    ///
    /// Risk control limit notice:
    /// Bybit will monitor on your API requests. When the total number of orders of a single user (aggregated the number of orders across main account and subaccounts) within a day (UTC 0 - UTC 24) exceeds a certain upper limit, the platform will reserve the right to remind, warn, and impose necessary restrictions. Customers who use API default to acceptance of these terms and have the obligation to cooperate with adjustments.
    ///
    /// Reduce only orders:
    /// If reduceOnly=true and order qty > max order qty, the order will automatically be split up into multiple orders.
    ///
    /// Spot Stop Order
    /// Spot supports TP/SL order, Conditional order, however, the system logic is different between classic account and Unified account
    /// classic account: When the stop order is created, you will get an order ID. After it is triggered, you will get a new order ID
    /// Unified account: When the stop order is created, you will get an order ID. After it is triggered, the order ID will not be changed
    #[tracing::instrument(skip(self), err)]
    pub async fn place_order(
        &self,
        request: PlaceOrderRequest,
    ) -> Result<Response<PlaceOrderResponse>, Error> {
        let url = format!("{}{}", self.base_url, Path::TradeOrderCreate);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Amend Order
    /// info
    /// You can only modify unfilled or partially filled orders.
    #[tracing::instrument(skip(self), err)]
    pub async fn amend_order(
        &self,
        request: AmendOrderRequest,
    ) -> Result<Response<AmendOrderResponse>, Error> {
        let url = format!("{}{}", self.base_url, Path::TradeOrderAmend);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Cancel Order
    /// important
    /// You must specify orderId or orderLinkId to cancel the order.
    /// If orderId and orderLinkId do not match, the system will process orderId first.
    /// You can only cancel unfilled or partially filled orders.
    #[tracing::instrument(skip(self), err)]
    pub async fn cancel_order(
        &self,
        request: CancelOrderRequest,
    ) -> Result<Response<CancelOrderResponse>, Error> {
        let url = format!("{}{}", self.base_url, Path::TradeOrderCancel);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Get Open & Closed Orders.
    /// Primarily query unfilled or partially filled orders in real-time, but also supports querying recent 500 closed status (Cancelled, Filled) orders. Please see the usage of request param openOnly.
    /// And to query older order records, please use the order history interface.
    ///
    /// Tip
    /// UTA2.0 can query filled, canceled, and rejected orders to the most recent 500 orders for spot, linear, inverse and option categories
    /// UTA1.0 can query filled, canceled, and rejected orders to the most recent 500 orders for spot, linear, and option categories. The inverse category is not subject to this limitation.
    /// You can query by symbol, baseCoin, orderId and orderLinkId, and if you pass multiple params, the system will process them according to this priority: orderId > orderLinkId > symbol > baseCoin.
    /// The records are sorted by the createdTime from newest to oldest.
    ///
    /// info
    /// classic account spot can return open orders only
    /// After a server release or restart, filled, canceled, and rejected orders of Unified account should only be queried through order history.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_open_closed_orders(
        &self,
        params: GetOpenClosedOrdersParams,
    ) -> Result<Response<CursorPagination<Order>>, Error> {
        let query = serialize_query(&params)?;
        let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderRealtime);
        let headers = self.get_signed_headers(&query);

        let request = self.client.request(Method::GET, url).headers(headers);

        let response = send(request).await?;
        Ok(response)
    }

    /// Collect all pages of open/closed orders into a single `Vec`.
    /// Repeatedly calls [`get_open_closed_orders`] following `next_page_cursor`
    /// until the last page is reached.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_open_closed_orders_all(
        &self,
        params: GetOpenClosedOrdersParams,
    ) -> Result<Vec<Order>, Error> {
        let mut all = Vec::new();
        let mut p = params;
        loop {
            let page = self.get_open_closed_orders(p.clone()).await?;
            all.extend(page.result.list);
            match page.result.next_page_cursor {
                Some(cursor) => p = p.with_cursor(cursor),
                None => break,
            }
        }
        Ok(all)
    }

    /// Cancel All Orders.
    /// Cancel all open orders. Support linear, inverse, spot, and option.
    #[tracing::instrument(skip(self), err)]
    pub async fn cancel_all_orders(
        &self,
        request: CancelAllOrdersRequest,
    ) -> Result<Response<CancelAllOrdersResponse>, Error> {
        let url = format!("{}{}", self.base_url, Path::TradeOrderCancelAll);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Get Order History.
    /// Query order history. As order creation/cancellation is asynchronous, the data returned may be delayed.
    /// Supports up to 2 years of data.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_order_history(
        &self,
        params: GetOrderHistoryParams,
    ) -> Result<Response<CursorPagination<Order>>, Error> {
        let query = serialize_query(&params)?;
        let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderHistory);
        let headers = self.get_signed_headers(&query);

        let request = self.client.request(Method::GET, url).headers(headers);

        let response = send(request).await?;
        Ok(response)
    }

    /// Collect all pages of order history into a single `Vec`.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_order_history_all(
        &self,
        params: GetOrderHistoryParams,
    ) -> Result<Vec<Order>, Error> {
        let mut all = Vec::new();
        let mut p = params;
        loop {
            let page = self.get_order_history(p.clone()).await?;
            all.extend(page.result.list);
            match page.result.next_page_cursor {
                Some(cursor) => p = p.with_cursor(cursor),
                None => break,
            }
        }
        Ok(all)
    }

    /// Place Batch Orders.
    /// Supports up to 20 orders per request.
    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
    #[tracing::instrument(skip(self), err)]
    pub async fn place_orders_batch(
        &self,
        request: PlaceOrderBatchRequest,
    ) -> Result<Response<List<PlaceOrderBatchResult>>, Error> {
        let url = format!("{}{}", self.base_url, Path::TradeOrderCreateBatch);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Amend Batch Orders.
    /// Supports up to 20 orders per request.
    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
    #[tracing::instrument(skip(self), err)]
    pub async fn amend_orders_batch(
        &self,
        request: AmendOrderBatchRequest,
    ) -> Result<Response<List<AmendOrderBatchResult>>, Error> {
        let url = format!("{}{}", self.base_url, Path::TradeOrderAmendBatch);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Cancel Batch Orders.
    /// Supports up to 20 orders per request.
    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
    #[tracing::instrument(skip(self), err)]
    pub async fn cancel_orders_batch(
        &self,
        request: CancelOrderBatchRequest,
    ) -> Result<Response<List<CancelOrderBatchResult>>, Error> {
        let url = format!("{}{}", self.base_url, Path::TradeOrderCancelBatch);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }
}

// Position.
impl Client {
    /// Query real-time position data, such as position size, cumulative realized PNL, etc.
    /// Query real-time position data, such as position size, cumulative realized PNL, etc.
    ///
    /// INFO
    // UTA2.0(inverse)
    // You can query all open positions with /v5/position/list?category=inverse;
    // Cannot query multiple symbols in one request
    // UTA1.0(inverse) & Classic (inverse)
    // You can query all open positions with /v5/position/list?category=inverse;
    // symbol parameter can pass up to 10 symbols, e.g., symbol=BTCUSD,ETHUSD
    #[tracing::instrument(skip(self), err)]
    pub async fn get_position_info(
        &self,
        params: GetPositionInfoParams,
    ) -> Result<Response<CursorPagination<Position>>, Error> {
        let query = serialize_query(&params)?;
        let url = format!("{}{}?{query}", self.base_url, Path::PositionList);
        let headers = self.get_signed_headers(&query);

        let request = self.client.request(Method::GET, url).headers(headers);

        let response = send(request).await?;
        Ok(response)
    }

    /// Collect all pages of position info into a single `Vec`.
    /// Repeatedly calls [`get_position_info`] following `next_page_cursor`
    /// until the last page is reached.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_position_info_all(
        &self,
        params: GetPositionInfoParams,
    ) -> Result<Vec<Position>, Error> {
        let mut all = Vec::new();
        let mut p = params;
        loop {
            let page = self.get_position_info(p.clone()).await?;
            all.extend(page.result.list);
            match page.result.next_page_cursor {
                Some(cursor) => p = p.with_cursor(cursor),
                None => break,
            }
        }
        Ok(all)
    }

    /// Set Leverage.
    /// Set the leverage for a position. Only for isolated margin mode.
    #[tracing::instrument(skip(self), err)]
    pub async fn set_leverage(
        &self,
        request: SetLeverageRequest,
    ) -> Result<Response<EmptyResult>, Error> {
        let url = format!("{}{}", self.base_url, Path::PositionSetLeverage);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Set Trading Stop.
    /// Set take profit, stop loss, or trailing stop for a position.
    #[tracing::instrument(skip(self), err)]
    pub async fn set_trading_stop(
        &self,
        request: SetTradingStopRequest,
    ) -> Result<Response<EmptyResult>, Error> {
        let url = format!("{}{}", self.base_url, Path::PositionTradingStop);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Switch Cross/Isolated Margin.
    /// Switch the margin mode for a symbol between cross and isolated.
    #[tracing::instrument(skip(self), err)]
    pub async fn switch_cross_isolated_margin(
        &self,
        request: SwitchCrossIsolatedMarginRequest,
    ) -> Result<Response<EmptyResult>, Error> {
        let url = format!("{}{}", self.base_url, Path::PositionSwitchIsolated);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Switch Position Mode.
    /// Switch between one-way (merged single) and hedge (both sides) position mode.
    #[tracing::instrument(skip(self), err)]
    pub async fn switch_position_mode(
        &self,
        request: SwitchPositionModeRequest,
    ) -> Result<Response<EmptyResult>, Error> {
        let url = format!("{}{}", self.base_url, Path::PositionSwitchMode);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Set Auto Add Margin.
    /// Turn on/off auto-add-margin for an isolated margin position.
    #[tracing::instrument(skip(self), err)]
    pub async fn set_auto_add_margin(
        &self,
        request: SetAutoAddMarginRequest,
    ) -> Result<Response<EmptyResult>, Error> {
        let url = format!("{}{}", self.base_url, Path::PositionSetAutoAddMargin);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Set Risk Limit.
    /// Set the risk limit for a position. The response includes the new risk limit and its value.
    #[tracing::instrument(skip(self), err)]
    pub async fn set_risk_limit(
        &self,
        request: SetRiskLimitRequest,
    ) -> Result<Response<SetRiskLimitResponse>, Error> {
        let url = format!("{}{}", self.base_url, Path::PositionSetRiskLimit);
        let json = serialize_json(&request)?;
        let headers = self.get_signed_headers(&json);

        let request = self
            .client
            .request(Method::POST, url)
            .headers(headers)
            .body(json);

        let response = send(request).await?;
        Ok(response)
    }

    /// Get Closed P&L.
    /// Query the closed profit and loss records of positions.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_closed_pnl(
        &self,
        params: GetClosedPnlParams,
    ) -> Result<Response<CursorPagination<ClosedPnl>>, Error> {
        let query = serialize_query(&params)?;
        let url = format!("{}{}?{query}", self.base_url, Path::PositionClosedPnl);
        let headers = self.get_signed_headers(&query);

        let request = self.client.request(Method::GET, url).headers(headers);

        let response = send(request).await?;
        Ok(response)
    }

    /// Collect all pages of closed P&L into a single `Vec`.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_closed_pnl_all(
        &self,
        params: GetClosedPnlParams,
    ) -> Result<Vec<ClosedPnl>, Error> {
        let mut all = Vec::new();
        let mut p = params;
        loop {
            let page = self.get_closed_pnl(p.clone()).await?;
            all.extend(page.result.list);
            match page.result.next_page_cursor {
                Some(cursor) => p = p.with_cursor(cursor),
                None => break,
            }
        }
        Ok(all)
    }

    /// Get Execution List.
    /// Query users' execution (trading) records, sorted by execTime descending.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_execution_list(
        &self,
        params: GetExecutionListParams,
    ) -> Result<Response<CursorPagination<ExecutionEntry>>, Error> {
        let query = serialize_query(&params)?;
        let url = format!("{}{}?{query}", self.base_url, Path::ExecutionList);
        let headers = self.get_signed_headers(&query);

        let request = self.client.request(Method::GET, url).headers(headers);

        let response = send(request).await?;
        Ok(response)
    }

    /// Collect all pages of execution list entries into a single `Vec`.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_execution_list_all(
        &self,
        params: GetExecutionListParams,
    ) -> Result<Vec<ExecutionEntry>, Error> {
        let mut all = Vec::new();
        let mut p = params;
        loop {
            let page = self.get_execution_list(p.clone()).await?;
            all.extend(page.result.list);
            match page.result.next_page_cursor {
                Some(cursor) => p = p.with_cursor(cursor),
                None => break,
            }
        }
        Ok(all)
    }
}

// Account.
impl Client {
    /// Obtain wallet balance, query asset information of each currency. By default, currency information with assets or liabilities of 0 is not returned.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_wallet_balance(
        &self,
        params: GetWalletBalanceParams,
    ) -> Result<Response<List<WalletBalance>>, Error> {
        let query = serialize_query(&params)?;
        let url = format!("{}{}?{query}", self.base_url, Path::AccountWalletBalance);
        let headers = self.get_signed_headers(&query);

        let request = self.client.request(Method::GET, url).headers(headers);

        let response = send(request).await?;
        Ok(response)
    }

    /// Get Transaction Log
    /// Query for transaction logs in your Unified account. It supports up to 2 years worth of data.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_transaction_log(
        &self,
        params: GetTransactionLogParams,
    ) -> Result<Response<CursorPagination<TransactionLog>>, Error> {
        let query = serialize_query(&params)?;
        let url = format!("{}{}?{query}", self.base_url, Path::AccountTransactionLog);
        let headers = self.get_signed_headers(&query);

        let request = self.client.request(Method::GET, url).headers(headers);

        let response = send(request).await?;
        Ok(response)
    }

    /// Collect all pages of transaction log entries into a single `Vec`.
    /// Repeatedly calls [`get_transaction_log`] following `next_page_cursor`
    /// until the last page is reached.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_transaction_log_all(
        &self,
        params: GetTransactionLogParams,
    ) -> Result<Vec<TransactionLog>, Error> {
        let mut all = Vec::new();
        let mut p = params;
        loop {
            let page = self.get_transaction_log(p.clone()).await?;
            all.extend(page.result.list);
            match page.result.next_page_cursor {
                Some(cursor) => p = p.with_cursor(cursor),
                None => break,
            }
        }
        Ok(all)
    }

    /// Query the account information, like margin mode, account mode, etc.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_account_info(&self) -> Result<Response<AccountInfo>, Error> {
        let url = format!("{}{}", self.base_url, Path::AccountInfo);
        let query = "";
        let headers = self.get_signed_headers(query);

        let request = self.client.request(Method::GET, url).headers(headers);

        let response = send(request).await?;
        Ok(response)
    }
}

// User.
impl Client {
    /// Get API Key Information.
    /// Get the information of the api key. Use the api key pending to be checked to call the endpoint. Both master and sub user's api key are applicable.
    #[tracing::instrument(skip(self), err)]
    pub async fn get_api_key_information(&self) -> Result<Response<APIKeyInformation>, Error> {
        let url = format!("{}{}", self.base_url, Path::UserQueryApi);
        let query = "";
        let headers = self.get_signed_headers(query);

        let request = self.client.request(Method::GET, url).headers(headers);

        let response = send(request).await?;
        Ok(response)
    }
}

async fn send<T>(request: RequestBuilder) -> Result<Response<T>, Error>
where
    T: serde::de::DeserializeOwned,
{
    let start = std::time::Instant::now();
    let response = request.send().await?;
    let elapsed_ms = start.elapsed().as_millis();
    let headers = parse_headers(response.headers());
    let json = response.text().await?;
    if !headers.is_ret_code_ok() {
        let msg: APIErrorResponse = deserialize_json(&json)?;
        tracing::debug!(elapsed_ms, ret_code = msg.ret_code, "api error response");
        return Err(msg.into());
    }

    tracing::debug!(
        elapsed_ms,
        api_limit = headers.api_limit,
        api_limit_status = headers.api_limit_status,
        "api call completed"
    );
    let response: Resp<_> = deserialize_json(&json)?;
    let response = Response {
        result: response.result,
        time: response.time,
        headers,
        ret_ext_info: response.ret_ext_info,
    };
    Ok(response)
}

/// Parse response headers: ret_code, traceid, timenow, X-Bapi-Limit, X-Bapi-Limit-Status, X-Bapi-Limit-Reset-Timestamp
fn parse_headers(headers: &HeaderMap) -> Headers {
    let ret_code = headers
        .get(HEADER_RET_CODE)
        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
    let trace_id = headers
        .get(HEADER_TRACE_ID)
        .and_then(|h| h.to_str().map(|str| str.into()).ok());
    let time_now = headers
        .get(HEADER_TIME_NOW)
        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());

    let api_limit = headers
        .get(HEADER_X_BAPI_LIMIT)
        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
    let api_limit_status = headers
        .get(HEADER_X_BAPI_LIMIT_STATUS)
        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
    let api_limit_reset_timestamp = headers
        .get(HEADER_X_BAPI_LIMIT_RESET_TIMESTAMP)
        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());

    Headers {
        ret_code,
        trace_id,
        time_now,
        api_limit,
        api_limit_status,
        api_limit_reset_timestamp,
    }
}