ccxt_exchanges/bybit/
rest.rs

1//! Bybit REST API implementation.
2//!
3//! Implements all REST API endpoint operations for the Bybit exchange.
4
5use super::{Bybit, BybitAuth, error, parser};
6use ccxt_core::{
7    Error, ParseError, Result,
8    types::{Balance, Market, OHLCV, Order, OrderBook, OrderSide, OrderType, Ticker, Trade},
9};
10use reqwest::header::{HeaderMap, HeaderValue};
11use serde_json::Value;
12use std::collections::HashMap;
13use tracing::{debug, info, warn};
14
15impl Bybit {
16    // ============================================================================
17    // Helper Methods
18    // ============================================================================
19
20    /// Get the current timestamp in milliseconds.
21    fn get_timestamp(&self) -> String {
22        chrono::Utc::now().timestamp_millis().to_string()
23    }
24
25    /// Get the authentication instance if credentials are configured.
26    fn get_auth(&self) -> Result<BybitAuth> {
27        let config = &self.base().config;
28
29        let api_key = config
30            .api_key
31            .as_ref()
32            .ok_or_else(|| Error::authentication("API key is required"))?;
33        let secret = config
34            .secret
35            .as_ref()
36            .ok_or_else(|| Error::authentication("API secret is required"))?;
37
38        Ok(BybitAuth::new(api_key.clone(), secret.clone()))
39    }
40
41    /// Check that required credentials are configured.
42    pub fn check_required_credentials(&self) -> Result<()> {
43        self.base().check_required_credentials()
44    }
45
46    /// Build the API path for Bybit V5 API.
47    fn build_api_path(&self, endpoint: &str) -> String {
48        format!("/v5{}", endpoint)
49    }
50
51    /// Get the category for API requests based on account type.
52    fn get_category(&self) -> &str {
53        match self.options().account_type.as_str() {
54            "SPOT" => "spot",
55            "CONTRACT" | "LINEAR" => "linear",
56            "INVERSE" => "inverse",
57            "OPTION" => "option",
58            _ => "spot",
59        }
60    }
61
62    /// Make a public API request (no authentication required).
63    async fn public_request(
64        &self,
65        method: &str,
66        path: &str,
67        params: Option<&HashMap<String, String>>,
68    ) -> Result<Value> {
69        let urls = self.urls();
70        let mut url = format!("{}{}", urls.rest, path);
71
72        if let Some(p) = params {
73            if !p.is_empty() {
74                let query: Vec<String> = p
75                    .iter()
76                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
77                    .collect();
78                url = format!("{}?{}", url, query.join("&"));
79            }
80        }
81
82        debug!("Bybit public request: {} {}", method, url);
83
84        let response = match method.to_uppercase().as_str() {
85            "GET" => self.base().http_client.get(&url, None).await?,
86            "POST" => self.base().http_client.post(&url, None, None).await?,
87            _ => {
88                return Err(Error::invalid_request(format!(
89                    "Unsupported HTTP method: {}",
90                    method
91                )));
92            }
93        };
94
95        // Check for Bybit error response
96        if error::is_error_response(&response) {
97            return Err(error::parse_error(&response));
98        }
99
100        Ok(response)
101    }
102
103    /// Make a private API request (authentication required).
104    async fn private_request(
105        &self,
106        method: &str,
107        path: &str,
108        params: Option<&HashMap<String, String>>,
109        body: Option<&Value>,
110    ) -> Result<Value> {
111        self.check_required_credentials()?;
112
113        let auth = self.get_auth()?;
114        let urls = self.urls();
115        let timestamp = self.get_timestamp();
116        let recv_window = self.options().recv_window;
117
118        // Build query string for GET requests
119        let query_string = if let Some(p) = params {
120            if !p.is_empty() {
121                let query: Vec<String> = p
122                    .iter()
123                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
124                    .collect();
125                query.join("&")
126            } else {
127                String::new()
128            }
129        } else {
130            String::new()
131        };
132
133        // Build body string for POST requests
134        let body_string = body
135            .map(|b| serde_json::to_string(b).unwrap_or_default())
136            .unwrap_or_default();
137
138        // Sign the request - Bybit uses query string for GET, body for POST
139        let sign_params = if method.to_uppercase() == "GET" {
140            &query_string
141        } else {
142            &body_string
143        };
144        let signature = auth.sign(&timestamp, recv_window, sign_params);
145
146        // Build headers
147        let mut headers = HeaderMap::new();
148        auth.add_auth_headers(&mut headers, &timestamp, &signature, recv_window);
149        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
150
151        let url = if query_string.is_empty() {
152            format!("{}{}", urls.rest, path)
153        } else {
154            format!("{}{}?{}", urls.rest, path, query_string)
155        };
156        debug!("Bybit private request: {} {}", method, url);
157
158        let response = match method.to_uppercase().as_str() {
159            "GET" => self.base().http_client.get(&url, Some(headers)).await?,
160            "POST" => {
161                let body_value = body.cloned();
162                self.base()
163                    .http_client
164                    .post(&url, Some(headers), body_value)
165                    .await?
166            }
167            "DELETE" => {
168                self.base()
169                    .http_client
170                    .delete(&url, Some(headers), None)
171                    .await?
172            }
173            _ => {
174                return Err(Error::invalid_request(format!(
175                    "Unsupported HTTP method: {}",
176                    method
177                )));
178            }
179        };
180
181        // Check for Bybit error response
182        if error::is_error_response(&response) {
183            return Err(error::parse_error(&response));
184        }
185
186        Ok(response)
187    }
188
189    // ============================================================================
190    // Public API Methods - Market Data
191    // ============================================================================
192
193    /// Fetch all trading markets.
194    ///
195    /// # Returns
196    ///
197    /// Returns a vector of [`Market`] structures containing market information.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if the API request fails or response parsing fails.
202    ///
203    /// # Example
204    ///
205    /// ```no_run
206    /// # use ccxt_exchanges::bybit::Bybit;
207    /// # async fn example() -> ccxt_core::Result<()> {
208    /// let bybit = Bybit::builder().build()?;
209    /// let markets = bybit.fetch_markets().await?;
210    /// println!("Found {} markets", markets.len());
211    /// # Ok(())
212    /// # }
213    /// ```
214    pub async fn fetch_markets(&self) -> Result<Vec<Market>> {
215        let path = self.build_api_path("/market/instruments-info");
216        let mut params = HashMap::new();
217        params.insert("category".to_string(), self.get_category().to_string());
218
219        let response = self.public_request("GET", &path, Some(&params)).await?;
220
221        let result = response
222            .get("result")
223            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
224
225        let list = result
226            .get("list")
227            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
228
229        let instruments = list.as_array().ok_or_else(|| {
230            Error::from(ParseError::invalid_format(
231                "list",
232                "Expected array of instruments",
233            ))
234        })?;
235
236        let mut markets = Vec::new();
237        for instrument in instruments {
238            match parser::parse_market(instrument) {
239                Ok(market) => markets.push(market),
240                Err(e) => {
241                    warn!(error = %e, "Failed to parse market");
242                }
243            }
244        }
245
246        // Cache the markets and preserve ownership for the caller
247        let markets = self.base().set_markets(markets, None).await?;
248
249        info!("Loaded {} markets for Bybit", markets.len());
250        Ok(markets)
251    }
252
253    /// Load and cache market data.
254    ///
255    /// If markets are already loaded and `reload` is false, returns cached data.
256    ///
257    /// # Arguments
258    ///
259    /// * `reload` - Whether to force reload market data from the API.
260    ///
261    /// # Returns
262    ///
263    /// Returns a `HashMap` containing all market data, keyed by symbol (e.g., "BTC/USDT").
264    pub async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
265        {
266            let cache = self.base().market_cache.read().await;
267            if cache.loaded && !reload {
268                debug!(
269                    "Returning cached markets for Bybit ({} markets)",
270                    cache.markets.len()
271                );
272                return Ok(cache.markets.clone());
273            }
274        }
275
276        info!("Loading markets for Bybit (reload: {})", reload);
277        let _markets = self.fetch_markets().await?;
278
279        let cache = self.base().market_cache.read().await;
280        Ok(cache.markets.clone())
281    }
282
283    /// Fetch ticker for a single trading pair.
284    ///
285    /// # Arguments
286    ///
287    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
288    ///
289    /// # Returns
290    ///
291    /// Returns [`Ticker`] data for the specified symbol.
292    pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
293        let market = self.base().market(symbol).await?;
294
295        let path = self.build_api_path("/market/tickers");
296        let mut params = HashMap::new();
297        params.insert("category".to_string(), self.get_category().to_string());
298        params.insert("symbol".to_string(), market.id.clone());
299
300        let response = self.public_request("GET", &path, Some(&params)).await?;
301
302        let result = response
303            .get("result")
304            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
305
306        let list = result
307            .get("list")
308            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
309
310        let tickers = list.as_array().ok_or_else(|| {
311            Error::from(ParseError::invalid_format(
312                "list",
313                "Expected array of tickers",
314            ))
315        })?;
316
317        if tickers.is_empty() {
318            return Err(Error::bad_symbol(format!("No ticker data for {}", symbol)));
319        }
320
321        parser::parse_ticker(&tickers[0], Some(&market))
322    }
323
324    /// Fetch tickers for multiple trading pairs.
325    ///
326    /// # Arguments
327    ///
328    /// * `symbols` - Optional list of trading pair symbols; fetches all if `None`.
329    ///
330    /// # Returns
331    ///
332    /// Returns a vector of [`Ticker`] structures.
333    pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
334        let cache = self.base().market_cache.read().await;
335        if !cache.loaded {
336            drop(cache);
337            return Err(Error::exchange(
338                "-1",
339                "Markets not loaded. Call load_markets() first.",
340            ));
341        }
342        drop(cache);
343
344        let path = self.build_api_path("/market/tickers");
345        let mut params = HashMap::new();
346        params.insert("category".to_string(), self.get_category().to_string());
347
348        let response = self.public_request("GET", &path, Some(&params)).await?;
349
350        let result = response
351            .get("result")
352            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
353
354        let list = result
355            .get("list")
356            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
357
358        let tickers_array = list.as_array().ok_or_else(|| {
359            Error::from(ParseError::invalid_format(
360                "list",
361                "Expected array of tickers",
362            ))
363        })?;
364
365        let mut tickers = Vec::new();
366        for ticker_data in tickers_array {
367            if let Some(symbol_id) = ticker_data["symbol"].as_str() {
368                let cache = self.base().market_cache.read().await;
369                if let Some(market) = cache.markets_by_id.get(symbol_id) {
370                    let market_clone = market.clone();
371                    drop(cache);
372
373                    match parser::parse_ticker(ticker_data, Some(&market_clone)) {
374                        Ok(ticker) => {
375                            if let Some(ref syms) = symbols {
376                                if syms.contains(&ticker.symbol) {
377                                    tickers.push(ticker);
378                                }
379                            } else {
380                                tickers.push(ticker);
381                            }
382                        }
383                        Err(e) => {
384                            warn!(
385                                error = %e,
386                                symbol = %symbol_id,
387                                "Failed to parse ticker"
388                            );
389                        }
390                    }
391                } else {
392                    drop(cache);
393                }
394            }
395        }
396
397        Ok(tickers)
398    }
399
400    // ============================================================================
401    // Public API Methods - Order Book and Trades
402    // ============================================================================
403
404    /// Fetch order book for a trading pair.
405    ///
406    /// # Arguments
407    ///
408    /// * `symbol` - Trading pair symbol.
409    /// * `limit` - Optional depth limit (valid values: 1-500; default: 25).
410    ///
411    /// # Returns
412    ///
413    /// Returns [`OrderBook`] data containing bids and asks.
414    pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
415        let market = self.base().market(symbol).await?;
416
417        let path = self.build_api_path("/market/orderbook");
418        let mut params = HashMap::new();
419        params.insert("category".to_string(), self.get_category().to_string());
420        params.insert("symbol".to_string(), market.id.clone());
421
422        // Bybit valid limits: 1-500, default 25
423        // Cap to maximum allowed value
424        let actual_limit = limit.map(|l| l.min(500)).unwrap_or(25);
425        params.insert("limit".to_string(), actual_limit.to_string());
426
427        let response = self.public_request("GET", &path, Some(&params)).await?;
428
429        let result = response
430            .get("result")
431            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
432
433        parser::parse_orderbook(result, market.symbol.clone())
434    }
435
436    /// Fetch recent public trades.
437    ///
438    /// # Arguments
439    ///
440    /// * `symbol` - Trading pair symbol.
441    /// * `limit` - Optional limit on number of trades (maximum: 1000).
442    ///
443    /// # Returns
444    ///
445    /// Returns a vector of [`Trade`] structures, sorted by timestamp in descending order.
446    pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
447        let market = self.base().market(symbol).await?;
448
449        let path = self.build_api_path("/market/recent-trade");
450        let mut params = HashMap::new();
451        params.insert("category".to_string(), self.get_category().to_string());
452        params.insert("symbol".to_string(), market.id.clone());
453
454        // Bybit maximum limit is 1000
455        let actual_limit = limit.map(|l| l.min(1000)).unwrap_or(60);
456        params.insert("limit".to_string(), actual_limit.to_string());
457
458        let response = self.public_request("GET", &path, Some(&params)).await?;
459
460        let result = response
461            .get("result")
462            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
463
464        let list = result
465            .get("list")
466            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
467
468        let trades_array = list.as_array().ok_or_else(|| {
469            Error::from(ParseError::invalid_format(
470                "list",
471                "Expected array of trades",
472            ))
473        })?;
474
475        let mut trades = Vec::new();
476        for trade_data in trades_array {
477            match parser::parse_trade(trade_data, Some(&market)) {
478                Ok(trade) => trades.push(trade),
479                Err(e) => {
480                    warn!(error = %e, "Failed to parse trade");
481                }
482            }
483        }
484
485        // Sort by timestamp descending (newest first)
486        trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
487
488        Ok(trades)
489    }
490
491    /// Fetch OHLCV (candlestick) data.
492    ///
493    /// # Arguments
494    ///
495    /// * `symbol` - Trading pair symbol.
496    /// * `timeframe` - Candlestick timeframe (e.g., "1m", "5m", "1h", "1d").
497    /// * `since` - Optional start timestamp in milliseconds.
498    /// * `limit` - Optional limit on number of candles (maximum: 1000).
499    ///
500    /// # Returns
501    ///
502    /// Returns a vector of [`OHLCV`] structures.
503    pub async fn fetch_ohlcv(
504        &self,
505        symbol: &str,
506        timeframe: &str,
507        since: Option<i64>,
508        limit: Option<u32>,
509    ) -> Result<Vec<OHLCV>> {
510        let market = self.base().market(symbol).await?;
511
512        // Convert timeframe to Bybit format
513        let timeframes = self.timeframes();
514        let bybit_timeframe = timeframes.get(timeframe).ok_or_else(|| {
515            Error::invalid_request(format!("Unsupported timeframe: {}", timeframe))
516        })?;
517
518        let path = self.build_api_path("/market/kline");
519        let mut params = HashMap::new();
520        params.insert("category".to_string(), self.get_category().to_string());
521        params.insert("symbol".to_string(), market.id.clone());
522        params.insert("interval".to_string(), bybit_timeframe.clone());
523
524        // Bybit maximum limit is 1000
525        let actual_limit = limit.map(|l| l.min(1000)).unwrap_or(200);
526        params.insert("limit".to_string(), actual_limit.to_string());
527
528        if let Some(start_time) = since {
529            params.insert("start".to_string(), start_time.to_string());
530        }
531
532        let response = self.public_request("GET", &path, Some(&params)).await?;
533
534        let result = response
535            .get("result")
536            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
537
538        let list = result
539            .get("list")
540            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
541
542        let candles_array = list.as_array().ok_or_else(|| {
543            Error::from(ParseError::invalid_format(
544                "list",
545                "Expected array of candles",
546            ))
547        })?;
548
549        let mut ohlcv = Vec::new();
550        for candle_data in candles_array {
551            match parser::parse_ohlcv(candle_data) {
552                Ok(candle) => ohlcv.push(candle),
553                Err(e) => {
554                    warn!(error = %e, "Failed to parse OHLCV");
555                }
556            }
557        }
558
559        // Sort by timestamp ascending (oldest first)
560        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
561
562        Ok(ohlcv)
563    }
564
565    // ============================================================================
566    // Private API Methods - Account
567    // ============================================================================
568
569    /// Fetch account balances.
570    ///
571    /// # Returns
572    ///
573    /// Returns a [`Balance`] structure with all currency balances.
574    ///
575    /// # Errors
576    ///
577    /// Returns an error if authentication fails or the API request fails.
578    pub async fn fetch_balance(&self) -> Result<Balance> {
579        let path = self.build_api_path("/account/wallet-balance");
580        let mut params = HashMap::new();
581        params.insert(
582            "accountType".to_string(),
583            self.options().account_type.clone(),
584        );
585
586        let response = self
587            .private_request("GET", &path, Some(&params), None)
588            .await?;
589
590        let result = response
591            .get("result")
592            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
593
594        parser::parse_balance(result)
595    }
596
597    /// Fetch user's trade history.
598    ///
599    /// # Arguments
600    ///
601    /// * `symbol` - Trading pair symbol.
602    /// * `since` - Optional start timestamp in milliseconds.
603    /// * `limit` - Optional limit on number of trades (maximum: 100).
604    ///
605    /// # Returns
606    ///
607    /// Returns a vector of [`Trade`] structures representing user's trade history.
608    pub async fn fetch_my_trades(
609        &self,
610        symbol: &str,
611        since: Option<i64>,
612        limit: Option<u32>,
613    ) -> Result<Vec<Trade>> {
614        let market = self.base().market(symbol).await?;
615
616        let path = self.build_api_path("/execution/list");
617        let mut params = HashMap::new();
618        params.insert("category".to_string(), self.get_category().to_string());
619        params.insert("symbol".to_string(), market.id.clone());
620
621        // Bybit maximum limit is 100
622        let actual_limit = limit.map(|l| l.min(100)).unwrap_or(50);
623        params.insert("limit".to_string(), actual_limit.to_string());
624
625        if let Some(start_time) = since {
626            params.insert("startTime".to_string(), start_time.to_string());
627        }
628
629        let response = self
630            .private_request("GET", &path, Some(&params), None)
631            .await?;
632
633        let result = response
634            .get("result")
635            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
636
637        let list = result
638            .get("list")
639            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
640
641        let trades_array = list.as_array().ok_or_else(|| {
642            Error::from(ParseError::invalid_format(
643                "list",
644                "Expected array of trades",
645            ))
646        })?;
647
648        let mut trades = Vec::new();
649        for trade_data in trades_array {
650            match parser::parse_trade(trade_data, Some(&market)) {
651                Ok(trade) => trades.push(trade),
652                Err(e) => {
653                    warn!(error = %e, "Failed to parse my trade");
654                }
655            }
656        }
657
658        Ok(trades)
659    }
660
661    // ============================================================================
662    // Private API Methods - Order Management
663    // ============================================================================
664
665    /// Create a new order.
666    ///
667    /// # Arguments
668    ///
669    /// * `symbol` - Trading pair symbol.
670    /// * `order_type` - Order type (Market, Limit).
671    /// * `side` - Order side (Buy or Sell).
672    /// * `amount` - Order quantity.
673    /// * `price` - Optional price (required for limit orders).
674    ///
675    /// # Returns
676    ///
677    /// Returns the created [`Order`] structure with order details.
678    pub async fn create_order(
679        &self,
680        symbol: &str,
681        order_type: OrderType,
682        side: OrderSide,
683        amount: f64,
684        price: Option<f64>,
685    ) -> Result<Order> {
686        let market = self.base().market(symbol).await?;
687
688        let path = self.build_api_path("/order/create");
689
690        // Build order body
691        let mut map = serde_json::Map::new();
692        map.insert(
693            "category".to_string(),
694            serde_json::Value::String(self.get_category().to_string()),
695        );
696        map.insert(
697            "symbol".to_string(),
698            serde_json::Value::String(market.id.clone()),
699        );
700        map.insert(
701            "side".to_string(),
702            serde_json::Value::String(match side {
703                OrderSide::Buy => "Buy".to_string(),
704                OrderSide::Sell => "Sell".to_string(),
705            }),
706        );
707        map.insert(
708            "orderType".to_string(),
709            serde_json::Value::String(match order_type {
710                OrderType::Market => "Market".to_string(),
711                OrderType::Limit => "Limit".to_string(),
712                OrderType::LimitMaker => "Limit".to_string(),
713                _ => "Limit".to_string(),
714            }),
715        );
716        map.insert(
717            "qty".to_string(),
718            serde_json::Value::String(amount.to_string()),
719        );
720
721        // Add price for limit orders
722        if let Some(p) = price {
723            if order_type == OrderType::Limit || order_type == OrderType::LimitMaker {
724                map.insert(
725                    "price".to_string(),
726                    serde_json::Value::String(p.to_string()),
727                );
728            }
729        }
730
731        // Add time in force for limit maker orders
732        if order_type == OrderType::LimitMaker {
733            map.insert(
734                "timeInForce".to_string(),
735                serde_json::Value::String("PostOnly".to_string()),
736            );
737        }
738
739        let body = serde_json::Value::Object(map);
740
741        let response = self
742            .private_request("POST", &path, None, Some(&body))
743            .await?;
744
745        let result = response
746            .get("result")
747            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
748
749        parser::parse_order(result, Some(&market))
750    }
751
752    /// Cancel an existing order.
753    ///
754    /// # Arguments
755    ///
756    /// * `id` - Order ID to cancel.
757    /// * `symbol` - Trading pair symbol.
758    ///
759    /// # Returns
760    ///
761    /// Returns the canceled [`Order`] structure.
762    pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
763        let market = self.base().market(symbol).await?;
764
765        let path = self.build_api_path("/order/cancel");
766
767        let mut map = serde_json::Map::new();
768        map.insert(
769            "category".to_string(),
770            serde_json::Value::String(self.get_category().to_string()),
771        );
772        map.insert(
773            "symbol".to_string(),
774            serde_json::Value::String(market.id.clone()),
775        );
776        map.insert(
777            "orderId".to_string(),
778            serde_json::Value::String(id.to_string()),
779        );
780        let body = serde_json::Value::Object(map);
781
782        let response = self
783            .private_request("POST", &path, None, Some(&body))
784            .await?;
785
786        let result = response
787            .get("result")
788            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
789
790        parser::parse_order(result, Some(&market))
791    }
792
793    /// Fetch a single order by ID.
794    ///
795    /// # Arguments
796    ///
797    /// * `id` - Order ID to fetch.
798    /// * `symbol` - Trading pair symbol.
799    ///
800    /// # Returns
801    ///
802    /// Returns the [`Order`] structure with current status.
803    pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
804        let market = self.base().market(symbol).await?;
805
806        let path = self.build_api_path("/order/realtime");
807        let mut params = HashMap::new();
808        params.insert("category".to_string(), self.get_category().to_string());
809        params.insert("symbol".to_string(), market.id.clone());
810        params.insert("orderId".to_string(), id.to_string());
811
812        let response = self
813            .private_request("GET", &path, Some(&params), None)
814            .await?;
815
816        let result = response
817            .get("result")
818            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
819
820        let list = result
821            .get("list")
822            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
823
824        let orders = list.as_array().ok_or_else(|| {
825            Error::from(ParseError::invalid_format(
826                "list",
827                "Expected array of orders",
828            ))
829        })?;
830
831        if orders.is_empty() {
832            return Err(Error::exchange("110008", "Order not found"));
833        }
834
835        parser::parse_order(&orders[0], Some(&market))
836    }
837
838    /// Fetch open orders.
839    ///
840    /// # Arguments
841    ///
842    /// * `symbol` - Optional trading pair symbol. If None, fetches all open orders.
843    /// * `since` - Optional start timestamp in milliseconds.
844    /// * `limit` - Optional limit on number of orders (maximum: 50).
845    ///
846    /// # Returns
847    ///
848    /// Returns a vector of open [`Order`] structures.
849    pub async fn fetch_open_orders(
850        &self,
851        symbol: Option<&str>,
852        since: Option<i64>,
853        limit: Option<u32>,
854    ) -> Result<Vec<Order>> {
855        let path = self.build_api_path("/order/realtime");
856        let mut params = HashMap::new();
857        params.insert("category".to_string(), self.get_category().to_string());
858
859        let market = if let Some(sym) = symbol {
860            let m = self.base().market(sym).await?;
861            params.insert("symbol".to_string(), m.id.clone());
862            Some(m)
863        } else {
864            None
865        };
866
867        // Bybit maximum limit is 50
868        let actual_limit = limit.map(|l| l.min(50)).unwrap_or(50);
869        params.insert("limit".to_string(), actual_limit.to_string());
870
871        if let Some(start_time) = since {
872            params.insert("startTime".to_string(), start_time.to_string());
873        }
874
875        let response = self
876            .private_request("GET", &path, Some(&params), None)
877            .await?;
878
879        let result = response
880            .get("result")
881            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
882
883        let list = result
884            .get("list")
885            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
886
887        let orders_array = list.as_array().ok_or_else(|| {
888            Error::from(ParseError::invalid_format(
889                "list",
890                "Expected array of orders",
891            ))
892        })?;
893
894        let mut orders = Vec::new();
895        for order_data in orders_array {
896            match parser::parse_order(order_data, market.as_ref()) {
897                Ok(order) => orders.push(order),
898                Err(e) => {
899                    warn!(error = %e, "Failed to parse open order");
900                }
901            }
902        }
903
904        Ok(orders)
905    }
906
907    /// Fetch closed orders.
908    ///
909    /// # Arguments
910    ///
911    /// * `symbol` - Optional trading pair symbol. If None, fetches all closed orders.
912    /// * `since` - Optional start timestamp in milliseconds.
913    /// * `limit` - Optional limit on number of orders (maximum: 50).
914    ///
915    /// # Returns
916    ///
917    /// Returns a vector of closed [`Order`] structures.
918    pub async fn fetch_closed_orders(
919        &self,
920        symbol: Option<&str>,
921        since: Option<i64>,
922        limit: Option<u32>,
923    ) -> Result<Vec<Order>> {
924        let path = self.build_api_path("/order/history");
925        let mut params = HashMap::new();
926        params.insert("category".to_string(), self.get_category().to_string());
927
928        let market = if let Some(sym) = symbol {
929            let m = self.base().market(sym).await?;
930            params.insert("symbol".to_string(), m.id.clone());
931            Some(m)
932        } else {
933            None
934        };
935
936        // Bybit maximum limit is 50
937        let actual_limit = limit.map(|l| l.min(50)).unwrap_or(50);
938        params.insert("limit".to_string(), actual_limit.to_string());
939
940        if let Some(start_time) = since {
941            params.insert("startTime".to_string(), start_time.to_string());
942        }
943
944        let response = self
945            .private_request("GET", &path, Some(&params), None)
946            .await?;
947
948        let result = response
949            .get("result")
950            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
951
952        let list = result
953            .get("list")
954            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
955
956        let orders_array = list.as_array().ok_or_else(|| {
957            Error::from(ParseError::invalid_format(
958                "list",
959                "Expected array of orders",
960            ))
961        })?;
962
963        let mut orders = Vec::new();
964        for order_data in orders_array {
965            match parser::parse_order(order_data, market.as_ref()) {
966                Ok(order) => orders.push(order),
967                Err(e) => {
968                    warn!(error = %e, "Failed to parse closed order");
969                }
970            }
971        }
972
973        Ok(orders)
974    }
975}
976
977#[cfg(test)]
978mod tests {
979    use super::*;
980
981    #[test]
982    fn test_build_api_path() {
983        let bybit = Bybit::builder().build().unwrap();
984        let path = bybit.build_api_path("/market/instruments-info");
985        assert_eq!(path, "/v5/market/instruments-info");
986    }
987
988    #[test]
989    fn test_get_category_spot() {
990        let bybit = Bybit::builder().build().unwrap();
991        let category = bybit.get_category();
992        assert_eq!(category, "spot");
993    }
994
995    #[test]
996    fn test_get_category_linear() {
997        let bybit = Bybit::builder().account_type("LINEAR").build().unwrap();
998        let category = bybit.get_category();
999        assert_eq!(category, "linear");
1000    }
1001
1002    #[test]
1003    fn test_get_timestamp() {
1004        let bybit = Bybit::builder().build().unwrap();
1005        let ts = bybit.get_timestamp();
1006
1007        // Should be a valid timestamp string
1008        assert!(!ts.is_empty());
1009        let parsed: i64 = ts.parse().unwrap();
1010        assert!(parsed > 0);
1011    }
1012}