Skip to main content

nautilus_bybit/http/
client.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Provides the HTTP client integration for the [Bybit](https://bybit.com) REST API.
17//!
18//! Bybit API reference <https://bybit-exchange.github.io/docs/>.
19
20use std::{
21    cmp::Reverse,
22    collections::HashMap,
23    fmt::{Debug, Display},
24    num::NonZeroU32,
25    sync::{
26        Arc, LazyLock,
27        atomic::{AtomicBool, Ordering},
28    },
29};
30
31use ahash::{AHashMap, AHashSet};
32use chrono::{DateTime, Utc};
33use nautilus_core::{
34    AtomicMap, AtomicTime, consts::NAUTILUS_USER_AGENT, env::get_or_env_var_opt, nanos::UnixNanos,
35    time::get_atomic_clock_realtime,
36};
37use nautilus_model::{
38    data::{Bar, BarType, FundingRateUpdate, OrderBookDeltas, TradeTick},
39    enums::{MarketStatusAction, OrderSide, OrderType, PositionSideSpecified, TimeInForce},
40    events::account::state::AccountState,
41    identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
42    instruments::{Instrument, InstrumentAny},
43    reports::{FillReport, OrderStatusReport, PositionStatusReport},
44    types::{Price, Quantity},
45};
46use nautilus_network::{
47    http::{HttpClient, Method, USER_AGENT},
48    ratelimiter::quota::Quota,
49    retry::{RetryConfig, RetryManager},
50};
51use rust_decimal::Decimal;
52use serde::{Serialize, de::DeserializeOwned};
53use tokio_util::sync::CancellationToken;
54use ustr::Ustr;
55
56use super::{
57    error::BybitHttpError,
58    models::{
59        BybitAccountDetailsResponse, BybitAccountInfoResponse, BybitBorrowResponse,
60        BybitEscrowSubMembersResponse, BybitFeeRate, BybitFeeRateResponse, BybitFundingResponse,
61        BybitInstrumentInverse, BybitInstrumentInverseResponse, BybitInstrumentLinear,
62        BybitInstrumentLinearResponse, BybitInstrumentOption, BybitInstrumentOptionResponse,
63        BybitInstrumentSpot, BybitInstrumentSpotResponse, BybitKlinesResponse,
64        BybitNoConvertRepayResponse, BybitOpenOrdersResponse, BybitOrder,
65        BybitOrderHistoryResponse, BybitOrderbookResponse, BybitPlaceOrderResponse,
66        BybitPositionListResponse, BybitServerTimeResponse, BybitSetLeverageResponse,
67        BybitSetMarginModeResponse, BybitSetTradingStopResponse, BybitSubApiKeyInfo,
68        BybitSubApiKeysResponse, BybitSubMember, BybitSubMembersPagedResponse,
69        BybitSubMembersResponse, BybitSwitchModeResponse, BybitTickerData, BybitTickerOption,
70        BybitTickersOptionResponse, BybitTradeHistoryResponse, BybitTradesResponse,
71        BybitUpdateMasterApiResponse, BybitUpdateSubApiResponse, BybitWalletBalanceResponse,
72    },
73    query::{
74        BybitAmendOrderParamsBuilder, BybitBatchAmendOrderEntryBuilder,
75        BybitBatchCancelOrderEntryBuilder, BybitBatchCancelOrderParamsBuilder,
76        BybitBatchPlaceOrderEntryBuilder, BybitBorrowParamsBuilder,
77        BybitCancelAllOrdersParamsBuilder, BybitCancelOrderParamsBuilder, BybitFeeRateParams,
78        BybitFeeRateParamsBuilder, BybitFundingParams, BybitFundingParamsBuilder,
79        BybitInstrumentsInfoParams, BybitKlinesParams, BybitKlinesParamsBuilder,
80        BybitNoConvertRepayParamsBuilder, BybitOpenOrdersParamsBuilder,
81        BybitOrderHistoryParamsBuilder, BybitOrderbookParams, BybitOrderbookParamsBuilder,
82        BybitPlaceOrderParamsBuilder, BybitPositionListParams, BybitSetLeverageParamsBuilder,
83        BybitSetMarginModeParamsBuilder, BybitSetTradingStopParams, BybitSubApiKeysParams,
84        BybitSubMembersPageParams, BybitSwitchModeParamsBuilder, BybitTickersParams,
85        BybitTradeHistoryParams, BybitTradesParams, BybitTradesParamsBuilder,
86        BybitUpdateMasterApiParams, BybitUpdateSubApiParams, BybitWalletBalanceParams,
87    },
88};
89use crate::common::{
90    consts::{BYBIT_NAUTILUS_BROKER_ID, BYBIT_VENUE},
91    credential::{Credential, credential_env_vars},
92    enums::{
93        BybitAccountType, BybitContractType, BybitEnvironment, BybitMarginMode, BybitOpenOnly,
94        BybitOrderFilter, BybitOrderSide, BybitOrderType, BybitPositionIdx, BybitPositionMode,
95        BybitProductType,
96    },
97    models::{BybitCursorListResponse, BybitErrorCheck, BybitResponseCheck},
98    parse::{
99        bar_spec_to_bybit_interval, make_bybit_symbol, map_time_in_force, parse_account_state,
100        parse_fill_report, parse_funding_rate, parse_inverse_instrument, parse_kline_bar,
101        parse_linear_instrument, parse_option_instrument, parse_order_status_report,
102        parse_orderbook, parse_position_status_report, parse_spot_instrument, parse_trade_tick,
103        spot_leverage, spot_market_unit, trigger_direction,
104    },
105    symbol::BybitSymbol,
106    urls::bybit_http_base_url,
107};
108
109const DEFAULT_RECV_WINDOW_MS: u64 = 5_000;
110
111trait BuilderResultExt<T> {
112    fn build_anyhow(self) -> anyhow::Result<T>;
113}
114
115impl<T, E: Display> BuilderResultExt<T> for Result<T, E> {
116    fn build_anyhow(self) -> anyhow::Result<T> {
117        self.map_err(|e| anyhow::anyhow!("{e}"))
118    }
119}
120
121const BYBIT_ORDER_REALTIME: &str = "/v5/order/realtime";
122const BYBIT_ORDER_HISTORY: &str = "/v5/order/history";
123
124/// Default Bybit REST API rate limit.
125///
126/// Bybit implements rate limiting per endpoint with varying limits.
127/// We use a conservative 10 requests per second as a general default.
128pub static BYBIT_REST_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
129    Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
130});
131
132/// Bybit repay endpoint rate limit.
133///
134/// Conservative limit to avoid hitting API restrictions when repaying small borrows.
135pub static BYBIT_REPAY_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
136    Quota::per_second(NonZeroU32::new(1).expect("non-zero")).expect("valid constant")
137});
138
139const BYBIT_GLOBAL_RATE_KEY: &str = "bybit:global";
140const BYBIT_REPAY_ROUTE_KEY: &str = "bybit:/v5/account/no-convert-repay";
141
142/// Raw HTTP client for low-level Bybit API operations.
143///
144/// This client handles request/response operations with the Bybit API,
145/// returning venue-specific response types. It does not parse to Nautilus domain types.
146#[cfg_attr(
147    feature = "python",
148    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
149)]
150#[cfg_attr(
151    feature = "python",
152    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
153)]
154#[derive(Clone)]
155pub struct BybitRawHttpClient {
156    base_url: String,
157    client: HttpClient,
158    credential: Option<Credential>,
159    recv_window_ms: u64,
160    retry_manager: RetryManager<BybitHttpError>,
161    cancellation_token: Arc<std::sync::Mutex<CancellationToken>>,
162}
163
164impl Default for BybitRawHttpClient {
165    fn default() -> Self {
166        Self::new(None, 60, 3, 1000, 10_000, DEFAULT_RECV_WINDOW_MS, None)
167            .expect("Failed to create default BybitRawHttpClient")
168    }
169}
170
171impl Debug for BybitRawHttpClient {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.debug_struct(stringify!(BybitRawHttpClient))
174            .field("base_url", &self.base_url)
175            .field("has_credentials", &self.credential.is_some())
176            .field("recv_window_ms", &self.recv_window_ms)
177            .finish()
178    }
179}
180
181impl BybitRawHttpClient {
182    /// Cancels all pending HTTP requests.
183    #[expect(clippy::missing_panics_doc, reason = "mutex poisoning is not expected")]
184    pub fn cancel_all_requests(&self) {
185        self.cancellation_token
186            .lock()
187            .expect("cancellation token lock poisoned")
188            .cancel();
189    }
190
191    /// Replaces the cancelled token with a fresh one so subsequent
192    /// requests are not immediately short-circuited.
193    #[expect(clippy::missing_panics_doc, reason = "mutex poisoning is not expected")]
194    pub fn reset_cancellation_token(&self) {
195        let mut guard = self
196            .cancellation_token
197            .lock()
198            .expect("cancellation token lock poisoned");
199        *guard = CancellationToken::new();
200    }
201
202    /// Returns a clone of the current cancellation token.
203    #[expect(clippy::missing_panics_doc, reason = "mutex poisoning is not expected")]
204    pub fn cancellation_token(&self) -> CancellationToken {
205        self.cancellation_token
206            .lock()
207            .expect("cancellation token lock poisoned")
208            .clone()
209    }
210
211    /// Creates a new [`BybitRawHttpClient`] using the default Bybit HTTP URL.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if the retry manager cannot be created.
216    pub fn new(
217        base_url: Option<String>,
218        timeout_secs: u64,
219        max_retries: u32,
220        retry_delay_ms: u64,
221        retry_delay_max_ms: u64,
222        recv_window_ms: u64,
223        proxy_url: Option<String>,
224    ) -> Result<Self, BybitHttpError> {
225        let retry_config = RetryConfig {
226            max_retries,
227            initial_delay_ms: retry_delay_ms,
228            max_delay_ms: retry_delay_max_ms,
229            backoff_factor: 2.0,
230            jitter_ms: 1000,
231            operation_timeout_ms: Some(60_000),
232            immediate_first: false,
233            max_elapsed_ms: Some(180_000),
234        };
235
236        let retry_manager = RetryManager::new(retry_config);
237
238        Ok(Self {
239            base_url: base_url
240                .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
241            client: HttpClient::new(
242                Self::default_headers(),
243                vec![],
244                Self::rate_limiter_quotas(),
245                Some(*BYBIT_REST_QUOTA),
246                Some(timeout_secs),
247                proxy_url,
248            )
249            .map_err(|e| {
250                BybitHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
251            })?,
252            credential: None,
253            recv_window_ms,
254            retry_manager,
255            cancellation_token: Arc::new(std::sync::Mutex::new(CancellationToken::new())),
256        })
257    }
258
259    /// Creates a new [`BybitRawHttpClient`] configured with credentials.
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if the HTTP client cannot be created.
264    #[expect(clippy::too_many_arguments)]
265    pub fn with_credentials(
266        api_key: String,
267        api_secret: String,
268        base_url: Option<String>,
269        timeout_secs: u64,
270        max_retries: u32,
271        retry_delay_ms: u64,
272        retry_delay_max_ms: u64,
273        recv_window_ms: u64,
274        proxy_url: Option<String>,
275    ) -> Result<Self, BybitHttpError> {
276        let retry_config = RetryConfig {
277            max_retries,
278            initial_delay_ms: retry_delay_ms,
279            max_delay_ms: retry_delay_max_ms,
280            backoff_factor: 2.0,
281            jitter_ms: 1000,
282            operation_timeout_ms: Some(60_000),
283            immediate_first: false,
284            max_elapsed_ms: Some(180_000),
285        };
286
287        let retry_manager = RetryManager::new(retry_config);
288
289        Ok(Self {
290            base_url: base_url
291                .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
292            client: HttpClient::new(
293                Self::default_headers(),
294                vec![],
295                Self::rate_limiter_quotas(),
296                Some(*BYBIT_REST_QUOTA),
297                Some(timeout_secs),
298                proxy_url,
299            )
300            .map_err(|e| {
301                BybitHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
302            })?,
303            credential: Some(Credential::new(api_key, api_secret)),
304            recv_window_ms,
305            retry_manager,
306            cancellation_token: Arc::new(std::sync::Mutex::new(CancellationToken::new())),
307        })
308    }
309
310    /// Creates a new [`BybitRawHttpClient`] with environment variable credential resolution.
311    ///
312    /// If `api_key` or `api_secret` are not provided, they will be loaded from
313    /// environment variables based on the environment flags:
314    /// - Demo: `BYBIT_DEMO_API_KEY`, `BYBIT_DEMO_API_SECRET`
315    /// - Testnet: `BYBIT_TESTNET_API_KEY`, `BYBIT_TESTNET_API_SECRET`
316    /// - Mainnet: `BYBIT_API_KEY`, `BYBIT_API_SECRET`
317    ///
318    /// # Errors
319    ///
320    /// Returns an error if the HTTP client cannot be created.
321    #[expect(clippy::too_many_arguments)]
322    pub fn new_with_env(
323        api_key: Option<String>,
324        api_secret: Option<String>,
325        base_url: Option<String>,
326        demo: bool,
327        testnet: bool,
328        timeout_secs: u64,
329        max_retries: u32,
330        retry_delay_ms: u64,
331        retry_delay_max_ms: u64,
332        recv_window_ms: u64,
333        proxy_url: Option<String>,
334    ) -> Result<Self, BybitHttpError> {
335        let environment = if demo {
336            BybitEnvironment::Demo
337        } else if testnet {
338            BybitEnvironment::Testnet
339        } else {
340            BybitEnvironment::Mainnet
341        };
342        let (key_var, secret_var) = credential_env_vars(environment);
343        let key = get_or_env_var_opt(api_key, key_var);
344        let secret = get_or_env_var_opt(api_secret, secret_var);
345
346        if let (Some(k), Some(s)) = (key, secret) {
347            Self::with_credentials(
348                k,
349                s,
350                base_url,
351                timeout_secs,
352                max_retries,
353                retry_delay_ms,
354                retry_delay_max_ms,
355                recv_window_ms,
356                proxy_url,
357            )
358        } else {
359            Self::new(
360                base_url,
361                timeout_secs,
362                max_retries,
363                retry_delay_ms,
364                retry_delay_max_ms,
365                recv_window_ms,
366                proxy_url,
367            )
368        }
369    }
370
371    fn default_headers() -> HashMap<String, String> {
372        HashMap::from([
373            (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
374            (
375                "X-Referer".to_string(),
376                BYBIT_NAUTILUS_BROKER_ID.to_string(),
377            ),
378        ])
379    }
380
381    fn rate_limiter_quotas() -> Vec<(String, Quota)> {
382        vec![
383            (BYBIT_GLOBAL_RATE_KEY.to_string(), *BYBIT_REST_QUOTA),
384            (BYBIT_REPAY_ROUTE_KEY.to_string(), *BYBIT_REPAY_QUOTA),
385        ]
386    }
387
388    fn rate_limit_keys(endpoint: &str) -> Vec<String> {
389        let normalized = endpoint.split('?').next().unwrap_or(endpoint);
390        let route = format!("bybit:{normalized}");
391
392        vec![BYBIT_GLOBAL_RATE_KEY.to_string(), route]
393    }
394
395    fn sign_request(
396        &self,
397        timestamp: &str,
398        params: Option<&str>,
399    ) -> Result<HashMap<String, String>, BybitHttpError> {
400        let credential = self
401            .credential
402            .as_ref()
403            .ok_or(BybitHttpError::MissingCredentials)?;
404
405        let signature = credential.sign_with_payload(timestamp, self.recv_window_ms, params);
406
407        let mut headers = HashMap::new();
408        headers.insert(
409            "X-BAPI-API-KEY".to_string(),
410            credential.api_key().to_string(),
411        );
412        headers.insert("X-BAPI-TIMESTAMP".to_string(), timestamp.to_string());
413        headers.insert("X-BAPI-SIGN".to_string(), signature);
414        headers.insert(
415            "X-BAPI-RECV-WINDOW".to_string(),
416            self.recv_window_ms.to_string(),
417        );
418
419        Ok(headers)
420    }
421
422    async fn send_request<T: DeserializeOwned + BybitResponseCheck, P: Serialize>(
423        &self,
424        method: Method,
425        endpoint: &str,
426        params: Option<&P>,
427        body: Option<Vec<u8>>,
428        authenticate: bool,
429    ) -> Result<T, BybitHttpError> {
430        let endpoint = endpoint.to_string();
431        let url = format!("{}{endpoint}", self.base_url);
432        let method_clone = method.clone();
433        let body_clone = body.clone();
434
435        // Serialize params before closure to avoid reference lifetime issues
436        let params_str = if method == Method::GET {
437            params
438                .map(serde_urlencoded::to_string)
439                .transpose()
440                .map_err(|e| {
441                    BybitHttpError::JsonError(format!("Failed to serialize params: {e}"))
442                })?
443        } else {
444            None
445        };
446
447        let operation = || {
448            let url = url.clone();
449            let method = method_clone.clone();
450            let body = body_clone.clone();
451            let endpoint = endpoint.clone();
452            let params_str = params_str.clone();
453
454            async move {
455                let mut headers = Self::default_headers();
456
457                if authenticate {
458                    let timestamp = get_atomic_clock_realtime().get_time_ms().to_string();
459
460                    let sign_payload = if method == Method::GET {
461                        params_str.as_deref()
462                    } else {
463                        body.as_ref().and_then(|b| std::str::from_utf8(b).ok())
464                    };
465
466                    let auth_headers = self.sign_request(&timestamp, sign_payload)?;
467                    headers.extend(auth_headers);
468                }
469
470                if method == Method::POST || method == Method::PUT {
471                    headers.insert("Content-Type".to_string(), "application/json".to_string());
472                }
473
474                let full_url = if let Some(ref query) = params_str {
475                    if query.is_empty() {
476                        url
477                    } else {
478                        format!("{url}?{query}")
479                    }
480                } else {
481                    url
482                };
483
484                let rate_limit_keys = Self::rate_limit_keys(&endpoint);
485
486                let response = self
487                    .client
488                    .request(
489                        method,
490                        full_url,
491                        None,
492                        Some(headers),
493                        body,
494                        None,
495                        Some(rate_limit_keys),
496                    )
497                    .await?;
498
499                if response.status.as_u16() >= 400 {
500                    let body = String::from_utf8_lossy(&response.body).to_string();
501                    return Err(BybitHttpError::UnexpectedStatus {
502                        status: response.status.as_u16(),
503                        body,
504                    });
505                }
506
507                // Try to deserialize into the target type
508                match serde_json::from_slice::<T>(&response.body) {
509                    Ok(result) => {
510                        // Check for API-level errors
511                        if result.ret_code() != 0 {
512                            return Err(BybitHttpError::BybitError {
513                                error_code: result.ret_code() as i32,
514                                message: result.ret_msg().to_string(),
515                            });
516                        }
517                        Ok(result)
518                    }
519                    Err(json_err) => {
520                        // Deserialization failed - check if it's a Bybit error response
521                        // (error responses often have result: null which fails typed deserialization)
522                        if let Ok(error_check) =
523                            serde_json::from_slice::<BybitErrorCheck>(&response.body)
524                            && error_check.ret_code != 0
525                        {
526                            return Err(BybitHttpError::BybitError {
527                                error_code: error_check.ret_code as i32,
528                                message: error_check.ret_msg,
529                            });
530                        }
531                        // Not a Bybit error, propagate the JSON parse error
532                        Err(json_err.into())
533                    }
534                }
535            }
536        };
537
538        let should_retry = |error: &BybitHttpError| -> bool {
539            match error {
540                BybitHttpError::NetworkError(_) => true,
541                BybitHttpError::UnexpectedStatus { status, .. } => *status == 429 || *status >= 500,
542                _ => false,
543            }
544        };
545
546        let create_error = |msg: String| -> BybitHttpError {
547            if msg == "canceled" {
548                BybitHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
549            } else {
550                BybitHttpError::NetworkError(msg)
551            }
552        };
553
554        let token = self.cancellation_token();
555
556        self.retry_manager
557            .execute_with_retry_with_cancel(
558                endpoint.as_str(),
559                operation,
560                should_retry,
561                create_error,
562                &token,
563            )
564            .await
565    }
566
567    #[cfg(test)]
568    fn build_path<S: Serialize>(base: &str, params: &S) -> Result<String, BybitHttpError> {
569        let query = serde_urlencoded::to_string(params)
570            .map_err(|e| BybitHttpError::JsonError(e.to_string()))?;
571
572        if query.is_empty() {
573            Ok(base.to_owned())
574        } else {
575            Ok(format!("{base}?{query}"))
576        }
577    }
578
579    /// Fetches the current server time from Bybit.
580    ///
581    /// # Errors
582    ///
583    /// Returns an error if the request fails or the response cannot be parsed.
584    ///
585    /// # References
586    ///
587    /// - <https://bybit-exchange.github.io/docs/v5/market/time>
588    pub async fn get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
589        self.send_request::<_, ()>(Method::GET, "/v5/market/time", None, None, false)
590            .await
591    }
592
593    /// Fetches instrument information from Bybit for a given product category.
594    ///
595    /// # Errors
596    ///
597    /// Returns an error if the request fails or the response cannot be parsed.
598    ///
599    /// # References
600    ///
601    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
602    pub async fn get_instruments<T: DeserializeOwned + BybitResponseCheck>(
603        &self,
604        params: &BybitInstrumentsInfoParams,
605    ) -> Result<T, BybitHttpError> {
606        self.send_request(
607            Method::GET,
608            "/v5/market/instruments-info",
609            Some(params),
610            None,
611            false,
612        )
613        .await
614    }
615
616    /// Fetches spot instrument information from Bybit.
617    ///
618    /// # Errors
619    ///
620    /// Returns an error if the request fails or the response cannot be parsed.
621    ///
622    /// # References
623    ///
624    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
625    pub async fn get_instruments_spot(
626        &self,
627        params: &BybitInstrumentsInfoParams,
628    ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
629        self.get_instruments(params).await
630    }
631
632    /// Fetches linear instrument information from Bybit.
633    ///
634    /// # Errors
635    ///
636    /// Returns an error if the request fails or the response cannot be parsed.
637    ///
638    /// # References
639    ///
640    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
641    pub async fn get_instruments_linear(
642        &self,
643        params: &BybitInstrumentsInfoParams,
644    ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
645        self.get_instruments(params).await
646    }
647
648    /// Fetches inverse instrument information from Bybit.
649    ///
650    /// # Errors
651    ///
652    /// Returns an error if the request fails or the response cannot be parsed.
653    ///
654    /// # References
655    ///
656    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
657    pub async fn get_instruments_inverse(
658        &self,
659        params: &BybitInstrumentsInfoParams,
660    ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
661        self.get_instruments(params).await
662    }
663
664    /// Fetches option instrument information from Bybit.
665    ///
666    /// # Errors
667    ///
668    /// Returns an error if the request fails or the response cannot be parsed.
669    ///
670    /// # References
671    ///
672    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
673    pub async fn get_instruments_option(
674        &self,
675        params: &BybitInstrumentsInfoParams,
676    ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
677        self.get_instruments(params).await
678    }
679
680    /// Fetches kline/candlestick data from Bybit.
681    ///
682    /// # Errors
683    ///
684    /// Returns an error if the request fails or the response cannot be parsed.
685    ///
686    /// # References
687    ///
688    /// - <https://bybit-exchange.github.io/docs/v5/market/kline>
689    pub async fn get_klines(
690        &self,
691        params: &BybitKlinesParams,
692    ) -> Result<BybitKlinesResponse, BybitHttpError> {
693        self.send_request(Method::GET, "/v5/market/kline", Some(params), None, false)
694            .await
695    }
696
697    /// Fetches recent trades from Bybit.
698    ///
699    /// # Errors
700    ///
701    /// Returns an error if the request fails or the response cannot be parsed.
702    ///
703    /// # References
704    ///
705    /// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
706    pub async fn get_recent_trades(
707        &self,
708        params: &BybitTradesParams,
709    ) -> Result<BybitTradesResponse, BybitHttpError> {
710        self.send_request(
711            Method::GET,
712            "/v5/market/recent-trade",
713            Some(params),
714            None,
715            false,
716        )
717        .await
718    }
719
720    /// Fetches funding data from Bybit.
721    ///
722    /// # Errors
723    ///
724    /// Returns an error if the request fails or the response cannot be parsed.
725    ///
726    /// # References
727    ///
728    /// - <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
729    pub async fn get_funding_history(
730        &self,
731        params: &BybitFundingParams,
732    ) -> Result<BybitFundingResponse, BybitHttpError> {
733        self.send_request(
734            Method::GET,
735            "/v5/market/funding/history",
736            Some(params),
737            None,
738            false,
739        )
740        .await
741    }
742
743    /// Fetches orderbook from Bybit.
744    ///
745    /// # Errors
746    ///
747    /// Returns an error if the request fails or the response cannot be parsed.
748    ///
749    /// # References
750    ///
751    /// - <https://bybit-exchange.github.io/docs/v5/market/orderbook>
752    pub async fn get_orderbook(
753        &self,
754        params: &BybitOrderbookParams,
755    ) -> Result<BybitOrderbookResponse, BybitHttpError> {
756        self.send_request(
757            Method::GET,
758            "/v5/market/orderbook",
759            Some(params),
760            None,
761            false,
762        )
763        .await
764    }
765
766    /// Fetches open orders (requires authentication).
767    ///
768    /// # Errors
769    ///
770    /// Returns an error if the request fails or the response cannot be parsed.
771    ///
772    /// # Panics
773    ///
774    /// Panics if the parameter builder fails (should never happen with valid inputs).
775    ///
776    /// # References
777    ///
778    /// - <https://bybit-exchange.github.io/docs/v5/order/open-order>
779    #[expect(clippy::too_many_arguments)]
780    pub async fn get_open_orders(
781        &self,
782        category: BybitProductType,
783        symbol: Option<String>,
784        base_coin: Option<String>,
785        settle_coin: Option<String>,
786        order_id: Option<String>,
787        order_link_id: Option<String>,
788        open_only: Option<BybitOpenOnly>,
789        order_filter: Option<BybitOrderFilter>,
790        limit: Option<u32>,
791        cursor: Option<String>,
792    ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
793        let mut builder = BybitOpenOrdersParamsBuilder::default();
794        builder.category(category);
795
796        if let Some(s) = symbol {
797            builder.symbol(s);
798        }
799
800        if let Some(bc) = base_coin {
801            builder.base_coin(bc);
802        }
803
804        if let Some(sc) = settle_coin {
805            builder.settle_coin(sc);
806        }
807
808        if let Some(oi) = order_id {
809            builder.order_id(oi);
810        }
811
812        if let Some(ol) = order_link_id {
813            builder.order_link_id(ol);
814        }
815
816        if let Some(oo) = open_only {
817            builder.open_only(oo);
818        }
819
820        if let Some(of) = order_filter {
821            builder.order_filter(of);
822        }
823
824        if let Some(l) = limit {
825            builder.limit(l);
826        }
827
828        if let Some(c) = cursor {
829            builder.cursor(c);
830        }
831
832        let params = builder
833            .build()
834            .expect("Failed to build BybitOpenOrdersParams");
835
836        self.send_request(Method::GET, BYBIT_ORDER_REALTIME, Some(&params), None, true)
837            .await
838    }
839
840    /// Places a new order (requires authentication).
841    ///
842    /// # Errors
843    ///
844    /// Returns an error if the request fails or the response cannot be parsed.
845    ///
846    /// # References
847    ///
848    /// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
849    pub async fn place_order(
850        &self,
851        request: &serde_json::Value,
852    ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
853        let body = serde_json::to_vec(request)?;
854        self.send_request::<_, ()>(Method::POST, "/v5/order/create", None, Some(body), true)
855            .await
856    }
857
858    /// Fetches wallet balance (requires authentication).
859    ///
860    /// # Errors
861    ///
862    /// Returns an error if the request fails or the response cannot be parsed.
863    ///
864    /// # References
865    ///
866    /// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
867    pub async fn get_wallet_balance(
868        &self,
869        params: &BybitWalletBalanceParams,
870    ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
871        self.send_request(
872            Method::GET,
873            "/v5/account/wallet-balance",
874            Some(params),
875            None,
876            true,
877        )
878        .await
879    }
880
881    /// Fetches account information (requires authentication).
882    ///
883    /// # Errors
884    ///
885    /// Returns an error if the request fails or the response cannot be parsed.
886    ///
887    /// # References
888    ///
889    /// - <https://bybit-exchange.github.io/docs/v5/account/account-info>
890    pub async fn get_account_info(&self) -> Result<BybitAccountInfoResponse, BybitHttpError> {
891        self.send_request::<_, ()>(Method::GET, "/v5/account/info", None, None, true)
892            .await
893    }
894
895    /// Fetches account details (requires authentication).
896    ///
897    /// # Errors
898    ///
899    /// Returns an error if the request fails or the response cannot be parsed.
900    ///
901    /// # References
902    ///
903    /// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
904    pub async fn get_account_details(&self) -> Result<BybitAccountDetailsResponse, BybitHttpError> {
905        self.send_request::<_, ()>(Method::GET, "/v5/user/query-api", None, None, true)
906            .await
907    }
908
909    /// Modifies a sub-account API key (requires authentication).
910    ///
911    /// # Errors
912    ///
913    /// Returns an error if the request fails or the response cannot be parsed.
914    ///
915    /// # References
916    ///
917    /// - <https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey>
918    pub async fn update_sub_api_key(
919        &self,
920        params: &BybitUpdateSubApiParams,
921    ) -> Result<BybitUpdateSubApiResponse, BybitHttpError> {
922        let body = serde_json::to_vec(params)?;
923        self.send_request::<_, ()>(
924            Method::POST,
925            "/v5/user/update-sub-api",
926            None,
927            Some(body),
928            true,
929        )
930        .await
931    }
932
933    /// Modifies the master API key that issued the request (requires authentication).
934    ///
935    /// # Errors
936    ///
937    /// Returns an error if the request fails or the response cannot be parsed.
938    ///
939    /// # References
940    ///
941    /// - <https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey>
942    pub async fn update_master_api_key(
943        &self,
944        params: &BybitUpdateMasterApiParams,
945    ) -> Result<BybitUpdateMasterApiResponse, BybitHttpError> {
946        let body = serde_json::to_vec(params)?;
947        self.send_request::<_, ()>(Method::POST, "/v5/user/update-api", None, Some(body), true)
948            .await
949    }
950
951    /// Fetches the sub-account list (up to 1000 rows, non-paginated).
952    ///
953    /// # Errors
954    ///
955    /// Returns an error if the request fails or the response cannot be parsed.
956    ///
957    /// # References
958    ///
959    /// - <https://bybit-exchange.github.io/docs/v5/user/subuid-list>
960    pub async fn get_sub_members(&self) -> Result<BybitSubMembersResponse, BybitHttpError> {
961        self.send_request::<_, ()>(Method::GET, "/v5/user/query-sub-members", None, None, true)
962            .await
963    }
964
965    /// Fetches a cursor-paginated sub-account list (`/v5/user/submembers`).
966    ///
967    /// # Errors
968    ///
969    /// Returns an error if the request fails or the response cannot be parsed.
970    ///
971    /// # References
972    ///
973    /// - <https://bybit-exchange.github.io/docs/v5/user/page-subuid>
974    pub async fn get_sub_members_paged(
975        &self,
976        params: &BybitSubMembersPageParams,
977    ) -> Result<BybitSubMembersPagedResponse, BybitHttpError> {
978        self.send_request(Method::GET, "/v5/user/submembers", Some(params), None, true)
979            .await
980    }
981
982    /// Fetches fund-custodial sub-accounts (`/v5/user/escrow_sub_members`).
983    ///
984    /// # Errors
985    ///
986    /// Returns an error if the request fails or the response cannot be parsed.
987    ///
988    /// # References
989    ///
990    /// - <https://bybit-exchange.github.io/docs/v5/user/fund-subuid-list>
991    pub async fn get_escrow_sub_members(
992        &self,
993        params: &BybitSubMembersPageParams,
994    ) -> Result<BybitEscrowSubMembersResponse, BybitHttpError> {
995        self.send_request(
996            Method::GET,
997            "/v5/user/escrow_sub_members",
998            Some(params),
999            None,
1000            true,
1001        )
1002        .await
1003    }
1004
1005    /// Fetches all API keys belonging to a given sub-account.
1006    ///
1007    /// # Errors
1008    ///
1009    /// Returns an error if the request fails or the response cannot be parsed.
1010    ///
1011    /// # References
1012    ///
1013    /// - <https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys>
1014    pub async fn get_sub_api_keys(
1015        &self,
1016        params: &BybitSubApiKeysParams,
1017    ) -> Result<BybitSubApiKeysResponse, BybitHttpError> {
1018        self.send_request(
1019            Method::GET,
1020            "/v5/user/sub-apikeys",
1021            Some(params),
1022            None,
1023            true,
1024        )
1025        .await
1026    }
1027
1028    /// Fetches every sub-account page via `/v5/user/submembers` and returns the
1029    /// flattened list. Walks the cursor until Bybit signals end-of-pages.
1030    ///
1031    /// # Errors
1032    ///
1033    /// Returns an error if any page request fails or the response cannot be parsed.
1034    pub async fn fetch_all_sub_members_paged(
1035        &self,
1036        page_size: Option<u32>,
1037    ) -> Result<Vec<BybitSubMember>, BybitHttpError> {
1038        let mut members = Vec::new();
1039        let mut cursor: Option<String> = None;
1040
1041        loop {
1042            let params = BybitSubMembersPageParams {
1043                page_size,
1044                next_cursor: cursor.take(),
1045            };
1046            let mut page = self.get_sub_members_paged(&params).await?;
1047            let next = page.result.continuation_cursor().map(str::to_owned);
1048            members.append(&mut page.result.sub_members);
1049
1050            match next {
1051                Some(c) => cursor = Some(c),
1052                None => break,
1053            }
1054        }
1055
1056        Ok(members)
1057    }
1058
1059    /// Fetches every fund-custodial page via `/v5/user/escrow_sub_members` and
1060    /// returns the flattened list. Walks the cursor until Bybit signals
1061    /// end-of-pages.
1062    ///
1063    /// # Errors
1064    ///
1065    /// Returns an error if any page request fails or the response cannot be parsed.
1066    pub async fn fetch_all_escrow_sub_members(
1067        &self,
1068        page_size: Option<u32>,
1069    ) -> Result<Vec<BybitSubMember>, BybitHttpError> {
1070        let mut members = Vec::new();
1071        let mut cursor: Option<String> = None;
1072
1073        loop {
1074            let params = BybitSubMembersPageParams {
1075                page_size,
1076                next_cursor: cursor.take(),
1077            };
1078            let mut page = self.get_escrow_sub_members(&params).await?;
1079            let next = page.result.continuation_cursor().map(str::to_owned);
1080            members.append(&mut page.result.sub_members);
1081
1082            match next {
1083                Some(c) => cursor = Some(c),
1084                None => break,
1085            }
1086        }
1087
1088        Ok(members)
1089    }
1090
1091    /// Fetches every page of sub-account API keys for `sub_member_id` and
1092    /// returns the flattened list. Walks the cursor until Bybit signals
1093    /// end-of-pages.
1094    ///
1095    /// # Errors
1096    ///
1097    /// Returns an error if any page request fails or the response cannot be parsed.
1098    pub async fn fetch_all_sub_api_keys(
1099        &self,
1100        sub_member_id: impl Into<String>,
1101        limit: Option<u32>,
1102    ) -> Result<Vec<BybitSubApiKeyInfo>, BybitHttpError> {
1103        let sub_member_id = sub_member_id.into();
1104        let mut keys = Vec::new();
1105        let mut cursor: Option<String> = None;
1106
1107        loop {
1108            let params = BybitSubApiKeysParams {
1109                sub_member_id: sub_member_id.clone(),
1110                limit,
1111                cursor: cursor.take(),
1112            };
1113            let mut page = self.get_sub_api_keys(&params).await?;
1114            let next = page.result.continuation_cursor().map(str::to_owned);
1115            keys.append(&mut page.result.keys);
1116
1117            match next {
1118                Some(c) => cursor = Some(c),
1119                None => break,
1120            }
1121        }
1122
1123        Ok(keys)
1124    }
1125
1126    /// Fetches trading fee rates for symbols.
1127    ///
1128    /// # Errors
1129    ///
1130    /// Returns an error if the request fails or the response cannot be parsed.
1131    ///
1132    /// # References
1133    ///
1134    /// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
1135    pub async fn get_fee_rate(
1136        &self,
1137        params: &BybitFeeRateParams,
1138    ) -> Result<BybitFeeRateResponse, BybitHttpError> {
1139        self.send_request(
1140            Method::GET,
1141            "/v5/account/fee-rate",
1142            Some(params),
1143            None,
1144            true,
1145        )
1146        .await
1147    }
1148
1149    /// Sets the margin mode for the account.
1150    ///
1151    /// # Errors
1152    ///
1153    /// Returns an error if:
1154    /// - Credentials are missing.
1155    /// - The request fails.
1156    /// - The API returns an error.
1157    ///
1158    /// # Panics
1159    ///
1160    /// Panics if required parameters are not provided (should not happen with current implementation).
1161    ///
1162    /// # References
1163    ///
1164    /// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
1165    pub async fn set_margin_mode(
1166        &self,
1167        margin_mode: BybitMarginMode,
1168    ) -> Result<BybitSetMarginModeResponse, BybitHttpError> {
1169        let params = BybitSetMarginModeParamsBuilder::default()
1170            .set_margin_mode(margin_mode)
1171            .build()
1172            .expect("Failed to build BybitSetMarginModeParams");
1173
1174        let body = serde_json::to_vec(&params)?;
1175        self.send_request::<_, ()>(
1176            Method::POST,
1177            "/v5/account/set-margin-mode",
1178            None,
1179            Some(body),
1180            true,
1181        )
1182        .await
1183    }
1184
1185    /// Sets leverage for a symbol.
1186    ///
1187    /// # Errors
1188    ///
1189    /// Returns an error if:
1190    /// - Credentials are missing.
1191    /// - The request fails.
1192    /// - The API returns an error.
1193    ///
1194    /// # Panics
1195    ///
1196    /// Panics if required parameters are not provided (should not happen with current implementation).
1197    ///
1198    /// # References
1199    ///
1200    /// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
1201    pub async fn set_leverage(
1202        &self,
1203        product_type: BybitProductType,
1204        symbol: &str,
1205        buy_leverage: &str,
1206        sell_leverage: &str,
1207    ) -> Result<BybitSetLeverageResponse, BybitHttpError> {
1208        let params = BybitSetLeverageParamsBuilder::default()
1209            .category(product_type)
1210            .symbol(symbol.to_string())
1211            .buy_leverage(buy_leverage.to_string())
1212            .sell_leverage(sell_leverage.to_string())
1213            .build()
1214            .expect("Failed to build BybitSetLeverageParams");
1215
1216        let body = serde_json::to_vec(&params)?;
1217        self.send_request::<_, ()>(
1218            Method::POST,
1219            "/v5/position/set-leverage",
1220            None,
1221            Some(body),
1222            true,
1223        )
1224        .await
1225    }
1226
1227    /// Switches position mode for a product type.
1228    ///
1229    /// # Errors
1230    ///
1231    /// Returns an error if:
1232    /// - Credentials are missing.
1233    /// - The request fails.
1234    /// - The API returns an error.
1235    ///
1236    /// # Panics
1237    ///
1238    /// Panics if required parameters are not provided (should not happen with current implementation).
1239    ///
1240    /// # References
1241    ///
1242    /// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
1243    pub async fn switch_mode(
1244        &self,
1245        product_type: BybitProductType,
1246        mode: BybitPositionMode,
1247        symbol: Option<String>,
1248        coin: Option<String>,
1249    ) -> Result<BybitSwitchModeResponse, BybitHttpError> {
1250        let mut builder = BybitSwitchModeParamsBuilder::default();
1251        builder.category(product_type);
1252        builder.mode(mode);
1253
1254        if let Some(s) = symbol {
1255            builder.symbol(s);
1256        }
1257
1258        if let Some(c) = coin {
1259            builder.coin(c);
1260        }
1261
1262        let params = builder
1263            .build()
1264            .expect("Failed to build BybitSwitchModeParams");
1265
1266        let body = serde_json::to_vec(&params)?;
1267        self.send_request::<_, ()>(
1268            Method::POST,
1269            "/v5/position/switch-mode",
1270            None,
1271            Some(body),
1272            true,
1273        )
1274        .await
1275    }
1276
1277    /// Sets trading stop parameters including trailing stops.
1278    ///
1279    /// # Errors
1280    ///
1281    /// Returns an error if:
1282    /// - Credentials are missing.
1283    /// - The request fails.
1284    /// - The API returns an error.
1285    ///
1286    /// # References
1287    ///
1288    /// - <https://bybit-exchange.github.io/docs/v5/position/trading-stop>
1289    pub async fn set_trading_stop(
1290        &self,
1291        params: &BybitSetTradingStopParams,
1292    ) -> Result<BybitSetTradingStopResponse, BybitHttpError> {
1293        let body = serde_json::to_vec(params)?;
1294        self.send_request::<_, ()>(
1295            Method::POST,
1296            "/v5/position/trading-stop",
1297            None,
1298            Some(body),
1299            true,
1300        )
1301        .await
1302    }
1303
1304    /// Manually borrows coins for margin trading.
1305    ///
1306    /// # Errors
1307    ///
1308    /// Returns an error if:
1309    /// - Credentials are missing.
1310    /// - The request fails.
1311    /// - Insufficient collateral for the borrow.
1312    ///
1313    /// # Panics
1314    ///
1315    /// Panics if the parameter builder fails (should never happen with valid inputs).
1316    ///
1317    /// # References
1318    ///
1319    /// - <https://bybit-exchange.github.io/docs/v5/account/borrow>
1320    pub async fn borrow(
1321        &self,
1322        coin: &str,
1323        amount: &str,
1324    ) -> Result<BybitBorrowResponse, BybitHttpError> {
1325        let params = BybitBorrowParamsBuilder::default()
1326            .coin(coin.to_string())
1327            .amount(amount.to_string())
1328            .build()
1329            .expect("Failed to build BybitBorrowParams");
1330
1331        let body = serde_json::to_vec(&params)?;
1332        self.send_request::<_, ()>(Method::POST, "/v5/account/borrow", None, Some(body), true)
1333            .await
1334    }
1335
1336    /// Manually repays borrowed coins without asset conversion.
1337    ///
1338    /// # Errors
1339    ///
1340    /// Returns an error if:
1341    /// - Credentials are missing.
1342    /// - The request fails.
1343    /// - Called between 04:00-05:30 UTC (interest calculation window).
1344    /// - Insufficient spot balance for repayment.
1345    ///
1346    /// # Panics
1347    ///
1348    /// Panics if the parameter builder fails (should never happen with valid inputs).
1349    ///
1350    /// # References
1351    ///
1352    /// - <https://bybit-exchange.github.io/docs/v5/account/no-convert-repay>
1353    pub async fn no_convert_repay(
1354        &self,
1355        coin: &str,
1356        amount: Option<&str>,
1357    ) -> Result<BybitNoConvertRepayResponse, BybitHttpError> {
1358        let mut builder = BybitNoConvertRepayParamsBuilder::default();
1359        builder.coin(coin.to_string());
1360
1361        if let Some(amt) = amount {
1362            builder.amount(amt.to_string());
1363        }
1364
1365        let params = builder
1366            .build()
1367            .expect("Failed to build BybitNoConvertRepayParams");
1368
1369        if let Ok(params_json) = serde_json::to_string(&params) {
1370            log::debug!("Repay request params: {params_json}");
1371        }
1372
1373        let body = serde_json::to_vec(&params)?;
1374        let result = self
1375            .send_request::<_, ()>(
1376                Method::POST,
1377                "/v5/account/no-convert-repay",
1378                None,
1379                Some(body),
1380                true,
1381            )
1382            .await;
1383
1384        if let Err(ref e) = result
1385            && let Ok(params_json) = serde_json::to_string(&params)
1386        {
1387            log::error!("Repay request failed with params {params_json}: {e}");
1388        }
1389
1390        result
1391    }
1392
1393    /// Fetches tickers for market data.
1394    ///
1395    /// # Errors
1396    ///
1397    /// Returns an error if the request fails or the response cannot be parsed.
1398    ///
1399    /// # References
1400    ///
1401    /// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
1402    pub async fn get_tickers<T: DeserializeOwned + BybitResponseCheck>(
1403        &self,
1404        params: &BybitTickersParams,
1405    ) -> Result<T, BybitHttpError> {
1406        self.send_request(Method::GET, "/v5/market/tickers", Some(params), None, false)
1407            .await
1408    }
1409
1410    /// Fetches trade execution history (requires authentication).
1411    ///
1412    /// # Errors
1413    ///
1414    /// Returns an error if the request fails or the response cannot be parsed.
1415    ///
1416    /// # References
1417    ///
1418    /// - <https://bybit-exchange.github.io/docs/v5/order/execution>
1419    pub async fn get_trade_history(
1420        &self,
1421        params: &BybitTradeHistoryParams,
1422    ) -> Result<BybitTradeHistoryResponse, BybitHttpError> {
1423        self.send_request(Method::GET, "/v5/execution/list", Some(params), None, true)
1424            .await
1425    }
1426
1427    /// Fetches position information (requires authentication).
1428    ///
1429    /// # Errors
1430    ///
1431    /// This function returns an error if:
1432    /// - Credentials are missing.
1433    /// - The request fails.
1434    /// - The API returns an error.
1435    ///
1436    /// # References
1437    ///
1438    /// - <https://bybit-exchange.github.io/docs/v5/position>
1439    pub async fn get_positions(
1440        &self,
1441        params: &BybitPositionListParams,
1442    ) -> Result<BybitPositionListResponse, BybitHttpError> {
1443        self.send_request(Method::GET, "/v5/position/list", Some(params), None, true)
1444            .await
1445    }
1446
1447    /// Returns the base URL used for requests.
1448    #[must_use]
1449    pub fn base_url(&self) -> &str {
1450        &self.base_url
1451    }
1452
1453    /// Returns the configured receive window in milliseconds.
1454    #[must_use]
1455    pub fn recv_window_ms(&self) -> u64 {
1456        self.recv_window_ms
1457    }
1458
1459    /// Returns the API credential if configured.
1460    #[must_use]
1461    pub fn credential(&self) -> Option<&Credential> {
1462        self.credential.as_ref()
1463    }
1464}
1465
1466/// Provides a HTTP client for connecting to the [Bybit](https://bybit.com) REST API.
1467#[cfg_attr(
1468    feature = "python",
1469    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
1470)]
1471#[cfg_attr(
1472    feature = "python",
1473    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.bybit")
1474)]
1475/// High-level HTTP client that wraps the raw client and provides Nautilus domain types.
1476///
1477/// This client maintains an instrument cache and uses it to parse venue responses
1478/// into Nautilus domain objects.
1479pub struct BybitHttpClient {
1480    pub(crate) inner: Arc<BybitRawHttpClient>,
1481    pub(crate) instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
1482    clock: &'static AtomicTime,
1483    cache_initialized: Arc<AtomicBool>,
1484    use_spot_position_reports: Arc<AtomicBool>,
1485}
1486
1487impl Clone for BybitHttpClient {
1488    fn clone(&self) -> Self {
1489        Self {
1490            inner: self.inner.clone(),
1491            instruments_cache: self.instruments_cache.clone(),
1492            cache_initialized: self.cache_initialized.clone(),
1493            use_spot_position_reports: self.use_spot_position_reports.clone(),
1494            clock: self.clock,
1495        }
1496    }
1497}
1498
1499impl Default for BybitHttpClient {
1500    fn default() -> Self {
1501        Self::new(None, 60, 3, 1000, 10_000, DEFAULT_RECV_WINDOW_MS, None)
1502            .expect("Failed to create default BybitHttpClient")
1503    }
1504}
1505
1506impl Debug for BybitHttpClient {
1507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1508        f.debug_struct(stringify!(BybitHttpClient))
1509            .field("inner", &self.inner)
1510            .finish()
1511    }
1512}
1513
1514impl BybitHttpClient {
1515    /// Creates a new [`BybitHttpClient`] using the default Bybit HTTP URL.
1516    ///
1517    /// # Errors
1518    ///
1519    /// Returns an error if the retry manager cannot be created.
1520    pub fn new(
1521        base_url: Option<String>,
1522        timeout_secs: u64,
1523        max_retries: u32,
1524        retry_delay_ms: u64,
1525        retry_delay_max_ms: u64,
1526        recv_window_ms: u64,
1527        proxy_url: Option<String>,
1528    ) -> Result<Self, BybitHttpError> {
1529        Ok(Self {
1530            inner: Arc::new(BybitRawHttpClient::new(
1531                base_url,
1532                timeout_secs,
1533                max_retries,
1534                retry_delay_ms,
1535                retry_delay_max_ms,
1536                recv_window_ms,
1537                proxy_url,
1538            )?),
1539            instruments_cache: Arc::new(AtomicMap::new()),
1540            cache_initialized: Arc::new(AtomicBool::new(false)),
1541            use_spot_position_reports: Arc::new(AtomicBool::new(false)),
1542            clock: get_atomic_clock_realtime(),
1543        })
1544    }
1545
1546    /// Creates a new [`BybitHttpClient`] configured with credentials.
1547    ///
1548    /// # Errors
1549    ///
1550    /// Returns an error if the retry manager cannot be created.
1551    #[expect(clippy::too_many_arguments)]
1552    pub fn with_credentials(
1553        api_key: String,
1554        api_secret: String,
1555        base_url: Option<String>,
1556        timeout_secs: u64,
1557        max_retries: u32,
1558        retry_delay_ms: u64,
1559        retry_delay_max_ms: u64,
1560        recv_window_ms: u64,
1561        proxy_url: Option<String>,
1562    ) -> Result<Self, BybitHttpError> {
1563        Ok(Self {
1564            inner: Arc::new(BybitRawHttpClient::with_credentials(
1565                api_key,
1566                api_secret,
1567                base_url,
1568                timeout_secs,
1569                max_retries,
1570                retry_delay_ms,
1571                retry_delay_max_ms,
1572                recv_window_ms,
1573                proxy_url,
1574            )?),
1575            instruments_cache: Arc::new(AtomicMap::new()),
1576            cache_initialized: Arc::new(AtomicBool::new(false)),
1577            use_spot_position_reports: Arc::new(AtomicBool::new(false)),
1578            clock: get_atomic_clock_realtime(),
1579        })
1580    }
1581
1582    /// Creates a new [`BybitHttpClient`] with optional credentials resolved from environment variables.
1583    ///
1584    /// Credentials are resolved in the following order:
1585    /// 1. Use provided `api_key`/`api_secret` if `Some`
1586    /// 2. Fall back to environment variables based on environment:
1587    ///    - Demo: `BYBIT_DEMO_API_KEY`, `BYBIT_DEMO_API_SECRET`
1588    ///    - Testnet: `BYBIT_TESTNET_API_KEY`, `BYBIT_TESTNET_API_SECRET`
1589    ///    - Mainnet: `BYBIT_API_KEY`, `BYBIT_API_SECRET`
1590    ///
1591    /// # Errors
1592    ///
1593    /// Returns an error if the retry manager cannot be created.
1594    #[expect(clippy::too_many_arguments)]
1595    pub fn new_with_env(
1596        api_key: Option<String>,
1597        api_secret: Option<String>,
1598        base_url: Option<String>,
1599        demo: bool,
1600        testnet: bool,
1601        timeout_secs: u64,
1602        max_retries: u32,
1603        retry_delay_ms: u64,
1604        retry_delay_max_ms: u64,
1605        recv_window_ms: u64,
1606        proxy_url: Option<String>,
1607    ) -> Result<Self, BybitHttpError> {
1608        let environment = if demo {
1609            BybitEnvironment::Demo
1610        } else if testnet {
1611            BybitEnvironment::Testnet
1612        } else {
1613            BybitEnvironment::Mainnet
1614        };
1615        let (key_var, secret_var) = credential_env_vars(environment);
1616        let key = get_or_env_var_opt(api_key, key_var);
1617        let secret = get_or_env_var_opt(api_secret, secret_var);
1618
1619        match (key, secret) {
1620            (Some(k), Some(s)) => Self::with_credentials(
1621                k,
1622                s,
1623                base_url,
1624                timeout_secs,
1625                max_retries,
1626                retry_delay_ms,
1627                retry_delay_max_ms,
1628                recv_window_ms,
1629                proxy_url,
1630            ),
1631            _ => Self::new(
1632                base_url,
1633                timeout_secs,
1634                max_retries,
1635                retry_delay_ms,
1636                retry_delay_max_ms,
1637                recv_window_ms,
1638                proxy_url,
1639            ),
1640        }
1641    }
1642
1643    #[must_use]
1644    pub fn base_url(&self) -> &str {
1645        self.inner.base_url()
1646    }
1647
1648    #[must_use]
1649    pub fn recv_window_ms(&self) -> u64 {
1650        self.inner.recv_window_ms()
1651    }
1652
1653    #[must_use]
1654    pub fn credential(&self) -> Option<&Credential> {
1655        self.inner.credential()
1656    }
1657
1658    pub fn set_use_spot_position_reports(&self, use_spot_position_reports: bool) {
1659        self.use_spot_position_reports
1660            .store(use_spot_position_reports, Ordering::Relaxed);
1661    }
1662
1663    pub fn cancel_all_requests(&self) {
1664        self.inner.cancel_all_requests();
1665    }
1666
1667    pub fn reset_cancellation_token(&self) {
1668        self.inner.reset_cancellation_token();
1669    }
1670
1671    pub fn cancellation_token(&self) -> CancellationToken {
1672        self.inner.cancellation_token()
1673    }
1674
1675    /// Any existing instrument with the same symbol will be replaced.
1676    pub fn cache_instrument(&self, instrument: InstrumentAny) {
1677        self.instruments_cache
1678            .insert(instrument.symbol().inner(), instrument);
1679        self.cache_initialized.store(true, Ordering::Release);
1680    }
1681
1682    /// Any existing instruments with the same symbols will be replaced.
1683    pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
1684        self.instruments_cache.rcu(|m| {
1685            for instrument in instruments {
1686                m.insert(instrument.symbol().inner(), instrument.clone());
1687            }
1688        });
1689        self.cache_initialized.store(true, Ordering::Release);
1690    }
1691
1692    pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1693        self.instruments_cache.get_cloned(symbol)
1694    }
1695
1696    fn instrument_from_cache(&self, symbol: &Symbol) -> anyhow::Result<InstrumentAny> {
1697        self.get_instrument(&symbol.inner()).ok_or_else(|| {
1698            anyhow::anyhow!(
1699                "Instrument {symbol} not found in cache, ensure instruments loaded first"
1700            )
1701        })
1702    }
1703
1704    #[must_use]
1705    fn generate_ts_init(&self) -> UnixNanos {
1706        self.clock.get_time_ns()
1707    }
1708
1709    /// Fetches the current server time from Bybit.
1710    ///
1711    /// # Errors
1712    ///
1713    /// Returns an error if:
1714    /// - The request fails.
1715    /// - The response cannot be parsed.
1716    ///
1717    /// # References
1718    ///
1719    /// - <https://bybit-exchange.github.io/docs/v5/market/time>
1720    pub async fn get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
1721        self.inner.get_server_time().await
1722    }
1723
1724    /// Fetches instrument information from Bybit for a given product category.
1725    ///
1726    /// # Errors
1727    ///
1728    /// Returns an error if:
1729    /// - The request fails.
1730    /// - The response cannot be parsed.
1731    ///
1732    /// # References
1733    ///
1734    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
1735    pub async fn get_instruments<T: DeserializeOwned + BybitResponseCheck>(
1736        &self,
1737        params: &BybitInstrumentsInfoParams,
1738    ) -> Result<T, BybitHttpError> {
1739        self.inner.get_instruments(params).await
1740    }
1741
1742    /// Fetches spot instrument information from Bybit.
1743    ///
1744    /// # Errors
1745    ///
1746    /// Returns an error if:
1747    /// - The request fails.
1748    /// - The response cannot be parsed.
1749    ///
1750    /// # References
1751    ///
1752    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
1753    pub async fn get_instruments_spot(
1754        &self,
1755        params: &BybitInstrumentsInfoParams,
1756    ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
1757        self.inner.get_instruments_spot(params).await
1758    }
1759
1760    /// Fetches linear instrument information from Bybit.
1761    ///
1762    /// # Errors
1763    ///
1764    /// Returns an error if:
1765    /// - The request fails.
1766    /// - The response cannot be parsed.
1767    ///
1768    /// # References
1769    ///
1770    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
1771    pub async fn get_instruments_linear(
1772        &self,
1773        params: &BybitInstrumentsInfoParams,
1774    ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
1775        self.inner.get_instruments_linear(params).await
1776    }
1777
1778    /// Fetches inverse instrument information from Bybit.
1779    ///
1780    /// # Errors
1781    ///
1782    /// Returns an error if:
1783    /// - The request fails.
1784    /// - The response cannot be parsed.
1785    ///
1786    /// # References
1787    ///
1788    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
1789    pub async fn get_instruments_inverse(
1790        &self,
1791        params: &BybitInstrumentsInfoParams,
1792    ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
1793        self.inner.get_instruments_inverse(params).await
1794    }
1795
1796    /// Fetches option instrument information from Bybit.
1797    ///
1798    /// # Errors
1799    ///
1800    /// Returns an error if:
1801    /// - The request fails.
1802    /// - The response cannot be parsed.
1803    ///
1804    /// # References
1805    ///
1806    /// - <https://bybit-exchange.github.io/docs/v5/market/instrument>
1807    pub async fn get_instruments_option(
1808        &self,
1809        params: &BybitInstrumentsInfoParams,
1810    ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
1811        self.inner.get_instruments_option(params).await
1812    }
1813
1814    /// Fetches kline/candlestick data from Bybit.
1815    ///
1816    /// # Errors
1817    ///
1818    /// Returns an error if:
1819    /// - The request fails.
1820    /// - The response cannot be parsed.
1821    ///
1822    /// # References
1823    ///
1824    /// - <https://bybit-exchange.github.io/docs/v5/market/kline>
1825    pub async fn get_klines(
1826        &self,
1827        params: &BybitKlinesParams,
1828    ) -> Result<BybitKlinesResponse, BybitHttpError> {
1829        self.inner.get_klines(params).await
1830    }
1831
1832    /// Fetches recent trades from Bybit.
1833    ///
1834    /// # Errors
1835    ///
1836    /// Returns an error if:
1837    /// - The request fails.
1838    /// - The response cannot be parsed.
1839    ///
1840    /// # References
1841    ///
1842    /// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
1843    pub async fn get_recent_trades(
1844        &self,
1845        params: &BybitTradesParams,
1846    ) -> Result<BybitTradesResponse, BybitHttpError> {
1847        self.inner.get_recent_trades(params).await
1848    }
1849
1850    /// Fetches open orders (requires authentication).
1851    ///
1852    /// # Errors
1853    ///
1854    /// Returns an error if:
1855    /// - The request fails.
1856    /// - The response cannot be parsed.
1857    ///
1858    /// # References
1859    ///
1860    /// - <https://bybit-exchange.github.io/docs/v5/order/open-order>
1861    #[expect(clippy::too_many_arguments)]
1862    pub async fn get_open_orders(
1863        &self,
1864        category: BybitProductType,
1865        symbol: Option<String>,
1866        base_coin: Option<String>,
1867        settle_coin: Option<String>,
1868        order_id: Option<String>,
1869        order_link_id: Option<String>,
1870        open_only: Option<BybitOpenOnly>,
1871        order_filter: Option<BybitOrderFilter>,
1872        limit: Option<u32>,
1873        cursor: Option<String>,
1874    ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
1875        self.inner
1876            .get_open_orders(
1877                category,
1878                symbol,
1879                base_coin,
1880                settle_coin,
1881                order_id,
1882                order_link_id,
1883                open_only,
1884                order_filter,
1885                limit,
1886                cursor,
1887            )
1888            .await
1889    }
1890
1891    /// Places a new order (requires authentication).
1892    ///
1893    /// # Errors
1894    ///
1895    /// Returns an error if:
1896    /// - The request fails.
1897    /// - The response cannot be parsed.
1898    ///
1899    /// # References
1900    ///
1901    /// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
1902    pub async fn place_order(
1903        &self,
1904        request: &serde_json::Value,
1905    ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
1906        self.inner.place_order(request).await
1907    }
1908
1909    /// Fetches wallet balance (requires authentication).
1910    ///
1911    /// # Errors
1912    ///
1913    /// Returns an error if:
1914    /// - The request fails.
1915    /// - The response cannot be parsed.
1916    ///
1917    /// # References
1918    ///
1919    /// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
1920    pub async fn get_wallet_balance(
1921        &self,
1922        params: &BybitWalletBalanceParams,
1923    ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
1924        self.inner.get_wallet_balance(params).await
1925    }
1926
1927    /// Fetches account information (requires authentication).
1928    ///
1929    /// # Errors
1930    ///
1931    /// Returns an error if:
1932    /// - The request fails.
1933    /// - The response cannot be parsed.
1934    ///
1935    /// # References
1936    ///
1937    /// - <https://bybit-exchange.github.io/docs/v5/account/account-info>
1938    pub async fn get_account_info(&self) -> Result<BybitAccountInfoResponse, BybitHttpError> {
1939        self.inner.get_account_info().await
1940    }
1941
1942    /// Fetches API key information including account details (requires authentication).
1943    ///
1944    /// # Errors
1945    ///
1946    /// Returns an error if:
1947    /// - The request fails.
1948    /// - The response cannot be parsed.
1949    ///
1950    /// # References
1951    ///
1952    /// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
1953    pub async fn get_account_details(&self) -> Result<BybitAccountDetailsResponse, BybitHttpError> {
1954        self.inner.get_account_details().await
1955    }
1956
1957    /// Modifies a sub-account API key (requires authentication).
1958    ///
1959    /// # Errors
1960    ///
1961    /// Returns an error if:
1962    /// - The request fails.
1963    /// - The response cannot be parsed.
1964    ///
1965    /// # References
1966    ///
1967    /// - <https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey>
1968    pub async fn update_sub_api_key(
1969        &self,
1970        params: &BybitUpdateSubApiParams,
1971    ) -> Result<BybitUpdateSubApiResponse, BybitHttpError> {
1972        self.inner.update_sub_api_key(params).await
1973    }
1974
1975    /// Modifies the master API key that issued the request (requires authentication).
1976    ///
1977    /// # Errors
1978    ///
1979    /// Returns an error if:
1980    /// - The request fails.
1981    /// - The response cannot be parsed.
1982    ///
1983    /// # References
1984    ///
1985    /// - <https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey>
1986    pub async fn update_master_api_key(
1987        &self,
1988        params: &BybitUpdateMasterApiParams,
1989    ) -> Result<BybitUpdateMasterApiResponse, BybitHttpError> {
1990        self.inner.update_master_api_key(params).await
1991    }
1992
1993    /// Fetches the sub-account list (up to 1000 rows, non-paginated).
1994    ///
1995    /// # Errors
1996    ///
1997    /// Returns an error if:
1998    /// - The request fails.
1999    /// - The response cannot be parsed.
2000    ///
2001    /// # References
2002    ///
2003    /// - <https://bybit-exchange.github.io/docs/v5/user/subuid-list>
2004    pub async fn get_sub_members(&self) -> Result<BybitSubMembersResponse, BybitHttpError> {
2005        self.inner.get_sub_members().await
2006    }
2007
2008    /// Fetches a cursor-paginated sub-account list.
2009    ///
2010    /// # Errors
2011    ///
2012    /// Returns an error if:
2013    /// - The request fails.
2014    /// - The response cannot be parsed.
2015    ///
2016    /// # References
2017    ///
2018    /// - <https://bybit-exchange.github.io/docs/v5/user/page-subuid>
2019    pub async fn get_sub_members_paged(
2020        &self,
2021        params: &BybitSubMembersPageParams,
2022    ) -> Result<BybitSubMembersPagedResponse, BybitHttpError> {
2023        self.inner.get_sub_members_paged(params).await
2024    }
2025
2026    /// Fetches fund-custodial sub-accounts.
2027    ///
2028    /// # Errors
2029    ///
2030    /// Returns an error if:
2031    /// - The request fails.
2032    /// - The response cannot be parsed.
2033    ///
2034    /// # References
2035    ///
2036    /// - <https://bybit-exchange.github.io/docs/v5/user/fund-subuid-list>
2037    pub async fn get_escrow_sub_members(
2038        &self,
2039        params: &BybitSubMembersPageParams,
2040    ) -> Result<BybitEscrowSubMembersResponse, BybitHttpError> {
2041        self.inner.get_escrow_sub_members(params).await
2042    }
2043
2044    /// Fetches all API keys belonging to a given sub-account.
2045    ///
2046    /// # Errors
2047    ///
2048    /// Returns an error if:
2049    /// - The request fails.
2050    /// - The response cannot be parsed.
2051    ///
2052    /// # References
2053    ///
2054    /// - <https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys>
2055    pub async fn get_sub_api_keys(
2056        &self,
2057        params: &BybitSubApiKeysParams,
2058    ) -> Result<BybitSubApiKeysResponse, BybitHttpError> {
2059        self.inner.get_sub_api_keys(params).await
2060    }
2061
2062    /// Fetches position information (requires authentication).
2063    ///
2064    /// # Errors
2065    ///
2066    /// Returns an error if:
2067    /// - Credentials are missing.
2068    /// - The request fails.
2069    /// - The API returns an error.
2070    ///
2071    /// # References
2072    ///
2073    /// - <https://bybit-exchange.github.io/docs/v5/position>
2074    pub async fn get_positions(
2075        &self,
2076        params: &BybitPositionListParams,
2077    ) -> Result<BybitPositionListResponse, BybitHttpError> {
2078        self.inner.get_positions(params).await
2079    }
2080
2081    /// Fetches fee rate (requires authentication).
2082    ///
2083    /// # Errors
2084    ///
2085    /// Returns an error if:
2086    /// - Credentials are missing.
2087    /// - The request fails.
2088    /// - The API returns an error.
2089    ///
2090    /// # References
2091    ///
2092    /// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
2093    pub async fn get_fee_rate(
2094        &self,
2095        params: &BybitFeeRateParams,
2096    ) -> Result<BybitFeeRateResponse, BybitHttpError> {
2097        self.inner.get_fee_rate(params).await
2098    }
2099
2100    /// Sets margin mode (requires authentication).
2101    ///
2102    /// # Errors
2103    ///
2104    /// Returns an error if:
2105    /// - Credentials are missing.
2106    /// - The request fails.
2107    /// - The API returns an error.
2108    ///
2109    /// # References
2110    ///
2111    /// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
2112    pub async fn set_margin_mode(
2113        &self,
2114        margin_mode: BybitMarginMode,
2115    ) -> Result<BybitSetMarginModeResponse, BybitHttpError> {
2116        self.inner.set_margin_mode(margin_mode).await
2117    }
2118
2119    /// Sets leverage for a symbol (requires authentication).
2120    ///
2121    /// # Errors
2122    ///
2123    /// Returns an error if:
2124    /// - Credentials are missing.
2125    /// - The request fails.
2126    /// - The API returns an error.
2127    ///
2128    /// # References
2129    ///
2130    /// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
2131    pub async fn set_leverage(
2132        &self,
2133        product_type: BybitProductType,
2134        symbol: &str,
2135        buy_leverage: &str,
2136        sell_leverage: &str,
2137    ) -> Result<BybitSetLeverageResponse, BybitHttpError> {
2138        self.inner
2139            .set_leverage(product_type, symbol, buy_leverage, sell_leverage)
2140            .await
2141    }
2142
2143    /// Switches position mode (requires authentication).
2144    ///
2145    /// # Errors
2146    ///
2147    /// Returns an error if:
2148    /// - Credentials are missing.
2149    /// - The request fails.
2150    /// - The API returns an error.
2151    ///
2152    /// # References
2153    ///
2154    /// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
2155    pub async fn switch_mode(
2156        &self,
2157        product_type: BybitProductType,
2158        mode: BybitPositionMode,
2159        symbol: Option<String>,
2160        coin: Option<String>,
2161    ) -> Result<BybitSwitchModeResponse, BybitHttpError> {
2162        self.inner
2163            .switch_mode(product_type, mode, symbol, coin)
2164            .await
2165    }
2166
2167    /// Sets trading stop parameters including trailing stops (requires authentication).
2168    ///
2169    /// # Errors
2170    ///
2171    /// Returns an error if:
2172    /// - Credentials are missing.
2173    /// - The request fails.
2174    /// - The API returns an error.
2175    ///
2176    /// # References
2177    ///
2178    /// - <https://bybit-exchange.github.io/docs/v5/position/trading-stop>
2179    pub async fn set_trading_stop(
2180        &self,
2181        params: &BybitSetTradingStopParams,
2182    ) -> Result<BybitSetTradingStopResponse, BybitHttpError> {
2183        self.inner.set_trading_stop(params).await
2184    }
2185
2186    /// Get the outstanding spot borrow amount for a specific coin.
2187    ///
2188    /// Returns zero if no borrow exists.
2189    ///
2190    /// # Parameters
2191    ///
2192    /// - `coin`: The coin to check (e.g., "BTC", "ETH")
2193    ///
2194    /// # Errors
2195    ///
2196    /// Returns an error if:
2197    /// - Credentials are missing.
2198    /// - The request fails.
2199    /// - The coin is not found in the wallet.
2200    pub async fn get_spot_borrow_amount(&self, coin: &str) -> anyhow::Result<Decimal> {
2201        let params = BybitWalletBalanceParams {
2202            account_type: BybitAccountType::Unified,
2203            coin: Some(coin.to_string()),
2204        };
2205
2206        let response = self.inner.get_wallet_balance(&params).await?;
2207
2208        let borrow_amount = response
2209            .result
2210            .list
2211            .first()
2212            .and_then(|wallet| wallet.coin.iter().find(|c| c.coin.as_str() == coin))
2213            .map_or(Decimal::ZERO, |balance| balance.spot_borrow);
2214
2215        Ok(borrow_amount)
2216    }
2217
2218    /// Borrows coins for spot margin trading.
2219    ///
2220    /// This should be called before opening short spot positions.
2221    ///
2222    /// # Parameters
2223    ///
2224    /// - `coin`: The coin to repay (e.g., "BTC", "ETH")
2225    /// - `amount`: Optional amount to borrow. If None, repays all outstanding borrows.
2226    ///
2227    /// # Errors
2228    ///
2229    /// Returns an error if:
2230    /// - Credentials are missing.
2231    /// - The request fails.
2232    /// - Insufficient collateral for the borrow.
2233    pub async fn borrow_spot(
2234        &self,
2235        coin: &str,
2236        amount: Quantity,
2237    ) -> anyhow::Result<BybitBorrowResponse> {
2238        let amount_str = amount.to_string();
2239        self.inner
2240            .borrow(coin, &amount_str)
2241            .await
2242            .map_err(|e| anyhow::anyhow!("Failed to borrow {amount} {coin}: {e}"))
2243    }
2244
2245    /// Repays spot borrows for a specific coin.
2246    ///
2247    /// This should be called after closing short spot positions to avoid accruing interest.
2248    ///
2249    /// # Parameters
2250    ///
2251    /// - `coin`: The coin to repay (e.g., "BTC", "ETH")
2252    /// - `amount`: Optional amount to repay. If None, repays all outstanding borrows.
2253    ///
2254    /// # Errors
2255    ///
2256    /// Returns an error if:
2257    /// - Credentials are missing.
2258    /// - The request fails.
2259    /// - Called between 04:00-05:30 UTC (interest calculation window).
2260    /// - Insufficient spot balance for repayment.
2261    pub async fn repay_spot_borrow(
2262        &self,
2263        coin: &str,
2264        amount: Option<Quantity>,
2265    ) -> anyhow::Result<BybitNoConvertRepayResponse> {
2266        let amount_str = amount.as_ref().map(|q| q.to_string());
2267        self.inner
2268            .no_convert_repay(coin, amount_str.as_deref())
2269            .await
2270            .map_err(|e| anyhow::anyhow!("Failed to repay spot borrow for {coin}: {e}"))
2271    }
2272
2273    /// Generate SPOT position reports from wallet balances.
2274    ///
2275    /// # Errors
2276    ///
2277    /// Returns an error if:
2278    /// - The wallet balance request fails.
2279    /// - Parsing fails.
2280    async fn generate_spot_position_reports_from_wallet(
2281        &self,
2282        account_id: AccountId,
2283        instrument_id: Option<InstrumentId>,
2284    ) -> anyhow::Result<Vec<PositionStatusReport>> {
2285        let params = BybitWalletBalanceParams {
2286            account_type: BybitAccountType::Unified,
2287            coin: None,
2288        };
2289
2290        let response = self.inner.get_wallet_balance(&params).await?;
2291        let ts_init = self.generate_ts_init();
2292
2293        let mut wallet_by_coin: HashMap<Ustr, Decimal> = HashMap::new();
2294
2295        for wallet in &response.result.list {
2296            for coin_balance in &wallet.coin {
2297                let balance = coin_balance.wallet_balance - coin_balance.spot_borrow;
2298                *wallet_by_coin
2299                    .entry(coin_balance.coin)
2300                    .or_insert(Decimal::ZERO) += balance;
2301            }
2302        }
2303
2304        let mut reports = Vec::new();
2305
2306        if let Some(instrument_id) = instrument_id {
2307            if let Some(instrument) = self
2308                .instruments_cache
2309                .get_cloned(&instrument_id.symbol.inner())
2310            {
2311                let base_currency = instrument
2312                    .base_currency()
2313                    .expect("SPOT instrument should have base currency");
2314                let coin = base_currency.code;
2315                let wallet_balance = wallet_by_coin.get(&coin).copied().unwrap_or(Decimal::ZERO);
2316
2317                let side = if wallet_balance > Decimal::ZERO {
2318                    PositionSideSpecified::Long
2319                } else if wallet_balance < Decimal::ZERO {
2320                    PositionSideSpecified::Short
2321                } else {
2322                    PositionSideSpecified::Flat
2323                };
2324
2325                let abs_balance = wallet_balance.abs();
2326                let quantity = Quantity::from_decimal_dp(abs_balance, instrument.size_precision())?;
2327
2328                let report = PositionStatusReport::new(
2329                    account_id,
2330                    instrument_id,
2331                    side,
2332                    quantity,
2333                    ts_init,
2334                    ts_init,
2335                    None,
2336                    None,
2337                    None,
2338                );
2339
2340                reports.push(report);
2341            }
2342        } else {
2343            // Generate reports for all SPOT instruments with non-zero balance
2344            let instruments_guard = self.instruments_cache.load();
2345            for (symbol, instrument) in instruments_guard.iter() {
2346                // Only consider SPOT instruments
2347                if !symbol.as_str().ends_with("-SPOT") {
2348                    continue;
2349                }
2350
2351                let base_currency = match instrument.base_currency() {
2352                    Some(currency) => currency,
2353                    None => continue,
2354                };
2355
2356                let coin = base_currency.code;
2357                let wallet_balance = wallet_by_coin.get(&coin).copied().unwrap_or(Decimal::ZERO);
2358
2359                if wallet_balance.is_zero() {
2360                    continue;
2361                }
2362
2363                let side = if wallet_balance > Decimal::ZERO {
2364                    PositionSideSpecified::Long
2365                } else if wallet_balance < Decimal::ZERO {
2366                    PositionSideSpecified::Short
2367                } else {
2368                    PositionSideSpecified::Flat
2369                };
2370
2371                let abs_balance = wallet_balance.abs();
2372                let quantity = Quantity::from_decimal_dp(abs_balance, instrument.size_precision())?;
2373
2374                if quantity.is_zero() {
2375                    continue;
2376                }
2377
2378                let report = PositionStatusReport::new(
2379                    account_id,
2380                    instrument.id(),
2381                    side,
2382                    quantity,
2383                    ts_init,
2384                    ts_init,
2385                    None,
2386                    None,
2387                    None,
2388                );
2389
2390                reports.push(report);
2391            }
2392        }
2393
2394        Ok(reports)
2395    }
2396
2397    /// Submit a new order.
2398    ///
2399    /// # Errors
2400    ///
2401    /// Returns an error if:
2402    /// - Credentials are missing.
2403    /// - The request fails.
2404    /// - Order validation fails.
2405    /// - The order is rejected.
2406    /// - The API returns an error.
2407    #[expect(clippy::too_many_arguments)]
2408    pub async fn submit_order(
2409        &self,
2410        account_id: AccountId,
2411        product_type: BybitProductType,
2412        instrument_id: InstrumentId,
2413        client_order_id: ClientOrderId,
2414        order_side: OrderSide,
2415        order_type: OrderType,
2416        quantity: Quantity,
2417        time_in_force: Option<TimeInForce>,
2418        price: Option<Price>,
2419        trigger_price: Option<Price>,
2420        post_only: Option<bool>,
2421        reduce_only: bool,
2422        is_quote_quantity: bool,
2423        is_leverage: bool,
2424        position_idx: Option<BybitPositionIdx>,
2425    ) -> anyhow::Result<OrderStatusReport> {
2426        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2427        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2428
2429        let bybit_side = match order_side {
2430            OrderSide::Buy => BybitOrderSide::Buy,
2431            OrderSide::Sell => BybitOrderSide::Sell,
2432            _ => anyhow::bail!("Invalid order side: {order_side:?}"),
2433        };
2434
2435        // For stop/conditional orders, Bybit uses Market/Limit with trigger parameters
2436        let (bybit_order_type, is_stop_order) = match order_type {
2437            OrderType::Market => (BybitOrderType::Market, false),
2438            OrderType::Limit => (BybitOrderType::Limit, false),
2439            OrderType::StopMarket | OrderType::MarketIfTouched => (BybitOrderType::Market, true),
2440            OrderType::StopLimit | OrderType::LimitIfTouched => (BybitOrderType::Limit, true),
2441            _ => anyhow::bail!("Unsupported order type: {order_type:?}"),
2442        };
2443
2444        let bybit_tif = map_time_in_force(bybit_order_type, time_in_force, post_only)
2445            .map_err(|tif| anyhow::anyhow!("Unsupported time in force: {tif:?}"))?;
2446        let market_unit = spot_market_unit(product_type, bybit_order_type, is_quote_quantity);
2447        let trigger_dir = trigger_direction(order_type, order_side, is_stop_order);
2448
2449        let mut order_entry = BybitBatchPlaceOrderEntryBuilder::default();
2450        order_entry.symbol(bybit_symbol.raw_symbol().to_string());
2451        order_entry.side(bybit_side);
2452        order_entry.order_type(bybit_order_type);
2453        order_entry.qty(quantity.to_string());
2454        order_entry.time_in_force(bybit_tif);
2455        order_entry.order_link_id(client_order_id.to_string());
2456        order_entry.market_unit(market_unit);
2457        order_entry.trigger_direction(trigger_dir);
2458
2459        if let Some(price) = price {
2460            order_entry.price(Some(price.to_string()));
2461        }
2462
2463        if let Some(trigger_price) = trigger_price {
2464            order_entry.trigger_price(Some(trigger_price.to_string()));
2465        }
2466
2467        if reduce_only {
2468            order_entry.reduce_only(Some(true));
2469        }
2470
2471        order_entry.is_leverage(spot_leverage(product_type, is_leverage));
2472
2473        if let Some(idx) = position_idx {
2474            order_entry.position_idx(Some(idx));
2475        }
2476
2477        let order_entry = order_entry.build().build_anyhow()?;
2478
2479        let mut params = BybitPlaceOrderParamsBuilder::default();
2480        params.category(product_type);
2481        params.order(order_entry);
2482
2483        let params = params.build().build_anyhow()?;
2484
2485        let body = serde_json::to_value(&params)?;
2486        let response = self.inner.place_order(&body).await?;
2487
2488        let order_id = response
2489            .result
2490            .order_id
2491            .ok_or_else(|| anyhow::anyhow!("No order_id in response"))?;
2492
2493        let order = self
2494            .query_order_by_id(
2495                product_type,
2496                order_id.as_str(),
2497                BYBIT_ORDER_REALTIME,
2498                "after submission",
2499            )
2500            .await?;
2501
2502        // Only bail on rejection if there are no fills
2503        // If the order has fills (cum_exec_qty > 0), let the parser remap Rejected -> Canceled
2504        if order.order_status == crate::common::enums::BybitOrderStatus::Rejected
2505            && (order.cum_exec_qty.as_str() == "0" || order.cum_exec_qty.is_empty())
2506        {
2507            anyhow::bail!("Order rejected: {}", order.reject_reason);
2508        }
2509
2510        let ts_init = self.generate_ts_init();
2511
2512        parse_order_status_report(&order, &instrument, account_id, ts_init)
2513    }
2514
2515    /// Cancel an order.
2516    ///
2517    /// # Errors
2518    ///
2519    /// Returns an error if:
2520    /// - Credentials are missing.
2521    /// - The request fails.
2522    /// - The order doesn't exist.
2523    /// - The API returns an error.
2524    pub async fn cancel_order(
2525        &self,
2526        account_id: AccountId,
2527        product_type: BybitProductType,
2528        instrument_id: InstrumentId,
2529        client_order_id: Option<ClientOrderId>,
2530        venue_order_id: Option<VenueOrderId>,
2531    ) -> anyhow::Result<OrderStatusReport> {
2532        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2533        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2534
2535        let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
2536        cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
2537
2538        if let Some(venue_order_id) = venue_order_id {
2539            cancel_entry.order_id(venue_order_id.to_string());
2540        } else if let Some(client_order_id) = client_order_id {
2541            cancel_entry.order_link_id(client_order_id.to_string());
2542        } else {
2543            anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2544        }
2545
2546        let cancel_entry = cancel_entry.build().build_anyhow()?;
2547
2548        let mut params = BybitCancelOrderParamsBuilder::default();
2549        params.category(product_type);
2550        params.order(cancel_entry);
2551
2552        let params = params.build().build_anyhow()?;
2553        let body = serde_json::to_vec(&params)?;
2554
2555        let response: BybitPlaceOrderResponse = self
2556            .inner
2557            .send_request::<_, ()>(Method::POST, "/v5/order/cancel", None, Some(body), true)
2558            .await?;
2559
2560        let order_id = response
2561            .result
2562            .order_id
2563            .ok_or_else(|| anyhow::anyhow!("No order_id in cancel response"))?;
2564
2565        let order = self
2566            .query_order_by_id(
2567                product_type,
2568                order_id.as_str(),
2569                BYBIT_ORDER_HISTORY,
2570                "after cancellation",
2571            )
2572            .await?;
2573
2574        let ts_init = self.generate_ts_init();
2575
2576        parse_order_status_report(&order, &instrument, account_id, ts_init)
2577    }
2578
2579    /// Batch cancel multiple orders.
2580    ///
2581    /// # Errors
2582    ///
2583    /// Returns an error if:
2584    /// - Credentials are missing.
2585    /// - The request fails.
2586    /// - Any of the orders don't exist.
2587    /// - The API returns an error.
2588    pub async fn batch_cancel_orders(
2589        &self,
2590        account_id: AccountId,
2591        product_type: BybitProductType,
2592        instrument_ids: Vec<InstrumentId>,
2593        client_order_ids: Vec<Option<ClientOrderId>>,
2594        venue_order_ids: Vec<Option<VenueOrderId>>,
2595    ) -> anyhow::Result<Vec<OrderStatusReport>> {
2596        if instrument_ids.len() != client_order_ids.len()
2597            || instrument_ids.len() != venue_order_ids.len()
2598        {
2599            anyhow::bail!(
2600                "instrument_ids, client_order_ids, and venue_order_ids must have the same length"
2601            );
2602        }
2603
2604        if instrument_ids.is_empty() {
2605            return Ok(Vec::new());
2606        }
2607
2608        if instrument_ids.len() > 20 {
2609            anyhow::bail!("Batch cancel limit is 20 orders per request");
2610        }
2611
2612        let mut cancel_entries = Vec::new();
2613
2614        for ((instrument_id, client_order_id), venue_order_id) in instrument_ids
2615            .iter()
2616            .zip(client_order_ids.iter())
2617            .zip(venue_order_ids.iter())
2618        {
2619            let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2620            let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
2621            cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
2622
2623            if let Some(venue_order_id) = venue_order_id {
2624                cancel_entry.order_id(venue_order_id.to_string());
2625            } else if let Some(client_order_id) = client_order_id {
2626                cancel_entry.order_link_id(client_order_id.to_string());
2627            } else {
2628                anyhow::bail!(
2629                    "Either client_order_id or venue_order_id must be provided for each order"
2630                );
2631            }
2632
2633            cancel_entries.push(cancel_entry.build().build_anyhow()?);
2634        }
2635
2636        let mut params = BybitBatchCancelOrderParamsBuilder::default();
2637        params.category(product_type);
2638        params.request(cancel_entries);
2639
2640        let params = params.build().build_anyhow()?;
2641        let body = serde_json::to_vec(&params)?;
2642
2643        let _response: BybitPlaceOrderResponse = self
2644            .inner
2645            .send_request::<_, ()>(
2646                Method::POST,
2647                "/v5/order/cancel-batch",
2648                None,
2649                Some(body),
2650                true,
2651            )
2652            .await?;
2653
2654        // Query each order to get full details after cancellation
2655        let mut reports = Vec::new();
2656
2657        for (instrument_id, (client_order_id, venue_order_id)) in instrument_ids
2658            .iter()
2659            .zip(client_order_ids.iter().zip(venue_order_ids.iter()))
2660        {
2661            let Ok(instrument) = self.instrument_from_cache(&instrument_id.symbol) else {
2662                log::debug!(
2663                    "Skipping cancelled order report for instrument not in cache: symbol={}",
2664                    instrument_id.symbol
2665                );
2666                continue;
2667            };
2668
2669            let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2670
2671            let mut query_params = BybitOpenOrdersParamsBuilder::default();
2672            query_params.category(product_type);
2673            query_params.symbol(bybit_symbol.raw_symbol().to_string());
2674
2675            if let Some(venue_order_id) = venue_order_id {
2676                query_params.order_id(venue_order_id.to_string());
2677            } else if let Some(client_order_id) = client_order_id {
2678                query_params.order_link_id(client_order_id.to_string());
2679            }
2680
2681            let query_params = query_params.build().build_anyhow()?;
2682            let order_response: BybitOrderHistoryResponse = self
2683                .inner
2684                .send_request(
2685                    Method::GET,
2686                    BYBIT_ORDER_HISTORY,
2687                    Some(&query_params),
2688                    None,
2689                    true,
2690                )
2691                .await?;
2692
2693            if let Some(order) = order_response.result.list.into_iter().next() {
2694                let ts_init = self.generate_ts_init();
2695                let report = parse_order_status_report(&order, &instrument, account_id, ts_init)?;
2696                reports.push(report);
2697            }
2698        }
2699
2700        Ok(reports)
2701    }
2702
2703    /// Cancel all orders for an instrument.
2704    ///
2705    /// # Errors
2706    ///
2707    /// Returns an error if:
2708    /// - Credentials are missing.
2709    /// - The request fails.
2710    /// - The API returns an error.
2711    pub async fn cancel_all_orders(
2712        &self,
2713        account_id: AccountId,
2714        product_type: BybitProductType,
2715        instrument_id: InstrumentId,
2716    ) -> anyhow::Result<Vec<OrderStatusReport>> {
2717        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2718        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2719
2720        let mut params = BybitCancelAllOrdersParamsBuilder::default();
2721        params.category(product_type);
2722        params.symbol(bybit_symbol.raw_symbol().to_string());
2723
2724        let params = params.build().build_anyhow()?;
2725        let body = serde_json::to_vec(&params)?;
2726
2727        let _response: crate::common::models::BybitListResponse<serde_json::Value> = self
2728            .inner
2729            .send_request::<_, ()>(Method::POST, "/v5/order/cancel-all", None, Some(body), true)
2730            .await?;
2731
2732        // Query the order history to get all canceled orders
2733        let mut query_params = BybitOrderHistoryParamsBuilder::default();
2734        query_params.category(product_type);
2735        query_params.symbol(bybit_symbol.raw_symbol().to_string());
2736        query_params.limit(50u32);
2737
2738        let query_params = query_params.build().build_anyhow()?;
2739        let order_response: BybitOrderHistoryResponse = self
2740            .inner
2741            .send_request(
2742                Method::GET,
2743                BYBIT_ORDER_HISTORY,
2744                Some(&query_params),
2745                None,
2746                true,
2747            )
2748            .await?;
2749
2750        let ts_init = self.generate_ts_init();
2751
2752        let mut reports = Vec::new();
2753
2754        for order in order_response.result.list {
2755            if let Ok(report) = parse_order_status_report(&order, &instrument, account_id, ts_init)
2756            {
2757                reports.push(report);
2758            }
2759        }
2760
2761        Ok(reports)
2762    }
2763
2764    /// Modify an existing order.
2765    ///
2766    /// # Errors
2767    ///
2768    /// Returns an error if:
2769    /// - Credentials are missing.
2770    /// - The request fails.
2771    /// - The order doesn't exist.
2772    /// - The order is already closed.
2773    /// - The API returns an error.
2774    #[expect(clippy::too_many_arguments)]
2775    pub async fn modify_order(
2776        &self,
2777        account_id: AccountId,
2778        product_type: BybitProductType,
2779        instrument_id: InstrumentId,
2780        client_order_id: Option<ClientOrderId>,
2781        venue_order_id: Option<VenueOrderId>,
2782        quantity: Option<Quantity>,
2783        price: Option<Price>,
2784    ) -> anyhow::Result<OrderStatusReport> {
2785        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2786        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2787
2788        let mut amend_entry = BybitBatchAmendOrderEntryBuilder::default();
2789        amend_entry.symbol(bybit_symbol.raw_symbol().to_string());
2790
2791        if let Some(venue_order_id) = venue_order_id {
2792            amend_entry.order_id(venue_order_id.to_string());
2793        } else if let Some(client_order_id) = client_order_id {
2794            amend_entry.order_link_id(client_order_id.to_string());
2795        } else {
2796            anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2797        }
2798
2799        if let Some(quantity) = quantity {
2800            amend_entry.qty(Some(quantity.to_string()));
2801        }
2802
2803        if let Some(price) = price {
2804            amend_entry.price(Some(price.to_string()));
2805        }
2806
2807        let amend_entry = amend_entry.build().build_anyhow()?;
2808
2809        let mut params = BybitAmendOrderParamsBuilder::default();
2810        params.category(product_type);
2811        params.order(amend_entry);
2812
2813        let params = params.build().build_anyhow()?;
2814        let body = serde_json::to_vec(&params)?;
2815
2816        let response: BybitPlaceOrderResponse = self
2817            .inner
2818            .send_request::<_, ()>(Method::POST, "/v5/order/amend", None, Some(body), true)
2819            .await?;
2820
2821        let order_id = response
2822            .result
2823            .order_id
2824            .ok_or_else(|| anyhow::anyhow!("No order_id in amend response"))?;
2825
2826        let order = self
2827            .query_order_by_id(
2828                product_type,
2829                order_id.as_str(),
2830                BYBIT_ORDER_REALTIME,
2831                "after amendment",
2832            )
2833            .await?;
2834
2835        let ts_init = self.generate_ts_init();
2836
2837        parse_order_status_report(&order, &instrument, account_id, ts_init)
2838    }
2839
2840    /// Query a single order by client order ID or venue order ID.
2841    ///
2842    /// # Errors
2843    ///
2844    /// Returns an error if:
2845    /// - Credentials are missing.
2846    /// - The request fails.
2847    /// - The API returns an error.
2848    pub async fn query_order(
2849        &self,
2850        account_id: AccountId,
2851        product_type: BybitProductType,
2852        instrument_id: InstrumentId,
2853        client_order_id: Option<ClientOrderId>,
2854        venue_order_id: Option<VenueOrderId>,
2855    ) -> anyhow::Result<Option<OrderStatusReport>> {
2856        log::debug!(
2857            "query_order: instrument_id={instrument_id}, client_order_id={client_order_id:?}, venue_order_id={venue_order_id:?}"
2858        );
2859
2860        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2861
2862        let mut params = BybitOpenOrdersParamsBuilder::default();
2863        params.category(product_type);
2864        // Use the raw Bybit symbol (e.g., "ETHUSDT") not the full instrument symbol
2865        params.symbol(bybit_symbol.raw_symbol().to_string());
2866
2867        if let Some(venue_order_id) = venue_order_id {
2868            params.order_id(venue_order_id.to_string());
2869        } else if let Some(client_order_id) = client_order_id {
2870            params.order_link_id(client_order_id.to_string());
2871        } else {
2872            anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2873        }
2874
2875        let params = params.build().build_anyhow()?;
2876        let mut response: BybitOpenOrdersResponse = self
2877            .inner
2878            .send_request(Method::GET, BYBIT_ORDER_REALTIME, Some(&params), None, true)
2879            .await?;
2880
2881        // Options do not support the StopOrder filter
2882        if response.result.list.is_empty() && product_type != BybitProductType::Option {
2883            log::debug!("Order not found in open orders, trying with StopOrder filter");
2884
2885            let mut stop_params = BybitOpenOrdersParamsBuilder::default();
2886            stop_params.category(product_type);
2887            stop_params.symbol(bybit_symbol.raw_symbol().to_string());
2888            stop_params.order_filter(BybitOrderFilter::StopOrder);
2889
2890            if let Some(venue_order_id) = venue_order_id {
2891                stop_params.order_id(venue_order_id.to_string());
2892            } else if let Some(client_order_id) = client_order_id {
2893                stop_params.order_link_id(client_order_id.to_string());
2894            }
2895
2896            let stop_params = stop_params.build().build_anyhow()?;
2897            response = self
2898                .inner
2899                .send_request(
2900                    Method::GET,
2901                    BYBIT_ORDER_REALTIME,
2902                    Some(&stop_params),
2903                    None,
2904                    true,
2905                )
2906                .await?;
2907        }
2908
2909        // If not found in open orders, check order history
2910        if response.result.list.is_empty() {
2911            log::debug!("Order not found in open orders, checking order history");
2912
2913            let mut history_params = BybitOrderHistoryParamsBuilder::default();
2914            history_params.category(product_type);
2915            history_params.symbol(bybit_symbol.raw_symbol().to_string());
2916
2917            if let Some(venue_order_id) = venue_order_id {
2918                history_params.order_id(venue_order_id.to_string());
2919            } else if let Some(client_order_id) = client_order_id {
2920                history_params.order_link_id(client_order_id.to_string());
2921            }
2922
2923            let history_params = history_params.build().build_anyhow()?;
2924
2925            let mut history_response: BybitOrderHistoryResponse = self
2926                .inner
2927                .send_request(
2928                    Method::GET,
2929                    BYBIT_ORDER_HISTORY,
2930                    Some(&history_params),
2931                    None,
2932                    true,
2933                )
2934                .await?;
2935
2936            if history_response.result.list.is_empty() && product_type == BybitProductType::Option {
2937                log::debug!("Option order not found in order history");
2938                return Ok(None);
2939            }
2940
2941            // Options do not support the StopOrder filter
2942            if history_response.result.list.is_empty() && product_type != BybitProductType::Option {
2943                log::debug!("Order not found in order history, trying with StopOrder filter");
2944
2945                let mut stop_history_params = BybitOrderHistoryParamsBuilder::default();
2946                stop_history_params.category(product_type);
2947                stop_history_params.symbol(bybit_symbol.raw_symbol().to_string());
2948                stop_history_params.order_filter(BybitOrderFilter::StopOrder);
2949
2950                if let Some(venue_order_id) = venue_order_id {
2951                    stop_history_params.order_id(venue_order_id.to_string());
2952                } else if let Some(client_order_id) = client_order_id {
2953                    stop_history_params.order_link_id(client_order_id.to_string());
2954                }
2955
2956                let stop_history_params = stop_history_params
2957                    .build()
2958                    .map_err(|e| anyhow::anyhow!(e))?;
2959
2960                history_response = self
2961                    .inner
2962                    .send_request(
2963                        Method::GET,
2964                        BYBIT_ORDER_HISTORY,
2965                        Some(&stop_history_params),
2966                        None,
2967                        true,
2968                    )
2969                    .await?;
2970
2971                if history_response.result.list.is_empty() {
2972                    log::debug!("Order not found in order history with StopOrder filter either");
2973                    return Ok(None);
2974                }
2975            }
2976
2977            // Move the order from history response to the response list
2978            response.result.list = history_response.result.list;
2979        }
2980
2981        let order = &response.result.list[0];
2982        let ts_init = self.generate_ts_init();
2983
2984        log::debug!(
2985            "Query order response: symbol={}, order_id={}, order_link_id={}",
2986            order.symbol.as_str(),
2987            order.order_id.as_str(),
2988            order.order_link_id.as_str()
2989        );
2990
2991        let instrument = self
2992            .instrument_from_cache(&instrument_id.symbol)
2993            .map_err(|e| {
2994                log::error!(
2995                    "Instrument cache miss for symbol '{}': {}",
2996                    instrument_id.symbol.as_str(),
2997                    e
2998                );
2999                anyhow::anyhow!(
3000                    "Failed to query order {}: {}",
3001                    client_order_id
3002                        .as_ref()
3003                        .map(|id| id.to_string())
3004                        .or_else(|| venue_order_id.as_ref().map(|id| id.to_string()))
3005                        .unwrap_or_else(|| "unknown".to_string()),
3006                    e
3007                )
3008            })?;
3009
3010        log::debug!("Retrieved instrument from cache: id={}", instrument.id());
3011
3012        let report =
3013            parse_order_status_report(order, &instrument, account_id, ts_init).map_err(|e| {
3014                log::error!(
3015                    "Failed to parse order status report for {}: {}",
3016                    order.order_link_id.as_str(),
3017                    e
3018                );
3019                e
3020            })?;
3021
3022        log::debug!(
3023            "Successfully created OrderStatusReport for {}",
3024            order.order_link_id.as_str()
3025        );
3026
3027        Ok(Some(report))
3028    }
3029
3030    async fn fetch_fee_map(
3031        &self,
3032        product_type: BybitProductType,
3033        base_coin: Option<Ustr>,
3034    ) -> anyhow::Result<AHashMap<Ustr, BybitFeeRate>> {
3035        let mut fee_params = BybitFeeRateParamsBuilder::default();
3036        fee_params.category(product_type);
3037        if let Some(bc) = base_coin {
3038            fee_params.base_coin(bc.to_string());
3039        }
3040        let Ok(params) = fee_params.build() else {
3041            return Ok(AHashMap::new());
3042        };
3043
3044        match self.inner.get_fee_rate(&params).await {
3045            Ok(response) => Ok(response
3046                .result
3047                .list
3048                .into_iter()
3049                .map(|f| (f.symbol, f))
3050                .collect()),
3051            Err(BybitHttpError::MissingCredentials) => {
3052                log::warn!("Missing credentials for fee rates, using defaults");
3053                Ok(AHashMap::new())
3054            }
3055            Err(BybitHttpError::BybitError {
3056                error_code,
3057                ref message,
3058            }) => {
3059                log::warn!(
3060                    "{}",
3061                    self.fee_rate_rejection_warning(product_type, error_code, message)
3062                );
3063                Ok(AHashMap::new())
3064            }
3065            Err(e) => Err(e.into()),
3066        }
3067    }
3068
3069    async fn fetch_option_fee_map(
3070        &self,
3071        base_coin: Option<Ustr>,
3072    ) -> anyhow::Result<AHashMap<Ustr, BybitFeeRate>> {
3073        let mut fee_params = BybitFeeRateParamsBuilder::default();
3074        fee_params.category(BybitProductType::Option);
3075        if let Some(bc) = base_coin {
3076            fee_params.base_coin(bc.to_string());
3077        }
3078        let Ok(params) = fee_params.build() else {
3079            return Ok(AHashMap::new());
3080        };
3081
3082        match self.inner.get_fee_rate(&params).await {
3083            Ok(response) => Ok(response
3084                .result
3085                .list
3086                .into_iter()
3087                .filter_map(|f| f.base_coin.map(|bc| (bc, f)))
3088                .collect()),
3089            Err(BybitHttpError::MissingCredentials) => {
3090                log::warn!("Missing credentials for option fee rates, using defaults");
3091                Ok(AHashMap::new())
3092            }
3093            Err(BybitHttpError::BybitError {
3094                error_code,
3095                ref message,
3096            }) => {
3097                let error_detail = Self::format_bybit_error_detail(error_code, message);
3098                log::warn!(
3099                    "Option fee rate request rejected via /v5/account/fee-rate ({error_detail}), using defaults"
3100                );
3101                Ok(AHashMap::new())
3102            }
3103            Err(e) => {
3104                log::warn!("Option fee rate request failed ({e}), using defaults");
3105                Ok(AHashMap::new())
3106            }
3107        }
3108    }
3109
3110    fn fee_rate_rejection_warning(
3111        &self,
3112        product_type: BybitProductType,
3113        error_code: i32,
3114        message: &str,
3115    ) -> String {
3116        let product_type = product_type.as_ref().to_ascii_lowercase();
3117        let error_detail = Self::format_bybit_error_detail(error_code, message);
3118
3119        if self
3120            .base_url()
3121            .starts_with(bybit_http_base_url(BybitEnvironment::Demo))
3122            && matches!(product_type.as_str(), "linear" | "inverse")
3123            && error_code == 10001
3124        {
3125            format!(
3126                "Bybit demo rejected the {product_type} fee rate request via \
3127                 /v5/account/fee-rate ({error_detail}); demo derivatives fee rates appear \
3128                 unsupported, using defaults"
3129            )
3130        } else {
3131            format!(
3132                "Fee rate request rejected for {product_type} instruments via \
3133                 /v5/account/fee-rate ({error_detail}), using defaults"
3134            )
3135        }
3136    }
3137
3138    fn format_bybit_error_detail(error_code: i32, message: &str) -> String {
3139        let message = message.trim();
3140        if message.is_empty() {
3141            format!("error {error_code}, no message")
3142        } else {
3143            format!("error {error_code}: {message}")
3144        }
3145    }
3146
3147    async fn paginate_instruments<D, F>(
3148        &self,
3149        product_type: BybitProductType,
3150        symbol: &Option<String>,
3151        base_coin: Option<Ustr>,
3152        mut parse: F,
3153    ) -> anyhow::Result<Vec<InstrumentAny>>
3154    where
3155        D: DeserializeOwned,
3156        BybitCursorListResponse<D>: BybitResponseCheck,
3157        F: FnMut(&D) -> Option<InstrumentAny>,
3158    {
3159        let mut instruments = Vec::new();
3160        let mut cursor: Option<String> = None;
3161        let mut prev_cursor: Option<String> = None;
3162
3163        loop {
3164            let params = BybitInstrumentsInfoParams {
3165                category: product_type,
3166                symbol: symbol.clone(),
3167                status: None,
3168                base_coin: base_coin.map(|u| u.to_string()),
3169                limit: Some(1000),
3170                cursor: cursor.clone(),
3171            };
3172
3173            let response: BybitCursorListResponse<D> = self.inner.get_instruments(&params).await?;
3174
3175            for definition in &response.result.list {
3176                if let Some(instrument) = parse(definition) {
3177                    instruments.push(instrument);
3178                }
3179            }
3180
3181            cursor = response.result.next_page_cursor;
3182            if cursor.as_ref().is_none_or(|c| c.is_empty()) || cursor == prev_cursor {
3183                break;
3184            }
3185            prev_cursor = cursor.clone();
3186        }
3187
3188        Ok(instruments)
3189    }
3190
3191    /// Fetches instrument info and returns the current status of each symbol.
3192    ///
3193    /// Paginates through the instruments endpoint collecting only
3194    /// `(InstrumentId, MarketStatusAction)` pairs. This avoids fee-rate
3195    /// fetching and full instrument parsing.
3196    ///
3197    /// # Errors
3198    ///
3199    /// Returns an error if the request fails.
3200    pub async fn request_instrument_statuses(
3201        &self,
3202        product_type: BybitProductType,
3203    ) -> anyhow::Result<AHashMap<InstrumentId, MarketStatusAction>> {
3204        let mut statuses = AHashMap::new();
3205        let mut cursor: Option<String> = None;
3206
3207        loop {
3208            let params = BybitInstrumentsInfoParams {
3209                category: product_type,
3210                symbol: None,
3211                status: None,
3212                base_coin: None,
3213                limit: Some(1000),
3214                cursor: cursor.clone(),
3215            };
3216
3217            match product_type {
3218                BybitProductType::Spot => {
3219                    let response: BybitCursorListResponse<BybitInstrumentSpot> =
3220                        self.inner.get_instruments(&params).await?;
3221
3222                    for def in &response.result.list {
3223                        let symbol = make_bybit_symbol(def.symbol, product_type);
3224                        let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3225                        statuses.insert(id, MarketStatusAction::from(def.status));
3226                    }
3227                    cursor = response.result.next_page_cursor;
3228                }
3229                BybitProductType::Linear => {
3230                    let response: BybitCursorListResponse<BybitInstrumentLinear> =
3231                        self.inner.get_instruments(&params).await?;
3232
3233                    for def in &response.result.list {
3234                        let symbol = make_bybit_symbol(def.symbol, product_type);
3235                        let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3236                        let status = MarketStatusAction::from(def.status);
3237                        if status == MarketStatusAction::Trading
3238                            && def.contract_type == BybitContractType::LinearPerpetual
3239                            && def.delivery_time != "0"
3240                        {
3241                            statuses.insert(id, MarketStatusAction::PreClose);
3242                        } else {
3243                            statuses.insert(id, status);
3244                        }
3245                    }
3246                    cursor = response.result.next_page_cursor;
3247                }
3248                BybitProductType::Inverse => {
3249                    let response: BybitCursorListResponse<BybitInstrumentInverse> =
3250                        self.inner.get_instruments(&params).await?;
3251
3252                    for def in &response.result.list {
3253                        let symbol = make_bybit_symbol(def.symbol, product_type);
3254                        let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3255                        let status = MarketStatusAction::from(def.status);
3256                        if status == MarketStatusAction::Trading
3257                            && def.contract_type == BybitContractType::InversePerpetual
3258                            && def.delivery_time != "0"
3259                        {
3260                            statuses.insert(id, MarketStatusAction::PreClose);
3261                        } else {
3262                            statuses.insert(id, status);
3263                        }
3264                    }
3265                    cursor = response.result.next_page_cursor;
3266                }
3267                BybitProductType::Option => {
3268                    let response: BybitCursorListResponse<BybitInstrumentOption> =
3269                        self.inner.get_instruments(&params).await?;
3270
3271                    for def in &response.result.list {
3272                        let symbol = make_bybit_symbol(def.symbol, product_type);
3273                        let id = InstrumentId::new(Symbol::from(symbol), *BYBIT_VENUE);
3274                        statuses.insert(id, MarketStatusAction::from(def.status));
3275                    }
3276                    cursor = response.result.next_page_cursor;
3277                }
3278            }
3279
3280            if cursor.as_ref().is_none_or(|c| c.is_empty()) {
3281                break;
3282            }
3283        }
3284
3285        Ok(statuses)
3286    }
3287
3288    /// Request instruments for a given product type.
3289    ///
3290    /// When `base_coin` is provided, the request is narrowed to that base coin.
3291    /// This is required for `Option`: Bybit's API returns only `BTC` options when
3292    /// `baseCoin` is omitted.
3293    ///
3294    /// # Errors
3295    ///
3296    /// Returns an error if the request fails or parsing fails.
3297    pub async fn request_instruments(
3298        &self,
3299        product_type: BybitProductType,
3300        symbol: Option<String>,
3301        base_coin: Option<Ustr>,
3302    ) -> anyhow::Result<Vec<InstrumentAny>> {
3303        let ts_init = self.generate_ts_init();
3304
3305        let default_fee_rate = |symbol: Ustr| BybitFeeRate {
3306            symbol,
3307            taker_fee_rate: "0.001".to_string(),
3308            maker_fee_rate: "0.001".to_string(),
3309            base_coin: None,
3310        };
3311
3312        let instruments = match product_type {
3313            BybitProductType::Spot => {
3314                let fee_map = self.fetch_fee_map(product_type, base_coin).await?;
3315                self.paginate_instruments::<BybitInstrumentSpot, _>(
3316                    product_type,
3317                    &symbol,
3318                    base_coin,
3319                    |def| {
3320                        let fee = fee_map
3321                            .get(&def.symbol)
3322                            .cloned()
3323                            .unwrap_or_else(|| default_fee_rate(def.symbol));
3324                        parse_spot_instrument(def, &fee, ts_init, ts_init).ok()
3325                    },
3326                )
3327                .await?
3328            }
3329            BybitProductType::Linear => {
3330                let fee_map = self.fetch_fee_map(product_type, base_coin).await?;
3331                self.paginate_instruments::<BybitInstrumentLinear, _>(
3332                    product_type,
3333                    &symbol,
3334                    base_coin,
3335                    |def| {
3336                        let fee = fee_map
3337                            .get(&def.symbol)
3338                            .cloned()
3339                            .unwrap_or_else(|| default_fee_rate(def.symbol));
3340                        parse_linear_instrument(def, &fee, ts_init, ts_init).ok()
3341                    },
3342                )
3343                .await?
3344            }
3345            BybitProductType::Inverse => {
3346                let fee_map = self.fetch_fee_map(product_type, base_coin).await?;
3347                self.paginate_instruments::<BybitInstrumentInverse, _>(
3348                    product_type,
3349                    &symbol,
3350                    base_coin,
3351                    |def| {
3352                        let fee = fee_map
3353                            .get(&def.symbol)
3354                            .cloned()
3355                            .unwrap_or_else(|| default_fee_rate(def.symbol));
3356                        parse_inverse_instrument(def, &fee, ts_init, ts_init).ok()
3357                    },
3358                )
3359                .await?
3360            }
3361            BybitProductType::Option => {
3362                let fee_map = self.fetch_option_fee_map(base_coin).await?;
3363                self.paginate_instruments::<BybitInstrumentOption, _>(
3364                    product_type,
3365                    &symbol,
3366                    base_coin,
3367                    |def| {
3368                        let fee = fee_map.get(&def.base_coin);
3369                        parse_option_instrument(def, fee, ts_init, ts_init).ok()
3370                    },
3371                )
3372                .await?
3373            }
3374        };
3375
3376        self.cache_instruments(&instruments);
3377
3378        Ok(instruments)
3379    }
3380
3381    /// Request ticker information for market data.
3382    ///
3383    /// Fetches ticker data from Bybit's `/v5/market/tickers` endpoint and returns
3384    /// a unified `BybitTickerData` structure compatible with all product types.
3385    ///
3386    /// # Errors
3387    ///
3388    /// Returns an error if the request fails or parsing fails.
3389    ///
3390    /// # References
3391    ///
3392    /// <https://bybit-exchange.github.io/docs/v5/market/tickers>
3393    pub async fn request_tickers(
3394        &self,
3395        params: &BybitTickersParams,
3396    ) -> anyhow::Result<Vec<BybitTickerData>> {
3397        use super::models::{
3398            BybitTickersLinearResponse, BybitTickersOptionResponse, BybitTickersSpotResponse,
3399        };
3400
3401        match params.category {
3402            BybitProductType::Spot => {
3403                let response: BybitTickersSpotResponse = self.inner.get_tickers(params).await?;
3404                Ok(response.result.list.into_iter().map(Into::into).collect())
3405            }
3406            BybitProductType::Linear | BybitProductType::Inverse => {
3407                let response: BybitTickersLinearResponse = self.inner.get_tickers(params).await?;
3408                Ok(response.result.list.into_iter().map(Into::into).collect())
3409            }
3410            BybitProductType::Option => {
3411                let response: BybitTickersOptionResponse = self.inner.get_tickers(params).await?;
3412                Ok(response.result.list.into_iter().map(Into::into).collect())
3413            }
3414        }
3415    }
3416
3417    /// Requests raw option tickers for a given base coin.
3418    ///
3419    /// Returns `Vec<BybitTickerOption>` with the raw fields including `underlying_price`.
3420    /// Used for fetching forward prices for option chain bootstrap.
3421    ///
3422    /// # Errors
3423    ///
3424    /// Returns an error if the request fails.
3425    pub async fn request_option_tickers_raw(
3426        &self,
3427        base_coin: &str,
3428    ) -> anyhow::Result<Vec<BybitTickerOption>> {
3429        let params = BybitTickersParams {
3430            category: BybitProductType::Option,
3431            symbol: None,
3432            base_coin: Some(base_coin.to_string()),
3433            exp_date: None,
3434        };
3435        let response: BybitTickersOptionResponse = self.inner.get_tickers(&params).await?;
3436        Ok(response.result.list)
3437    }
3438
3439    /// Request raw option tickers with custom params.
3440    ///
3441    /// This allows fetching a single instrument by setting `symbol` in the params,
3442    /// instead of fetching all options for a base coin.
3443    ///
3444    /// # Errors
3445    ///
3446    /// Returns an error if the request fails.
3447    pub async fn request_option_tickers_raw_with_params(
3448        &self,
3449        params: &BybitTickersParams,
3450    ) -> anyhow::Result<Vec<BybitTickerOption>> {
3451        let response: BybitTickersOptionResponse = self.inner.get_tickers(params).await?;
3452        Ok(response.result.list)
3453    }
3454
3455    /// Request recent trade tick history for a given symbol.
3456    ///
3457    /// Returns the most recent public trades from Bybit's `/v5/market/recent-trade` endpoint.
3458    /// This endpoint only provides recent trades (up to 1000 most recent), typically covering
3459    /// only the last few minutes for active markets.
3460    ///
3461    /// **Note**: For historical trade data with time ranges, use the klines endpoint instead.
3462    /// The Bybit public API does not support fetching historical trades by time range.
3463    ///
3464    /// # Errors
3465    ///
3466    /// Returns an error if:
3467    /// - The instrument is not found in cache.
3468    /// - The request fails.
3469    /// - Parsing fails.
3470    ///
3471    /// # References
3472    ///
3473    /// <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
3474    pub async fn request_trades(
3475        &self,
3476        product_type: BybitProductType,
3477        instrument_id: InstrumentId,
3478        limit: Option<u32>,
3479    ) -> anyhow::Result<Vec<TradeTick>> {
3480        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
3481        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3482
3483        let mut params_builder = BybitTradesParamsBuilder::default();
3484        params_builder.category(product_type);
3485        params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3486
3487        if let Some(limit_val) = limit {
3488            params_builder.limit(limit_val);
3489        }
3490
3491        let params = params_builder.build().build_anyhow()?;
3492        let response = self.inner.get_recent_trades(&params).await?;
3493
3494        let mut trades = Vec::new();
3495
3496        for trade in response.result.list {
3497            if let Ok(trade_tick) = parse_trade_tick(&trade, &instrument, None) {
3498                trades.push(trade_tick);
3499            }
3500        }
3501
3502        Ok(trades)
3503    }
3504
3505    /// Request funding rate history for a given symbol.
3506    ///
3507    /// # Errors
3508    ///
3509    /// Returns an error if:
3510    /// - The instrument is not found in cache.
3511    /// - The request fails.
3512    /// - Parsing fails.
3513    ///
3514    /// # References
3515    ///
3516    /// <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
3517    pub async fn request_funding_rates(
3518        &self,
3519        product_type: BybitProductType,
3520        instrument_id: InstrumentId,
3521        start: Option<DateTime<Utc>>,
3522        end: Option<DateTime<Utc>>,
3523        limit: Option<u32>,
3524    ) -> anyhow::Result<Vec<FundingRateUpdate>> {
3525        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
3526        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3527
3528        let start_ms = start.map(|dt| dt.timestamp_millis());
3529        let mut seen_timestamps: AHashSet<i64> = AHashSet::new();
3530
3531        let mut raw_funding_rates = Vec::new();
3532
3533        // Bybit requires endTime when startTime is provided
3534        let mut current_end_ms = match (start, end) {
3535            (Some(_), None) => Some(Utc::now().timestamp_millis()),
3536            _ => end.map(|dt| dt.timestamp_millis()),
3537        };
3538
3539        loop {
3540            let mut params_builder = BybitFundingParamsBuilder::default();
3541            params_builder.category(product_type);
3542            params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3543            params_builder.limit(limit.unwrap_or(200).clamp(0, 200)); // 200 is the maximum for the Bybit API
3544
3545            if let Some(start_val) = start_ms {
3546                params_builder.start_time(start_val);
3547            }
3548
3549            if let Some(end_val) = current_end_ms {
3550                params_builder.end_time(end_val);
3551            }
3552
3553            let params = params_builder.build().build_anyhow()?;
3554            let response = self.inner.get_funding_history(&params).await?;
3555
3556            let funding_rates = response.result.list;
3557
3558            let mut new_funding_rates_with_ts: Vec<(i64, _)> = funding_rates
3559                .into_iter()
3560                .filter_map(|f| {
3561                    let Ok(ts) = f.funding_rate_timestamp.parse::<i64>() else {
3562                        return None;
3563                    };
3564
3565                    seen_timestamps.insert(ts).then_some((ts, f))
3566                })
3567                .collect();
3568
3569            new_funding_rates_with_ts.sort_by_key(|(ts, _)| Reverse(*ts));
3570
3571            let earliest_funding_time = match new_funding_rates_with_ts.last() {
3572                Some((last_ts, _)) => *last_ts,
3573                None => break,
3574            };
3575
3576            let new_funding_rates = new_funding_rates_with_ts.into_iter().map(|(_, f)| f);
3577            raw_funding_rates.extend(new_funding_rates);
3578
3579            // Check if we've reached the requested limit
3580            if let Some(limit_val) = limit
3581                && raw_funding_rates.len() >= limit_val as usize
3582            {
3583                break;
3584            }
3585
3586            if let Some(start_val) = start_ms
3587                && earliest_funding_time <= start_val
3588            {
3589                break;
3590            }
3591
3592            // Move end time backwards to get earlier data
3593            current_end_ms = Some(earliest_funding_time - 1);
3594        }
3595
3596        if let Some(limit_val) = limit {
3597            raw_funding_rates.truncate(limit_val as usize);
3598        }
3599        let mut rates: Vec<FundingRateUpdate> = Vec::with_capacity(raw_funding_rates.len());
3600
3601        for window in raw_funding_rates.windows(2) {
3602            let raw = &window[0];
3603            let timestamp = raw
3604                .funding_rate_timestamp
3605                .parse::<i64>()
3606                .map_err(|_| anyhow::anyhow!("invalid funding_rate_timestamp"))?;
3607            let older_timestamp = window[1]
3608                .funding_rate_timestamp
3609                .parse::<i64>()
3610                .map_err(|_| anyhow::anyhow!("invalid funding_rate_timestamp"))?;
3611
3612            let interval_millis = timestamp - older_timestamp;
3613            let rate = parse_funding_rate(raw, &instrument, Some(interval_millis))?;
3614
3615            rates.push(rate);
3616        }
3617
3618        if let Some(last_raw) = raw_funding_rates.last() {
3619            let rate = parse_funding_rate(last_raw, &instrument, None)?;
3620            rates.push(rate);
3621        }
3622
3623        rates.reverse();
3624
3625        Ok(rates)
3626    }
3627
3628    /// Request an orderbook snapshot for a given symbol.
3629    ///
3630    /// Bybit limits the amount of levels (depth) for each product type to:
3631    /// - Spot: `1..=200` (default: `1`)
3632    /// - Linear & Inverse: `1..=500` (default: `25`)
3633    /// - Options: `1..=25` (default: `1`)
3634    ///
3635    /// # Errors
3636    ///
3637    /// Returns an error if:
3638    /// - The instrument is not found in cache.
3639    /// - The request fails.
3640    /// - Parsing fails.
3641    ///
3642    /// # References
3643    ///
3644    /// <https://bybit-exchange.github.io/docs/v5/market/orderbook>
3645    pub async fn request_orderbook_snapshot(
3646        &self,
3647        product_type: BybitProductType,
3648        instrument_id: InstrumentId,
3649        limit: Option<u32>,
3650    ) -> anyhow::Result<OrderBookDeltas> {
3651        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
3652        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
3653
3654        let mut params_builder = BybitOrderbookParamsBuilder::default();
3655        params_builder.category(product_type);
3656        params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3657
3658        if let Some(limit) = limit {
3659            let max_limit = match product_type {
3660                BybitProductType::Spot => 200,
3661                BybitProductType::Option => 25,
3662                BybitProductType::Linear | BybitProductType::Inverse => 500,
3663            };
3664            let clamped_limit = limit.min(max_limit);
3665            if limit > max_limit {
3666                log::warn!(
3667                    "Bybit orderbook snapshot request depth limit exceeds venue maximum; clamping: limit={limit}, clamped_limit={clamped_limit}",
3668                );
3669            }
3670            params_builder.limit(clamped_limit);
3671        }
3672
3673        let params = params_builder.build().build_anyhow()?;
3674        let response = self.inner.get_orderbook(&params).await?;
3675
3676        let deltas = parse_orderbook(&response.result, &instrument, None)?;
3677
3678        Ok(deltas)
3679    }
3680
3681    /// Request bar/kline history for a given symbol.
3682    ///
3683    /// # Errors
3684    ///
3685    /// Returns an error if:
3686    /// - The instrument is not found in cache.
3687    /// - The request fails.
3688    /// - Parsing fails.
3689    ///
3690    /// # References
3691    ///
3692    /// <https://bybit-exchange.github.io/docs/v5/market/kline>
3693    pub async fn request_bars(
3694        &self,
3695        product_type: BybitProductType,
3696        bar_type: BarType,
3697        start: Option<DateTime<Utc>>,
3698        end: Option<DateTime<Utc>>,
3699        limit: Option<u32>,
3700        timestamp_on_close: bool,
3701    ) -> anyhow::Result<Vec<Bar>> {
3702        let instrument = self.instrument_from_cache(&bar_type.instrument_id().symbol)?;
3703        let bybit_symbol = BybitSymbol::new(bar_type.instrument_id().symbol.as_str())?;
3704
3705        // Convert Nautilus BarSpec to Bybit interval
3706        let interval = bar_spec_to_bybit_interval(
3707            bar_type.spec().aggregation,
3708            bar_type.spec().step.get() as u64,
3709        )?;
3710
3711        let start_ms = start.map(|dt| dt.timestamp_millis());
3712        let mut seen_timestamps: AHashSet<i64> = AHashSet::new();
3713        let current_time_ms = get_atomic_clock_realtime().get_time_ms() as i64;
3714
3715        // Pagination strategy: work backwards from end time
3716        // - Each page fetched is older than the previous page
3717        // - Within each page, bars are in chronological order (oldest to newest)
3718        // - We collect pages in reverse order (newest first) then reverse at the end
3719        // Example with 2 pages:
3720        //   Page 1 (most recent): bars [T=2000..2999]
3721        //   Page 2 (older):       bars [T=1000..1999]
3722        //   Collected: [[T=2000..2999], [T=1000..1999]]
3723        //   After reverse + flatten: [T=1000..1999, T=2000..2999] ✓ chronological
3724        let mut pages: Vec<Vec<Bar>> = Vec::new();
3725        let mut total_bars = 0usize;
3726        let mut current_end = end.map(|dt| dt.timestamp_millis());
3727        let mut page_count = 0;
3728
3729        loop {
3730            page_count += 1;
3731
3732            let mut params_builder = BybitKlinesParamsBuilder::default();
3733            params_builder.category(product_type);
3734            params_builder.symbol(bybit_symbol.raw_symbol().to_string());
3735            params_builder.interval(interval);
3736            params_builder.limit(1000u32); // Limit for data size per page (maximum for the Bybit API)
3737
3738            if let Some(start_val) = start_ms {
3739                params_builder.start(start_val);
3740            }
3741
3742            if let Some(end_val) = current_end {
3743                params_builder.end(end_val);
3744            }
3745
3746            let params = params_builder.build().build_anyhow()?;
3747            let response = self.inner.get_klines(&params).await?;
3748
3749            let klines = response.result.list;
3750            if klines.is_empty() {
3751                break;
3752            }
3753
3754            // Parse timestamps once and pair with klines for sorting
3755            let mut klines_with_ts: Vec<(i64, _)> = klines
3756                .into_iter()
3757                .filter_map(|k| k.start.parse::<i64>().ok().map(|ts| (ts, k)))
3758                .collect();
3759
3760            klines_with_ts.sort_by_key(|(ts, _)| *ts);
3761
3762            // Check if we have any new timestamps
3763            let has_new = klines_with_ts
3764                .iter()
3765                .any(|(ts, _)| !seen_timestamps.contains(ts));
3766
3767            if !has_new {
3768                break;
3769            }
3770
3771            let mut page_bars = Vec::with_capacity(klines_with_ts.len());
3772
3773            let mut earliest_ts: Option<i64> = None;
3774
3775            for (start_time, kline) in &klines_with_ts {
3776                // Track earliest timestamp for pagination
3777                if earliest_ts.is_none_or(|ts| *start_time < ts) {
3778                    earliest_ts = Some(*start_time);
3779                }
3780
3781                let bar_end_time = interval.bar_end_time_ms(*start_time);
3782                if bar_end_time > current_time_ms {
3783                    continue;
3784                }
3785
3786                if !seen_timestamps.contains(start_time)
3787                    && let Ok(bar) =
3788                        parse_kline_bar(kline, &instrument, bar_type, timestamp_on_close, None)
3789                {
3790                    page_bars.push(bar);
3791                    seen_timestamps.insert(*start_time);
3792                }
3793            }
3794
3795            // page_bars may be empty if all klines were partial, but pagination
3796            // continues to fetch older closed bars
3797            total_bars += page_bars.len();
3798            pages.push(page_bars);
3799
3800            // Check if we've reached the requested limit
3801            if let Some(limit_val) = limit
3802                && total_bars >= limit_val as usize
3803            {
3804                break;
3805            }
3806
3807            // Move end time backwards to get earlier data
3808            // Set new end to be 1ms before the first bar of this page
3809            let Some(earliest_bar_time) = earliest_ts else {
3810                break;
3811            };
3812
3813            if let Some(start_val) = start_ms
3814                && earliest_bar_time <= start_val
3815            {
3816                break;
3817            }
3818
3819            current_end = Some(earliest_bar_time - 1);
3820
3821            // Safety check to prevent infinite loops
3822            if page_count > 100 {
3823                break;
3824            }
3825        }
3826
3827        // Reverse pages and flatten to get chronological order (oldest to newest)
3828        let mut all_bars: Vec<Bar> = Vec::with_capacity(total_bars);
3829        for page in pages.into_iter().rev() {
3830            all_bars.extend(page);
3831        }
3832
3833        // If limit is specified and we have more bars, return the last N bars (most recent)
3834        if let Some(limit_val) = limit {
3835            let limit_usize = limit_val as usize;
3836            if all_bars.len() > limit_usize {
3837                let start_idx = all_bars.len() - limit_usize;
3838                return Ok(all_bars[start_idx..].to_vec());
3839            }
3840        }
3841
3842        Ok(all_bars)
3843    }
3844
3845    /// Requests trading fee rates for the specified product type and optional filters.
3846    ///
3847    /// # Errors
3848    ///
3849    /// Returns an error if:
3850    /// - The request fails.
3851    /// - Parsing fails.
3852    ///
3853    /// # References
3854    ///
3855    /// <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
3856    pub async fn request_fee_rates(
3857        &self,
3858        product_type: BybitProductType,
3859        symbol: Option<String>,
3860        base_coin: Option<String>,
3861    ) -> anyhow::Result<Vec<BybitFeeRate>> {
3862        let params = BybitFeeRateParams {
3863            category: product_type,
3864            symbol,
3865            base_coin,
3866        };
3867
3868        let response = self.inner.get_fee_rate(&params).await?;
3869        Ok(response.result.list)
3870    }
3871
3872    /// Requests the current account state for the specified account type.
3873    ///
3874    /// # Errors
3875    ///
3876    /// Returns an error if:
3877    /// - The request fails.
3878    /// - Parsing fails.
3879    ///
3880    /// # References
3881    ///
3882    /// <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
3883    pub async fn request_account_state(
3884        &self,
3885        account_type: BybitAccountType,
3886        account_id: AccountId,
3887    ) -> anyhow::Result<AccountState> {
3888        let params = BybitWalletBalanceParams {
3889            account_type,
3890            coin: None,
3891        };
3892
3893        let response = self.inner.get_wallet_balance(&params).await?;
3894        let ts_init = self.generate_ts_init();
3895
3896        // Take the first wallet balance from the list
3897        let wallet_balance = response
3898            .result
3899            .list
3900            .first()
3901            .ok_or_else(|| anyhow::anyhow!("No wallet balance found in response"))?;
3902
3903        parse_account_state(wallet_balance, account_id, ts_init)
3904    }
3905
3906    /// Request multiple order status reports.
3907    ///
3908    /// Orders for instruments not currently loaded in cache will be skipped.
3909    ///
3910    /// # Errors
3911    ///
3912    /// Returns an error if:
3913    /// - Credentials are missing.
3914    /// - The request fails.
3915    /// - The API returns an error.
3916    #[expect(clippy::too_many_arguments)]
3917    pub async fn request_order_status_reports(
3918        &self,
3919        account_id: AccountId,
3920        product_type: BybitProductType,
3921        instrument_id: Option<InstrumentId>,
3922        open_only: bool,
3923        start: Option<DateTime<Utc>>,
3924        end: Option<DateTime<Utc>>,
3925        limit: Option<u32>,
3926    ) -> anyhow::Result<Vec<OrderStatusReport>> {
3927        // Extract symbol parameter from instrument_id if provided
3928        let symbol_param = if let Some(id) = instrument_id.as_ref() {
3929            let symbol_str = id.symbol.as_str();
3930            if symbol_str.is_empty() {
3931                None
3932            } else {
3933                Some(BybitSymbol::new(symbol_str)?.raw_symbol().to_string())
3934            }
3935        } else {
3936            None
3937        };
3938
3939        // For LINEAR without symbol, query all settle coins to avoid filtering
3940        // For INVERSE, never use settle_coin parameter
3941        let settle_coins_to_query: Vec<Option<String>> =
3942            if product_type == BybitProductType::Linear && symbol_param.is_none() {
3943                vec![Some("USDT".to_string()), Some("USDC".to_string())]
3944            } else {
3945                match product_type {
3946                    BybitProductType::Inverse => vec![None],
3947                    _ => vec![None],
3948                }
3949            };
3950
3951        let mut all_collected_orders = Vec::new();
3952        let mut total_collected_across_coins = 0;
3953
3954        for settle_coin in settle_coins_to_query {
3955            let remaining_limit = if let Some(limit) = limit {
3956                let remaining = (limit as usize).saturating_sub(total_collected_across_coins);
3957                if remaining == 0 {
3958                    break;
3959                }
3960                Some(remaining as u32)
3961            } else {
3962                None
3963            };
3964
3965            let orders_for_coin = if open_only {
3966                let mut all_orders = Vec::new();
3967                let mut seen_ids: AHashSet<Ustr> = AHashSet::new();
3968
3969                // Query regular orders then conditional (stop/MIT) orders.
3970                // Options do not support the StopOrder filter.
3971                let order_filters: Vec<Option<BybitOrderFilter>> =
3972                    if product_type == BybitProductType::Option {
3973                        vec![None]
3974                    } else {
3975                        vec![None, Some(BybitOrderFilter::StopOrder)]
3976                    };
3977
3978                for order_filter in order_filters {
3979                    let mut cursor: Option<String> = None;
3980
3981                    loop {
3982                        let remaining = if let Some(limit) = remaining_limit {
3983                            (limit as usize).saturating_sub(all_orders.len())
3984                        } else {
3985                            usize::MAX
3986                        };
3987
3988                        if remaining == 0 {
3989                            break;
3990                        }
3991
3992                        // Max 50 per Bybit API
3993                        let page_limit = std::cmp::min(remaining, 50);
3994
3995                        let mut p = BybitOpenOrdersParamsBuilder::default();
3996                        p.category(product_type);
3997
3998                        if let Some(symbol) = symbol_param.clone() {
3999                            p.symbol(symbol);
4000                        }
4001
4002                        if let Some(coin) = settle_coin.clone() {
4003                            p.settle_coin(coin);
4004                        }
4005
4006                        if let Some(of) = order_filter {
4007                            p.order_filter(of);
4008                        }
4009                        p.limit(page_limit as u32);
4010
4011                        if let Some(c) = cursor {
4012                            p.cursor(c);
4013                        }
4014                        let params = p.build().build_anyhow()?;
4015                        let response: BybitOpenOrdersResponse = self
4016                            .inner
4017                            .send_request(
4018                                Method::GET,
4019                                BYBIT_ORDER_REALTIME,
4020                                Some(&params),
4021                                None,
4022                                true,
4023                            )
4024                            .await?;
4025
4026                        for order in response.result.list {
4027                            if seen_ids.insert(order.order_id) {
4028                                all_orders.push(order);
4029                            }
4030                        }
4031
4032                        cursor = response.result.next_page_cursor;
4033                        if cursor.as_ref().is_none_or(|c| c.is_empty()) {
4034                            break;
4035                        }
4036                    }
4037                }
4038
4039                all_orders
4040            } else {
4041                // Query both realtime and history endpoints
4042                // Realtime has current open orders, history may lag for recent orders
4043                let mut all_orders = Vec::new();
4044                let mut open_orders = Vec::new();
4045                let mut seen_open_ids: AHashSet<Ustr> = AHashSet::new();
4046
4047                // Query regular orders then conditional (stop/MIT) orders.
4048                // Options do not support the StopOrder filter.
4049                let order_filters: Vec<Option<BybitOrderFilter>> =
4050                    if product_type == BybitProductType::Option {
4051                        vec![None]
4052                    } else {
4053                        vec![None, Some(BybitOrderFilter::StopOrder)]
4054                    };
4055
4056                for order_filter in &order_filters {
4057                    let mut cursor: Option<String> = None;
4058
4059                    loop {
4060                        let remaining = if let Some(limit) = remaining_limit {
4061                            (limit as usize).saturating_sub(open_orders.len())
4062                        } else {
4063                            usize::MAX
4064                        };
4065
4066                        if remaining == 0 {
4067                            break;
4068                        }
4069
4070                        // Max 50 per Bybit API
4071                        let page_limit = std::cmp::min(remaining, 50);
4072
4073                        let mut open_params = BybitOpenOrdersParamsBuilder::default();
4074                        open_params.category(product_type);
4075
4076                        if let Some(symbol) = symbol_param.clone() {
4077                            open_params.symbol(symbol);
4078                        }
4079
4080                        if let Some(coin) = settle_coin.clone() {
4081                            open_params.settle_coin(coin);
4082                        }
4083
4084                        if let Some(of) = order_filter {
4085                            open_params.order_filter(*of);
4086                        }
4087                        open_params.limit(page_limit as u32);
4088
4089                        if let Some(c) = cursor {
4090                            open_params.cursor(c);
4091                        }
4092                        let open_params = open_params.build().build_anyhow()?;
4093                        let open_response: BybitOpenOrdersResponse = self
4094                            .inner
4095                            .send_request(
4096                                Method::GET,
4097                                BYBIT_ORDER_REALTIME,
4098                                Some(&open_params),
4099                                None,
4100                                true,
4101                            )
4102                            .await?;
4103
4104                        for order in open_response.result.list {
4105                            if !seen_open_ids.contains(&order.order_id) {
4106                                seen_open_ids.insert(order.order_id);
4107                                open_orders.push(order);
4108                            }
4109                        }
4110
4111                        cursor = open_response.result.next_page_cursor;
4112                        if cursor.as_ref().is_none_or(|c| c.is_empty()) {
4113                            break;
4114                        }
4115                    }
4116                }
4117
4118                let seen_order_ids: AHashSet<Ustr> = seen_open_ids;
4119                let total_open_orders = open_orders.len();
4120
4121                all_orders.extend(open_orders);
4122
4123                let mut total_history_orders = 0;
4124
4125                for order_filter in &order_filters {
4126                    let mut cursor: Option<String> = None;
4127
4128                    loop {
4129                        let total_orders = total_open_orders + total_history_orders;
4130                        let remaining = if let Some(limit) = remaining_limit {
4131                            (limit as usize).saturating_sub(total_orders)
4132                        } else {
4133                            usize::MAX
4134                        };
4135
4136                        if remaining == 0 {
4137                            break;
4138                        }
4139
4140                        // Max 50 per Bybit API
4141                        let page_limit = std::cmp::min(remaining, 50);
4142
4143                        let mut history_params = BybitOrderHistoryParamsBuilder::default();
4144                        history_params.category(product_type);
4145
4146                        if let Some(symbol) = symbol_param.clone() {
4147                            history_params.symbol(symbol);
4148                        }
4149
4150                        if let Some(coin) = settle_coin.clone() {
4151                            history_params.settle_coin(coin);
4152                        }
4153
4154                        if let Some(of) = order_filter {
4155                            history_params.order_filter(*of);
4156                        }
4157
4158                        if let Some(start) = start {
4159                            history_params.start_time(start.timestamp_millis());
4160                        }
4161
4162                        if let Some(end) = end {
4163                            history_params.end_time(end.timestamp_millis());
4164                        }
4165                        history_params.limit(page_limit as u32);
4166
4167                        if let Some(c) = cursor {
4168                            history_params.cursor(c);
4169                        }
4170                        let history_params = history_params.build().build_anyhow()?;
4171                        let history_response: BybitOrderHistoryResponse = self
4172                            .inner
4173                            .send_request(
4174                                Method::GET,
4175                                BYBIT_ORDER_HISTORY,
4176                                Some(&history_params),
4177                                None,
4178                                true,
4179                            )
4180                            .await?;
4181
4182                        // Open orders might appear in both realtime and history
4183                        for order in history_response.result.list {
4184                            if !seen_order_ids.contains(&order.order_id) {
4185                                all_orders.push(order);
4186                                total_history_orders += 1;
4187                            }
4188                        }
4189
4190                        cursor = history_response.result.next_page_cursor;
4191                        if cursor.as_ref().is_none_or(|c| c.is_empty()) {
4192                            break;
4193                        }
4194                    }
4195                }
4196
4197                all_orders
4198            };
4199
4200            total_collected_across_coins += orders_for_coin.len();
4201            all_collected_orders.extend(orders_for_coin);
4202        }
4203
4204        let ts_init = self.generate_ts_init();
4205
4206        let mut reports = Vec::new();
4207
4208        for order in all_collected_orders {
4209            if let Some(ref instrument_id) = instrument_id {
4210                let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
4211
4212                if let Ok(report) =
4213                    parse_order_status_report(&order, &instrument, account_id, ts_init)
4214                {
4215                    reports.push(report);
4216                }
4217            } else {
4218                // Bybit returns raw symbol (e.g. "ETHUSDT"), need to add product suffix for cache lookup
4219                // Note: instruments are stored in cache by symbol only (without venue)
4220                if !order.symbol.is_empty() {
4221                    let symbol_with_product =
4222                        Symbol::from_ustr_unchecked(make_bybit_symbol(order.symbol, product_type));
4223
4224                    let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
4225                        log::debug!(
4226                            "Skipping order report for instrument not in cache: symbol={}, full_symbol={}",
4227                            order.symbol,
4228                            symbol_with_product
4229                        );
4230                        continue;
4231                    };
4232
4233                    match parse_order_status_report(&order, &instrument, account_id, ts_init) {
4234                        Ok(report) => reports.push(report),
4235                        Err(e) => {
4236                            log::error!("Failed to parse order status report: {e}");
4237                        }
4238                    }
4239                }
4240            }
4241        }
4242
4243        Ok(reports)
4244    }
4245
4246    /// Fetches execution history (fills) for the account and returns a list of [`FillReport`]s.
4247    ///
4248    /// Executions for instruments not currently loaded in cache will be skipped.
4249    ///
4250    /// # Errors
4251    ///
4252    /// This function returns an error if the request fails.
4253    ///
4254    /// # References
4255    ///
4256    /// <https://bybit-exchange.github.io/docs/v5/order/execution>
4257    pub async fn request_fill_reports(
4258        &self,
4259        account_id: AccountId,
4260        product_type: BybitProductType,
4261        instrument_id: Option<InstrumentId>,
4262        start: Option<i64>,
4263        end: Option<i64>,
4264        limit: Option<u32>,
4265    ) -> anyhow::Result<Vec<FillReport>> {
4266        // Build query parameters
4267        let symbol = if let Some(id) = instrument_id {
4268            let bybit_symbol = BybitSymbol::new(id.symbol.as_str())?;
4269            Some(bybit_symbol.raw_symbol().to_string())
4270        } else {
4271            None
4272        };
4273
4274        // Fetch all executions with pagination
4275        let mut all_executions = Vec::new();
4276        let mut cursor: Option<String> = None;
4277        let mut total_executions = 0;
4278
4279        loop {
4280            // Calculate how many more executions we can request
4281            let remaining = if let Some(limit) = limit {
4282                (limit as usize).saturating_sub(total_executions)
4283            } else {
4284                usize::MAX
4285            };
4286
4287            // If we've reached the limit, stop
4288            if remaining == 0 {
4289                break;
4290            }
4291
4292            // Size the page request to respect caller's limit (max 100 per Bybit API)
4293            let page_limit = std::cmp::min(remaining, 100);
4294
4295            let params = BybitTradeHistoryParams {
4296                category: product_type,
4297                symbol: symbol.clone(),
4298                base_coin: None,
4299                order_id: None,
4300                order_link_id: None,
4301                start_time: start,
4302                end_time: end,
4303                exec_type: None,
4304                limit: Some(page_limit as u32),
4305                cursor: cursor.clone(),
4306            };
4307
4308            let response = self.inner.get_trade_history(&params).await?;
4309            let list_len = response.result.list.len();
4310            all_executions.extend(response.result.list);
4311            total_executions += list_len;
4312
4313            cursor = response.result.next_page_cursor;
4314            if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
4315                break;
4316            }
4317        }
4318
4319        let ts_init = self.generate_ts_init();
4320        let mut reports = Vec::new();
4321
4322        for execution in all_executions {
4323            // Get instrument for this execution
4324            // Bybit returns raw symbol (e.g. "ETHUSDT"), need to add product suffix for cache lookup
4325            let symbol_with_product =
4326                Symbol::from_ustr_unchecked(make_bybit_symbol(execution.symbol, product_type));
4327
4328            let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
4329                log::debug!(
4330                    "Skipping fill report for instrument not in cache: symbol={}, full_symbol={}",
4331                    execution.symbol,
4332                    symbol_with_product
4333                );
4334                continue;
4335            };
4336
4337            match parse_fill_report(&execution, account_id, &instrument, ts_init) {
4338                Ok(report) => reports.push(report),
4339                Err(e) => {
4340                    log::error!("Failed to parse fill report: {e}");
4341                }
4342            }
4343        }
4344
4345        Ok(reports)
4346    }
4347
4348    /// Fetches position information for the account and returns a list of [`PositionStatusReport`]s.
4349    ///
4350    /// Positions for instruments not currently loaded in cache will be skipped.
4351    ///
4352    /// # Errors
4353    ///
4354    /// This function returns an error if the request fails.
4355    ///
4356    /// # References
4357    ///
4358    /// <https://bybit-exchange.github.io/docs/v5/position>
4359    pub async fn request_position_status_reports(
4360        &self,
4361        account_id: AccountId,
4362        product_type: BybitProductType,
4363        instrument_id: Option<InstrumentId>,
4364    ) -> anyhow::Result<Vec<PositionStatusReport>> {
4365        // Handle SPOT position reports via wallet balances if flag is enabled
4366        if product_type == BybitProductType::Spot {
4367            if self.use_spot_position_reports.load(Ordering::Relaxed) {
4368                return self
4369                    .generate_spot_position_reports_from_wallet(account_id, instrument_id)
4370                    .await;
4371            } else {
4372                // Return empty vector when SPOT position reports are disabled
4373                return Ok(Vec::new());
4374            }
4375        }
4376
4377        let ts_init = self.generate_ts_init();
4378        let mut reports = Vec::new();
4379
4380        // Build query parameters based on whether a specific instrument is requested
4381        let symbol = if let Some(id) = instrument_id {
4382            let symbol_str = id.symbol.as_str();
4383            if symbol_str.is_empty() {
4384                anyhow::bail!("InstrumentId symbol is empty");
4385            }
4386            let bybit_symbol = BybitSymbol::new(symbol_str)?;
4387            Some(bybit_symbol.raw_symbol().to_string())
4388        } else {
4389            None
4390        };
4391
4392        // For LINEAR category, the API requires either symbol OR settleCoin
4393        // When querying all positions (no symbol), we must iterate through settle coins
4394        if product_type == BybitProductType::Linear && symbol.is_none() {
4395            // Query positions for each known settle coin with pagination
4396            for settle_coin in ["USDT", "USDC"] {
4397                let mut cursor: Option<String> = None;
4398
4399                loop {
4400                    let params = BybitPositionListParams {
4401                        category: product_type,
4402                        symbol: None,
4403                        base_coin: None,
4404                        settle_coin: Some(settle_coin.to_string()),
4405                        limit: Some(200), // Max 200 per request
4406                        cursor: cursor.clone(),
4407                    };
4408
4409                    let response = self.inner.get_positions(&params).await?;
4410
4411                    for position in response.result.list {
4412                        if position.symbol.is_empty() {
4413                            continue;
4414                        }
4415
4416                        let symbol_with_product = Symbol::new(format!(
4417                            "{}{}",
4418                            position.symbol.as_str(),
4419                            product_type.suffix()
4420                        ));
4421
4422                        let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
4423                        else {
4424                            log::debug!(
4425                                "Skipping position report for instrument not in cache: symbol={}, full_symbol={}",
4426                                position.symbol,
4427                                symbol_with_product
4428                            );
4429                            continue;
4430                        };
4431
4432                        match parse_position_status_report(
4433                            &position,
4434                            account_id,
4435                            &instrument,
4436                            ts_init,
4437                        ) {
4438                            Ok(report) => reports.push(report),
4439                            Err(e) => {
4440                                log::error!("Failed to parse position status report: {e}");
4441                            }
4442                        }
4443                    }
4444
4445                    cursor = response.result.next_page_cursor;
4446                    if cursor.as_ref().is_none_or(|c| c.is_empty()) {
4447                        break;
4448                    }
4449                }
4450            }
4451        } else {
4452            // For other product types or when a specific symbol is requested with pagination
4453            let mut cursor: Option<String> = None;
4454
4455            loop {
4456                let params = BybitPositionListParams {
4457                    category: product_type,
4458                    symbol: symbol.clone(),
4459                    base_coin: None,
4460                    settle_coin: None,
4461                    limit: Some(200), // Max 200 per request
4462                    cursor: cursor.clone(),
4463                };
4464
4465                let response = self.inner.get_positions(&params).await?;
4466
4467                for position in response.result.list {
4468                    if position.symbol.is_empty() {
4469                        continue;
4470                    }
4471
4472                    let symbol_with_product = Symbol::new(format!(
4473                        "{}{}",
4474                        position.symbol.as_str(),
4475                        product_type.suffix()
4476                    ));
4477
4478                    let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
4479                        log::debug!(
4480                            "Skipping position report for instrument not in cache: symbol={}, full_symbol={}",
4481                            position.symbol,
4482                            symbol_with_product
4483                        );
4484                        continue;
4485                    };
4486
4487                    match parse_position_status_report(&position, account_id, &instrument, ts_init)
4488                    {
4489                        Ok(report) => reports.push(report),
4490                        Err(e) => {
4491                            log::error!("Failed to parse position status report: {e}");
4492                        }
4493                    }
4494                }
4495
4496                cursor = response.result.next_page_cursor;
4497                if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
4498                    break;
4499                }
4500            }
4501        }
4502
4503        Ok(reports)
4504    }
4505
4506    async fn query_order_by_id(
4507        &self,
4508        product_type: BybitProductType,
4509        order_id: &str,
4510        endpoint: &str,
4511        context: &str,
4512    ) -> anyhow::Result<BybitOrder> {
4513        let mut query_params = BybitOpenOrdersParamsBuilder::default();
4514        query_params.category(product_type);
4515        query_params.order_id(order_id.to_string());
4516
4517        let query_params = query_params.build().build_anyhow()?;
4518        let order_response: BybitOpenOrdersResponse = self
4519            .inner
4520            .send_request(Method::GET, endpoint, Some(&query_params), None, true)
4521            .await?;
4522
4523        order_response
4524            .result
4525            .list
4526            .into_iter()
4527            .next()
4528            .ok_or_else(|| anyhow::anyhow!("No order returned {context}"))
4529    }
4530}
4531
4532#[cfg(test)]
4533mod tests {
4534    use rstest::rstest;
4535
4536    use super::*;
4537
4538    #[rstest]
4539    fn test_client_creation() {
4540        let client = BybitHttpClient::new(None, 60, 3, 1000, 10_000, 5_000, None);
4541        assert!(client.is_ok());
4542
4543        let client = client.unwrap();
4544        assert!(client.base_url().contains("bybit.com"));
4545        assert!(client.credential().is_none());
4546    }
4547
4548    #[rstest]
4549    fn test_client_with_credentials() {
4550        let client = BybitHttpClient::with_credentials(
4551            "test_key".to_string(),
4552            "test_secret".to_string(),
4553            Some("https://api-testnet.bybit.com".to_string()),
4554            60,
4555            3,
4556            1000,
4557            10_000,
4558            5_000,
4559            None,
4560        );
4561        assert!(client.is_ok());
4562
4563        let client = client.unwrap();
4564        assert!(client.credential().is_some());
4565    }
4566
4567    #[rstest]
4568    fn test_build_path_with_params() {
4569        #[derive(Serialize)]
4570        struct TestParams {
4571            category: String,
4572            symbol: String,
4573        }
4574
4575        let params = TestParams {
4576            category: "linear".to_string(),
4577            symbol: "BTCUSDT".to_string(),
4578        };
4579
4580        let path = BybitRawHttpClient::build_path("/v5/market/test", &params);
4581        assert!(path.is_ok());
4582        assert!(path.unwrap().contains("category=linear"));
4583    }
4584
4585    #[rstest]
4586    fn test_build_path_without_params() {
4587        let params = ();
4588        let path = BybitRawHttpClient::build_path("/v5/market/time", &params);
4589        assert!(path.is_ok());
4590        assert_eq!(path.unwrap(), "/v5/market/time");
4591    }
4592
4593    #[rstest]
4594    fn test_params_serialization_matches_build_path() {
4595        // This test ensures our new serialization produces the same result as the old build_path
4596        #[derive(Serialize)]
4597        struct TestParams {
4598            category: String,
4599            limit: u32,
4600        }
4601
4602        let params = TestParams {
4603            category: "spot".to_string(),
4604            limit: 50,
4605        };
4606
4607        // Old way: build_path serialized params
4608        let old_path = BybitRawHttpClient::build_path(BYBIT_ORDER_REALTIME, &params).unwrap();
4609        let old_query = old_path.split('?').nth(1).unwrap_or("");
4610
4611        // New way: direct serialization
4612        let new_query = serde_urlencoded::to_string(&params).unwrap();
4613
4614        // They must match for signatures to work
4615        assert_eq!(old_query, new_query);
4616    }
4617
4618    #[rstest]
4619    fn test_params_serialization_order() {
4620        // Verify that serialization order is deterministic
4621        #[derive(Serialize)]
4622        struct OrderParams {
4623            category: String,
4624            symbol: String,
4625            limit: u32,
4626        }
4627
4628        let params = OrderParams {
4629            category: "spot".to_string(),
4630            symbol: "BTCUSDT".to_string(),
4631            limit: 50,
4632        };
4633
4634        // Serialize multiple times to ensure consistent ordering
4635        let query1 = serde_urlencoded::to_string(&params).unwrap();
4636        let query2 = serde_urlencoded::to_string(&params).unwrap();
4637        let query3 = serde_urlencoded::to_string(&params).unwrap();
4638
4639        assert_eq!(query1, query2);
4640        assert_eq!(query2, query3);
4641
4642        // The query should contain all params
4643        assert!(query1.contains("category=spot"));
4644        assert!(query1.contains("symbol=BTCUSDT"));
4645        assert!(query1.contains("limit=50"));
4646    }
4647
4648    #[rstest]
4649    #[case(
4650        "https://api-demo.bybit.com",
4651        BybitProductType::Linear,
4652        10001,
4653        "",
4654        "Bybit demo rejected the linear fee rate request via /v5/account/fee-rate \
4655         (error 10001, no message); demo derivatives fee rates appear unsupported, using defaults"
4656    )]
4657    #[case(
4658        "https://api-demo.bybit.com",
4659        BybitProductType::Inverse,
4660        10001,
4661        "",
4662        "Bybit demo rejected the inverse fee rate request via /v5/account/fee-rate \
4663         (error 10001, no message); demo derivatives fee rates appear unsupported, using defaults"
4664    )]
4665    #[case(
4666        "https://api.bybit.com",
4667        BybitProductType::Spot,
4668        10001,
4669        "Parameter error",
4670        "Fee rate request rejected for spot instruments via /v5/account/fee-rate \
4671         (error 10001: Parameter error), using defaults"
4672    )]
4673    #[case(
4674        "https://api-demo.bybit.com",
4675        BybitProductType::Spot,
4676        10001,
4677        "Parameter error",
4678        "Fee rate request rejected for spot instruments via /v5/account/fee-rate \
4679         (error 10001: Parameter error), using defaults"
4680    )]
4681    #[case(
4682        "https://api.bybit.com",
4683        BybitProductType::Linear,
4684        10001,
4685        "Parameter error",
4686        "Fee rate request rejected for linear instruments via /v5/account/fee-rate \
4687         (error 10001: Parameter error), using defaults"
4688    )]
4689    fn test_fee_rate_rejection_warning(
4690        #[case] base_url: &str,
4691        #[case] product_type: BybitProductType,
4692        #[case] error_code: i32,
4693        #[case] message: &str,
4694        #[case] expected: &str,
4695    ) {
4696        let client =
4697            BybitHttpClient::new(Some(base_url.to_string()), 60, 3, 1000, 10_000, 5_000, None)
4698                .unwrap();
4699
4700        let warning = client.fee_rate_rejection_warning(product_type, error_code, message);
4701
4702        assert_eq!(warning, expected);
4703    }
4704
4705    #[rstest]
4706    #[case(10001, "", "error 10001, no message")]
4707    #[case(10001, "Parameter error", "error 10001: Parameter error")]
4708    fn test_format_bybit_error_detail(
4709        #[case] error_code: i32,
4710        #[case] message: &str,
4711        #[case] expected: &str,
4712    ) {
4713        let detail = BybitHttpClient::format_bybit_error_detail(error_code, message);
4714
4715        assert_eq!(detail, expected);
4716    }
4717
4718    #[rstest]
4719    #[case(
4720        10001,
4721        "",
4722        "Option fee rate request rejected via /v5/account/fee-rate \
4723         (error 10001, no message), using defaults"
4724    )]
4725    #[case(
4726        10001,
4727        "Parameter error",
4728        "Option fee rate request rejected via /v5/account/fee-rate \
4729         (error 10001: Parameter error), using defaults"
4730    )]
4731    fn test_option_fee_rate_warning_message(
4732        #[case] error_code: i32,
4733        #[case] message: &str,
4734        #[case] expected: &str,
4735    ) {
4736        let error_detail = BybitHttpClient::format_bybit_error_detail(error_code, message);
4737        let warning = format!(
4738            "Option fee rate request rejected via /v5/account/fee-rate ({error_detail}), using defaults"
4739        );
4740
4741        assert_eq!(warning, expected);
4742    }
4743}