1use std::{
21 cmp::Reverse,
22 collections::HashMap,
23 fmt::{Debug, Display},
24 num::NonZeroU32,
25 sync::{
26 Arc, LazyLock,
27 atomic::{AtomicBool, Ordering},
28 },
29};
30
31use ahash::{AHashMap, AHashSet};
32use chrono::{DateTime, Utc};
33use nautilus_core::{
34 AtomicMap, AtomicTime, consts::NAUTILUS_USER_AGENT, env::get_or_env_var_opt, nanos::UnixNanos,
35 time::get_atomic_clock_realtime,
36};
37use nautilus_model::{
38 data::{Bar, BarType, FundingRateUpdate, OrderBookDeltas, TradeTick},
39 enums::{MarketStatusAction, OrderSide, OrderType, PositionSideSpecified, TimeInForce},
40 events::account::state::AccountState,
41 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
42 instruments::{Instrument, InstrumentAny},
43 reports::{FillReport, OrderStatusReport, PositionStatusReport},
44 types::{Price, Quantity},
45};
46use nautilus_network::{
47 http::{HttpClient, Method, USER_AGENT},
48 ratelimiter::quota::Quota,
49 retry::{RetryConfig, RetryManager},
50};
51use rust_decimal::Decimal;
52use serde::{Serialize, de::DeserializeOwned};
53use tokio_util::sync::CancellationToken;
54use ustr::Ustr;
55
56use super::{
57 error::BybitHttpError,
58 models::{
59 BybitAccountDetailsResponse, BybitAccountInfoResponse, BybitBorrowResponse,
60 BybitEscrowSubMembersResponse, BybitFeeRate, BybitFeeRateResponse, BybitFundingResponse,
61 BybitInstrumentInverse, BybitInstrumentInverseResponse, BybitInstrumentLinear,
62 BybitInstrumentLinearResponse, BybitInstrumentOption, BybitInstrumentOptionResponse,
63 BybitInstrumentSpot, BybitInstrumentSpotResponse, BybitKlinesResponse,
64 BybitNoConvertRepayResponse, BybitOpenOrdersResponse, BybitOrder,
65 BybitOrderHistoryResponse, BybitOrderbookResponse, BybitPlaceOrderResponse,
66 BybitPositionListResponse, BybitServerTimeResponse, BybitSetLeverageResponse,
67 BybitSetMarginModeResponse, BybitSetTradingStopResponse, BybitSubApiKeyInfo,
68 BybitSubApiKeysResponse, BybitSubMember, BybitSubMembersPagedResponse,
69 BybitSubMembersResponse, BybitSwitchModeResponse, BybitTickerData, BybitTickerOption,
70 BybitTickersOptionResponse, BybitTradeHistoryResponse, BybitTradesResponse,
71 BybitUpdateMasterApiResponse, BybitUpdateSubApiResponse, BybitWalletBalanceResponse,
72 },
73 query::{
74 BybitAmendOrderParamsBuilder, BybitBatchAmendOrderEntryBuilder,
75 BybitBatchCancelOrderEntryBuilder, BybitBatchCancelOrderParamsBuilder,
76 BybitBatchPlaceOrderEntryBuilder, BybitBorrowParamsBuilder,
77 BybitCancelAllOrdersParamsBuilder, BybitCancelOrderParamsBuilder, BybitFeeRateParams,
78 BybitFeeRateParamsBuilder, BybitFundingParams, BybitFundingParamsBuilder,
79 BybitInstrumentsInfoParams, BybitKlinesParams, BybitKlinesParamsBuilder,
80 BybitNoConvertRepayParamsBuilder, BybitOpenOrdersParamsBuilder,
81 BybitOrderHistoryParamsBuilder, BybitOrderbookParams, BybitOrderbookParamsBuilder,
82 BybitPlaceOrderParamsBuilder, BybitPositionListParams, BybitSetLeverageParamsBuilder,
83 BybitSetMarginModeParamsBuilder, BybitSetTradingStopParams, BybitSubApiKeysParams,
84 BybitSubMembersPageParams, BybitSwitchModeParamsBuilder, BybitTickersParams,
85 BybitTradeHistoryParams, BybitTradesParams, BybitTradesParamsBuilder,
86 BybitUpdateMasterApiParams, BybitUpdateSubApiParams, BybitWalletBalanceParams,
87 },
88};
89use crate::common::{
90 consts::{BYBIT_NAUTILUS_BROKER_ID, BYBIT_VENUE},
91 credential::{Credential, credential_env_vars},
92 enums::{
93 BybitAccountType, BybitContractType, BybitEnvironment, BybitMarginMode, BybitOpenOnly,
94 BybitOrderFilter, BybitOrderSide, BybitOrderType, BybitPositionIdx, BybitPositionMode,
95 BybitProductType,
96 },
97 models::{BybitCursorListResponse, BybitErrorCheck, BybitResponseCheck},
98 parse::{
99 bar_spec_to_bybit_interval, make_bybit_symbol, map_time_in_force, parse_account_state,
100 parse_fill_report, parse_funding_rate, parse_inverse_instrument, parse_kline_bar,
101 parse_linear_instrument, parse_option_instrument, parse_order_status_report,
102 parse_orderbook, parse_position_status_report, parse_spot_instrument, parse_trade_tick,
103 spot_leverage, spot_market_unit, trigger_direction,
104 },
105 symbol::BybitSymbol,
106 urls::bybit_http_base_url,
107};
108
109const DEFAULT_RECV_WINDOW_MS: u64 = 5_000;
110
111trait BuilderResultExt<T> {
112 fn build_anyhow(self) -> anyhow::Result<T>;
113}
114
115impl<T, E: Display> BuilderResultExt<T> for Result<T, E> {
116 fn build_anyhow(self) -> anyhow::Result<T> {
117 self.map_err(|e| anyhow::anyhow!("{e}"))
118 }
119}
120
121const BYBIT_ORDER_REALTIME: &str = "/v5/order/realtime";
122const BYBIT_ORDER_HISTORY: &str = "/v5/order/history";
123
124pub static BYBIT_REST_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
129 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
130});
131
132pub static BYBIT_REPAY_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
136 Quota::per_second(NonZeroU32::new(1).expect("non-zero")).expect("valid constant")
137});
138
139const BYBIT_GLOBAL_RATE_KEY: &str = "bybit:global";
140const BYBIT_REPAY_ROUTE_KEY: &str = "bybit:/v5/account/no-convert-repay";
141
142#[cfg_attr(
147 feature = "python",
148 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
149)]
150#[cfg_attr(
151 feature = "python",
152 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
153)]
154#[derive(Clone)]
155pub struct BybitRawHttpClient {
156 base_url: String,
157 client: HttpClient,
158 credential: Option<Credential>,
159 recv_window_ms: u64,
160 retry_manager: RetryManager<BybitHttpError>,
161 cancellation_token: Arc<std::sync::Mutex<CancellationToken>>,
162}
163
164impl Default for BybitRawHttpClient {
165 fn default() -> Self {
166 Self::new(None, 60, 3, 1000, 10_000, DEFAULT_RECV_WINDOW_MS, None)
167 .expect("Failed to create default BybitRawHttpClient")
168 }
169}
170
171impl Debug for BybitRawHttpClient {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 f.debug_struct(stringify!(BybitRawHttpClient))
174 .field("base_url", &self.base_url)
175 .field("has_credentials", &self.credential.is_some())
176 .field("recv_window_ms", &self.recv_window_ms)
177 .finish()
178 }
179}
180
181impl BybitRawHttpClient {
182 #[expect(clippy::missing_panics_doc, reason = "mutex poisoning is not expected")]
184 pub fn cancel_all_requests(&self) {
185 self.cancellation_token
186 .lock()
187 .expect("cancellation token lock poisoned")
188 .cancel();
189 }
190
191 #[expect(clippy::missing_panics_doc, reason = "mutex poisoning is not expected")]
194 pub fn reset_cancellation_token(&self) {
195 let mut guard = self
196 .cancellation_token
197 .lock()
198 .expect("cancellation token lock poisoned");
199 *guard = CancellationToken::new();
200 }
201
202 #[expect(clippy::missing_panics_doc, reason = "mutex poisoning is not expected")]
204 pub fn cancellation_token(&self) -> CancellationToken {
205 self.cancellation_token
206 .lock()
207 .expect("cancellation token lock poisoned")
208 .clone()
209 }
210
211 pub fn new(
217 base_url: Option<String>,
218 timeout_secs: u64,
219 max_retries: u32,
220 retry_delay_ms: u64,
221 retry_delay_max_ms: u64,
222 recv_window_ms: u64,
223 proxy_url: Option<String>,
224 ) -> Result<Self, BybitHttpError> {
225 let retry_config = RetryConfig {
226 max_retries,
227 initial_delay_ms: retry_delay_ms,
228 max_delay_ms: retry_delay_max_ms,
229 backoff_factor: 2.0,
230 jitter_ms: 1000,
231 operation_timeout_ms: Some(60_000),
232 immediate_first: false,
233 max_elapsed_ms: Some(180_000),
234 };
235
236 let retry_manager = RetryManager::new(retry_config);
237
238 Ok(Self {
239 base_url: base_url
240 .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
241 client: HttpClient::new(
242 Self::default_headers(),
243 vec![],
244 Self::rate_limiter_quotas(),
245 Some(*BYBIT_REST_QUOTA),
246 Some(timeout_secs),
247 proxy_url,
248 )
249 .map_err(|e| {
250 BybitHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
251 })?,
252 credential: None,
253 recv_window_ms,
254 retry_manager,
255 cancellation_token: Arc::new(std::sync::Mutex::new(CancellationToken::new())),
256 })
257 }
258
259 #[expect(clippy::too_many_arguments)]
265 pub fn with_credentials(
266 api_key: String,
267 api_secret: String,
268 base_url: Option<String>,
269 timeout_secs: u64,
270 max_retries: u32,
271 retry_delay_ms: u64,
272 retry_delay_max_ms: u64,
273 recv_window_ms: u64,
274 proxy_url: Option<String>,
275 ) -> Result<Self, BybitHttpError> {
276 let retry_config = RetryConfig {
277 max_retries,
278 initial_delay_ms: retry_delay_ms,
279 max_delay_ms: retry_delay_max_ms,
280 backoff_factor: 2.0,
281 jitter_ms: 1000,
282 operation_timeout_ms: Some(60_000),
283 immediate_first: false,
284 max_elapsed_ms: Some(180_000),
285 };
286
287 let retry_manager = RetryManager::new(retry_config);
288
289 Ok(Self {
290 base_url: base_url
291 .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
292 client: HttpClient::new(
293 Self::default_headers(),
294 vec![],
295 Self::rate_limiter_quotas(),
296 Some(*BYBIT_REST_QUOTA),
297 Some(timeout_secs),
298 proxy_url,
299 )
300 .map_err(|e| {
301 BybitHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
302 })?,
303 credential: Some(Credential::new(api_key, api_secret)),
304 recv_window_ms,
305 retry_manager,
306 cancellation_token: Arc::new(std::sync::Mutex::new(CancellationToken::new())),
307 })
308 }
309
310 #[expect(clippy::too_many_arguments)]
322 pub fn new_with_env(
323 api_key: Option<String>,
324 api_secret: Option<String>,
325 base_url: Option<String>,
326 demo: bool,
327 testnet: bool,
328 timeout_secs: u64,
329 max_retries: u32,
330 retry_delay_ms: u64,
331 retry_delay_max_ms: u64,
332 recv_window_ms: u64,
333 proxy_url: Option<String>,
334 ) -> Result<Self, BybitHttpError> {
335 let environment = if demo {
336 BybitEnvironment::Demo
337 } else if testnet {
338 BybitEnvironment::Testnet
339 } else {
340 BybitEnvironment::Mainnet
341 };
342 let (key_var, secret_var) = credential_env_vars(environment);
343 let key = get_or_env_var_opt(api_key, key_var);
344 let secret = get_or_env_var_opt(api_secret, secret_var);
345
346 if let (Some(k), Some(s)) = (key, secret) {
347 Self::with_credentials(
348 k,
349 s,
350 base_url,
351 timeout_secs,
352 max_retries,
353 retry_delay_ms,
354 retry_delay_max_ms,
355 recv_window_ms,
356 proxy_url,
357 )
358 } else {
359 Self::new(
360 base_url,
361 timeout_secs,
362 max_retries,
363 retry_delay_ms,
364 retry_delay_max_ms,
365 recv_window_ms,
366 proxy_url,
367 )
368 }
369 }
370
371 fn default_headers() -> HashMap<String, String> {
372 HashMap::from([
373 (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
374 (
375 "X-Referer".to_string(),
376 BYBIT_NAUTILUS_BROKER_ID.to_string(),
377 ),
378 ])
379 }
380
381 fn rate_limiter_quotas() -> Vec<(String, Quota)> {
382 vec![
383 (BYBIT_GLOBAL_RATE_KEY.to_string(), *BYBIT_REST_QUOTA),
384 (BYBIT_REPAY_ROUTE_KEY.to_string(), *BYBIT_REPAY_QUOTA),
385 ]
386 }
387
388 fn rate_limit_keys(endpoint: &str) -> Vec<String> {
389 let normalized = endpoint.split('?').next().unwrap_or(endpoint);
390 let route = format!("bybit:{normalized}");
391
392 vec![BYBIT_GLOBAL_RATE_KEY.to_string(), route]
393 }
394
395 fn sign_request(
396 &self,
397 timestamp: &str,
398 params: Option<&str>,
399 ) -> Result<HashMap<String, String>, BybitHttpError> {
400 let credential = self
401 .credential
402 .as_ref()
403 .ok_or(BybitHttpError::MissingCredentials)?;
404
405 let signature = credential.sign_with_payload(timestamp, self.recv_window_ms, params);
406
407 let mut headers = HashMap::new();
408 headers.insert(
409 "X-BAPI-API-KEY".to_string(),
410 credential.api_key().to_string(),
411 );
412 headers.insert("X-BAPI-TIMESTAMP".to_string(), timestamp.to_string());
413 headers.insert("X-BAPI-SIGN".to_string(), signature);
414 headers.insert(
415 "X-BAPI-RECV-WINDOW".to_string(),
416 self.recv_window_ms.to_string(),
417 );
418
419 Ok(headers)
420 }
421
422 async fn send_request<T: DeserializeOwned + BybitResponseCheck, P: Serialize>(
423 &self,
424 method: Method,
425 endpoint: &str,
426 params: Option<&P>,
427 body: Option<Vec<u8>>,
428 authenticate: bool,
429 ) -> Result<T, BybitHttpError> {
430 let endpoint = endpoint.to_string();
431 let url = format!("{}{endpoint}", self.base_url);
432 let method_clone = method.clone();
433 let body_clone = body.clone();
434
435 let params_str = if method == Method::GET {
437 params
438 .map(serde_urlencoded::to_string)
439 .transpose()
440 .map_err(|e| {
441 BybitHttpError::JsonError(format!("Failed to serialize params: {e}"))
442 })?
443 } else {
444 None
445 };
446
447 let operation = || {
448 let url = url.clone();
449 let method = method_clone.clone();
450 let body = body_clone.clone();
451 let endpoint = endpoint.clone();
452 let params_str = params_str.clone();
453
454 async move {
455 let mut headers = Self::default_headers();
456
457 if authenticate {
458 let timestamp = get_atomic_clock_realtime().get_time_ms().to_string();
459
460 let sign_payload = if method == Method::GET {
461 params_str.as_deref()
462 } else {
463 body.as_ref().and_then(|b| std::str::from_utf8(b).ok())
464 };
465
466 let auth_headers = self.sign_request(×tamp, sign_payload)?;
467 headers.extend(auth_headers);
468 }
469
470 if method == Method::POST || method == Method::PUT {
471 headers.insert("Content-Type".to_string(), "application/json".to_string());
472 }
473
474 let full_url = if let Some(ref query) = params_str {
475 if query.is_empty() {
476 url
477 } else {
478 format!("{url}?{query}")
479 }
480 } else {
481 url
482 };
483
484 let rate_limit_keys = Self::rate_limit_keys(&endpoint);
485
486 let response = self
487 .client
488 .request(
489 method,
490 full_url,
491 None,
492 Some(headers),
493 body,
494 None,
495 Some(rate_limit_keys),
496 )
497 .await?;
498
499 if response.status.as_u16() >= 400 {
500 let body = String::from_utf8_lossy(&response.body).to_string();
501 return Err(BybitHttpError::UnexpectedStatus {
502 status: response.status.as_u16(),
503 body,
504 });
505 }
506
507 match serde_json::from_slice::<T>(&response.body) {
509 Ok(result) => {
510 if result.ret_code() != 0 {
512 return Err(BybitHttpError::BybitError {
513 error_code: result.ret_code() as i32,
514 message: result.ret_msg().to_string(),
515 });
516 }
517 Ok(result)
518 }
519 Err(json_err) => {
520 if let Ok(error_check) =
523 serde_json::from_slice::<BybitErrorCheck>(&response.body)
524 && error_check.ret_code != 0
525 {
526 return Err(BybitHttpError::BybitError {
527 error_code: error_check.ret_code as i32,
528 message: error_check.ret_msg,
529 });
530 }
531 Err(json_err.into())
533 }
534 }
535 }
536 };
537
538 let should_retry = |error: &BybitHttpError| -> bool {
539 match error {
540 BybitHttpError::NetworkError(_) => true,
541 BybitHttpError::UnexpectedStatus { status, .. } => *status == 429 || *status >= 500,
542 _ => false,
543 }
544 };
545
546 let create_error = |msg: String| -> BybitHttpError {
547 if msg == "canceled" {
548 BybitHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
549 } else {
550 BybitHttpError::NetworkError(msg)
551 }
552 };
553
554 let token = self.cancellation_token();
555
556 self.retry_manager
557 .execute_with_retry_with_cancel(
558 endpoint.as_str(),
559 operation,
560 should_retry,
561 create_error,
562 &token,
563 )
564 .await
565 }
566
567 #[cfg(test)]
568 fn build_path<S: Serialize>(base: &str, params: &S) -> Result<String, BybitHttpError> {
569 let query = serde_urlencoded::to_string(params)
570 .map_err(|e| BybitHttpError::JsonError(e.to_string()))?;
571
572 if query.is_empty() {
573 Ok(base.to_owned())
574 } else {
575 Ok(format!("{base}?{query}"))
576 }
577 }
578
579 pub async fn get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
589 self.send_request::<_, ()>(Method::GET, "/v5/market/time", None, None, false)
590 .await
591 }
592
593 pub async fn get_instruments<T: DeserializeOwned + BybitResponseCheck>(
603 &self,
604 params: &BybitInstrumentsInfoParams,
605 ) -> Result<T, BybitHttpError> {
606 self.send_request(
607 Method::GET,
608 "/v5/market/instruments-info",
609 Some(params),
610 None,
611 false,
612 )
613 .await
614 }
615
616 pub async fn get_instruments_spot(
626 &self,
627 params: &BybitInstrumentsInfoParams,
628 ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
629 self.get_instruments(params).await
630 }
631
632 pub async fn get_instruments_linear(
642 &self,
643 params: &BybitInstrumentsInfoParams,
644 ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
645 self.get_instruments(params).await
646 }
647
648 pub async fn get_instruments_inverse(
658 &self,
659 params: &BybitInstrumentsInfoParams,
660 ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
661 self.get_instruments(params).await
662 }
663
664 pub async fn get_instruments_option(
674 &self,
675 params: &BybitInstrumentsInfoParams,
676 ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
677 self.get_instruments(params).await
678 }
679
680 pub async fn get_klines(
690 &self,
691 params: &BybitKlinesParams,
692 ) -> Result<BybitKlinesResponse, BybitHttpError> {
693 self.send_request(Method::GET, "/v5/market/kline", Some(params), None, false)
694 .await
695 }
696
697 pub async fn get_recent_trades(
707 &self,
708 params: &BybitTradesParams,
709 ) -> Result<BybitTradesResponse, BybitHttpError> {
710 self.send_request(
711 Method::GET,
712 "/v5/market/recent-trade",
713 Some(params),
714 None,
715 false,
716 )
717 .await
718 }
719
720 pub async fn get_funding_history(
730 &self,
731 params: &BybitFundingParams,
732 ) -> Result<BybitFundingResponse, BybitHttpError> {
733 self.send_request(
734 Method::GET,
735 "/v5/market/funding/history",
736 Some(params),
737 None,
738 false,
739 )
740 .await
741 }
742
743 pub async fn get_orderbook(
753 &self,
754 params: &BybitOrderbookParams,
755 ) -> Result<BybitOrderbookResponse, BybitHttpError> {
756 self.send_request(
757 Method::GET,
758 "/v5/market/orderbook",
759 Some(params),
760 None,
761 false,
762 )
763 .await
764 }
765
766 #[expect(clippy::too_many_arguments)]
780 pub async fn get_open_orders(
781 &self,
782 category: BybitProductType,
783 symbol: Option<String>,
784 base_coin: Option<String>,
785 settle_coin: Option<String>,
786 order_id: Option<String>,
787 order_link_id: Option<String>,
788 open_only: Option<BybitOpenOnly>,
789 order_filter: Option<BybitOrderFilter>,
790 limit: Option<u32>,
791 cursor: Option<String>,
792 ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
793 let mut builder = BybitOpenOrdersParamsBuilder::default();
794 builder.category(category);
795
796 if let Some(s) = symbol {
797 builder.symbol(s);
798 }
799
800 if let Some(bc) = base_coin {
801 builder.base_coin(bc);
802 }
803
804 if let Some(sc) = settle_coin {
805 builder.settle_coin(sc);
806 }
807
808 if let Some(oi) = order_id {
809 builder.order_id(oi);
810 }
811
812 if let Some(ol) = order_link_id {
813 builder.order_link_id(ol);
814 }
815
816 if let Some(oo) = open_only {
817 builder.open_only(oo);
818 }
819
820 if let Some(of) = order_filter {
821 builder.order_filter(of);
822 }
823
824 if let Some(l) = limit {
825 builder.limit(l);
826 }
827
828 if let Some(c) = cursor {
829 builder.cursor(c);
830 }
831
832 let params = builder
833 .build()
834 .expect("Failed to build BybitOpenOrdersParams");
835
836 self.send_request(Method::GET, BYBIT_ORDER_REALTIME, Some(¶ms), None, true)
837 .await
838 }
839
840 pub async fn place_order(
850 &self,
851 request: &serde_json::Value,
852 ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
853 let body = serde_json::to_vec(request)?;
854 self.send_request::<_, ()>(Method::POST, "/v5/order/create", None, Some(body), true)
855 .await
856 }
857
858 pub async fn get_wallet_balance(
868 &self,
869 params: &BybitWalletBalanceParams,
870 ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
871 self.send_request(
872 Method::GET,
873 "/v5/account/wallet-balance",
874 Some(params),
875 None,
876 true,
877 )
878 .await
879 }
880
881 pub async fn get_account_info(&self) -> Result<BybitAccountInfoResponse, BybitHttpError> {
891 self.send_request::<_, ()>(Method::GET, "/v5/account/info", None, None, true)
892 .await
893 }
894
895 pub async fn get_account_details(&self) -> Result<BybitAccountDetailsResponse, BybitHttpError> {
905 self.send_request::<_, ()>(Method::GET, "/v5/user/query-api", None, None, true)
906 .await
907 }
908
909 pub async fn update_sub_api_key(
919 &self,
920 params: &BybitUpdateSubApiParams,
921 ) -> Result<BybitUpdateSubApiResponse, BybitHttpError> {
922 let body = serde_json::to_vec(params)?;
923 self.send_request::<_, ()>(
924 Method::POST,
925 "/v5/user/update-sub-api",
926 None,
927 Some(body),
928 true,
929 )
930 .await
931 }
932
933 pub async fn update_master_api_key(
943 &self,
944 params: &BybitUpdateMasterApiParams,
945 ) -> Result<BybitUpdateMasterApiResponse, BybitHttpError> {
946 let body = serde_json::to_vec(params)?;
947 self.send_request::<_, ()>(Method::POST, "/v5/user/update-api", None, Some(body), true)
948 .await
949 }
950
951 pub async fn get_sub_members(&self) -> Result<BybitSubMembersResponse, BybitHttpError> {
961 self.send_request::<_, ()>(Method::GET, "/v5/user/query-sub-members", None, None, true)
962 .await
963 }
964
965 pub async fn get_sub_members_paged(
975 &self,
976 params: &BybitSubMembersPageParams,
977 ) -> Result<BybitSubMembersPagedResponse, BybitHttpError> {
978 self.send_request(Method::GET, "/v5/user/submembers", Some(params), None, true)
979 .await
980 }
981
982 pub async fn get_escrow_sub_members(
992 &self,
993 params: &BybitSubMembersPageParams,
994 ) -> Result<BybitEscrowSubMembersResponse, BybitHttpError> {
995 self.send_request(
996 Method::GET,
997 "/v5/user/escrow_sub_members",
998 Some(params),
999 None,
1000 true,
1001 )
1002 .await
1003 }
1004
1005 pub async fn get_sub_api_keys(
1015 &self,
1016 params: &BybitSubApiKeysParams,
1017 ) -> Result<BybitSubApiKeysResponse, BybitHttpError> {
1018 self.send_request(
1019 Method::GET,
1020 "/v5/user/sub-apikeys",
1021 Some(params),
1022 None,
1023 true,
1024 )
1025 .await
1026 }
1027
1028 pub async fn fetch_all_sub_members_paged(
1035 &self,
1036 page_size: Option<u32>,
1037 ) -> Result<Vec<BybitSubMember>, BybitHttpError> {
1038 let mut members = Vec::new();
1039 let mut cursor: Option<String> = None;
1040
1041 loop {
1042 let params = BybitSubMembersPageParams {
1043 page_size,
1044 next_cursor: cursor.take(),
1045 };
1046 let mut page = self.get_sub_members_paged(¶ms).await?;
1047 let next = page.result.continuation_cursor().map(str::to_owned);
1048 members.append(&mut page.result.sub_members);
1049
1050 match next {
1051 Some(c) => cursor = Some(c),
1052 None => break,
1053 }
1054 }
1055
1056 Ok(members)
1057 }
1058
1059 pub async fn fetch_all_escrow_sub_members(
1067 &self,
1068 page_size: Option<u32>,
1069 ) -> Result<Vec<BybitSubMember>, BybitHttpError> {
1070 let mut members = Vec::new();
1071 let mut cursor: Option<String> = None;
1072
1073 loop {
1074 let params = BybitSubMembersPageParams {
1075 page_size,
1076 next_cursor: cursor.take(),
1077 };
1078 let mut page = self.get_escrow_sub_members(¶ms).await?;
1079 let next = page.result.continuation_cursor().map(str::to_owned);
1080 members.append(&mut page.result.sub_members);
1081
1082 match next {
1083 Some(c) => cursor = Some(c),
1084 None => break,
1085 }
1086 }
1087
1088 Ok(members)
1089 }
1090
1091 pub async fn fetch_all_sub_api_keys(
1099 &self,
1100 sub_member_id: impl Into<String>,
1101 limit: Option<u32>,
1102 ) -> Result<Vec<BybitSubApiKeyInfo>, BybitHttpError> {
1103 let sub_member_id = sub_member_id.into();
1104 let mut keys = Vec::new();
1105 let mut cursor: Option<String> = None;
1106
1107 loop {
1108 let params = BybitSubApiKeysParams {
1109 sub_member_id: sub_member_id.clone(),
1110 limit,
1111 cursor: cursor.take(),
1112 };
1113 let mut page = self.get_sub_api_keys(¶ms).await?;
1114 let next = page.result.continuation_cursor().map(str::to_owned);
1115 keys.append(&mut page.result.keys);
1116
1117 match next {
1118 Some(c) => cursor = Some(c),
1119 None => break,
1120 }
1121 }
1122
1123 Ok(keys)
1124 }
1125
1126 pub async fn get_fee_rate(
1136 &self,
1137 params: &BybitFeeRateParams,
1138 ) -> Result<BybitFeeRateResponse, BybitHttpError> {
1139 self.send_request(
1140 Method::GET,
1141 "/v5/account/fee-rate",
1142 Some(params),
1143 None,
1144 true,
1145 )
1146 .await
1147 }
1148
1149 pub async fn set_margin_mode(
1166 &self,
1167 margin_mode: BybitMarginMode,
1168 ) -> Result<BybitSetMarginModeResponse, BybitHttpError> {
1169 let params = BybitSetMarginModeParamsBuilder::default()
1170 .set_margin_mode(margin_mode)
1171 .build()
1172 .expect("Failed to build BybitSetMarginModeParams");
1173
1174 let body = serde_json::to_vec(¶ms)?;
1175 self.send_request::<_, ()>(
1176 Method::POST,
1177 "/v5/account/set-margin-mode",
1178 None,
1179 Some(body),
1180 true,
1181 )
1182 .await
1183 }
1184
1185 pub async fn set_leverage(
1202 &self,
1203 product_type: BybitProductType,
1204 symbol: &str,
1205 buy_leverage: &str,
1206 sell_leverage: &str,
1207 ) -> Result<BybitSetLeverageResponse, BybitHttpError> {
1208 let params = BybitSetLeverageParamsBuilder::default()
1209 .category(product_type)
1210 .symbol(symbol.to_string())
1211 .buy_leverage(buy_leverage.to_string())
1212 .sell_leverage(sell_leverage.to_string())
1213 .build()
1214 .expect("Failed to build BybitSetLeverageParams");
1215
1216 let body = serde_json::to_vec(¶ms)?;
1217 self.send_request::<_, ()>(
1218 Method::POST,
1219 "/v5/position/set-leverage",
1220 None,
1221 Some(body),
1222 true,
1223 )
1224 .await
1225 }
1226
1227 pub async fn switch_mode(
1244 &self,
1245 product_type: BybitProductType,
1246 mode: BybitPositionMode,
1247 symbol: Option<String>,
1248 coin: Option<String>,
1249 ) -> Result<BybitSwitchModeResponse, BybitHttpError> {
1250 let mut builder = BybitSwitchModeParamsBuilder::default();
1251 builder.category(product_type);
1252 builder.mode(mode);
1253
1254 if let Some(s) = symbol {
1255 builder.symbol(s);
1256 }
1257
1258 if let Some(c) = coin {
1259 builder.coin(c);
1260 }
1261
1262 let params = builder
1263 .build()
1264 .expect("Failed to build BybitSwitchModeParams");
1265
1266 let body = serde_json::to_vec(¶ms)?;
1267 self.send_request::<_, ()>(
1268 Method::POST,
1269 "/v5/position/switch-mode",
1270 None,
1271 Some(body),
1272 true,
1273 )
1274 .await
1275 }
1276
1277 pub async fn set_trading_stop(
1290 &self,
1291 params: &BybitSetTradingStopParams,
1292 ) -> Result<BybitSetTradingStopResponse, BybitHttpError> {
1293 let body = serde_json::to_vec(params)?;
1294 self.send_request::<_, ()>(
1295 Method::POST,
1296 "/v5/position/trading-stop",
1297 None,
1298 Some(body),
1299 true,
1300 )
1301 .await
1302 }
1303
1304 pub async fn borrow(
1321 &self,
1322 coin: &str,
1323 amount: &str,
1324 ) -> Result<BybitBorrowResponse, BybitHttpError> {
1325 let params = BybitBorrowParamsBuilder::default()
1326 .coin(coin.to_string())
1327 .amount(amount.to_string())
1328 .build()
1329 .expect("Failed to build BybitBorrowParams");
1330
1331 let body = serde_json::to_vec(¶ms)?;
1332 self.send_request::<_, ()>(Method::POST, "/v5/account/borrow", None, Some(body), true)
1333 .await
1334 }
1335
1336 pub async fn no_convert_repay(
1354 &self,
1355 coin: &str,
1356 amount: Option<&str>,
1357 ) -> Result<BybitNoConvertRepayResponse, BybitHttpError> {
1358 let mut builder = BybitNoConvertRepayParamsBuilder::default();
1359 builder.coin(coin.to_string());
1360
1361 if let Some(amt) = amount {
1362 builder.amount(amt.to_string());
1363 }
1364
1365 let params = builder
1366 .build()
1367 .expect("Failed to build BybitNoConvertRepayParams");
1368
1369 if let Ok(params_json) = serde_json::to_string(¶ms) {
1370 log::debug!("Repay request params: {params_json}");
1371 }
1372
1373 let body = serde_json::to_vec(¶ms)?;
1374 let result = self
1375 .send_request::<_, ()>(
1376 Method::POST,
1377 "/v5/account/no-convert-repay",
1378 None,
1379 Some(body),
1380 true,
1381 )
1382 .await;
1383
1384 if let Err(ref e) = result
1385 && let Ok(params_json) = serde_json::to_string(¶ms)
1386 {
1387 log::error!("Repay request failed with params {params_json}: {e}");
1388 }
1389
1390 result
1391 }
1392
1393 pub async fn get_tickers<T: DeserializeOwned + BybitResponseCheck>(
1403 &self,
1404 params: &BybitTickersParams,
1405 ) -> Result<T, BybitHttpError> {
1406 self.send_request(Method::GET, "/v5/market/tickers", Some(params), None, false)
1407 .await
1408 }
1409
1410 pub async fn get_trade_history(
1420 &self,
1421 params: &BybitTradeHistoryParams,
1422 ) -> Result<BybitTradeHistoryResponse, BybitHttpError> {
1423 self.send_request(Method::GET, "/v5/execution/list", Some(params), None, true)
1424 .await
1425 }
1426
1427 pub async fn get_positions(
1440 &self,
1441 params: &BybitPositionListParams,
1442 ) -> Result<BybitPositionListResponse, BybitHttpError> {
1443 self.send_request(Method::GET, "/v5/position/list", Some(params), None, true)
1444 .await
1445 }
1446
1447 #[must_use]
1449 pub fn base_url(&self) -> &str {
1450 &self.base_url
1451 }
1452
1453 #[must_use]
1455 pub fn recv_window_ms(&self) -> u64 {
1456 self.recv_window_ms
1457 }
1458
1459 #[must_use]
1461 pub fn credential(&self) -> Option<&Credential> {
1462 self.credential.as_ref()
1463 }
1464}
1465
1466#[cfg_attr(
1468 feature = "python",
1469 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
1470)]
1471#[cfg_attr(
1472 feature = "python",
1473 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
1474)]
1475pub struct BybitHttpClient {
1480 pub(crate) inner: Arc<BybitRawHttpClient>,
1481 pub(crate) instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
1482 clock: &'static AtomicTime,
1483 cache_initialized: Arc<AtomicBool>,
1484 use_spot_position_reports: Arc<AtomicBool>,
1485}
1486
1487impl Clone for BybitHttpClient {
1488 fn clone(&self) -> Self {
1489 Self {
1490 inner: self.inner.clone(),
1491 instruments_cache: self.instruments_cache.clone(),
1492 cache_initialized: self.cache_initialized.clone(),
1493 use_spot_position_reports: self.use_spot_position_reports.clone(),
1494 clock: self.clock,
1495 }
1496 }
1497}
1498
1499impl Default for BybitHttpClient {
1500 fn default() -> Self {
1501 Self::new(None, 60, 3, 1000, 10_000, DEFAULT_RECV_WINDOW_MS, None)
1502 .expect("Failed to create default BybitHttpClient")
1503 }
1504}
1505
1506impl Debug for BybitHttpClient {
1507 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1508 f.debug_struct(stringify!(BybitHttpClient))
1509 .field("inner", &self.inner)
1510 .finish()
1511 }
1512}
1513
1514impl BybitHttpClient {
1515 pub fn new(
1521 base_url: Option<String>,
1522 timeout_secs: u64,
1523 max_retries: u32,
1524 retry_delay_ms: u64,
1525 retry_delay_max_ms: u64,
1526 recv_window_ms: u64,
1527 proxy_url: Option<String>,
1528 ) -> Result<Self, BybitHttpError> {
1529 Ok(Self {
1530 inner: Arc::new(BybitRawHttpClient::new(
1531 base_url,
1532 timeout_secs,
1533 max_retries,
1534 retry_delay_ms,
1535 retry_delay_max_ms,
1536 recv_window_ms,
1537 proxy_url,
1538 )?),
1539 instruments_cache: Arc::new(AtomicMap::new()),
1540 cache_initialized: Arc::new(AtomicBool::new(false)),
1541 use_spot_position_reports: Arc::new(AtomicBool::new(false)),
1542 clock: get_atomic_clock_realtime(),
1543 })
1544 }
1545
1546 #[expect(clippy::too_many_arguments)]
1552 pub fn with_credentials(
1553 api_key: String,
1554 api_secret: String,
1555 base_url: Option<String>,
1556 timeout_secs: u64,
1557 max_retries: u32,
1558 retry_delay_ms: u64,
1559 retry_delay_max_ms: u64,
1560 recv_window_ms: u64,
1561 proxy_url: Option<String>,
1562 ) -> Result<Self, BybitHttpError> {
1563 Ok(Self {
1564 inner: Arc::new(BybitRawHttpClient::with_credentials(
1565 api_key,
1566 api_secret,
1567 base_url,
1568 timeout_secs,
1569 max_retries,
1570 retry_delay_ms,
1571 retry_delay_max_ms,
1572 recv_window_ms,
1573 proxy_url,
1574 )?),
1575 instruments_cache: Arc::new(AtomicMap::new()),
1576 cache_initialized: Arc::new(AtomicBool::new(false)),
1577 use_spot_position_reports: Arc::new(AtomicBool::new(false)),
1578 clock: get_atomic_clock_realtime(),
1579 })
1580 }
1581
1582 #[expect(clippy::too_many_arguments)]
1595 pub fn new_with_env(
1596 api_key: Option<String>,
1597 api_secret: Option<String>,
1598 base_url: Option<String>,
1599 demo: bool,
1600 testnet: bool,
1601 timeout_secs: u64,
1602 max_retries: u32,
1603 retry_delay_ms: u64,
1604 retry_delay_max_ms: u64,
1605 recv_window_ms: u64,
1606 proxy_url: Option<String>,
1607 ) -> Result<Self, BybitHttpError> {
1608 let environment = if demo {
1609 BybitEnvironment::Demo
1610 } else if testnet {
1611 BybitEnvironment::Testnet
1612 } else {
1613 BybitEnvironment::Mainnet
1614 };
1615 let (key_var, secret_var) = credential_env_vars(environment);
1616 let key = get_or_env_var_opt(api_key, key_var);
1617 let secret = get_or_env_var_opt(api_secret, secret_var);
1618
1619 match (key, secret) {
1620 (Some(k), Some(s)) => Self::with_credentials(
1621 k,
1622 s,
1623 base_url,
1624 timeout_secs,
1625 max_retries,
1626 retry_delay_ms,
1627 retry_delay_max_ms,
1628 recv_window_ms,
1629 proxy_url,
1630 ),
1631 _ => Self::new(
1632 base_url,
1633 timeout_secs,
1634 max_retries,
1635 retry_delay_ms,
1636 retry_delay_max_ms,
1637 recv_window_ms,
1638 proxy_url,
1639 ),
1640 }
1641 }
1642
1643 #[must_use]
1644 pub fn base_url(&self) -> &str {
1645 self.inner.base_url()
1646 }
1647
1648 #[must_use]
1649 pub fn recv_window_ms(&self) -> u64 {
1650 self.inner.recv_window_ms()
1651 }
1652
1653 #[must_use]
1654 pub fn credential(&self) -> Option<&Credential> {
1655 self.inner.credential()
1656 }
1657
1658 pub fn set_use_spot_position_reports(&self, use_spot_position_reports: bool) {
1659 self.use_spot_position_reports
1660 .store(use_spot_position_reports, Ordering::Relaxed);
1661 }
1662
1663 pub fn cancel_all_requests(&self) {
1664 self.inner.cancel_all_requests();
1665 }
1666
1667 pub fn reset_cancellation_token(&self) {
1668 self.inner.reset_cancellation_token();
1669 }
1670
1671 pub fn cancellation_token(&self) -> CancellationToken {
1672 self.inner.cancellation_token()
1673 }
1674
1675 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1677 self.instruments_cache
1678 .insert(instrument.symbol().inner(), instrument);
1679 self.cache_initialized.store(true, Ordering::Release);
1680 }
1681
1682 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
1684 self.instruments_cache.rcu(|m| {
1685 for instrument in instruments {
1686 m.insert(instrument.symbol().inner(), instrument.clone());
1687 }
1688 });
1689 self.cache_initialized.store(true, Ordering::Release);
1690 }
1691
1692 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1693 self.instruments_cache.get_cloned(symbol)
1694 }
1695
1696 fn instrument_from_cache(&self, symbol: &Symbol) -> anyhow::Result<InstrumentAny> {
1697 self.get_instrument(&symbol.inner()).ok_or_else(|| {
1698 anyhow::anyhow!(
1699 "Instrument {symbol} not found in cache, ensure instruments loaded first"
1700 )
1701 })
1702 }
1703
1704 #[must_use]
1705 fn generate_ts_init(&self) -> UnixNanos {
1706 self.clock.get_time_ns()
1707 }
1708
1709 pub async fn get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
1721 self.inner.get_server_time().await
1722 }
1723
1724 pub async fn get_instruments<T: DeserializeOwned + BybitResponseCheck>(
1736 &self,
1737 params: &BybitInstrumentsInfoParams,
1738 ) -> Result<T, BybitHttpError> {
1739 self.inner.get_instruments(params).await
1740 }
1741
1742 pub async fn get_instruments_spot(
1754 &self,
1755 params: &BybitInstrumentsInfoParams,
1756 ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
1757 self.inner.get_instruments_spot(params).await
1758 }
1759
1760 pub async fn get_instruments_linear(
1772 &self,
1773 params: &BybitInstrumentsInfoParams,
1774 ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
1775 self.inner.get_instruments_linear(params).await
1776 }
1777
1778 pub async fn get_instruments_inverse(
1790 &self,
1791 params: &BybitInstrumentsInfoParams,
1792 ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
1793 self.inner.get_instruments_inverse(params).await
1794 }
1795
1796 pub async fn get_instruments_option(
1808 &self,
1809 params: &BybitInstrumentsInfoParams,
1810 ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
1811 self.inner.get_instruments_option(params).await
1812 }
1813
1814 pub async fn get_klines(
1826 &self,
1827 params: &BybitKlinesParams,
1828 ) -> Result<BybitKlinesResponse, BybitHttpError> {
1829 self.inner.get_klines(params).await
1830 }
1831
1832 pub async fn get_recent_trades(
1844 &self,
1845 params: &BybitTradesParams,
1846 ) -> Result<BybitTradesResponse, BybitHttpError> {
1847 self.inner.get_recent_trades(params).await
1848 }
1849
1850 #[expect(clippy::too_many_arguments)]
1862 pub async fn get_open_orders(
1863 &self,
1864 category: BybitProductType,
1865 symbol: Option<String>,
1866 base_coin: Option<String>,
1867 settle_coin: Option<String>,
1868 order_id: Option<String>,
1869 order_link_id: Option<String>,
1870 open_only: Option<BybitOpenOnly>,
1871 order_filter: Option<BybitOrderFilter>,
1872 limit: Option<u32>,
1873 cursor: Option<String>,
1874 ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
1875 self.inner
1876 .get_open_orders(
1877 category,
1878 symbol,
1879 base_coin,
1880 settle_coin,
1881 order_id,
1882 order_link_id,
1883 open_only,
1884 order_filter,
1885 limit,
1886 cursor,
1887 )
1888 .await
1889 }
1890
1891 pub async fn place_order(
1903 &self,
1904 request: &serde_json::Value,
1905 ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
1906 self.inner.place_order(request).await
1907 }
1908
1909 pub async fn get_wallet_balance(
1921 &self,
1922 params: &BybitWalletBalanceParams,
1923 ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
1924 self.inner.get_wallet_balance(params).await
1925 }
1926
1927 pub async fn get_account_info(&self) -> Result<BybitAccountInfoResponse, BybitHttpError> {
1939 self.inner.get_account_info().await
1940 }
1941
1942 pub async fn get_account_details(&self) -> Result<BybitAccountDetailsResponse, BybitHttpError> {
1954 self.inner.get_account_details().await
1955 }
1956
1957 pub async fn update_sub_api_key(
1969 &self,
1970 params: &BybitUpdateSubApiParams,
1971 ) -> Result<BybitUpdateSubApiResponse, BybitHttpError> {
1972 self.inner.update_sub_api_key(params).await
1973 }
1974
1975 pub async fn update_master_api_key(
1987 &self,
1988 params: &BybitUpdateMasterApiParams,
1989 ) -> Result<BybitUpdateMasterApiResponse, BybitHttpError> {
1990 self.inner.update_master_api_key(params).await
1991 }
1992
1993 pub async fn get_sub_members(&self) -> Result<BybitSubMembersResponse, BybitHttpError> {
2005 self.inner.get_sub_members().await
2006 }
2007
2008 pub async fn get_sub_members_paged(
2020 &self,
2021 params: &BybitSubMembersPageParams,
2022 ) -> Result<BybitSubMembersPagedResponse, BybitHttpError> {
2023 self.inner.get_sub_members_paged(params).await
2024 }
2025
2026 pub async fn get_escrow_sub_members(
2038 &self,
2039 params: &BybitSubMembersPageParams,
2040 ) -> Result<BybitEscrowSubMembersResponse, BybitHttpError> {
2041 self.inner.get_escrow_sub_members(params).await
2042 }
2043
2044 pub async fn get_sub_api_keys(
2056 &self,
2057 params: &BybitSubApiKeysParams,
2058 ) -> Result<BybitSubApiKeysResponse, BybitHttpError> {
2059 self.inner.get_sub_api_keys(params).await
2060 }
2061
2062 pub async fn get_positions(
2075 &self,
2076 params: &BybitPositionListParams,
2077 ) -> Result<BybitPositionListResponse, BybitHttpError> {
2078 self.inner.get_positions(params).await
2079 }
2080
2081 pub async fn get_fee_rate(
2094 &self,
2095 params: &BybitFeeRateParams,
2096 ) -> Result<BybitFeeRateResponse, BybitHttpError> {
2097 self.inner.get_fee_rate(params).await
2098 }
2099
2100 pub async fn set_margin_mode(
2113 &self,
2114 margin_mode: BybitMarginMode,
2115 ) -> Result<BybitSetMarginModeResponse, BybitHttpError> {
2116 self.inner.set_margin_mode(margin_mode).await
2117 }
2118
2119 pub async fn set_leverage(
2132 &self,
2133 product_type: BybitProductType,
2134 symbol: &str,
2135 buy_leverage: &str,
2136 sell_leverage: &str,
2137 ) -> Result<BybitSetLeverageResponse, BybitHttpError> {
2138 self.inner
2139 .set_leverage(product_type, symbol, buy_leverage, sell_leverage)
2140 .await
2141 }
2142
2143 pub async fn switch_mode(
2156 &self,
2157 product_type: BybitProductType,
2158 mode: BybitPositionMode,
2159 symbol: Option<String>,
2160 coin: Option<String>,
2161 ) -> Result<BybitSwitchModeResponse, BybitHttpError> {
2162 self.inner
2163 .switch_mode(product_type, mode, symbol, coin)
2164 .await
2165 }
2166
2167 pub async fn set_trading_stop(
2180 &self,
2181 params: &BybitSetTradingStopParams,
2182 ) -> Result<BybitSetTradingStopResponse, BybitHttpError> {
2183 self.inner.set_trading_stop(params).await
2184 }
2185
2186 pub async fn get_spot_borrow_amount(&self, coin: &str) -> anyhow::Result<Decimal> {
2201 let params = BybitWalletBalanceParams {
2202 account_type: BybitAccountType::Unified,
2203 coin: Some(coin.to_string()),
2204 };
2205
2206 let response = self.inner.get_wallet_balance(¶ms).await?;
2207
2208 let borrow_amount = response
2209 .result
2210 .list
2211 .first()
2212 .and_then(|wallet| wallet.coin.iter().find(|c| c.coin.as_str() == coin))
2213 .map_or(Decimal::ZERO, |balance| balance.spot_borrow);
2214
2215 Ok(borrow_amount)
2216 }
2217
2218 pub async fn borrow_spot(
2234 &self,
2235 coin: &str,
2236 amount: Quantity,
2237 ) -> anyhow::Result<BybitBorrowResponse> {
2238 let amount_str = amount.to_string();
2239 self.inner
2240 .borrow(coin, &amount_str)
2241 .await
2242 .map_err(|e| anyhow::anyhow!("Failed to borrow {amount} {coin}: {e}"))
2243 }
2244
2245 pub async fn repay_spot_borrow(
2262 &self,
2263 coin: &str,
2264 amount: Option<Quantity>,
2265 ) -> anyhow::Result<BybitNoConvertRepayResponse> {
2266 let amount_str = amount.as_ref().map(|q| q.to_string());
2267 self.inner
2268 .no_convert_repay(coin, amount_str.as_deref())
2269 .await
2270 .map_err(|e| anyhow::anyhow!("Failed to repay spot borrow for {coin}: {e}"))
2271 }
2272
2273 async fn generate_spot_position_reports_from_wallet(
2281 &self,
2282 account_id: AccountId,
2283 instrument_id: Option<InstrumentId>,
2284 ) -> anyhow::Result<Vec<PositionStatusReport>> {
2285 let params = BybitWalletBalanceParams {
2286 account_type: BybitAccountType::Unified,
2287 coin: None,
2288 };
2289
2290 let response = self.inner.get_wallet_balance(¶ms).await?;
2291 let ts_init = self.generate_ts_init();
2292
2293 let mut wallet_by_coin: HashMap<Ustr, Decimal> = HashMap::new();
2294
2295 for wallet in &response.result.list {
2296 for coin_balance in &wallet.coin {
2297 let balance = coin_balance.wallet_balance - coin_balance.spot_borrow;
2298 *wallet_by_coin
2299 .entry(coin_balance.coin)
2300 .or_insert(Decimal::ZERO) += balance;
2301 }
2302 }
2303
2304 let mut reports = Vec::new();
2305
2306 if let Some(instrument_id) = instrument_id {
2307 if let Some(instrument) = self
2308 .instruments_cache
2309 .get_cloned(&instrument_id.symbol.inner())
2310 {
2311 let base_currency = instrument
2312 .base_currency()
2313 .expect("SPOT instrument should have base currency");
2314 let coin = base_currency.code;
2315 let wallet_balance = wallet_by_coin.get(&coin).copied().unwrap_or(Decimal::ZERO);
2316
2317 let side = if wallet_balance > Decimal::ZERO {
2318 PositionSideSpecified::Long
2319 } else if wallet_balance < Decimal::ZERO {
2320 PositionSideSpecified::Short
2321 } else {
2322 PositionSideSpecified::Flat
2323 };
2324
2325 let abs_balance = wallet_balance.abs();
2326 let quantity = Quantity::from_decimal_dp(abs_balance, instrument.size_precision())?;
2327
2328 let report = PositionStatusReport::new(
2329 account_id,
2330 instrument_id,
2331 side,
2332 quantity,
2333 ts_init,
2334 ts_init,
2335 None,
2336 None,
2337 None,
2338 );
2339
2340 reports.push(report);
2341 }
2342 } else {
2343 let instruments_guard = self.instruments_cache.load();
2345 for (symbol, instrument) in instruments_guard.iter() {
2346 if !symbol.as_str().ends_with("-SPOT") {
2348 continue;
2349 }
2350
2351 let base_currency = match instrument.base_currency() {
2352 Some(currency) => currency,
2353 None => continue,
2354 };
2355
2356 let coin = base_currency.code;
2357 let wallet_balance = wallet_by_coin.get(&coin).copied().unwrap_or(Decimal::ZERO);
2358
2359 if wallet_balance.is_zero() {
2360 continue;
2361 }
2362
2363 let side = if wallet_balance > Decimal::ZERO {
2364 PositionSideSpecified::Long
2365 } else if wallet_balance < Decimal::ZERO {
2366 PositionSideSpecified::Short
2367 } else {
2368 PositionSideSpecified::Flat
2369 };
2370
2371 let abs_balance = wallet_balance.abs();
2372 let quantity = Quantity::from_decimal_dp(abs_balance, instrument.size_precision())?;
2373
2374 if quantity.is_zero() {
2375 continue;
2376 }
2377
2378 let report = PositionStatusReport::new(
2379 account_id,
2380 instrument.id(),
2381 side,
2382 quantity,
2383 ts_init,
2384 ts_init,
2385 None,
2386 None,
2387 None,
2388 );
2389
2390 reports.push(report);
2391 }
2392 }
2393
2394 Ok(reports)
2395 }
2396
2397 #[expect(clippy::too_many_arguments)]
2408 pub async fn submit_order(
2409 &self,
2410 account_id: AccountId,
2411 product_type: BybitProductType,
2412 instrument_id: InstrumentId,
2413 client_order_id: ClientOrderId,
2414 order_side: OrderSide,
2415 order_type: OrderType,
2416 quantity: Quantity,
2417 time_in_force: Option<TimeInForce>,
2418 price: Option<Price>,
2419 trigger_price: Option<Price>,
2420 post_only: Option<bool>,
2421 reduce_only: bool,
2422 is_quote_quantity: bool,
2423 is_leverage: bool,
2424 position_idx: Option<BybitPositionIdx>,
2425 ) -> anyhow::Result<OrderStatusReport> {
2426 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2427 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2428
2429 let bybit_side = match order_side {
2430 OrderSide::Buy => BybitOrderSide::Buy,
2431 OrderSide::Sell => BybitOrderSide::Sell,
2432 _ => anyhow::bail!("Invalid order side: {order_side:?}"),
2433 };
2434
2435 let (bybit_order_type, is_stop_order) = match order_type {
2437 OrderType::Market => (BybitOrderType::Market, false),
2438 OrderType::Limit => (BybitOrderType::Limit, false),
2439 OrderType::StopMarket | OrderType::MarketIfTouched => (BybitOrderType::Market, true),
2440 OrderType::StopLimit | OrderType::LimitIfTouched => (BybitOrderType::Limit, true),
2441 _ => anyhow::bail!("Unsupported order type: {order_type:?}"),
2442 };
2443
2444 let bybit_tif = map_time_in_force(bybit_order_type, time_in_force, post_only)
2445 .map_err(|tif| anyhow::anyhow!("Unsupported time in force: {tif:?}"))?;
2446 let market_unit = spot_market_unit(product_type, bybit_order_type, is_quote_quantity);
2447 let trigger_dir = trigger_direction(order_type, order_side, is_stop_order);
2448
2449 let mut order_entry = BybitBatchPlaceOrderEntryBuilder::default();
2450 order_entry.symbol(bybit_symbol.raw_symbol().to_string());
2451 order_entry.side(bybit_side);
2452 order_entry.order_type(bybit_order_type);
2453 order_entry.qty(quantity.to_string());
2454 order_entry.time_in_force(bybit_tif);
2455 order_entry.order_link_id(client_order_id.to_string());
2456 order_entry.market_unit(market_unit);
2457 order_entry.trigger_direction(trigger_dir);
2458
2459 if let Some(price) = price {
2460 order_entry.price(Some(price.to_string()));
2461 }
2462
2463 if let Some(trigger_price) = trigger_price {
2464 order_entry.trigger_price(Some(trigger_price.to_string()));
2465 }
2466
2467 if reduce_only {
2468 order_entry.reduce_only(Some(true));
2469 }
2470
2471 order_entry.is_leverage(spot_leverage(product_type, is_leverage));
2472
2473 if let Some(idx) = position_idx {
2474 order_entry.position_idx(Some(idx));
2475 }
2476
2477 let order_entry = order_entry.build().build_anyhow()?;
2478
2479 let mut params = BybitPlaceOrderParamsBuilder::default();
2480 params.category(product_type);
2481 params.order(order_entry);
2482
2483 let params = params.build().build_anyhow()?;
2484
2485 let body = serde_json::to_value(¶ms)?;
2486 let response = self.inner.place_order(&body).await?;
2487
2488 let order_id = response
2489 .result
2490 .order_id
2491 .ok_or_else(|| anyhow::anyhow!("No order_id in response"))?;
2492
2493 let order = self
2494 .query_order_by_id(
2495 product_type,
2496 order_id.as_str(),
2497 BYBIT_ORDER_REALTIME,
2498 "after submission",
2499 )
2500 .await?;
2501
2502 if order.order_status == crate::common::enums::BybitOrderStatus::Rejected
2505 && (order.cum_exec_qty.as_str() == "0" || order.cum_exec_qty.is_empty())
2506 {
2507 anyhow::bail!("Order rejected: {}", order.reject_reason);
2508 }
2509
2510 let ts_init = self.generate_ts_init();
2511
2512 parse_order_status_report(&order, &instrument, account_id, ts_init)
2513 }
2514
2515 pub async fn cancel_order(
2525 &self,
2526 account_id: AccountId,
2527 product_type: BybitProductType,
2528 instrument_id: InstrumentId,
2529 client_order_id: Option<ClientOrderId>,
2530 venue_order_id: Option<VenueOrderId>,
2531 ) -> anyhow::Result<OrderStatusReport> {
2532 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2533 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2534
2535 let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
2536 cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
2537
2538 if let Some(venue_order_id) = venue_order_id {
2539 cancel_entry.order_id(venue_order_id.to_string());
2540 } else if let Some(client_order_id) = client_order_id {
2541 cancel_entry.order_link_id(client_order_id.to_string());
2542 } else {
2543 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2544 }
2545
2546 let cancel_entry = cancel_entry.build().build_anyhow()?;
2547
2548 let mut params = BybitCancelOrderParamsBuilder::default();
2549 params.category(product_type);
2550 params.order(cancel_entry);
2551
2552 let params = params.build().build_anyhow()?;
2553 let body = serde_json::to_vec(¶ms)?;
2554
2555 let response: BybitPlaceOrderResponse = self
2556 .inner
2557 .send_request::<_, ()>(Method::POST, "/v5/order/cancel", None, Some(body), true)
2558 .await?;
2559
2560 let order_id = response
2561 .result
2562 .order_id
2563 .ok_or_else(|| anyhow::anyhow!("No order_id in cancel response"))?;
2564
2565 let order = self
2566 .query_order_by_id(
2567 product_type,
2568 order_id.as_str(),
2569 BYBIT_ORDER_HISTORY,
2570 "after cancellation",
2571 )
2572 .await?;
2573
2574 let ts_init = self.generate_ts_init();
2575
2576 parse_order_status_report(&order, &instrument, account_id, ts_init)
2577 }
2578
2579 pub async fn batch_cancel_orders(
2589 &self,
2590 account_id: AccountId,
2591 product_type: BybitProductType,
2592 instrument_ids: Vec<InstrumentId>,
2593 client_order_ids: Vec<Option<ClientOrderId>>,
2594 venue_order_ids: Vec<Option<VenueOrderId>>,
2595 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2596 if instrument_ids.len() != client_order_ids.len()
2597 || instrument_ids.len() != venue_order_ids.len()
2598 {
2599 anyhow::bail!(
2600 "instrument_ids, client_order_ids, and venue_order_ids must have the same length"
2601 );
2602 }
2603
2604 if instrument_ids.is_empty() {
2605 return Ok(Vec::new());
2606 }
2607
2608 if instrument_ids.len() > 20 {
2609 anyhow::bail!("Batch cancel limit is 20 orders per request");
2610 }
2611
2612 let mut cancel_entries = Vec::new();
2613
2614 for ((instrument_id, client_order_id), venue_order_id) in instrument_ids
2615 .iter()
2616 .zip(client_order_ids.iter())
2617 .zip(venue_order_ids.iter())
2618 {
2619 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2620 let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
2621 cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
2622
2623 if let Some(venue_order_id) = venue_order_id {
2624 cancel_entry.order_id(venue_order_id.to_string());
2625 } else if let Some(client_order_id) = client_order_id {
2626 cancel_entry.order_link_id(client_order_id.to_string());
2627 } else {
2628 anyhow::bail!(
2629 "Either client_order_id or venue_order_id must be provided for each order"
2630 );
2631 }
2632
2633 cancel_entries.push(cancel_entry.build().build_anyhow()?);
2634 }
2635
2636 let mut params = BybitBatchCancelOrderParamsBuilder::default();
2637 params.category(product_type);
2638 params.request(cancel_entries);
2639
2640 let params = params.build().build_anyhow()?;
2641 let body = serde_json::to_vec(¶ms)?;
2642
2643 let _response: BybitPlaceOrderResponse = self
2644 .inner
2645 .send_request::<_, ()>(
2646 Method::POST,
2647 "/v5/order/cancel-batch",
2648 None,
2649 Some(body),
2650 true,
2651 )
2652 .await?;
2653
2654 let mut reports = Vec::new();
2656
2657 for (instrument_id, (client_order_id, venue_order_id)) in instrument_ids
2658 .iter()
2659 .zip(client_order_ids.iter().zip(venue_order_ids.iter()))
2660 {
2661 let Ok(instrument) = self.instrument_from_cache(&instrument_id.symbol) else {
2662 log::debug!(
2663 "Skipping cancelled order report for instrument not in cache: symbol={}",
2664 instrument_id.symbol
2665 );
2666 continue;
2667 };
2668
2669 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2670
2671 let mut query_params = BybitOpenOrdersParamsBuilder::default();
2672 query_params.category(product_type);
2673 query_params.symbol(bybit_symbol.raw_symbol().to_string());
2674
2675 if let Some(venue_order_id) = venue_order_id {
2676 query_params.order_id(venue_order_id.to_string());
2677 } else if let Some(client_order_id) = client_order_id {
2678 query_params.order_link_id(client_order_id.to_string());
2679 }
2680
2681 let query_params = query_params.build().build_anyhow()?;
2682 let order_response: BybitOrderHistoryResponse = self
2683 .inner
2684 .send_request(
2685 Method::GET,
2686 BYBIT_ORDER_HISTORY,
2687 Some(&query_params),
2688 None,
2689 true,
2690 )
2691 .await?;
2692
2693 if let Some(order) = order_response.result.list.into_iter().next() {
2694 let ts_init = self.generate_ts_init();
2695 let report = parse_order_status_report(&order, &instrument, account_id, ts_init)?;
2696 reports.push(report);
2697 }
2698 }
2699
2700 Ok(reports)
2701 }
2702
2703 pub async fn cancel_all_orders(
2712 &self,
2713 account_id: AccountId,
2714 product_type: BybitProductType,
2715 instrument_id: InstrumentId,
2716 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2717 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2718 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2719
2720 let mut params = BybitCancelAllOrdersParamsBuilder::default();
2721 params.category(product_type);
2722 params.symbol(bybit_symbol.raw_symbol().to_string());
2723
2724 let params = params.build().build_anyhow()?;
2725 let body = serde_json::to_vec(¶ms)?;
2726
2727 let _response: crate::common::models::BybitListResponse<serde_json::Value> = self
2728 .inner
2729 .send_request::<_, ()>(Method::POST, "/v5/order/cancel-all", None, Some(body), true)
2730 .await?;
2731
2732 let mut query_params = BybitOrderHistoryParamsBuilder::default();
2734 query_params.category(product_type);
2735 query_params.symbol(bybit_symbol.raw_symbol().to_string());
2736 query_params.limit(50u32);
2737
2738 let query_params = query_params.build().build_anyhow()?;
2739 let order_response: BybitOrderHistoryResponse = self
2740 .inner
2741 .send_request(
2742 Method::GET,
2743 BYBIT_ORDER_HISTORY,
2744 Some(&query_params),
2745 None,
2746 true,
2747 )
2748 .await?;
2749
2750 let ts_init = self.generate_ts_init();
2751
2752 let mut reports = Vec::new();
2753
2754 for order in order_response.result.list {
2755 if let Ok(report) = parse_order_status_report(&order, &instrument, account_id, ts_init)
2756 {
2757 reports.push(report);
2758 }
2759 }
2760
2761 Ok(reports)
2762 }
2763
2764 #[expect(clippy::too_many_arguments)]
2775 pub async fn modify_order(
2776 &self,
2777 account_id: AccountId,
2778 product_type: BybitProductType,
2779 instrument_id: InstrumentId,
2780 client_order_id: Option<ClientOrderId>,
2781 venue_order_id: Option<VenueOrderId>,
2782 quantity: Option<Quantity>,
2783 price: Option<Price>,
2784 ) -> anyhow::Result<OrderStatusReport> {
2785 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2786 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2787
2788 let mut amend_entry = BybitBatchAmendOrderEntryBuilder::default();
2789 amend_entry.symbol(bybit_symbol.raw_symbol().to_string());
2790
2791 if let Some(venue_order_id) = venue_order_id {
2792 amend_entry.order_id(venue_order_id.to_string());
2793 } else if let Some(client_order_id) = client_order_id {
2794 amend_entry.order_link_id(client_order_id.to_string());
2795 } else {
2796 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2797 }
2798
2799 if let Some(quantity) = quantity {
2800 amend_entry.qty(Some(quantity.to_string()));
2801 }
2802
2803 if let Some(price) = price {
2804 amend_entry.price(Some(price.to_string()));
2805 }
2806
2807 let amend_entry = amend_entry.build().build_anyhow()?;
2808
2809 let mut params = BybitAmendOrderParamsBuilder::default();
2810 params.category(product_type);
2811 params.order(amend_entry);
2812
2813 let params = params.build().build_anyhow()?;
2814 let body = serde_json::to_vec(¶ms)?;
2815
2816 let response: BybitPlaceOrderResponse = self
2817 .inner
2818 .send_request::<_, ()>(Method::POST, "/v5/order/amend", None, Some(body), true)
2819 .await?;
2820
2821 let order_id = response
2822 .result
2823 .order_id
2824 .ok_or_else(|| anyhow::anyhow!("No order_id in amend response"))?;
2825
2826 let order = self
2827 .query_order_by_id(
2828 product_type,
2829 order_id.as_str(),
2830 BYBIT_ORDER_REALTIME,
2831 "after amendment",
2832 )
2833 .await?;
2834
2835 let ts_init = self.generate_ts_init();
2836
2837 parse_order_status_report(&order, &instrument, account_id, ts_init)
2838 }
2839
2840 pub async fn query_order(
2849 &self,
2850 account_id: AccountId,
2851 product_type: BybitProductType,
2852 instrument_id: InstrumentId,
2853 client_order_id: Option<ClientOrderId>,
2854 venue_order_id: Option<VenueOrderId>,
2855 ) -> anyhow::Result<Option<OrderStatusReport>> {
2856 log::debug!(
2857 "query_order: instrument_id={instrument_id}, client_order_id={client_order_id:?}, venue_order_id={venue_order_id:?}"
2858 );
2859
2860 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2861
2862 let mut params = BybitOpenOrdersParamsBuilder::default();
2863 params.category(product_type);
2864 params.symbol(bybit_symbol.raw_symbol().to_string());
2866
2867 if let Some(venue_order_id) = venue_order_id {
2868 params.order_id(venue_order_id.to_string());
2869 } else if let Some(client_order_id) = client_order_id {
2870 params.order_link_id(client_order_id.to_string());
2871 } else {
2872 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2873 }
2874
2875 let params = params.build().build_anyhow()?;
2876 let mut response: BybitOpenOrdersResponse = self
2877 .inner
2878 .send_request(Method::GET, BYBIT_ORDER_REALTIME, Some(¶ms), None, true)
2879 .await?;
2880
2881 if response.result.list.is_empty() && product_type != BybitProductType::Option {
2883 log::debug!("Order not found in open orders, trying with StopOrder filter");
2884
2885 let mut stop_params = BybitOpenOrdersParamsBuilder::default();
2886 stop_params.category(product_type);
2887 stop_params.symbol(bybit_symbol.raw_symbol().to_string());
2888 stop_params.order_filter(BybitOrderFilter::StopOrder);
2889
2890 if let Some(venue_order_id) = venue_order_id {
2891 stop_params.order_id(venue_order_id.to_string());
2892 } else if let Some(client_order_id) = client_order_id {
2893 stop_params.order_link_id(client_order_id.to_string());
2894 }
2895
2896 let stop_params = stop_params.build().build_anyhow()?;
2897 response = self
2898 .inner
2899 .send_request(
2900 Method::GET,
2901 BYBIT_ORDER_REALTIME,
2902 Some(&stop_params),
2903 None,
2904 true,
2905 )
2906 .await?;
2907 }
2908
2909 if response.result.list.is_empty() {
2911 log::debug!("Order not found in open orders, checking order history");
2912
2913 let mut history_params = BybitOrderHistoryParamsBuilder::default();
2914 history_params.category(product_type);
2915 history_params.symbol(bybit_symbol.raw_symbol().to_string());
2916
2917 if let Some(venue_order_id) = venue_order_id {
2918 history_params.order_id(venue_order_id.to_string());
2919 } else if let Some(client_order_id) = client_order_id {
2920 history_params.order_link_id(client_order_id.to_string());
2921 }
2922
2923 let history_params = history_params.build().build_anyhow()?;
2924
2925 let mut history_response: BybitOrderHistoryResponse = self
2926 .inner
2927 .send_request(
2928 Method::GET,
2929 BYBIT_ORDER_HISTORY,
2930 Some(&history_params),
2931 None,
2932 true,
2933 )
2934 .await?;
2935
2936 if history_response.result.list.is_empty() && product_type == BybitProductType::Option {
2937 log::debug!("Option order not found in order history");
2938 return Ok(None);
2939 }
2940
2941 if history_response.result.list.is_empty() && product_type != BybitProductType::Option {
2943 log::debug!("Order not found in order history, trying with StopOrder filter");
2944
2945 let mut stop_history_params = BybitOrderHistoryParamsBuilder::default();
2946 stop_history_params.category(product_type);
2947 stop_history_params.symbol(bybit_symbol.raw_symbol().to_string());
2948 stop_history_params.order_filter(BybitOrderFilter::StopOrder);
2949
2950 if let Some(venue_order_id) = venue_order_id {
2951 stop_history_params.order_id(venue_order_id.to_string());
2952 } else if let Some(client_order_id) = client_order_id {
2953 stop_history_params.order_link_id(client_order_id.to_string());
2954 }
2955
2956 let stop_history_params = stop_history_params
2957 .build()
2958 .map_err(|e| anyhow::anyhow!(e))?;
2959
2960 history_response = self
2961 .inner
2962 .send_request(
2963 Method::GET,
2964 BYBIT_ORDER_HISTORY,
2965 Some(&stop_history_params),
2966 None,
2967 true,
2968 )
2969 .await?;
2970
2971 if history_response.result.list.is_empty() {
2972 log::debug!("Order not found in order history with StopOrder filter either");
2973 return Ok(None);
2974 }
2975 }
2976
2977 response.result.list = history_response.result.list;
2979 }
2980
2981 let order = &response.result.list[0];
2982 let ts_init = self.generate_ts_init();
2983
2984 log::debug!(
2985 "Query order response: symbol={}, order_id={}, order_link_id={}",
2986 order.symbol.as_str(),
2987 order.order_id.as_str(),
2988 order.order_link_id.as_str()
2989 );
2990
2991 let instrument = self
2992 .instrument_from_cache(&instrument_id.symbol)
2993 .map_err(|e| {
2994 log::error!(
2995 "Instrument cache miss for symbol '{}': {}",
2996 instrument_id.symbol.as_str(),
2997 e
2998 );
2999 anyhow::anyhow!(
3000 "Failed to query order {}: {}",
3001 client_order_id
3002 .as_ref()
3003 .map(|id| id.to_string())
3004 .or_else(|| venue_order_id.as_ref().map(|id| id.to_string()))
3005 .unwrap_or_else(|| "unknown".to_string()),
3006 e
3007 )
3008 })?;
3009
3010 log::debug!("Retrieved instrument from cache: id={}", instrument.id());
3011
3012 let report =
3013 parse_order_status_report(order, &instrument, account_id, ts_init).map_err(|e| {
3014 log::error!(
3015 "Failed to parse order status report for {}: {}",
3016 order.order_link_id.as_str(),
3017 e
3018 );
3019 e
3020 })?;
3021
3022 log::debug!(
3023 "Successfully created OrderStatusReport for {}",
3024 order.order_link_id.as_str()
3025 );
3026
3027 Ok(Some(report))
3028 }
3029
3030 async fn fetch_fee_map(
3031 &self,
3032 product_type: BybitProductType,
3033 base_coin: Option<Ustr>,
3034 ) -> anyhow::Result<AHashMap<Ustr, BybitFeeRate>> {
3035 let mut fee_params = BybitFeeRateParamsBuilder::default();
3036 fee_params.category(product_type);
3037 if let Some(bc) = base_coin {
3038 fee_params.base_coin(bc.to_string());
3039 }
3040 let Ok(params) = fee_params.build() else {
3041 return Ok(AHashMap::new());
3042 };
3043
3044 match self.inner.get_fee_rate(¶ms).await {
3045 Ok(response) => Ok(response
3046 .result
3047 .list
3048 .into_iter()
3049 .map(|f| (f.symbol, f))
3050 .collect()),
3051 Err(BybitHttpError::MissingCredentials) => {
3052 log::warn!("Missing credentials for fee rates, using defaults");
3053 Ok(AHashMap::new())
3054 }
3055 Err(BybitHttpError::BybitError {
3056 error_code,
3057 ref message,
3058 }) => {
3059 log::warn!(
3060 "{}",
3061 self.fee_rate_rejection_warning(product_type, error_code, message)
3062 );
3063 Ok(AHashMap::new())
3064 }
3065 Err(e) => Err(e.into()),
3066 }
3067 }
3068
3069 async fn fetch_option_fee_map(
3070 &self,
3071 base_coin: Option<Ustr>,
3072 ) -> anyhow::Result<AHashMap<Ustr, BybitFeeRate>> {
3073 let mut fee_params = BybitFeeRateParamsBuilder::default();
3074 fee_params.category(BybitProductType::Option);
3075 if let Some(bc) = base_coin {
3076 fee_params.base_coin(bc.to_string());
3077 }
3078 let Ok(params) = fee_params.build() else {
3079 return Ok(AHashMap::new());
3080 };
3081
3082 match self.inner.get_fee_rate(¶ms).await {
3083 Ok(response) => Ok(response
3084 .result
3085 .list
3086 .into_iter()
3087 .filter_map(|f| f.base_coin.map(|bc| (bc, f)))
3088 .collect()),
3089 Err(BybitHttpError::MissingCredentials) => {
3090 log::warn!("Missing credentials for option fee rates, using defaults");
3091 Ok(AHashMap::new())
3092 }
3093 Err(BybitHttpError::BybitError {
3094 error_code,
3095 ref message,
3096 }) => {
3097 let error_detail = Self::format_bybit_error_detail(error_code, message);
3098 log::warn!(
3099 "Option fee rate request rejected via /v5/account/fee-rate ({error_detail}), using defaults"
3100 );
3101 Ok(AHashMap::new())
3102 }
3103 Err(e) => {
3104 log::warn!("Option fee rate request failed ({e}), using defaults");
3105 Ok(AHashMap::new())
3106 }
3107 }
3108 }
3109
3110 fn fee_rate_rejection_warning(
3111 &self,
3112 product_type: BybitProductType,
3113 error_code: i32,
3114 message: &str,
3115 ) -> String {
3116 let product_type = product_type.as_ref().to_ascii_lowercase();
3117 let error_detail = Self::format_bybit_error_detail(error_code, message);
3118
3119 if self
3120 .base_url()
3121 .starts_with(bybit_http_base_url(BybitEnvironment::Demo))
3122 && matches!(product_type.as_str(), "linear" | "inverse")
3123 && error_code == 10001
3124 {
3125 format!(
3126 "Bybit demo rejected the {product_type} fee rate request via \
3127 /v5/account/fee-rate ({error_detail}); demo derivatives fee rates appear \
3128 unsupported, using defaults"
3129 )
3130 } else {
3131 format!(
3132 "Fee rate request rejected for {product_type} instruments via \
3133 /v5/account/fee-rate ({error_detail}), using defaults"
3134 )
3135 }
3136 }
3137
3138 fn format_bybit_error_detail(error_code: i32, message: &str) -> String {
3139 let message = message.trim();
3140 if message.is_empty() {
3141 format!("error {error_code}, no message")
3142 } else {
3143 format!("error {error_code}: {message}")
3144 }
3145 }
3146
3147 async fn paginate_instruments<D, F>(
3148 &self,
3149 product_type: BybitProductType,
3150 symbol: &Option<String>,
3151 base_coin: Option<Ustr>,
3152 mut parse: F,
3153 ) -> anyhow::Result<Vec<InstrumentAny>>
3154 where
3155 D: DeserializeOwned,
3156 BybitCursorListResponse<D>: BybitResponseCheck,
3157 F: FnMut(&D) -> Option<InstrumentAny>,
3158 {
3159 let mut instruments = Vec::new();
3160 let mut cursor: Option<String> = None;
3161 let mut prev_cursor: Option<String> = None;
3162
3163 loop {
3164 let params = BybitInstrumentsInfoParams {
3165 category: product_type,
3166 symbol: symbol.clone(),
3167 status: None,
3168 base_coin: base_coin.map(|u| u.to_string()),
3169 limit: Some(1000),
3170 cursor: cursor.clone(),
3171 };
3172
3173 let response: BybitCursorListResponse<D> = self.inner.get_instruments(¶ms).await?;
3174
3175 for definition in &response.result.list {
3176 if let Some(instrument) = parse(definition) {
3177 instruments.push(instrument);
3178 }
3179 }
3180
3181 cursor = response.result.next_page_cursor;
3182 if cursor.as_ref().is_none_or(|c| c.is_empty()) || cursor == prev_cursor {
3183 break;
3184 }
3185 prev_cursor = cursor.clone();
3186 }
3187
3188 Ok(instruments)
3189 }
3190
3191 pub async fn request_instrument_statuses(
3201 &self,
3202 product_type: BybitProductType,
3203 ) -> anyhow::Result<AHashMap<InstrumentId, MarketStatusAction>> {
3204 let mut statuses = AHashMap::new();
3205 let mut cursor: Option<String> = None;
3206
3207 loop {
3208 let params = BybitInstrumentsInfoParams {
3209 category: product_type,
3210 symbol: None,
3211 status: None,
3212 base_coin: None,
3213 limit: Some(1000),
3214 cursor: cursor.clone(),
3215 };
3216
3217 match product_type {
3218 BybitProductType::Spot => {
3219 let response: BybitCursorListResponse<BybitInstrumentSpot> =
3220 self.inner.get_instruments(¶ms).await?;
3221
3222 for def in &response.result.list {
3223 let symbol = make_bybit_symbol(def.symbol, product_type);
3224 let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3225 statuses.insert(id, MarketStatusAction::from(def.status));
3226 }
3227 cursor = response.result.next_page_cursor;
3228 }
3229 BybitProductType::Linear => {
3230 let response: BybitCursorListResponse<BybitInstrumentLinear> =
3231 self.inner.get_instruments(¶ms).await?;
3232
3233 for def in &response.result.list {
3234 let symbol = make_bybit_symbol(def.symbol, product_type);
3235 let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3236 let status = MarketStatusAction::from(def.status);
3237 if status == MarketStatusAction::Trading
3238 && def.contract_type == BybitContractType::LinearPerpetual
3239 && def.delivery_time != "0"
3240 {
3241 statuses.insert(id, MarketStatusAction::PreClose);
3242 } else {
3243 statuses.insert(id, status);
3244 }
3245 }
3246 cursor = response.result.next_page_cursor;
3247 }
3248 BybitProductType::Inverse => {
3249 let response: BybitCursorListResponse<BybitInstrumentInverse> =
3250 self.inner.get_instruments(¶ms).await?;
3251
3252 for def in &response.result.list {
3253 let symbol = make_bybit_symbol(def.symbol, product_type);
3254 let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3255 let status = MarketStatusAction::from(def.status);
3256 if status == MarketStatusAction::Trading
3257 && def.contract_type == BybitContractType::InversePerpetual
3258 && def.delivery_time != "0"
3259 {
3260 statuses.insert(id, MarketStatusAction::PreClose);
3261 } else {
3262 statuses.insert(id, status);
3263 }
3264 }
3265 cursor = response.result.next_page_cursor;
3266 }
3267 BybitProductType::Option => {
3268 let response: BybitCursorListResponse<BybitInstrumentOption> =
3269 self.inner.get_instruments(¶ms).await?;
3270
3271 for def in &response.result.list {
3272 let symbol = make_bybit_symbol(def.symbol, product_type);
3273 let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3274 statuses.insert(id, MarketStatusAction::from(def.status));
3275 }
3276 cursor = response.result.next_page_cursor;
3277 }
3278 }
3279
3280 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
3281 break;
3282 }
3283 }
3284
3285 Ok(statuses)
3286 }
3287
3288 pub async fn request_instruments(
3298 &self,
3299 product_type: BybitProductType,
3300 symbol: Option<String>,
3301 base_coin: Option<Ustr>,
3302 ) -> anyhow::Result<Vec<InstrumentAny>> {
3303 let ts_init = self.generate_ts_init();
3304
3305 let default_fee_rate = |symbol: Ustr| BybitFeeRate {
3306 symbol,
3307 taker_fee_rate: "0.001".to_string(),
3308 maker_fee_rate: "0.001".to_string(),
3309 base_coin: None,
3310 };
3311
3312 let instruments = match product_type {
3313 BybitProductType::Spot => {
3314 let fee_map = self.fetch_fee_map(product_type, base_coin).await?;
3315 self.paginate_instruments::<BybitInstrumentSpot, _>(
3316 product_type,
3317 &symbol,
3318 base_coin,
3319 |def| {
3320 let fee = fee_map
3321 .get(&def.symbol)
3322 .cloned()
3323 .unwrap_or_else(|| default_fee_rate(def.symbol));
3324 parse_spot_instrument(def, &fee, ts_init, ts_init).ok()
3325 },
3326 )
3327 .await?
3328 }
3329 BybitProductType::Linear => {
3330 let fee_map = self.fetch_fee_map(product_type, base_coin).await?;
3331 self.paginate_instruments::<BybitInstrumentLinear, _>(
3332 product_type,
3333 &symbol,
3334 base_coin,
3335 |def| {
3336 let fee = fee_map
3337 .get(&def.symbol)
3338 .cloned()
3339 .unwrap_or_else(|| default_fee_rate(def.symbol));
3340 parse_linear_instrument(def, &fee, ts_init, ts_init).ok()
3341 },
3342 )
3343 .await?
3344 }
3345 BybitProductType::Inverse => {
3346 let fee_map = self.fetch_fee_map(product_type, base_coin).await?;
3347 self.paginate_instruments::<BybitInstrumentInverse, _>(
3348 product_type,
3349 &symbol,
3350 base_coin,
3351 |def| {
3352 let fee = fee_map
3353 .get(&def.symbol)
3354 .cloned()
3355 .unwrap_or_else(|| default_fee_rate(def.symbol));
3356 parse_inverse_instrument(def, &fee, ts_init, ts_init).ok()
3357 },
3358 )
3359 .await?
3360 }
3361 BybitProductType::Option => {
3362 let fee_map = self.fetch_option_fee_map(base_coin).await?;
3363 self.paginate_instruments::<BybitInstrumentOption, _>(
3364 product_type,
3365 &symbol,
3366 base_coin,
3367 |def| {
3368 let fee = fee_map.get(&def.base_coin);
3369 parse_option_instrument(def, fee, ts_init, ts_init).ok()
3370 },
3371 )
3372 .await?
3373 }
3374 };
3375
3376 self.cache_instruments(&instruments);
3377
3378 Ok(instruments)
3379 }
3380
3381 pub async fn request_tickers(
3394 &self,
3395 params: &BybitTickersParams,
3396 ) -> anyhow::Result<Vec<BybitTickerData>> {
3397 use super::models::{
3398 BybitTickersLinearResponse, BybitTickersOptionResponse, BybitTickersSpotResponse,
3399 };
3400
3401 match params.category {
3402 BybitProductType::Spot => {
3403 let response: BybitTickersSpotResponse = self.inner.get_tickers(params).await?;
3404 Ok(response.result.list.into_iter().map(Into::into).collect())
3405 }
3406 BybitProductType::Linear | BybitProductType::Inverse => {
3407 let response: BybitTickersLinearResponse = self.inner.get_tickers(params).await?;
3408 Ok(response.result.list.into_iter().map(Into::into).collect())
3409 }
3410 BybitProductType::Option => {
3411 let response: BybitTickersOptionResponse = self.inner.get_tickers(params).await?;
3412 Ok(response.result.list.into_iter().map(Into::into).collect())
3413 }
3414 }
3415 }
3416
3417 pub async fn request_option_tickers_raw(
3426 &self,
3427 base_coin: &str,
3428 ) -> anyhow::Result<Vec<BybitTickerOption>> {
3429 let params = BybitTickersParams {
3430 category: BybitProductType::Option,
3431 symbol: None,
3432 base_coin: Some(base_coin.to_string()),
3433 exp_date: None,
3434 };
3435 let response: BybitTickersOptionResponse = self.inner.get_tickers(¶ms).await?;
3436 Ok(response.result.list)
3437 }
3438
3439 pub async fn request_option_tickers_raw_with_params(
3448 &self,
3449 params: &BybitTickersParams,
3450 ) -> anyhow::Result<Vec<BybitTickerOption>> {
3451 let response: BybitTickersOptionResponse = self.inner.get_tickers(params).await?;
3452 Ok(response.result.list)
3453 }
3454
3455 pub async fn request_trades(
3475 &self,
3476 product_type: BybitProductType,
3477 instrument_id: InstrumentId,
3478 limit: Option<u32>,
3479 ) -> anyhow::Result<Vec<TradeTick>> {
3480 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
3481 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3482
3483 let mut params_builder = BybitTradesParamsBuilder::default();
3484 params_builder.category(product_type);
3485 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3486
3487 if let Some(limit_val) = limit {
3488 params_builder.limit(limit_val);
3489 }
3490
3491 let params = params_builder.build().build_anyhow()?;
3492 let response = self.inner.get_recent_trades(¶ms).await?;
3493
3494 let mut trades = Vec::new();
3495
3496 for trade in response.result.list {
3497 if let Ok(trade_tick) = parse_trade_tick(&trade, &instrument, None) {
3498 trades.push(trade_tick);
3499 }
3500 }
3501
3502 Ok(trades)
3503 }
3504
3505 pub async fn request_funding_rates(
3518 &self,
3519 product_type: BybitProductType,
3520 instrument_id: InstrumentId,
3521 start: Option<DateTime<Utc>>,
3522 end: Option<DateTime<Utc>>,
3523 limit: Option<u32>,
3524 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
3525 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
3526 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3527
3528 let start_ms = start.map(|dt| dt.timestamp_millis());
3529 let mut seen_timestamps: AHashSet<i64> = AHashSet::new();
3530
3531 let mut raw_funding_rates = Vec::new();
3532
3533 let mut current_end_ms = match (start, end) {
3535 (Some(_), None) => Some(Utc::now().timestamp_millis()),
3536 _ => end.map(|dt| dt.timestamp_millis()),
3537 };
3538
3539 loop {
3540 let mut params_builder = BybitFundingParamsBuilder::default();
3541 params_builder.category(product_type);
3542 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3543 params_builder.limit(limit.unwrap_or(200).clamp(0, 200)); if let Some(start_val) = start_ms {
3546 params_builder.start_time(start_val);
3547 }
3548
3549 if let Some(end_val) = current_end_ms {
3550 params_builder.end_time(end_val);
3551 }
3552
3553 let params = params_builder.build().build_anyhow()?;
3554 let response = self.inner.get_funding_history(¶ms).await?;
3555
3556 let funding_rates = response.result.list;
3557
3558 let mut new_funding_rates_with_ts: Vec<(i64, _)> = funding_rates
3559 .into_iter()
3560 .filter_map(|f| {
3561 let Ok(ts) = f.funding_rate_timestamp.parse::<i64>() else {
3562 return None;
3563 };
3564
3565 seen_timestamps.insert(ts).then_some((ts, f))
3566 })
3567 .collect();
3568
3569 new_funding_rates_with_ts.sort_by_key(|(ts, _)| Reverse(*ts));
3570
3571 let earliest_funding_time = match new_funding_rates_with_ts.last() {
3572 Some((last_ts, _)) => *last_ts,
3573 None => break,
3574 };
3575
3576 let new_funding_rates = new_funding_rates_with_ts.into_iter().map(|(_, f)| f);
3577 raw_funding_rates.extend(new_funding_rates);
3578
3579 if let Some(limit_val) = limit
3581 && raw_funding_rates.len() >= limit_val as usize
3582 {
3583 break;
3584 }
3585
3586 if let Some(start_val) = start_ms
3587 && earliest_funding_time <= start_val
3588 {
3589 break;
3590 }
3591
3592 current_end_ms = Some(earliest_funding_time - 1);
3594 }
3595
3596 if let Some(limit_val) = limit {
3597 raw_funding_rates.truncate(limit_val as usize);
3598 }
3599 let mut rates: Vec<FundingRateUpdate> = Vec::with_capacity(raw_funding_rates.len());
3600
3601 for window in raw_funding_rates.windows(2) {
3602 let raw = &window[0];
3603 let timestamp = raw
3604 .funding_rate_timestamp
3605 .parse::<i64>()
3606 .map_err(|_| anyhow::anyhow!("invalid funding_rate_timestamp"))?;
3607 let older_timestamp = window[1]
3608 .funding_rate_timestamp
3609 .parse::<i64>()
3610 .map_err(|_| anyhow::anyhow!("invalid funding_rate_timestamp"))?;
3611
3612 let interval_millis = timestamp - older_timestamp;
3613 let rate = parse_funding_rate(raw, &instrument, Some(interval_millis))?;
3614
3615 rates.push(rate);
3616 }
3617
3618 if let Some(last_raw) = raw_funding_rates.last() {
3619 let rate = parse_funding_rate(last_raw, &instrument, None)?;
3620 rates.push(rate);
3621 }
3622
3623 rates.reverse();
3624
3625 Ok(rates)
3626 }
3627
3628 pub async fn request_orderbook_snapshot(
3646 &self,
3647 product_type: BybitProductType,
3648 instrument_id: InstrumentId,
3649 limit: Option<u32>,
3650 ) -> anyhow::Result<OrderBookDeltas> {
3651 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
3652 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3653
3654 let mut params_builder = BybitOrderbookParamsBuilder::default();
3655 params_builder.category(product_type);
3656 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3657
3658 if let Some(limit) = limit {
3659 let max_limit = match product_type {
3660 BybitProductType::Spot => 200,
3661 BybitProductType::Option => 25,
3662 BybitProductType::Linear | BybitProductType::Inverse => 500,
3663 };
3664 let clamped_limit = limit.min(max_limit);
3665 if limit > max_limit {
3666 log::warn!(
3667 "Bybit orderbook snapshot request depth limit exceeds venue maximum; clamping: limit={limit}, clamped_limit={clamped_limit}",
3668 );
3669 }
3670 params_builder.limit(clamped_limit);
3671 }
3672
3673 let params = params_builder.build().build_anyhow()?;
3674 let response = self.inner.get_orderbook(¶ms).await?;
3675
3676 let deltas = parse_orderbook(&response.result, &instrument, None)?;
3677
3678 Ok(deltas)
3679 }
3680
3681 pub async fn request_bars(
3694 &self,
3695 product_type: BybitProductType,
3696 bar_type: BarType,
3697 start: Option<DateTime<Utc>>,
3698 end: Option<DateTime<Utc>>,
3699 limit: Option<u32>,
3700 timestamp_on_close: bool,
3701 ) -> anyhow::Result<Vec<Bar>> {
3702 let instrument = self.instrument_from_cache(&bar_type.instrument_id().symbol)?;
3703 let bybit_symbol = BybitSymbol::new(bar_type.instrument_id().symbol.as_str())?;
3704
3705 let interval = bar_spec_to_bybit_interval(
3707 bar_type.spec().aggregation,
3708 bar_type.spec().step.get() as u64,
3709 )?;
3710
3711 let start_ms = start.map(|dt| dt.timestamp_millis());
3712 let mut seen_timestamps: AHashSet<i64> = AHashSet::new();
3713 let current_time_ms = get_atomic_clock_realtime().get_time_ms() as i64;
3714
3715 let mut pages: Vec<Vec<Bar>> = Vec::new();
3725 let mut total_bars = 0usize;
3726 let mut current_end = end.map(|dt| dt.timestamp_millis());
3727 let mut page_count = 0;
3728
3729 loop {
3730 page_count += 1;
3731
3732 let mut params_builder = BybitKlinesParamsBuilder::default();
3733 params_builder.category(product_type);
3734 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3735 params_builder.interval(interval);
3736 params_builder.limit(1000u32); if let Some(start_val) = start_ms {
3739 params_builder.start(start_val);
3740 }
3741
3742 if let Some(end_val) = current_end {
3743 params_builder.end(end_val);
3744 }
3745
3746 let params = params_builder.build().build_anyhow()?;
3747 let response = self.inner.get_klines(¶ms).await?;
3748
3749 let klines = response.result.list;
3750 if klines.is_empty() {
3751 break;
3752 }
3753
3754 let mut klines_with_ts: Vec<(i64, _)> = klines
3756 .into_iter()
3757 .filter_map(|k| k.start.parse::<i64>().ok().map(|ts| (ts, k)))
3758 .collect();
3759
3760 klines_with_ts.sort_by_key(|(ts, _)| *ts);
3761
3762 let has_new = klines_with_ts
3764 .iter()
3765 .any(|(ts, _)| !seen_timestamps.contains(ts));
3766
3767 if !has_new {
3768 break;
3769 }
3770
3771 let mut page_bars = Vec::with_capacity(klines_with_ts.len());
3772
3773 let mut earliest_ts: Option<i64> = None;
3774
3775 for (start_time, kline) in &klines_with_ts {
3776 if earliest_ts.is_none_or(|ts| *start_time < ts) {
3778 earliest_ts = Some(*start_time);
3779 }
3780
3781 let bar_end_time = interval.bar_end_time_ms(*start_time);
3782 if bar_end_time > current_time_ms {
3783 continue;
3784 }
3785
3786 if !seen_timestamps.contains(start_time)
3787 && let Ok(bar) =
3788 parse_kline_bar(kline, &instrument, bar_type, timestamp_on_close, None)
3789 {
3790 page_bars.push(bar);
3791 seen_timestamps.insert(*start_time);
3792 }
3793 }
3794
3795 total_bars += page_bars.len();
3798 pages.push(page_bars);
3799
3800 if let Some(limit_val) = limit
3802 && total_bars >= limit_val as usize
3803 {
3804 break;
3805 }
3806
3807 let Some(earliest_bar_time) = earliest_ts else {
3810 break;
3811 };
3812
3813 if let Some(start_val) = start_ms
3814 && earliest_bar_time <= start_val
3815 {
3816 break;
3817 }
3818
3819 current_end = Some(earliest_bar_time - 1);
3820
3821 if page_count > 100 {
3823 break;
3824 }
3825 }
3826
3827 let mut all_bars: Vec<Bar> = Vec::with_capacity(total_bars);
3829 for page in pages.into_iter().rev() {
3830 all_bars.extend(page);
3831 }
3832
3833 if let Some(limit_val) = limit {
3835 let limit_usize = limit_val as usize;
3836 if all_bars.len() > limit_usize {
3837 let start_idx = all_bars.len() - limit_usize;
3838 return Ok(all_bars[start_idx..].to_vec());
3839 }
3840 }
3841
3842 Ok(all_bars)
3843 }
3844
3845 pub async fn request_fee_rates(
3857 &self,
3858 product_type: BybitProductType,
3859 symbol: Option<String>,
3860 base_coin: Option<String>,
3861 ) -> anyhow::Result<Vec<BybitFeeRate>> {
3862 let params = BybitFeeRateParams {
3863 category: product_type,
3864 symbol,
3865 base_coin,
3866 };
3867
3868 let response = self.inner.get_fee_rate(¶ms).await?;
3869 Ok(response.result.list)
3870 }
3871
3872 pub async fn request_account_state(
3884 &self,
3885 account_type: BybitAccountType,
3886 account_id: AccountId,
3887 ) -> anyhow::Result<AccountState> {
3888 let params = BybitWalletBalanceParams {
3889 account_type,
3890 coin: None,
3891 };
3892
3893 let response = self.inner.get_wallet_balance(¶ms).await?;
3894 let ts_init = self.generate_ts_init();
3895
3896 let wallet_balance = response
3898 .result
3899 .list
3900 .first()
3901 .ok_or_else(|| anyhow::anyhow!("No wallet balance found in response"))?;
3902
3903 parse_account_state(wallet_balance, account_id, ts_init)
3904 }
3905
3906 #[expect(clippy::too_many_arguments)]
3917 pub async fn request_order_status_reports(
3918 &self,
3919 account_id: AccountId,
3920 product_type: BybitProductType,
3921 instrument_id: Option<InstrumentId>,
3922 open_only: bool,
3923 start: Option<DateTime<Utc>>,
3924 end: Option<DateTime<Utc>>,
3925 limit: Option<u32>,
3926 ) -> anyhow::Result<Vec<OrderStatusReport>> {
3927 let symbol_param = if let Some(id) = instrument_id.as_ref() {
3929 let symbol_str = id.symbol.as_str();
3930 if symbol_str.is_empty() {
3931 None
3932 } else {
3933 Some(BybitSymbol::new(symbol_str)?.raw_symbol().to_string())
3934 }
3935 } else {
3936 None
3937 };
3938
3939 let settle_coins_to_query: Vec<Option<String>> =
3942 if product_type == BybitProductType::Linear && symbol_param.is_none() {
3943 vec![Some("USDT".to_string()), Some("USDC".to_string())]
3944 } else {
3945 match product_type {
3946 BybitProductType::Inverse => vec![None],
3947 _ => vec![None],
3948 }
3949 };
3950
3951 let mut all_collected_orders = Vec::new();
3952 let mut total_collected_across_coins = 0;
3953
3954 for settle_coin in settle_coins_to_query {
3955 let remaining_limit = if let Some(limit) = limit {
3956 let remaining = (limit as usize).saturating_sub(total_collected_across_coins);
3957 if remaining == 0 {
3958 break;
3959 }
3960 Some(remaining as u32)
3961 } else {
3962 None
3963 };
3964
3965 let orders_for_coin = if open_only {
3966 let mut all_orders = Vec::new();
3967 let mut seen_ids: AHashSet<Ustr> = AHashSet::new();
3968
3969 let order_filters: Vec<Option<BybitOrderFilter>> =
3972 if product_type == BybitProductType::Option {
3973 vec![None]
3974 } else {
3975 vec![None, Some(BybitOrderFilter::StopOrder)]
3976 };
3977
3978 for order_filter in order_filters {
3979 let mut cursor: Option<String> = None;
3980
3981 loop {
3982 let remaining = if let Some(limit) = remaining_limit {
3983 (limit as usize).saturating_sub(all_orders.len())
3984 } else {
3985 usize::MAX
3986 };
3987
3988 if remaining == 0 {
3989 break;
3990 }
3991
3992 let page_limit = std::cmp::min(remaining, 50);
3994
3995 let mut p = BybitOpenOrdersParamsBuilder::default();
3996 p.category(product_type);
3997
3998 if let Some(symbol) = symbol_param.clone() {
3999 p.symbol(symbol);
4000 }
4001
4002 if let Some(coin) = settle_coin.clone() {
4003 p.settle_coin(coin);
4004 }
4005
4006 if let Some(of) = order_filter {
4007 p.order_filter(of);
4008 }
4009 p.limit(page_limit as u32);
4010
4011 if let Some(c) = cursor {
4012 p.cursor(c);
4013 }
4014 let params = p.build().build_anyhow()?;
4015 let response: BybitOpenOrdersResponse = self
4016 .inner
4017 .send_request(
4018 Method::GET,
4019 BYBIT_ORDER_REALTIME,
4020 Some(¶ms),
4021 None,
4022 true,
4023 )
4024 .await?;
4025
4026 for order in response.result.list {
4027 if seen_ids.insert(order.order_id) {
4028 all_orders.push(order);
4029 }
4030 }
4031
4032 cursor = response.result.next_page_cursor;
4033 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
4034 break;
4035 }
4036 }
4037 }
4038
4039 all_orders
4040 } else {
4041 let mut all_orders = Vec::new();
4044 let mut open_orders = Vec::new();
4045 let mut seen_open_ids: AHashSet<Ustr> = AHashSet::new();
4046
4047 let order_filters: Vec<Option<BybitOrderFilter>> =
4050 if product_type == BybitProductType::Option {
4051 vec![None]
4052 } else {
4053 vec![None, Some(BybitOrderFilter::StopOrder)]
4054 };
4055
4056 for order_filter in &order_filters {
4057 let mut cursor: Option<String> = None;
4058
4059 loop {
4060 let remaining = if let Some(limit) = remaining_limit {
4061 (limit as usize).saturating_sub(open_orders.len())
4062 } else {
4063 usize::MAX
4064 };
4065
4066 if remaining == 0 {
4067 break;
4068 }
4069
4070 let page_limit = std::cmp::min(remaining, 50);
4072
4073 let mut open_params = BybitOpenOrdersParamsBuilder::default();
4074 open_params.category(product_type);
4075
4076 if let Some(symbol) = symbol_param.clone() {
4077 open_params.symbol(symbol);
4078 }
4079
4080 if let Some(coin) = settle_coin.clone() {
4081 open_params.settle_coin(coin);
4082 }
4083
4084 if let Some(of) = order_filter {
4085 open_params.order_filter(*of);
4086 }
4087 open_params.limit(page_limit as u32);
4088
4089 if let Some(c) = cursor {
4090 open_params.cursor(c);
4091 }
4092 let open_params = open_params.build().build_anyhow()?;
4093 let open_response: BybitOpenOrdersResponse = self
4094 .inner
4095 .send_request(
4096 Method::GET,
4097 BYBIT_ORDER_REALTIME,
4098 Some(&open_params),
4099 None,
4100 true,
4101 )
4102 .await?;
4103
4104 for order in open_response.result.list {
4105 if !seen_open_ids.contains(&order.order_id) {
4106 seen_open_ids.insert(order.order_id);
4107 open_orders.push(order);
4108 }
4109 }
4110
4111 cursor = open_response.result.next_page_cursor;
4112 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
4113 break;
4114 }
4115 }
4116 }
4117
4118 let seen_order_ids: AHashSet<Ustr> = seen_open_ids;
4119 let total_open_orders = open_orders.len();
4120
4121 all_orders.extend(open_orders);
4122
4123 let mut total_history_orders = 0;
4124
4125 for order_filter in &order_filters {
4126 let mut cursor: Option<String> = None;
4127
4128 loop {
4129 let total_orders = total_open_orders + total_history_orders;
4130 let remaining = if let Some(limit) = remaining_limit {
4131 (limit as usize).saturating_sub(total_orders)
4132 } else {
4133 usize::MAX
4134 };
4135
4136 if remaining == 0 {
4137 break;
4138 }
4139
4140 let page_limit = std::cmp::min(remaining, 50);
4142
4143 let mut history_params = BybitOrderHistoryParamsBuilder::default();
4144 history_params.category(product_type);
4145
4146 if let Some(symbol) = symbol_param.clone() {
4147 history_params.symbol(symbol);
4148 }
4149
4150 if let Some(coin) = settle_coin.clone() {
4151 history_params.settle_coin(coin);
4152 }
4153
4154 if let Some(of) = order_filter {
4155 history_params.order_filter(*of);
4156 }
4157
4158 if let Some(start) = start {
4159 history_params.start_time(start.timestamp_millis());
4160 }
4161
4162 if let Some(end) = end {
4163 history_params.end_time(end.timestamp_millis());
4164 }
4165 history_params.limit(page_limit as u32);
4166
4167 if let Some(c) = cursor {
4168 history_params.cursor(c);
4169 }
4170 let history_params = history_params.build().build_anyhow()?;
4171 let history_response: BybitOrderHistoryResponse = self
4172 .inner
4173 .send_request(
4174 Method::GET,
4175 BYBIT_ORDER_HISTORY,
4176 Some(&history_params),
4177 None,
4178 true,
4179 )
4180 .await?;
4181
4182 for order in history_response.result.list {
4184 if !seen_order_ids.contains(&order.order_id) {
4185 all_orders.push(order);
4186 total_history_orders += 1;
4187 }
4188 }
4189
4190 cursor = history_response.result.next_page_cursor;
4191 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
4192 break;
4193 }
4194 }
4195 }
4196
4197 all_orders
4198 };
4199
4200 total_collected_across_coins += orders_for_coin.len();
4201 all_collected_orders.extend(orders_for_coin);
4202 }
4203
4204 let ts_init = self.generate_ts_init();
4205
4206 let mut reports = Vec::new();
4207
4208 for order in all_collected_orders {
4209 if let Some(ref instrument_id) = instrument_id {
4210 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
4211
4212 if let Ok(report) =
4213 parse_order_status_report(&order, &instrument, account_id, ts_init)
4214 {
4215 reports.push(report);
4216 }
4217 } else {
4218 if !order.symbol.is_empty() {
4221 let symbol_with_product =
4222 Symbol::from_ustr_unchecked(make_bybit_symbol(order.symbol, product_type));
4223
4224 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
4225 log::debug!(
4226 "Skipping order report for instrument not in cache: symbol={}, full_symbol={}",
4227 order.symbol,
4228 symbol_with_product
4229 );
4230 continue;
4231 };
4232
4233 match parse_order_status_report(&order, &instrument, account_id, ts_init) {
4234 Ok(report) => reports.push(report),
4235 Err(e) => {
4236 log::error!("Failed to parse order status report: {e}");
4237 }
4238 }
4239 }
4240 }
4241 }
4242
4243 Ok(reports)
4244 }
4245
4246 pub async fn request_fill_reports(
4258 &self,
4259 account_id: AccountId,
4260 product_type: BybitProductType,
4261 instrument_id: Option<InstrumentId>,
4262 start: Option<i64>,
4263 end: Option<i64>,
4264 limit: Option<u32>,
4265 ) -> anyhow::Result<Vec<FillReport>> {
4266 let symbol = if let Some(id) = instrument_id {
4268 let bybit_symbol = BybitSymbol::new(id.symbol.as_str())?;
4269 Some(bybit_symbol.raw_symbol().to_string())
4270 } else {
4271 None
4272 };
4273
4274 let mut all_executions = Vec::new();
4276 let mut cursor: Option<String> = None;
4277 let mut total_executions = 0;
4278
4279 loop {
4280 let remaining = if let Some(limit) = limit {
4282 (limit as usize).saturating_sub(total_executions)
4283 } else {
4284 usize::MAX
4285 };
4286
4287 if remaining == 0 {
4289 break;
4290 }
4291
4292 let page_limit = std::cmp::min(remaining, 100);
4294
4295 let params = BybitTradeHistoryParams {
4296 category: product_type,
4297 symbol: symbol.clone(),
4298 base_coin: None,
4299 order_id: None,
4300 order_link_id: None,
4301 start_time: start,
4302 end_time: end,
4303 exec_type: None,
4304 limit: Some(page_limit as u32),
4305 cursor: cursor.clone(),
4306 };
4307
4308 let response = self.inner.get_trade_history(¶ms).await?;
4309 let list_len = response.result.list.len();
4310 all_executions.extend(response.result.list);
4311 total_executions += list_len;
4312
4313 cursor = response.result.next_page_cursor;
4314 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
4315 break;
4316 }
4317 }
4318
4319 let ts_init = self.generate_ts_init();
4320 let mut reports = Vec::new();
4321
4322 for execution in all_executions {
4323 let symbol_with_product =
4326 Symbol::from_ustr_unchecked(make_bybit_symbol(execution.symbol, product_type));
4327
4328 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
4329 log::debug!(
4330 "Skipping fill report for instrument not in cache: symbol={}, full_symbol={}",
4331 execution.symbol,
4332 symbol_with_product
4333 );
4334 continue;
4335 };
4336
4337 match parse_fill_report(&execution, account_id, &instrument, ts_init) {
4338 Ok(report) => reports.push(report),
4339 Err(e) => {
4340 log::error!("Failed to parse fill report: {e}");
4341 }
4342 }
4343 }
4344
4345 Ok(reports)
4346 }
4347
4348 pub async fn request_position_status_reports(
4360 &self,
4361 account_id: AccountId,
4362 product_type: BybitProductType,
4363 instrument_id: Option<InstrumentId>,
4364 ) -> anyhow::Result<Vec<PositionStatusReport>> {
4365 if product_type == BybitProductType::Spot {
4367 if self.use_spot_position_reports.load(Ordering::Relaxed) {
4368 return self
4369 .generate_spot_position_reports_from_wallet(account_id, instrument_id)
4370 .await;
4371 } else {
4372 return Ok(Vec::new());
4374 }
4375 }
4376
4377 let ts_init = self.generate_ts_init();
4378 let mut reports = Vec::new();
4379
4380 let symbol = if let Some(id) = instrument_id {
4382 let symbol_str = id.symbol.as_str();
4383 if symbol_str.is_empty() {
4384 anyhow::bail!("InstrumentId symbol is empty");
4385 }
4386 let bybit_symbol = BybitSymbol::new(symbol_str)?;
4387 Some(bybit_symbol.raw_symbol().to_string())
4388 } else {
4389 None
4390 };
4391
4392 if product_type == BybitProductType::Linear && symbol.is_none() {
4395 for settle_coin in ["USDT", "USDC"] {
4397 let mut cursor: Option<String> = None;
4398
4399 loop {
4400 let params = BybitPositionListParams {
4401 category: product_type,
4402 symbol: None,
4403 base_coin: None,
4404 settle_coin: Some(settle_coin.to_string()),
4405 limit: Some(200), cursor: cursor.clone(),
4407 };
4408
4409 let response = self.inner.get_positions(¶ms).await?;
4410
4411 for position in response.result.list {
4412 if position.symbol.is_empty() {
4413 continue;
4414 }
4415
4416 let symbol_with_product = Symbol::new(format!(
4417 "{}{}",
4418 position.symbol.as_str(),
4419 product_type.suffix()
4420 ));
4421
4422 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
4423 else {
4424 log::debug!(
4425 "Skipping position report for instrument not in cache: symbol={}, full_symbol={}",
4426 position.symbol,
4427 symbol_with_product
4428 );
4429 continue;
4430 };
4431
4432 match parse_position_status_report(
4433 &position,
4434 account_id,
4435 &instrument,
4436 ts_init,
4437 ) {
4438 Ok(report) => reports.push(report),
4439 Err(e) => {
4440 log::error!("Failed to parse position status report: {e}");
4441 }
4442 }
4443 }
4444
4445 cursor = response.result.next_page_cursor;
4446 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
4447 break;
4448 }
4449 }
4450 }
4451 } else {
4452 let mut cursor: Option<String> = None;
4454
4455 loop {
4456 let params = BybitPositionListParams {
4457 category: product_type,
4458 symbol: symbol.clone(),
4459 base_coin: None,
4460 settle_coin: None,
4461 limit: Some(200), cursor: cursor.clone(),
4463 };
4464
4465 let response = self.inner.get_positions(¶ms).await?;
4466
4467 for position in response.result.list {
4468 if position.symbol.is_empty() {
4469 continue;
4470 }
4471
4472 let symbol_with_product = Symbol::new(format!(
4473 "{}{}",
4474 position.symbol.as_str(),
4475 product_type.suffix()
4476 ));
4477
4478 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
4479 log::debug!(
4480 "Skipping position report for instrument not in cache: symbol={}, full_symbol={}",
4481 position.symbol,
4482 symbol_with_product
4483 );
4484 continue;
4485 };
4486
4487 match parse_position_status_report(&position, account_id, &instrument, ts_init)
4488 {
4489 Ok(report) => reports.push(report),
4490 Err(e) => {
4491 log::error!("Failed to parse position status report: {e}");
4492 }
4493 }
4494 }
4495
4496 cursor = response.result.next_page_cursor;
4497 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
4498 break;
4499 }
4500 }
4501 }
4502
4503 Ok(reports)
4504 }
4505
4506 async fn query_order_by_id(
4507 &self,
4508 product_type: BybitProductType,
4509 order_id: &str,
4510 endpoint: &str,
4511 context: &str,
4512 ) -> anyhow::Result<BybitOrder> {
4513 let mut query_params = BybitOpenOrdersParamsBuilder::default();
4514 query_params.category(product_type);
4515 query_params.order_id(order_id.to_string());
4516
4517 let query_params = query_params.build().build_anyhow()?;
4518 let order_response: BybitOpenOrdersResponse = self
4519 .inner
4520 .send_request(Method::GET, endpoint, Some(&query_params), None, true)
4521 .await?;
4522
4523 order_response
4524 .result
4525 .list
4526 .into_iter()
4527 .next()
4528 .ok_or_else(|| anyhow::anyhow!("No order returned {context}"))
4529 }
4530}
4531
4532#[cfg(test)]
4533mod tests {
4534 use rstest::rstest;
4535
4536 use super::*;
4537
4538 #[rstest]
4539 fn test_client_creation() {
4540 let client = BybitHttpClient::new(None, 60, 3, 1000, 10_000, 5_000, None);
4541 assert!(client.is_ok());
4542
4543 let client = client.unwrap();
4544 assert!(client.base_url().contains("bybit.com"));
4545 assert!(client.credential().is_none());
4546 }
4547
4548 #[rstest]
4549 fn test_client_with_credentials() {
4550 let client = BybitHttpClient::with_credentials(
4551 "test_key".to_string(),
4552 "test_secret".to_string(),
4553 Some("https://api-testnet.bybit.com".to_string()),
4554 60,
4555 3,
4556 1000,
4557 10_000,
4558 5_000,
4559 None,
4560 );
4561 assert!(client.is_ok());
4562
4563 let client = client.unwrap();
4564 assert!(client.credential().is_some());
4565 }
4566
4567 #[rstest]
4568 fn test_build_path_with_params() {
4569 #[derive(Serialize)]
4570 struct TestParams {
4571 category: String,
4572 symbol: String,
4573 }
4574
4575 let params = TestParams {
4576 category: "linear".to_string(),
4577 symbol: "BTCUSDT".to_string(),
4578 };
4579
4580 let path = BybitRawHttpClient::build_path("/v5/market/test", ¶ms);
4581 assert!(path.is_ok());
4582 assert!(path.unwrap().contains("category=linear"));
4583 }
4584
4585 #[rstest]
4586 fn test_build_path_without_params() {
4587 let params = ();
4588 let path = BybitRawHttpClient::build_path("/v5/market/time", ¶ms);
4589 assert!(path.is_ok());
4590 assert_eq!(path.unwrap(), "/v5/market/time");
4591 }
4592
4593 #[rstest]
4594 fn test_params_serialization_matches_build_path() {
4595 #[derive(Serialize)]
4597 struct TestParams {
4598 category: String,
4599 limit: u32,
4600 }
4601
4602 let params = TestParams {
4603 category: "spot".to_string(),
4604 limit: 50,
4605 };
4606
4607 let old_path = BybitRawHttpClient::build_path(BYBIT_ORDER_REALTIME, ¶ms).unwrap();
4609 let old_query = old_path.split('?').nth(1).unwrap_or("");
4610
4611 let new_query = serde_urlencoded::to_string(¶ms).unwrap();
4613
4614 assert_eq!(old_query, new_query);
4616 }
4617
4618 #[rstest]
4619 fn test_params_serialization_order() {
4620 #[derive(Serialize)]
4622 struct OrderParams {
4623 category: String,
4624 symbol: String,
4625 limit: u32,
4626 }
4627
4628 let params = OrderParams {
4629 category: "spot".to_string(),
4630 symbol: "BTCUSDT".to_string(),
4631 limit: 50,
4632 };
4633
4634 let query1 = serde_urlencoded::to_string(¶ms).unwrap();
4636 let query2 = serde_urlencoded::to_string(¶ms).unwrap();
4637 let query3 = serde_urlencoded::to_string(¶ms).unwrap();
4638
4639 assert_eq!(query1, query2);
4640 assert_eq!(query2, query3);
4641
4642 assert!(query1.contains("category=spot"));
4644 assert!(query1.contains("symbol=BTCUSDT"));
4645 assert!(query1.contains("limit=50"));
4646 }
4647
4648 #[rstest]
4649 #[case(
4650 "https://api-demo.bybit.com",
4651 BybitProductType::Linear,
4652 10001,
4653 "",
4654 "Bybit demo rejected the linear fee rate request via /v5/account/fee-rate \
4655 (error 10001, no message); demo derivatives fee rates appear unsupported, using defaults"
4656 )]
4657 #[case(
4658 "https://api-demo.bybit.com",
4659 BybitProductType::Inverse,
4660 10001,
4661 "",
4662 "Bybit demo rejected the inverse fee rate request via /v5/account/fee-rate \
4663 (error 10001, no message); demo derivatives fee rates appear unsupported, using defaults"
4664 )]
4665 #[case(
4666 "https://api.bybit.com",
4667 BybitProductType::Spot,
4668 10001,
4669 "Parameter error",
4670 "Fee rate request rejected for spot instruments via /v5/account/fee-rate \
4671 (error 10001: Parameter error), using defaults"
4672 )]
4673 #[case(
4674 "https://api-demo.bybit.com",
4675 BybitProductType::Spot,
4676 10001,
4677 "Parameter error",
4678 "Fee rate request rejected for spot instruments via /v5/account/fee-rate \
4679 (error 10001: Parameter error), using defaults"
4680 )]
4681 #[case(
4682 "https://api.bybit.com",
4683 BybitProductType::Linear,
4684 10001,
4685 "Parameter error",
4686 "Fee rate request rejected for linear instruments via /v5/account/fee-rate \
4687 (error 10001: Parameter error), using defaults"
4688 )]
4689 fn test_fee_rate_rejection_warning(
4690 #[case] base_url: &str,
4691 #[case] product_type: BybitProductType,
4692 #[case] error_code: i32,
4693 #[case] message: &str,
4694 #[case] expected: &str,
4695 ) {
4696 let client =
4697 BybitHttpClient::new(Some(base_url.to_string()), 60, 3, 1000, 10_000, 5_000, None)
4698 .unwrap();
4699
4700 let warning = client.fee_rate_rejection_warning(product_type, error_code, message);
4701
4702 assert_eq!(warning, expected);
4703 }
4704
4705 #[rstest]
4706 #[case(10001, "", "error 10001, no message")]
4707 #[case(10001, "Parameter error", "error 10001: Parameter error")]
4708 fn test_format_bybit_error_detail(
4709 #[case] error_code: i32,
4710 #[case] message: &str,
4711 #[case] expected: &str,
4712 ) {
4713 let detail = BybitHttpClient::format_bybit_error_detail(error_code, message);
4714
4715 assert_eq!(detail, expected);
4716 }
4717
4718 #[rstest]
4719 #[case(
4720 10001,
4721 "",
4722 "Option fee rate request rejected via /v5/account/fee-rate \
4723 (error 10001, no message), using defaults"
4724 )]
4725 #[case(
4726 10001,
4727 "Parameter error",
4728 "Option fee rate request rejected via /v5/account/fee-rate \
4729 (error 10001: Parameter error), using defaults"
4730 )]
4731 fn test_option_fee_rate_warning_message(
4732 #[case] error_code: i32,
4733 #[case] message: &str,
4734 #[case] expected: &str,
4735 ) {
4736 let error_detail = BybitHttpClient::format_bybit_error_detail(error_code, message);
4737 let warning = format!(
4738 "Option fee rate request rejected via /v5/account/fee-rate ({error_detail}), using defaults"
4739 );
4740
4741 assert_eq!(warning, expected);
4742 }
4743}