nautilus_bybit/http/
client.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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    collections::HashMap,
22    fmt::Debug,
23    num::NonZeroU32,
24    sync::{Arc, LazyLock, Mutex},
25};
26
27use nautilus_core::{
28    consts::NAUTILUS_USER_AGENT, nanos::UnixNanos, time::get_atomic_clock_realtime,
29};
30use nautilus_model::{
31    data::{Bar, BarType, TradeTick},
32    enums::{BarAggregation, OrderSide, OrderType, TimeInForce},
33    events::account::state::AccountState,
34    identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
35    instruments::{Instrument, InstrumentAny},
36    reports::{FillReport, OrderStatusReport, PositionStatusReport},
37    types::{Price, Quantity},
38};
39use nautilus_network::{
40    http::HttpClient,
41    ratelimiter::quota::Quota,
42    retry::{RetryConfig, RetryManager},
43};
44use reqwest::{Method, header::USER_AGENT};
45use serde::{Serialize, de::DeserializeOwned};
46use tokio_util::sync::CancellationToken;
47use ustr::Ustr;
48
49use super::{
50    error::BybitHttpError,
51    models::{
52        BybitFeeRate, BybitFeeRateResponse, BybitInstrumentInverseResponse,
53        BybitInstrumentLinearResponse, BybitInstrumentOptionResponse, BybitInstrumentSpotResponse,
54        BybitKlinesResponse, BybitOpenOrdersResponse, BybitOrderHistoryResponse,
55        BybitPlaceOrderResponse, BybitPositionListResponse, BybitServerTimeResponse,
56        BybitTradeHistoryResponse, BybitTradesResponse, BybitWalletBalanceResponse,
57    },
58    query::{
59        BybitAmendOrderParamsBuilder, BybitBatchAmendOrderEntryBuilder,
60        BybitBatchCancelOrderEntryBuilder, BybitBatchPlaceOrderEntryBuilder,
61        BybitCancelAllOrdersParamsBuilder, BybitCancelOrderParamsBuilder, BybitFeeRateParams,
62        BybitInstrumentsInfoParams, BybitKlinesParams, BybitKlinesParamsBuilder,
63        BybitOpenOrdersParamsBuilder, BybitOrderHistoryParamsBuilder, BybitPlaceOrderParamsBuilder,
64        BybitPositionListParams, BybitTickersParams, BybitTradeHistoryParams, BybitTradesParams,
65        BybitTradesParamsBuilder, BybitWalletBalanceParams,
66    },
67};
68use crate::{
69    common::{
70        consts::BYBIT_NAUTILUS_BROKER_ID,
71        credential::Credential,
72        enums::{
73            BybitAccountType, BybitEnvironment, BybitKlineInterval, BybitOrderSide, BybitOrderType,
74            BybitProductType, BybitTimeInForce,
75        },
76        models::BybitResponse,
77        parse::{
78            parse_account_state, parse_fill_report, parse_inverse_instrument, parse_kline_bar,
79            parse_linear_instrument, parse_option_instrument, parse_order_status_report,
80            parse_position_status_report, parse_spot_instrument, parse_trade_tick,
81        },
82        symbol::BybitSymbol,
83        urls::bybit_http_base_url,
84    },
85    http::query::BybitFeeRateParamsBuilder,
86};
87
88const DEFAULT_RECV_WINDOW_MS: u64 = 5_000;
89
90/// Default Bybit REST API rate limit.
91///
92/// Bybit implements rate limiting per endpoint with varying limits.
93/// We use a conservative 10 requests per second as a general default.
94pub static BYBIT_REST_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
95    Quota::per_second(NonZeroU32::new(10).expect("Should be a valid non-zero u32"))
96});
97
98const BYBIT_GLOBAL_RATE_KEY: &str = "bybit:global";
99
100/// Inner HTTP client implementation containing the actual HTTP logic.
101pub struct BybitHttpInnerClient {
102    base_url: String,
103    client: HttpClient,
104    credential: Option<Credential>,
105    recv_window_ms: u64,
106    retry_manager: RetryManager<BybitHttpError>,
107    cancellation_token: CancellationToken,
108    instruments_cache: Arc<Mutex<HashMap<Ustr, InstrumentAny>>>,
109}
110
111impl Default for BybitHttpInnerClient {
112    fn default() -> Self {
113        Self::new(None, Some(60), None, None, None)
114            .expect("Failed to create default BybitHttpInnerClient")
115    }
116}
117
118impl Debug for BybitHttpInnerClient {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("BybitHttpInnerClient")
121            .field("base_url", &self.base_url)
122            .field("has_credentials", &self.credential.is_some())
123            .field("recv_window_ms", &self.recv_window_ms)
124            .finish()
125    }
126}
127
128impl BybitHttpInnerClient {
129    /// Cancel all pending HTTP requests.
130    pub fn cancel_all_requests(&self) {
131        self.cancellation_token.cancel();
132    }
133
134    /// Get the cancellation token for this client.
135    pub fn cancellation_token(&self) -> &CancellationToken {
136        &self.cancellation_token
137    }
138
139    /// Creates a new [`BybitHttpInnerClient`] using the default Bybit HTTP URL.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if the retry manager cannot be created.
144    #[allow(clippy::too_many_arguments)]
145    pub fn new(
146        base_url: Option<String>,
147        timeout_secs: Option<u64>,
148        max_retries: Option<u32>,
149        retry_delay_ms: Option<u64>,
150        retry_delay_max_ms: Option<u64>,
151    ) -> Result<Self, BybitHttpError> {
152        let retry_config = RetryConfig {
153            max_retries: max_retries.unwrap_or(3),
154            initial_delay_ms: retry_delay_ms.unwrap_or(1000),
155            max_delay_ms: retry_delay_max_ms.unwrap_or(10_000),
156            backoff_factor: 2.0,
157            jitter_ms: 1000,
158            operation_timeout_ms: Some(60_000),
159            immediate_first: false,
160            max_elapsed_ms: Some(180_000),
161        };
162
163        let retry_manager = RetryManager::new(retry_config).map_err(|e| {
164            BybitHttpError::NetworkError(format!("Failed to create retry manager: {e}"))
165        })?;
166
167        Ok(Self {
168            base_url: base_url
169                .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
170            client: HttpClient::new(
171                Self::default_headers(),
172                vec![],
173                Self::rate_limiter_quotas(),
174                Some(*BYBIT_REST_QUOTA),
175                timeout_secs,
176            ),
177            credential: None,
178            recv_window_ms: DEFAULT_RECV_WINDOW_MS,
179            retry_manager,
180            cancellation_token: CancellationToken::new(),
181            instruments_cache: Arc::new(Mutex::new(HashMap::new())),
182        })
183    }
184
185    /// Creates a new [`BybitHttpInnerClient`] configured with credentials.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the retry manager cannot be created.
190    #[allow(clippy::too_many_arguments)]
191    pub fn with_credentials(
192        api_key: String,
193        api_secret: String,
194        base_url: Option<String>,
195        timeout_secs: Option<u64>,
196        max_retries: Option<u32>,
197        retry_delay_ms: Option<u64>,
198        retry_delay_max_ms: Option<u64>,
199    ) -> Result<Self, BybitHttpError> {
200        let retry_config = RetryConfig {
201            max_retries: max_retries.unwrap_or(3),
202            initial_delay_ms: retry_delay_ms.unwrap_or(1000),
203            max_delay_ms: retry_delay_max_ms.unwrap_or(10_000),
204            backoff_factor: 2.0,
205            jitter_ms: 1000,
206            operation_timeout_ms: Some(60_000),
207            immediate_first: false,
208            max_elapsed_ms: Some(180_000),
209        };
210
211        let retry_manager = RetryManager::new(retry_config).map_err(|e| {
212            BybitHttpError::NetworkError(format!("Failed to create retry manager: {e}"))
213        })?;
214
215        Ok(Self {
216            base_url: base_url
217                .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
218            client: HttpClient::new(
219                Self::default_headers(),
220                vec![],
221                Self::rate_limiter_quotas(),
222                Some(*BYBIT_REST_QUOTA),
223                timeout_secs,
224            ),
225            credential: Some(Credential::new(api_key, api_secret)),
226            recv_window_ms: DEFAULT_RECV_WINDOW_MS,
227            retry_manager,
228            cancellation_token: CancellationToken::new(),
229            instruments_cache: Arc::new(Mutex::new(HashMap::new())),
230        })
231    }
232
233    fn default_headers() -> HashMap<String, String> {
234        HashMap::from([
235            (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
236            ("Referer".to_string(), BYBIT_NAUTILUS_BROKER_ID.to_string()),
237        ])
238    }
239
240    fn rate_limiter_quotas() -> Vec<(String, Quota)> {
241        vec![(BYBIT_GLOBAL_RATE_KEY.to_string(), *BYBIT_REST_QUOTA)]
242    }
243
244    fn rate_limit_keys(endpoint: &str) -> Vec<String> {
245        let normalized = endpoint.split('?').next().unwrap_or(endpoint);
246        let route = format!("bybit:{normalized}");
247
248        vec![BYBIT_GLOBAL_RATE_KEY.to_string(), route]
249    }
250
251    fn sign_request(
252        &self,
253        timestamp: &str,
254        params: Option<&str>,
255    ) -> Result<HashMap<String, String>, BybitHttpError> {
256        let credential = self
257            .credential
258            .as_ref()
259            .ok_or(BybitHttpError::MissingCredentials)?;
260
261        let signature = credential.sign_with_payload(timestamp, self.recv_window_ms, params);
262
263        let mut headers = HashMap::new();
264        headers.insert(
265            "X-BAPI-API-KEY".to_string(),
266            credential.api_key().to_string(),
267        );
268        headers.insert("X-BAPI-TIMESTAMP".to_string(), timestamp.to_string());
269        headers.insert("X-BAPI-SIGN".to_string(), signature);
270        headers.insert(
271            "X-BAPI-RECV-WINDOW".to_string(),
272            self.recv_window_ms.to_string(),
273        );
274
275        Ok(headers)
276    }
277
278    async fn send_request<T: DeserializeOwned>(
279        &self,
280        method: Method,
281        endpoint: &str,
282        body: Option<Vec<u8>>,
283        authenticate: bool,
284    ) -> Result<T, BybitHttpError> {
285        let endpoint = endpoint.to_string();
286        let url = format!("{}{endpoint}", self.base_url);
287        let method_clone = method.clone();
288        let body_clone = body.clone();
289
290        let operation = || {
291            let url = url.clone();
292            let method = method_clone.clone();
293            let body = body_clone.clone();
294            let endpoint = endpoint.clone();
295
296            async move {
297                let mut headers = Self::default_headers();
298
299                if authenticate {
300                    let timestamp = get_atomic_clock_realtime().get_time_ms().to_string();
301                    let params_str = if method == Method::GET {
302                        endpoint.split('?').nth(1)
303                    } else {
304                        body.as_ref().and_then(|b| std::str::from_utf8(b).ok())
305                    };
306
307                    let auth_headers = self.sign_request(&timestamp, params_str)?;
308                    headers.extend(auth_headers);
309                }
310
311                if method == Method::POST || method == Method::PUT {
312                    headers.insert("Content-Type".to_string(), "application/json".to_string());
313                }
314
315                let rate_limit_keys = Self::rate_limit_keys(&endpoint);
316
317                let response = self
318                    .client
319                    .request(
320                        method,
321                        url,
322                        Some(headers),
323                        body,
324                        None,
325                        Some(rate_limit_keys),
326                    )
327                    .await?;
328
329                if response.status.as_u16() >= 400 {
330                    let body = String::from_utf8_lossy(&response.body).to_string();
331                    return Err(BybitHttpError::UnexpectedStatus {
332                        status: response.status.as_u16(),
333                        body,
334                    });
335                }
336
337                // Parse as BybitResponse to check retCode
338                let bybit_response: BybitResponse<serde_json::Value> =
339                    serde_json::from_slice(&response.body)?;
340
341                if bybit_response.ret_code != 0 {
342                    return Err(BybitHttpError::BybitError {
343                        error_code: bybit_response.ret_code as i32,
344                        message: bybit_response.ret_msg,
345                    });
346                }
347
348                // Deserialize the full response
349                let result: T = serde_json::from_slice(&response.body)?;
350                Ok(result)
351            }
352        };
353
354        let should_retry = |error: &BybitHttpError| -> bool {
355            match error {
356                BybitHttpError::NetworkError(_) => true,
357                BybitHttpError::UnexpectedStatus { status, .. } => *status >= 500,
358                _ => false,
359            }
360        };
361
362        let create_error = |msg: String| -> BybitHttpError {
363            if msg == "canceled" {
364                BybitHttpError::NetworkError("Request canceled".to_string())
365            } else {
366                BybitHttpError::NetworkError(msg)
367            }
368        };
369
370        self.retry_manager
371            .execute_with_retry_with_cancel(
372                endpoint.as_str(),
373                operation,
374                should_retry,
375                create_error,
376                &self.cancellation_token,
377            )
378            .await
379    }
380
381    fn build_path<S: Serialize>(base: &str, params: &S) -> Result<String, BybitHttpError> {
382        let query = serde_urlencoded::to_string(params)
383            .map_err(|e| BybitHttpError::JsonError(e.to_string()))?;
384        if query.is_empty() {
385            Ok(base.to_owned())
386        } else {
387            Ok(format!("{base}?{query}"))
388        }
389    }
390
391    // =========================================================================
392    // Low-level HTTP API methods
393    // =========================================================================
394
395    /// Fetches the current server time from Bybit.
396    ///
397    /// # Errors
398    ///
399    /// Returns an error if the request fails or the response cannot be parsed.
400    ///
401    /// # References
402    ///
403    /// - <https://bybit-exchange.github.io/docs/v5/market/time>
404    pub async fn http_get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
405        self.send_request(Method::GET, "/v5/market/time", None, false)
406            .await
407    }
408
409    /// Fetches instrument information from Bybit for a given product category.
410    ///
411    /// # Errors
412    ///
413    /// Returns an error if the request fails or the response cannot be parsed.
414    ///
415    /// # References
416    ///
417    /// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
418    pub async fn http_get_instruments<T: DeserializeOwned>(
419        &self,
420        params: &BybitInstrumentsInfoParams,
421    ) -> Result<T, BybitHttpError> {
422        let path = Self::build_path("/v5/market/instruments-info", params)?;
423        self.send_request(Method::GET, &path, None, false).await
424    }
425
426    /// Fetches spot instrument information from Bybit.
427    ///
428    /// # Errors
429    ///
430    /// Returns an error if the request fails or the response cannot be parsed.
431    ///
432    /// # References
433    ///
434    /// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
435    pub async fn http_get_instruments_spot(
436        &self,
437        params: &BybitInstrumentsInfoParams,
438    ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
439        self.http_get_instruments(params).await
440    }
441
442    /// Fetches linear instrument information from Bybit.
443    ///
444    /// # Errors
445    ///
446    /// Returns an error if the request fails or the response cannot be parsed.
447    ///
448    /// # References
449    ///
450    /// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
451    pub async fn http_get_instruments_linear(
452        &self,
453        params: &BybitInstrumentsInfoParams,
454    ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
455        self.http_get_instruments(params).await
456    }
457
458    /// Fetches inverse instrument information from Bybit.
459    ///
460    /// # Errors
461    ///
462    /// Returns an error if the request fails or the response cannot be parsed.
463    ///
464    /// # References
465    ///
466    /// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
467    pub async fn http_get_instruments_inverse(
468        &self,
469        params: &BybitInstrumentsInfoParams,
470    ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
471        self.http_get_instruments(params).await
472    }
473
474    /// Fetches option instrument information from Bybit.
475    ///
476    /// # Errors
477    ///
478    /// Returns an error if the request fails or the response cannot be parsed.
479    ///
480    /// # References
481    ///
482    /// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
483    pub async fn http_get_instruments_option(
484        &self,
485        params: &BybitInstrumentsInfoParams,
486    ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
487        self.http_get_instruments(params).await
488    }
489
490    /// Fetches kline/candlestick data from Bybit.
491    ///
492    /// # Errors
493    ///
494    /// Returns an error if the request fails or the response cannot be parsed.
495    ///
496    /// # References
497    ///
498    /// - <https://bybit-exchange.github.io/docs/v5/market/kline>
499    pub async fn http_get_klines(
500        &self,
501        params: &BybitKlinesParams,
502    ) -> Result<BybitKlinesResponse, BybitHttpError> {
503        let path = Self::build_path("/v5/market/kline", params)?;
504        self.send_request(Method::GET, &path, None, false).await
505    }
506
507    /// Fetches recent trades from Bybit.
508    ///
509    /// # Errors
510    ///
511    /// Returns an error if the request fails or the response cannot be parsed.
512    ///
513    /// # References
514    ///
515    /// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
516    pub async fn http_get_recent_trades(
517        &self,
518        params: &BybitTradesParams,
519    ) -> Result<BybitTradesResponse, BybitHttpError> {
520        let path = Self::build_path("/v5/market/recent-trade", params)?;
521        self.send_request(Method::GET, &path, None, false).await
522    }
523
524    /// Fetches open orders (requires authentication).
525    ///
526    /// # Errors
527    ///
528    /// Returns an error if the request fails or the response cannot be parsed.
529    ///
530    /// # References
531    ///
532    /// - <https://bybit-exchange.github.io/docs/v5/order/open-order>
533    pub async fn http_get_open_orders(
534        &self,
535        category: BybitProductType,
536        symbol: Option<&str>,
537    ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
538        #[derive(Serialize)]
539        #[serde(rename_all = "camelCase")]
540        struct Params<'a> {
541            category: BybitProductType,
542            #[serde(skip_serializing_if = "Option::is_none")]
543            symbol: Option<&'a str>,
544        }
545
546        let params = Params { category, symbol };
547        let path = Self::build_path("/v5/order/realtime", &params)?;
548        self.send_request(Method::GET, &path, None, true).await
549    }
550
551    /// Places a new order (requires authentication).
552    ///
553    /// # Errors
554    ///
555    /// Returns an error if the request fails or the response cannot be parsed.
556    ///
557    /// # References
558    ///
559    /// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
560    pub async fn http_place_order(
561        &self,
562        request: &serde_json::Value,
563    ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
564        let body = serde_json::to_vec(request)?;
565        self.send_request(Method::POST, "/v5/order/create", Some(body), true)
566            .await
567    }
568
569    /// Fetches wallet balance (requires authentication).
570    ///
571    /// # Errors
572    ///
573    /// Returns an error if the request fails or the response cannot be parsed.
574    ///
575    /// # References
576    ///
577    /// - <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
578    pub async fn http_get_wallet_balance(
579        &self,
580        params: &BybitWalletBalanceParams,
581    ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
582        let path = Self::build_path("/v5/account/wallet-balance", params)?;
583        self.send_request(Method::GET, &path, None, true).await
584    }
585
586    /// Fetches trading fee rates for symbols.
587    ///
588    /// # Errors
589    ///
590    /// Returns an error if the request fails or the response cannot be parsed.
591    ///
592    /// # References
593    ///
594    /// - <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
595    pub async fn http_get_fee_rate(
596        &self,
597        params: &BybitFeeRateParams,
598    ) -> Result<BybitFeeRateResponse, BybitHttpError> {
599        let path = Self::build_path("/v5/account/fee-rate", params)?;
600        self.send_request(Method::GET, &path, None, true).await
601    }
602
603    /// Fetches tickers for market data.
604    ///
605    /// # Errors
606    ///
607    /// Returns an error if the request fails or the response cannot be parsed.
608    ///
609    /// # References
610    ///
611    /// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
612    pub async fn http_get_tickers<T: DeserializeOwned>(
613        &self,
614        params: &BybitTickersParams,
615    ) -> Result<T, BybitHttpError> {
616        let path = Self::build_path("/v5/market/tickers", params)?;
617        self.send_request(Method::GET, &path, None, false).await
618    }
619
620    /// Fetches trade execution history (requires authentication).
621    ///
622    /// # Errors
623    ///
624    /// Returns an error if the request fails or the response cannot be parsed.
625    ///
626    /// # References
627    ///
628    /// - <https://bybit-exchange.github.io/docs/v5/order/execution>
629    pub async fn http_get_trade_history(
630        &self,
631        params: &BybitTradeHistoryParams,
632    ) -> Result<BybitTradeHistoryResponse, BybitHttpError> {
633        let path = Self::build_path("/v5/execution/list", params)?;
634        self.send_request(Method::GET, &path, None, true).await
635    }
636
637    /// Fetches position information (requires authentication).
638    ///
639    /// # Errors
640    ///
641    /// This function returns an error if:
642    /// - Credentials are missing.
643    /// - The request fails.
644    /// - The API returns an error.
645    ///
646    /// # References
647    ///
648    /// - <https://bybit-exchange.github.io/docs/v5/position/position-info>
649    pub async fn http_get_positions(
650        &self,
651        params: &BybitPositionListParams,
652    ) -> Result<BybitPositionListResponse, BybitHttpError> {
653        let path = Self::build_path("/v5/position/list", params)?;
654        self.send_request(Method::GET, &path, None, true).await
655    }
656
657    /// Returns the base URL used for requests.
658    #[must_use]
659    pub fn base_url(&self) -> &str {
660        &self.base_url
661    }
662
663    /// Returns the configured receive window in milliseconds.
664    #[must_use]
665    pub fn recv_window_ms(&self) -> u64 {
666        self.recv_window_ms
667    }
668
669    /// Returns the API credential if configured.
670    #[must_use]
671    pub fn credential(&self) -> Option<&Credential> {
672        self.credential.as_ref()
673    }
674
675    /// Add an instrument to the cache.
676    ///
677    /// # Panics
678    ///
679    /// Panics if the instruments cache mutex is poisoned.
680    pub fn add_instrument(&self, instrument: InstrumentAny) {
681        let mut cache = self.instruments_cache.lock().unwrap();
682        let symbol = Ustr::from(instrument.id().symbol.as_str());
683        cache.insert(symbol, instrument);
684    }
685
686    /// Get an instrument from the cache.
687    ///
688    /// # Errors
689    ///
690    /// Returns an error if the instrument is not found in the cache.
691    ///
692    /// # Panics
693    ///
694    /// Panics if the instruments cache mutex is poisoned.
695    pub fn instrument_from_cache(&self, symbol: &Symbol) -> anyhow::Result<InstrumentAny> {
696        let cache = self.instruments_cache.lock().unwrap();
697        cache.get(&symbol.inner()).cloned().ok_or_else(|| {
698            anyhow::anyhow!(
699                "Instrument {symbol} not found in cache, ensure instruments loaded first"
700            )
701        })
702    }
703
704    /// Generate a timestamp for initialization.
705    #[must_use]
706    pub fn generate_ts_init(&self) -> UnixNanos {
707        get_atomic_clock_realtime().get_time_ns()
708    }
709
710    // =========================================================================
711    // High-level domain methods
712    // =========================================================================
713
714    /// Submit a new order.
715    ///
716    /// # Errors
717    ///
718    /// Returns an error if:
719    /// - Credentials are missing.
720    /// - The request fails.
721    /// - Order validation fails.
722    /// - The order is rejected.
723    /// - The API returns an error.
724    #[allow(clippy::too_many_arguments)]
725    pub async fn submit_order(
726        &self,
727        product_type: BybitProductType,
728        instrument_id: InstrumentId,
729        client_order_id: ClientOrderId,
730        order_side: OrderSide,
731        order_type: OrderType,
732        quantity: Quantity,
733        time_in_force: TimeInForce,
734        price: Option<Price>,
735        reduce_only: bool,
736    ) -> anyhow::Result<OrderStatusReport> {
737        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
738        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
739
740        let bybit_side = match order_side {
741            OrderSide::Buy => BybitOrderSide::Buy,
742            OrderSide::Sell => BybitOrderSide::Sell,
743            _ => anyhow::bail!("Invalid order side: {order_side:?}"),
744        };
745
746        let bybit_order_type = match order_type {
747            OrderType::Market => BybitOrderType::Market,
748            OrderType::Limit => BybitOrderType::Limit,
749            _ => anyhow::bail!("Unsupported order type: {order_type:?}"),
750        };
751
752        let bybit_tif = match time_in_force {
753            TimeInForce::Gtc => BybitTimeInForce::Gtc,
754            TimeInForce::Ioc => BybitTimeInForce::Ioc,
755            TimeInForce::Fok => BybitTimeInForce::Fok,
756            _ => anyhow::bail!("Unsupported time in force: {time_in_force:?}"),
757        };
758
759        let mut order_entry = BybitBatchPlaceOrderEntryBuilder::default();
760        order_entry.symbol(bybit_symbol.raw_symbol().to_string());
761        order_entry.side(bybit_side);
762        order_entry.order_type(bybit_order_type);
763        order_entry.qty(quantity.to_string());
764        order_entry.time_in_force(Some(bybit_tif));
765        order_entry.order_link_id(client_order_id.to_string());
766
767        if let Some(price) = price {
768            order_entry.price(Some(price.to_string()));
769        }
770
771        if reduce_only {
772            order_entry.reduce_only(Some(true));
773        }
774
775        let order_entry = order_entry.build().map_err(|e| anyhow::anyhow!(e))?;
776
777        let mut params = BybitPlaceOrderParamsBuilder::default();
778        params.category(product_type);
779        params.order(order_entry);
780
781        let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
782
783        let body = serde_json::to_value(&params)?;
784        let response = self.http_place_order(&body).await?;
785
786        let order_id = response
787            .result
788            .order_id
789            .ok_or_else(|| anyhow::anyhow!("No order_id in response"))?;
790
791        // Query the order to get full details
792        let mut query_params = BybitOpenOrdersParamsBuilder::default();
793        query_params.category(product_type);
794        query_params.order_id(order_id.as_str().to_string());
795
796        let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
797        let path = Self::build_path("/v5/order/realtime", &query_params)?;
798        let order_response: BybitOpenOrdersResponse =
799            self.send_request(Method::GET, &path, None, true).await?;
800
801        let order = order_response
802            .result
803            .list
804            .into_iter()
805            .next()
806            .ok_or_else(|| anyhow::anyhow!("No order returned after submission"))?;
807
808        // Only bail on rejection if there are no fills
809        // If the order has fills (cum_exec_qty > 0), let the parser remap Rejected -> Canceled
810        if order.order_status == crate::common::enums::BybitOrderStatus::Rejected
811            && (order.cum_exec_qty.as_str() == "0" || order.cum_exec_qty.is_empty())
812        {
813            anyhow::bail!("Order rejected: {}", order.reject_reason);
814        }
815
816        let account_id = AccountId::new("BYBIT");
817        let ts_init = self.generate_ts_init();
818
819        parse_order_status_report(&order, &instrument, account_id, ts_init)
820    }
821
822    /// Cancel an order.
823    ///
824    /// # Errors
825    ///
826    /// Returns an error if:
827    /// - Credentials are missing.
828    /// - The request fails.
829    /// - The order doesn't exist.
830    /// - The API returns an error.
831    pub async fn cancel_order(
832        &self,
833        product_type: BybitProductType,
834        instrument_id: InstrumentId,
835        client_order_id: Option<ClientOrderId>,
836        venue_order_id: Option<VenueOrderId>,
837    ) -> anyhow::Result<OrderStatusReport> {
838        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
839        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
840
841        let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
842        cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
843
844        if let Some(venue_order_id) = venue_order_id {
845            cancel_entry.order_id(venue_order_id.to_string());
846        } else if let Some(client_order_id) = client_order_id {
847            cancel_entry.order_link_id(client_order_id.to_string());
848        } else {
849            anyhow::bail!("Either client_order_id or venue_order_id must be provided");
850        }
851
852        let cancel_entry = cancel_entry.build().map_err(|e| anyhow::anyhow!(e))?;
853
854        let mut params = BybitCancelOrderParamsBuilder::default();
855        params.category(product_type);
856        params.order(cancel_entry);
857
858        let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
859        let body = serde_json::to_vec(&params)?;
860
861        let response: BybitPlaceOrderResponse = self
862            .send_request(Method::POST, "/v5/order/cancel", Some(body), true)
863            .await?;
864
865        let order_id = response
866            .result
867            .order_id
868            .ok_or_else(|| anyhow::anyhow!("No order_id in cancel response"))?;
869
870        // Query the order to get full details after cancellation
871        let mut query_params = BybitOpenOrdersParamsBuilder::default();
872        query_params.category(product_type);
873        query_params.order_id(order_id.as_str().to_string());
874
875        let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
876        let path = Self::build_path("/v5/order/history", &query_params)?;
877        let order_response: BybitOrderHistoryResponse =
878            self.send_request(Method::GET, &path, None, true).await?;
879
880        let order = order_response
881            .result
882            .list
883            .into_iter()
884            .next()
885            .ok_or_else(|| anyhow::anyhow!("No order returned in cancel response"))?;
886
887        let account_id = AccountId::new("BYBIT");
888        let ts_init = self.generate_ts_init();
889
890        parse_order_status_report(&order, &instrument, account_id, ts_init)
891    }
892
893    /// Cancel all orders for an instrument.
894    ///
895    /// # Errors
896    ///
897    /// Returns an error if:
898    /// - Credentials are missing.
899    /// - The request fails.
900    /// - The API returns an error.
901    pub async fn cancel_all_orders(
902        &self,
903        product_type: BybitProductType,
904        instrument_id: InstrumentId,
905    ) -> anyhow::Result<Vec<OrderStatusReport>> {
906        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
907        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
908
909        let mut params = BybitCancelAllOrdersParamsBuilder::default();
910        params.category(product_type);
911        params.symbol(bybit_symbol.raw_symbol().to_string());
912
913        let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
914        let body = serde_json::to_vec(&params)?;
915
916        let _response: crate::common::models::BybitListResponse<serde_json::Value> = self
917            .send_request(Method::POST, "/v5/order/cancel-all", Some(body), true)
918            .await?;
919
920        // Query the order history to get all canceled orders
921        let mut query_params = BybitOrderHistoryParamsBuilder::default();
922        query_params.category(product_type);
923        query_params.symbol(bybit_symbol.raw_symbol().to_string());
924        query_params.limit(50);
925
926        let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
927        let path = Self::build_path("/v5/order/history", &query_params)?;
928        let order_response: BybitOrderHistoryResponse =
929            self.send_request(Method::GET, &path, None, true).await?;
930
931        let account_id = AccountId::new("BYBIT");
932        let ts_init = self.generate_ts_init();
933
934        let mut reports = Vec::new();
935        for order in order_response.result.list {
936            if let Ok(report) = parse_order_status_report(&order, &instrument, account_id, ts_init)
937            {
938                reports.push(report);
939            }
940        }
941
942        Ok(reports)
943    }
944
945    /// Modify an existing order.
946    ///
947    /// # Errors
948    ///
949    /// Returns an error if:
950    /// - Credentials are missing.
951    /// - The request fails.
952    /// - The order doesn't exist.
953    /// - The order is already closed.
954    /// - The API returns an error.
955    pub async fn modify_order(
956        &self,
957        product_type: BybitProductType,
958        instrument_id: InstrumentId,
959        client_order_id: Option<ClientOrderId>,
960        venue_order_id: Option<VenueOrderId>,
961        quantity: Option<Quantity>,
962        price: Option<Price>,
963    ) -> anyhow::Result<OrderStatusReport> {
964        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
965        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
966
967        let mut amend_entry = BybitBatchAmendOrderEntryBuilder::default();
968        amend_entry.symbol(bybit_symbol.raw_symbol().to_string());
969
970        if let Some(venue_order_id) = venue_order_id {
971            amend_entry.order_id(venue_order_id.to_string());
972        } else if let Some(client_order_id) = client_order_id {
973            amend_entry.order_link_id(client_order_id.to_string());
974        } else {
975            anyhow::bail!("Either client_order_id or venue_order_id must be provided");
976        }
977
978        if let Some(quantity) = quantity {
979            amend_entry.qty(Some(quantity.to_string()));
980        }
981
982        if let Some(price) = price {
983            amend_entry.price(Some(price.to_string()));
984        }
985
986        let amend_entry = amend_entry.build().map_err(|e| anyhow::anyhow!(e))?;
987
988        let mut params = BybitAmendOrderParamsBuilder::default();
989        params.category(product_type);
990        params.order(amend_entry);
991
992        let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
993        let body = serde_json::to_vec(&params)?;
994
995        let response: BybitPlaceOrderResponse = self
996            .send_request(Method::POST, "/v5/order/amend", Some(body), true)
997            .await?;
998
999        let order_id = response
1000            .result
1001            .order_id
1002            .ok_or_else(|| anyhow::anyhow!("No order_id in amend response"))?;
1003
1004        // Query the order to get full details after amendment
1005        let mut query_params = BybitOpenOrdersParamsBuilder::default();
1006        query_params.category(product_type);
1007        query_params.order_id(order_id.as_str().to_string());
1008
1009        let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
1010        let path = Self::build_path("/v5/order/realtime", &query_params)?;
1011        let order_response: BybitOpenOrdersResponse =
1012            self.send_request(Method::GET, &path, None, true).await?;
1013
1014        let order = order_response
1015            .result
1016            .list
1017            .into_iter()
1018            .next()
1019            .ok_or_else(|| anyhow::anyhow!("No order returned after modification"))?;
1020
1021        let account_id = AccountId::new("BYBIT");
1022        let ts_init = self.generate_ts_init();
1023
1024        parse_order_status_report(&order, &instrument, account_id, ts_init)
1025    }
1026
1027    /// Query a single order by client order ID or venue order ID.
1028    ///
1029    /// # Errors
1030    ///
1031    /// Returns an error if:
1032    /// - Credentials are missing.
1033    /// - The request fails.
1034    /// - The API returns an error.
1035    pub async fn query_order(
1036        &self,
1037        account_id: AccountId,
1038        product_type: BybitProductType,
1039        instrument_id: InstrumentId,
1040        client_order_id: Option<ClientOrderId>,
1041        venue_order_id: Option<VenueOrderId>,
1042    ) -> anyhow::Result<Option<OrderStatusReport>> {
1043        tracing::info!(
1044            "query_order called: instrument_id={}, client_order_id={:?}, venue_order_id={:?}",
1045            instrument_id,
1046            client_order_id,
1047            venue_order_id
1048        );
1049
1050        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
1051
1052        let mut params = BybitOpenOrdersParamsBuilder::default();
1053        params.category(product_type);
1054        // Use the raw Bybit symbol (e.g., "ETHUSDT") not the full instrument symbol
1055        params.symbol(bybit_symbol.raw_symbol().to_string());
1056
1057        if let Some(venue_order_id) = venue_order_id {
1058            params.order_id(venue_order_id.to_string());
1059        } else if let Some(client_order_id) = client_order_id {
1060            params.order_link_id(client_order_id.to_string());
1061        } else {
1062            anyhow::bail!("Either client_order_id or venue_order_id must be provided");
1063        }
1064
1065        let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
1066        let path = Self::build_path("/v5/order/realtime", &params)?;
1067
1068        let response: BybitOpenOrdersResponse =
1069            self.send_request(Method::GET, &path, None, true).await?;
1070
1071        if response.result.list.is_empty() {
1072            return Ok(None);
1073        }
1074
1075        let order = &response.result.list[0];
1076        let ts_init = self.generate_ts_init();
1077
1078        tracing::debug!(
1079            "Query order response: symbol={}, order_id={}, order_link_id={}",
1080            order.symbol.as_str(),
1081            order.order_id.as_str(),
1082            order.order_link_id.as_str()
1083        );
1084
1085        // Get instrument from cache with better error context
1086        let instrument = self
1087            .instrument_from_cache(&instrument_id.symbol)
1088            .map_err(|e| {
1089                tracing::error!(
1090                    "Instrument cache miss for symbol '{}': {}",
1091                    instrument_id.symbol.as_str(),
1092                    e
1093                );
1094                anyhow::anyhow!(
1095                    "Failed to query order {}: {}",
1096                    client_order_id
1097                        .as_ref()
1098                        .map(|id| id.to_string())
1099                        .or_else(|| venue_order_id.as_ref().map(|id| id.to_string()))
1100                        .unwrap_or_else(|| "unknown".to_string()),
1101                    e
1102                )
1103            })?;
1104
1105        tracing::debug!("Retrieved instrument from cache: id={}", instrument.id());
1106
1107        let report =
1108            parse_order_status_report(order, &instrument, account_id, ts_init).map_err(|e| {
1109                tracing::error!(
1110                    "Failed to parse order status report for {}: {}",
1111                    order.order_link_id.as_str(),
1112                    e
1113                );
1114                e
1115            })?;
1116
1117        tracing::debug!(
1118            "Successfully created OrderStatusReport for {}",
1119            order.order_link_id.as_str()
1120        );
1121
1122        Ok(Some(report))
1123    }
1124
1125    /// Request instruments for a given product type.
1126    ///
1127    /// # Errors
1128    ///
1129    /// Returns an error if:
1130    /// - The request fails.
1131    /// - Parsing fails.
1132    pub async fn request_instruments(
1133        &self,
1134        product_type: BybitProductType,
1135        symbol: Option<String>,
1136    ) -> anyhow::Result<Vec<InstrumentAny>> {
1137        let ts_init = self.generate_ts_init();
1138
1139        let params = BybitInstrumentsInfoParams {
1140            category: product_type,
1141            symbol,
1142            status: None,
1143            base_coin: None,
1144            limit: None,
1145            cursor: None,
1146        };
1147
1148        let mut instruments = Vec::new();
1149
1150        let default_fee_rate = |symbol: ustr::Ustr| BybitFeeRate {
1151            symbol,
1152            taker_fee_rate: "0.001".to_string(),
1153            maker_fee_rate: "0.001".to_string(),
1154            base_coin: None,
1155        };
1156
1157        match product_type {
1158            BybitProductType::Spot => {
1159                let response: BybitInstrumentSpotResponse =
1160                    self.http_get_instruments(&params).await?;
1161
1162                // Try to get fee rates, use defaults if credentials are missing
1163                let fee_map: HashMap<_, _> = {
1164                    let mut fee_params = BybitFeeRateParamsBuilder::default();
1165                    fee_params.category(product_type);
1166                    if let Ok(params) = fee_params.build() {
1167                        match self.http_get_fee_rate(&params).await {
1168                            Ok(fee_response) => fee_response
1169                                .result
1170                                .list
1171                                .into_iter()
1172                                .map(|f| (f.symbol, f))
1173                                .collect(),
1174                            Err(BybitHttpError::MissingCredentials) => {
1175                                tracing::warn!("Missing credentials for fee rates, using defaults");
1176                                HashMap::new()
1177                            }
1178                            Err(e) => return Err(e.into()),
1179                        }
1180                    } else {
1181                        HashMap::new()
1182                    }
1183                };
1184
1185                for definition in response.result.list {
1186                    let fee_rate = fee_map
1187                        .get(&definition.symbol)
1188                        .cloned()
1189                        .unwrap_or_else(|| default_fee_rate(definition.symbol));
1190                    if let Ok(instrument) =
1191                        parse_spot_instrument(&definition, &fee_rate, ts_init, ts_init)
1192                    {
1193                        instruments.push(instrument);
1194                    }
1195                }
1196            }
1197            BybitProductType::Linear => {
1198                let response: BybitInstrumentLinearResponse =
1199                    self.http_get_instruments(&params).await?;
1200
1201                // Try to get fee rates, use defaults if credentials are missing
1202                let fee_map: HashMap<_, _> = {
1203                    let mut fee_params = BybitFeeRateParamsBuilder::default();
1204                    fee_params.category(product_type);
1205                    if let Ok(params) = fee_params.build() {
1206                        match self.http_get_fee_rate(&params).await {
1207                            Ok(fee_response) => fee_response
1208                                .result
1209                                .list
1210                                .into_iter()
1211                                .map(|f| (f.symbol, f))
1212                                .collect(),
1213                            Err(BybitHttpError::MissingCredentials) => {
1214                                tracing::warn!("Missing credentials for fee rates, using defaults");
1215                                HashMap::new()
1216                            }
1217                            Err(e) => return Err(e.into()),
1218                        }
1219                    } else {
1220                        HashMap::new()
1221                    }
1222                };
1223
1224                for definition in response.result.list {
1225                    let fee_rate = fee_map
1226                        .get(&definition.symbol)
1227                        .cloned()
1228                        .unwrap_or_else(|| default_fee_rate(definition.symbol));
1229                    if let Ok(instrument) =
1230                        parse_linear_instrument(&definition, &fee_rate, ts_init, ts_init)
1231                    {
1232                        instruments.push(instrument);
1233                    }
1234                }
1235            }
1236            BybitProductType::Inverse => {
1237                let response: BybitInstrumentInverseResponse =
1238                    self.http_get_instruments(&params).await?;
1239
1240                // Try to get fee rates, use defaults if credentials are missing
1241                let fee_map: HashMap<_, _> = {
1242                    let mut fee_params = BybitFeeRateParamsBuilder::default();
1243                    fee_params.category(product_type);
1244                    if let Ok(params) = fee_params.build() {
1245                        match self.http_get_fee_rate(&params).await {
1246                            Ok(fee_response) => fee_response
1247                                .result
1248                                .list
1249                                .into_iter()
1250                                .map(|f| (f.symbol, f))
1251                                .collect(),
1252                            Err(BybitHttpError::MissingCredentials) => {
1253                                tracing::warn!("Missing credentials for fee rates, using defaults");
1254                                HashMap::new()
1255                            }
1256                            Err(e) => return Err(e.into()),
1257                        }
1258                    } else {
1259                        HashMap::new()
1260                    }
1261                };
1262
1263                for definition in response.result.list {
1264                    let fee_rate = fee_map
1265                        .get(&definition.symbol)
1266                        .cloned()
1267                        .unwrap_or_else(|| default_fee_rate(definition.symbol));
1268                    if let Ok(instrument) =
1269                        parse_inverse_instrument(&definition, &fee_rate, ts_init, ts_init)
1270                    {
1271                        instruments.push(instrument);
1272                    }
1273                }
1274            }
1275            BybitProductType::Option => {
1276                let response: BybitInstrumentOptionResponse =
1277                    self.http_get_instruments(&params).await?;
1278
1279                for definition in response.result.list {
1280                    if let Ok(instrument) = parse_option_instrument(&definition, ts_init, ts_init) {
1281                        instruments.push(instrument);
1282                    }
1283                }
1284            }
1285        }
1286
1287        // Add all instruments to cache
1288        for instrument in &instruments {
1289            self.add_instrument(instrument.clone());
1290        }
1291
1292        Ok(instruments)
1293    }
1294
1295    /// Request trade tick history for a given symbol.
1296    ///
1297    /// # Errors
1298    ///
1299    /// Returns an error if:
1300    /// - The instrument is not found in cache.
1301    /// - The request fails.
1302    /// - Parsing fails.
1303    ///
1304    /// # References
1305    ///
1306    /// <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
1307    pub async fn request_trades(
1308        &self,
1309        product_type: BybitProductType,
1310        instrument_id: InstrumentId,
1311        limit: Option<u32>,
1312    ) -> anyhow::Result<Vec<TradeTick>> {
1313        let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
1314        let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
1315
1316        let mut params_builder = BybitTradesParamsBuilder::default();
1317        params_builder.category(product_type);
1318        params_builder.symbol(bybit_symbol.raw_symbol().to_string());
1319        if let Some(limit_val) = limit {
1320            params_builder.limit(limit_val);
1321        }
1322
1323        let params = params_builder.build().map_err(|e| anyhow::anyhow!(e))?;
1324        let response = self.http_get_recent_trades(&params).await?;
1325
1326        let ts_init = self.generate_ts_init();
1327        let mut trades = Vec::new();
1328
1329        for trade in response.result.list {
1330            if let Ok(trade_tick) = parse_trade_tick(&trade, &instrument, ts_init) {
1331                trades.push(trade_tick);
1332            }
1333        }
1334
1335        Ok(trades)
1336    }
1337
1338    /// Request bar/kline history for a given symbol.
1339    ///
1340    /// # Errors
1341    ///
1342    /// Returns an error if:
1343    /// - The instrument is not found in cache.
1344    /// - The request fails.
1345    /// - Parsing fails.
1346    ///
1347    /// # References
1348    ///
1349    /// <https://bybit-exchange.github.io/docs/v5/market/kline>
1350    pub async fn request_bars(
1351        &self,
1352        product_type: BybitProductType,
1353        bar_type: BarType,
1354        start: Option<i64>,
1355        end: Option<i64>,
1356        limit: Option<u32>,
1357    ) -> anyhow::Result<Vec<Bar>> {
1358        let instrument = self.instrument_from_cache(&bar_type.instrument_id().symbol)?;
1359        let bybit_symbol = BybitSymbol::new(bar_type.instrument_id().symbol.as_str())?;
1360
1361        // Convert Nautilus BarAggregation to BybitKlineInterval
1362        let interval = match bar_type.spec().aggregation {
1363            BarAggregation::Minute => BybitKlineInterval::Minute1,
1364            BarAggregation::Hour => BybitKlineInterval::Hour1,
1365            BarAggregation::Day => BybitKlineInterval::Day1,
1366            _ => anyhow::bail!(
1367                "Unsupported bar aggregation: {:?}",
1368                bar_type.spec().aggregation
1369            ),
1370        };
1371
1372        let mut params_builder = BybitKlinesParamsBuilder::default();
1373        params_builder.category(product_type);
1374        params_builder.symbol(bybit_symbol.raw_symbol().to_string());
1375        params_builder.interval(interval);
1376
1377        if let Some(start_ts) = start {
1378            params_builder.start(start_ts);
1379        }
1380        if let Some(end_ts) = end {
1381            params_builder.end(end_ts);
1382        }
1383        if let Some(limit_val) = limit {
1384            params_builder.limit(limit_val);
1385        }
1386
1387        let params = params_builder.build().map_err(|e| anyhow::anyhow!(e))?;
1388        let response = self.http_get_klines(&params).await?;
1389
1390        let ts_init = self.generate_ts_init();
1391        let mut bars = Vec::new();
1392
1393        for kline in response.result.list {
1394            if let Ok(bar) = parse_kline_bar(&kline, &instrument, bar_type, false, ts_init) {
1395                bars.push(bar);
1396            }
1397        }
1398
1399        Ok(bars)
1400    }
1401
1402    /// Requests trading fee rates for the specified product type and optional filters.
1403    ///
1404    /// # Errors
1405    ///
1406    /// Returns an error if:
1407    /// - The request fails.
1408    /// - Parsing fails.
1409    ///
1410    /// # References
1411    ///
1412    /// <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
1413    pub async fn request_fee_rates(
1414        &self,
1415        product_type: BybitProductType,
1416        symbol: Option<String>,
1417        base_coin: Option<String>,
1418    ) -> anyhow::Result<Vec<BybitFeeRate>> {
1419        let params = BybitFeeRateParams {
1420            category: product_type,
1421            symbol,
1422            base_coin,
1423        };
1424
1425        let response = self.http_get_fee_rate(&params).await?;
1426        Ok(response.result.list)
1427    }
1428
1429    /// Requests the current account state for the specified account type.
1430    ///
1431    /// # Errors
1432    ///
1433    /// Returns an error if:
1434    /// - The request fails.
1435    /// - Parsing fails.
1436    ///
1437    /// # References
1438    ///
1439    /// <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
1440    pub async fn request_account_state(
1441        &self,
1442        account_type: BybitAccountType,
1443        account_id: AccountId,
1444    ) -> anyhow::Result<AccountState> {
1445        let params = BybitWalletBalanceParams {
1446            account_type,
1447            coin: None,
1448        };
1449
1450        let response = self.http_get_wallet_balance(&params).await?;
1451        let ts_init = self.generate_ts_init();
1452
1453        // Take the first wallet balance from the list
1454        let wallet_balance = response
1455            .result
1456            .list
1457            .first()
1458            .ok_or_else(|| anyhow::anyhow!("No wallet balance found in response"))?;
1459
1460        parse_account_state(wallet_balance, account_id, ts_init)
1461    }
1462
1463    /// Request multiple order status reports.
1464    ///
1465    /// # Errors
1466    ///
1467    /// Returns an error if:
1468    /// - Credentials are missing.
1469    /// - The request fails.
1470    /// - The API returns an error.
1471    pub async fn request_order_status_reports(
1472        &self,
1473        account_id: AccountId,
1474        product_type: BybitProductType,
1475        instrument_id: Option<InstrumentId>,
1476        open_only: bool,
1477        limit: Option<u32>,
1478    ) -> anyhow::Result<Vec<OrderStatusReport>> {
1479        // Extract symbol parameter from instrument_id if provided
1480        let symbol_param = if let Some(id) = instrument_id.as_ref() {
1481            let symbol_str = id.symbol.as_str();
1482            if symbol_str.is_empty() {
1483                None
1484            } else {
1485                Some(BybitSymbol::new(symbol_str)?.raw_symbol().to_string())
1486            }
1487        } else {
1488            None
1489        };
1490
1491        let params = if open_only {
1492            let mut p = BybitOpenOrdersParamsBuilder::default();
1493            p.category(product_type);
1494            if let Some(symbol) = symbol_param.clone() {
1495                p.symbol(symbol);
1496            }
1497            let params = p.build().map_err(|e| anyhow::anyhow!(e))?;
1498            let path = Self::build_path("/v5/order/realtime", &params)?;
1499            let response: BybitOpenOrdersResponse =
1500                self.send_request(Method::GET, &path, None, true).await?;
1501            response.result.list
1502        } else {
1503            let mut p = BybitOrderHistoryParamsBuilder::default();
1504            p.category(product_type);
1505            if let Some(symbol) = symbol_param {
1506                p.symbol(symbol);
1507            }
1508            if let Some(limit) = limit {
1509                p.limit(limit);
1510            }
1511            let params = p.build().map_err(|e| anyhow::anyhow!(e))?;
1512            let path = Self::build_path("/v5/order/history", &params)?;
1513            let response: BybitOrderHistoryResponse =
1514                self.send_request(Method::GET, &path, None, true).await?;
1515            response.result.list
1516        };
1517
1518        let ts_init = self.generate_ts_init();
1519
1520        let mut reports = Vec::new();
1521        for order in params {
1522            if let Some(ref instrument_id) = instrument_id {
1523                let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
1524                if let Ok(report) =
1525                    parse_order_status_report(&order, &instrument, account_id, ts_init)
1526                {
1527                    reports.push(report);
1528                }
1529            } else {
1530                // Try to get instrument from symbol
1531                // Bybit returns raw symbol (e.g. "ETHUSDT"), need to add product suffix for cache lookup
1532                if !order.symbol.is_empty() {
1533                    let symbol_with_product = Symbol::new(format!(
1534                        "{}{}",
1535                        order.symbol.as_str(),
1536                        product_type.suffix()
1537                    ));
1538                    if let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
1539                        && let Ok(report) =
1540                            parse_order_status_report(&order, &instrument, account_id, ts_init)
1541                    {
1542                        reports.push(report);
1543                    }
1544                }
1545            }
1546        }
1547
1548        Ok(reports)
1549    }
1550
1551    /// Fetches execution history (fills) for the account and returns a list of [`FillReport`]s.
1552    ///
1553    /// # Errors
1554    ///
1555    /// This function returns an error if:
1556    /// - Required instruments are not cached.
1557    /// - The instrument is not found in cache.
1558    /// - The request fails.
1559    /// - Parsing fails.
1560    ///
1561    /// # References
1562    ///
1563    /// <https://bybit-exchange.github.io/docs/v5/order/execution>
1564    pub async fn request_fill_reports(
1565        &self,
1566        account_id: AccountId,
1567        product_type: BybitProductType,
1568        instrument_id: Option<InstrumentId>,
1569        start: Option<i64>,
1570        end: Option<i64>,
1571        limit: Option<u32>,
1572    ) -> anyhow::Result<Vec<FillReport>> {
1573        // Build query parameters
1574        let symbol = if let Some(id) = instrument_id {
1575            let bybit_symbol = BybitSymbol::new(id.symbol.as_str())?;
1576            Some(bybit_symbol.raw_symbol().to_string())
1577        } else {
1578            None
1579        };
1580        let params = BybitTradeHistoryParams {
1581            category: product_type,
1582            symbol,
1583            base_coin: None,
1584            order_id: None,
1585            order_link_id: None,
1586            start_time: start,
1587            end_time: end,
1588            exec_type: None,
1589            limit,
1590            cursor: None,
1591        };
1592
1593        let response = self.http_get_trade_history(&params).await?;
1594        let ts_init = self.generate_ts_init();
1595        let mut reports = Vec::new();
1596
1597        for execution in response.result.list {
1598            // Get instrument for this execution
1599            // Bybit returns raw symbol (e.g. "ETHUSDT"), need to add product suffix for cache lookup
1600            // TODO: Extract this to a helper
1601            let symbol_with_product = Symbol::new(format!(
1602                "{}{}",
1603                execution.symbol.as_str(),
1604                product_type.suffix()
1605            ));
1606            let instrument = self.instrument_from_cache(&symbol_with_product)?;
1607
1608            if let Ok(report) = parse_fill_report(&execution, account_id, &instrument, ts_init) {
1609                reports.push(report);
1610            }
1611        }
1612
1613        Ok(reports)
1614    }
1615
1616    /// Fetches position information for the account and returns a list of [`PositionStatusReport`]s.
1617    ///
1618    /// # Errors
1619    ///
1620    /// This function returns an error if:
1621    /// - Required instruments are not cached.
1622    /// - The instrument is not found in cache.
1623    /// - The request fails.
1624    /// - Parsing fails.
1625    ///
1626    /// # References
1627    ///
1628    /// <https://bybit-exchange.github.io/docs/v5/position/position-info>
1629    pub async fn request_position_status_reports(
1630        &self,
1631        account_id: AccountId,
1632        product_type: BybitProductType,
1633        instrument_id: Option<InstrumentId>,
1634    ) -> anyhow::Result<Vec<PositionStatusReport>> {
1635        let ts_init = self.generate_ts_init();
1636        let mut reports = Vec::new();
1637
1638        // Build query parameters based on whether a specific instrument is requested
1639        let symbol = if let Some(id) = instrument_id {
1640            let symbol_str = id.symbol.as_str();
1641            if symbol_str.is_empty() {
1642                anyhow::bail!("InstrumentId symbol is empty");
1643            }
1644            let bybit_symbol = BybitSymbol::new(symbol_str)?;
1645            Some(bybit_symbol.raw_symbol().to_string())
1646        } else {
1647            None
1648        };
1649
1650        // For LINEAR category, the API requires either symbol OR settleCoin
1651        // When querying all positions (no symbol), we must iterate through settle coins
1652        if product_type == BybitProductType::Linear && symbol.is_none() {
1653            // Query positions for each known settle coin
1654            for settle_coin in ["USDT", "USDC"] {
1655                let params = BybitPositionListParams {
1656                    category: product_type,
1657                    symbol: None,
1658                    base_coin: None,
1659                    settle_coin: Some(settle_coin.to_string()),
1660                    limit: None,
1661                    cursor: None,
1662                };
1663
1664                let response = self.http_get_positions(&params).await?;
1665
1666                for position in response.result.list {
1667                    if position.symbol.is_empty() {
1668                        continue;
1669                    }
1670
1671                    let symbol_with_product = Symbol::new(format!(
1672                        "{}{}",
1673                        position.symbol.as_str(),
1674                        product_type.suffix()
1675                    ));
1676
1677                    if let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
1678                        && let Ok(report) = parse_position_status_report(
1679                            &position,
1680                            account_id,
1681                            &instrument,
1682                            ts_init,
1683                        )
1684                    {
1685                        reports.push(report);
1686                    }
1687                }
1688            }
1689        } else {
1690            // For other product types or when a specific symbol is requested
1691            let params = BybitPositionListParams {
1692                category: product_type,
1693                symbol,
1694                base_coin: None,
1695                settle_coin: None,
1696                limit: None,
1697                cursor: None,
1698            };
1699
1700            let response = self.http_get_positions(&params).await?;
1701
1702            for position in response.result.list {
1703                if position.symbol.is_empty() {
1704                    continue;
1705                }
1706
1707                let symbol_with_product = Symbol::new(format!(
1708                    "{}{}",
1709                    position.symbol.as_str(),
1710                    product_type.suffix()
1711                ));
1712
1713                if let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
1714                    && let Ok(report) =
1715                        parse_position_status_report(&position, account_id, &instrument, ts_init)
1716                {
1717                    reports.push(report);
1718                }
1719            }
1720        }
1721
1722        Ok(reports)
1723    }
1724}
1725
1726////////////////////////////////////////////////////////////////////////////////
1727// Outer Client
1728////////////////////////////////////////////////////////////////////////////////
1729
1730/// Provides a HTTP client for connecting to the [Bybit](https://bybit.com) REST API.
1731#[derive(Clone)]
1732#[cfg_attr(
1733    feature = "python",
1734    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
1735)]
1736pub struct BybitHttpClient {
1737    pub(crate) inner: Arc<BybitHttpInnerClient>,
1738}
1739
1740impl Default for BybitHttpClient {
1741    fn default() -> Self {
1742        Self::new(None, Some(60), None, None, None)
1743            .expect("Failed to create default BybitHttpClient")
1744    }
1745}
1746
1747impl Debug for BybitHttpClient {
1748    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1749        f.debug_struct("BybitHttpClient")
1750            .field("inner", &self.inner)
1751            .finish()
1752    }
1753}
1754
1755impl BybitHttpClient {
1756    /// Creates a new [`BybitHttpClient`] using the default Bybit HTTP URL.
1757    ///
1758    /// # Errors
1759    ///
1760    /// Returns an error if the retry manager cannot be created.
1761    #[allow(clippy::too_many_arguments)]
1762    pub fn new(
1763        base_url: Option<String>,
1764        timeout_secs: Option<u64>,
1765        max_retries: Option<u32>,
1766        retry_delay_ms: Option<u64>,
1767        retry_delay_max_ms: Option<u64>,
1768    ) -> Result<Self, BybitHttpError> {
1769        Ok(Self {
1770            inner: Arc::new(BybitHttpInnerClient::new(
1771                base_url,
1772                timeout_secs,
1773                max_retries,
1774                retry_delay_ms,
1775                retry_delay_max_ms,
1776            )?),
1777        })
1778    }
1779
1780    /// Creates a new [`BybitHttpClient`] configured with credentials.
1781    ///
1782    /// # Errors
1783    ///
1784    /// Returns an error if the retry manager cannot be created.
1785    #[allow(clippy::too_many_arguments)]
1786    pub fn with_credentials(
1787        api_key: String,
1788        api_secret: String,
1789        base_url: Option<String>,
1790        timeout_secs: Option<u64>,
1791        max_retries: Option<u32>,
1792        retry_delay_ms: Option<u64>,
1793        retry_delay_max_ms: Option<u64>,
1794    ) -> Result<Self, BybitHttpError> {
1795        Ok(Self {
1796            inner: Arc::new(BybitHttpInnerClient::with_credentials(
1797                api_key,
1798                api_secret,
1799                base_url,
1800                timeout_secs,
1801                max_retries,
1802                retry_delay_ms,
1803                retry_delay_max_ms,
1804            )?),
1805        })
1806    }
1807
1808    /// Returns the base URL used for requests.
1809    #[must_use]
1810    pub fn base_url(&self) -> &str {
1811        self.inner.base_url()
1812    }
1813
1814    /// Returns the configured receive window in milliseconds.
1815    #[must_use]
1816    pub fn recv_window_ms(&self) -> u64 {
1817        self.inner.recv_window_ms()
1818    }
1819
1820    /// Returns the API credential if configured.
1821    #[must_use]
1822    pub fn credential(&self) -> Option<&Credential> {
1823        self.inner.credential()
1824    }
1825
1826    /// Cancel all pending HTTP requests.
1827    pub fn cancel_all_requests(&self) {
1828        self.inner.cancel_all_requests();
1829    }
1830
1831    /// Get the cancellation token for this client.
1832    pub fn cancellation_token(&self) -> &CancellationToken {
1833        self.inner.cancellation_token()
1834    }
1835
1836    // =========================================================================
1837    // Low-level HTTP API methods
1838    // =========================================================================
1839
1840    /// Fetches the current server time from Bybit.
1841    ///
1842    /// # Errors
1843    ///
1844    /// Returns an error if:
1845    /// - The request fails.
1846    /// - The response cannot be parsed.
1847    ///
1848    /// # References
1849    ///
1850    /// - <https://bybit-exchange.github.io/docs/v5/market/time>
1851    pub async fn http_get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
1852        self.inner.http_get_server_time().await
1853    }
1854
1855    /// Fetches instrument information from Bybit for a given product category.
1856    ///
1857    /// # Errors
1858    ///
1859    /// Returns an error if:
1860    /// - The request fails.
1861    /// - The response cannot be parsed.
1862    ///
1863    /// # References
1864    ///
1865    /// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
1866    pub async fn http_get_instruments<T: DeserializeOwned>(
1867        &self,
1868        params: &BybitInstrumentsInfoParams,
1869    ) -> Result<T, BybitHttpError> {
1870        self.inner.http_get_instruments(params).await
1871    }
1872
1873    /// Fetches spot instrument information from Bybit.
1874    ///
1875    /// # Errors
1876    ///
1877    /// Returns an error if:
1878    /// - The request fails.
1879    /// - The response cannot be parsed.
1880    ///
1881    /// # References
1882    ///
1883    /// - <https://bybit-exchange.github.io/docs/v5/market/instruments-info>
1884    pub async fn http_get_instruments_spot(
1885        &self,
1886        params: &BybitInstrumentsInfoParams,
1887    ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
1888        self.inner.http_get_instruments_spot(params).await
1889    }
1890
1891    /// Fetches linear instrument information from Bybit.
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/market/instruments-info>
1902    pub async fn http_get_instruments_linear(
1903        &self,
1904        params: &BybitInstrumentsInfoParams,
1905    ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
1906        self.inner.http_get_instruments_linear(params).await
1907    }
1908
1909    /// Fetches inverse instrument information from Bybit.
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/market/instruments-info>
1920    pub async fn http_get_instruments_inverse(
1921        &self,
1922        params: &BybitInstrumentsInfoParams,
1923    ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
1924        self.inner.http_get_instruments_inverse(params).await
1925    }
1926
1927    /// Fetches option instrument information from Bybit.
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/market/instruments-info>
1938    pub async fn http_get_instruments_option(
1939        &self,
1940        params: &BybitInstrumentsInfoParams,
1941    ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
1942        self.inner.http_get_instruments_option(params).await
1943    }
1944
1945    /// Fetches kline/candlestick data from Bybit.
1946    ///
1947    /// # Errors
1948    ///
1949    /// Returns an error if:
1950    /// - The request fails.
1951    /// - The response cannot be parsed.
1952    ///
1953    /// # References
1954    ///
1955    /// - <https://bybit-exchange.github.io/docs/v5/market/kline>
1956    pub async fn http_get_klines(
1957        &self,
1958        params: &BybitKlinesParams,
1959    ) -> Result<BybitKlinesResponse, BybitHttpError> {
1960        self.inner.http_get_klines(params).await
1961    }
1962
1963    /// Fetches recent trades from Bybit.
1964    ///
1965    /// # Errors
1966    ///
1967    /// Returns an error if:
1968    /// - The request fails.
1969    /// - The response cannot be parsed.
1970    ///
1971    /// # References
1972    ///
1973    /// - <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
1974    pub async fn http_get_recent_trades(
1975        &self,
1976        params: &BybitTradesParams,
1977    ) -> Result<BybitTradesResponse, BybitHttpError> {
1978        self.inner.http_get_recent_trades(params).await
1979    }
1980
1981    /// Fetches open orders (requires authentication).
1982    ///
1983    /// # Errors
1984    ///
1985    /// Returns an error if:
1986    /// - The request fails.
1987    /// - The response cannot be parsed.
1988    ///
1989    /// # References
1990    ///
1991    /// - <https://bybit-exchange.github.io/docs/v5/order/open-order>
1992    pub async fn http_get_open_orders(
1993        &self,
1994        category: BybitProductType,
1995        symbol: Option<&str>,
1996    ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
1997        self.inner.http_get_open_orders(category, symbol).await
1998    }
1999
2000    /// Places a new order (requires authentication).
2001    ///
2002    /// # Errors
2003    ///
2004    /// Returns an error if:
2005    /// - The request fails.
2006    /// - The response cannot be parsed.
2007    ///
2008    /// # References
2009    ///
2010    /// - <https://bybit-exchange.github.io/docs/v5/order/create-order>
2011    pub async fn http_place_order(
2012        &self,
2013        request: &serde_json::Value,
2014    ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
2015        self.inner.http_place_order(request).await
2016    }
2017
2018    // =========================================================================
2019    // High-level methods using Nautilus domain objects
2020    // =========================================================================
2021
2022    /// Add an instrument to the cache.
2023    pub fn add_instrument(&self, instrument: InstrumentAny) {
2024        self.inner.add_instrument(instrument);
2025    }
2026
2027    /// Submit a new order.
2028    ///
2029    /// # Errors
2030    ///
2031    /// Returns an error if:
2032    /// - Credentials are missing.
2033    /// - The request fails.
2034    /// - Order validation fails.
2035    /// - The order is rejected.
2036    /// - The API returns an error.
2037    #[allow(clippy::too_many_arguments)]
2038    pub async fn submit_order(
2039        &self,
2040        product_type: BybitProductType,
2041        instrument_id: InstrumentId,
2042        client_order_id: ClientOrderId,
2043        order_side: OrderSide,
2044        order_type: OrderType,
2045        quantity: Quantity,
2046        time_in_force: TimeInForce,
2047        price: Option<Price>,
2048        reduce_only: bool,
2049    ) -> anyhow::Result<OrderStatusReport> {
2050        self.inner
2051            .submit_order(
2052                product_type,
2053                instrument_id,
2054                client_order_id,
2055                order_side,
2056                order_type,
2057                quantity,
2058                time_in_force,
2059                price,
2060                reduce_only,
2061            )
2062            .await
2063    }
2064
2065    /// Modify an existing order.
2066    ///
2067    /// # Errors
2068    ///
2069    /// Returns an error if:
2070    /// - Credentials are missing.
2071    /// - The request fails.
2072    /// - The order doesn't exist.
2073    /// - The order is already closed.
2074    /// - The API returns an error.
2075    pub async fn modify_order(
2076        &self,
2077        product_type: BybitProductType,
2078        instrument_id: InstrumentId,
2079        client_order_id: Option<ClientOrderId>,
2080        venue_order_id: Option<VenueOrderId>,
2081        quantity: Option<Quantity>,
2082        price: Option<Price>,
2083    ) -> anyhow::Result<OrderStatusReport> {
2084        self.inner
2085            .modify_order(
2086                product_type,
2087                instrument_id,
2088                client_order_id,
2089                venue_order_id,
2090                quantity,
2091                price,
2092            )
2093            .await
2094    }
2095
2096    /// Cancel an order.
2097    ///
2098    /// # Errors
2099    ///
2100    /// Returns an error if:
2101    /// - Credentials are missing.
2102    /// - The request fails.
2103    /// - The order doesn't exist.
2104    /// - The API returns an error.
2105    pub async fn cancel_order(
2106        &self,
2107        product_type: BybitProductType,
2108        instrument_id: InstrumentId,
2109        client_order_id: Option<ClientOrderId>,
2110        venue_order_id: Option<VenueOrderId>,
2111    ) -> anyhow::Result<OrderStatusReport> {
2112        self.inner
2113            .cancel_order(product_type, instrument_id, client_order_id, venue_order_id)
2114            .await
2115    }
2116
2117    /// Cancel all orders for an instrument.
2118    ///
2119    /// # Errors
2120    ///
2121    /// Returns an error if:
2122    /// - Credentials are missing.
2123    /// - The request fails.
2124    /// - The API returns an error.
2125    pub async fn cancel_all_orders(
2126        &self,
2127        product_type: BybitProductType,
2128        instrument_id: InstrumentId,
2129    ) -> anyhow::Result<Vec<OrderStatusReport>> {
2130        self.inner
2131            .cancel_all_orders(product_type, instrument_id)
2132            .await
2133    }
2134
2135    /// Query a single order by client order ID or venue order ID.
2136    ///
2137    /// # Errors
2138    ///
2139    /// Returns an error if:
2140    /// - Credentials are missing.
2141    /// - The request fails.
2142    /// - The API returns an error.
2143    pub async fn query_order(
2144        &self,
2145        account_id: AccountId,
2146        product_type: BybitProductType,
2147        instrument_id: InstrumentId,
2148        client_order_id: Option<ClientOrderId>,
2149        venue_order_id: Option<VenueOrderId>,
2150    ) -> anyhow::Result<Option<OrderStatusReport>> {
2151        self.inner
2152            .query_order(
2153                account_id,
2154                product_type,
2155                instrument_id,
2156                client_order_id,
2157                venue_order_id,
2158            )
2159            .await
2160    }
2161
2162    /// Request multiple order status reports.
2163    ///
2164    /// # Errors
2165    ///
2166    /// Returns an error if:
2167    /// - Credentials are missing.
2168    /// - The request fails.
2169    /// - The API returns an error.
2170    pub async fn request_order_status_reports(
2171        &self,
2172        account_id: AccountId,
2173        product_type: BybitProductType,
2174        instrument_id: Option<InstrumentId>,
2175        open_only: bool,
2176        limit: Option<u32>,
2177    ) -> anyhow::Result<Vec<OrderStatusReport>> {
2178        self.inner
2179            .request_order_status_reports(account_id, product_type, instrument_id, open_only, limit)
2180            .await
2181    }
2182
2183    /// Request instruments for a given product type.
2184    ///
2185    /// # Errors
2186    ///
2187    /// Returns an error if:
2188    /// - The request fails.
2189    /// - Parsing fails.
2190    pub async fn request_instruments(
2191        &self,
2192        product_type: BybitProductType,
2193        symbol: Option<String>,
2194    ) -> anyhow::Result<Vec<InstrumentAny>> {
2195        self.inner.request_instruments(product_type, symbol).await
2196    }
2197
2198    /// Request trade tick history for a given symbol.
2199    ///
2200    /// # Errors
2201    ///
2202    /// Returns an error if:
2203    /// - The instrument is not found in cache.
2204    /// - The request fails.
2205    /// - Parsing fails.
2206    ///
2207    /// # References
2208    ///
2209    /// <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
2210    pub async fn request_trades(
2211        &self,
2212        product_type: BybitProductType,
2213        instrument_id: InstrumentId,
2214        limit: Option<u32>,
2215    ) -> anyhow::Result<Vec<TradeTick>> {
2216        self.inner
2217            .request_trades(product_type, instrument_id, limit)
2218            .await
2219    }
2220
2221    /// Request bar/kline history for a given symbol.
2222    ///
2223    /// # Errors
2224    ///
2225    /// Returns an error if:
2226    /// - The instrument is not found in cache.
2227    /// - The request fails.
2228    /// - Parsing fails.
2229    ///
2230    /// # References
2231    ///
2232    /// <https://bybit-exchange.github.io/docs/v5/market/kline>
2233    pub async fn request_bars(
2234        &self,
2235        product_type: BybitProductType,
2236        bar_type: BarType,
2237        start: Option<i64>,
2238        end: Option<i64>,
2239        limit: Option<u32>,
2240    ) -> anyhow::Result<Vec<Bar>> {
2241        self.inner
2242            .request_bars(product_type, bar_type, start, end, limit)
2243            .await
2244    }
2245
2246    /// Fetches execution history (fills) for the account.
2247    ///
2248    /// # Errors
2249    ///
2250    /// This function returns an error if:
2251    /// - Required instruments are not cached.
2252    /// - The instrument is not found in cache.
2253    /// - The request fails.
2254    /// - Parsing fails.
2255    ///
2256    /// # References
2257    ///
2258    /// <https://bybit-exchange.github.io/docs/v5/order/execution>
2259    pub async fn request_fill_reports(
2260        &self,
2261        account_id: AccountId,
2262        product_type: BybitProductType,
2263        instrument_id: Option<InstrumentId>,
2264        start: Option<i64>,
2265        end: Option<i64>,
2266        limit: Option<u32>,
2267    ) -> anyhow::Result<Vec<FillReport>> {
2268        self.inner
2269            .request_fill_reports(account_id, product_type, instrument_id, start, end, limit)
2270            .await
2271    }
2272
2273    /// Fetches position information for the account.
2274    ///
2275    /// # Errors
2276    ///
2277    /// This function returns an error if:
2278    /// - Required instruments are not cached.
2279    /// - The instrument is not found in cache.
2280    /// - The request fails.
2281    /// - Parsing fails.
2282    ///
2283    /// # References
2284    ///
2285    /// <https://bybit-exchange.github.io/docs/v5/position/position-info>
2286    pub async fn request_position_status_reports(
2287        &self,
2288        account_id: AccountId,
2289        product_type: BybitProductType,
2290        instrument_id: Option<InstrumentId>,
2291    ) -> anyhow::Result<Vec<PositionStatusReport>> {
2292        self.inner
2293            .request_position_status_reports(account_id, product_type, instrument_id)
2294            .await
2295    }
2296
2297    /// Requests the current account state for the specified account type.
2298    ///
2299    /// # Errors
2300    ///
2301    /// Returns an error if:
2302    /// - The request fails.
2303    /// - Parsing fails.
2304    ///
2305    /// # References
2306    ///
2307    /// <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
2308    pub async fn request_account_state(
2309        &self,
2310        account_type: crate::common::enums::BybitAccountType,
2311        account_id: AccountId,
2312    ) -> anyhow::Result<AccountState> {
2313        self.inner
2314            .request_account_state(account_type, account_id)
2315            .await
2316    }
2317
2318    /// Requests trading fee rates for the specified product type and optional filters.
2319    ///
2320    /// # Errors
2321    ///
2322    /// Returns an error if:
2323    /// - The request fails.
2324    /// - Parsing fails.
2325    ///
2326    /// # References
2327    ///
2328    /// <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
2329    pub async fn request_fee_rates(
2330        &self,
2331        product_type: BybitProductType,
2332        symbol: Option<String>,
2333        base_coin: Option<String>,
2334    ) -> anyhow::Result<Vec<BybitFeeRate>> {
2335        self.inner
2336            .request_fee_rates(product_type, symbol, base_coin)
2337            .await
2338    }
2339}
2340
2341////////////////////////////////////////////////////////////////////////////////
2342// Tests
2343////////////////////////////////////////////////////////////////////////////////
2344
2345#[cfg(test)]
2346mod tests {
2347    use rstest::rstest;
2348
2349    use super::*;
2350
2351    #[rstest]
2352    fn test_client_creation() {
2353        let client = BybitHttpClient::new(None, Some(60), None, None, None);
2354        assert!(client.is_ok());
2355
2356        let client = client.unwrap();
2357        assert!(client.base_url().contains("bybit.com"));
2358        assert!(client.credential().is_none());
2359    }
2360
2361    #[rstest]
2362    fn test_client_with_credentials() {
2363        let client = BybitHttpClient::with_credentials(
2364            "test_key".to_string(),
2365            "test_secret".to_string(),
2366            Some("https://api-testnet.bybit.com".to_string()),
2367            Some(60),
2368            None,
2369            None,
2370            None,
2371        );
2372        assert!(client.is_ok());
2373
2374        let client = client.unwrap();
2375        assert!(client.credential().is_some());
2376    }
2377
2378    #[rstest]
2379    fn test_build_path_with_params() {
2380        #[derive(Serialize)]
2381        struct TestParams {
2382            category: String,
2383            symbol: String,
2384        }
2385
2386        let params = TestParams {
2387            category: "linear".to_string(),
2388            symbol: "BTCUSDT".to_string(),
2389        };
2390
2391        let path = BybitHttpInnerClient::build_path("/v5/market/test", &params);
2392        assert!(path.is_ok());
2393        assert!(path.unwrap().contains("category=linear"));
2394    }
2395
2396    #[rstest]
2397    fn test_build_path_without_params() {
2398        let params = ();
2399        let path = BybitHttpInnerClient::build_path("/v5/market/time", &params);
2400        assert!(path.is_ok());
2401        assert_eq!(path.unwrap(), "/v5/market/time");
2402    }
2403}