Skip to main content

brokapi_bybit_protocol/http/
endpoints.rs

1//! One method per REST endpoint the connectors use.
2//!
3//! Numbers and enumerations are the words of the API. Nothing here throttles, retries or interprets: it builds the
4//! request, signs it when the endpoint is private, and parses the reply.
5
6use rust_decimal::Decimal;
7
8use super::client::BybitHttpClient;
9use super::models::{
10    AmendOrderRequest, CancelAllResult, CancelOrderRequest, CreateOrderRequest, ExecutionRecord,
11    InstrumentInfo, Kline, KlineResult, OrderBookResult, OrderRecord, OrderRef, Page,
12    PositionRecord, PublicTradeRecord, ServerTime, Ticker, WalletRecord,
13};
14use crate::error::BybitApiError;
15
16impl BybitHttpClient {
17    /// `GET /v5/market/time`.
18    pub async fn get_server_time(&self) -> Result<ServerTime, BybitApiError> {
19        self.get_public("/v5/market/time", &[]).await
20    }
21
22    /// `GET /v5/market/instruments-info` for one symbol.
23    ///
24    /// # Errors
25    ///
26    /// [`BybitApiError::Api`] with code 10001 for a symbol the exchange does not know.
27    ///
28    /// # Examples
29    ///
30    /// ```
31    /// # #[tokio::main(flavor = "current_thread")]
32    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
33    /// # let server = brokapi_mocks::bybit::MockServer::start(Default::default()).await;
34    /// # let cfg = brokapi_bybit_protocol::config::BybitConfig::for_mock("mock-key", "mock-secret", &server.http_base);
35    /// use brokapi_bybit_protocol::http::client::BybitHttpClient;
36    ///
37    /// let client = BybitHttpClient::new(cfg)?;
38    /// let info = client.get_instrument("linear", "MOCKUSDT").await?;
39    /// assert!(info.price_filter.tick_size.is_sign_positive());
40    /// # Ok(())
41    /// # }
42    /// ```
43    pub async fn get_instrument(
44        &self,
45        category: &str,
46        symbol: &str,
47    ) -> Result<InstrumentInfo, BybitApiError> {
48        let page: Page<InstrumentInfo> = self
49            .get_public(
50                "/v5/market/instruments-info",
51                &[("category", category.into()), ("symbol", symbol.into())],
52            )
53            .await?;
54        page.list
55            .into_iter()
56            .next()
57            .ok_or_else(|| BybitApiError::Api {
58                code: 10001,
59                message: format!("params error: symbol invalid ({symbol} is not in the answer)"),
60            })
61    }
62
63    /// `GET /v5/market/instruments-info`, one page of a category.
64    pub async fn get_instruments_page(
65        &self,
66        category: &str,
67        limit: u32,
68        cursor: Option<&str>,
69    ) -> Result<Page<InstrumentInfo>, BybitApiError> {
70        let mut q = vec![
71            ("category", category.to_string()),
72            ("limit", limit.to_string()),
73        ];
74        if let Some(c) = cursor.filter(|c| !c.is_empty()) {
75            q.push(("cursor", c.to_string()));
76        }
77        self.get_public("/v5/market/instruments-info", &q).await
78    }
79
80    /// `GET /v5/market/orderbook`, `limit` levels per side (1 to 1000 for linear; the exchange default is 25).
81    pub async fn get_order_book(
82        &self,
83        category: &str,
84        symbol: &str,
85        limit: Option<u32>,
86    ) -> Result<OrderBookView, BybitApiError> {
87        let mut q = vec![
88            ("category", category.to_string()),
89            ("symbol", symbol.to_string()),
90        ];
91        if let Some(l) = limit {
92            q.push(("limit", l.to_string()));
93        }
94        let r: OrderBookResult = self.get_public("/v5/market/orderbook", &q).await?;
95        Ok(OrderBookView {
96            symbol: r.symbol,
97            bids: r.bids.iter().map(|l| (l.0, l.1)).collect(),
98            asks: r.asks.iter().map(|l| (l.0, l.1)).collect(),
99            ts_ms: r.ts,
100            update_id: r.update_id,
101        })
102    }
103
104    /// `GET /v5/market/tickers` for one symbol.
105    pub async fn get_ticker(&self, category: &str, symbol: &str) -> Result<Ticker, BybitApiError> {
106        let page: Page<Ticker> = self
107            .get_public(
108                "/v5/market/tickers",
109                &[("category", category.into()), ("symbol", symbol.into())],
110            )
111            .await?;
112        page.list
113            .into_iter()
114            .next()
115            .ok_or_else(|| BybitApiError::Api {
116                code: 10001,
117                message: format!("params error: symbol invalid ({symbol} is not in the answer)"),
118            })
119    }
120
121    /// `GET /v5/market/recent-trade`, the newest first. `limit` is 1 to 1000.
122    pub async fn get_recent_trades(
123        &self,
124        category: &str,
125        symbol: &str,
126        limit: u32,
127    ) -> Result<Vec<PublicTradeRecord>, BybitApiError> {
128        let page: Page<PublicTradeRecord> = self
129            .get_public(
130                "/v5/market/recent-trade",
131                &[
132                    ("category", category.into()),
133                    ("symbol", symbol.into()),
134                    ("limit", limit.to_string()),
135                ],
136            )
137            .await?;
138        Ok(page.list)
139    }
140
141    /// `GET /v5/market/kline`: candles from `start_ms` to `end_ms`, at most `limit` (1 to 1000), oldest first.
142    /// `interval` is one of `1 3 5 15 30 60 120 240 360 720 D W M`.
143    pub async fn get_klines(
144        &self,
145        category: &str,
146        symbol: &str,
147        interval: &str,
148        start_ms: Option<i64>,
149        end_ms: Option<i64>,
150        limit: u32,
151    ) -> Result<Vec<Kline>, BybitApiError> {
152        let mut q = vec![
153            ("category", category.to_string()),
154            ("symbol", symbol.to_string()),
155            ("interval", interval.to_string()),
156            ("limit", limit.to_string()),
157        ];
158        if let Some(s) = start_ms {
159            q.push(("start", s.to_string()));
160        }
161        if let Some(e) = end_ms {
162            q.push(("end", e.to_string()));
163        }
164        let r: KlineResult = self.get_public("/v5/market/kline", &q).await?;
165        let mut out: Vec<Kline> = r
166            .list
167            .iter()
168            .filter_map(|row| Kline::from_row(row))
169            .collect();
170        out.sort_by_key(|k| k.start_ms);
171        Ok(out)
172    }
173
174    /// `POST /v5/order/create`. A command: sent once.
175    pub async fn create_order(&self, body: &CreateOrderRequest) -> Result<OrderRef, BybitApiError> {
176        self.post_command("/v5/order/create", body).await
177    }
178
179    /// `POST /v5/order/amend`. A command: sent once.
180    pub async fn amend_order(&self, body: &AmendOrderRequest) -> Result<OrderRef, BybitApiError> {
181        self.post_command("/v5/order/amend", body).await
182    }
183
184    /// `POST /v5/order/cancel`. A command: sent once.
185    pub async fn cancel_order(&self, body: &CancelOrderRequest) -> Result<OrderRef, BybitApiError> {
186        self.post_command("/v5/order/cancel", body).await
187    }
188
189    /// `POST /v5/order/cancel-all` for the symbol, or for the whole settle coin when `symbol` is `None`.
190    pub async fn cancel_all(
191        &self,
192        category: &str,
193        symbol: Option<&str>,
194        settle_coin: &str,
195    ) -> Result<CancelAllResult, BybitApiError> {
196        let mut body = serde_json::json!({"category": category});
197        match symbol {
198            Some(s) => body["symbol"] = s.into(),
199            None => body["settleCoin"] = settle_coin.into(),
200        }
201        self.post_command("/v5/order/cancel-all", &body).await
202    }
203
204    /// `GET /v5/order/realtime`: one page of the working orders (conditional ones included), or the order with the
205    /// given `orderLinkId` or `orderId` whatever its status (the exchange keeps the 500 latest closed ones).
206    #[allow(clippy::too_many_arguments)]
207    pub async fn get_open_orders(
208        &self,
209        category: &str,
210        symbol: Option<&str>,
211        settle_coin: Option<&str>,
212        order_id: Option<&str>,
213        order_link_id: Option<&str>,
214        limit: u32,
215        cursor: Option<&str>,
216    ) -> Result<Page<OrderRecord>, BybitApiError> {
217        let q = order_query(
218            category,
219            symbol,
220            settle_coin,
221            order_id,
222            order_link_id,
223            limit,
224            cursor,
225        );
226        self.get_private("/v5/order/realtime", &q).await
227    }
228
229    /// `GET /v5/order/history`: the orders of the last 7 days (or of the given range), closed ones included.
230    #[allow(clippy::too_many_arguments)]
231    pub async fn get_order_history(
232        &self,
233        category: &str,
234        symbol: Option<&str>,
235        order_id: Option<&str>,
236        order_link_id: Option<&str>,
237        limit: u32,
238        cursor: Option<&str>,
239    ) -> Result<Page<OrderRecord>, BybitApiError> {
240        let q = order_query(
241            category,
242            symbol,
243            None,
244            order_id,
245            order_link_id,
246            limit,
247            cursor,
248        );
249        self.get_private("/v5/order/history", &q).await
250    }
251
252    /// `GET /v5/execution/list`: fills in `[start_ms, end_ms]`, at most 7 days apart.
253    #[allow(clippy::too_many_arguments)]
254    pub async fn get_executions(
255        &self,
256        category: &str,
257        symbol: Option<&str>,
258        start_ms: Option<i64>,
259        end_ms: Option<i64>,
260        limit: u32,
261        cursor: Option<&str>,
262    ) -> Result<Page<ExecutionRecord>, BybitApiError> {
263        let mut q = vec![
264            ("category", category.to_string()),
265            ("limit", limit.to_string()),
266        ];
267        if let Some(s) = symbol {
268            q.push(("symbol", s.to_string()));
269        }
270        if let Some(s) = start_ms {
271            q.push(("startTime", s.to_string()));
272        }
273        if let Some(e) = end_ms {
274            q.push(("endTime", e.to_string()));
275        }
276        if let Some(c) = cursor.filter(|c| !c.is_empty()) {
277            q.push(("cursor", c.to_string()));
278        }
279        self.get_private("/v5/execution/list", &q).await
280    }
281
282    /// `GET /v5/position/list` for the settle coin (every position of the category with a size above zero).
283    pub async fn get_positions(
284        &self,
285        category: &str,
286        settle_coin: &str,
287        limit: u32,
288        cursor: Option<&str>,
289    ) -> Result<Page<PositionRecord>, BybitApiError> {
290        let mut q = vec![
291            ("category", category.to_string()),
292            ("settleCoin", settle_coin.to_string()),
293            ("limit", limit.to_string()),
294        ];
295        if let Some(c) = cursor.filter(|c| !c.is_empty()) {
296            q.push(("cursor", c.to_string()));
297        }
298        self.get_private("/v5/position/list", &q).await
299    }
300
301    /// `GET /v5/account/wallet-balance` of the unified account.
302    pub async fn get_wallet_balance(&self) -> Result<Vec<WalletRecord>, BybitApiError> {
303        #[derive(serde::Deserialize)]
304        struct R {
305            #[serde(default)]
306            list: Vec<WalletRecord>,
307        }
308        let r: R = self
309            .get_private(
310                "/v5/account/wallet-balance",
311                &[("accountType", "UNIFIED".to_string())],
312            )
313            .await?;
314        Ok(r.list)
315    }
316
317    /// `POST /v5/account/demo-apply-money`: adds virtual money to a demo account (`coin`, `amount`). The exchange
318    /// allows one such request a minute, and only on the demo contour.
319    pub async fn demo_apply_money(
320        &self,
321        coin: &str,
322        amount: Decimal,
323    ) -> Result<serde_json::Value, BybitApiError> {
324        let body = serde_json::json!({
325            "adjustType": 0,
326            "utaDemoApplyMoney": [{"coin": coin, "amountStr": amount.to_string()}],
327        });
328        self.post_command("/v5/account/demo-apply-money", &body)
329            .await
330    }
331}
332
333/// An order book as plain tuples (price, size), best first.
334#[derive(Debug, Clone)]
335pub struct OrderBookView {
336    /// The symbol.
337    pub symbol: String,
338    /// Bids, best first.
339    pub bids: Vec<(Decimal, Decimal)>,
340    /// Asks, best first.
341    pub asks: Vec<(Decimal, Decimal)>,
342    /// The time the exchange made the data, milliseconds.
343    pub ts_ms: i64,
344    /// The update id.
345    pub update_id: i64,
346}
347
348fn order_query(
349    category: &str,
350    symbol: Option<&str>,
351    settle_coin: Option<&str>,
352    order_id: Option<&str>,
353    order_link_id: Option<&str>,
354    limit: u32,
355    cursor: Option<&str>,
356) -> Vec<(&'static str, String)> {
357    let mut q = vec![
358        ("category", category.to_string()),
359        ("limit", limit.to_string()),
360    ];
361    if let Some(s) = symbol {
362        q.push(("symbol", s.to_string()));
363    }
364    if let Some(s) = settle_coin {
365        q.push(("settleCoin", s.to_string()));
366    }
367    if let Some(s) = order_id {
368        q.push(("orderId", s.to_string()));
369    }
370    if let Some(s) = order_link_id {
371        q.push(("orderLinkId", s.to_string()));
372    }
373    if let Some(c) = cursor.filter(|c| !c.is_empty()) {
374        q.push(("cursor", c.to_string()));
375    }
376    q
377}