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