1use std::{
21 collections::HashMap,
22 fmt::Debug,
23 num::NonZeroU32,
24 sync::{Arc, LazyLock, Mutex},
25};
26
27use nautilus_core::{
28 consts::NAUTILUS_USER_AGENT, nanos::UnixNanos, time::get_atomic_clock_realtime,
29};
30use nautilus_model::{
31 data::{Bar, BarType, TradeTick},
32 enums::{BarAggregation, OrderSide, OrderType, TimeInForce},
33 events::account::state::AccountState,
34 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
35 instruments::{Instrument, InstrumentAny},
36 reports::{FillReport, OrderStatusReport, PositionStatusReport},
37 types::{Price, Quantity},
38};
39use nautilus_network::{
40 http::HttpClient,
41 ratelimiter::quota::Quota,
42 retry::{RetryConfig, RetryManager},
43};
44use reqwest::{Method, header::USER_AGENT};
45use serde::{Serialize, de::DeserializeOwned};
46use tokio_util::sync::CancellationToken;
47use ustr::Ustr;
48
49use super::{
50 error::BybitHttpError,
51 models::{
52 BybitFeeRate, BybitFeeRateResponse, BybitInstrumentInverseResponse,
53 BybitInstrumentLinearResponse, BybitInstrumentOptionResponse, BybitInstrumentSpotResponse,
54 BybitKlinesResponse, BybitOpenOrdersResponse, BybitOrderHistoryResponse,
55 BybitPlaceOrderResponse, BybitPositionListResponse, BybitServerTimeResponse,
56 BybitTradeHistoryResponse, BybitTradesResponse, BybitWalletBalanceResponse,
57 },
58 query::{
59 BybitAmendOrderParamsBuilder, BybitBatchAmendOrderEntryBuilder,
60 BybitBatchCancelOrderEntryBuilder, BybitBatchPlaceOrderEntryBuilder,
61 BybitCancelAllOrdersParamsBuilder, BybitCancelOrderParamsBuilder, BybitFeeRateParams,
62 BybitInstrumentsInfoParams, BybitKlinesParams, BybitKlinesParamsBuilder,
63 BybitOpenOrdersParamsBuilder, BybitOrderHistoryParamsBuilder, BybitPlaceOrderParamsBuilder,
64 BybitPositionListParams, BybitTickersParams, BybitTradeHistoryParams, BybitTradesParams,
65 BybitTradesParamsBuilder, BybitWalletBalanceParams,
66 },
67};
68use crate::{
69 common::{
70 consts::BYBIT_NAUTILUS_BROKER_ID,
71 credential::Credential,
72 enums::{
73 BybitAccountType, BybitEnvironment, BybitKlineInterval, BybitOrderSide, BybitOrderType,
74 BybitProductType, BybitTimeInForce,
75 },
76 models::BybitResponse,
77 parse::{
78 parse_account_state, parse_fill_report, parse_inverse_instrument, parse_kline_bar,
79 parse_linear_instrument, parse_option_instrument, parse_order_status_report,
80 parse_position_status_report, parse_spot_instrument, parse_trade_tick,
81 },
82 symbol::BybitSymbol,
83 urls::bybit_http_base_url,
84 },
85 http::query::BybitFeeRateParamsBuilder,
86};
87
88const DEFAULT_RECV_WINDOW_MS: u64 = 5_000;
89
90pub static BYBIT_REST_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
95 Quota::per_second(NonZeroU32::new(10).expect("Should be a valid non-zero u32"))
96});
97
98const BYBIT_GLOBAL_RATE_KEY: &str = "bybit:global";
99
100pub struct BybitHttpInnerClient {
102 base_url: String,
103 client: HttpClient,
104 credential: Option<Credential>,
105 recv_window_ms: u64,
106 retry_manager: RetryManager<BybitHttpError>,
107 cancellation_token: CancellationToken,
108 instruments_cache: Arc<Mutex<HashMap<Ustr, InstrumentAny>>>,
109}
110
111impl Default for BybitHttpInnerClient {
112 fn default() -> Self {
113 Self::new(None, Some(60), None, None, None)
114 .expect("Failed to create default BybitHttpInnerClient")
115 }
116}
117
118impl Debug for BybitHttpInnerClient {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.debug_struct("BybitHttpInnerClient")
121 .field("base_url", &self.base_url)
122 .field("has_credentials", &self.credential.is_some())
123 .field("recv_window_ms", &self.recv_window_ms)
124 .finish()
125 }
126}
127
128impl BybitHttpInnerClient {
129 pub fn cancel_all_requests(&self) {
131 self.cancellation_token.cancel();
132 }
133
134 pub fn cancellation_token(&self) -> &CancellationToken {
136 &self.cancellation_token
137 }
138
139 #[allow(clippy::too_many_arguments)]
145 pub fn new(
146 base_url: Option<String>,
147 timeout_secs: Option<u64>,
148 max_retries: Option<u32>,
149 retry_delay_ms: Option<u64>,
150 retry_delay_max_ms: Option<u64>,
151 ) -> Result<Self, BybitHttpError> {
152 let retry_config = RetryConfig {
153 max_retries: max_retries.unwrap_or(3),
154 initial_delay_ms: retry_delay_ms.unwrap_or(1000),
155 max_delay_ms: retry_delay_max_ms.unwrap_or(10_000),
156 backoff_factor: 2.0,
157 jitter_ms: 1000,
158 operation_timeout_ms: Some(60_000),
159 immediate_first: false,
160 max_elapsed_ms: Some(180_000),
161 };
162
163 let retry_manager = RetryManager::new(retry_config).map_err(|e| {
164 BybitHttpError::NetworkError(format!("Failed to create retry manager: {e}"))
165 })?;
166
167 Ok(Self {
168 base_url: base_url
169 .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
170 client: HttpClient::new(
171 Self::default_headers(),
172 vec![],
173 Self::rate_limiter_quotas(),
174 Some(*BYBIT_REST_QUOTA),
175 timeout_secs,
176 ),
177 credential: None,
178 recv_window_ms: DEFAULT_RECV_WINDOW_MS,
179 retry_manager,
180 cancellation_token: CancellationToken::new(),
181 instruments_cache: Arc::new(Mutex::new(HashMap::new())),
182 })
183 }
184
185 #[allow(clippy::too_many_arguments)]
191 pub fn with_credentials(
192 api_key: String,
193 api_secret: String,
194 base_url: Option<String>,
195 timeout_secs: Option<u64>,
196 max_retries: Option<u32>,
197 retry_delay_ms: Option<u64>,
198 retry_delay_max_ms: Option<u64>,
199 ) -> Result<Self, BybitHttpError> {
200 let retry_config = RetryConfig {
201 max_retries: max_retries.unwrap_or(3),
202 initial_delay_ms: retry_delay_ms.unwrap_or(1000),
203 max_delay_ms: retry_delay_max_ms.unwrap_or(10_000),
204 backoff_factor: 2.0,
205 jitter_ms: 1000,
206 operation_timeout_ms: Some(60_000),
207 immediate_first: false,
208 max_elapsed_ms: Some(180_000),
209 };
210
211 let retry_manager = RetryManager::new(retry_config).map_err(|e| {
212 BybitHttpError::NetworkError(format!("Failed to create retry manager: {e}"))
213 })?;
214
215 Ok(Self {
216 base_url: base_url
217 .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
218 client: HttpClient::new(
219 Self::default_headers(),
220 vec![],
221 Self::rate_limiter_quotas(),
222 Some(*BYBIT_REST_QUOTA),
223 timeout_secs,
224 ),
225 credential: Some(Credential::new(api_key, api_secret)),
226 recv_window_ms: DEFAULT_RECV_WINDOW_MS,
227 retry_manager,
228 cancellation_token: CancellationToken::new(),
229 instruments_cache: Arc::new(Mutex::new(HashMap::new())),
230 })
231 }
232
233 fn default_headers() -> HashMap<String, String> {
234 HashMap::from([
235 (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
236 ("Referer".to_string(), BYBIT_NAUTILUS_BROKER_ID.to_string()),
237 ])
238 }
239
240 fn rate_limiter_quotas() -> Vec<(String, Quota)> {
241 vec![(BYBIT_GLOBAL_RATE_KEY.to_string(), *BYBIT_REST_QUOTA)]
242 }
243
244 fn rate_limit_keys(endpoint: &str) -> Vec<String> {
245 let normalized = endpoint.split('?').next().unwrap_or(endpoint);
246 let route = format!("bybit:{normalized}");
247
248 vec![BYBIT_GLOBAL_RATE_KEY.to_string(), route]
249 }
250
251 fn sign_request(
252 &self,
253 timestamp: &str,
254 params: Option<&str>,
255 ) -> Result<HashMap<String, String>, BybitHttpError> {
256 let credential = self
257 .credential
258 .as_ref()
259 .ok_or(BybitHttpError::MissingCredentials)?;
260
261 let signature = credential.sign_with_payload(timestamp, self.recv_window_ms, params);
262
263 let mut headers = HashMap::new();
264 headers.insert(
265 "X-BAPI-API-KEY".to_string(),
266 credential.api_key().to_string(),
267 );
268 headers.insert("X-BAPI-TIMESTAMP".to_string(), timestamp.to_string());
269 headers.insert("X-BAPI-SIGN".to_string(), signature);
270 headers.insert(
271 "X-BAPI-RECV-WINDOW".to_string(),
272 self.recv_window_ms.to_string(),
273 );
274
275 Ok(headers)
276 }
277
278 async fn send_request<T: DeserializeOwned>(
279 &self,
280 method: Method,
281 endpoint: &str,
282 body: Option<Vec<u8>>,
283 authenticate: bool,
284 ) -> Result<T, BybitHttpError> {
285 let endpoint = endpoint.to_string();
286 let url = format!("{}{endpoint}", self.base_url);
287 let method_clone = method.clone();
288 let body_clone = body.clone();
289
290 let operation = || {
291 let url = url.clone();
292 let method = method_clone.clone();
293 let body = body_clone.clone();
294 let endpoint = endpoint.clone();
295
296 async move {
297 let mut headers = Self::default_headers();
298
299 if authenticate {
300 let timestamp = get_atomic_clock_realtime().get_time_ms().to_string();
301 let params_str = if method == Method::GET {
302 endpoint.split('?').nth(1)
303 } else {
304 body.as_ref().and_then(|b| std::str::from_utf8(b).ok())
305 };
306
307 let auth_headers = self.sign_request(×tamp, params_str)?;
308 headers.extend(auth_headers);
309 }
310
311 if method == Method::POST || method == Method::PUT {
312 headers.insert("Content-Type".to_string(), "application/json".to_string());
313 }
314
315 let rate_limit_keys = Self::rate_limit_keys(&endpoint);
316
317 let response = self
318 .client
319 .request(
320 method,
321 url,
322 Some(headers),
323 body,
324 None,
325 Some(rate_limit_keys),
326 )
327 .await?;
328
329 if response.status.as_u16() >= 400 {
330 let body = String::from_utf8_lossy(&response.body).to_string();
331 return Err(BybitHttpError::UnexpectedStatus {
332 status: response.status.as_u16(),
333 body,
334 });
335 }
336
337 let bybit_response: BybitResponse<serde_json::Value> =
339 serde_json::from_slice(&response.body)?;
340
341 if bybit_response.ret_code != 0 {
342 return Err(BybitHttpError::BybitError {
343 error_code: bybit_response.ret_code as i32,
344 message: bybit_response.ret_msg,
345 });
346 }
347
348 let result: T = serde_json::from_slice(&response.body)?;
350 Ok(result)
351 }
352 };
353
354 let should_retry = |error: &BybitHttpError| -> bool {
355 match error {
356 BybitHttpError::NetworkError(_) => true,
357 BybitHttpError::UnexpectedStatus { status, .. } => *status >= 500,
358 _ => false,
359 }
360 };
361
362 let create_error = |msg: String| -> BybitHttpError {
363 if msg == "canceled" {
364 BybitHttpError::NetworkError("Request canceled".to_string())
365 } else {
366 BybitHttpError::NetworkError(msg)
367 }
368 };
369
370 self.retry_manager
371 .execute_with_retry_with_cancel(
372 endpoint.as_str(),
373 operation,
374 should_retry,
375 create_error,
376 &self.cancellation_token,
377 )
378 .await
379 }
380
381 fn build_path<S: Serialize>(base: &str, params: &S) -> Result<String, BybitHttpError> {
382 let query = serde_urlencoded::to_string(params)
383 .map_err(|e| BybitHttpError::JsonError(e.to_string()))?;
384 if query.is_empty() {
385 Ok(base.to_owned())
386 } else {
387 Ok(format!("{base}?{query}"))
388 }
389 }
390
391 pub async fn http_get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
405 self.send_request(Method::GET, "/v5/market/time", None, false)
406 .await
407 }
408
409 pub async fn http_get_instruments<T: DeserializeOwned>(
419 &self,
420 params: &BybitInstrumentsInfoParams,
421 ) -> Result<T, BybitHttpError> {
422 let path = Self::build_path("/v5/market/instruments-info", params)?;
423 self.send_request(Method::GET, &path, None, false).await
424 }
425
426 pub async fn http_get_instruments_spot(
436 &self,
437 params: &BybitInstrumentsInfoParams,
438 ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
439 self.http_get_instruments(params).await
440 }
441
442 pub async fn http_get_instruments_linear(
452 &self,
453 params: &BybitInstrumentsInfoParams,
454 ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
455 self.http_get_instruments(params).await
456 }
457
458 pub async fn http_get_instruments_inverse(
468 &self,
469 params: &BybitInstrumentsInfoParams,
470 ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
471 self.http_get_instruments(params).await
472 }
473
474 pub async fn http_get_instruments_option(
484 &self,
485 params: &BybitInstrumentsInfoParams,
486 ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
487 self.http_get_instruments(params).await
488 }
489
490 pub async fn http_get_klines(
500 &self,
501 params: &BybitKlinesParams,
502 ) -> Result<BybitKlinesResponse, BybitHttpError> {
503 let path = Self::build_path("/v5/market/kline", params)?;
504 self.send_request(Method::GET, &path, None, false).await
505 }
506
507 pub async fn http_get_recent_trades(
517 &self,
518 params: &BybitTradesParams,
519 ) -> Result<BybitTradesResponse, BybitHttpError> {
520 let path = Self::build_path("/v5/market/recent-trade", params)?;
521 self.send_request(Method::GET, &path, None, false).await
522 }
523
524 pub async fn http_get_open_orders(
534 &self,
535 category: BybitProductType,
536 symbol: Option<&str>,
537 ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
538 #[derive(Serialize)]
539 #[serde(rename_all = "camelCase")]
540 struct Params<'a> {
541 category: BybitProductType,
542 #[serde(skip_serializing_if = "Option::is_none")]
543 symbol: Option<&'a str>,
544 }
545
546 let params = Params { category, symbol };
547 let path = Self::build_path("/v5/order/realtime", ¶ms)?;
548 self.send_request(Method::GET, &path, None, true).await
549 }
550
551 pub async fn http_place_order(
561 &self,
562 request: &serde_json::Value,
563 ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
564 let body = serde_json::to_vec(request)?;
565 self.send_request(Method::POST, "/v5/order/create", Some(body), true)
566 .await
567 }
568
569 pub async fn http_get_wallet_balance(
579 &self,
580 params: &BybitWalletBalanceParams,
581 ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
582 let path = Self::build_path("/v5/account/wallet-balance", params)?;
583 self.send_request(Method::GET, &path, None, true).await
584 }
585
586 pub async fn http_get_fee_rate(
596 &self,
597 params: &BybitFeeRateParams,
598 ) -> Result<BybitFeeRateResponse, BybitHttpError> {
599 let path = Self::build_path("/v5/account/fee-rate", params)?;
600 self.send_request(Method::GET, &path, None, true).await
601 }
602
603 pub async fn http_get_tickers<T: DeserializeOwned>(
613 &self,
614 params: &BybitTickersParams,
615 ) -> Result<T, BybitHttpError> {
616 let path = Self::build_path("/v5/market/tickers", params)?;
617 self.send_request(Method::GET, &path, None, false).await
618 }
619
620 pub async fn http_get_trade_history(
630 &self,
631 params: &BybitTradeHistoryParams,
632 ) -> Result<BybitTradeHistoryResponse, BybitHttpError> {
633 let path = Self::build_path("/v5/execution/list", params)?;
634 self.send_request(Method::GET, &path, None, true).await
635 }
636
637 pub async fn http_get_positions(
650 &self,
651 params: &BybitPositionListParams,
652 ) -> Result<BybitPositionListResponse, BybitHttpError> {
653 let path = Self::build_path("/v5/position/list", params)?;
654 self.send_request(Method::GET, &path, None, true).await
655 }
656
657 #[must_use]
659 pub fn base_url(&self) -> &str {
660 &self.base_url
661 }
662
663 #[must_use]
665 pub fn recv_window_ms(&self) -> u64 {
666 self.recv_window_ms
667 }
668
669 #[must_use]
671 pub fn credential(&self) -> Option<&Credential> {
672 self.credential.as_ref()
673 }
674
675 pub fn add_instrument(&self, instrument: InstrumentAny) {
681 let mut cache = self.instruments_cache.lock().unwrap();
682 let symbol = Ustr::from(instrument.id().symbol.as_str());
683 cache.insert(symbol, instrument);
684 }
685
686 pub fn instrument_from_cache(&self, symbol: &Symbol) -> anyhow::Result<InstrumentAny> {
696 let cache = self.instruments_cache.lock().unwrap();
697 cache.get(&symbol.inner()).cloned().ok_or_else(|| {
698 anyhow::anyhow!(
699 "Instrument {symbol} not found in cache, ensure instruments loaded first"
700 )
701 })
702 }
703
704 #[must_use]
706 pub fn generate_ts_init(&self) -> UnixNanos {
707 get_atomic_clock_realtime().get_time_ns()
708 }
709
710 #[allow(clippy::too_many_arguments)]
725 pub async fn submit_order(
726 &self,
727 product_type: BybitProductType,
728 instrument_id: InstrumentId,
729 client_order_id: ClientOrderId,
730 order_side: OrderSide,
731 order_type: OrderType,
732 quantity: Quantity,
733 time_in_force: TimeInForce,
734 price: Option<Price>,
735 reduce_only: bool,
736 ) -> anyhow::Result<OrderStatusReport> {
737 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
738 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
739
740 let bybit_side = match order_side {
741 OrderSide::Buy => BybitOrderSide::Buy,
742 OrderSide::Sell => BybitOrderSide::Sell,
743 _ => anyhow::bail!("Invalid order side: {order_side:?}"),
744 };
745
746 let bybit_order_type = match order_type {
747 OrderType::Market => BybitOrderType::Market,
748 OrderType::Limit => BybitOrderType::Limit,
749 _ => anyhow::bail!("Unsupported order type: {order_type:?}"),
750 };
751
752 let bybit_tif = match time_in_force {
753 TimeInForce::Gtc => BybitTimeInForce::Gtc,
754 TimeInForce::Ioc => BybitTimeInForce::Ioc,
755 TimeInForce::Fok => BybitTimeInForce::Fok,
756 _ => anyhow::bail!("Unsupported time in force: {time_in_force:?}"),
757 };
758
759 let mut order_entry = BybitBatchPlaceOrderEntryBuilder::default();
760 order_entry.symbol(bybit_symbol.raw_symbol().to_string());
761 order_entry.side(bybit_side);
762 order_entry.order_type(bybit_order_type);
763 order_entry.qty(quantity.to_string());
764 order_entry.time_in_force(Some(bybit_tif));
765 order_entry.order_link_id(client_order_id.to_string());
766
767 if let Some(price) = price {
768 order_entry.price(Some(price.to_string()));
769 }
770
771 if reduce_only {
772 order_entry.reduce_only(Some(true));
773 }
774
775 let order_entry = order_entry.build().map_err(|e| anyhow::anyhow!(e))?;
776
777 let mut params = BybitPlaceOrderParamsBuilder::default();
778 params.category(product_type);
779 params.order(order_entry);
780
781 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
782
783 let body = serde_json::to_value(¶ms)?;
784 let response = self.http_place_order(&body).await?;
785
786 let order_id = response
787 .result
788 .order_id
789 .ok_or_else(|| anyhow::anyhow!("No order_id in response"))?;
790
791 let mut query_params = BybitOpenOrdersParamsBuilder::default();
793 query_params.category(product_type);
794 query_params.order_id(order_id.as_str().to_string());
795
796 let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
797 let path = Self::build_path("/v5/order/realtime", &query_params)?;
798 let order_response: BybitOpenOrdersResponse =
799 self.send_request(Method::GET, &path, None, true).await?;
800
801 let order = order_response
802 .result
803 .list
804 .into_iter()
805 .next()
806 .ok_or_else(|| anyhow::anyhow!("No order returned after submission"))?;
807
808 if order.order_status == crate::common::enums::BybitOrderStatus::Rejected
811 && (order.cum_exec_qty.as_str() == "0" || order.cum_exec_qty.is_empty())
812 {
813 anyhow::bail!("Order rejected: {}", order.reject_reason);
814 }
815
816 let account_id = AccountId::new("BYBIT");
817 let ts_init = self.generate_ts_init();
818
819 parse_order_status_report(&order, &instrument, account_id, ts_init)
820 }
821
822 pub async fn cancel_order(
832 &self,
833 product_type: BybitProductType,
834 instrument_id: InstrumentId,
835 client_order_id: Option<ClientOrderId>,
836 venue_order_id: Option<VenueOrderId>,
837 ) -> anyhow::Result<OrderStatusReport> {
838 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
839 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
840
841 let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
842 cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
843
844 if let Some(venue_order_id) = venue_order_id {
845 cancel_entry.order_id(venue_order_id.to_string());
846 } else if let Some(client_order_id) = client_order_id {
847 cancel_entry.order_link_id(client_order_id.to_string());
848 } else {
849 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
850 }
851
852 let cancel_entry = cancel_entry.build().map_err(|e| anyhow::anyhow!(e))?;
853
854 let mut params = BybitCancelOrderParamsBuilder::default();
855 params.category(product_type);
856 params.order(cancel_entry);
857
858 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
859 let body = serde_json::to_vec(¶ms)?;
860
861 let response: BybitPlaceOrderResponse = self
862 .send_request(Method::POST, "/v5/order/cancel", Some(body), true)
863 .await?;
864
865 let order_id = response
866 .result
867 .order_id
868 .ok_or_else(|| anyhow::anyhow!("No order_id in cancel response"))?;
869
870 let mut query_params = BybitOpenOrdersParamsBuilder::default();
872 query_params.category(product_type);
873 query_params.order_id(order_id.as_str().to_string());
874
875 let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
876 let path = Self::build_path("/v5/order/history", &query_params)?;
877 let order_response: BybitOrderHistoryResponse =
878 self.send_request(Method::GET, &path, None, true).await?;
879
880 let order = order_response
881 .result
882 .list
883 .into_iter()
884 .next()
885 .ok_or_else(|| anyhow::anyhow!("No order returned in cancel response"))?;
886
887 let account_id = AccountId::new("BYBIT");
888 let ts_init = self.generate_ts_init();
889
890 parse_order_status_report(&order, &instrument, account_id, ts_init)
891 }
892
893 pub async fn cancel_all_orders(
902 &self,
903 product_type: BybitProductType,
904 instrument_id: InstrumentId,
905 ) -> anyhow::Result<Vec<OrderStatusReport>> {
906 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
907 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
908
909 let mut params = BybitCancelAllOrdersParamsBuilder::default();
910 params.category(product_type);
911 params.symbol(bybit_symbol.raw_symbol().to_string());
912
913 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
914 let body = serde_json::to_vec(¶ms)?;
915
916 let _response: crate::common::models::BybitListResponse<serde_json::Value> = self
917 .send_request(Method::POST, "/v5/order/cancel-all", Some(body), true)
918 .await?;
919
920 let mut query_params = BybitOrderHistoryParamsBuilder::default();
922 query_params.category(product_type);
923 query_params.symbol(bybit_symbol.raw_symbol().to_string());
924 query_params.limit(50);
925
926 let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
927 let path = Self::build_path("/v5/order/history", &query_params)?;
928 let order_response: BybitOrderHistoryResponse =
929 self.send_request(Method::GET, &path, None, true).await?;
930
931 let account_id = AccountId::new("BYBIT");
932 let ts_init = self.generate_ts_init();
933
934 let mut reports = Vec::new();
935 for order in order_response.result.list {
936 if let Ok(report) = parse_order_status_report(&order, &instrument, account_id, ts_init)
937 {
938 reports.push(report);
939 }
940 }
941
942 Ok(reports)
943 }
944
945 pub async fn modify_order(
956 &self,
957 product_type: BybitProductType,
958 instrument_id: InstrumentId,
959 client_order_id: Option<ClientOrderId>,
960 venue_order_id: Option<VenueOrderId>,
961 quantity: Option<Quantity>,
962 price: Option<Price>,
963 ) -> anyhow::Result<OrderStatusReport> {
964 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
965 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
966
967 let mut amend_entry = BybitBatchAmendOrderEntryBuilder::default();
968 amend_entry.symbol(bybit_symbol.raw_symbol().to_string());
969
970 if let Some(venue_order_id) = venue_order_id {
971 amend_entry.order_id(venue_order_id.to_string());
972 } else if let Some(client_order_id) = client_order_id {
973 amend_entry.order_link_id(client_order_id.to_string());
974 } else {
975 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
976 }
977
978 if let Some(quantity) = quantity {
979 amend_entry.qty(Some(quantity.to_string()));
980 }
981
982 if let Some(price) = price {
983 amend_entry.price(Some(price.to_string()));
984 }
985
986 let amend_entry = amend_entry.build().map_err(|e| anyhow::anyhow!(e))?;
987
988 let mut params = BybitAmendOrderParamsBuilder::default();
989 params.category(product_type);
990 params.order(amend_entry);
991
992 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
993 let body = serde_json::to_vec(¶ms)?;
994
995 let response: BybitPlaceOrderResponse = self
996 .send_request(Method::POST, "/v5/order/amend", Some(body), true)
997 .await?;
998
999 let order_id = response
1000 .result
1001 .order_id
1002 .ok_or_else(|| anyhow::anyhow!("No order_id in amend response"))?;
1003
1004 let mut query_params = BybitOpenOrdersParamsBuilder::default();
1006 query_params.category(product_type);
1007 query_params.order_id(order_id.as_str().to_string());
1008
1009 let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
1010 let path = Self::build_path("/v5/order/realtime", &query_params)?;
1011 let order_response: BybitOpenOrdersResponse =
1012 self.send_request(Method::GET, &path, None, true).await?;
1013
1014 let order = order_response
1015 .result
1016 .list
1017 .into_iter()
1018 .next()
1019 .ok_or_else(|| anyhow::anyhow!("No order returned after modification"))?;
1020
1021 let account_id = AccountId::new("BYBIT");
1022 let ts_init = self.generate_ts_init();
1023
1024 parse_order_status_report(&order, &instrument, account_id, ts_init)
1025 }
1026
1027 pub async fn query_order(
1036 &self,
1037 account_id: AccountId,
1038 product_type: BybitProductType,
1039 instrument_id: InstrumentId,
1040 client_order_id: Option<ClientOrderId>,
1041 venue_order_id: Option<VenueOrderId>,
1042 ) -> anyhow::Result<Option<OrderStatusReport>> {
1043 tracing::info!(
1044 "query_order called: instrument_id={}, client_order_id={:?}, venue_order_id={:?}",
1045 instrument_id,
1046 client_order_id,
1047 venue_order_id
1048 );
1049
1050 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
1051
1052 let mut params = BybitOpenOrdersParamsBuilder::default();
1053 params.category(product_type);
1054 params.symbol(bybit_symbol.raw_symbol().to_string());
1056
1057 if let Some(venue_order_id) = venue_order_id {
1058 params.order_id(venue_order_id.to_string());
1059 } else if let Some(client_order_id) = client_order_id {
1060 params.order_link_id(client_order_id.to_string());
1061 } else {
1062 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
1063 }
1064
1065 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
1066 let path = Self::build_path("/v5/order/realtime", ¶ms)?;
1067
1068 let response: BybitOpenOrdersResponse =
1069 self.send_request(Method::GET, &path, None, true).await?;
1070
1071 if response.result.list.is_empty() {
1072 return Ok(None);
1073 }
1074
1075 let order = &response.result.list[0];
1076 let ts_init = self.generate_ts_init();
1077
1078 tracing::debug!(
1079 "Query order response: symbol={}, order_id={}, order_link_id={}",
1080 order.symbol.as_str(),
1081 order.order_id.as_str(),
1082 order.order_link_id.as_str()
1083 );
1084
1085 let instrument = self
1087 .instrument_from_cache(&instrument_id.symbol)
1088 .map_err(|e| {
1089 tracing::error!(
1090 "Instrument cache miss for symbol '{}': {}",
1091 instrument_id.symbol.as_str(),
1092 e
1093 );
1094 anyhow::anyhow!(
1095 "Failed to query order {}: {}",
1096 client_order_id
1097 .as_ref()
1098 .map(|id| id.to_string())
1099 .or_else(|| venue_order_id.as_ref().map(|id| id.to_string()))
1100 .unwrap_or_else(|| "unknown".to_string()),
1101 e
1102 )
1103 })?;
1104
1105 tracing::debug!("Retrieved instrument from cache: id={}", instrument.id());
1106
1107 let report =
1108 parse_order_status_report(order, &instrument, account_id, ts_init).map_err(|e| {
1109 tracing::error!(
1110 "Failed to parse order status report for {}: {}",
1111 order.order_link_id.as_str(),
1112 e
1113 );
1114 e
1115 })?;
1116
1117 tracing::debug!(
1118 "Successfully created OrderStatusReport for {}",
1119 order.order_link_id.as_str()
1120 );
1121
1122 Ok(Some(report))
1123 }
1124
1125 pub async fn request_instruments(
1133 &self,
1134 product_type: BybitProductType,
1135 symbol: Option<String>,
1136 ) -> anyhow::Result<Vec<InstrumentAny>> {
1137 let ts_init = self.generate_ts_init();
1138
1139 let params = BybitInstrumentsInfoParams {
1140 category: product_type,
1141 symbol,
1142 status: None,
1143 base_coin: None,
1144 limit: None,
1145 cursor: None,
1146 };
1147
1148 let mut instruments = Vec::new();
1149
1150 let default_fee_rate = |symbol: ustr::Ustr| BybitFeeRate {
1151 symbol,
1152 taker_fee_rate: "0.001".to_string(),
1153 maker_fee_rate: "0.001".to_string(),
1154 base_coin: None,
1155 };
1156
1157 match product_type {
1158 BybitProductType::Spot => {
1159 let response: BybitInstrumentSpotResponse =
1160 self.http_get_instruments(¶ms).await?;
1161
1162 let fee_map: HashMap<_, _> = {
1164 let mut fee_params = BybitFeeRateParamsBuilder::default();
1165 fee_params.category(product_type);
1166 if let Ok(params) = fee_params.build() {
1167 match self.http_get_fee_rate(¶ms).await {
1168 Ok(fee_response) => fee_response
1169 .result
1170 .list
1171 .into_iter()
1172 .map(|f| (f.symbol, f))
1173 .collect(),
1174 Err(BybitHttpError::MissingCredentials) => {
1175 tracing::warn!("Missing credentials for fee rates, using defaults");
1176 HashMap::new()
1177 }
1178 Err(e) => return Err(e.into()),
1179 }
1180 } else {
1181 HashMap::new()
1182 }
1183 };
1184
1185 for definition in response.result.list {
1186 let fee_rate = fee_map
1187 .get(&definition.symbol)
1188 .cloned()
1189 .unwrap_or_else(|| default_fee_rate(definition.symbol));
1190 if let Ok(instrument) =
1191 parse_spot_instrument(&definition, &fee_rate, ts_init, ts_init)
1192 {
1193 instruments.push(instrument);
1194 }
1195 }
1196 }
1197 BybitProductType::Linear => {
1198 let response: BybitInstrumentLinearResponse =
1199 self.http_get_instruments(¶ms).await?;
1200
1201 let fee_map: HashMap<_, _> = {
1203 let mut fee_params = BybitFeeRateParamsBuilder::default();
1204 fee_params.category(product_type);
1205 if let Ok(params) = fee_params.build() {
1206 match self.http_get_fee_rate(¶ms).await {
1207 Ok(fee_response) => fee_response
1208 .result
1209 .list
1210 .into_iter()
1211 .map(|f| (f.symbol, f))
1212 .collect(),
1213 Err(BybitHttpError::MissingCredentials) => {
1214 tracing::warn!("Missing credentials for fee rates, using defaults");
1215 HashMap::new()
1216 }
1217 Err(e) => return Err(e.into()),
1218 }
1219 } else {
1220 HashMap::new()
1221 }
1222 };
1223
1224 for definition in response.result.list {
1225 let fee_rate = fee_map
1226 .get(&definition.symbol)
1227 .cloned()
1228 .unwrap_or_else(|| default_fee_rate(definition.symbol));
1229 if let Ok(instrument) =
1230 parse_linear_instrument(&definition, &fee_rate, ts_init, ts_init)
1231 {
1232 instruments.push(instrument);
1233 }
1234 }
1235 }
1236 BybitProductType::Inverse => {
1237 let response: BybitInstrumentInverseResponse =
1238 self.http_get_instruments(¶ms).await?;
1239
1240 let fee_map: HashMap<_, _> = {
1242 let mut fee_params = BybitFeeRateParamsBuilder::default();
1243 fee_params.category(product_type);
1244 if let Ok(params) = fee_params.build() {
1245 match self.http_get_fee_rate(¶ms).await {
1246 Ok(fee_response) => fee_response
1247 .result
1248 .list
1249 .into_iter()
1250 .map(|f| (f.symbol, f))
1251 .collect(),
1252 Err(BybitHttpError::MissingCredentials) => {
1253 tracing::warn!("Missing credentials for fee rates, using defaults");
1254 HashMap::new()
1255 }
1256 Err(e) => return Err(e.into()),
1257 }
1258 } else {
1259 HashMap::new()
1260 }
1261 };
1262
1263 for definition in response.result.list {
1264 let fee_rate = fee_map
1265 .get(&definition.symbol)
1266 .cloned()
1267 .unwrap_or_else(|| default_fee_rate(definition.symbol));
1268 if let Ok(instrument) =
1269 parse_inverse_instrument(&definition, &fee_rate, ts_init, ts_init)
1270 {
1271 instruments.push(instrument);
1272 }
1273 }
1274 }
1275 BybitProductType::Option => {
1276 let response: BybitInstrumentOptionResponse =
1277 self.http_get_instruments(¶ms).await?;
1278
1279 for definition in response.result.list {
1280 if let Ok(instrument) = parse_option_instrument(&definition, ts_init, ts_init) {
1281 instruments.push(instrument);
1282 }
1283 }
1284 }
1285 }
1286
1287 for instrument in &instruments {
1289 self.add_instrument(instrument.clone());
1290 }
1291
1292 Ok(instruments)
1293 }
1294
1295 pub async fn request_trades(
1308 &self,
1309 product_type: BybitProductType,
1310 instrument_id: InstrumentId,
1311 limit: Option<u32>,
1312 ) -> anyhow::Result<Vec<TradeTick>> {
1313 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
1314 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
1315
1316 let mut params_builder = BybitTradesParamsBuilder::default();
1317 params_builder.category(product_type);
1318 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
1319 if let Some(limit_val) = limit {
1320 params_builder.limit(limit_val);
1321 }
1322
1323 let params = params_builder.build().map_err(|e| anyhow::anyhow!(e))?;
1324 let response = self.http_get_recent_trades(¶ms).await?;
1325
1326 let ts_init = self.generate_ts_init();
1327 let mut trades = Vec::new();
1328
1329 for trade in response.result.list {
1330 if let Ok(trade_tick) = parse_trade_tick(&trade, &instrument, ts_init) {
1331 trades.push(trade_tick);
1332 }
1333 }
1334
1335 Ok(trades)
1336 }
1337
1338 pub async fn request_bars(
1351 &self,
1352 product_type: BybitProductType,
1353 bar_type: BarType,
1354 start: Option<i64>,
1355 end: Option<i64>,
1356 limit: Option<u32>,
1357 ) -> anyhow::Result<Vec<Bar>> {
1358 let instrument = self.instrument_from_cache(&bar_type.instrument_id().symbol)?;
1359 let bybit_symbol = BybitSymbol::new(bar_type.instrument_id().symbol.as_str())?;
1360
1361 let interval = match bar_type.spec().aggregation {
1363 BarAggregation::Minute => BybitKlineInterval::Minute1,
1364 BarAggregation::Hour => BybitKlineInterval::Hour1,
1365 BarAggregation::Day => BybitKlineInterval::Day1,
1366 _ => anyhow::bail!(
1367 "Unsupported bar aggregation: {:?}",
1368 bar_type.spec().aggregation
1369 ),
1370 };
1371
1372 let mut params_builder = BybitKlinesParamsBuilder::default();
1373 params_builder.category(product_type);
1374 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
1375 params_builder.interval(interval);
1376
1377 if let Some(start_ts) = start {
1378 params_builder.start(start_ts);
1379 }
1380 if let Some(end_ts) = end {
1381 params_builder.end(end_ts);
1382 }
1383 if let Some(limit_val) = limit {
1384 params_builder.limit(limit_val);
1385 }
1386
1387 let params = params_builder.build().map_err(|e| anyhow::anyhow!(e))?;
1388 let response = self.http_get_klines(¶ms).await?;
1389
1390 let ts_init = self.generate_ts_init();
1391 let mut bars = Vec::new();
1392
1393 for kline in response.result.list {
1394 if let Ok(bar) = parse_kline_bar(&kline, &instrument, bar_type, false, ts_init) {
1395 bars.push(bar);
1396 }
1397 }
1398
1399 Ok(bars)
1400 }
1401
1402 pub async fn request_fee_rates(
1414 &self,
1415 product_type: BybitProductType,
1416 symbol: Option<String>,
1417 base_coin: Option<String>,
1418 ) -> anyhow::Result<Vec<BybitFeeRate>> {
1419 let params = BybitFeeRateParams {
1420 category: product_type,
1421 symbol,
1422 base_coin,
1423 };
1424
1425 let response = self.http_get_fee_rate(¶ms).await?;
1426 Ok(response.result.list)
1427 }
1428
1429 pub async fn request_account_state(
1441 &self,
1442 account_type: BybitAccountType,
1443 account_id: AccountId,
1444 ) -> anyhow::Result<AccountState> {
1445 let params = BybitWalletBalanceParams {
1446 account_type,
1447 coin: None,
1448 };
1449
1450 let response = self.http_get_wallet_balance(¶ms).await?;
1451 let ts_init = self.generate_ts_init();
1452
1453 let wallet_balance = response
1455 .result
1456 .list
1457 .first()
1458 .ok_or_else(|| anyhow::anyhow!("No wallet balance found in response"))?;
1459
1460 parse_account_state(wallet_balance, account_id, ts_init)
1461 }
1462
1463 pub async fn request_order_status_reports(
1472 &self,
1473 account_id: AccountId,
1474 product_type: BybitProductType,
1475 instrument_id: Option<InstrumentId>,
1476 open_only: bool,
1477 limit: Option<u32>,
1478 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1479 let symbol_param = if let Some(id) = instrument_id.as_ref() {
1481 let symbol_str = id.symbol.as_str();
1482 if symbol_str.is_empty() {
1483 None
1484 } else {
1485 Some(BybitSymbol::new(symbol_str)?.raw_symbol().to_string())
1486 }
1487 } else {
1488 None
1489 };
1490
1491 let params = if open_only {
1492 let mut p = BybitOpenOrdersParamsBuilder::default();
1493 p.category(product_type);
1494 if let Some(symbol) = symbol_param.clone() {
1495 p.symbol(symbol);
1496 }
1497 let params = p.build().map_err(|e| anyhow::anyhow!(e))?;
1498 let path = Self::build_path("/v5/order/realtime", ¶ms)?;
1499 let response: BybitOpenOrdersResponse =
1500 self.send_request(Method::GET, &path, None, true).await?;
1501 response.result.list
1502 } else {
1503 let mut p = BybitOrderHistoryParamsBuilder::default();
1504 p.category(product_type);
1505 if let Some(symbol) = symbol_param {
1506 p.symbol(symbol);
1507 }
1508 if let Some(limit) = limit {
1509 p.limit(limit);
1510 }
1511 let params = p.build().map_err(|e| anyhow::anyhow!(e))?;
1512 let path = Self::build_path("/v5/order/history", ¶ms)?;
1513 let response: BybitOrderHistoryResponse =
1514 self.send_request(Method::GET, &path, None, true).await?;
1515 response.result.list
1516 };
1517
1518 let ts_init = self.generate_ts_init();
1519
1520 let mut reports = Vec::new();
1521 for order in params {
1522 if let Some(ref instrument_id) = instrument_id {
1523 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
1524 if let Ok(report) =
1525 parse_order_status_report(&order, &instrument, account_id, ts_init)
1526 {
1527 reports.push(report);
1528 }
1529 } else {
1530 if !order.symbol.is_empty() {
1533 let symbol_with_product = Symbol::new(format!(
1534 "{}{}",
1535 order.symbol.as_str(),
1536 product_type.suffix()
1537 ));
1538 if let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
1539 && let Ok(report) =
1540 parse_order_status_report(&order, &instrument, account_id, ts_init)
1541 {
1542 reports.push(report);
1543 }
1544 }
1545 }
1546 }
1547
1548 Ok(reports)
1549 }
1550
1551 pub async fn request_fill_reports(
1565 &self,
1566 account_id: AccountId,
1567 product_type: BybitProductType,
1568 instrument_id: Option<InstrumentId>,
1569 start: Option<i64>,
1570 end: Option<i64>,
1571 limit: Option<u32>,
1572 ) -> anyhow::Result<Vec<FillReport>> {
1573 let symbol = if let Some(id) = instrument_id {
1575 let bybit_symbol = BybitSymbol::new(id.symbol.as_str())?;
1576 Some(bybit_symbol.raw_symbol().to_string())
1577 } else {
1578 None
1579 };
1580 let params = BybitTradeHistoryParams {
1581 category: product_type,
1582 symbol,
1583 base_coin: None,
1584 order_id: None,
1585 order_link_id: None,
1586 start_time: start,
1587 end_time: end,
1588 exec_type: None,
1589 limit,
1590 cursor: None,
1591 };
1592
1593 let response = self.http_get_trade_history(¶ms).await?;
1594 let ts_init = self.generate_ts_init();
1595 let mut reports = Vec::new();
1596
1597 for execution in response.result.list {
1598 let symbol_with_product = Symbol::new(format!(
1602 "{}{}",
1603 execution.symbol.as_str(),
1604 product_type.suffix()
1605 ));
1606 let instrument = self.instrument_from_cache(&symbol_with_product)?;
1607
1608 if let Ok(report) = parse_fill_report(&execution, account_id, &instrument, ts_init) {
1609 reports.push(report);
1610 }
1611 }
1612
1613 Ok(reports)
1614 }
1615
1616 pub async fn request_position_status_reports(
1630 &self,
1631 account_id: AccountId,
1632 product_type: BybitProductType,
1633 instrument_id: Option<InstrumentId>,
1634 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1635 let ts_init = self.generate_ts_init();
1636 let mut reports = Vec::new();
1637
1638 let symbol = if let Some(id) = instrument_id {
1640 let symbol_str = id.symbol.as_str();
1641 if symbol_str.is_empty() {
1642 anyhow::bail!("InstrumentId symbol is empty");
1643 }
1644 let bybit_symbol = BybitSymbol::new(symbol_str)?;
1645 Some(bybit_symbol.raw_symbol().to_string())
1646 } else {
1647 None
1648 };
1649
1650 if product_type == BybitProductType::Linear && symbol.is_none() {
1653 for settle_coin in ["USDT", "USDC"] {
1655 let params = BybitPositionListParams {
1656 category: product_type,
1657 symbol: None,
1658 base_coin: None,
1659 settle_coin: Some(settle_coin.to_string()),
1660 limit: None,
1661 cursor: None,
1662 };
1663
1664 let response = self.http_get_positions(¶ms).await?;
1665
1666 for position in response.result.list {
1667 if position.symbol.is_empty() {
1668 continue;
1669 }
1670
1671 let symbol_with_product = Symbol::new(format!(
1672 "{}{}",
1673 position.symbol.as_str(),
1674 product_type.suffix()
1675 ));
1676
1677 if let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
1678 && let Ok(report) = parse_position_status_report(
1679 &position,
1680 account_id,
1681 &instrument,
1682 ts_init,
1683 )
1684 {
1685 reports.push(report);
1686 }
1687 }
1688 }
1689 } else {
1690 let params = BybitPositionListParams {
1692 category: product_type,
1693 symbol,
1694 base_coin: None,
1695 settle_coin: None,
1696 limit: None,
1697 cursor: None,
1698 };
1699
1700 let response = self.http_get_positions(¶ms).await?;
1701
1702 for position in response.result.list {
1703 if position.symbol.is_empty() {
1704 continue;
1705 }
1706
1707 let symbol_with_product = Symbol::new(format!(
1708 "{}{}",
1709 position.symbol.as_str(),
1710 product_type.suffix()
1711 ));
1712
1713 if let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
1714 && let Ok(report) =
1715 parse_position_status_report(&position, account_id, &instrument, ts_init)
1716 {
1717 reports.push(report);
1718 }
1719 }
1720 }
1721
1722 Ok(reports)
1723 }
1724}
1725
1726#[derive(Clone)]
1732#[cfg_attr(
1733 feature = "python",
1734 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
1735)]
1736pub struct BybitHttpClient {
1737 pub(crate) inner: Arc<BybitHttpInnerClient>,
1738}
1739
1740impl Default for BybitHttpClient {
1741 fn default() -> Self {
1742 Self::new(None, Some(60), None, None, None)
1743 .expect("Failed to create default BybitHttpClient")
1744 }
1745}
1746
1747impl Debug for BybitHttpClient {
1748 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1749 f.debug_struct("BybitHttpClient")
1750 .field("inner", &self.inner)
1751 .finish()
1752 }
1753}
1754
1755impl BybitHttpClient {
1756 #[allow(clippy::too_many_arguments)]
1762 pub fn new(
1763 base_url: Option<String>,
1764 timeout_secs: Option<u64>,
1765 max_retries: Option<u32>,
1766 retry_delay_ms: Option<u64>,
1767 retry_delay_max_ms: Option<u64>,
1768 ) -> Result<Self, BybitHttpError> {
1769 Ok(Self {
1770 inner: Arc::new(BybitHttpInnerClient::new(
1771 base_url,
1772 timeout_secs,
1773 max_retries,
1774 retry_delay_ms,
1775 retry_delay_max_ms,
1776 )?),
1777 })
1778 }
1779
1780 #[allow(clippy::too_many_arguments)]
1786 pub fn with_credentials(
1787 api_key: String,
1788 api_secret: String,
1789 base_url: Option<String>,
1790 timeout_secs: Option<u64>,
1791 max_retries: Option<u32>,
1792 retry_delay_ms: Option<u64>,
1793 retry_delay_max_ms: Option<u64>,
1794 ) -> Result<Self, BybitHttpError> {
1795 Ok(Self {
1796 inner: Arc::new(BybitHttpInnerClient::with_credentials(
1797 api_key,
1798 api_secret,
1799 base_url,
1800 timeout_secs,
1801 max_retries,
1802 retry_delay_ms,
1803 retry_delay_max_ms,
1804 )?),
1805 })
1806 }
1807
1808 #[must_use]
1810 pub fn base_url(&self) -> &str {
1811 self.inner.base_url()
1812 }
1813
1814 #[must_use]
1816 pub fn recv_window_ms(&self) -> u64 {
1817 self.inner.recv_window_ms()
1818 }
1819
1820 #[must_use]
1822 pub fn credential(&self) -> Option<&Credential> {
1823 self.inner.credential()
1824 }
1825
1826 pub fn cancel_all_requests(&self) {
1828 self.inner.cancel_all_requests();
1829 }
1830
1831 pub fn cancellation_token(&self) -> &CancellationToken {
1833 self.inner.cancellation_token()
1834 }
1835
1836 pub async fn http_get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
1852 self.inner.http_get_server_time().await
1853 }
1854
1855 pub async fn http_get_instruments<T: DeserializeOwned>(
1867 &self,
1868 params: &BybitInstrumentsInfoParams,
1869 ) -> Result<T, BybitHttpError> {
1870 self.inner.http_get_instruments(params).await
1871 }
1872
1873 pub async fn http_get_instruments_spot(
1885 &self,
1886 params: &BybitInstrumentsInfoParams,
1887 ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
1888 self.inner.http_get_instruments_spot(params).await
1889 }
1890
1891 pub async fn http_get_instruments_linear(
1903 &self,
1904 params: &BybitInstrumentsInfoParams,
1905 ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
1906 self.inner.http_get_instruments_linear(params).await
1907 }
1908
1909 pub async fn http_get_instruments_inverse(
1921 &self,
1922 params: &BybitInstrumentsInfoParams,
1923 ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
1924 self.inner.http_get_instruments_inverse(params).await
1925 }
1926
1927 pub async fn http_get_instruments_option(
1939 &self,
1940 params: &BybitInstrumentsInfoParams,
1941 ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
1942 self.inner.http_get_instruments_option(params).await
1943 }
1944
1945 pub async fn http_get_klines(
1957 &self,
1958 params: &BybitKlinesParams,
1959 ) -> Result<BybitKlinesResponse, BybitHttpError> {
1960 self.inner.http_get_klines(params).await
1961 }
1962
1963 pub async fn http_get_recent_trades(
1975 &self,
1976 params: &BybitTradesParams,
1977 ) -> Result<BybitTradesResponse, BybitHttpError> {
1978 self.inner.http_get_recent_trades(params).await
1979 }
1980
1981 pub async fn http_get_open_orders(
1993 &self,
1994 category: BybitProductType,
1995 symbol: Option<&str>,
1996 ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
1997 self.inner.http_get_open_orders(category, symbol).await
1998 }
1999
2000 pub async fn http_place_order(
2012 &self,
2013 request: &serde_json::Value,
2014 ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
2015 self.inner.http_place_order(request).await
2016 }
2017
2018 pub fn add_instrument(&self, instrument: InstrumentAny) {
2024 self.inner.add_instrument(instrument);
2025 }
2026
2027 #[allow(clippy::too_many_arguments)]
2038 pub async fn submit_order(
2039 &self,
2040 product_type: BybitProductType,
2041 instrument_id: InstrumentId,
2042 client_order_id: ClientOrderId,
2043 order_side: OrderSide,
2044 order_type: OrderType,
2045 quantity: Quantity,
2046 time_in_force: TimeInForce,
2047 price: Option<Price>,
2048 reduce_only: bool,
2049 ) -> anyhow::Result<OrderStatusReport> {
2050 self.inner
2051 .submit_order(
2052 product_type,
2053 instrument_id,
2054 client_order_id,
2055 order_side,
2056 order_type,
2057 quantity,
2058 time_in_force,
2059 price,
2060 reduce_only,
2061 )
2062 .await
2063 }
2064
2065 pub async fn modify_order(
2076 &self,
2077 product_type: BybitProductType,
2078 instrument_id: InstrumentId,
2079 client_order_id: Option<ClientOrderId>,
2080 venue_order_id: Option<VenueOrderId>,
2081 quantity: Option<Quantity>,
2082 price: Option<Price>,
2083 ) -> anyhow::Result<OrderStatusReport> {
2084 self.inner
2085 .modify_order(
2086 product_type,
2087 instrument_id,
2088 client_order_id,
2089 venue_order_id,
2090 quantity,
2091 price,
2092 )
2093 .await
2094 }
2095
2096 pub async fn cancel_order(
2106 &self,
2107 product_type: BybitProductType,
2108 instrument_id: InstrumentId,
2109 client_order_id: Option<ClientOrderId>,
2110 venue_order_id: Option<VenueOrderId>,
2111 ) -> anyhow::Result<OrderStatusReport> {
2112 self.inner
2113 .cancel_order(product_type, instrument_id, client_order_id, venue_order_id)
2114 .await
2115 }
2116
2117 pub async fn cancel_all_orders(
2126 &self,
2127 product_type: BybitProductType,
2128 instrument_id: InstrumentId,
2129 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2130 self.inner
2131 .cancel_all_orders(product_type, instrument_id)
2132 .await
2133 }
2134
2135 pub async fn query_order(
2144 &self,
2145 account_id: AccountId,
2146 product_type: BybitProductType,
2147 instrument_id: InstrumentId,
2148 client_order_id: Option<ClientOrderId>,
2149 venue_order_id: Option<VenueOrderId>,
2150 ) -> anyhow::Result<Option<OrderStatusReport>> {
2151 self.inner
2152 .query_order(
2153 account_id,
2154 product_type,
2155 instrument_id,
2156 client_order_id,
2157 venue_order_id,
2158 )
2159 .await
2160 }
2161
2162 pub async fn request_order_status_reports(
2171 &self,
2172 account_id: AccountId,
2173 product_type: BybitProductType,
2174 instrument_id: Option<InstrumentId>,
2175 open_only: bool,
2176 limit: Option<u32>,
2177 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2178 self.inner
2179 .request_order_status_reports(account_id, product_type, instrument_id, open_only, limit)
2180 .await
2181 }
2182
2183 pub async fn request_instruments(
2191 &self,
2192 product_type: BybitProductType,
2193 symbol: Option<String>,
2194 ) -> anyhow::Result<Vec<InstrumentAny>> {
2195 self.inner.request_instruments(product_type, symbol).await
2196 }
2197
2198 pub async fn request_trades(
2211 &self,
2212 product_type: BybitProductType,
2213 instrument_id: InstrumentId,
2214 limit: Option<u32>,
2215 ) -> anyhow::Result<Vec<TradeTick>> {
2216 self.inner
2217 .request_trades(product_type, instrument_id, limit)
2218 .await
2219 }
2220
2221 pub async fn request_bars(
2234 &self,
2235 product_type: BybitProductType,
2236 bar_type: BarType,
2237 start: Option<i64>,
2238 end: Option<i64>,
2239 limit: Option<u32>,
2240 ) -> anyhow::Result<Vec<Bar>> {
2241 self.inner
2242 .request_bars(product_type, bar_type, start, end, limit)
2243 .await
2244 }
2245
2246 pub async fn request_fill_reports(
2260 &self,
2261 account_id: AccountId,
2262 product_type: BybitProductType,
2263 instrument_id: Option<InstrumentId>,
2264 start: Option<i64>,
2265 end: Option<i64>,
2266 limit: Option<u32>,
2267 ) -> anyhow::Result<Vec<FillReport>> {
2268 self.inner
2269 .request_fill_reports(account_id, product_type, instrument_id, start, end, limit)
2270 .await
2271 }
2272
2273 pub async fn request_position_status_reports(
2287 &self,
2288 account_id: AccountId,
2289 product_type: BybitProductType,
2290 instrument_id: Option<InstrumentId>,
2291 ) -> anyhow::Result<Vec<PositionStatusReport>> {
2292 self.inner
2293 .request_position_status_reports(account_id, product_type, instrument_id)
2294 .await
2295 }
2296
2297 pub async fn request_account_state(
2309 &self,
2310 account_type: crate::common::enums::BybitAccountType,
2311 account_id: AccountId,
2312 ) -> anyhow::Result<AccountState> {
2313 self.inner
2314 .request_account_state(account_type, account_id)
2315 .await
2316 }
2317
2318 pub async fn request_fee_rates(
2330 &self,
2331 product_type: BybitProductType,
2332 symbol: Option<String>,
2333 base_coin: Option<String>,
2334 ) -> anyhow::Result<Vec<BybitFeeRate>> {
2335 self.inner
2336 .request_fee_rates(product_type, symbol, base_coin)
2337 .await
2338 }
2339}
2340
2341#[cfg(test)]
2346mod tests {
2347 use rstest::rstest;
2348
2349 use super::*;
2350
2351 #[rstest]
2352 fn test_client_creation() {
2353 let client = BybitHttpClient::new(None, Some(60), None, None, None);
2354 assert!(client.is_ok());
2355
2356 let client = client.unwrap();
2357 assert!(client.base_url().contains("bybit.com"));
2358 assert!(client.credential().is_none());
2359 }
2360
2361 #[rstest]
2362 fn test_client_with_credentials() {
2363 let client = BybitHttpClient::with_credentials(
2364 "test_key".to_string(),
2365 "test_secret".to_string(),
2366 Some("https://api-testnet.bybit.com".to_string()),
2367 Some(60),
2368 None,
2369 None,
2370 None,
2371 );
2372 assert!(client.is_ok());
2373
2374 let client = client.unwrap();
2375 assert!(client.credential().is_some());
2376 }
2377
2378 #[rstest]
2379 fn test_build_path_with_params() {
2380 #[derive(Serialize)]
2381 struct TestParams {
2382 category: String,
2383 symbol: String,
2384 }
2385
2386 let params = TestParams {
2387 category: "linear".to_string(),
2388 symbol: "BTCUSDT".to_string(),
2389 };
2390
2391 let path = BybitHttpInnerClient::build_path("/v5/market/test", ¶ms);
2392 assert!(path.is_ok());
2393 assert!(path.unwrap().contains("category=linear"));
2394 }
2395
2396 #[rstest]
2397 fn test_build_path_without_params() {
2398 let params = ();
2399 let path = BybitHttpInnerClient::build_path("/v5/market/time", ¶ms);
2400 assert!(path.is_ok());
2401 assert_eq!(path.unwrap(), "/v5/market/time");
2402 }
2403}