1use std::{
21 cmp::Reverse,
22 collections::HashMap,
23 fmt::{Debug, Display},
24 num::NonZeroU32,
25 sync::{
26 Arc, LazyLock,
27 atomic::{AtomicBool, AtomicU64, Ordering},
28 },
29};
30
31use ahash::{AHashMap, AHashSet};
32use anyhow::Context;
33use arc_swap::ArcSwap;
34use jiff::Timestamp;
35use nautilus_common::cache::InstrumentLookupError;
36use nautilus_core::{
37 AtomicMap, AtomicTime, env::get_or_env_var_opt, nanos::UnixNanos,
38 time::get_atomic_clock_realtime,
39};
40use nautilus_model::{
41 data::{Bar, BarType, FundingRateUpdate, OrderBookDeltas, TradeTick},
42 enums::{MarketStatusAction, OrderSide, OrderType, PositionSide, TimeInForce},
43 events::account::state::AccountState,
44 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
45 instruments::{Instrument, InstrumentAny},
46 reports::{FillReport, OrderStatusReport, PositionStatusReport},
47 types::{Price, Quantity},
48};
49use nautilus_network::{
50 http::{HttpClient, HttpRedirectPolicy, Method, create_standard_nautilus_headers},
51 ratelimiter::quota::Quota,
52 retry::{RetryConfig, RetryError, RetryManager},
53};
54use rust_decimal::Decimal;
55use serde::{Serialize, de::DeserializeOwned};
56use tokio_util::sync::CancellationToken;
57use ustr::Ustr;
58
59use super::{
60 error::{BybitCancelOrderError, BybitHttpError, BybitModifyOrderError, BybitSubmitOrderError},
61 models::{
62 BybitAccountDetailsResponse, BybitAccountInfoResponse, BybitBorrowResponse,
63 BybitEscrowSubMembersResponse, BybitFeeRate, BybitFeeRateResponse, BybitFundingResponse,
64 BybitInstrumentInverse, BybitInstrumentInverseResponse, BybitInstrumentLinear,
65 BybitInstrumentLinearResponse, BybitInstrumentOption, BybitInstrumentOptionResponse,
66 BybitInstrumentSpot, BybitInstrumentSpotResponse, BybitKlinesResponse,
67 BybitNoConvertRepayResponse, BybitOpenOrdersResponse, BybitOrder,
68 BybitOrderHistoryResponse, BybitOrderbookResponse, BybitPlaceOrderResponse,
69 BybitPositionListResponse, BybitRepayResponse, BybitServerTimeResponse,
70 BybitSetLeverageResponse, BybitSetMarginModeResponse, BybitSetTradingStopResponse,
71 BybitSubApiKeyInfo, BybitSubApiKeysResponse, BybitSubMember, BybitSubMembersPagedResponse,
72 BybitSubMembersResponse, BybitSwitchModeResponse, BybitTickerData, BybitTickerOption,
73 BybitTickersOptionResponse, BybitTradeHistoryResponse, BybitTradesResponse,
74 BybitUpdateMasterApiResponse, BybitUpdateSubApiResponse, BybitWalletBalanceResponse,
75 },
76 query::{
77 BybitAmendOrderParamsBuilder, BybitBatchAmendOrderEntryBuilder,
78 BybitBatchCancelOrderEntryBuilder, BybitBatchCancelOrderParamsBuilder,
79 BybitBatchPlaceOrderEntryBuilder, BybitBorrowParamsBuilder,
80 BybitCancelAllOrdersParamsBuilder, BybitCancelOrderParamsBuilder, BybitFeeRateParams,
81 BybitFundingParams, BybitFundingParamsBuilder, BybitInstrumentsInfoParams,
82 BybitKlinesParams, BybitKlinesParamsBuilder, BybitNativeTpSlParams,
83 BybitNoConvertRepayParamsBuilder, BybitOpenOrdersParamsBuilder,
84 BybitOrderHistoryParamsBuilder, BybitOrderbookParams, BybitOrderbookParamsBuilder,
85 BybitPlaceOrderParamsBuilder, BybitPositionListParams, BybitRepayParamsBuilder,
86 BybitSetLeverageParamsBuilder, BybitSetMarginModeParamsBuilder, BybitSetTradingStopParams,
87 BybitSubApiKeysParams, BybitSubMembersPageParams, BybitSwitchModeParamsBuilder,
88 BybitTickersParams, BybitTradeHistoryParams, BybitTradesParams, BybitTradesParamsBuilder,
89 BybitUpdateMasterApiParams, BybitUpdateSubApiParams, BybitWalletBalanceParams,
90 },
91};
92use crate::common::{
93 consts::{BYBIT_NAUTILUS_BROKER_ID, BYBIT_VENUE},
94 credential::{Credential, credential_env_vars},
95 enums::{
96 BybitAccountType, BybitBboSideType, BybitContractType, BybitEnvironment, BybitExecType,
97 BybitMarginMode, BybitOpenOnly, BybitOrderFilter, BybitOrderSide, BybitOrderSmpType,
98 BybitOrderType, BybitPositionIdx, BybitPositionMode, BybitProductType, BybitRepayStatus,
99 BybitTpSlMode,
100 },
101 models::{BybitCursorListResponse, BybitErrorCheck, BybitResponseCheck},
102 parse::{
103 bar_spec_to_bybit_interval, bybit_rejection_due_post_only, make_bybit_symbol,
104 map_time_in_force, parse_account_state, parse_fill_report, parse_funding_rate,
105 parse_inverse_instrument, parse_kline_bar, parse_linear_instrument,
106 parse_option_instrument, parse_order_status_report, parse_orderbook,
107 parse_position_status_report, parse_spot_instrument, parse_trade_tick, spot_leverage,
108 spot_market_unit, trigger_direction,
109 },
110 rate_limit::{
111 BYBIT_RATE_LIMIT_HEADER, BYBIT_RATE_LIMIT_RESET_HEADER, BYBIT_RATE_LIMIT_STATUS_HEADER,
112 BybitRateLimiter, batch_call_limit, batch_endpoint_limit, batch_send_limit, batch_weight,
113 category_from_payload,
114 },
115 retry::should_retry_http,
116 symbol::BybitSymbol,
117 urls::bybit_http_base_url,
118};
119
120const DEFAULT_RECV_WINDOW_MS: u64 = 5_000;
121
122trait BuilderResultExt<T> {
123 fn build_anyhow(self) -> anyhow::Result<T>;
124}
125
126impl<T, E: Display> BuilderResultExt<T> for Result<T, E> {
127 fn build_anyhow(self) -> anyhow::Result<T> {
128 self.map_err(|e| anyhow::anyhow!("{e}"))
129 }
130}
131
132const BYBIT_INSTRUMENTS_INFO: &str = "/v5/market/instruments-info";
133const BYBIT_ORDER_REALTIME: &str = "/v5/order/realtime";
134const BYBIT_ORDER_HISTORY: &str = "/v5/order/history";
135const BYBIT_EXECUTION_LIST: &str = "/v5/execution/list";
136const BYBIT_POSITION_LIST: &str = "/v5/position/list";
137
138#[derive(Debug, Default)]
150struct CursorWalk {
151 followed: AHashSet<String>,
152 last: Option<String>,
153}
154
155impl CursorWalk {
156 fn advance(
164 &mut self,
165 endpoint: &str,
166 cursor: Option<String>,
167 ) -> anyhow::Result<Option<String>> {
168 let Some(cursor) = cursor.filter(|cursor| !cursor.is_empty()) else {
169 return Ok(None);
170 };
171
172 anyhow::ensure!(
173 self.last.as_deref() != Some(cursor.as_str()),
174 "{endpoint} pagination cursor did not advance from {cursor:?}",
175 );
176 anyhow::ensure!(
177 self.followed.insert(cursor.clone()),
178 "{endpoint} pagination repeated cursor {cursor:?}",
179 );
180
181 self.last = Some(cursor.clone());
182
183 Ok(Some(cursor))
184 }
185}
186
187pub static BYBIT_REST_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
189 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
190});
191
192pub static BYBIT_REPAY_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
194 Quota::per_second(NonZeroU32::new(1).expect("non-zero")).expect("valid constant")
195});
196
197#[cfg_attr(
202 feature = "python",
203 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
204)]
205#[cfg_attr(
206 feature = "python",
207 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
208)]
209#[derive(Clone)]
210pub struct BybitRawHttpClient {
211 base_url: String,
212 client: Arc<ArcSwap<HttpClient>>,
213 rate_limiter: BybitRateLimiter,
214 credential: Option<Credential>,
215 recv_window_ms: u64,
216 timeout_secs: u64,
217 proxy_url: Option<String>,
218 session_generation: Arc<AtomicU64>,
219 retry_manager: RetryManager<BybitHttpError>,
220 cancellation_token: Arc<parking_lot::Mutex<CancellationToken>>,
221}
222
223impl Default for BybitRawHttpClient {
224 fn default() -> Self {
225 Self::new(None, 60, 3, 1000, 10_000, DEFAULT_RECV_WINDOW_MS, None)
226 .expect("Failed to create default BybitRawHttpClient")
227 }
228}
229
230impl Debug for BybitRawHttpClient {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 f.debug_struct(stringify!(BybitRawHttpClient))
233 .field("base_url", &self.base_url)
234 .field("has_credentials", &self.credential.is_some())
235 .field("recv_window_ms", &self.recv_window_ms)
236 .finish()
237 }
238}
239
240impl BybitRawHttpClient {
241 pub fn cancel_all_requests(&self) {
243 self.cancellation_token.lock().cancel();
244 }
245
246 pub fn reset_cancellation_token(&self) {
249 let mut guard = self.cancellation_token.lock();
250 *guard = CancellationToken::new();
251 }
252
253 pub fn cancellation_token(&self) -> CancellationToken {
255 self.cancellation_token.lock().clone()
256 }
257
258 pub fn new(
264 base_url: Option<String>,
265 timeout_secs: u64,
266 max_retries: u32,
267 retry_delay_ms: u64,
268 retry_delay_max_ms: u64,
269 recv_window_ms: u64,
270 proxy_url: Option<String>,
271 ) -> Result<Self, BybitHttpError> {
272 let retry_config = RetryConfig {
273 max_retries,
274 initial_delay_ms: retry_delay_ms,
275 max_delay_ms: retry_delay_max_ms,
276 backoff_factor: 2.0,
277 jitter_ms: 1000,
278 operation_timeout_ms: Some(60_000),
279 immediate_first: false,
280 max_elapsed_ms: Some(180_000),
281 };
282
283 let retry_manager = RetryManager::new(retry_config);
284 let base_url =
285 base_url.unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string());
286 let rate_limiter = BybitRateLimiter::for_http(&base_url, None, proxy_url.as_deref());
287 let client = Self::build_http_client(timeout_secs, proxy_url.clone())?;
288 let session_generation = rate_limiter.http_session_generation();
289
290 Ok(Self {
291 base_url,
292 client: Arc::new(ArcSwap::from_pointee(client)),
293 rate_limiter,
294 credential: None,
295 recv_window_ms,
296 timeout_secs,
297 proxy_url,
298 session_generation: Arc::new(AtomicU64::new(session_generation)),
299 retry_manager,
300 cancellation_token: Arc::new(parking_lot::Mutex::new(CancellationToken::new())),
301 })
302 }
303
304 #[expect(clippy::too_many_arguments)]
310 pub fn with_credentials(
311 api_key: String,
312 api_secret: String,
313 base_url: Option<String>,
314 timeout_secs: u64,
315 max_retries: u32,
316 retry_delay_ms: u64,
317 retry_delay_max_ms: u64,
318 recv_window_ms: u64,
319 proxy_url: Option<String>,
320 ) -> Result<Self, BybitHttpError> {
321 let retry_config = RetryConfig {
322 max_retries,
323 initial_delay_ms: retry_delay_ms,
324 max_delay_ms: retry_delay_max_ms,
325 backoff_factor: 2.0,
326 jitter_ms: 1000,
327 operation_timeout_ms: Some(60_000),
328 immediate_first: false,
329 max_elapsed_ms: Some(180_000),
330 };
331
332 let retry_manager = RetryManager::new(retry_config);
333 let base_url =
334 base_url.unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string());
335 let credential = Credential::new(api_key, api_secret);
336 let rate_limiter =
337 BybitRateLimiter::for_http(&base_url, Some(credential.api_key()), proxy_url.as_deref());
338 let client = Self::build_http_client(timeout_secs, proxy_url.clone())?;
339 let session_generation = rate_limiter.http_session_generation();
340
341 Ok(Self {
342 base_url,
343 client: Arc::new(ArcSwap::from_pointee(client)),
344 rate_limiter,
345 credential: Some(credential),
346 recv_window_ms,
347 timeout_secs,
348 proxy_url,
349 session_generation: Arc::new(AtomicU64::new(session_generation)),
350 retry_manager,
351 cancellation_token: Arc::new(parking_lot::Mutex::new(CancellationToken::new())),
352 })
353 }
354
355 #[expect(clippy::too_many_arguments)]
367 pub fn new_with_env(
368 api_key: Option<String>,
369 api_secret: Option<String>,
370 base_url: Option<String>,
371 demo: bool,
372 testnet: bool,
373 timeout_secs: u64,
374 max_retries: u32,
375 retry_delay_ms: u64,
376 retry_delay_max_ms: u64,
377 recv_window_ms: u64,
378 proxy_url: Option<String>,
379 ) -> Result<Self, BybitHttpError> {
380 let environment = if demo {
381 BybitEnvironment::Demo
382 } else if testnet {
383 BybitEnvironment::Testnet
384 } else {
385 BybitEnvironment::Mainnet
386 };
387 let base_url =
388 Some(base_url.unwrap_or_else(|| bybit_http_base_url(environment).to_string()));
389 let (key_var, secret_var) = credential_env_vars(environment);
390 let key = get_or_env_var_opt(api_key, key_var);
391 let secret = get_or_env_var_opt(api_secret, secret_var);
392
393 if let (Some(k), Some(s)) = (key, secret) {
394 Self::with_credentials(
395 k,
396 s,
397 base_url,
398 timeout_secs,
399 max_retries,
400 retry_delay_ms,
401 retry_delay_max_ms,
402 recv_window_ms,
403 proxy_url,
404 )
405 } else {
406 Self::new(
407 base_url,
408 timeout_secs,
409 max_retries,
410 retry_delay_ms,
411 retry_delay_max_ms,
412 recv_window_ms,
413 proxy_url,
414 )
415 }
416 }
417
418 fn default_headers() -> HashMap<String, String> {
419 let mut headers: HashMap<String, String> =
420 create_standard_nautilus_headers().into_iter().collect();
421 headers.insert(
422 "X-Referer".to_string(),
423 BYBIT_NAUTILUS_BROKER_ID.to_string(),
424 );
425 headers
426 }
427
428 fn build_http_client(
429 timeout_secs: u64,
430 proxy_url: Option<String>,
431 ) -> Result<HttpClient, BybitHttpError> {
432 HttpClient::builder()
433 .redirect_policy(HttpRedirectPolicy::Reject)
434 .headers(Self::default_headers())
435 .header_keys(vec![
436 BYBIT_RATE_LIMIT_HEADER.to_string(),
437 BYBIT_RATE_LIMIT_STATUS_HEADER.to_string(),
438 BYBIT_RATE_LIMIT_RESET_HEADER.to_string(),
439 ])
440 .timeout_secs(timeout_secs)
441 .maybe_proxy_url(proxy_url)
442 .rate_limiters(Vec::new())
443 .build()
444 .map_err(|e| BybitHttpError::NetworkError(format!("Failed to create HTTP client: {e}")))
445 }
446
447 fn refresh_http_session(&self, generation: u64) -> Result<(), BybitHttpError> {
448 let current = self.session_generation.load(Ordering::Acquire);
449 if current == generation {
450 return Ok(());
451 }
452
453 let client = Self::build_http_client(self.timeout_secs, self.proxy_url.clone())?;
454 self.client.store(Arc::new(client));
455 self.session_generation.store(generation, Ordering::Release);
456 Ok(())
457 }
458
459 fn request_rate_limit(
460 endpoint: &str,
461 payload: Option<&str>,
462 ) -> (Option<BybitProductType>, u32) {
463 let category = category_from_payload(payload);
464 let weight = if endpoint.ends_with("-batch") {
465 let order_count = payload
466 .and_then(|payload| serde_json::from_str::<serde_json::Value>(payload).ok())
467 .and_then(|value| value.get("request")?.as_array().map(Vec::len))
468 .unwrap_or(1);
469 category.map_or(1, |category| batch_weight(category, order_count))
470 } else {
471 1
472 };
473 (category, weight)
474 }
475
476 fn observe_rate_limit_headers(
477 &self,
478 endpoint: &str,
479 category: Option<BybitProductType>,
480 headers: &HashMap<String, String>,
481 ) {
482 let Some(limit) = headers
483 .get(BYBIT_RATE_LIMIT_HEADER)
484 .and_then(|value| value.parse::<u32>().ok())
485 else {
486 return;
487 };
488 let Some(remaining) = headers
489 .get(BYBIT_RATE_LIMIT_STATUS_HEADER)
490 .and_then(|value| value.parse::<u32>().ok())
491 else {
492 return;
493 };
494 let reset_timestamp_ms = headers
495 .get(BYBIT_RATE_LIMIT_RESET_HEADER)
496 .and_then(|value| value.parse::<i64>().ok());
497 self.rate_limiter
498 .observe_account(endpoint, category, limit, remaining, reset_timestamp_ms);
499 }
500
501 fn is_rate_limit_403(status: u16, body: &str) -> bool {
502 status == 403 && body.to_ascii_lowercase().contains("access too frequent")
503 }
504
505 fn sign_request(
506 &self,
507 timestamp: &str,
508 params: Option<&str>,
509 ) -> Result<HashMap<String, String>, BybitHttpError> {
510 let credential = self
511 .credential
512 .as_ref()
513 .ok_or(BybitHttpError::MissingCredentials)?;
514
515 let signature = credential.sign_with_payload(timestamp, self.recv_window_ms, params);
516
517 let mut headers = HashMap::new();
518 headers.insert(
519 "X-BAPI-API-KEY".to_string(),
520 credential.api_key().to_string(),
521 );
522 headers.insert("X-BAPI-TIMESTAMP".to_string(), timestamp.to_string());
523 headers.insert("X-BAPI-SIGN".to_string(), signature);
524 headers.insert(
525 "X-BAPI-RECV-WINDOW".to_string(),
526 self.recv_window_ms.to_string(),
527 );
528
529 Ok(headers)
530 }
531
532 async fn send_request<T: DeserializeOwned + BybitResponseCheck, P: Serialize>(
533 &self,
534 method: Method,
535 endpoint: &str,
536 params: Option<&P>,
537 body: Option<Vec<u8>>,
538 authenticate: bool,
539 ) -> Result<T, BybitHttpError> {
540 let endpoint = endpoint.to_string();
541 let url = format!("{}{endpoint}", self.base_url);
542 let method_clone = method.clone();
543 let body_clone = body.clone();
544
545 let params_str = if method == Method::GET {
547 params
548 .map(serde_urlencoded::to_string)
549 .transpose()
550 .map_err(|e| {
551 BybitHttpError::JsonError(format!("Failed to serialize params: {e}"))
552 })?
553 } else {
554 None
555 };
556
557 let operation = || {
558 let url = url.clone();
559 let method = method_clone.clone();
560 let body = body_clone.clone();
561 let endpoint = endpoint.clone();
562 let params_str = params_str.clone();
563
564 async move {
565 let full_url = if let Some(ref query) = params_str {
566 if query.is_empty() {
567 url
568 } else {
569 format!("{url}?{query}")
570 }
571 } else {
572 url
573 };
574
575 let sign_payload = if method == Method::GET {
576 params_str.as_deref()
577 } else {
578 body.as_ref()
579 .and_then(|body| std::str::from_utf8(body).ok())
580 };
581 let (category, weight) = Self::request_rate_limit(&endpoint, sign_payload);
582
583 let generation = self.rate_limiter.http_session_generation();
584 self.refresh_http_session(generation)?;
585 self.rate_limiter
586 .acquire_http(&endpoint, category, weight, authenticate)
587 .await
588 .map_err(BybitHttpError::ValidationError)?;
589 let generation = self.rate_limiter.http_session_generation();
590 self.refresh_http_session(generation)?;
591
592 let mut headers = Self::default_headers();
593
594 if authenticate {
595 let timestamp = get_atomic_clock_realtime().get_time_ms().to_string();
596 let auth_headers = self.sign_request(×tamp, sign_payload)?;
597 headers.extend(auth_headers);
598 }
599
600 if method == Method::POST || method == Method::PUT {
601 headers.insert("Content-Type".to_string(), "application/json".to_string());
602 }
603
604 let response = self
605 .client
606 .load()
607 .request(method, full_url, None, Some(headers), body, None, None)
608 .await?;
609
610 self.observe_rate_limit_headers(&endpoint, category, &response.headers);
611
612 if response.status.as_u16() >= 400 {
613 let body = String::from_utf8_lossy(&response.body).to_string();
614 if Self::is_rate_limit_403(response.status.as_u16(), &body) {
615 let generation = self.rate_limiter.reset_http_sessions();
616 self.refresh_http_session(generation)?;
617 }
618 return Err(BybitHttpError::UnexpectedStatus {
619 status: response.status.as_u16(),
620 body,
621 });
622 }
623
624 match serde_json::from_slice::<T>(&response.body) {
626 Ok(result) => {
627 if result.ret_code() != 0 {
629 return Err(BybitHttpError::BybitError {
630 error_code: result.ret_code() as i32,
631 message: result.ret_msg().to_string(),
632 });
633 }
634 Ok(result)
635 }
636 Err(json_err) => {
637 if let Ok(error_check) =
640 serde_json::from_slice::<BybitErrorCheck>(&response.body)
641 && error_check.ret_code != 0
642 {
643 return Err(BybitHttpError::BybitError {
644 error_code: error_check.ret_code as i32,
645 message: error_check.ret_msg,
646 });
647 }
648 Err(json_err.into())
650 }
651 }
652 }
653 };
654
655 let create_error = |error: RetryError| -> BybitHttpError {
656 match error {
657 RetryError::Canceled => {
658 BybitHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
659 }
660 error => BybitHttpError::NetworkError(error.to_string()),
661 }
662 };
663
664 let token = self.cancellation_token();
665
666 self.retry_manager
667 .invocation(
668 endpoint.as_str(),
669 operation,
670 should_retry_http,
671 create_error,
672 )
673 .cancellation_token(&token)
674 .execute()
675 .await
676 }
677
678 #[cfg(test)]
679 fn build_path<S: Serialize>(base: &str, params: &S) -> Result<String, BybitHttpError> {
680 let query = serde_urlencoded::to_string(params)
681 .map_err(|e| BybitHttpError::JsonError(e.to_string()))?;
682
683 if query.is_empty() {
684 Ok(base.to_owned())
685 } else {
686 Ok(format!("{base}?{query}"))
687 }
688 }
689
690 pub async fn get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
700 self.send_request::<_, ()>(Method::GET, "/v5/market/time", None, None, false)
701 .await
702 }
703
704 pub async fn get_instruments<T: DeserializeOwned + BybitResponseCheck>(
714 &self,
715 params: &BybitInstrumentsInfoParams,
716 ) -> Result<T, BybitHttpError> {
717 self.send_request(
718 Method::GET,
719 BYBIT_INSTRUMENTS_INFO,
720 Some(params),
721 None,
722 false,
723 )
724 .await
725 }
726
727 pub async fn get_instruments_spot(
737 &self,
738 params: &BybitInstrumentsInfoParams,
739 ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
740 self.get_instruments(params).await
741 }
742
743 pub async fn get_instruments_linear(
753 &self,
754 params: &BybitInstrumentsInfoParams,
755 ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
756 self.get_instruments(params).await
757 }
758
759 pub async fn get_instruments_inverse(
769 &self,
770 params: &BybitInstrumentsInfoParams,
771 ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
772 self.get_instruments(params).await
773 }
774
775 pub async fn get_instruments_option(
785 &self,
786 params: &BybitInstrumentsInfoParams,
787 ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
788 self.get_instruments(params).await
789 }
790
791 pub async fn get_klines(
801 &self,
802 params: &BybitKlinesParams,
803 ) -> Result<BybitKlinesResponse, BybitHttpError> {
804 self.send_request(Method::GET, "/v5/market/kline", Some(params), None, false)
805 .await
806 }
807
808 pub async fn get_recent_trades(
818 &self,
819 params: &BybitTradesParams,
820 ) -> Result<BybitTradesResponse, BybitHttpError> {
821 self.send_request(
822 Method::GET,
823 "/v5/market/recent-trade",
824 Some(params),
825 None,
826 false,
827 )
828 .await
829 }
830
831 pub async fn get_funding_history(
841 &self,
842 params: &BybitFundingParams,
843 ) -> Result<BybitFundingResponse, BybitHttpError> {
844 self.send_request(
845 Method::GET,
846 "/v5/market/funding/history",
847 Some(params),
848 None,
849 false,
850 )
851 .await
852 }
853
854 pub async fn get_orderbook(
864 &self,
865 params: &BybitOrderbookParams,
866 ) -> Result<BybitOrderbookResponse, BybitHttpError> {
867 self.send_request(
868 Method::GET,
869 "/v5/market/orderbook",
870 Some(params),
871 None,
872 false,
873 )
874 .await
875 }
876
877 #[expect(clippy::too_many_arguments)]
891 pub async fn get_open_orders(
892 &self,
893 category: BybitProductType,
894 symbol: Option<String>,
895 base_coin: Option<String>,
896 settle_coin: Option<String>,
897 order_id: Option<String>,
898 order_link_id: Option<String>,
899 open_only: Option<BybitOpenOnly>,
900 order_filter: Option<BybitOrderFilter>,
901 limit: Option<u32>,
902 cursor: Option<String>,
903 ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
904 let mut builder = BybitOpenOrdersParamsBuilder::default();
905 builder.category(category);
906
907 if let Some(s) = symbol {
908 builder.symbol(s);
909 }
910
911 if let Some(bc) = base_coin {
912 builder.base_coin(bc);
913 }
914
915 if let Some(sc) = settle_coin {
916 builder.settle_coin(sc);
917 }
918
919 if let Some(oi) = order_id {
920 builder.order_id(oi);
921 }
922
923 if let Some(ol) = order_link_id {
924 builder.order_link_id(ol);
925 }
926
927 if let Some(oo) = open_only {
928 builder.open_only(oo);
929 }
930
931 if let Some(of) = order_filter {
932 builder.order_filter(of);
933 }
934
935 if let Some(l) = limit {
936 builder.limit(l);
937 }
938
939 if let Some(c) = cursor {
940 builder.cursor(c);
941 }
942
943 let params = builder
944 .build()
945 .expect("Failed to build BybitOpenOrdersParams");
946
947 self.send_request(Method::GET, BYBIT_ORDER_REALTIME, Some(¶ms), None, true)
948 .await
949 }
950
951 pub async fn place_order(
961 &self,
962 request: &serde_json::Value,
963 ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
964 let body = serde_json::to_vec(request)?;
965 self.send_request::<_, ()>(Method::POST, "/v5/order/create", None, Some(body), true)
966 .await
967 }
968
969 pub async fn get_wallet_balance(
979 &self,
980 params: &BybitWalletBalanceParams,
981 ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
982 self.send_request(
983 Method::GET,
984 "/v5/account/wallet-balance",
985 Some(params),
986 None,
987 true,
988 )
989 .await
990 }
991
992 pub async fn get_account_info(&self) -> Result<BybitAccountInfoResponse, BybitHttpError> {
1002 self.send_request::<_, ()>(Method::GET, "/v5/account/info", None, None, true)
1003 .await
1004 }
1005
1006 pub async fn get_account_details(&self) -> Result<BybitAccountDetailsResponse, BybitHttpError> {
1016 self.send_request::<_, ()>(Method::GET, "/v5/user/query-api", None, None, true)
1017 .await
1018 }
1019
1020 pub async fn update_sub_api_key(
1030 &self,
1031 params: &BybitUpdateSubApiParams,
1032 ) -> Result<BybitUpdateSubApiResponse, BybitHttpError> {
1033 let body = serde_json::to_vec(params)?;
1034 self.send_request::<_, ()>(
1035 Method::POST,
1036 "/v5/user/update-sub-api",
1037 None,
1038 Some(body),
1039 true,
1040 )
1041 .await
1042 }
1043
1044 pub async fn update_master_api_key(
1054 &self,
1055 params: &BybitUpdateMasterApiParams,
1056 ) -> Result<BybitUpdateMasterApiResponse, BybitHttpError> {
1057 let body = serde_json::to_vec(params)?;
1058 self.send_request::<_, ()>(Method::POST, "/v5/user/update-api", None, Some(body), true)
1059 .await
1060 }
1061
1062 pub async fn get_sub_members(&self) -> Result<BybitSubMembersResponse, BybitHttpError> {
1072 self.send_request::<_, ()>(Method::GET, "/v5/user/query-sub-members", None, None, true)
1073 .await
1074 }
1075
1076 pub async fn get_sub_members_paged(
1086 &self,
1087 params: &BybitSubMembersPageParams,
1088 ) -> Result<BybitSubMembersPagedResponse, BybitHttpError> {
1089 self.send_request(Method::GET, "/v5/user/submembers", Some(params), None, true)
1090 .await
1091 }
1092
1093 pub async fn get_escrow_sub_members(
1103 &self,
1104 params: &BybitSubMembersPageParams,
1105 ) -> Result<BybitEscrowSubMembersResponse, BybitHttpError> {
1106 self.send_request(
1107 Method::GET,
1108 "/v5/user/escrow_sub_members",
1109 Some(params),
1110 None,
1111 true,
1112 )
1113 .await
1114 }
1115
1116 pub async fn get_sub_api_keys(
1126 &self,
1127 params: &BybitSubApiKeysParams,
1128 ) -> Result<BybitSubApiKeysResponse, BybitHttpError> {
1129 self.send_request(
1130 Method::GET,
1131 "/v5/user/sub-apikeys",
1132 Some(params),
1133 None,
1134 true,
1135 )
1136 .await
1137 }
1138
1139 pub async fn fetch_all_sub_members_paged(
1146 &self,
1147 page_size: Option<u32>,
1148 ) -> Result<Vec<BybitSubMember>, BybitHttpError> {
1149 let mut members = Vec::new();
1150 let mut cursor: Option<String> = None;
1151
1152 loop {
1153 let params = BybitSubMembersPageParams {
1154 page_size,
1155 next_cursor: cursor.take(),
1156 };
1157 let mut page = self.get_sub_members_paged(¶ms).await?;
1158 let next = page.result.continuation_cursor().map(str::to_owned);
1159 members.append(&mut page.result.sub_members);
1160
1161 match next {
1162 Some(c) => cursor = Some(c),
1163 None => break,
1164 }
1165 }
1166
1167 Ok(members)
1168 }
1169
1170 pub async fn fetch_all_escrow_sub_members(
1178 &self,
1179 page_size: Option<u32>,
1180 ) -> Result<Vec<BybitSubMember>, BybitHttpError> {
1181 let mut members = Vec::new();
1182 let mut cursor: Option<String> = None;
1183
1184 loop {
1185 let params = BybitSubMembersPageParams {
1186 page_size,
1187 next_cursor: cursor.take(),
1188 };
1189 let mut page = self.get_escrow_sub_members(¶ms).await?;
1190 let next = page.result.continuation_cursor().map(str::to_owned);
1191 members.append(&mut page.result.sub_members);
1192
1193 match next {
1194 Some(c) => cursor = Some(c),
1195 None => break,
1196 }
1197 }
1198
1199 Ok(members)
1200 }
1201
1202 pub async fn fetch_all_sub_api_keys(
1210 &self,
1211 sub_member_id: impl Into<String>,
1212 limit: Option<u32>,
1213 ) -> Result<Vec<BybitSubApiKeyInfo>, BybitHttpError> {
1214 let sub_member_id = sub_member_id.into();
1215 let mut keys = Vec::new();
1216 let mut cursor: Option<String> = None;
1217
1218 loop {
1219 let params = BybitSubApiKeysParams {
1220 sub_member_id: sub_member_id.clone(),
1221 limit,
1222 cursor: cursor.take(),
1223 };
1224 let mut page = self.get_sub_api_keys(¶ms).await?;
1225 let next = page.result.continuation_cursor().map(str::to_owned);
1226 keys.append(&mut page.result.keys);
1227
1228 match next {
1229 Some(c) => cursor = Some(c),
1230 None => break,
1231 }
1232 }
1233
1234 Ok(keys)
1235 }
1236
1237 pub async fn get_fee_rate(
1247 &self,
1248 params: &BybitFeeRateParams,
1249 ) -> Result<BybitFeeRateResponse, BybitHttpError> {
1250 self.send_request(
1251 Method::GET,
1252 "/v5/account/fee-rate",
1253 Some(params),
1254 None,
1255 true,
1256 )
1257 .await
1258 }
1259
1260 pub async fn set_margin_mode(
1277 &self,
1278 margin_mode: BybitMarginMode,
1279 ) -> Result<BybitSetMarginModeResponse, BybitHttpError> {
1280 let params = BybitSetMarginModeParamsBuilder::default()
1281 .set_margin_mode(margin_mode)
1282 .build()
1283 .expect("Failed to build BybitSetMarginModeParams");
1284
1285 let body = serde_json::to_vec(¶ms)?;
1286 self.send_request::<_, ()>(
1287 Method::POST,
1288 "/v5/account/set-margin-mode",
1289 None,
1290 Some(body),
1291 true,
1292 )
1293 .await
1294 }
1295
1296 pub async fn set_leverage(
1313 &self,
1314 product_type: BybitProductType,
1315 symbol: &str,
1316 buy_leverage: &str,
1317 sell_leverage: &str,
1318 ) -> Result<BybitSetLeverageResponse, BybitHttpError> {
1319 let params = BybitSetLeverageParamsBuilder::default()
1320 .category(product_type)
1321 .symbol(symbol.to_string())
1322 .buy_leverage(buy_leverage.to_string())
1323 .sell_leverage(sell_leverage.to_string())
1324 .build()
1325 .expect("Failed to build BybitSetLeverageParams");
1326
1327 let body = serde_json::to_vec(¶ms)?;
1328 self.send_request::<_, ()>(
1329 Method::POST,
1330 "/v5/position/set-leverage",
1331 None,
1332 Some(body),
1333 true,
1334 )
1335 .await
1336 }
1337
1338 pub async fn switch_mode(
1355 &self,
1356 product_type: BybitProductType,
1357 mode: BybitPositionMode,
1358 symbol: Option<String>,
1359 coin: Option<String>,
1360 ) -> Result<BybitSwitchModeResponse, BybitHttpError> {
1361 let mut builder = BybitSwitchModeParamsBuilder::default();
1362 builder.category(product_type);
1363 builder.mode(mode);
1364
1365 if let Some(s) = symbol {
1366 builder.symbol(s);
1367 }
1368
1369 if let Some(c) = coin {
1370 builder.coin(c);
1371 }
1372
1373 let params = builder
1374 .build()
1375 .expect("Failed to build BybitSwitchModeParams");
1376
1377 let body = serde_json::to_vec(¶ms)?;
1378 self.send_request::<_, ()>(
1379 Method::POST,
1380 "/v5/position/switch-mode",
1381 None,
1382 Some(body),
1383 true,
1384 )
1385 .await
1386 }
1387
1388 pub async fn set_trading_stop(
1401 &self,
1402 params: &BybitSetTradingStopParams,
1403 ) -> Result<BybitSetTradingStopResponse, BybitHttpError> {
1404 let body = serde_json::to_vec(params)?;
1405 self.send_request::<_, ()>(
1406 Method::POST,
1407 "/v5/position/trading-stop",
1408 None,
1409 Some(body),
1410 true,
1411 )
1412 .await
1413 }
1414
1415 pub async fn borrow(
1432 &self,
1433 coin: &str,
1434 amount: &str,
1435 ) -> Result<BybitBorrowResponse, BybitHttpError> {
1436 let params = BybitBorrowParamsBuilder::default()
1437 .coin(coin.to_string())
1438 .amount(amount.to_string())
1439 .build()
1440 .expect("Failed to build BybitBorrowParams");
1441
1442 let body = serde_json::to_vec(¶ms)?;
1443 self.send_request::<_, ()>(Method::POST, "/v5/account/borrow", None, Some(body), true)
1444 .await
1445 }
1446
1447 pub async fn no_convert_repay(
1465 &self,
1466 coin: &str,
1467 amount: Option<&str>,
1468 ) -> Result<BybitNoConvertRepayResponse, BybitHttpError> {
1469 let mut builder = BybitNoConvertRepayParamsBuilder::default();
1470 builder.coin(coin.to_string());
1471
1472 if let Some(amt) = amount {
1473 builder.amount(amt.to_string());
1474 }
1475
1476 let params = builder
1477 .build()
1478 .expect("Failed to build BybitNoConvertRepayParams");
1479
1480 if let Ok(params_json) = serde_json::to_string(¶ms) {
1481 log::debug!("Repay request params: {params_json}");
1482 }
1483
1484 let body = serde_json::to_vec(¶ms)?;
1485 let result = self
1486 .send_request::<_, ()>(
1487 Method::POST,
1488 "/v5/account/no-convert-repay",
1489 None,
1490 Some(body),
1491 true,
1492 )
1493 .await;
1494
1495 if let Err(ref e) = result
1496 && let Ok(params_json) = serde_json::to_string(¶ms)
1497 {
1498 log::debug!("Repay request failed with params {params_json}: {e}");
1499 }
1500
1501 result
1502 }
1503
1504 pub async fn repay(
1522 &self,
1523 coin: Option<&str>,
1524 amount: Option<&str>,
1525 ) -> Result<BybitRepayResponse, BybitHttpError> {
1526 let mut builder = BybitRepayParamsBuilder::default();
1527
1528 if let Some(coin) = coin {
1529 builder.coin(coin.to_string());
1530 }
1531
1532 if let Some(amt) = amount {
1533 builder.amount(amt.to_string());
1534 }
1535
1536 let params = builder.build().expect("Failed to build BybitRepayParams");
1537
1538 if let Ok(params_json) = serde_json::to_string(¶ms) {
1539 log::debug!("Repay request params: {params_json}");
1540 }
1541
1542 let body = serde_json::to_vec(¶ms)?;
1543 let result = self
1544 .send_request::<_, ()>(Method::POST, "/v5/account/repay", None, Some(body), true)
1545 .await;
1546
1547 if let Err(ref e) = result
1548 && let Ok(params_json) = serde_json::to_string(¶ms)
1549 {
1550 log::debug!("Repay request failed with params {params_json}: {e}");
1551 }
1552
1553 result
1554 }
1555
1556 pub async fn get_tickers<T: DeserializeOwned + BybitResponseCheck>(
1566 &self,
1567 params: &BybitTickersParams,
1568 ) -> Result<T, BybitHttpError> {
1569 self.send_request(Method::GET, "/v5/market/tickers", Some(params), None, false)
1570 .await
1571 }
1572
1573 pub async fn get_trade_history(
1583 &self,
1584 params: &BybitTradeHistoryParams,
1585 ) -> Result<BybitTradeHistoryResponse, BybitHttpError> {
1586 self.send_request(Method::GET, BYBIT_EXECUTION_LIST, Some(params), None, true)
1587 .await
1588 }
1589
1590 pub async fn get_positions(
1603 &self,
1604 params: &BybitPositionListParams,
1605 ) -> Result<BybitPositionListResponse, BybitHttpError> {
1606 self.send_request(Method::GET, BYBIT_POSITION_LIST, Some(params), None, true)
1607 .await
1608 }
1609
1610 #[must_use]
1612 pub fn base_url(&self) -> &str {
1613 &self.base_url
1614 }
1615
1616 #[must_use]
1618 pub fn recv_window_ms(&self) -> u64 {
1619 self.recv_window_ms
1620 }
1621
1622 #[must_use]
1624 pub fn credential(&self) -> Option<&Credential> {
1625 self.credential.as_ref()
1626 }
1627}
1628
1629#[cfg_attr(
1631 feature = "python",
1632 pyo3::pyclass(module = "nautilus_trader.adapters.bybit", from_py_object)
1633)]
1634#[cfg_attr(
1635 feature = "python",
1636 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
1637)]
1638pub struct BybitHttpClient {
1643 pub(crate) inner: Arc<BybitRawHttpClient>,
1644 pub(crate) instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
1645 clock: &'static AtomicTime,
1646 cache_initialized: Arc<AtomicBool>,
1647 use_spot_position_reports: Arc<AtomicBool>,
1648}
1649
1650impl Clone for BybitHttpClient {
1651 fn clone(&self) -> Self {
1652 Self {
1653 inner: self.inner.clone(),
1654 instruments_cache: self.instruments_cache.clone(),
1655 cache_initialized: self.cache_initialized.clone(),
1656 use_spot_position_reports: self.use_spot_position_reports.clone(),
1657 clock: self.clock,
1658 }
1659 }
1660}
1661
1662impl Default for BybitHttpClient {
1663 fn default() -> Self {
1664 Self::new(None, 60, 3, 1000, 10_000, DEFAULT_RECV_WINDOW_MS, None)
1665 .expect("Failed to create default BybitHttpClient")
1666 }
1667}
1668
1669impl Debug for BybitHttpClient {
1670 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1671 f.debug_struct(stringify!(BybitHttpClient))
1672 .field("inner", &self.inner)
1673 .finish()
1674 }
1675}
1676
1677impl BybitHttpClient {
1678 pub fn new(
1684 base_url: Option<String>,
1685 timeout_secs: u64,
1686 max_retries: u32,
1687 retry_delay_ms: u64,
1688 retry_delay_max_ms: u64,
1689 recv_window_ms: u64,
1690 proxy_url: Option<String>,
1691 ) -> Result<Self, BybitHttpError> {
1692 Ok(Self {
1693 inner: Arc::new(BybitRawHttpClient::new(
1694 base_url,
1695 timeout_secs,
1696 max_retries,
1697 retry_delay_ms,
1698 retry_delay_max_ms,
1699 recv_window_ms,
1700 proxy_url,
1701 )?),
1702 instruments_cache: Arc::new(AtomicMap::new()),
1703 cache_initialized: Arc::new(AtomicBool::new(false)),
1704 use_spot_position_reports: Arc::new(AtomicBool::new(false)),
1705 clock: get_atomic_clock_realtime(),
1706 })
1707 }
1708
1709 #[expect(clippy::too_many_arguments)]
1715 pub fn with_credentials(
1716 api_key: String,
1717 api_secret: String,
1718 base_url: Option<String>,
1719 timeout_secs: u64,
1720 max_retries: u32,
1721 retry_delay_ms: u64,
1722 retry_delay_max_ms: u64,
1723 recv_window_ms: u64,
1724 proxy_url: Option<String>,
1725 ) -> Result<Self, BybitHttpError> {
1726 Ok(Self {
1727 inner: Arc::new(BybitRawHttpClient::with_credentials(
1728 api_key,
1729 api_secret,
1730 base_url,
1731 timeout_secs,
1732 max_retries,
1733 retry_delay_ms,
1734 retry_delay_max_ms,
1735 recv_window_ms,
1736 proxy_url,
1737 )?),
1738 instruments_cache: Arc::new(AtomicMap::new()),
1739 cache_initialized: Arc::new(AtomicBool::new(false)),
1740 use_spot_position_reports: Arc::new(AtomicBool::new(false)),
1741 clock: get_atomic_clock_realtime(),
1742 })
1743 }
1744
1745 #[expect(clippy::too_many_arguments)]
1758 pub fn new_with_env(
1759 api_key: Option<String>,
1760 api_secret: Option<String>,
1761 base_url: Option<String>,
1762 demo: bool,
1763 testnet: bool,
1764 timeout_secs: u64,
1765 max_retries: u32,
1766 retry_delay_ms: u64,
1767 retry_delay_max_ms: u64,
1768 recv_window_ms: u64,
1769 proxy_url: Option<String>,
1770 ) -> Result<Self, BybitHttpError> {
1771 Ok(Self {
1772 inner: Arc::new(BybitRawHttpClient::new_with_env(
1773 api_key,
1774 api_secret,
1775 base_url,
1776 demo,
1777 testnet,
1778 timeout_secs,
1779 max_retries,
1780 retry_delay_ms,
1781 retry_delay_max_ms,
1782 recv_window_ms,
1783 proxy_url,
1784 )?),
1785 instruments_cache: Arc::new(AtomicMap::new()),
1786 cache_initialized: Arc::new(AtomicBool::new(false)),
1787 use_spot_position_reports: Arc::new(AtomicBool::new(false)),
1788 clock: get_atomic_clock_realtime(),
1789 })
1790 }
1791
1792 #[must_use]
1793 pub fn base_url(&self) -> &str {
1794 self.inner.base_url()
1795 }
1796
1797 #[must_use]
1798 pub fn recv_window_ms(&self) -> u64 {
1799 self.inner.recv_window_ms()
1800 }
1801
1802 #[must_use]
1803 pub fn credential(&self) -> Option<&Credential> {
1804 self.inner.credential()
1805 }
1806
1807 pub fn set_use_spot_position_reports(&self, use_spot_position_reports: bool) {
1808 self.use_spot_position_reports
1809 .store(use_spot_position_reports, Ordering::Relaxed);
1810 }
1811
1812 pub fn cancel_all_requests(&self) {
1813 self.inner.cancel_all_requests();
1814 }
1815
1816 pub fn reset_cancellation_token(&self) {
1817 self.inner.reset_cancellation_token();
1818 }
1819
1820 pub fn cancellation_token(&self) -> CancellationToken {
1821 self.inner.cancellation_token()
1822 }
1823
1824 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1826 self.instruments_cache
1827 .insert(instrument.symbol().inner(), instrument);
1828 self.cache_initialized.store(true, Ordering::Release);
1829 }
1830
1831 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
1833 self.instruments_cache.rcu(|m| {
1834 for instrument in instruments {
1835 m.insert(instrument.symbol().inner(), instrument.clone());
1836 }
1837 });
1838 self.cache_initialized.store(true, Ordering::Release);
1839 }
1840
1841 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1842 self.instruments_cache.get_cloned(symbol)
1843 }
1844
1845 fn instrument_from_cache(&self, symbol: &Symbol) -> anyhow::Result<InstrumentAny> {
1846 self.get_instrument(&symbol.inner()).ok_or_else(|| {
1847 anyhow::anyhow!(
1848 "Instrument {symbol} not found in cache, ensure instruments loaded first"
1849 )
1850 })
1851 }
1852
1853 #[must_use]
1854 fn generate_ts_init(&self) -> UnixNanos {
1855 self.clock.get_time_ns()
1856 }
1857
1858 pub async fn get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
1870 self.inner.get_server_time().await
1871 }
1872
1873 pub async fn get_instruments<T: DeserializeOwned + BybitResponseCheck>(
1885 &self,
1886 params: &BybitInstrumentsInfoParams,
1887 ) -> Result<T, BybitHttpError> {
1888 self.inner.get_instruments(params).await
1889 }
1890
1891 pub async fn get_instruments_spot(
1903 &self,
1904 params: &BybitInstrumentsInfoParams,
1905 ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
1906 self.inner.get_instruments_spot(params).await
1907 }
1908
1909 pub async fn get_instruments_linear(
1921 &self,
1922 params: &BybitInstrumentsInfoParams,
1923 ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
1924 self.inner.get_instruments_linear(params).await
1925 }
1926
1927 pub async fn get_instruments_inverse(
1939 &self,
1940 params: &BybitInstrumentsInfoParams,
1941 ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
1942 self.inner.get_instruments_inverse(params).await
1943 }
1944
1945 pub async fn get_instruments_option(
1957 &self,
1958 params: &BybitInstrumentsInfoParams,
1959 ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
1960 self.inner.get_instruments_option(params).await
1961 }
1962
1963 pub async fn get_klines(
1975 &self,
1976 params: &BybitKlinesParams,
1977 ) -> Result<BybitKlinesResponse, BybitHttpError> {
1978 self.inner.get_klines(params).await
1979 }
1980
1981 pub async fn get_recent_trades(
1993 &self,
1994 params: &BybitTradesParams,
1995 ) -> Result<BybitTradesResponse, BybitHttpError> {
1996 self.inner.get_recent_trades(params).await
1997 }
1998
1999 #[expect(clippy::too_many_arguments)]
2011 pub async fn get_open_orders(
2012 &self,
2013 category: BybitProductType,
2014 symbol: Option<String>,
2015 base_coin: Option<String>,
2016 settle_coin: Option<String>,
2017 order_id: Option<String>,
2018 order_link_id: Option<String>,
2019 open_only: Option<BybitOpenOnly>,
2020 order_filter: Option<BybitOrderFilter>,
2021 limit: Option<u32>,
2022 cursor: Option<String>,
2023 ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
2024 self.inner
2025 .get_open_orders(
2026 category,
2027 symbol,
2028 base_coin,
2029 settle_coin,
2030 order_id,
2031 order_link_id,
2032 open_only,
2033 order_filter,
2034 limit,
2035 cursor,
2036 )
2037 .await
2038 }
2039
2040 pub async fn place_order(
2052 &self,
2053 request: &serde_json::Value,
2054 ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
2055 self.inner.place_order(request).await
2056 }
2057
2058 pub async fn get_wallet_balance(
2070 &self,
2071 params: &BybitWalletBalanceParams,
2072 ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
2073 self.inner.get_wallet_balance(params).await
2074 }
2075
2076 pub async fn get_account_info(&self) -> Result<BybitAccountInfoResponse, BybitHttpError> {
2088 self.inner.get_account_info().await
2089 }
2090
2091 pub async fn get_account_details(&self) -> Result<BybitAccountDetailsResponse, BybitHttpError> {
2103 self.inner.get_account_details().await
2104 }
2105
2106 pub async fn update_sub_api_key(
2118 &self,
2119 params: &BybitUpdateSubApiParams,
2120 ) -> Result<BybitUpdateSubApiResponse, BybitHttpError> {
2121 self.inner.update_sub_api_key(params).await
2122 }
2123
2124 pub async fn update_master_api_key(
2136 &self,
2137 params: &BybitUpdateMasterApiParams,
2138 ) -> Result<BybitUpdateMasterApiResponse, BybitHttpError> {
2139 self.inner.update_master_api_key(params).await
2140 }
2141
2142 pub async fn get_sub_members(&self) -> Result<BybitSubMembersResponse, BybitHttpError> {
2154 self.inner.get_sub_members().await
2155 }
2156
2157 pub async fn get_sub_members_paged(
2169 &self,
2170 params: &BybitSubMembersPageParams,
2171 ) -> Result<BybitSubMembersPagedResponse, BybitHttpError> {
2172 self.inner.get_sub_members_paged(params).await
2173 }
2174
2175 pub async fn get_escrow_sub_members(
2187 &self,
2188 params: &BybitSubMembersPageParams,
2189 ) -> Result<BybitEscrowSubMembersResponse, BybitHttpError> {
2190 self.inner.get_escrow_sub_members(params).await
2191 }
2192
2193 pub async fn get_sub_api_keys(
2205 &self,
2206 params: &BybitSubApiKeysParams,
2207 ) -> Result<BybitSubApiKeysResponse, BybitHttpError> {
2208 self.inner.get_sub_api_keys(params).await
2209 }
2210
2211 pub async fn get_positions(
2224 &self,
2225 params: &BybitPositionListParams,
2226 ) -> Result<BybitPositionListResponse, BybitHttpError> {
2227 self.inner.get_positions(params).await
2228 }
2229
2230 pub async fn get_fee_rate(
2243 &self,
2244 params: &BybitFeeRateParams,
2245 ) -> Result<BybitFeeRateResponse, BybitHttpError> {
2246 self.inner.get_fee_rate(params).await
2247 }
2248
2249 pub async fn set_margin_mode(
2262 &self,
2263 margin_mode: BybitMarginMode,
2264 ) -> Result<BybitSetMarginModeResponse, BybitHttpError> {
2265 self.inner.set_margin_mode(margin_mode).await
2266 }
2267
2268 pub async fn set_leverage(
2281 &self,
2282 product_type: BybitProductType,
2283 symbol: &str,
2284 buy_leverage: &str,
2285 sell_leverage: &str,
2286 ) -> Result<BybitSetLeverageResponse, BybitHttpError> {
2287 self.inner
2288 .set_leverage(product_type, symbol, buy_leverage, sell_leverage)
2289 .await
2290 }
2291
2292 pub async fn switch_mode(
2305 &self,
2306 product_type: BybitProductType,
2307 mode: BybitPositionMode,
2308 symbol: Option<String>,
2309 coin: Option<String>,
2310 ) -> Result<BybitSwitchModeResponse, BybitHttpError> {
2311 self.inner
2312 .switch_mode(product_type, mode, symbol, coin)
2313 .await
2314 }
2315
2316 pub async fn set_trading_stop(
2329 &self,
2330 params: &BybitSetTradingStopParams,
2331 ) -> Result<BybitSetTradingStopResponse, BybitHttpError> {
2332 self.inner.set_trading_stop(params).await
2333 }
2334
2335 pub async fn get_spot_borrow_amount(&self, coin: &str) -> anyhow::Result<Decimal> {
2350 let params = BybitWalletBalanceParams {
2351 account_type: BybitAccountType::Unified,
2352 coin: Some(coin.to_string()),
2353 };
2354
2355 let response = self.inner.get_wallet_balance(¶ms).await?;
2356
2357 let borrow_amount = response
2358 .result
2359 .list
2360 .first()
2361 .and_then(|wallet| wallet.coin.iter().find(|c| c.coin == coin))
2362 .map_or(Decimal::ZERO, |balance| balance.spot_borrow);
2363
2364 Ok(borrow_amount)
2365 }
2366
2367 pub async fn borrow_spot(
2383 &self,
2384 coin: &str,
2385 amount: Quantity,
2386 ) -> anyhow::Result<BybitBorrowResponse> {
2387 let amount_str = amount.to_string();
2388 self.inner
2389 .borrow(coin, &amount_str)
2390 .await
2391 .map_err(|e| anyhow::anyhow!("Failed to borrow {amount} {coin}: {e}"))
2392 }
2393
2394 pub async fn repay_spot_borrow(
2411 &self,
2412 coin: &str,
2413 amount: Option<Quantity>,
2414 ) -> anyhow::Result<BybitNoConvertRepayResponse> {
2415 let amount_str = amount.as_ref().map(|q| q.to_string());
2416 let response = self
2417 .inner
2418 .no_convert_repay(coin, amount_str.as_deref())
2419 .await
2420 .map_err(|e| anyhow::anyhow!("Failed to repay spot borrow for {coin}: {e}"))?;
2421 Self::ensure_repay_accepted(coin, response.result.result_status)?;
2422 Ok(response)
2423 }
2424
2425 pub async fn repay_spot_borrow_with_conversion(
2443 &self,
2444 coin: &str,
2445 amount: Option<Quantity>,
2446 ) -> anyhow::Result<BybitRepayResponse> {
2447 let amount_str = amount.as_ref().map(|q| q.to_string());
2448 let response = self
2449 .inner
2450 .repay(Some(coin), amount_str.as_deref())
2451 .await
2452 .map_err(|e| {
2453 anyhow::anyhow!("Failed to repay spot borrow (with conversion) for {coin}: {e}")
2454 })?;
2455 Self::ensure_repay_accepted(coin, response.result.result_status)?;
2456 Ok(response)
2457 }
2458
2459 fn ensure_repay_accepted(coin: &str, status: BybitRepayStatus) -> anyhow::Result<()> {
2460 anyhow::ensure!(
2461 status != BybitRepayStatus::Failed,
2462 "Bybit repay for {coin} returned result status {status}"
2463 );
2464 Ok(())
2465 }
2466
2467 async fn generate_spot_position_reports_from_wallet(
2475 &self,
2476 account_id: AccountId,
2477 instrument_id: InstrumentId,
2478 ) -> anyhow::Result<Vec<PositionStatusReport>> {
2479 let params = BybitWalletBalanceParams {
2480 account_type: BybitAccountType::Unified,
2481 coin: None,
2482 };
2483
2484 let response = self.inner.get_wallet_balance(¶ms).await?;
2485 let ts_init = self.generate_ts_init();
2486
2487 let mut wallet_by_coin: HashMap<Ustr, Decimal> = HashMap::new();
2488
2489 for wallet in &response.result.list {
2490 for coin_balance in &wallet.coin {
2491 let balance = coin_balance.wallet_balance - coin_balance.spot_borrow;
2492 *wallet_by_coin
2493 .entry(coin_balance.coin)
2494 .or_insert(Decimal::ZERO) += balance;
2495 }
2496 }
2497
2498 let mut reports = Vec::new();
2499
2500 if let Some(instrument) = self
2501 .instruments_cache
2502 .get_cloned(&instrument_id.symbol.inner())
2503 {
2504 let base_currency = instrument
2505 .base_currency()
2506 .expect("SPOT instrument should have base currency");
2507 let coin = base_currency.code;
2508 let wallet_balance = wallet_by_coin.get(&coin).copied().unwrap_or(Decimal::ZERO);
2509
2510 let side = if wallet_balance > Decimal::ZERO {
2511 PositionSide::Long
2512 } else if wallet_balance < Decimal::ZERO {
2513 PositionSide::Short
2514 } else {
2515 PositionSide::Flat
2516 };
2517
2518 let abs_balance = wallet_balance.abs();
2519 let quantity = Quantity::from_decimal_dp(abs_balance, instrument.size_precision())?;
2520
2521 let report = PositionStatusReport::new(
2522 account_id,
2523 instrument_id,
2524 side,
2525 quantity,
2526 ts_init,
2527 ts_init,
2528 None,
2529 None,
2530 None,
2531 );
2532
2533 reports.push(report);
2534 }
2535
2536 Ok(reports)
2537 }
2538
2539 #[expect(clippy::too_many_arguments)]
2550 pub async fn submit_order(
2551 &self,
2552 account_id: AccountId,
2553 product_type: BybitProductType,
2554 instrument_id: InstrumentId,
2555 client_order_id: ClientOrderId,
2556 order_side: OrderSide,
2557 order_type: OrderType,
2558 quantity: Quantity,
2559 time_in_force: Option<TimeInForce>,
2560 price: Option<Price>,
2561 trigger_price: Option<Price>,
2562 post_only: Option<bool>,
2563 reduce_only: bool,
2564 is_quote_quantity: bool,
2565 is_leverage: bool,
2566 position_idx: Option<BybitPositionIdx>,
2567 bbo_side_type: Option<BybitBboSideType>,
2568 bbo_level: Option<String>,
2569 smp_type: Option<BybitOrderSmpType>,
2570 native_tp_sl: Option<&BybitNativeTpSlParams>,
2571 ) -> anyhow::Result<OrderStatusReport> {
2572 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2573 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2574
2575 let bybit_side = match order_side {
2576 OrderSide::Buy => BybitOrderSide::Buy,
2577 OrderSide::Sell => BybitOrderSide::Sell,
2578 };
2579
2580 let (bybit_order_type, is_stop_order) = match order_type {
2582 OrderType::Market => (BybitOrderType::Market, false),
2583 OrderType::Limit => (BybitOrderType::Limit, false),
2584 OrderType::StopMarket | OrderType::MarketIfTouched => (BybitOrderType::Market, true),
2585 OrderType::StopLimit | OrderType::LimitIfTouched => (BybitOrderType::Limit, true),
2586 _ => anyhow::bail!("Unsupported order type: {order_type:?}"),
2587 };
2588
2589 let bybit_tif = map_time_in_force(bybit_order_type, time_in_force, post_only)
2590 .map_err(|tif| anyhow::anyhow!("Unsupported time in force: {tif:?}"))?;
2591 let market_unit = spot_market_unit(product_type, bybit_order_type, is_quote_quantity);
2592 let trigger_dir = trigger_direction(order_type, order_side, is_stop_order);
2593
2594 let mut order_entry = BybitBatchPlaceOrderEntryBuilder::default();
2595 order_entry.symbol(bybit_symbol.raw_symbol().to_string());
2596 order_entry.side(bybit_side);
2597 order_entry.order_type(bybit_order_type);
2598 order_entry.qty(quantity.to_string());
2599 order_entry.time_in_force(bybit_tif);
2600 order_entry.order_link_id(client_order_id.to_string());
2601 order_entry.market_unit(market_unit);
2602 order_entry.trigger_direction(trigger_dir);
2603
2604 if bbo_side_type.is_none()
2605 && let Some(price) = price
2606 {
2607 order_entry.price(Some(price.to_string()));
2608 }
2609
2610 if let Some(trigger_price) = trigger_price {
2611 order_entry.trigger_price(Some(trigger_price.to_string()));
2612 }
2613
2614 if reduce_only {
2615 order_entry.reduce_only(Some(true));
2616 }
2617
2618 order_entry.is_leverage(spot_leverage(product_type, is_leverage));
2619
2620 if let Some(idx) = position_idx {
2621 order_entry.position_idx(Some(idx));
2622 }
2623
2624 order_entry.bbo_side_type(bbo_side_type);
2625 order_entry.bbo_level(bbo_level);
2626 order_entry.smp_type(smp_type);
2627
2628 if let Some(tp_sl) = native_tp_sl {
2629 if let Some(ref tp) = tp_sl.take_profit {
2630 order_entry.take_profit(Some(tp.clone()));
2631 }
2632
2633 if let Some(ref sl) = tp_sl.stop_loss {
2634 order_entry.stop_loss(Some(sl.clone()));
2635 }
2636
2637 if let Some(tp_trigger) = tp_sl.tp_trigger_by {
2638 order_entry.tp_trigger_by(Some(tp_trigger));
2639 }
2640
2641 if let Some(sl_trigger) = tp_sl.sl_trigger_by {
2642 order_entry.sl_trigger_by(Some(sl_trigger));
2643 }
2644
2645 if let Some(tp_ot) = tp_sl.tp_order_type {
2646 order_entry.tp_order_type(Some(tp_ot));
2647 }
2648
2649 if let Some(sl_ot) = tp_sl.sl_order_type {
2650 order_entry.sl_order_type(Some(sl_ot));
2651 }
2652
2653 if let Some(ref tp_lp) = tp_sl.tp_limit_price {
2654 order_entry.tp_limit_price(Some(tp_lp.clone()));
2655 }
2656
2657 if let Some(ref sl_lp) = tp_sl.sl_limit_price {
2658 order_entry.sl_limit_price(Some(sl_lp.clone()));
2659 }
2660
2661 let mode = tp_sl.tpsl_mode.or_else(|| {
2664 (tp_sl.take_profit.is_some() || tp_sl.stop_loss.is_some())
2665 .then_some(BybitTpSlMode::Full)
2666 });
2667
2668 if let Some(m) = mode {
2669 order_entry.tpsl_mode(Some(m));
2670 }
2671
2672 if let Some(close) = tp_sl.close_on_trigger {
2673 order_entry.close_on_trigger(Some(close));
2674 }
2675
2676 if let Some(ref iv) = tp_sl.order_iv {
2677 order_entry.order_iv(Some(iv.clone()));
2678 }
2679
2680 if let Some(mmp) = tp_sl.mmp {
2681 order_entry.mmp(Some(mmp));
2682 }
2683 }
2684
2685 let order_entry = order_entry.build().build_anyhow()?;
2686
2687 let mut params = BybitPlaceOrderParamsBuilder::default();
2688 params.category(product_type);
2689 params.order(order_entry);
2690
2691 let params = params.build().build_anyhow()?;
2692
2693 let body = serde_json::to_value(¶ms)?;
2694 let response = self.inner.place_order(&body).await?;
2695
2696 let order_id = response
2697 .result
2698 .order_id
2699 .ok_or(BybitSubmitOrderError::MissingOrderId)?;
2700
2701 let order = self
2702 .query_order_by_id(
2703 product_type,
2704 order_id.as_str(),
2705 BYBIT_ORDER_REALTIME,
2706 "after submission",
2707 )
2708 .await
2709 .map_err(|source| BybitSubmitOrderError::PostSubmitLookup { source })?;
2710
2711 let is_rejection = order.order_status == crate::common::enums::BybitOrderStatus::Rejected
2716 || (order.order_status == crate::common::enums::BybitOrderStatus::Canceled
2717 && bybit_rejection_due_post_only(order.reject_reason.as_str()));
2718 if is_rejection && (order.cum_exec_qty.as_str() == "0" || order.cum_exec_qty.is_empty()) {
2719 return Err(BybitSubmitOrderError::Rejected {
2720 reason: order.reject_reason.to_string(),
2721 }
2722 .into());
2723 }
2724
2725 let ts_init = self.generate_ts_init();
2726
2727 parse_order_status_report(&order, &instrument, account_id, ts_init)
2728 }
2729
2730 pub async fn cancel_order(
2740 &self,
2741 account_id: AccountId,
2742 product_type: BybitProductType,
2743 instrument_id: InstrumentId,
2744 client_order_id: Option<ClientOrderId>,
2745 venue_order_id: Option<VenueOrderId>,
2746 ) -> anyhow::Result<OrderStatusReport> {
2747 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2748 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2749
2750 let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
2751 cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
2752
2753 if let Some(venue_order_id) = venue_order_id {
2754 cancel_entry.order_id(venue_order_id.to_string());
2755 } else if let Some(client_order_id) = client_order_id {
2756 cancel_entry.order_link_id(client_order_id.to_string());
2757 } else {
2758 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2759 }
2760
2761 let cancel_entry = cancel_entry.build().build_anyhow()?;
2762
2763 let mut params = BybitCancelOrderParamsBuilder::default();
2764 params.category(product_type);
2765 params.order(cancel_entry);
2766
2767 let params = params.build().build_anyhow()?;
2768 let body = serde_json::to_vec(¶ms)?;
2769
2770 let response: BybitPlaceOrderResponse = self
2771 .inner
2772 .send_request::<_, ()>(Method::POST, "/v5/order/cancel", None, Some(body), true)
2773 .await?;
2774
2775 let order_id = response
2776 .result
2777 .order_id
2778 .ok_or(BybitCancelOrderError::MissingOrderId)?;
2779
2780 let order = self
2781 .query_order_by_id(
2782 product_type,
2783 order_id.as_str(),
2784 BYBIT_ORDER_HISTORY,
2785 "after cancellation",
2786 )
2787 .await
2788 .map_err(|source| BybitCancelOrderError::PostCancelLookup { source })?;
2789
2790 let ts_init = self.generate_ts_init();
2791
2792 parse_order_status_report(&order, &instrument, account_id, ts_init)
2793 }
2794
2795 pub async fn batch_cancel_orders(
2805 &self,
2806 account_id: AccountId,
2807 product_type: BybitProductType,
2808 instrument_ids: Vec<InstrumentId>,
2809 client_order_ids: Vec<Option<ClientOrderId>>,
2810 venue_order_ids: Vec<Option<VenueOrderId>>,
2811 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2812 if instrument_ids.len() != client_order_ids.len()
2813 || instrument_ids.len() != venue_order_ids.len()
2814 {
2815 anyhow::bail!(
2816 "instrument_ids, client_order_ids, and venue_order_ids must have the same length"
2817 );
2818 }
2819
2820 if instrument_ids.is_empty() {
2821 return Ok(Vec::new());
2822 }
2823
2824 let call_limit = batch_call_limit(product_type);
2825 if instrument_ids.len() > call_limit {
2826 anyhow::bail!(
2827 "Batch cancel limit is {call_limit} orders for {}",
2828 product_type.as_str()
2829 );
2830 }
2831
2832 let mut cancel_entries = Vec::new();
2833
2834 for ((instrument_id, client_order_id), venue_order_id) in instrument_ids
2835 .iter()
2836 .zip(client_order_ids.iter())
2837 .zip(venue_order_ids.iter())
2838 {
2839 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2840 let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
2841 cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
2842
2843 if let Some(venue_order_id) = venue_order_id {
2844 cancel_entry.order_id(venue_order_id.to_string());
2845 } else if let Some(client_order_id) = client_order_id {
2846 cancel_entry.order_link_id(client_order_id.to_string());
2847 } else {
2848 anyhow::bail!(
2849 "Either client_order_id or venue_order_id must be provided for each order"
2850 );
2851 }
2852
2853 cancel_entries.push(cancel_entry.build().build_anyhow()?);
2854 }
2855
2856 let chunk_limit = batch_endpoint_limit(product_type).min(batch_send_limit(product_type));
2857 for chunk in cancel_entries.chunks(chunk_limit) {
2858 let mut params = BybitBatchCancelOrderParamsBuilder::default();
2859 params.category(product_type);
2860 params.request(chunk.to_vec());
2861
2862 let params = params.build().build_anyhow()?;
2863 let body = serde_json::to_vec(¶ms)?;
2864
2865 let _response: BybitPlaceOrderResponse = self
2866 .inner
2867 .send_request::<_, ()>(
2868 Method::POST,
2869 "/v5/order/cancel-batch",
2870 None,
2871 Some(body),
2872 true,
2873 )
2874 .await?;
2875 }
2876
2877 let mut reports = Vec::new();
2879
2880 for (instrument_id, (client_order_id, venue_order_id)) in instrument_ids
2881 .iter()
2882 .zip(client_order_ids.iter().zip(venue_order_ids.iter()))
2883 {
2884 let Ok(instrument) = self.instrument_from_cache(&instrument_id.symbol) else {
2885 log::debug!(
2886 "Skipping cancelled order report for instrument not in cache: symbol={}",
2887 instrument_id.symbol
2888 );
2889 continue;
2890 };
2891
2892 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2893
2894 let mut query_params = BybitOpenOrdersParamsBuilder::default();
2895 query_params.category(product_type);
2896 query_params.symbol(bybit_symbol.raw_symbol().to_string());
2897
2898 if let Some(venue_order_id) = venue_order_id {
2899 query_params.order_id(venue_order_id.to_string());
2900 } else if let Some(client_order_id) = client_order_id {
2901 query_params.order_link_id(client_order_id.to_string());
2902 }
2903
2904 let query_params = query_params.build().build_anyhow()?;
2905 let order_response: BybitOrderHistoryResponse = self
2906 .inner
2907 .send_request(
2908 Method::GET,
2909 BYBIT_ORDER_HISTORY,
2910 Some(&query_params),
2911 None,
2912 true,
2913 )
2914 .await?;
2915
2916 if let Some(order) = order_response.result.list.into_iter().next() {
2917 let ts_init = self.generate_ts_init();
2918 let report = parse_order_status_report(&order, &instrument, account_id, ts_init)?;
2919 reports.push(report);
2920 }
2921 }
2922
2923 Ok(reports)
2924 }
2925
2926 pub async fn cancel_all_orders(
2935 &self,
2936 account_id: AccountId,
2937 product_type: BybitProductType,
2938 instrument_id: InstrumentId,
2939 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2940 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2941 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2942
2943 let mut params = BybitCancelAllOrdersParamsBuilder::default();
2944 params.category(product_type);
2945 params.symbol(bybit_symbol.raw_symbol().to_string());
2946
2947 let params = params.build().build_anyhow()?;
2948 let body = serde_json::to_vec(¶ms)?;
2949
2950 let _response: crate::common::models::BybitListResponse<serde_json::Value> = self
2951 .inner
2952 .send_request::<_, ()>(Method::POST, "/v5/order/cancel-all", None, Some(body), true)
2953 .await?;
2954
2955 let mut query_params = BybitOrderHistoryParamsBuilder::default();
2957 query_params.category(product_type);
2958 query_params.symbol(bybit_symbol.raw_symbol().to_string());
2959 query_params.limit(50u32);
2960
2961 let query_params = query_params.build().build_anyhow()?;
2962 let order_response: BybitOrderHistoryResponse = self
2963 .inner
2964 .send_request(
2965 Method::GET,
2966 BYBIT_ORDER_HISTORY,
2967 Some(&query_params),
2968 None,
2969 true,
2970 )
2971 .await?;
2972
2973 let ts_init = self.generate_ts_init();
2974
2975 let mut reports = Vec::new();
2976
2977 for order in order_response.result.list {
2978 if let Ok(report) = parse_order_status_report(&order, &instrument, account_id, ts_init)
2979 {
2980 reports.push(report);
2981 }
2982 }
2983
2984 Ok(reports)
2985 }
2986
2987 #[expect(clippy::too_many_arguments)]
2998 pub async fn modify_order(
2999 &self,
3000 account_id: AccountId,
3001 product_type: BybitProductType,
3002 instrument_id: InstrumentId,
3003 client_order_id: Option<ClientOrderId>,
3004 venue_order_id: Option<VenueOrderId>,
3005 quantity: Option<Quantity>,
3006 price: Option<Price>,
3007 ) -> anyhow::Result<OrderStatusReport> {
3008 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
3009 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3010
3011 let mut amend_entry = BybitBatchAmendOrderEntryBuilder::default();
3012 amend_entry.symbol(bybit_symbol.raw_symbol().to_string());
3013
3014 if let Some(venue_order_id) = venue_order_id {
3015 amend_entry.order_id(venue_order_id.to_string());
3016 } else if let Some(client_order_id) = client_order_id {
3017 amend_entry.order_link_id(client_order_id.to_string());
3018 } else {
3019 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
3020 }
3021
3022 if let Some(quantity) = quantity {
3023 amend_entry.qty(Some(quantity.to_string()));
3024 }
3025
3026 if let Some(price) = price {
3027 amend_entry.price(Some(price.to_string()));
3028 }
3029
3030 let amend_entry = amend_entry.build().build_anyhow()?;
3031
3032 let mut params = BybitAmendOrderParamsBuilder::default();
3033 params.category(product_type);
3034 params.order(amend_entry);
3035
3036 let params = params.build().build_anyhow()?;
3037 let body = serde_json::to_vec(¶ms)?;
3038
3039 let response: BybitPlaceOrderResponse = self
3040 .inner
3041 .send_request::<_, ()>(Method::POST, "/v5/order/amend", None, Some(body), true)
3042 .await?;
3043
3044 let order_id = response
3045 .result
3046 .order_id
3047 .ok_or(BybitModifyOrderError::MissingOrderId)?;
3048
3049 let order = self
3050 .query_order_by_id(
3051 product_type,
3052 order_id.as_str(),
3053 BYBIT_ORDER_REALTIME,
3054 "after amendment",
3055 )
3056 .await
3057 .map_err(|source| BybitModifyOrderError::PostModifyLookup { source })?;
3058
3059 let ts_init = self.generate_ts_init();
3060
3061 parse_order_status_report(&order, &instrument, account_id, ts_init)
3062 }
3063
3064 pub async fn query_order(
3073 &self,
3074 account_id: AccountId,
3075 product_type: BybitProductType,
3076 instrument_id: InstrumentId,
3077 client_order_id: Option<ClientOrderId>,
3078 venue_order_id: Option<VenueOrderId>,
3079 ) -> anyhow::Result<Option<OrderStatusReport>> {
3080 log::debug!(
3081 "query_order: instrument_id={instrument_id}, client_order_id={client_order_id:?}, venue_order_id={venue_order_id:?}"
3082 );
3083
3084 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3085
3086 let mut params = BybitOpenOrdersParamsBuilder::default();
3087 params.category(product_type);
3088 params.symbol(bybit_symbol.raw_symbol().to_string());
3090
3091 if let Some(venue_order_id) = venue_order_id {
3092 params.order_id(venue_order_id.to_string());
3093 } else if let Some(client_order_id) = client_order_id {
3094 params.order_link_id(client_order_id.to_string());
3095 } else {
3096 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
3097 }
3098
3099 let params = params.build().build_anyhow()?;
3100 let mut response: BybitOpenOrdersResponse = self
3101 .inner
3102 .send_request(Method::GET, BYBIT_ORDER_REALTIME, Some(¶ms), None, true)
3103 .await?;
3104
3105 if response.result.list.is_empty() && product_type != BybitProductType::Option {
3107 log::debug!("Order not found in open orders, trying with StopOrder filter");
3108
3109 let mut stop_params = BybitOpenOrdersParamsBuilder::default();
3110 stop_params.category(product_type);
3111 stop_params.symbol(bybit_symbol.raw_symbol().to_string());
3112 stop_params.order_filter(BybitOrderFilter::StopOrder);
3113
3114 if let Some(venue_order_id) = venue_order_id {
3115 stop_params.order_id(venue_order_id.to_string());
3116 } else if let Some(client_order_id) = client_order_id {
3117 stop_params.order_link_id(client_order_id.to_string());
3118 }
3119
3120 let stop_params = stop_params.build().build_anyhow()?;
3121 response = self
3122 .inner
3123 .send_request(
3124 Method::GET,
3125 BYBIT_ORDER_REALTIME,
3126 Some(&stop_params),
3127 None,
3128 true,
3129 )
3130 .await?;
3131 }
3132
3133 if response.result.list.is_empty() {
3135 log::debug!("Order not found in open orders, checking order history");
3136
3137 let mut history_params = BybitOrderHistoryParamsBuilder::default();
3138 history_params.category(product_type);
3139 history_params.symbol(bybit_symbol.raw_symbol().to_string());
3140
3141 if let Some(venue_order_id) = venue_order_id {
3142 history_params.order_id(venue_order_id.to_string());
3143 } else if let Some(client_order_id) = client_order_id {
3144 history_params.order_link_id(client_order_id.to_string());
3145 }
3146
3147 let history_params = history_params.build().build_anyhow()?;
3148
3149 let mut history_response: BybitOrderHistoryResponse = self
3150 .inner
3151 .send_request(
3152 Method::GET,
3153 BYBIT_ORDER_HISTORY,
3154 Some(&history_params),
3155 None,
3156 true,
3157 )
3158 .await?;
3159
3160 if history_response.result.list.is_empty() && product_type == BybitProductType::Option {
3161 log::debug!("Option order not found in order history");
3162 return Ok(None);
3163 }
3164
3165 if history_response.result.list.is_empty() && product_type != BybitProductType::Option {
3167 log::debug!("Order not found in order history, trying with StopOrder filter");
3168
3169 let mut stop_history_params = BybitOrderHistoryParamsBuilder::default();
3170 stop_history_params.category(product_type);
3171 stop_history_params.symbol(bybit_symbol.raw_symbol().to_string());
3172 stop_history_params.order_filter(BybitOrderFilter::StopOrder);
3173
3174 if let Some(venue_order_id) = venue_order_id {
3175 stop_history_params.order_id(venue_order_id.to_string());
3176 } else if let Some(client_order_id) = client_order_id {
3177 stop_history_params.order_link_id(client_order_id.to_string());
3178 }
3179
3180 let stop_history_params = stop_history_params
3181 .build()
3182 .map_err(|e| anyhow::anyhow!(e))?;
3183
3184 history_response = self
3185 .inner
3186 .send_request(
3187 Method::GET,
3188 BYBIT_ORDER_HISTORY,
3189 Some(&stop_history_params),
3190 None,
3191 true,
3192 )
3193 .await?;
3194
3195 if history_response.result.list.is_empty() {
3196 log::debug!("Order not found in order history with StopOrder filter either");
3197 return Ok(None);
3198 }
3199 }
3200
3201 response.result.list = history_response.result.list;
3203 }
3204
3205 let order = &response.result.list[0];
3206 let ts_init = self.generate_ts_init();
3207
3208 log::debug!(
3209 "Query order response: symbol={}, order_id={}, order_link_id={}",
3210 order.symbol.as_str(),
3211 order.order_id.as_str(),
3212 order.order_link_id.as_str()
3213 );
3214
3215 let instrument = self
3216 .instrument_from_cache(&instrument_id.symbol)
3217 .map_err(|e| {
3218 anyhow::anyhow!(
3219 "Failed to query order {}: {}",
3220 client_order_id
3221 .as_ref()
3222 .map(|id| id.to_string())
3223 .or_else(|| venue_order_id.as_ref().map(|id| id.to_string()))
3224 .unwrap_or_else(|| "unknown".to_string()),
3225 e
3226 )
3227 })?;
3228
3229 log::debug!("Retrieved instrument from cache: id={}", instrument.id());
3230
3231 let report =
3232 parse_order_status_report(order, &instrument, account_id, ts_init).map_err(|e| {
3233 log::debug!(
3234 "Failed to parse order status report for {}: {}",
3235 order.order_link_id.as_str(),
3236 e
3237 );
3238 e
3239 })?;
3240
3241 log::debug!(
3242 "Successfully created OrderStatusReport for {}",
3243 order.order_link_id.as_str()
3244 );
3245
3246 Ok(Some(report))
3247 }
3248
3249 async fn paginate_instruments<D, F>(
3250 &self,
3251 product_type: BybitProductType,
3252 symbol: &Option<String>,
3253 base_coin: Option<Ustr>,
3254 mut parse: F,
3255 ) -> anyhow::Result<Vec<InstrumentAny>>
3256 where
3257 D: DeserializeOwned,
3258 BybitCursorListResponse<D>: BybitResponseCheck,
3259 F: FnMut(&D) -> Option<InstrumentAny>,
3260 {
3261 let mut instruments = Vec::new();
3262 let mut cursor: Option<String> = None;
3263 let mut cursor_walk = CursorWalk::default();
3264
3265 loop {
3266 let params = BybitInstrumentsInfoParams {
3267 category: product_type,
3268 symbol: symbol.clone(),
3269 status: None,
3270 base_coin: base_coin.map(|u| u.to_string()),
3271 limit: Some(1000),
3272 cursor: cursor.clone(),
3273 };
3274
3275 let response: BybitCursorListResponse<D> = self.inner.get_instruments(¶ms).await?;
3276
3277 for definition in &response.result.list {
3278 if let Some(instrument) = parse(definition) {
3279 instruments.push(instrument);
3280 }
3281 }
3282
3283 cursor = match cursor_walk
3287 .advance(BYBIT_INSTRUMENTS_INFO, response.result.next_page_cursor)
3288 {
3289 Ok(cursor) => cursor,
3290 Err(e) => {
3291 log::warn!(
3292 "{e}, keeping the {} instrument(s) already read",
3293 instruments.len()
3294 );
3295 break;
3296 }
3297 };
3298
3299 if cursor.is_none() {
3300 break;
3301 }
3302 }
3303
3304 Ok(instruments)
3305 }
3306
3307 pub async fn request_instrument_statuses(
3317 &self,
3318 product_type: BybitProductType,
3319 ) -> anyhow::Result<AHashMap<InstrumentId, MarketStatusAction>> {
3320 let mut statuses = AHashMap::new();
3321 let mut cursor: Option<String> = None;
3322 let mut cursor_walk = CursorWalk::default();
3323
3324 loop {
3325 let params = BybitInstrumentsInfoParams {
3326 category: product_type,
3327 symbol: None,
3328 status: None,
3329 base_coin: None,
3330 limit: Some(1000),
3331 cursor: cursor.clone(),
3332 };
3333
3334 match product_type {
3335 BybitProductType::Spot => {
3336 let response: BybitCursorListResponse<BybitInstrumentSpot> =
3337 self.inner.get_instruments(¶ms).await?;
3338
3339 for def in &response.result.list {
3340 let symbol = make_bybit_symbol(def.symbol, product_type);
3341 let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3342 statuses.insert(id, MarketStatusAction::from(def.status));
3343 }
3344 cursor = response.result.next_page_cursor;
3345 }
3346 BybitProductType::Linear => {
3347 let response: BybitCursorListResponse<BybitInstrumentLinear> =
3348 self.inner.get_instruments(¶ms).await?;
3349
3350 for def in &response.result.list {
3351 let symbol = make_bybit_symbol(def.symbol, product_type);
3352 let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3353 let status = MarketStatusAction::from(def.status);
3354 if status == MarketStatusAction::Trading
3355 && def.contract_type == BybitContractType::LinearPerpetual
3356 && def.delivery_time != "0"
3357 {
3358 statuses.insert(id, MarketStatusAction::PreClose);
3359 } else {
3360 statuses.insert(id, status);
3361 }
3362 }
3363 cursor = response.result.next_page_cursor;
3364 }
3365 BybitProductType::Inverse => {
3366 let response: BybitCursorListResponse<BybitInstrumentInverse> =
3367 self.inner.get_instruments(¶ms).await?;
3368
3369 for def in &response.result.list {
3370 let symbol = make_bybit_symbol(def.symbol, product_type);
3371 let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3372 let status = MarketStatusAction::from(def.status);
3373 if status == MarketStatusAction::Trading
3374 && def.contract_type == BybitContractType::InversePerpetual
3375 && def.delivery_time != "0"
3376 {
3377 statuses.insert(id, MarketStatusAction::PreClose);
3378 } else {
3379 statuses.insert(id, status);
3380 }
3381 }
3382 cursor = response.result.next_page_cursor;
3383 }
3384 BybitProductType::Option => {
3385 let response: BybitCursorListResponse<BybitInstrumentOption> =
3386 self.inner.get_instruments(¶ms).await?;
3387
3388 for def in &response.result.list {
3389 let symbol = make_bybit_symbol(def.symbol, product_type);
3390 let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3391 statuses.insert(id, MarketStatusAction::from(def.status));
3392 }
3393 cursor = response.result.next_page_cursor;
3394 }
3395 }
3396
3397 cursor = cursor_walk.advance(BYBIT_INSTRUMENTS_INFO, cursor)?;
3398 if cursor.is_none() {
3399 break;
3400 }
3401 }
3402
3403 Ok(statuses)
3404 }
3405
3406 pub async fn request_instruments(
3416 &self,
3417 product_type: BybitProductType,
3418 symbol: Option<String>,
3419 base_coin: Option<Ustr>,
3420 ) -> anyhow::Result<Vec<InstrumentAny>> {
3421 let ts_init = self.generate_ts_init();
3422
3423 let instruments = match product_type {
3424 BybitProductType::Spot => {
3425 self.paginate_instruments::<BybitInstrumentSpot, _>(
3426 product_type,
3427 &symbol,
3428 base_coin,
3429 |def| parse_spot_instrument(def, ts_init, ts_init).ok(),
3430 )
3431 .await?
3432 }
3433 BybitProductType::Linear => {
3434 self.paginate_instruments::<BybitInstrumentLinear, _>(
3435 product_type,
3436 &symbol,
3437 base_coin,
3438 |def| parse_linear_instrument(def, ts_init, ts_init).ok(),
3439 )
3440 .await?
3441 }
3442 BybitProductType::Inverse => {
3443 self.paginate_instruments::<BybitInstrumentInverse, _>(
3444 product_type,
3445 &symbol,
3446 base_coin,
3447 |def| parse_inverse_instrument(def, ts_init, ts_init).ok(),
3448 )
3449 .await?
3450 }
3451 BybitProductType::Option => {
3452 self.paginate_instruments::<BybitInstrumentOption, _>(
3453 product_type,
3454 &symbol,
3455 base_coin,
3456 |def| parse_option_instrument(def, ts_init, ts_init).ok(),
3457 )
3458 .await?
3459 }
3460 };
3461
3462 self.cache_instruments(&instruments);
3463
3464 Ok(instruments)
3465 }
3466
3467 pub async fn request_instruments_with_statuses(
3477 &self,
3478 product_type: BybitProductType,
3479 ) -> anyhow::Result<(
3480 Vec<InstrumentAny>,
3481 AHashMap<InstrumentId, MarketStatusAction>,
3482 )> {
3483 let ts_init = self.generate_ts_init();
3484 let mut statuses = AHashMap::new();
3485
3486 let perp_status = |status: MarketStatusAction, is_scheduled_perp: bool| {
3488 if status == MarketStatusAction::Trading && is_scheduled_perp {
3489 MarketStatusAction::PreClose
3490 } else {
3491 status
3492 }
3493 };
3494
3495 let instruments = match product_type {
3496 BybitProductType::Spot => {
3497 self.paginate_instruments::<BybitInstrumentSpot, _>(
3498 product_type,
3499 &None::<String>,
3500 None,
3501 |def| {
3502 let id = InstrumentId::new(
3503 Symbol::from(make_bybit_symbol(def.symbol, product_type)),
3504 *BYBIT_VENUE,
3505 );
3506 statuses.insert(id, MarketStatusAction::from(def.status));
3507 parse_spot_instrument(def, ts_init, ts_init).ok()
3508 },
3509 )
3510 .await?
3511 }
3512 BybitProductType::Linear => {
3513 self.paginate_instruments::<BybitInstrumentLinear, _>(
3514 product_type,
3515 &None::<String>,
3516 None,
3517 |def| {
3518 let id = InstrumentId::new(
3519 Symbol::from(make_bybit_symbol(def.symbol, product_type)),
3520 *BYBIT_VENUE,
3521 );
3522 let scheduled = def.contract_type == BybitContractType::LinearPerpetual
3523 && def.delivery_time != "0";
3524 statuses.insert(id, perp_status(def.status.into(), scheduled));
3525 parse_linear_instrument(def, ts_init, ts_init).ok()
3526 },
3527 )
3528 .await?
3529 }
3530 BybitProductType::Inverse => {
3531 self.paginate_instruments::<BybitInstrumentInverse, _>(
3532 product_type,
3533 &None::<String>,
3534 None,
3535 |def| {
3536 let id = InstrumentId::new(
3537 Symbol::from(make_bybit_symbol(def.symbol, product_type)),
3538 *BYBIT_VENUE,
3539 );
3540 let scheduled = def.contract_type == BybitContractType::InversePerpetual
3541 && def.delivery_time != "0";
3542 statuses.insert(id, perp_status(def.status.into(), scheduled));
3543 parse_inverse_instrument(def, ts_init, ts_init).ok()
3544 },
3545 )
3546 .await?
3547 }
3548 BybitProductType::Option => {
3549 self.paginate_instruments::<BybitInstrumentOption, _>(
3550 product_type,
3551 &None::<String>,
3552 None,
3553 |def| {
3554 let id = InstrumentId::new(
3555 Symbol::from(make_bybit_symbol(def.symbol, product_type)),
3556 *BYBIT_VENUE,
3557 );
3558 statuses.insert(id, MarketStatusAction::from(def.status));
3559 parse_option_instrument(def, ts_init, ts_init).ok()
3560 },
3561 )
3562 .await?
3563 }
3564 };
3565
3566 self.cache_instruments(&instruments);
3567
3568 Ok((instruments, statuses))
3569 }
3570
3571 pub async fn request_tickers(
3584 &self,
3585 params: &BybitTickersParams,
3586 ) -> anyhow::Result<Vec<BybitTickerData>> {
3587 use super::models::{
3588 BybitTickersLinearResponse, BybitTickersOptionResponse, BybitTickersSpotResponse,
3589 };
3590
3591 match params.category {
3592 BybitProductType::Spot => {
3593 let response: BybitTickersSpotResponse = self.inner.get_tickers(params).await?;
3594 Ok(response.result.list.into_iter().map(Into::into).collect())
3595 }
3596 BybitProductType::Linear | BybitProductType::Inverse => {
3597 let response: BybitTickersLinearResponse = self.inner.get_tickers(params).await?;
3598 Ok(response.result.list.into_iter().map(Into::into).collect())
3599 }
3600 BybitProductType::Option => {
3601 let response: BybitTickersOptionResponse = self.inner.get_tickers(params).await?;
3602 Ok(response.result.list.into_iter().map(Into::into).collect())
3603 }
3604 }
3605 }
3606
3607 pub async fn request_option_tickers_raw(
3615 &self,
3616 base_coin: &str,
3617 ) -> anyhow::Result<Vec<BybitTickerOption>> {
3618 let params = BybitTickersParams {
3619 category: BybitProductType::Option,
3620 symbol: None,
3621 base_coin: Some(base_coin.to_string()),
3622 exp_date: None,
3623 };
3624 let response: BybitTickersOptionResponse = self.inner.get_tickers(¶ms).await?;
3625 Ok(response.result.list)
3626 }
3627
3628 pub async fn request_option_tickers_raw_with_params(
3637 &self,
3638 params: &BybitTickersParams,
3639 ) -> anyhow::Result<Vec<BybitTickerOption>> {
3640 let response: BybitTickersOptionResponse = self.inner.get_tickers(params).await?;
3641 Ok(response.result.list)
3642 }
3643
3644 pub async fn request_trades(
3664 &self,
3665 product_type: BybitProductType,
3666 instrument_id: InstrumentId,
3667 limit: Option<u32>,
3668 ) -> anyhow::Result<Vec<TradeTick>> {
3669 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
3670 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3671
3672 let mut params_builder = BybitTradesParamsBuilder::default();
3673 params_builder.category(product_type);
3674 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3675
3676 if let Some(limit_val) = limit {
3677 params_builder.limit(limit_val);
3678 }
3679
3680 let params = params_builder.build().build_anyhow()?;
3681 let response = self.inner.get_recent_trades(¶ms).await?;
3682
3683 let mut trades = Vec::new();
3684
3685 for trade in response.result.list {
3686 if let Ok(trade_tick) = parse_trade_tick(&trade, &instrument, None) {
3687 trades.push(trade_tick);
3688 }
3689 }
3690
3691 Ok(trades)
3692 }
3693
3694 pub async fn request_funding_rates(
3707 &self,
3708 product_type: BybitProductType,
3709 instrument_id: InstrumentId,
3710 start: Option<Timestamp>,
3711 end: Option<Timestamp>,
3712 limit: Option<u32>,
3713 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
3714 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
3715 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3716
3717 let start_ms = start.map(|dt| dt.as_millisecond());
3718 let mut seen_timestamps: AHashSet<i64> = AHashSet::new();
3719
3720 let mut raw_funding_rates = Vec::new();
3721
3722 let mut current_end_ms = match (start, end) {
3724 (Some(_), None) => Some(Timestamp::now().as_millisecond()),
3725 _ => end.map(|dt| dt.as_millisecond()),
3726 };
3727
3728 loop {
3729 let mut params_builder = BybitFundingParamsBuilder::default();
3730 params_builder.category(product_type);
3731 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3732 params_builder.limit(limit.unwrap_or(200).clamp(0, 200)); if let Some(start_val) = start_ms {
3735 params_builder.start_time(start_val);
3736 }
3737
3738 if let Some(end_val) = current_end_ms {
3739 params_builder.end_time(end_val);
3740 }
3741
3742 let params = params_builder.build().build_anyhow()?;
3743 let response = self.inner.get_funding_history(¶ms).await?;
3744
3745 let funding_rates = response.result.list;
3746
3747 let mut new_funding_rates_with_ts: Vec<(i64, _)> = funding_rates
3748 .into_iter()
3749 .filter_map(|f| {
3750 let Ok(ts) = f.funding_rate_timestamp.parse::<i64>() else {
3751 return None;
3752 };
3753
3754 seen_timestamps.insert(ts).then_some((ts, f))
3755 })
3756 .collect();
3757
3758 new_funding_rates_with_ts.sort_by_key(|(ts, _)| Reverse(*ts));
3759
3760 let earliest_funding_time = match new_funding_rates_with_ts.last() {
3761 Some((last_ts, _)) => *last_ts,
3762 None => break,
3763 };
3764
3765 let new_funding_rates = new_funding_rates_with_ts.into_iter().map(|(_, f)| f);
3766 raw_funding_rates.extend(new_funding_rates);
3767
3768 if let Some(limit_val) = limit
3770 && raw_funding_rates.len() >= limit_val as usize
3771 {
3772 break;
3773 }
3774
3775 if let Some(start_val) = start_ms
3776 && earliest_funding_time <= start_val
3777 {
3778 break;
3779 }
3780
3781 current_end_ms = Some(earliest_funding_time - 1);
3783 }
3784
3785 if let Some(limit_val) = limit {
3786 raw_funding_rates.truncate(limit_val as usize);
3787 }
3788 let mut rates: Vec<FundingRateUpdate> = Vec::with_capacity(raw_funding_rates.len());
3789
3790 for window in raw_funding_rates.windows(2) {
3791 let raw = &window[0];
3792 let timestamp = raw
3793 .funding_rate_timestamp
3794 .parse::<i64>()
3795 .map_err(|_| anyhow::anyhow!("invalid funding_rate_timestamp"))?;
3796 let older_timestamp = window[1]
3797 .funding_rate_timestamp
3798 .parse::<i64>()
3799 .map_err(|_| anyhow::anyhow!("invalid funding_rate_timestamp"))?;
3800
3801 let interval_millis = timestamp - older_timestamp;
3802 let rate = parse_funding_rate(raw, &instrument, Some(interval_millis))?;
3803
3804 rates.push(rate);
3805 }
3806
3807 if let Some(last_raw) = raw_funding_rates.last() {
3808 let rate = parse_funding_rate(last_raw, &instrument, None)?;
3809 rates.push(rate);
3810 }
3811
3812 rates.reverse();
3813
3814 Ok(rates)
3815 }
3816
3817 pub async fn request_orderbook_snapshot(
3835 &self,
3836 product_type: BybitProductType,
3837 instrument_id: InstrumentId,
3838 limit: Option<u32>,
3839 ) -> anyhow::Result<OrderBookDeltas> {
3840 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
3841 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3842
3843 let mut params_builder = BybitOrderbookParamsBuilder::default();
3844 params_builder.category(product_type);
3845 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3846
3847 if let Some(limit) = limit {
3848 let max_limit = match product_type {
3849 BybitProductType::Spot => 200,
3850 BybitProductType::Option => 25,
3851 BybitProductType::Linear | BybitProductType::Inverse => 500,
3852 };
3853 let clamped_limit = limit.min(max_limit);
3854 if limit > max_limit {
3855 log::warn!(
3856 "Bybit orderbook snapshot request depth limit exceeds venue maximum; clamping: limit={limit}, clamped_limit={clamped_limit}",
3857 );
3858 }
3859 params_builder.limit(clamped_limit);
3860 }
3861
3862 let params = params_builder.build().build_anyhow()?;
3863 let response = self.inner.get_orderbook(¶ms).await?;
3864
3865 let deltas = parse_orderbook(&response.result, &instrument, None)?;
3866
3867 Ok(deltas)
3868 }
3869
3870 pub async fn request_bars(
3883 &self,
3884 product_type: BybitProductType,
3885 bar_type: BarType,
3886 start: Option<Timestamp>,
3887 end: Option<Timestamp>,
3888 limit: Option<u32>,
3889 timestamp_on_close: bool,
3890 ) -> anyhow::Result<Vec<Bar>> {
3891 let instrument_id = bar_type.instrument_id();
3892 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
3893 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3894
3895 let interval = bar_spec_to_bybit_interval(
3897 bar_type.spec().aggregation,
3898 bar_type.spec().step.get() as u64,
3899 )?;
3900
3901 let start_ms = start.map(|dt| dt.as_millisecond());
3902 let mut seen_timestamps: AHashSet<i64> = AHashSet::new();
3903 let current_time_ms = get_atomic_clock_realtime().get_time_ms() as i64;
3904
3905 let mut pages: Vec<Vec<Bar>> = Vec::new();
3915 let mut total_bars = 0usize;
3916 let mut current_end = end.map(|dt| dt.as_millisecond());
3917 let mut page_count = 0;
3918
3919 loop {
3920 page_count += 1;
3921
3922 let mut params_builder = BybitKlinesParamsBuilder::default();
3923 params_builder.category(product_type);
3924 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3925 params_builder.interval(interval);
3926 params_builder.limit(1000u32); if let Some(start_val) = start_ms {
3929 params_builder.start(start_val);
3930 }
3931
3932 if let Some(end_val) = current_end {
3933 params_builder.end(end_val);
3934 }
3935
3936 let params = params_builder.build().build_anyhow()?;
3937 let response = self.inner.get_klines(¶ms).await?;
3938
3939 let klines = response.result.list;
3940 if klines.is_empty() {
3941 break;
3942 }
3943
3944 let mut klines_with_ts: Vec<(i64, _)> = klines
3946 .into_iter()
3947 .filter_map(|k| k.start.parse::<i64>().ok().map(|ts| (ts, k)))
3948 .collect();
3949
3950 klines_with_ts.sort_by_key(|(ts, _)| *ts);
3951
3952 let has_new = klines_with_ts
3954 .iter()
3955 .any(|(ts, _)| !seen_timestamps.contains(ts));
3956
3957 if !has_new {
3958 break;
3959 }
3960
3961 let mut page_bars = Vec::with_capacity(klines_with_ts.len());
3962
3963 let mut earliest_ts: Option<i64> = None;
3964
3965 for (start_time, kline) in &klines_with_ts {
3966 if earliest_ts.is_none_or(|ts| *start_time < ts) {
3968 earliest_ts = Some(*start_time);
3969 }
3970
3971 let bar_end_time = interval.bar_end_time_ms(*start_time);
3972 if bar_end_time > current_time_ms {
3973 continue;
3974 }
3975
3976 if !seen_timestamps.contains(start_time)
3977 && let Ok(bar) =
3978 parse_kline_bar(kline, &instrument, bar_type, timestamp_on_close, None)
3979 {
3980 page_bars.push(bar);
3981 seen_timestamps.insert(*start_time);
3982 }
3983 }
3984
3985 total_bars += page_bars.len();
3988 pages.push(page_bars);
3989
3990 if let Some(limit_val) = limit
3992 && total_bars >= limit_val as usize
3993 {
3994 break;
3995 }
3996
3997 let Some(earliest_bar_time) = earliest_ts else {
4000 break;
4001 };
4002
4003 if let Some(start_val) = start_ms
4004 && earliest_bar_time <= start_val
4005 {
4006 break;
4007 }
4008
4009 current_end = Some(earliest_bar_time - 1);
4010
4011 if page_count > 100 {
4013 break;
4014 }
4015 }
4016
4017 let mut all_bars: Vec<Bar> = Vec::with_capacity(total_bars);
4019 for page in pages.into_iter().rev() {
4020 all_bars.extend(page);
4021 }
4022
4023 if let Some(limit_val) = limit {
4025 let limit_usize = limit_val as usize;
4026 if all_bars.len() > limit_usize {
4027 let start_idx = all_bars.len() - limit_usize;
4028 return Ok(all_bars[start_idx..].to_vec());
4029 }
4030 }
4031
4032 Ok(all_bars)
4033 }
4034
4035 fn instrument_from_cache_by_id(
4036 &self,
4037 instrument_id: InstrumentId,
4038 ) -> anyhow::Result<InstrumentAny> {
4039 self.get_instrument(&instrument_id.symbol.inner())
4040 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id).into())
4041 }
4042
4043 pub async fn request_fee_rates(
4055 &self,
4056 product_type: BybitProductType,
4057 symbol: Option<String>,
4058 base_coin: Option<String>,
4059 ) -> anyhow::Result<Vec<BybitFeeRate>> {
4060 let params = BybitFeeRateParams {
4061 category: product_type,
4062 symbol,
4063 base_coin,
4064 };
4065
4066 let response = self.inner.get_fee_rate(¶ms).await?;
4067 Ok(response.result.list)
4068 }
4069
4070 pub async fn request_account_state(
4082 &self,
4083 account_type: BybitAccountType,
4084 account_id: AccountId,
4085 ) -> anyhow::Result<AccountState> {
4086 let params = BybitWalletBalanceParams {
4087 account_type,
4088 coin: None,
4089 };
4090
4091 let response = self.inner.get_wallet_balance(¶ms).await?;
4092 let ts_init = self.generate_ts_init();
4093
4094 let wallet_balance = response
4096 .result
4097 .list
4098 .first()
4099 .ok_or_else(|| anyhow::anyhow!("No wallet balance found in response"))?;
4100
4101 parse_account_state(wallet_balance, account_id, ts_init)
4102 }
4103
4104 #[expect(clippy::too_many_arguments)]
4120 pub async fn request_order_status_reports(
4121 &self,
4122 account_id: AccountId,
4123 product_type: BybitProductType,
4124 instrument_id: Option<InstrumentId>,
4125 open_only: bool,
4126 start: Option<Timestamp>,
4127 end: Option<Timestamp>,
4128 limit: Option<u32>,
4129 ) -> anyhow::Result<Vec<OrderStatusReport>> {
4130 let symbol_param = if let Some(id) = instrument_id.as_ref() {
4132 let symbol_str = id.symbol.as_str();
4133 if symbol_str.is_empty() {
4134 None
4135 } else {
4136 Some(BybitSymbol::new(symbol_str)?.raw_symbol().to_string())
4137 }
4138 } else {
4139 None
4140 };
4141
4142 let settle_coins_to_query: Vec<Option<String>> =
4145 if product_type == BybitProductType::Linear && symbol_param.is_none() {
4146 vec![Some("USDT".to_string()), Some("USDC".to_string())]
4147 } else {
4148 match product_type {
4149 BybitProductType::Inverse => vec![None],
4150 _ => vec![None],
4151 }
4152 };
4153
4154 let mut all_collected_orders = Vec::new();
4155 let mut total_collected_across_coins = 0;
4156
4157 for settle_coin in settle_coins_to_query {
4158 let remaining_limit = if let Some(limit) = limit {
4159 let remaining = (limit as usize).saturating_sub(total_collected_across_coins);
4160 if remaining == 0 {
4161 break;
4162 }
4163 Some(remaining as u32)
4164 } else {
4165 None
4166 };
4167
4168 let orders_for_coin = if open_only {
4169 let mut all_orders = Vec::new();
4170 let mut seen_ids: AHashSet<Ustr> = AHashSet::new();
4171
4172 let order_filters: Vec<Option<BybitOrderFilter>> =
4175 if product_type == BybitProductType::Option {
4176 vec![None]
4177 } else {
4178 vec![None, Some(BybitOrderFilter::StopOrder)]
4179 };
4180
4181 let open_only_modes = [None, Some(BybitOpenOnly::ClosedRecent)];
4182
4183 for oo in open_only_modes {
4184 for order_filter in &order_filters {
4185 let mut cursor: Option<String> = None;
4186 let mut cursor_walk = CursorWalk::default();
4187
4188 loop {
4189 let remaining = if let Some(limit) = remaining_limit {
4190 (limit as usize).saturating_sub(all_orders.len())
4191 } else {
4192 usize::MAX
4193 };
4194
4195 if remaining == 0 {
4196 break;
4197 }
4198
4199 let page_limit = std::cmp::min(remaining, 50);
4201
4202 let mut p = BybitOpenOrdersParamsBuilder::default();
4203 p.category(product_type);
4204
4205 if let Some(symbol) = symbol_param.clone() {
4206 p.symbol(symbol);
4207 }
4208
4209 if let Some(coin) = settle_coin.clone() {
4210 p.settle_coin(coin);
4211 }
4212
4213 if let Some(of) = order_filter {
4214 p.order_filter(*of);
4215 }
4216
4217 if let Some(oo) = oo {
4218 p.open_only(oo);
4219 }
4220 p.limit(page_limit as u32);
4221
4222 if let Some(c) = cursor {
4223 p.cursor(c);
4224 }
4225 let params = p.build().build_anyhow()?;
4226 let response: BybitOpenOrdersResponse = self
4227 .inner
4228 .send_request(
4229 Method::GET,
4230 BYBIT_ORDER_REALTIME,
4231 Some(¶ms),
4232 None,
4233 true,
4234 )
4235 .await?;
4236
4237 for order in response.result.list {
4238 if seen_ids.insert(order.order_id) {
4239 all_orders.push(order);
4240 }
4241 }
4242
4243 if oo.is_some() {
4245 break;
4246 }
4247
4248 cursor = cursor_walk
4249 .advance(BYBIT_ORDER_REALTIME, response.result.next_page_cursor)?;
4250
4251 if cursor.is_none() {
4252 break;
4253 }
4254 }
4255 }
4256 }
4257
4258 all_orders
4259 } else {
4260 let mut all_orders = Vec::new();
4263 let mut open_orders = Vec::new();
4264 let mut seen_open_ids: AHashSet<Ustr> = AHashSet::new();
4265
4266 let order_filters: Vec<Option<BybitOrderFilter>> =
4269 if product_type == BybitProductType::Option {
4270 vec![None]
4271 } else {
4272 vec![None, Some(BybitOrderFilter::StopOrder)]
4273 };
4274
4275 for order_filter in &order_filters {
4276 let mut cursor: Option<String> = None;
4277 let mut cursor_walk = CursorWalk::default();
4278
4279 loop {
4280 let remaining = if let Some(limit) = remaining_limit {
4281 (limit as usize).saturating_sub(open_orders.len())
4282 } else {
4283 usize::MAX
4284 };
4285
4286 if remaining == 0 {
4287 break;
4288 }
4289
4290 let page_limit = std::cmp::min(remaining, 50);
4292
4293 let mut open_params = BybitOpenOrdersParamsBuilder::default();
4294 open_params.category(product_type);
4295
4296 if let Some(symbol) = symbol_param.clone() {
4297 open_params.symbol(symbol);
4298 }
4299
4300 if let Some(coin) = settle_coin.clone() {
4301 open_params.settle_coin(coin);
4302 }
4303
4304 if let Some(of) = order_filter {
4305 open_params.order_filter(*of);
4306 }
4307 open_params.limit(page_limit as u32);
4308
4309 if let Some(c) = cursor {
4310 open_params.cursor(c);
4311 }
4312 let open_params = open_params.build().build_anyhow()?;
4313 let open_response: BybitOpenOrdersResponse = self
4314 .inner
4315 .send_request(
4316 Method::GET,
4317 BYBIT_ORDER_REALTIME,
4318 Some(&open_params),
4319 None,
4320 true,
4321 )
4322 .await?;
4323
4324 for order in open_response.result.list {
4325 if !seen_open_ids.contains(&order.order_id) {
4326 seen_open_ids.insert(order.order_id);
4327 open_orders.push(order);
4328 }
4329 }
4330
4331 cursor = cursor_walk
4332 .advance(BYBIT_ORDER_REALTIME, open_response.result.next_page_cursor)?;
4333
4334 if cursor.is_none() {
4335 break;
4336 }
4337 }
4338 }
4339
4340 let seen_order_ids: AHashSet<Ustr> = seen_open_ids;
4341 let total_open_orders = open_orders.len();
4342
4343 all_orders.extend(open_orders);
4344
4345 let mut total_history_orders = 0;
4346
4347 for order_filter in &order_filters {
4348 let mut cursor: Option<String> = None;
4349 let mut cursor_walk = CursorWalk::default();
4350
4351 loop {
4352 let total_orders = total_open_orders + total_history_orders;
4353 let remaining = if let Some(limit) = remaining_limit {
4354 (limit as usize).saturating_sub(total_orders)
4355 } else {
4356 usize::MAX
4357 };
4358
4359 if remaining == 0 {
4360 break;
4361 }
4362
4363 let page_limit = std::cmp::min(remaining, 50);
4365
4366 let mut history_params = BybitOrderHistoryParamsBuilder::default();
4367 history_params.category(product_type);
4368
4369 if let Some(symbol) = symbol_param.clone() {
4370 history_params.symbol(symbol);
4371 }
4372
4373 if let Some(coin) = settle_coin.clone() {
4374 history_params.settle_coin(coin);
4375 }
4376
4377 if let Some(of) = order_filter {
4378 history_params.order_filter(*of);
4379 }
4380
4381 if let Some(start) = start {
4382 history_params.start_time(start.as_millisecond());
4383 }
4384
4385 if let Some(end) = end {
4386 history_params.end_time(end.as_millisecond());
4387 }
4388 history_params.limit(page_limit as u32);
4389
4390 if let Some(c) = cursor {
4391 history_params.cursor(c);
4392 }
4393 let history_params = history_params.build().build_anyhow()?;
4394 let history_response: BybitOrderHistoryResponse = self
4395 .inner
4396 .send_request(
4397 Method::GET,
4398 BYBIT_ORDER_HISTORY,
4399 Some(&history_params),
4400 None,
4401 true,
4402 )
4403 .await?;
4404
4405 for order in history_response.result.list {
4407 if !seen_order_ids.contains(&order.order_id) {
4408 all_orders.push(order);
4409 total_history_orders += 1;
4410 }
4411 }
4412
4413 cursor = cursor_walk.advance(
4414 BYBIT_ORDER_HISTORY,
4415 history_response.result.next_page_cursor,
4416 )?;
4417
4418 if cursor.is_none() {
4419 break;
4420 }
4421 }
4422 }
4423
4424 all_orders
4425 };
4426
4427 total_collected_across_coins += orders_for_coin.len();
4428 all_collected_orders.extend(orders_for_coin);
4429 }
4430
4431 let ts_init = self.generate_ts_init();
4432
4433 let mut reports = Vec::new();
4434
4435 for order in all_collected_orders {
4436 if let Some(ref instrument_id) = instrument_id {
4437 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
4438
4439 if let Ok(report) =
4440 parse_order_status_report(&order, &instrument, account_id, ts_init)
4441 {
4442 reports.push(report);
4443 }
4444 } else {
4445 if !order.symbol.is_empty() {
4448 let symbol_with_product =
4449 Symbol::from_ustr_unchecked(make_bybit_symbol(order.symbol, product_type));
4450
4451 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
4452 log::debug!(
4453 "Skipping order report for instrument not in cache: symbol={}, full_symbol={}",
4454 order.symbol,
4455 symbol_with_product
4456 );
4457 continue;
4458 };
4459
4460 match parse_order_status_report(&order, &instrument, account_id, ts_init) {
4461 Ok(report) => reports.push(report),
4462 Err(e) => {
4463 log::error!("Failed to parse order status report: {e}");
4464 }
4465 }
4466 }
4467 }
4468 }
4469
4470 Ok(reports)
4471 }
4472
4473 pub async fn request_fill_reports(
4485 &self,
4486 account_id: AccountId,
4487 product_type: BybitProductType,
4488 instrument_id: Option<InstrumentId>,
4489 start: Option<i64>,
4490 end: Option<i64>,
4491 limit: Option<u32>,
4492 ) -> anyhow::Result<Vec<FillReport>> {
4493 let symbol = if let Some(id) = instrument_id {
4495 let bybit_symbol = BybitSymbol::new(id.symbol.as_str())?;
4496 Some(bybit_symbol.raw_symbol().to_string())
4497 } else {
4498 None
4499 };
4500
4501 let mut all_executions = Vec::new();
4503 let mut cursor: Option<String> = None;
4504 let mut cursor_walk = CursorWalk::default();
4505 let mut total_executions = 0;
4506
4507 loop {
4508 let remaining = if let Some(limit) = limit {
4510 (limit as usize).saturating_sub(total_executions)
4511 } else {
4512 usize::MAX
4513 };
4514
4515 if remaining == 0 {
4517 break;
4518 }
4519
4520 let page_limit = std::cmp::min(remaining, 100);
4522
4523 let params = BybitTradeHistoryParams {
4524 category: product_type,
4525 symbol: symbol.clone(),
4526 base_coin: None,
4527 order_id: None,
4528 order_link_id: None,
4529 start_time: start,
4530 end_time: end,
4531 exec_type: None,
4532 limit: Some(page_limit as u32),
4533 cursor: cursor.clone(),
4534 };
4535
4536 let response = self.inner.get_trade_history(¶ms).await?;
4537 for execution in response.result.list {
4538 if execution.exec_type == BybitExecType::Funding {
4539 log::debug!(
4540 "Skipping funding execution: symbol={}, order_id={}, exec_id={}",
4541 execution.symbol,
4542 execution.order_id,
4543 execution.exec_id,
4544 );
4545 continue;
4546 }
4547
4548 all_executions.push(execution);
4549 total_executions += 1;
4550 }
4551
4552 cursor = cursor_walk.advance(BYBIT_EXECUTION_LIST, response.result.next_page_cursor)?;
4553 if cursor.is_none() {
4554 break;
4555 }
4556 }
4557
4558 let ts_init = self.generate_ts_init();
4559 let mut reports = Vec::new();
4560
4561 for execution in all_executions {
4562 let symbol_with_product =
4565 Symbol::from_ustr_unchecked(make_bybit_symbol(execution.symbol, product_type));
4566
4567 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
4568 log::debug!(
4569 "Skipping fill report for instrument not in cache: symbol={}, full_symbol={}",
4570 execution.symbol,
4571 symbol_with_product
4572 );
4573 continue;
4574 };
4575
4576 match parse_fill_report(&execution, account_id, &instrument, ts_init) {
4577 Ok(report) => reports.push(report),
4578 Err(e) => {
4579 log::error!("Failed to parse fill report: {e}");
4580 }
4581 }
4582 }
4583
4584 Ok(reports)
4585 }
4586
4587 pub async fn request_position_status_reports(
4608 &self,
4609 account_id: AccountId,
4610 product_type: BybitProductType,
4611 instrument_id: Option<InstrumentId>,
4612 ) -> anyhow::Result<Vec<PositionStatusReport>> {
4613 if product_type == BybitProductType::Spot {
4615 if self.use_spot_position_reports.load(Ordering::Relaxed) {
4616 let Some(instrument_id) = instrument_id else {
4617 anyhow::bail!(
4618 "SPOT wallet balances carry no pair identity and cannot be attributed for a bulk position report request"
4619 );
4620 };
4621 return self
4622 .generate_spot_position_reports_from_wallet(account_id, instrument_id)
4623 .await;
4624 } else {
4625 return Ok(Vec::new());
4627 }
4628 }
4629
4630 let ts_init = self.generate_ts_init();
4631 let mut reports = Vec::new();
4632
4633 let symbol = if let Some(id) = instrument_id {
4635 let symbol_str = id.symbol.as_str();
4636 if symbol_str.is_empty() {
4637 anyhow::bail!("InstrumentId symbol is empty");
4638 }
4639 let bybit_symbol = BybitSymbol::new(symbol_str)?;
4640 Some(bybit_symbol.raw_symbol().to_string())
4641 } else {
4642 None
4643 };
4644
4645 let settle_coins: &[Option<&str>] =
4647 if product_type == BybitProductType::Linear && symbol.is_none() {
4648 &[Some("USDT"), Some("USDC")]
4649 } else {
4650 &[None]
4651 };
4652
4653 for settle_coin in settle_coins {
4654 let params = BybitPositionListParams {
4655 category: product_type,
4656 symbol: symbol.clone(),
4657 base_coin: None,
4658 settle_coin: settle_coin.map(str::to_string),
4659 limit: Some(200), cursor: None,
4661 };
4662
4663 self.request_position_pages(account_id, params, ts_init, &mut reports)
4664 .await?;
4665 }
4666
4667 Ok(reports)
4668 }
4669
4670 async fn request_position_pages(
4671 &self,
4672 account_id: AccountId,
4673 mut params: BybitPositionListParams,
4674 ts_init: UnixNanos,
4675 reports: &mut Vec<PositionStatusReport>,
4676 ) -> anyhow::Result<()> {
4677 let mut cursor_walk = CursorWalk::default();
4678
4679 loop {
4680 let response = self.inner.get_positions(¶ms).await?;
4681
4682 for position in response.result.list {
4683 if position.symbol.is_empty() {
4684 anyhow::bail!(
4685 "Position symbol is empty, category={:?}, symbol={:?}, settle_coin={:?}",
4686 params.category,
4687 params.symbol,
4688 params.settle_coin,
4689 );
4690 }
4691
4692 let symbol = Symbol::new(format!(
4693 "{}{}",
4694 position.symbol.as_str(),
4695 params.category.suffix()
4696 ));
4697 let instrument = self.instrument_from_cache(&symbol)?;
4698 let report =
4699 parse_position_status_report(&position, account_id, &instrument, ts_init)
4700 .with_context(|| format!("failed to parse position report for {symbol}"))?;
4701 reports.push(report);
4702 }
4703
4704 params.cursor =
4705 cursor_walk.advance(BYBIT_POSITION_LIST, response.result.next_page_cursor)?;
4706
4707 if params.cursor.is_none() {
4708 break;
4709 }
4710 }
4711
4712 Ok(())
4713 }
4714
4715 async fn query_order_by_id(
4716 &self,
4717 product_type: BybitProductType,
4718 order_id: &str,
4719 endpoint: &str,
4720 context: &str,
4721 ) -> anyhow::Result<BybitOrder> {
4722 let mut query_params = BybitOpenOrdersParamsBuilder::default();
4723 query_params.category(product_type);
4724 query_params.order_id(order_id.to_string());
4725
4726 let query_params = query_params.build().build_anyhow()?;
4727 let order_response: BybitOpenOrdersResponse = self
4728 .inner
4729 .send_request(Method::GET, endpoint, Some(&query_params), None, true)
4730 .await?;
4731
4732 order_response
4733 .result
4734 .list
4735 .into_iter()
4736 .next()
4737 .ok_or_else(|| anyhow::anyhow!("No order returned {context}"))
4738 }
4739}
4740
4741#[cfg(test)]
4742mod tests {
4743 use nautilus_testkit::http::assert_http_redirect_rejected;
4744 use rstest::rstest;
4745
4746 use super::*;
4747
4748 #[tokio::test]
4749 async fn test_authenticated_client_rejects_redirects() {
4750 let client = BybitRawHttpClient::build_http_client(3, None).unwrap();
4751 assert_http_redirect_rejected(|url| async move {
4752 client
4753 .get(url, None, None, Some(3), None)
4754 .await
4755 .unwrap()
4756 .status
4757 .as_u16()
4758 })
4759 .await;
4760 }
4761
4762 #[rstest]
4763 fn test_client_creation() {
4764 let client = BybitHttpClient::new(None, 60, 3, 1000, 10_000, 5_000, None);
4765 assert!(client.is_ok());
4766
4767 let client = client.unwrap();
4768 assert!(client.base_url().contains("bybit.com"));
4769 assert!(client.credential().is_none());
4770 }
4771
4772 #[rstest]
4773 fn test_client_with_credentials() {
4774 let client = BybitHttpClient::with_credentials(
4775 "test_key".to_string(),
4776 "test_secret".to_string(),
4777 Some("https://api-testnet.bybit.com".to_string()),
4778 60,
4779 3,
4780 1000,
4781 10_000,
4782 5_000,
4783 None,
4784 );
4785 assert!(client.is_ok());
4786
4787 let client = client.unwrap();
4788 assert!(client.credential().is_some());
4789 }
4790
4791 #[rstest]
4792 fn test_build_path_with_params() {
4793 #[derive(Serialize)]
4794 struct TestParams {
4795 category: String,
4796 symbol: String,
4797 }
4798
4799 let params = TestParams {
4800 category: "linear".to_string(),
4801 symbol: "BTCUSDT".to_string(),
4802 };
4803
4804 let path = BybitRawHttpClient::build_path("/v5/market/test", ¶ms);
4805 assert!(path.is_ok());
4806 assert!(path.unwrap().contains("category=linear"));
4807 }
4808
4809 #[rstest]
4810 fn test_build_path_without_params() {
4811 let params = ();
4812 let path = BybitRawHttpClient::build_path("/v5/market/time", ¶ms);
4813 assert!(path.is_ok());
4814 assert_eq!(path.unwrap(), "/v5/market/time");
4815 }
4816
4817 #[rstest]
4818 fn test_params_serialization_matches_build_path() {
4819 #[derive(Serialize)]
4821 struct TestParams {
4822 category: String,
4823 limit: u32,
4824 }
4825
4826 let params = TestParams {
4827 category: "spot".to_string(),
4828 limit: 50,
4829 };
4830
4831 let old_path = BybitRawHttpClient::build_path(BYBIT_ORDER_REALTIME, ¶ms).unwrap();
4833 let old_query = old_path.split('?').nth(1).unwrap_or("");
4834
4835 let new_query = serde_urlencoded::to_string(¶ms).unwrap();
4837
4838 assert_eq!(old_query, new_query);
4840 }
4841
4842 #[rstest]
4843 fn test_params_serialization_order() {
4844 #[derive(Serialize)]
4846 struct OrderParams {
4847 category: String,
4848 symbol: String,
4849 limit: u32,
4850 }
4851
4852 let params = OrderParams {
4853 category: "spot".to_string(),
4854 symbol: "BTCUSDT".to_string(),
4855 limit: 50,
4856 };
4857
4858 let query1 = serde_urlencoded::to_string(¶ms).unwrap();
4860 let query2 = serde_urlencoded::to_string(¶ms).unwrap();
4861 let query3 = serde_urlencoded::to_string(¶ms).unwrap();
4862
4863 assert_eq!(query1, query2);
4864 assert_eq!(query2, query3);
4865
4866 assert!(query1.contains("category=spot"));
4868 assert!(query1.contains("symbol=BTCUSDT"));
4869 assert!(query1.contains("limit=50"));
4870 }
4871
4872 #[rstest]
4873 #[case(403, "Access too frequent", true)]
4874 #[case(403, "Forbidden", false)]
4875 #[case(429, "Access too frequent", false)]
4876 fn test_rate_limit_403_detection(
4877 #[case] status: u16,
4878 #[case] body: &str,
4879 #[case] expected: bool,
4880 ) {
4881 assert_eq!(
4882 BybitRawHttpClient::is_rate_limit_403(status, body),
4883 expected
4884 );
4885 }
4886
4887 #[rstest]
4888 fn test_cursor_walk_stops_on_absent_or_empty_cursor() {
4889 let mut walk = CursorWalk::default();
4890
4891 assert_eq!(walk.advance("/endpoint", None).unwrap(), None);
4892 assert_eq!(
4893 walk.advance("/endpoint", Some(String::new())).unwrap(),
4894 None
4895 );
4896 assert!(walk.followed.is_empty());
4897 }
4898
4899 #[rstest]
4900 fn test_cursor_walk_follows_advancing_cursors() {
4901 let mut walk = CursorWalk::default();
4902
4903 for page in ["page-2", "page-3", "page-4"] {
4904 assert_eq!(
4905 walk.advance("/endpoint", Some(page.to_string())).unwrap(),
4906 Some(page.to_string())
4907 );
4908 }
4909 }
4910
4911 #[rstest]
4912 fn test_cursor_walk_rejects_a_cursor_that_does_not_advance() {
4913 let mut walk = CursorWalk::default();
4914 walk.advance("/endpoint", Some("page-2".to_string()))
4915 .unwrap();
4916
4917 let error = walk
4918 .advance("/endpoint", Some("page-2".to_string()))
4919 .expect_err("a cursor that addresses the page it came with has no next page");
4920
4921 assert_eq!(
4922 error.to_string(),
4923 r#"/endpoint pagination cursor did not advance from "page-2""#
4924 );
4925 }
4926
4927 #[rstest]
4928 #[case::two_cycle(vec!["page-2", "page-3"], "page-2")]
4929 #[case::long_cycle(vec!["page-2", "page-3", "page-4", "page-5"], "page-3")]
4930 fn test_cursor_walk_rejects_a_cursor_already_followed(
4931 #[case] followed: Vec<&str>,
4932 #[case] repeated: &str,
4933 ) {
4934 let mut walk = CursorWalk::default();
4935 for cursor in followed {
4936 walk.advance("/endpoint", Some(cursor.to_string())).unwrap();
4937 }
4938
4939 let error = walk
4940 .advance("/endpoint", Some(repeated.to_string()))
4941 .expect_err("a cursor already followed has no further page to offer");
4942
4943 assert_eq!(
4944 error.to_string(),
4945 format!(r#"/endpoint pagination repeated cursor "{repeated}""#)
4946 );
4947 }
4948}