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, EmptyResult,
9        ExecutionEntry, GetClosedPnlParams, GetExecutionListParams, GetInstrumentsInfoParams,
10        GetKLinesParams, GetOpenClosedOrdersParams, GetOrderHistoryParams, GetPositionInfoParams,
11        GetTickersParams, GetTradesParams, GetTransactionLogParams, GetWalletBalanceParams,
12        Headers, InstrumentsInfo, KLine, List, Order, PlaceOrderBatchRequest,
13        PlaceOrderBatchResult, PlaceOrderRequest, PlaceOrderResponse, Position, Resp, Response,
14        ServerTime, SetAutoAddMarginRequest, SetLeverageRequest, SetRiskLimitRequest,
15        SetRiskLimitResponse, SetTradingStopRequest, SwitchCrossIsolatedMarginRequest,
16        SwitchPositionModeRequest, Ticker, Trade, TransactionLog, WalletBalance,
17    },
18    serde::{deserialize_json, serialize_json, serialize_query},
19    url::*,
20};
21use reqwest::{self, Method, RequestBuilder, header::HeaderMap};
22
23pub struct Config {
24    pub base_url: String,
25    pub api_key: Option<SensitiveString>,
26    pub api_secret: Option<SensitiveString>,
27    /// Milliseconds.
28    pub recv_window: Timestamp,
29    /// HTTP the header for broker users only.
30    pub referer: Option<String>,
31}
32
33// TODO: use proxy
34#[derive(Debug)]
35pub struct Client {
36    base_url: String,
37    headers: HeaderMap,
38    client: reqwest::Client,
39    signer: Option<Signer>,
40}
41
42impl Client {
43    pub fn new(cfg: Config) -> Result<Self, Error> {
44        let mut headers = HeaderMap::new();
45
46        if let Some(api_key) = cfg.api_key.as_ref() {
47            let api_key = api_key.expose().parse()?;
48            headers.append(HEADER_X_BAPI_API_KEY, api_key);
49        }
50
51        let recv_window = cfg.recv_window.to_string().parse()?;
52        headers.append(HEADER_X_BAPI_RECV_WINDOW, recv_window);
53
54        if let Some(referer) = cfg.referer {
55            let referer = referer.parse()?;
56            headers.append(HEADER_X_REFERER, referer);
57        }
58
59        let signer = cfg
60            .api_secret
61            .map(|api_secret| -> Result<Signer, Error> {
62                let api_key = cfg
63                    .api_key
64                    .ok_or_else(|| Error::from("api_key is required when api_secret is set"))?;
65                Ok(Signer::new(api_key, api_secret, cfg.recv_window, None))
66            })
67            .transpose()?;
68
69        Ok(Self {
70            base_url: cfg.base_url,
71            headers,
72            client: reqwest::Client::builder().build()?,
73            signer,
74        })
75    }
76
77    fn get_signed_headers(&self, s: &str) -> HeaderMap {
78        let mut headers = self.headers.clone();
79
80        let (signature, timestamp) = self.signer.as_ref().unwrap().sign(s);
81        let signature = signature.parse().unwrap();
82        headers.append(HEADER_X_BAPI_SIGN, signature);
83        let timestamp = timestamp.parse().unwrap();
84        headers.append(HEADER_X_BAPI_TIMESTAMP, timestamp);
85
86        headers
87    }
88}
89
90// Market.
91impl Client {
92    #[tracing::instrument(skip(self), err)]
93    pub async fn get_server_time(&self) -> Result<Response<ServerTime>, Error> {
94        let url = format!("{}{}", self.base_url, Path::MarketServerTime);
95
96        let request = self.client.request(Method::GET, url);
97
98        let response = send(request).await?;
99        Ok(response)
100    }
101
102    #[tracing::instrument(skip(self), err)]
103    pub async fn get_kline(&self, params: GetKLinesParams) -> Result<Response<KLine>, Error> {
104        let url = format!("{}{}", self.base_url, Path::MarketKline);
105
106        let request = self.client.request(Method::GET, url).query(&params);
107
108        let response = send(request).await?;
109        Ok(response)
110    }
111
112    /// Get Tickers
113    /// Query for the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours.
114    /// If category=option, symbol or baseCoin must be passed.
115    #[tracing::instrument(skip(self), err)]
116    pub async fn get_tickers(&self, params: GetTickersParams) -> Result<Response<Ticker>, Error> {
117        let url = format!("{}{}", self.base_url, Path::MarketTickers);
118
119        let request = self.client.request(Method::GET, url).query(&params);
120
121        let response = send(request).await?;
122        Ok(response)
123    }
124
125    #[tracing::instrument(skip(self), err)]
126    pub async fn get_instruments_info(
127        &self,
128        params: GetInstrumentsInfoParams,
129    ) -> Result<Response<InstrumentsInfo>, Error> {
130        let url = format!("{}{}", self.base_url, Path::MarketInstrumentsInfo);
131
132        let request = self.client.request(Method::GET, url).query(&params);
133
134        let response = send(request).await?;
135        Ok(response)
136    }
137
138    #[tracing::instrument(skip(self), err)]
139    pub async fn get_public_recent_trading_history(
140        &self,
141        params: GetTradesParams,
142    ) -> Result<Response<Trade>, Error> {
143        let url = format!("{}{}", self.base_url, Path::MarketRecentTrade);
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
152// Trade.
153impl Client {
154    /// Place Order
155    /// This endpoint supports to create the order for Spot, Margin trading, USDT perpetual, USDT futures, USDC perpetual, USDC futures, Inverse Futures and Options.
156    ///
157    /// INFO:
158    /// Supported order type (orderType):
159    /// Limit order: orderType=Limit, it is necessary to specify order qty and price.
160    ///
161    /// 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
162    /// Supported timeInForce strategy:
163    /// GTC
164    /// IOC
165    /// FOK
166    /// 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.
167    /// 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.
168    ///
169    /// How to create a conditional order:
170    /// 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.
171    ///
172    /// Take profit / Stop loss: You can set TP/SL while placing orders. Besides, you could modify the position's TP/SL.
173    ///
174    /// Order quantity: The quantity of perpetual contracts you are going to buy/sell. For the order quantity, Bybit only supports positive number at present.
175    ///
176    /// 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.
177    ///
178    /// 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.
179    ///
180    /// Open orders up limit:
181    /// Perps & Futures:
182    /// a) Each account can hold a maximum of 500 active orders simultaneously per symbol.
183    /// b) conditional orders: each account can hold a maximum of 10 active orders simultaneously per symbol.
184    /// 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
185    /// Option: a maximum of 50 open orders per account
186    ///
187    /// Rate limit:
188    /// 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
189    ///
190    /// Risk control limit notice:
191    /// 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.
192    ///
193    /// Reduce only orders:
194    /// If reduceOnly=true and order qty > max order qty, the order will automatically be split up into multiple orders.
195    ///
196    /// Spot Stop Order
197    /// Spot supports TP/SL order, Conditional order, however, the system logic is different between classic account and Unified account
198    /// 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
199    /// 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
200    #[tracing::instrument(skip(self), err)]
201    pub async fn place_order(
202        &self,
203        request: PlaceOrderRequest,
204    ) -> Result<Response<PlaceOrderResponse>, Error> {
205        let url = format!("{}{}", self.base_url, Path::TradeOrderCreate);
206        let json = serialize_json(&request)?;
207        let headers = self.get_signed_headers(&json);
208
209        let request = self
210            .client
211            .request(Method::POST, url)
212            .headers(headers)
213            .body(json);
214
215        let response = send(request).await?;
216        Ok(response)
217    }
218
219    /// Amend Order
220    /// info
221    /// You can only modify unfilled or partially filled orders.
222    #[tracing::instrument(skip(self), err)]
223    pub async fn amend_order(
224        &self,
225        request: AmendOrderRequest,
226    ) -> Result<Response<AmendOrderResponse>, Error> {
227        let url = format!("{}{}", self.base_url, Path::TradeOrderAmend);
228        let json = serialize_json(&request)?;
229        let headers = self.get_signed_headers(&json);
230
231        let request = self
232            .client
233            .request(Method::POST, url)
234            .headers(headers)
235            .body(json);
236
237        let response = send(request).await?;
238        Ok(response)
239    }
240
241    /// Cancel Order
242    /// important
243    /// You must specify orderId or orderLinkId to cancel the order.
244    /// If orderId and orderLinkId do not match, the system will process orderId first.
245    /// You can only cancel unfilled or partially filled orders.
246    #[tracing::instrument(skip(self), err)]
247    pub async fn cancel_order(
248        &self,
249        request: CancelOrderRequest,
250    ) -> Result<Response<CancelOrderResponse>, Error> {
251        let url = format!("{}{}", self.base_url, Path::TradeOrderCancel);
252        let json = serialize_json(&request)?;
253        let headers = self.get_signed_headers(&json);
254
255        let request = self
256            .client
257            .request(Method::POST, url)
258            .headers(headers)
259            .body(json);
260
261        let response = send(request).await?;
262        Ok(response)
263    }
264
265    /// Get Open & Closed Orders.
266    /// 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.
267    /// And to query older order records, please use the order history interface.
268    ///
269    /// Tip
270    /// UTA2.0 can query filled, canceled, and rejected orders to the most recent 500 orders for spot, linear, inverse and option categories
271    /// 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.
272    /// 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.
273    /// The records are sorted by the createdTime from newest to oldest.
274    ///
275    /// info
276    /// classic account spot can return open orders only
277    /// After a server release or restart, filled, canceled, and rejected orders of Unified account should only be queried through order history.
278    #[tracing::instrument(skip(self), err)]
279    pub async fn get_open_closed_orders(
280        &self,
281        params: GetOpenClosedOrdersParams,
282    ) -> Result<Response<CursorPagination<Order>>, Error> {
283        let query = serialize_query(&params)?;
284        let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderRealtime);
285        let headers = self.get_signed_headers(&query);
286
287        let request = self.client.request(Method::GET, url).headers(headers);
288
289        let response = send(request).await?;
290        Ok(response)
291    }
292
293    /// Collect all pages of open/closed orders into a single `Vec`.
294    /// Repeatedly calls [`get_open_closed_orders`] following `next_page_cursor`
295    /// until the last page is reached.
296    #[tracing::instrument(skip(self), err)]
297    pub async fn get_open_closed_orders_all(
298        &self,
299        params: GetOpenClosedOrdersParams,
300    ) -> Result<Vec<Order>, Error> {
301        let mut all = Vec::new();
302        let mut p = params;
303        loop {
304            let page = self.get_open_closed_orders(p.clone()).await?;
305            all.extend(page.result.list);
306            match page.result.next_page_cursor {
307                Some(cursor) => p = p.with_cursor(cursor),
308                None => break,
309            }
310        }
311        Ok(all)
312    }
313
314    /// Cancel All Orders.
315    /// Cancel all open orders. Support linear, inverse, spot, and option.
316    #[tracing::instrument(skip(self), err)]
317    pub async fn cancel_all_orders(
318        &self,
319        request: CancelAllOrdersRequest,
320    ) -> Result<Response<CancelAllOrdersResponse>, Error> {
321        let url = format!("{}{}", self.base_url, Path::TradeOrderCancelAll);
322        let json = serialize_json(&request)?;
323        let headers = self.get_signed_headers(&json);
324
325        let request = self
326            .client
327            .request(Method::POST, url)
328            .headers(headers)
329            .body(json);
330
331        let response = send(request).await?;
332        Ok(response)
333    }
334
335    /// Get Order History.
336    /// Query order history. As order creation/cancellation is asynchronous, the data returned may be delayed.
337    /// Supports up to 2 years of data.
338    #[tracing::instrument(skip(self), err)]
339    pub async fn get_order_history(
340        &self,
341        params: GetOrderHistoryParams,
342    ) -> Result<Response<CursorPagination<Order>>, Error> {
343        let query = serialize_query(&params)?;
344        let url = format!("{}{}?{query}", self.base_url, Path::TradeOrderHistory);
345        let headers = self.get_signed_headers(&query);
346
347        let request = self.client.request(Method::GET, url).headers(headers);
348
349        let response = send(request).await?;
350        Ok(response)
351    }
352
353    /// Collect all pages of order history into a single `Vec`.
354    #[tracing::instrument(skip(self), err)]
355    pub async fn get_order_history_all(
356        &self,
357        params: GetOrderHistoryParams,
358    ) -> Result<Vec<Order>, Error> {
359        let mut all = Vec::new();
360        let mut p = params;
361        loop {
362            let page = self.get_order_history(p.clone()).await?;
363            all.extend(page.result.list);
364            match page.result.next_page_cursor {
365                Some(cursor) => p = p.with_cursor(cursor),
366                None => break,
367            }
368        }
369        Ok(all)
370    }
371
372    /// Place Batch Orders.
373    /// Supports up to 20 orders per request.
374    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
375    #[tracing::instrument(skip(self), err)]
376    pub async fn place_orders_batch(
377        &self,
378        request: PlaceOrderBatchRequest,
379    ) -> Result<Response<List<PlaceOrderBatchResult>>, Error> {
380        let url = format!("{}{}", self.base_url, Path::TradeOrderCreateBatch);
381        let json = serialize_json(&request)?;
382        let headers = self.get_signed_headers(&json);
383
384        let request = self
385            .client
386            .request(Method::POST, url)
387            .headers(headers)
388            .body(json);
389
390        let response = send(request).await?;
391        Ok(response)
392    }
393
394    /// Amend Batch Orders.
395    /// Supports up to 20 orders per request.
396    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
397    #[tracing::instrument(skip(self), err)]
398    pub async fn amend_orders_batch(
399        &self,
400        request: AmendOrderBatchRequest,
401    ) -> Result<Response<List<AmendOrderBatchResult>>, Error> {
402        let url = format!("{}{}", self.base_url, Path::TradeOrderAmendBatch);
403        let json = serialize_json(&request)?;
404        let headers = self.get_signed_headers(&json);
405
406        let request = self
407            .client
408            .request(Method::POST, url)
409            .headers(headers)
410            .body(json);
411
412        let response = send(request).await?;
413        Ok(response)
414    }
415
416    /// Cancel Batch Orders.
417    /// Supports up to 20 orders per request.
418    /// Per-item results are in `response.ret_ext_info.list` (parallel to `response.result.list`).
419    #[tracing::instrument(skip(self), err)]
420    pub async fn cancel_orders_batch(
421        &self,
422        request: CancelOrderBatchRequest,
423    ) -> Result<Response<List<CancelOrderBatchResult>>, Error> {
424        let url = format!("{}{}", self.base_url, Path::TradeOrderCancelBatch);
425        let json = serialize_json(&request)?;
426        let headers = self.get_signed_headers(&json);
427
428        let request = self
429            .client
430            .request(Method::POST, url)
431            .headers(headers)
432            .body(json);
433
434        let response = send(request).await?;
435        Ok(response)
436    }
437}
438
439// Position.
440impl Client {
441    /// Query real-time position data, such as position size, cumulative realized PNL, etc.
442    /// Query real-time position data, such as position size, cumulative realized PNL, etc.
443    ///
444    /// INFO
445    // UTA2.0(inverse)
446    // You can query all open positions with /v5/position/list?category=inverse;
447    // Cannot query multiple symbols in one request
448    // UTA1.0(inverse) & Classic (inverse)
449    // You can query all open positions with /v5/position/list?category=inverse;
450    // symbol parameter can pass up to 10 symbols, e.g., symbol=BTCUSD,ETHUSD
451    #[tracing::instrument(skip(self), err)]
452    pub async fn get_position_info(
453        &self,
454        params: GetPositionInfoParams,
455    ) -> Result<Response<CursorPagination<Position>>, Error> {
456        let query = serialize_query(&params)?;
457        let url = format!("{}{}?{query}", self.base_url, Path::PositionList);
458        let headers = self.get_signed_headers(&query);
459
460        let request = self.client.request(Method::GET, url).headers(headers);
461
462        let response = send(request).await?;
463        Ok(response)
464    }
465
466    /// Collect all pages of position info into a single `Vec`.
467    /// Repeatedly calls [`get_position_info`] following `next_page_cursor`
468    /// until the last page is reached.
469    #[tracing::instrument(skip(self), err)]
470    pub async fn get_position_info_all(
471        &self,
472        params: GetPositionInfoParams,
473    ) -> Result<Vec<Position>, Error> {
474        let mut all = Vec::new();
475        let mut p = params;
476        loop {
477            let page = self.get_position_info(p.clone()).await?;
478            all.extend(page.result.list);
479            match page.result.next_page_cursor {
480                Some(cursor) => p = p.with_cursor(cursor),
481                None => break,
482            }
483        }
484        Ok(all)
485    }
486
487    /// Set Leverage.
488    /// Set the leverage for a position. Only for isolated margin mode.
489    #[tracing::instrument(skip(self), err)]
490    pub async fn set_leverage(
491        &self,
492        request: SetLeverageRequest,
493    ) -> Result<Response<EmptyResult>, Error> {
494        let url = format!("{}{}", self.base_url, Path::PositionSetLeverage);
495        let json = serialize_json(&request)?;
496        let headers = self.get_signed_headers(&json);
497
498        let request = self
499            .client
500            .request(Method::POST, url)
501            .headers(headers)
502            .body(json);
503
504        let response = send(request).await?;
505        Ok(response)
506    }
507
508    /// Set Trading Stop.
509    /// Set take profit, stop loss, or trailing stop for a position.
510    #[tracing::instrument(skip(self), err)]
511    pub async fn set_trading_stop(
512        &self,
513        request: SetTradingStopRequest,
514    ) -> Result<Response<EmptyResult>, Error> {
515        let url = format!("{}{}", self.base_url, Path::PositionTradingStop);
516        let json = serialize_json(&request)?;
517        let headers = self.get_signed_headers(&json);
518
519        let request = self
520            .client
521            .request(Method::POST, url)
522            .headers(headers)
523            .body(json);
524
525        let response = send(request).await?;
526        Ok(response)
527    }
528
529    /// Switch Cross/Isolated Margin.
530    /// Switch the margin mode for a symbol between cross and isolated.
531    #[tracing::instrument(skip(self), err)]
532    pub async fn switch_cross_isolated_margin(
533        &self,
534        request: SwitchCrossIsolatedMarginRequest,
535    ) -> Result<Response<EmptyResult>, Error> {
536        let url = format!("{}{}", self.base_url, Path::PositionSwitchIsolated);
537        let json = serialize_json(&request)?;
538        let headers = self.get_signed_headers(&json);
539
540        let request = self
541            .client
542            .request(Method::POST, url)
543            .headers(headers)
544            .body(json);
545
546        let response = send(request).await?;
547        Ok(response)
548    }
549
550    /// Switch Position Mode.
551    /// Switch between one-way (merged single) and hedge (both sides) position mode.
552    #[tracing::instrument(skip(self), err)]
553    pub async fn switch_position_mode(
554        &self,
555        request: SwitchPositionModeRequest,
556    ) -> Result<Response<EmptyResult>, Error> {
557        let url = format!("{}{}", self.base_url, Path::PositionSwitchMode);
558        let json = serialize_json(&request)?;
559        let headers = self.get_signed_headers(&json);
560
561        let request = self
562            .client
563            .request(Method::POST, url)
564            .headers(headers)
565            .body(json);
566
567        let response = send(request).await?;
568        Ok(response)
569    }
570
571    /// Set Auto Add Margin.
572    /// Turn on/off auto-add-margin for an isolated margin position.
573    #[tracing::instrument(skip(self), err)]
574    pub async fn set_auto_add_margin(
575        &self,
576        request: SetAutoAddMarginRequest,
577    ) -> Result<Response<EmptyResult>, Error> {
578        let url = format!("{}{}", self.base_url, Path::PositionSetAutoAddMargin);
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    /// Set Risk Limit.
593    /// Set the risk limit for a position. The response includes the new risk limit and its value.
594    #[tracing::instrument(skip(self), err)]
595    pub async fn set_risk_limit(
596        &self,
597        request: SetRiskLimitRequest,
598    ) -> Result<Response<SetRiskLimitResponse>, Error> {
599        let url = format!("{}{}", self.base_url, Path::PositionSetRiskLimit);
600        let json = serialize_json(&request)?;
601        let headers = self.get_signed_headers(&json);
602
603        let request = self
604            .client
605            .request(Method::POST, url)
606            .headers(headers)
607            .body(json);
608
609        let response = send(request).await?;
610        Ok(response)
611    }
612
613    /// Get Closed P&L.
614    /// Query the closed profit and loss records of positions.
615    #[tracing::instrument(skip(self), err)]
616    pub async fn get_closed_pnl(
617        &self,
618        params: GetClosedPnlParams,
619    ) -> Result<Response<CursorPagination<ClosedPnl>>, Error> {
620        let query = serialize_query(&params)?;
621        let url = format!("{}{}?{query}", self.base_url, Path::PositionClosedPnl);
622        let headers = self.get_signed_headers(&query);
623
624        let request = self.client.request(Method::GET, url).headers(headers);
625
626        let response = send(request).await?;
627        Ok(response)
628    }
629
630    /// Collect all pages of closed P&L into a single `Vec`.
631    #[tracing::instrument(skip(self), err)]
632    pub async fn get_closed_pnl_all(
633        &self,
634        params: GetClosedPnlParams,
635    ) -> Result<Vec<ClosedPnl>, Error> {
636        let mut all = Vec::new();
637        let mut p = params;
638        loop {
639            let page = self.get_closed_pnl(p.clone()).await?;
640            all.extend(page.result.list);
641            match page.result.next_page_cursor {
642                Some(cursor) => p = p.with_cursor(cursor),
643                None => break,
644            }
645        }
646        Ok(all)
647    }
648
649    /// Get Execution List.
650    /// Query users' execution (trading) records, sorted by execTime descending.
651    #[tracing::instrument(skip(self), err)]
652    pub async fn get_execution_list(
653        &self,
654        params: GetExecutionListParams,
655    ) -> Result<Response<CursorPagination<ExecutionEntry>>, Error> {
656        let query = serialize_query(&params)?;
657        let url = format!("{}{}?{query}", self.base_url, Path::ExecutionList);
658        let headers = self.get_signed_headers(&query);
659
660        let request = self.client.request(Method::GET, url).headers(headers);
661
662        let response = send(request).await?;
663        Ok(response)
664    }
665
666    /// Collect all pages of execution list entries into a single `Vec`.
667    #[tracing::instrument(skip(self), err)]
668    pub async fn get_execution_list_all(
669        &self,
670        params: GetExecutionListParams,
671    ) -> Result<Vec<ExecutionEntry>, Error> {
672        let mut all = Vec::new();
673        let mut p = params;
674        loop {
675            let page = self.get_execution_list(p.clone()).await?;
676            all.extend(page.result.list);
677            match page.result.next_page_cursor {
678                Some(cursor) => p = p.with_cursor(cursor),
679                None => break,
680            }
681        }
682        Ok(all)
683    }
684}
685
686// Account.
687impl Client {
688    /// Obtain wallet balance, query asset information of each currency. By default, currency information with assets or liabilities of 0 is not returned.
689    #[tracing::instrument(skip(self), err)]
690    pub async fn get_wallet_balance(
691        &self,
692        params: GetWalletBalanceParams,
693    ) -> Result<Response<List<WalletBalance>>, Error> {
694        let query = serialize_query(&params)?;
695        let url = format!("{}{}?{query}", self.base_url, Path::AccountWalletBalance);
696        let headers = self.get_signed_headers(&query);
697
698        let request = self.client.request(Method::GET, url).headers(headers);
699
700        let response = send(request).await?;
701        Ok(response)
702    }
703
704    /// Get Transaction Log
705    /// Query for transaction logs in your Unified account. It supports up to 2 years worth of data.
706    #[tracing::instrument(skip(self), err)]
707    pub async fn get_transaction_log(
708        &self,
709        params: GetTransactionLogParams,
710    ) -> Result<Response<CursorPagination<TransactionLog>>, Error> {
711        let query = serialize_query(&params)?;
712        let url = format!("{}{}?{query}", self.base_url, Path::AccountTransactionLog);
713        let headers = self.get_signed_headers(&query);
714
715        let request = self.client.request(Method::GET, url).headers(headers);
716
717        let response = send(request).await?;
718        Ok(response)
719    }
720
721    /// Collect all pages of transaction log entries into a single `Vec`.
722    /// Repeatedly calls [`get_transaction_log`] following `next_page_cursor`
723    /// until the last page is reached.
724    #[tracing::instrument(skip(self), err)]
725    pub async fn get_transaction_log_all(
726        &self,
727        params: GetTransactionLogParams,
728    ) -> Result<Vec<TransactionLog>, Error> {
729        let mut all = Vec::new();
730        let mut p = params;
731        loop {
732            let page = self.get_transaction_log(p.clone()).await?;
733            all.extend(page.result.list);
734            match page.result.next_page_cursor {
735                Some(cursor) => p = p.with_cursor(cursor),
736                None => break,
737            }
738        }
739        Ok(all)
740    }
741
742    /// Query the account information, like margin mode, account mode, etc.
743    #[tracing::instrument(skip(self), err)]
744    pub async fn get_account_info(&self) -> Result<Response<AccountInfo>, Error> {
745        let url = format!("{}{}", self.base_url, Path::AccountInfo);
746        let query = "";
747        let headers = self.get_signed_headers(query);
748
749        let request = self.client.request(Method::GET, url).headers(headers);
750
751        let response = send(request).await?;
752        Ok(response)
753    }
754}
755
756// User.
757impl Client {
758    /// Get API Key Information.
759    /// 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.
760    #[tracing::instrument(skip(self), err)]
761    pub async fn get_api_key_information(&self) -> Result<Response<APIKeyInformation>, Error> {
762        let url = format!("{}{}", self.base_url, Path::UserQueryApi);
763        let query = "";
764        let headers = self.get_signed_headers(query);
765
766        let request = self.client.request(Method::GET, url).headers(headers);
767
768        let response = send(request).await?;
769        Ok(response)
770    }
771}
772
773async fn send<T>(request: RequestBuilder) -> Result<Response<T>, Error>
774where
775    T: serde::de::DeserializeOwned,
776{
777    let start = std::time::Instant::now();
778    let response = request.send().await?;
779    let elapsed_ms = start.elapsed().as_millis();
780    let headers = parse_headers(response.headers());
781    let json = response.text().await?;
782    if !headers.is_ret_code_ok() {
783        let msg: APIErrorResponse = deserialize_json(&json)?;
784        tracing::debug!(elapsed_ms, ret_code = msg.ret_code, "api error response");
785        return Err(msg.into());
786    }
787
788    tracing::debug!(
789        elapsed_ms,
790        api_limit = headers.api_limit,
791        api_limit_status = headers.api_limit_status,
792        "api call completed"
793    );
794    let response: Resp<_> = deserialize_json(&json)?;
795    let response = Response {
796        result: response.result,
797        time: response.time,
798        headers,
799        ret_ext_info: response.ret_ext_info,
800    };
801    Ok(response)
802}
803
804/// Parse response headers: ret_code, traceid, timenow, X-Bapi-Limit, X-Bapi-Limit-Status, X-Bapi-Limit-Reset-Timestamp
805fn parse_headers(headers: &HeaderMap) -> Headers {
806    let ret_code = headers
807        .get(HEADER_RET_CODE)
808        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
809    let trace_id = headers
810        .get(HEADER_TRACE_ID)
811        .and_then(|h| h.to_str().map(|str| str.into()).ok());
812    let time_now = headers
813        .get(HEADER_TIME_NOW)
814        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
815
816    let api_limit = headers
817        .get(HEADER_X_BAPI_LIMIT)
818        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
819    let api_limit_status = headers
820        .get(HEADER_X_BAPI_LIMIT_STATUS)
821        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
822    let api_limit_reset_timestamp = headers
823        .get(HEADER_X_BAPI_LIMIT_RESET_TIMESTAMP)
824        .and_then(|h| h.to_str().unwrap_or_default().parse().ok());
825
826    Headers {
827        ret_code,
828        trace_id,
829        time_now,
830        api_limit,
831        api_limit_status,
832        api_limit_reset_timestamp,
833    }
834}