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, sync::Arc};
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<HashMap<String, Arc<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, Arc<Market>>> {
265        // Acquire the loading lock to serialize concurrent load_markets calls
266        // This prevents multiple tasks from making duplicate API calls
267        let _loading_guard = self.base().market_loading_lock.lock().await;
268
269        // Check cache status while holding the lock
270        {
271            let cache = self.base().market_cache.read().await;
272            if cache.loaded && !reload {
273                debug!(
274                    "Returning cached markets for Bybit ({} markets)",
275                    cache.markets.len()
276                );
277                return Ok(cache.markets.clone());
278            }
279        }
280
281        info!("Loading markets for Bybit (reload: {})", reload);
282        let _markets = self.fetch_markets().await?;
283
284        let cache = self.base().market_cache.read().await;
285        Ok(cache.markets.clone())
286    }
287
288    /// Fetch ticker for a single trading pair.
289    ///
290    /// # Arguments
291    ///
292    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
293    ///
294    /// # Returns
295    ///
296    /// Returns [`Ticker`] data for the specified symbol.
297    pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
298        let market = self.base().market(symbol).await?;
299
300        let path = self.build_api_path("/market/tickers");
301        let mut params = HashMap::new();
302        params.insert("category".to_string(), self.get_category().to_string());
303        params.insert("symbol".to_string(), market.id.clone());
304
305        let response = self.public_request("GET", &path, Some(&params)).await?;
306
307        let result = response
308            .get("result")
309            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
310
311        let list = result
312            .get("list")
313            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
314
315        let tickers = list.as_array().ok_or_else(|| {
316            Error::from(ParseError::invalid_format(
317                "list",
318                "Expected array of tickers",
319            ))
320        })?;
321
322        if tickers.is_empty() {
323            return Err(Error::bad_symbol(format!("No ticker data for {}", symbol)));
324        }
325
326        parser::parse_ticker(&tickers[0], Some(&market))
327    }
328
329    /// Fetch tickers for multiple trading pairs.
330    ///
331    /// # Arguments
332    ///
333    /// * `symbols` - Optional list of trading pair symbols; fetches all if `None`.
334    ///
335    /// # Returns
336    ///
337    /// Returns a vector of [`Ticker`] structures.
338    pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
339        let cache = self.base().market_cache.read().await;
340        if !cache.loaded {
341            drop(cache);
342            return Err(Error::exchange(
343                "-1",
344                "Markets not loaded. Call load_markets() first.",
345            ));
346        }
347        drop(cache);
348
349        let path = self.build_api_path("/market/tickers");
350        let mut params = HashMap::new();
351        params.insert("category".to_string(), self.get_category().to_string());
352
353        let response = self.public_request("GET", &path, Some(&params)).await?;
354
355        let result = response
356            .get("result")
357            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
358
359        let list = result
360            .get("list")
361            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
362
363        let tickers_array = list.as_array().ok_or_else(|| {
364            Error::from(ParseError::invalid_format(
365                "list",
366                "Expected array of tickers",
367            ))
368        })?;
369
370        let mut tickers = Vec::new();
371        for ticker_data in tickers_array {
372            if let Some(symbol_id) = ticker_data["symbol"].as_str() {
373                let cache = self.base().market_cache.read().await;
374                if let Some(market) = cache.markets_by_id.get(symbol_id) {
375                    let market_clone = market.clone();
376                    drop(cache);
377
378                    match parser::parse_ticker(ticker_data, Some(&market_clone)) {
379                        Ok(ticker) => {
380                            if let Some(ref syms) = symbols {
381                                if syms.contains(&ticker.symbol) {
382                                    tickers.push(ticker);
383                                }
384                            } else {
385                                tickers.push(ticker);
386                            }
387                        }
388                        Err(e) => {
389                            warn!(
390                                error = %e,
391                                symbol = %symbol_id,
392                                "Failed to parse ticker"
393                            );
394                        }
395                    }
396                } else {
397                    drop(cache);
398                }
399            }
400        }
401
402        Ok(tickers)
403    }
404
405    // ============================================================================
406    // Public API Methods - Order Book and Trades
407    // ============================================================================
408
409    /// Fetch order book for a trading pair.
410    ///
411    /// # Arguments
412    ///
413    /// * `symbol` - Trading pair symbol.
414    /// * `limit` - Optional depth limit (valid values: 1-500; default: 25).
415    ///
416    /// # Returns
417    ///
418    /// Returns [`OrderBook`] data containing bids and asks.
419    pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
420        let market = self.base().market(symbol).await?;
421
422        let path = self.build_api_path("/market/orderbook");
423        let mut params = HashMap::new();
424        params.insert("category".to_string(), self.get_category().to_string());
425        params.insert("symbol".to_string(), market.id.clone());
426
427        // Bybit valid limits: 1-500, default 25
428        // Cap to maximum allowed value
429        let actual_limit = limit.map(|l| l.min(500)).unwrap_or(25);
430        params.insert("limit".to_string(), actual_limit.to_string());
431
432        let response = self.public_request("GET", &path, Some(&params)).await?;
433
434        let result = response
435            .get("result")
436            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
437
438        parser::parse_orderbook(result, market.symbol.clone())
439    }
440
441    /// Fetch recent public trades.
442    ///
443    /// # Arguments
444    ///
445    /// * `symbol` - Trading pair symbol.
446    /// * `limit` - Optional limit on number of trades (maximum: 1000).
447    ///
448    /// # Returns
449    ///
450    /// Returns a vector of [`Trade`] structures, sorted by timestamp in descending order.
451    pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
452        let market = self.base().market(symbol).await?;
453
454        let path = self.build_api_path("/market/recent-trade");
455        let mut params = HashMap::new();
456        params.insert("category".to_string(), self.get_category().to_string());
457        params.insert("symbol".to_string(), market.id.clone());
458
459        // Bybit maximum limit is 1000
460        let actual_limit = limit.map(|l| l.min(1000)).unwrap_or(60);
461        params.insert("limit".to_string(), actual_limit.to_string());
462
463        let response = self.public_request("GET", &path, Some(&params)).await?;
464
465        let result = response
466            .get("result")
467            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
468
469        let list = result
470            .get("list")
471            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
472
473        let trades_array = list.as_array().ok_or_else(|| {
474            Error::from(ParseError::invalid_format(
475                "list",
476                "Expected array of trades",
477            ))
478        })?;
479
480        let mut trades = Vec::new();
481        for trade_data in trades_array {
482            match parser::parse_trade(trade_data, Some(&market)) {
483                Ok(trade) => trades.push(trade),
484                Err(e) => {
485                    warn!(error = %e, "Failed to parse trade");
486                }
487            }
488        }
489
490        // Sort by timestamp descending (newest first)
491        trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
492
493        Ok(trades)
494    }
495
496    /// Fetch OHLCV (candlestick) data.
497    ///
498    /// # Arguments
499    ///
500    /// * `symbol` - Trading pair symbol.
501    /// * `timeframe` - Candlestick timeframe (e.g., "1m", "5m", "1h", "1d").
502    /// * `since` - Optional start timestamp in milliseconds.
503    /// * `limit` - Optional limit on number of candles (maximum: 1000).
504    ///
505    /// # Returns
506    ///
507    /// Returns a vector of [`OHLCV`] structures.
508    pub async fn fetch_ohlcv(
509        &self,
510        symbol: &str,
511        timeframe: &str,
512        since: Option<i64>,
513        limit: Option<u32>,
514    ) -> Result<Vec<OHLCV>> {
515        let market = self.base().market(symbol).await?;
516
517        // Convert timeframe to Bybit format
518        let timeframes = self.timeframes();
519        let bybit_timeframe = timeframes.get(timeframe).ok_or_else(|| {
520            Error::invalid_request(format!("Unsupported timeframe: {}", timeframe))
521        })?;
522
523        let path = self.build_api_path("/market/kline");
524        let mut params = HashMap::new();
525        params.insert("category".to_string(), self.get_category().to_string());
526        params.insert("symbol".to_string(), market.id.clone());
527        params.insert("interval".to_string(), bybit_timeframe.clone());
528
529        // Bybit maximum limit is 1000
530        let actual_limit = limit.map(|l| l.min(1000)).unwrap_or(200);
531        params.insert("limit".to_string(), actual_limit.to_string());
532
533        if let Some(start_time) = since {
534            params.insert("start".to_string(), start_time.to_string());
535        }
536
537        let response = self.public_request("GET", &path, Some(&params)).await?;
538
539        let result = response
540            .get("result")
541            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
542
543        let list = result
544            .get("list")
545            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
546
547        let candles_array = list.as_array().ok_or_else(|| {
548            Error::from(ParseError::invalid_format(
549                "list",
550                "Expected array of candles",
551            ))
552        })?;
553
554        let mut ohlcv = Vec::new();
555        for candle_data in candles_array {
556            match parser::parse_ohlcv(candle_data) {
557                Ok(candle) => ohlcv.push(candle),
558                Err(e) => {
559                    warn!(error = %e, "Failed to parse OHLCV");
560                }
561            }
562        }
563
564        // Sort by timestamp ascending (oldest first)
565        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
566
567        Ok(ohlcv)
568    }
569
570    // ============================================================================
571    // Private API Methods - Account
572    // ============================================================================
573
574    /// Fetch account balances.
575    ///
576    /// # Returns
577    ///
578    /// Returns a [`Balance`] structure with all currency balances.
579    ///
580    /// # Errors
581    ///
582    /// Returns an error if authentication fails or the API request fails.
583    pub async fn fetch_balance(&self) -> Result<Balance> {
584        let path = self.build_api_path("/account/wallet-balance");
585        let mut params = HashMap::new();
586        params.insert(
587            "accountType".to_string(),
588            self.options().account_type.clone(),
589        );
590
591        let response = self
592            .private_request("GET", &path, Some(&params), None)
593            .await?;
594
595        let result = response
596            .get("result")
597            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
598
599        parser::parse_balance(result)
600    }
601
602    /// Fetch user's trade history.
603    ///
604    /// # Arguments
605    ///
606    /// * `symbol` - Trading pair symbol.
607    /// * `since` - Optional start timestamp in milliseconds.
608    /// * `limit` - Optional limit on number of trades (maximum: 100).
609    ///
610    /// # Returns
611    ///
612    /// Returns a vector of [`Trade`] structures representing user's trade history.
613    pub async fn fetch_my_trades(
614        &self,
615        symbol: &str,
616        since: Option<i64>,
617        limit: Option<u32>,
618    ) -> Result<Vec<Trade>> {
619        let market = self.base().market(symbol).await?;
620
621        let path = self.build_api_path("/execution/list");
622        let mut params = HashMap::new();
623        params.insert("category".to_string(), self.get_category().to_string());
624        params.insert("symbol".to_string(), market.id.clone());
625
626        // Bybit maximum limit is 100
627        let actual_limit = limit.map(|l| l.min(100)).unwrap_or(50);
628        params.insert("limit".to_string(), actual_limit.to_string());
629
630        if let Some(start_time) = since {
631            params.insert("startTime".to_string(), start_time.to_string());
632        }
633
634        let response = self
635            .private_request("GET", &path, Some(&params), None)
636            .await?;
637
638        let result = response
639            .get("result")
640            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
641
642        let list = result
643            .get("list")
644            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
645
646        let trades_array = list.as_array().ok_or_else(|| {
647            Error::from(ParseError::invalid_format(
648                "list",
649                "Expected array of trades",
650            ))
651        })?;
652
653        let mut trades = Vec::new();
654        for trade_data in trades_array {
655            match parser::parse_trade(trade_data, Some(&market)) {
656                Ok(trade) => trades.push(trade),
657                Err(e) => {
658                    warn!(error = %e, "Failed to parse my trade");
659                }
660            }
661        }
662
663        Ok(trades)
664    }
665
666    // ============================================================================
667    // Private API Methods - Order Management
668    // ============================================================================
669
670    /// Create a new order.
671    ///
672    /// # Arguments
673    ///
674    /// * `symbol` - Trading pair symbol.
675    /// * `order_type` - Order type (Market, Limit).
676    /// * `side` - Order side (Buy or Sell).
677    /// * `amount` - Order quantity.
678    /// * `price` - Optional price (required for limit orders).
679    ///
680    /// # Returns
681    ///
682    /// Returns the created [`Order`] structure with order details.
683    pub async fn create_order(
684        &self,
685        symbol: &str,
686        order_type: OrderType,
687        side: OrderSide,
688        amount: f64,
689        price: Option<f64>,
690    ) -> Result<Order> {
691        let market = self.base().market(symbol).await?;
692
693        let path = self.build_api_path("/order/create");
694
695        // Build order body
696        let mut map = serde_json::Map::new();
697        map.insert(
698            "category".to_string(),
699            serde_json::Value::String(self.get_category().to_string()),
700        );
701        map.insert(
702            "symbol".to_string(),
703            serde_json::Value::String(market.id.clone()),
704        );
705        map.insert(
706            "side".to_string(),
707            serde_json::Value::String(match side {
708                OrderSide::Buy => "Buy".to_string(),
709                OrderSide::Sell => "Sell".to_string(),
710            }),
711        );
712        map.insert(
713            "orderType".to_string(),
714            serde_json::Value::String(match order_type {
715                OrderType::Market => "Market".to_string(),
716                OrderType::Limit => "Limit".to_string(),
717                OrderType::LimitMaker => "Limit".to_string(),
718                _ => "Limit".to_string(),
719            }),
720        );
721        map.insert(
722            "qty".to_string(),
723            serde_json::Value::String(amount.to_string()),
724        );
725
726        // Add price for limit orders
727        if let Some(p) = price {
728            if order_type == OrderType::Limit || order_type == OrderType::LimitMaker {
729                map.insert(
730                    "price".to_string(),
731                    serde_json::Value::String(p.to_string()),
732                );
733            }
734        }
735
736        // Add time in force for limit maker orders
737        if order_type == OrderType::LimitMaker {
738            map.insert(
739                "timeInForce".to_string(),
740                serde_json::Value::String("PostOnly".to_string()),
741            );
742        }
743
744        let body = serde_json::Value::Object(map);
745
746        let response = self
747            .private_request("POST", &path, None, Some(&body))
748            .await?;
749
750        let result = response
751            .get("result")
752            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
753
754        parser::parse_order(result, Some(&market))
755    }
756
757    /// Cancel an existing order.
758    ///
759    /// # Arguments
760    ///
761    /// * `id` - Order ID to cancel.
762    /// * `symbol` - Trading pair symbol.
763    ///
764    /// # Returns
765    ///
766    /// Returns the canceled [`Order`] structure.
767    pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
768        let market = self.base().market(symbol).await?;
769
770        let path = self.build_api_path("/order/cancel");
771
772        let mut map = serde_json::Map::new();
773        map.insert(
774            "category".to_string(),
775            serde_json::Value::String(self.get_category().to_string()),
776        );
777        map.insert(
778            "symbol".to_string(),
779            serde_json::Value::String(market.id.clone()),
780        );
781        map.insert(
782            "orderId".to_string(),
783            serde_json::Value::String(id.to_string()),
784        );
785        let body = serde_json::Value::Object(map);
786
787        let response = self
788            .private_request("POST", &path, None, Some(&body))
789            .await?;
790
791        let result = response
792            .get("result")
793            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
794
795        parser::parse_order(result, Some(&market))
796    }
797
798    /// Fetch a single order by ID.
799    ///
800    /// # Arguments
801    ///
802    /// * `id` - Order ID to fetch.
803    /// * `symbol` - Trading pair symbol.
804    ///
805    /// # Returns
806    ///
807    /// Returns the [`Order`] structure with current status.
808    pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
809        let market = self.base().market(symbol).await?;
810
811        let path = self.build_api_path("/order/realtime");
812        let mut params = HashMap::new();
813        params.insert("category".to_string(), self.get_category().to_string());
814        params.insert("symbol".to_string(), market.id.clone());
815        params.insert("orderId".to_string(), id.to_string());
816
817        let response = self
818            .private_request("GET", &path, Some(&params), None)
819            .await?;
820
821        let result = response
822            .get("result")
823            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
824
825        let list = result
826            .get("list")
827            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
828
829        let orders = list.as_array().ok_or_else(|| {
830            Error::from(ParseError::invalid_format(
831                "list",
832                "Expected array of orders",
833            ))
834        })?;
835
836        if orders.is_empty() {
837            return Err(Error::exchange("110008", "Order not found"));
838        }
839
840        parser::parse_order(&orders[0], Some(&market))
841    }
842
843    /// Fetch open orders.
844    ///
845    /// # Arguments
846    ///
847    /// * `symbol` - Optional trading pair symbol. If None, fetches all open orders.
848    /// * `since` - Optional start timestamp in milliseconds.
849    /// * `limit` - Optional limit on number of orders (maximum: 50).
850    ///
851    /// # Returns
852    ///
853    /// Returns a vector of open [`Order`] structures.
854    pub async fn fetch_open_orders(
855        &self,
856        symbol: Option<&str>,
857        since: Option<i64>,
858        limit: Option<u32>,
859    ) -> Result<Vec<Order>> {
860        let path = self.build_api_path("/order/realtime");
861        let mut params = HashMap::new();
862        params.insert("category".to_string(), self.get_category().to_string());
863
864        let market = if let Some(sym) = symbol {
865            let m = self.base().market(sym).await?;
866            params.insert("symbol".to_string(), m.id.clone());
867            Some(m)
868        } else {
869            None
870        };
871
872        // Bybit maximum limit is 50
873        let actual_limit = limit.map(|l| l.min(50)).unwrap_or(50);
874        params.insert("limit".to_string(), actual_limit.to_string());
875
876        if let Some(start_time) = since {
877            params.insert("startTime".to_string(), start_time.to_string());
878        }
879
880        let response = self
881            .private_request("GET", &path, Some(&params), None)
882            .await?;
883
884        let result = response
885            .get("result")
886            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
887
888        let list = result
889            .get("list")
890            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
891
892        let orders_array = list.as_array().ok_or_else(|| {
893            Error::from(ParseError::invalid_format(
894                "list",
895                "Expected array of orders",
896            ))
897        })?;
898
899        let mut orders = Vec::new();
900        for order_data in orders_array {
901            match parser::parse_order(order_data, market.as_ref().map(|v| &**v)) {
902                Ok(order) => orders.push(order),
903                Err(e) => {
904                    warn!(error = %e, "Failed to parse open order");
905                }
906            }
907        }
908
909        Ok(orders)
910    }
911
912    /// Fetch closed orders.
913    ///
914    /// # Arguments
915    ///
916    /// * `symbol` - Optional trading pair symbol. If None, fetches all closed orders.
917    /// * `since` - Optional start timestamp in milliseconds.
918    /// * `limit` - Optional limit on number of orders (maximum: 50).
919    ///
920    /// # Returns
921    ///
922    /// Returns a vector of closed [`Order`] structures.
923    pub async fn fetch_closed_orders(
924        &self,
925        symbol: Option<&str>,
926        since: Option<i64>,
927        limit: Option<u32>,
928    ) -> Result<Vec<Order>> {
929        let path = self.build_api_path("/order/history");
930        let mut params = HashMap::new();
931        params.insert("category".to_string(), self.get_category().to_string());
932
933        let market = if let Some(sym) = symbol {
934            let m = self.base().market(sym).await?;
935            params.insert("symbol".to_string(), m.id.clone());
936            Some(m)
937        } else {
938            None
939        };
940
941        // Bybit maximum limit is 50
942        let actual_limit = limit.map(|l| l.min(50)).unwrap_or(50);
943        params.insert("limit".to_string(), actual_limit.to_string());
944
945        if let Some(start_time) = since {
946            params.insert("startTime".to_string(), start_time.to_string());
947        }
948
949        let response = self
950            .private_request("GET", &path, Some(&params), None)
951            .await?;
952
953        let result = response
954            .get("result")
955            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
956
957        let list = result
958            .get("list")
959            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
960
961        let orders_array = list.as_array().ok_or_else(|| {
962            Error::from(ParseError::invalid_format(
963                "list",
964                "Expected array of orders",
965            ))
966        })?;
967
968        let mut orders = Vec::new();
969        for order_data in orders_array {
970            match parser::parse_order(order_data, market.as_ref().map(|v| &**v)) {
971                Ok(order) => orders.push(order),
972                Err(e) => {
973                    warn!(error = %e, "Failed to parse closed order");
974                }
975            }
976        }
977
978        Ok(orders)
979    }
980}
981
982#[cfg(test)]
983mod tests {
984    use super::*;
985
986    #[test]
987    fn test_build_api_path() {
988        let bybit = Bybit::builder().build().unwrap();
989        let path = bybit.build_api_path("/market/instruments-info");
990        assert_eq!(path, "/v5/market/instruments-info");
991    }
992
993    #[test]
994    fn test_get_category_spot() {
995        let bybit = Bybit::builder().build().unwrap();
996        let category = bybit.get_category();
997        assert_eq!(category, "spot");
998    }
999
1000    #[test]
1001    fn test_get_category_linear() {
1002        let bybit = Bybit::builder().account_type("LINEAR").build().unwrap();
1003        let category = bybit.get_category();
1004        assert_eq!(category, "linear");
1005    }
1006
1007    #[test]
1008    fn test_get_timestamp() {
1009        let bybit = Bybit::builder().build().unwrap();
1010        let ts = bybit.get_timestamp();
1011
1012        // Should be a valid timestamp string
1013        assert!(!ts.is_empty());
1014        let parsed: i64 = ts.parse().unwrap();
1015        assert!(parsed > 0);
1016    }
1017}