Skip to main content

bybit/http/
client.rs

1use crate::{
2    Error, Timestamp,
3    crypto::{SensitiveString, Signer},
4    http::{
5        APIErrorResponse, APIKeyInformation, AccountInfo, AmendOrderBatchRequest,
6        AmendOrderBatchResult, AmendOrderRequest, AmendOrderResponse, CancelAllOrdersRequest,
7        CancelAllOrdersResponse, CancelOrderBatchRequest, CancelOrderBatchResult,
8        CancelOrderRequest, CancelOrderResponse, ClosedPnl, CursorPagination, DeliveryPrice,
9        EmptyResult, ExecutionEntry, FeeRateEntry, FundingRateHistory, GetClosedPnlParams,
10        GetDeliveryPriceParams, GetExecutionListParams, GetFeeRateParams,
11        GetFundingRateHistoryParams, GetHistoricalVolatilityParams, GetInstrumentsInfoParams,
12        GetInsuranceParams, GetKLinesParams, GetOpenClosedOrdersParams, GetOpenInterestParams,
13        GetOrderHistoryParams, GetOrderbookParams, GetPositionInfoParams, GetRiskLimitParams,
14        GetSpotBorrowCheckParams, GetTickersParams, GetTradesParams, GetTransactionLogParams,
15        GetWalletBalanceParams, Headers, HistoricalVolatilityEntry, InstrumentsInfo, Insurance,
16        KLine, List, OpenInterest, Order, Orderbook, PlaceOrderBatchRequest, PlaceOrderBatchResult,
17        PlaceOrderRequest, PlaceOrderResponse, Position, Resp, Response, RiskLimit, ServerTime,
18        SetAutoAddMarginRequest, SetLeverageRequest, SetMarginModeRequest, SetMarginModeResponse,
19        SetRiskLimitRequest, SetRiskLimitResponse, SetTradingStopRequest, SpotBorrowCheck,
20        SwitchCrossIsolatedMarginRequest, SwitchPositionModeRequest, Ticker, Trade, TransactionLog,
21        WalletBalance,
22    },
23    serde::{deserialize_json, serialize_json, serialize_query},
24    url::*,
25};
26use reqwest::{self, Method, RequestBuilder, header::HeaderMap};
27
28pub struct Config {
29    pub base_url: String,
30    pub api_key: Option<SensitiveString>,
31    pub api_secret: Option<SensitiveString>,
32    /// Milliseconds.
33    pub recv_window: Timestamp,
34    /// HTTP the header for broker users only.
35    pub referer: Option<String>,
36}
37
38// TODO: use proxy
39#[derive(Debug)]
40pub struct Client {
41    base_url: String,
42    headers: HeaderMap,
43    client: reqwest::Client,
44    signer: Option<Signer>,
45}
46
47impl Client {
48    pub fn new(cfg: Config) -> Result<Self, Error> {
49        let mut headers = HeaderMap::new();
50
51        if let Some(api_key) = cfg.api_key.as_ref() {
52            let api_key = api_key.expose().parse()?;
53            headers.append(HEADER_X_BAPI_API_KEY, api_key);
54        }
55
56        let recv_window = cfg.recv_window.to_string().parse()?;
57        headers.append(HEADER_X_BAPI_RECV_WINDOW, recv_window);
58
59        if let Some(referer) = cfg.referer {
60            let referer = referer.parse()?;
61            headers.append(HEADER_X_REFERER, referer);
62        }
63
64        let signer = cfg
65            .api_secret
66            .map(|api_secret| -> Result<Signer, Error> {
67                let api_key = cfg
68                    .api_key
69                    .ok_or_else(|| Error::from("api_key is required when api_secret is set"))?;
70                Ok(Signer::new(api_key, api_secret, cfg.recv_window, None))
71            })
72            .transpose()?;
73
74        Ok(Self {
75            base_url: cfg.base_url,
76            headers,
77            client: reqwest::Client::builder().build()?,
78            signer,
79        })
80    }
81
82    fn get_signed_headers(&self, s: &str) -> HeaderMap {
83        let mut headers = self.headers.clone();
84
85        let (signature, timestamp) = self.signer.as_ref().unwrap().sign(s);
86        let signature = signature.parse().unwrap();
87        headers.append(HEADER_X_BAPI_SIGN, signature);
88        let timestamp = timestamp.parse().unwrap();
89        headers.append(HEADER_X_BAPI_TIMESTAMP, timestamp);
90
91        headers
92    }
93}
94
95// Market.
96impl Client {
97    #[tracing::instrument(skip(self), err)]
98    pub async fn get_server_time(&self) -> Result<Response<ServerTime>, Error> {
99        let url = format!("{}{}", self.base_url, Path::MarketServerTime);
100
101        let request = self.client.request(Method::GET, url);
102
103        let response = send(request).await?;
104        Ok(response)
105    }
106
107    #[tracing::instrument(skip(self), err)]
108    pub async fn get_kline(&self, params: &GetKLinesParams) -> Result<Response<KLine>, Error> {
109        let url = format!("{}{}", self.base_url, Path::MarketKline);
110
111        let request = self.client.request(Method::GET, url).query(params);
112
113        let response = send(request).await?;
114        Ok(response)
115    }
116
117    /// Get Mark Price Kline.
118    /// Query the mark price kline data. Charts are returned in groups based on the requested interval.
119    ///
120    /// Covers: linear / inverse
121    #[tracing::instrument(skip(self), err)]
122    pub async fn get_mark_price_kline(
123        &self,
124        params: &GetKLinesParams,
125    ) -> Result<Response<KLine>, Error> {
126        let url = format!("{}{}", self.base_url, Path::MarketMarkPriceKline);
127
128        let request = self.client.request(Method::GET, url).query(params);
129
130        let response = send(request).await?;
131        Ok(response)
132    }
133
134    /// Get Index Price Kline.
135    /// Query the index price kline data. Charts are returned in groups based on the requested interval.
136    ///
137    /// Covers: linear / inverse
138    #[tracing::instrument(skip(self), err)]
139    pub async fn get_index_price_kline(
140        &self,
141        params: &GetKLinesParams,
142    ) -> Result<Response<KLine>, Error> {
143        let url = format!("{}{}", self.base_url, Path::MarketIndexPriceKline);
144
145        let request = self.client.request(Method::GET, url).query(params);
146
147        let response = send(request).await?;
148        Ok(response)
149    }
150
151    /// Get Premium Index Price Kline.
152    /// Retrieve the premium index price kline data. Charts are returned in groups based on the requested interval.
153    ///
154    /// Covers: linear
155    #[tracing::instrument(skip(self), err)]
156    pub async fn get_premium_index_price_kline(
157        &self,
158        params: &GetKLinesParams,
159    ) -> Result<Response<KLine>, Error> {
160        let url = format!("{}{}", self.base_url, Path::MarketPremiumIndexPriceKline);
161
162        let request = self.client.request(Method::GET, url).query(params);
163
164        let response = send(request).await?;
165        Ok(response)
166    }
167
168    /// Get Tickers
169    /// Query for the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours.
170    /// If category=option, symbol or baseCoin must be passed.
171    #[tracing::instrument(skip(self), err)]
172    pub async fn get_tickers(&self, params: &GetTickersParams) -> Result<Response<Ticker>, Error> {
173        let url = format!("{}{}", self.base_url, Path::MarketTickers);
174
175        let request = self.client.request(Method::GET, url).query(params);
176
177        let response = send(request).await?;
178        Ok(response)
179    }
180
181    /// Get Orderbook.
182    /// Query for orderbook depth data.
183    ///
184    /// Covers: Spot / USDT contract / USDC contract / Inverse contract / Option
185    #[tracing::instrument(skip(self), err)]
186    pub async fn get_orderbook(
187        &self,
188        params: &GetOrderbookParams,
189    ) -> Result<Response<Orderbook>, Error> {
190        let url = format!("{}{}", self.base_url, Path::MarketOrderbook);
191
192        let request = self.client.request(Method::GET, url).query(params);
193
194        let response = send(request).await?;
195        Ok(response)
196    }
197
198    #[tracing::instrument(skip(self), err)]
199    pub async fn get_instruments_info(
200        &self,
201        params: &GetInstrumentsInfoParams,
202    ) -> Result<Response<InstrumentsInfo>, Error> {
203        let url = format!("{}{}", self.base_url, Path::MarketInstrumentsInfo);
204
205        let request = self.client.request(Method::GET, url).query(params);
206
207        let response = send(request).await?;
208        Ok(response)
209    }
210
211    #[tracing::instrument(skip(self), err)]
212    pub async fn get_public_recent_trading_history(
213        &self,
214        params: &GetTradesParams,
215    ) -> Result<Response<Trade>, Error> {
216        let url = format!("{}{}", self.base_url, Path::MarketRecentTrade);
217
218        let request = self.client.request(Method::GET, url).query(params);
219
220        let response = send(request).await?;
221        Ok(response)
222    }
223
224    /// Get Funding Rate History.
225    /// Query for historical funding rates. Each request returns up to 200 rows of data.
226    ///
227    /// Covers: linear / inverse
228    #[tracing::instrument(skip(self), err)]
229    pub async fn get_funding_rate_history(
230        &self,
231        params: &GetFundingRateHistoryParams,
232    ) -> Result<Response<FundingRateHistory>, Error> {
233        let url = format!("{}{}", self.base_url, Path::MarketFundingHistory);
234
235        let request = self.client.request(Method::GET, url).query(params);
236
237        let response = send(request).await?;
238        Ok(response)
239    }
240
241    /// Get Open Interest.
242    /// Get the open interest of each symbol.
243    ///
244    /// Covers: linear / inverse
245    #[tracing::instrument(skip(self), err)]
246    pub async fn get_open_interest(
247        &self,
248        params: &GetOpenInterestParams,
249    ) -> Result<Response<OpenInterest>, Error> {
250        let url = format!("{}{}", self.base_url, Path::MarketOpenInterest);
251
252        let request = self.client.request(Method::GET, url).query(params);
253
254        let response = send(request).await?;
255        Ok(response)
256    }
257
258    /// Get Historical Volatility.
259    /// Query option historical volatility.
260    ///
261    /// Covers: option only
262    ///
263    /// Note: the result is returned as a top-level JSON array, so the response
264    /// wraps `Vec<HistoricalVolatilityEntry>` directly.
265    #[tracing::instrument(skip(self), err)]
266    pub async fn get_historical_volatility(
267        &self,
268        params: &GetHistoricalVolatilityParams,
269    ) -> Result<Response<Vec<HistoricalVolatilityEntry>>, Error> {
270        let url = format!("{}{}", self.base_url, Path::MarketHistoricalVolatility);
271
272        let request = self.client.request(Method::GET, url).query(params);
273
274        let response = send(request).await?;
275        Ok(response)
276    }
277
278    /// Get Insurance.
279    /// Query for Bybit insurance pool data (1 day delay).
280    #[tracing::instrument(skip(self), err)]
281    pub async fn get_insurance(
282        &self,
283        params: &GetInsuranceParams,
284    ) -> Result<Response<Insurance>, Error> {
285        let url = format!("{}{}", self.base_url, Path::MarketInsurance);
286
287        let request = self.client.request(Method::GET, url).query(params);
288
289        let response = send(request).await?;
290        Ok(response)
291    }
292
293    /// Get Risk Limit.
294    /// Query for the risk limit.
295    ///
296    /// Covers: linear / inverse
297    #[tracing::instrument(skip(self), err)]
298    pub async fn get_risk_limit(
299        &self,
300        params: &GetRiskLimitParams,
301    ) -> Result<Response<RiskLimit>, Error> {
302        let url = format!("{}{}", self.base_url, Path::MarketRiskLimit);
303
304        let request = self.client.request(Method::GET, url).query(params);
305
306        let response = send(request).await?;
307        Ok(response)
308    }
309
310    /// Get Delivery Price.
311    /// Get the delivery price for option and USDC futures contracts.
312    ///
313    /// Covers: linear / inverse / option
314    #[tracing::instrument(skip(self), err)]
315    pub async fn get_delivery_price(
316        &self,
317        params: &GetDeliveryPriceParams,
318    ) -> Result<Response<DeliveryPrice>, Error> {
319        let url = format!("{}{}", self.base_url, Path::MarketDeliveryPrice);
320
321        let request = self.client.request(Method::GET, url).query(params);
322
323        let response = send(request).await?;
324        Ok(response)
325    }
326}
327
328// Trade.
329impl Client {
330    /// Place Order
331    /// This endpoint supports to create the order for Spot, Margin trading, USDT perpetual, USDT futures, USDC perpetual, USDC futures, Inverse Futures and Options.
332    ///
333    /// INFO:
334    /// Supported order type (orderType):
335    /// Limit order: orderType=Limit, it is necessary to specify order qty and price.
336    ///
337    /// 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
338    /// Supported timeInForce strategy:
339    /// GTC
340    /// IOC
341    /// FOK
342    /// 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.
343    /// 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.
344    ///
345    /// How to create a conditional order:
346    /// 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.
347    ///
348    /// Take profit / Stop loss: You can set TP/SL while placing orders. Besides, you could modify the position's TP/SL.
349    ///
350    /// Order quantity: The quantity of perpetual contracts you are going to buy/sell. For the order quantity, Bybit only supports positive number at present.
351    ///
352    /// 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.
353    ///
354    /// 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.
355    ///
356    /// Open orders up limit:
357    /// Perps & Futures:
358    /// a) Each account can hold a maximum of 500 active orders simultaneously per symbol.
359    /// b) conditional orders: each account can hold a maximum of 10 active orders simultaneously per symbol.
360    /// 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
361    /// Option: a maximum of 50 open orders per account
362    ///
363    /// Rate limit:
364    /// 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
365    ///
366    /// Risk control limit notice:
367    /// 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.
368    ///
369    /// Reduce only orders:
370    /// If reduceOnly=true and order qty > max order qty, the order will automatically be split up into multiple orders.
371    ///
372    /// Spot Stop Order
373    /// Spot supports TP/SL order, Conditional order, however, the system logic is different between classic account and Unified account
374    /// 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
375    /// 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
376    #[tracing::instrument(skip(self), err)]
377    pub async fn place_order(
378        &self,
379        request: &PlaceOrderRequest,
380    ) -> Result<Response<PlaceOrderResponse>, Error> {
381        let url = format!("{}{}", self.base_url, Path::TradeOrderCreate);
382        let json = serialize_json(request)?;
383        let headers = self.get_signed_headers(&json);
384
385        let request = self
386            .client
387            .request(Method::POST, url)
388            .headers(headers)
389            .body(json);
390
391        let response = send(request).await?;
392        Ok(response)
393    }
394
395    /// Amend Order
396    /// info
397    /// You can only modify unfilled or partially filled orders.
398    #[tracing::instrument(skip(self), err)]
399    pub async fn amend_order(
400        &self,
401        request: &AmendOrderRequest,
402    ) -> Result<Response<AmendOrderResponse>, Error> {
403        let url = format!("{}{}", self.base_url, Path::TradeOrderAmend);
404        let json = serialize_json(request)?;
405        let headers = self.get_signed_headers(&json);
406
407        let request = self
408            .client
409            .request(Method::POST, url)
410            .headers(headers)
411            .body(json);
412
413        let response = send(request).await?;
414        Ok(response)
415    }
416
417    /// Cancel Order
418    /// important
419    /// You must specify orderId or orderLinkId to cancel the order.
420    /// If orderId and orderLinkId do not match, the system will process orderId first.
421    /// You can only cancel unfilled or partially filled orders.
422    #[tracing::instrument(skip(self), err)]
423    pub async fn cancel_order(
424        &self,
425        request: &CancelOrderRequest,
426    ) -> Result<Response<CancelOrderResponse>, Error> {
427        let url = format!("{}{}", self.base_url, Path::TradeOrderCancel);
428        let json = serialize_json(request)?;
429        let headers = self.get_signed_headers(&json);
430
431        let request = self
432            .client
433            .request(Method::POST, url)
434            .headers(headers)
435            .body(json);
436
437        let response = send(request).await?;
438        Ok(response)
439    }
440
441    /// Get Open & Closed Orders.
442    /// 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.
443    /// And to query older order records, please use the order history interface.
444    ///
445    /// Tip
446    /// UTA2.0 can query filled, canceled, and rejected orders to the most recent 500 orders for spot, linear, inverse and option categories
447    /// 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.
448    /// 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.
449    /// The records are sorted by the createdTime from newest to oldest.
450    ///
451    /// info
452    /// classic account spot can return open orders only
453    /// After a server release or restart, filled, canceled, and rejected orders of Unified account should only be queried through order history.
454    #[tracing::instrument(skip(self), err)]
455    pub async fn get_open_closed_orders(
456        &self,
457        params: &GetOpenClosedOrdersParams,
458    ) -> Result<Response<CursorPagination<Order>>, Error> {
459        let query = serialize_query(params)?;
460        let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderRealtime);
461        let headers = self.get_signed_headers(&query);
462
463        let request = self.client.request(Method::GET, url).headers(headers);
464
465        let response = send(request).await?;
466        Ok(response)
467    }
468
469    /// Collect all pages of open/closed orders into a single `Vec`.
470    /// Repeatedly calls [`get_open_closed_orders`] following `next_page_cursor`
471    /// until the last page is reached.
472    #[tracing::instrument(skip(self), err)]
473    pub async fn get_open_closed_orders_all(
474        &self,
475        params: &GetOpenClosedOrdersParams,
476    ) -> Result<Vec<Order>, Error> {
477        let mut all = Vec::new();
478        let mut p = params.clone();
479        loop {
480            let page = self.get_open_closed_orders(&p).await?;
481            all.extend(page.result.list);
482            match page.result.next_page_cursor {
483                Some(cursor) => p = p.with_cursor(cursor),
484                None => break,
485            }
486        }
487        Ok(all)
488    }
489
490    /// Cancel All Orders.
491    /// Cancel all open orders. Support linear, inverse, spot, and option.
492    #[tracing::instrument(skip(self), err)]
493    pub async fn cancel_all_orders(
494        &self,
495        request: &CancelAllOrdersRequest,
496    ) -> Result<Response<CancelAllOrdersResponse>, Error> {
497        let url = format!("{}{}", self.base_url, Path::TradeOrderCancelAll);
498        let json = serialize_json(request)?;
499        let headers = self.get_signed_headers(&json);
500
501        let request = self
502            .client
503            .request(Method::POST, url)
504            .headers(headers)
505            .body(json);
506
507        let response = send(request).await?;
508        Ok(response)
509    }
510
511    /// Get Order History.
512    /// Query order history. As order creation/cancellation is asynchronous, the data returned may be delayed.
513    /// Supports up to 2 years of data.
514    #[tracing::instrument(skip(self), err)]
515    pub async fn get_order_history(
516        &self,
517        params: &GetOrderHistoryParams,
518    ) -> Result<Response<CursorPagination<Order>>, Error> {
519        let query = serialize_query(params)?;
520        let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderHistory);
521        let headers = self.get_signed_headers(&query);
522
523        let request = self.client.request(Method::GET, url).headers(headers);
524
525        let response = send(request).await?;
526        Ok(response)
527    }
528
529    /// Collect all pages of order history into a single `Vec`.
530    #[tracing::instrument(skip(self), err)]
531    pub async fn get_order_history_all(
532        &self,
533        params: &GetOrderHistoryParams,
534    ) -> Result<Vec<Order>, Error> {
535        let mut all = Vec::new();
536        let mut p = params.clone();
537        loop {
538            let page = self.get_order_history(&p).await?;
539            all.extend(page.result.list);
540            match page.result.next_page_cursor {
541                Some(cursor) => p = p.with_cursor(cursor),
542                None => break,
543            }
544        }
545        Ok(all)
546    }
547
548    /// Place Batch Orders.
549    /// Supports up to 20 orders per request.
550    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
551    #[tracing::instrument(skip(self), err)]
552    pub async fn place_orders_batch(
553        &self,
554        request: &PlaceOrderBatchRequest,
555    ) -> Result<Response<List<PlaceOrderBatchResult>>, Error> {
556        let url = format!("{}{}", self.base_url, Path::TradeOrderCreateBatch);
557        let json = serialize_json(request)?;
558        let headers = self.get_signed_headers(&json);
559
560        let request = self
561            .client
562            .request(Method::POST, url)
563            .headers(headers)
564            .body(json);
565
566        let response = send(request).await?;
567        Ok(response)
568    }
569
570    /// Amend Batch Orders.
571    /// Supports up to 20 orders per request.
572    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
573    #[tracing::instrument(skip(self), err)]
574    pub async fn amend_orders_batch(
575        &self,
576        request: &AmendOrderBatchRequest,
577    ) -> Result<Response<List<AmendOrderBatchResult>>, Error> {
578        let url = format!("{}{}", self.base_url, Path::TradeOrderAmendBatch);
579        let json = serialize_json(request)?;
580        let headers = self.get_signed_headers(&json);
581
582        let request = self
583            .client
584            .request(Method::POST, url)
585            .headers(headers)
586            .body(json);
587
588        let response = send(request).await?;
589        Ok(response)
590    }
591
592    /// Cancel Batch Orders.
593    /// Supports up to 20 orders per request.
594    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
595    #[tracing::instrument(skip(self), err)]
596    pub async fn cancel_orders_batch(
597        &self,
598        request: &CancelOrderBatchRequest,
599    ) -> Result<Response<List<CancelOrderBatchResult>>, Error> {
600        let url = format!("{}{}", self.base_url, Path::TradeOrderCancelBatch);
601        let json = serialize_json(request)?;
602        let headers = self.get_signed_headers(&json);
603
604        let request = self
605            .client
606            .request(Method::POST, url)
607            .headers(headers)
608            .body(json);
609
610        let response = send(request).await?;
611        Ok(response)
612    }
613
614    /// Get Spot Borrow Check.
615    /// Query the maximum quantity for purchase or sale, and check the borrowable quantity based on the fee types.
616    ///
617    /// Covers: spot only. Requires authentication.
618    #[tracing::instrument(skip(self), err)]
619    pub async fn get_spot_borrow_check(
620        &self,
621        params: &GetSpotBorrowCheckParams,
622    ) -> Result<Response<SpotBorrowCheck>, Error> {
623        let url = format!("{}{}", self.base_url, Path::TradeOrderSpotBorrowCheck);
624        let query = serialize_query(params)?;
625        let headers = self.get_signed_headers(&query);
626
627        let request = self
628            .client
629            .request(Method::GET, url)
630            .headers(headers)
631            .query(params);
632
633        let response = send(request).await?;
634        Ok(response)
635    }
636}
637
638// Position.
639impl Client {
640    /// Query real-time position data, such as position size, cumulative realized PNL, etc.
641    /// Query real-time position data, such as position size, cumulative realized PNL, etc.
642    ///
643    /// INFO
644    // UTA2.0(inverse)
645    // You can query all open positions with /v5/position/list?category=inverse;
646    // Cannot query multiple symbols in one request
647    // UTA1.0(inverse) & Classic (inverse)
648    // You can query all open positions with /v5/position/list?category=inverse;
649    // symbol parameter can pass up to 10 symbols, e.g., symbol=BTCUSD,ETHUSD
650    #[tracing::instrument(skip(self), err)]
651    pub async fn get_position_info(
652        &self,
653        params: &GetPositionInfoParams,
654    ) -> Result<Response<CursorPagination<Position>>, Error> {
655        let query = serialize_query(params)?;
656        let url = format!("{}{}?{query}", self.base_url, Path::PositionList);
657        let headers = self.get_signed_headers(&query);
658
659        let request = self.client.request(Method::GET, url).headers(headers);
660
661        let response = send(request).await?;
662        Ok(response)
663    }
664
665    /// Collect all pages of position info into a single `Vec`.
666    /// Repeatedly calls [`get_position_info`] following `next_page_cursor`
667    /// until the last page is reached.
668    #[tracing::instrument(skip(self), err)]
669    pub async fn get_position_info_all(
670        &self,
671        params: &GetPositionInfoParams,
672    ) -> Result<Vec<Position>, Error> {
673        let mut all = Vec::new();
674        let mut p = params.clone();
675        loop {
676            let page = self.get_position_info(&p).await?;
677            all.extend(page.result.list);
678            match page.result.next_page_cursor {
679                Some(cursor) => p = p.with_cursor(cursor),
680                None => break,
681            }
682        }
683        Ok(all)
684    }
685
686    /// Set Leverage.
687    /// Set the leverage for a position. Only for isolated margin mode.
688    #[tracing::instrument(skip(self), err)]
689    pub async fn set_leverage(
690        &self,
691        request: &SetLeverageRequest,
692    ) -> Result<Response<EmptyResult>, Error> {
693        let url = format!("{}{}", self.base_url, Path::PositionSetLeverage);
694        let json = serialize_json(request)?;
695        let headers = self.get_signed_headers(&json);
696
697        let request = self
698            .client
699            .request(Method::POST, url)
700            .headers(headers)
701            .body(json);
702
703        let response = send(request).await?;
704        Ok(response)
705    }
706
707    /// Set Trading Stop.
708    /// Set take profit, stop loss, or trailing stop for a position.
709    #[tracing::instrument(skip(self), err)]
710    pub async fn set_trading_stop(
711        &self,
712        request: &SetTradingStopRequest,
713    ) -> Result<Response<EmptyResult>, Error> {
714        let url = format!("{}{}", self.base_url, Path::PositionTradingStop);
715        let json = serialize_json(request)?;
716        let headers = self.get_signed_headers(&json);
717
718        let request = self
719            .client
720            .request(Method::POST, url)
721            .headers(headers)
722            .body(json);
723
724        let response = send(request).await?;
725        Ok(response)
726    }
727
728    /// Switch Cross/Isolated Margin.
729    /// Switch the margin mode for a symbol between cross and isolated.
730    #[tracing::instrument(skip(self), err)]
731    pub async fn switch_cross_isolated_margin(
732        &self,
733        request: &SwitchCrossIsolatedMarginRequest,
734    ) -> Result<Response<EmptyResult>, Error> {
735        let url = format!("{}{}", self.base_url, Path::PositionSwitchIsolated);
736        let json = serialize_json(request)?;
737        let headers = self.get_signed_headers(&json);
738
739        let request = self
740            .client
741            .request(Method::POST, url)
742            .headers(headers)
743            .body(json);
744
745        let response = send(request).await?;
746        Ok(response)
747    }
748
749    /// Switch Position Mode.
750    /// Switch between one-way (merged single) and hedge (both sides) position mode.
751    #[tracing::instrument(skip(self), err)]
752    pub async fn switch_position_mode(
753        &self,
754        request: &SwitchPositionModeRequest,
755    ) -> Result<Response<EmptyResult>, Error> {
756        let url = format!("{}{}", self.base_url, Path::PositionSwitchMode);
757        let json = serialize_json(request)?;
758        let headers = self.get_signed_headers(&json);
759
760        let request = self
761            .client
762            .request(Method::POST, url)
763            .headers(headers)
764            .body(json);
765
766        let response = send(request).await?;
767        Ok(response)
768    }
769
770    /// Set Auto Add Margin.
771    /// Turn on/off auto-add-margin for an isolated margin position.
772    #[tracing::instrument(skip(self), err)]
773    pub async fn set_auto_add_margin(
774        &self,
775        request: &SetAutoAddMarginRequest,
776    ) -> Result<Response<EmptyResult>, Error> {
777        let url = format!("{}{}", self.base_url, Path::PositionSetAutoAddMargin);
778        let json = serialize_json(request)?;
779        let headers = self.get_signed_headers(&json);
780
781        let request = self
782            .client
783            .request(Method::POST, url)
784            .headers(headers)
785            .body(json);
786
787        let response = send(request).await?;
788        Ok(response)
789    }
790
791    /// Set Risk Limit.
792    /// Set the risk limit for a position. The response includes the new risk limit and its value.
793    #[tracing::instrument(skip(self), err)]
794    pub async fn set_risk_limit(
795        &self,
796        request: &SetRiskLimitRequest,
797    ) -> Result<Response<SetRiskLimitResponse>, Error> {
798        let url = format!("{}{}", self.base_url, Path::PositionSetRiskLimit);
799        let json = serialize_json(request)?;
800        let headers = self.get_signed_headers(&json);
801
802        let request = self
803            .client
804            .request(Method::POST, url)
805            .headers(headers)
806            .body(json);
807
808        let response = send(request).await?;
809        Ok(response)
810    }
811
812    /// Get Closed P&L.
813    /// Query the closed profit and loss records of positions.
814    #[tracing::instrument(skip(self), err)]
815    pub async fn get_closed_pnl(
816        &self,
817        params: &GetClosedPnlParams,
818    ) -> Result<Response<CursorPagination<ClosedPnl>>, Error> {
819        let query = serialize_query(params)?;
820        let url = format!("{}{}?{query}", self.base_url, Path::PositionClosedPnl);
821        let headers = self.get_signed_headers(&query);
822
823        let request = self.client.request(Method::GET, url).headers(headers);
824
825        let response = send(request).await?;
826        Ok(response)
827    }
828
829    /// Collect all pages of closed P&L into a single `Vec`.
830    #[tracing::instrument(skip(self), err)]
831    pub async fn get_closed_pnl_all(
832        &self,
833        params: &GetClosedPnlParams,
834    ) -> Result<Vec<ClosedPnl>, Error> {
835        let mut all = Vec::new();
836        let mut p = params.clone();
837        loop {
838            let page = self.get_closed_pnl(&p).await?;
839            all.extend(page.result.list);
840            match page.result.next_page_cursor {
841                Some(cursor) => p = p.with_cursor(cursor),
842                None => break,
843            }
844        }
845        Ok(all)
846    }
847
848    /// Get Execution List.
849    /// Query users' execution (trading) records, sorted by execTime descending.
850    #[tracing::instrument(skip(self), err)]
851    pub async fn get_execution_list(
852        &self,
853        params: &GetExecutionListParams,
854    ) -> Result<Response<CursorPagination<ExecutionEntry>>, Error> {
855        let query = serialize_query(params)?;
856        let url = format!("{}{}?{query}", self.base_url, Path::ExecutionList);
857        let headers = self.get_signed_headers(&query);
858
859        let request = self.client.request(Method::GET, url).headers(headers);
860
861        let response = send(request).await?;
862        Ok(response)
863    }
864
865    /// Collect all pages of execution list entries into a single `Vec`.
866    #[tracing::instrument(skip(self), err)]
867    pub async fn get_execution_list_all(
868        &self,
869        params: &GetExecutionListParams,
870    ) -> Result<Vec<ExecutionEntry>, Error> {
871        let mut all = Vec::new();
872        let mut p = params.clone();
873        loop {
874            let page = self.get_execution_list(&p).await?;
875            all.extend(page.result.list);
876            match page.result.next_page_cursor {
877                Some(cursor) => p = p.with_cursor(cursor),
878                None => break,
879            }
880        }
881        Ok(all)
882    }
883}
884
885// Account.
886impl Client {
887    /// Obtain wallet balance, query asset information of each currency. By default, currency information with assets or liabilities of 0 is not returned.
888    #[tracing::instrument(skip(self), err)]
889    pub async fn get_wallet_balance(
890        &self,
891        params: &GetWalletBalanceParams,
892    ) -> Result<Response<List<WalletBalance>>, Error> {
893        let query = serialize_query(params)?;
894        let url = format!("{}{}?{query}", self.base_url, Path::AccountWalletBalance);
895        let headers = self.get_signed_headers(&query);
896
897        let request = self.client.request(Method::GET, url).headers(headers);
898
899        let response = send(request).await?;
900        Ok(response)
901    }
902
903    /// Get Transaction Log
904    /// Query for transaction logs in your Unified account. It supports up to 2 years worth of data.
905    #[tracing::instrument(skip(self), err)]
906    pub async fn get_transaction_log(
907        &self,
908        params: &GetTransactionLogParams,
909    ) -> Result<Response<CursorPagination<TransactionLog>>, Error> {
910        let query = serialize_query(params)?;
911        let url = format!("{}{}?{query}", self.base_url, Path::AccountTransactionLog);
912        let headers = self.get_signed_headers(&query);
913
914        let request = self.client.request(Method::GET, url).headers(headers);
915
916        let response = send(request).await?;
917        Ok(response)
918    }
919
920    /// Collect all pages of transaction log entries into a single `Vec`.
921    /// Repeatedly calls [`get_transaction_log`] following `next_page_cursor`
922    /// until the last page is reached.
923    #[tracing::instrument(skip(self), err)]
924    pub async fn get_transaction_log_all(
925        &self,
926        params: &GetTransactionLogParams,
927    ) -> Result<Vec<TransactionLog>, Error> {
928        let mut all = Vec::new();
929        let mut p = params.clone();
930        loop {
931            let page = self.get_transaction_log(&p).await?;
932            all.extend(page.result.list);
933            match page.result.next_page_cursor {
934                Some(cursor) => p = p.with_cursor(cursor),
935                None => break,
936            }
937        }
938        Ok(all)
939    }
940
941    /// Query the account information, like margin mode, account mode, etc.
942    #[tracing::instrument(skip(self), err)]
943    pub async fn get_account_info(&self) -> Result<Response<AccountInfo>, Error> {
944        let url = format!("{}{}", self.base_url, Path::AccountInfo);
945        let query = "";
946        let headers = self.get_signed_headers(query);
947
948        let request = self.client.request(Method::GET, url).headers(headers);
949
950        let response = send(request).await?;
951        Ok(response)
952    }
953
954    /// Get Fee Rate.
955    /// Get the trading fee rate.
956    ///
957    /// Requires authentication.
958    #[tracing::instrument(skip(self), err)]
959    pub async fn get_fee_rate(
960        &self,
961        params: &GetFeeRateParams,
962    ) -> Result<Response<List<FeeRateEntry>>, Error> {
963        let url = format!("{}{}", self.base_url, Path::AccountFeeRate);
964        let query = serialize_query(params)?;
965        let headers = self.get_signed_headers(&query);
966
967        let request = self
968            .client
969            .request(Method::GET, url)
970            .headers(headers)
971            .query(params);
972
973        let response = send(request).await?;
974        Ok(response)
975    }
976
977    /// Set Margin Mode.
978    /// Switch between regular margin, isolated margin, and portfolio margin.
979    ///
980    /// Requires authentication.
981    #[tracing::instrument(skip(self), err)]
982    pub async fn set_margin_mode(
983        &self,
984        request_body: &SetMarginModeRequest,
985    ) -> Result<Response<SetMarginModeResponse>, Error> {
986        let url = format!("{}{}", self.base_url, Path::AccountSetMarginMode);
987        let body = serialize_json(request_body)?;
988        let headers = self.get_signed_headers(&body);
989
990        let request = self
991            .client
992            .request(Method::POST, url)
993            .headers(headers)
994            .body(body);
995
996        let response = send(request).await?;
997        Ok(response)
998    }
999}
1000
1001// User.
1002impl Client {
1003    /// Get API Key Information.
1004    /// 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.
1005    #[tracing::instrument(skip(self), err)]
1006    pub async fn get_api_key_information(&self) -> Result<Response<APIKeyInformation>, Error> {
1007        let url = format!("{}{}", self.base_url, Path::UserQueryApi);
1008        let query = "";
1009        let headers = self.get_signed_headers(query);
1010
1011        let request = self.client.request(Method::GET, url).headers(headers);
1012
1013        let response = send(request).await?;
1014        Ok(response)
1015    }
1016}
1017
1018async fn send<T>(request: RequestBuilder) -> Result<Response<T>, Error>
1019where
1020    T: serde::de::DeserializeOwned,
1021{
1022    let start = std::time::Instant::now();
1023    let response = request.send().await?;
1024    let elapsed_ms = start.elapsed().as_millis();
1025    let headers = parse_headers(response.headers());
1026    let json = response.text().await?;
1027    if !headers.is_ret_code_ok() {
1028        let msg: APIErrorResponse = deserialize_json(&json)?;
1029        tracing::debug!(elapsed_ms, ret_code = msg.ret_code, "api error response");
1030        return Err(msg.into());
1031    }
1032
1033    tracing::debug!(
1034        elapsed_ms,
1035        api_limit = headers.api_limit,
1036        api_limit_status = headers.api_limit_status,
1037        "api call completed"
1038    );
1039    let response: Resp<_> = deserialize_json(&json)?;
1040    let response = Response {
1041        result: response.result,
1042        time: response.time,
1043        headers,
1044        ret_ext_info: response.ret_ext_info,
1045    };
1046    Ok(response)
1047}
1048
1049/// Parse response headers: ret_code, traceid, timenow, X-Bapi-Limit, X-Bapi-Limit-Status, X-Bapi-Limit-Reset-Timestamp
1050fn parse_headers(headers: &HeaderMap) -> Headers {
1051    let ret_code = headers
1052        .get(HEADER_RET_CODE)
1053        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
1054    let trace_id = headers
1055        .get(HEADER_TRACE_ID)
1056        .and_then(|h| h.to_str().map(|str| str.into()).ok());
1057    let time_now = headers
1058        .get(HEADER_TIME_NOW)
1059        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
1060
1061    let api_limit = headers
1062        .get(HEADER_X_BAPI_LIMIT)
1063        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
1064    let api_limit_status = headers
1065        .get(HEADER_X_BAPI_LIMIT_STATUS)
1066        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
1067    let api_limit_reset_timestamp = headers
1068        .get(HEADER_X_BAPI_LIMIT_RESET_TIMESTAMP)
1069        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
1070
1071    Headers {
1072        ret_code,
1073        trace_id,
1074        time_now,
1075        api_limit,
1076        api_limit_status,
1077        api_limit_reset_timestamp,
1078    }
1079}