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::{
9        Amount, Balance, Market, OHLCV, OhlcvRequest, Order, OrderBook, OrderRequest, OrderSide,
10        OrderType, Price, Ticker, TimeInForce, Trade,
11    },
12};
13use reqwest::header::{HeaderMap, HeaderValue};
14use serde_json::Value;
15use std::{collections::HashMap, sync::Arc};
16use tracing::{debug, info, warn};
17
18impl Bybit {
19    // ============================================================================
20    // Helper Methods
21    // ============================================================================
22
23    /// Get the current timestamp in milliseconds.
24    ///
25    /// # Deprecated
26    ///
27    /// This method is deprecated. Use [`signed_request()`](Self::signed_request) instead.
28    /// The `signed_request()` builder handles timestamp generation internally.
29    #[deprecated(
30        since = "0.1.0",
31        note = "Use `signed_request()` builder instead which handles timestamps internally"
32    )]
33    #[allow(dead_code)]
34    fn get_timestamp() -> String {
35        chrono::Utc::now().timestamp_millis().to_string()
36    }
37
38    /// Get the authentication instance if credentials are configured.
39    pub fn get_auth(&self) -> Result<BybitAuth> {
40        let config = &self.base().config;
41
42        let api_key = config
43            .api_key
44            .as_ref()
45            .ok_or_else(|| Error::authentication("API key is required"))?;
46        let secret = config
47            .secret
48            .as_ref()
49            .ok_or_else(|| Error::authentication("API secret is required"))?;
50
51        Ok(BybitAuth::new(
52            api_key.expose_secret().to_string(),
53            secret.expose_secret().to_string(),
54        ))
55    }
56
57    /// Check that required credentials are configured.
58    pub fn check_required_credentials(&self) -> Result<()> {
59        self.base().check_required_credentials()
60    }
61
62    /// Build the API path for Bybit V5 API.
63    fn build_api_path(endpoint: &str) -> String {
64        format!("/v5{}", endpoint)
65    }
66
67    /// Get the category for API requests based on account type.
68    fn get_category(&self) -> &str {
69        match self.options().account_type.as_str() {
70            "CONTRACT" | "LINEAR" => "linear",
71            "INVERSE" => "inverse",
72            "OPTION" => "option",
73            _ => "spot",
74        }
75    }
76
77    /// Make a public API request (no authentication required).
78    async fn public_request(
79        &self,
80        method: &str,
81        path: &str,
82        params: Option<&HashMap<String, String>>,
83    ) -> Result<Value> {
84        let urls = self.urls();
85        let mut url = format!("{}{}", urls.rest, path);
86
87        if let Some(p) = params {
88            if !p.is_empty() {
89                let query: Vec<String> = p
90                    .iter()
91                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
92                    .collect();
93                url = format!("{}?{}", url, query.join("&"));
94            }
95        }
96
97        debug!("Bybit public request: {} {}", method, url);
98
99        let response = match method.to_uppercase().as_str() {
100            "GET" => self.base().http_client.get(&url, None).await?,
101            "POST" => self.base().http_client.post(&url, None, None).await?,
102            _ => {
103                return Err(Error::invalid_request(format!(
104                    "Unsupported HTTP method: {}",
105                    method
106                )));
107            }
108        };
109
110        // Check for Bybit error response
111        if error::is_error_response(&response) {
112            return Err(error::parse_error(&response));
113        }
114
115        Ok(response)
116    }
117
118    /// Make a private API request (authentication required).
119    ///
120    /// # Deprecated
121    ///
122    /// This method is deprecated. Use [`signed_request()`](Self::signed_request) instead.
123    /// The `signed_request()` builder provides a cleaner, more maintainable API for
124    /// constructing authenticated requests.
125    #[deprecated(
126        since = "0.1.0",
127        note = "Use `signed_request()` builder instead for cleaner, more maintainable code"
128    )]
129    #[allow(dead_code)]
130    #[allow(deprecated)]
131    async fn private_request(
132        &self,
133        method: &str,
134        path: &str,
135        params: Option<&HashMap<String, String>>,
136        body: Option<&Value>,
137    ) -> Result<Value> {
138        self.check_required_credentials()?;
139
140        let auth = self.get_auth()?;
141        let urls = self.urls();
142        let timestamp = Self::get_timestamp();
143        let recv_window = self.options().recv_window;
144
145        // Build query string for GET requests
146        let query_string = if let Some(p) = params {
147            if p.is_empty() {
148                String::new()
149            } else {
150                let query: Vec<String> = p
151                    .iter()
152                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
153                    .collect();
154                query.join("&")
155            }
156        } else {
157            String::new()
158        };
159
160        // Build body string for POST requests
161        let body_string = body
162            .map(|b| serde_json::to_string(b).unwrap_or_default())
163            .unwrap_or_default();
164
165        // Sign the request - Bybit uses query string for GET, body for POST
166        let sign_params = if method.to_uppercase() == "GET" {
167            &query_string
168        } else {
169            &body_string
170        };
171        let signature = auth.sign(&timestamp, recv_window, sign_params);
172
173        // Build headers
174        let mut headers = HeaderMap::new();
175        auth.add_auth_headers(&mut headers, &timestamp, &signature, recv_window);
176        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
177
178        let url = if query_string.is_empty() {
179            format!("{}{}", urls.rest, path)
180        } else {
181            format!("{}{}?{}", urls.rest, path, query_string)
182        };
183        debug!("Bybit private request: {} {}", method, url);
184
185        let response = match method.to_uppercase().as_str() {
186            "GET" => self.base().http_client.get(&url, Some(headers)).await?,
187            "POST" => {
188                let body_value = body.cloned();
189                self.base()
190                    .http_client
191                    .post(&url, Some(headers), body_value)
192                    .await?
193            }
194            "DELETE" => {
195                self.base()
196                    .http_client
197                    .delete(&url, Some(headers), None)
198                    .await?
199            }
200            _ => {
201                return Err(Error::invalid_request(format!(
202                    "Unsupported HTTP method: {}",
203                    method
204                )));
205            }
206        };
207
208        // Check for Bybit error response
209        if error::is_error_response(&response) {
210            return Err(error::parse_error(&response));
211        }
212
213        Ok(response)
214    }
215
216    // ============================================================================
217    // Public API Methods - Market Data
218    // ============================================================================
219
220    /// Fetch all trading markets.
221    ///
222    /// # Returns
223    ///
224    /// Returns a vector of [`Market`] structures containing market information.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if the API request fails or response parsing fails.
229    ///
230    /// # Example
231    ///
232    /// ```no_run
233    /// # use ccxt_exchanges::bybit::Bybit;
234    /// # async fn example() -> ccxt_core::Result<()> {
235    /// let bybit = Bybit::builder().build()?;
236    /// let markets = bybit.fetch_markets().await?;
237    /// println!("Found {} markets", markets.len());
238    /// # Ok(())
239    /// # }
240    /// ```
241    pub async fn fetch_markets(&self) -> Result<Arc<HashMap<String, Arc<Market>>>> {
242        let path = Self::build_api_path("/market/instruments-info");
243        let mut params = HashMap::new();
244        params.insert("category".to_string(), self.get_category().to_string());
245
246        let response = self.public_request("GET", &path, Some(&params)).await?;
247
248        let result = response
249            .get("result")
250            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
251
252        let list = result
253            .get("list")
254            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
255
256        let instruments = list.as_array().ok_or_else(|| {
257            Error::from(ParseError::invalid_format(
258                "list",
259                "Expected array of instruments",
260            ))
261        })?;
262
263        let mut markets = Vec::new();
264        for instrument in instruments {
265            match parser::parse_market(instrument) {
266                Ok(market) => markets.push(market),
267                Err(e) => {
268                    warn!(error = %e, "Failed to parse market");
269                }
270            }
271        }
272
273        // Cache the markets and preserve ownership for the caller
274        let markets = self.base().set_markets(markets, None).await?;
275
276        info!("Loaded {} markets for Bybit", markets.len());
277        Ok(markets)
278    }
279
280    /// Load and cache market data.
281    ///
282    /// If markets are already loaded and `reload` is false, returns cached data.
283    ///
284    /// # Arguments
285    ///
286    /// * `reload` - Whether to force reload market data from the API.
287    ///
288    /// # Returns
289    ///
290    /// Returns a `HashMap` containing all market data, keyed by symbol (e.g., "BTC/USDT").
291    pub async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
292        // Acquire the loading lock to serialize concurrent load_markets calls
293        // This prevents multiple tasks from making duplicate API calls
294        let _loading_guard = self.base().market_loading_lock.lock().await;
295
296        // Check cache status while holding the lock
297        {
298            let cache = self.base().market_cache.read().await;
299            if cache.loaded && !reload {
300                debug!(
301                    "Returning cached markets for Bybit ({} markets)",
302                    cache.markets.len()
303                );
304                return Ok(cache.markets.clone());
305            }
306        }
307
308        info!("Loading markets for Bybit (reload: {})", reload);
309        let _markets = self.fetch_markets().await?;
310
311        let cache = self.base().market_cache.read().await;
312        Ok(cache.markets.clone())
313    }
314
315    /// Fetch ticker for a single trading pair.
316    ///
317    /// # Arguments
318    ///
319    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
320    ///
321    /// # Returns
322    ///
323    /// Returns [`Ticker`] data for the specified symbol.
324    pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
325        let market = self.base().market(symbol).await?;
326
327        let path = Self::build_api_path("/market/tickers");
328        let mut params = HashMap::new();
329        params.insert("category".to_string(), self.get_category().to_string());
330        params.insert("symbol".to_string(), market.id.clone());
331
332        let response = self.public_request("GET", &path, Some(&params)).await?;
333
334        let result = response
335            .get("result")
336            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
337
338        let list = result
339            .get("list")
340            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
341
342        let tickers = list.as_array().ok_or_else(|| {
343            Error::from(ParseError::invalid_format(
344                "list",
345                "Expected array of tickers",
346            ))
347        })?;
348
349        if tickers.is_empty() {
350            return Err(Error::bad_symbol(format!("No ticker data for {}", symbol)));
351        }
352
353        parser::parse_ticker(&tickers[0], Some(&market))
354    }
355
356    /// Fetch tickers for multiple trading pairs.
357    ///
358    /// # Arguments
359    ///
360    /// * `symbols` - Optional list of trading pair symbols; fetches all if `None`.
361    ///
362    /// # Returns
363    ///
364    /// Returns a vector of [`Ticker`] structures.
365    pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
366        let cache = self.base().market_cache.read().await;
367        if !cache.loaded {
368            drop(cache);
369            return Err(Error::exchange(
370                "-1",
371                "Markets not loaded. Call load_markets() first.",
372            ));
373        }
374        drop(cache);
375
376        let path = Self::build_api_path("/market/tickers");
377        let mut params = HashMap::new();
378        params.insert("category".to_string(), self.get_category().to_string());
379
380        let response = self.public_request("GET", &path, Some(&params)).await?;
381
382        let result = response
383            .get("result")
384            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
385
386        let list = result
387            .get("list")
388            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
389
390        let tickers_array = list.as_array().ok_or_else(|| {
391            Error::from(ParseError::invalid_format(
392                "list",
393                "Expected array of tickers",
394            ))
395        })?;
396
397        let mut tickers = Vec::new();
398        for ticker_data in tickers_array {
399            if let Some(symbol_id) = ticker_data["symbol"].as_str() {
400                let cache = self.base().market_cache.read().await;
401                if let Some(market) = cache.markets_by_id.get(symbol_id) {
402                    let market_clone = market.clone();
403                    drop(cache);
404
405                    match parser::parse_ticker(ticker_data, Some(&market_clone)) {
406                        Ok(ticker) => {
407                            if let Some(ref syms) = symbols {
408                                if syms.contains(&ticker.symbol) {
409                                    tickers.push(ticker);
410                                }
411                            } else {
412                                tickers.push(ticker);
413                            }
414                        }
415                        Err(e) => {
416                            warn!(
417                                error = %e,
418                                symbol = %symbol_id,
419                                "Failed to parse ticker"
420                            );
421                        }
422                    }
423                } else {
424                    drop(cache);
425                }
426            }
427        }
428
429        Ok(tickers)
430    }
431
432    // ============================================================================
433    // Public API Methods - Order Book and Trades
434    // ============================================================================
435
436    /// Fetch order book for a trading pair.
437    ///
438    /// # Arguments
439    ///
440    /// * `symbol` - Trading pair symbol.
441    /// * `limit` - Optional depth limit (valid values: 1-500; default: 25).
442    ///
443    /// # Returns
444    ///
445    /// Returns [`OrderBook`] data containing bids and asks.
446    pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
447        let market = self.base().market(symbol).await?;
448
449        let path = Self::build_api_path("/market/orderbook");
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 valid limits: 1-500, default 25
455        // Cap to maximum allowed value
456        let actual_limit = limit.map_or(25, |l| l.min(500));
457        params.insert("limit".to_string(), actual_limit.to_string());
458
459        let response = self.public_request("GET", &path, Some(&params)).await?;
460
461        let result = response
462            .get("result")
463            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
464
465        parser::parse_orderbook(result, market.symbol.clone())
466    }
467
468    /// Fetch recent public trades.
469    ///
470    /// # Arguments
471    ///
472    /// * `symbol` - Trading pair symbol.
473    /// * `limit` - Optional limit on number of trades (maximum: 1000).
474    ///
475    /// # Returns
476    ///
477    /// Returns a vector of [`Trade`] structures, sorted by timestamp in descending order.
478    pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
479        let market = self.base().market(symbol).await?;
480
481        let path = Self::build_api_path("/market/recent-trade");
482        let mut params = HashMap::new();
483        params.insert("category".to_string(), self.get_category().to_string());
484        params.insert("symbol".to_string(), market.id.clone());
485
486        // Bybit maximum limit is 1000
487        let actual_limit = limit.map_or(60, |l| l.min(1000));
488        params.insert("limit".to_string(), actual_limit.to_string());
489
490        let response = self.public_request("GET", &path, Some(&params)).await?;
491
492        let result = response
493            .get("result")
494            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
495
496        let list = result
497            .get("list")
498            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
499
500        let trades_array = list.as_array().ok_or_else(|| {
501            Error::from(ParseError::invalid_format(
502                "list",
503                "Expected array of trades",
504            ))
505        })?;
506
507        let mut trades = Vec::new();
508        for trade_data in trades_array {
509            match parser::parse_trade(trade_data, Some(&market)) {
510                Ok(trade) => trades.push(trade),
511                Err(e) => {
512                    warn!(error = %e, "Failed to parse trade");
513                }
514            }
515        }
516
517        // Sort by timestamp descending (newest first)
518        trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
519
520        Ok(trades)
521    }
522
523    /// Fetch OHLCV (candlestick) data using the builder pattern.
524    ///
525    /// This is the preferred method for fetching OHLCV data. It accepts an [`OhlcvRequest`]
526    /// built using the builder pattern, which provides validation and a more ergonomic API.
527    ///
528    /// # Arguments
529    ///
530    /// * `request` - OHLCV request built via [`OhlcvRequest::builder()`]
531    ///
532    /// # Returns
533    ///
534    /// Returns a vector of [`OHLCV`] structures.
535    ///
536    /// # Errors
537    ///
538    /// Returns an error if the market is not found or the API request fails.
539    ///
540    /// _Requirements: 2.3, 2.6_
541    pub async fn fetch_ohlcv_v2(&self, request: OhlcvRequest) -> Result<Vec<OHLCV>> {
542        let market = self.base().market(&request.symbol).await?;
543
544        // Convert timeframe to Bybit format
545        let timeframes = self.timeframes();
546        let bybit_timeframe = timeframes.get(&request.timeframe).ok_or_else(|| {
547            Error::invalid_request(format!("Unsupported timeframe: {}", request.timeframe))
548        })?;
549
550        let path = Self::build_api_path("/market/kline");
551        let mut params = HashMap::new();
552        params.insert("category".to_string(), self.get_category().to_string());
553        params.insert("symbol".to_string(), market.id.clone());
554        params.insert("interval".to_string(), bybit_timeframe.clone());
555
556        // Bybit maximum limit is 1000
557        let actual_limit = request.limit.map_or(200, |l| l.min(1000));
558        params.insert("limit".to_string(), actual_limit.to_string());
559
560        if let Some(start_time) = request.since {
561            params.insert("start".to_string(), start_time.to_string());
562        }
563
564        if let Some(end_time) = request.until {
565            params.insert("end".to_string(), end_time.to_string());
566        }
567
568        let response = self.public_request("GET", &path, Some(&params)).await?;
569
570        let result = response
571            .get("result")
572            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
573
574        let list = result
575            .get("list")
576            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
577
578        let candles_array = list.as_array().ok_or_else(|| {
579            Error::from(ParseError::invalid_format(
580                "list",
581                "Expected array of candles",
582            ))
583        })?;
584
585        let mut ohlcv = Vec::new();
586        for candle_data in candles_array {
587            match parser::parse_ohlcv(candle_data) {
588                Ok(candle) => ohlcv.push(candle),
589                Err(e) => {
590                    warn!(error = %e, "Failed to parse OHLCV");
591                }
592            }
593        }
594
595        // Sort by timestamp ascending (oldest first)
596        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
597
598        Ok(ohlcv)
599    }
600
601    /// Fetch OHLCV (candlestick) data (deprecated).
602    ///
603    /// # Deprecated
604    ///
605    /// This method is deprecated. Use [`fetch_ohlcv_v2`](Self::fetch_ohlcv_v2) with
606    /// [`OhlcvRequest::builder()`] instead for a more ergonomic API.
607    ///
608    /// # Arguments
609    ///
610    /// * `symbol` - Trading pair symbol.
611    /// * `timeframe` - Candlestick timeframe (e.g., "1m", "5m", "1h", "1d").
612    /// * `since` - Optional start timestamp in milliseconds.
613    /// * `limit` - Optional limit on number of candles (maximum: 1000).
614    ///
615    /// # Returns
616    ///
617    /// Returns a vector of [`OHLCV`] structures.
618    #[deprecated(
619        since = "0.2.0",
620        note = "Use fetch_ohlcv_v2 with OhlcvRequest::builder() instead"
621    )]
622    pub async fn fetch_ohlcv(
623        &self,
624        symbol: &str,
625        timeframe: &str,
626        since: Option<i64>,
627        limit: Option<u32>,
628    ) -> Result<Vec<OHLCV>> {
629        let market = self.base().market(symbol).await?;
630
631        // Convert timeframe to Bybit format
632        let timeframes = self.timeframes();
633        let bybit_timeframe = timeframes.get(timeframe).ok_or_else(|| {
634            Error::invalid_request(format!("Unsupported timeframe: {}", timeframe))
635        })?;
636
637        let path = Self::build_api_path("/market/kline");
638        let mut params = HashMap::new();
639        params.insert("category".to_string(), self.get_category().to_string());
640        params.insert("symbol".to_string(), market.id.clone());
641        params.insert("interval".to_string(), bybit_timeframe.clone());
642
643        // Bybit maximum limit is 1000
644        let actual_limit = limit.map_or(200, |l| l.min(1000));
645        params.insert("limit".to_string(), actual_limit.to_string());
646
647        if let Some(start_time) = since {
648            params.insert("start".to_string(), start_time.to_string());
649        }
650
651        let response = self.public_request("GET", &path, Some(&params)).await?;
652
653        let result = response
654            .get("result")
655            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
656
657        let list = result
658            .get("list")
659            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
660
661        let candles_array = list.as_array().ok_or_else(|| {
662            Error::from(ParseError::invalid_format(
663                "list",
664                "Expected array of candles",
665            ))
666        })?;
667
668        let mut ohlcv = Vec::new();
669        for candle_data in candles_array {
670            match parser::parse_ohlcv(candle_data) {
671                Ok(candle) => ohlcv.push(candle),
672                Err(e) => {
673                    warn!(error = %e, "Failed to parse OHLCV");
674                }
675            }
676        }
677
678        // Sort by timestamp ascending (oldest first)
679        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
680
681        Ok(ohlcv)
682    }
683
684    // ============================================================================
685    // Private API Methods - Account
686    // ============================================================================
687
688    /// Fetch account balances.
689    ///
690    /// # Returns
691    ///
692    /// Returns a [`Balance`] structure with all currency balances.
693    ///
694    /// # Errors
695    ///
696    /// Returns an error if authentication fails or the API request fails.
697    pub async fn fetch_balance(&self) -> Result<Balance> {
698        let path = Self::build_api_path("/account/wallet-balance");
699
700        let response = self
701            .signed_request(&path)
702            .param("accountType", &self.options().account_type)
703            .execute()
704            .await?;
705
706        let result = response
707            .get("result")
708            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
709
710        parser::parse_balance(result)
711    }
712
713    /// Fetch user's trade history.
714    ///
715    /// # Arguments
716    ///
717    /// * `symbol` - Trading pair symbol.
718    /// * `since` - Optional start timestamp in milliseconds.
719    /// * `limit` - Optional limit on number of trades (maximum: 100).
720    ///
721    /// # Returns
722    ///
723    /// Returns a vector of [`Trade`] structures representing user's trade history.
724    pub async fn fetch_my_trades(
725        &self,
726        symbol: &str,
727        since: Option<i64>,
728        limit: Option<u32>,
729    ) -> Result<Vec<Trade>> {
730        let market = self.base().market(symbol).await?;
731
732        let path = Self::build_api_path("/execution/list");
733
734        // Bybit maximum limit is 100
735        let actual_limit = limit.map_or(50, |l| l.min(100));
736
737        let response = self
738            .signed_request(&path)
739            .param("category", self.get_category())
740            .param("symbol", &market.id)
741            .param("limit", actual_limit)
742            .optional_param("startTime", since)
743            .execute()
744            .await?;
745
746        let result = response
747            .get("result")
748            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
749
750        let list = result
751            .get("list")
752            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
753
754        let trades_array = list.as_array().ok_or_else(|| {
755            Error::from(ParseError::invalid_format(
756                "list",
757                "Expected array of trades",
758            ))
759        })?;
760
761        let mut trades = Vec::new();
762        for trade_data in trades_array {
763            match parser::parse_trade(trade_data, Some(&market)) {
764                Ok(trade) => trades.push(trade),
765                Err(e) => {
766                    warn!(error = %e, "Failed to parse my trade");
767                }
768            }
769        }
770
771        Ok(trades)
772    }
773
774    // ============================================================================
775    // Private API Methods - Order Management
776    // ============================================================================
777
778    /// Create a new order.
779    ///
780    /// # Arguments
781    ///
782    /// * `symbol` - Trading pair symbol.
783    /// * `order_type` - Order type (Market, Limit).
784    /// * `side` - Order side (Buy or Sell).
785    /// * `amount` - Order quantity as [`Amount`] type.
786    /// * `price` - Optional price as [`Price`] type (required for limit orders).
787    ///
788    /// # Returns
789    ///
790    /// Returns the created [`Order`] structure with order details.
791    ///
792    /// _Requirements: 2.2, 2.6_
793    pub async fn create_order_v2(&self, request: OrderRequest) -> Result<Order> {
794        use crate::bybit::signed_request::HttpMethod;
795
796        let market = self.base().market(&request.symbol).await?;
797
798        let path = Self::build_api_path("/order/create");
799
800        // Build order body
801        let mut map = serde_json::Map::new();
802        map.insert(
803            "category".to_string(),
804            serde_json::Value::String(self.get_category().to_string()),
805        );
806        map.insert(
807            "symbol".to_string(),
808            serde_json::Value::String(market.id.clone()),
809        );
810        map.insert(
811            "side".to_string(),
812            serde_json::Value::String(match request.side {
813                OrderSide::Buy => "Buy".to_string(),
814                OrderSide::Sell => "Sell".to_string(),
815            }),
816        );
817        map.insert(
818            "orderType".to_string(),
819            serde_json::Value::String(match request.order_type {
820                OrderType::Market
821                | OrderType::StopLoss
822                | OrderType::StopMarket
823                | OrderType::TakeProfit
824                | OrderType::TrailingStop => "Market".to_string(),
825                _ => "Limit".to_string(),
826            }),
827        );
828        map.insert(
829            "qty".to_string(),
830            serde_json::Value::String(request.amount.to_string()),
831        );
832
833        // Add price for limit orders
834        if let Some(p) = request.price {
835            if request.order_type == OrderType::Limit || request.order_type == OrderType::LimitMaker
836            {
837                map.insert(
838                    "price".to_string(),
839                    serde_json::Value::String(p.to_string()),
840                );
841            }
842        }
843
844        // Handle time in force
845        if let Some(tif) = request.time_in_force {
846            let tif_str = match tif {
847                TimeInForce::GTC => "GTC",
848                TimeInForce::IOC => "IOC",
849                TimeInForce::FOK => "FOK",
850                TimeInForce::PO => "PostOnly",
851            };
852            map.insert(
853                "timeInForce".to_string(),
854                serde_json::Value::String(tif_str.to_string()),
855            );
856        } else if request.order_type == OrderType::LimitMaker || request.post_only == Some(true) {
857            map.insert(
858                "timeInForce".to_string(),
859                serde_json::Value::String("PostOnly".to_string()),
860            );
861        }
862
863        // Handle client order ID
864        if let Some(client_id) = request.client_order_id {
865            map.insert(
866                "orderLinkId".to_string(),
867                serde_json::Value::String(client_id),
868            );
869        }
870
871        // Handle reduce only
872        if let Some(reduce_only) = request.reduce_only {
873            map.insert(
874                "reduceOnly".to_string(),
875                serde_json::Value::Bool(reduce_only),
876            );
877        }
878
879        // Handle stop price / trigger price
880        if let Some(trigger) = request.trigger_price.or(request.stop_price) {
881            map.insert(
882                "triggerPrice".to_string(),
883                serde_json::Value::String(trigger.to_string()),
884            );
885        }
886
887        // Handle take profit price
888        if let Some(tp) = request.take_profit_price {
889            map.insert(
890                "takeProfit".to_string(),
891                serde_json::Value::String(tp.to_string()),
892            );
893        }
894
895        // Handle stop loss price
896        if let Some(sl) = request.stop_loss_price {
897            map.insert(
898                "stopLoss".to_string(),
899                serde_json::Value::String(sl.to_string()),
900            );
901        }
902
903        // Handle position side (for hedge mode)
904        if let Some(pos_side) = request.position_side {
905            map.insert(
906                "positionIdx".to_string(),
907                serde_json::Value::String(match pos_side.as_str() {
908                    "LONG" => "1".to_string(),
909                    "SHORT" => "2".to_string(),
910                    _ => "0".to_string(), // BOTH
911                }),
912            );
913        }
914
915        let body = serde_json::Value::Object(map);
916
917        let response = self
918            .signed_request(&path)
919            .method(HttpMethod::Post)
920            .body(body)
921            .execute()
922            .await?;
923
924        let result = response
925            .get("result")
926            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
927
928        parser::parse_order(result, Some(&market))
929    }
930
931    /// Create a new order (deprecated).
932    ///
933    /// # Deprecated
934    ///
935    /// This method is deprecated. Use [`create_order_v2`](Self::create_order_v2) with
936    /// [`OrderRequest::builder()`] instead for a more ergonomic API.
937    ///
938    /// # Arguments
939    ///
940    /// * `symbol` - Trading pair symbol.
941    /// * `order_type` - Order type (Market, Limit).
942    /// * `side` - Order side (Buy or Sell).
943    /// * `amount` - Order quantity as [`Amount`] type.
944    /// * `price` - Optional price as [`Price`] type (required for limit orders).
945    ///
946    /// # Returns
947    ///
948    /// Returns the created [`Order`] structure with order details.
949    #[deprecated(
950        since = "0.2.0",
951        note = "Use create_order_v2 with OrderRequest::builder() instead"
952    )]
953    pub async fn create_order(
954        &self,
955        symbol: &str,
956        order_type: OrderType,
957        side: OrderSide,
958        amount: Amount,
959        price: Option<Price>,
960    ) -> Result<Order> {
961        use crate::bybit::signed_request::HttpMethod;
962
963        let market = self.base().market(symbol).await?;
964
965        let path = Self::build_api_path("/order/create");
966
967        // Build order body
968        let mut map = serde_json::Map::new();
969        map.insert(
970            "category".to_string(),
971            serde_json::Value::String(self.get_category().to_string()),
972        );
973        map.insert(
974            "symbol".to_string(),
975            serde_json::Value::String(market.id.clone()),
976        );
977        map.insert(
978            "side".to_string(),
979            serde_json::Value::String(match side {
980                OrderSide::Buy => "Buy".to_string(),
981                OrderSide::Sell => "Sell".to_string(),
982            }),
983        );
984        map.insert(
985            "orderType".to_string(),
986            serde_json::Value::String(match order_type {
987                OrderType::Market => "Market".to_string(),
988                _ => "Limit".to_string(),
989            }),
990        );
991        map.insert(
992            "qty".to_string(),
993            serde_json::Value::String(amount.to_string()),
994        );
995
996        // Add price for limit orders
997        if let Some(p) = price {
998            if order_type == OrderType::Limit || order_type == OrderType::LimitMaker {
999                map.insert(
1000                    "price".to_string(),
1001                    serde_json::Value::String(p.to_string()),
1002                );
1003            }
1004        }
1005
1006        // Add time in force for limit maker orders
1007        if order_type == OrderType::LimitMaker {
1008            map.insert(
1009                "timeInForce".to_string(),
1010                serde_json::Value::String("PostOnly".to_string()),
1011            );
1012        }
1013
1014        let body = serde_json::Value::Object(map);
1015
1016        let response = self
1017            .signed_request(&path)
1018            .method(HttpMethod::Post)
1019            .body(body)
1020            .execute()
1021            .await?;
1022
1023        let result = response
1024            .get("result")
1025            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1026
1027        parser::parse_order(result, Some(&market))
1028    }
1029
1030    /// Cancel an existing order.
1031    ///
1032    /// # Arguments
1033    ///
1034    /// * `id` - Order ID to cancel.
1035    /// * `symbol` - Trading pair symbol.
1036    ///
1037    /// # Returns
1038    ///
1039    /// Returns the canceled [`Order`] structure.
1040    pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
1041        use crate::bybit::signed_request::HttpMethod;
1042
1043        let market = self.base().market(symbol).await?;
1044
1045        let path = Self::build_api_path("/order/cancel");
1046
1047        let mut map = serde_json::Map::new();
1048        map.insert(
1049            "category".to_string(),
1050            serde_json::Value::String(self.get_category().to_string()),
1051        );
1052        map.insert(
1053            "symbol".to_string(),
1054            serde_json::Value::String(market.id.clone()),
1055        );
1056        map.insert(
1057            "orderId".to_string(),
1058            serde_json::Value::String(id.to_string()),
1059        );
1060        let body = serde_json::Value::Object(map);
1061
1062        let response = self
1063            .signed_request(&path)
1064            .method(HttpMethod::Post)
1065            .body(body)
1066            .execute()
1067            .await?;
1068
1069        let result = response
1070            .get("result")
1071            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1072
1073        parser::parse_order(result, Some(&market))
1074    }
1075
1076    /// Fetch a single order by ID.
1077    ///
1078    /// # Arguments
1079    ///
1080    /// * `id` - Order ID to fetch.
1081    /// * `symbol` - Trading pair symbol.
1082    ///
1083    /// # Returns
1084    ///
1085    /// Returns the [`Order`] structure with current status.
1086    pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
1087        let market = self.base().market(symbol).await?;
1088
1089        let path = Self::build_api_path("/order/realtime");
1090
1091        let response = self
1092            .signed_request(&path)
1093            .param("category", self.get_category())
1094            .param("symbol", &market.id)
1095            .param("orderId", id)
1096            .execute()
1097            .await?;
1098
1099        let result = response
1100            .get("result")
1101            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1102
1103        let list = result
1104            .get("list")
1105            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
1106
1107        let orders = list.as_array().ok_or_else(|| {
1108            Error::from(ParseError::invalid_format(
1109                "list",
1110                "Expected array of orders",
1111            ))
1112        })?;
1113
1114        if orders.is_empty() {
1115            return Err(Error::exchange("110008", "Order not found"));
1116        }
1117
1118        parser::parse_order(&orders[0], Some(&market))
1119    }
1120
1121    /// Fetch open orders.
1122    ///
1123    /// # Arguments
1124    ///
1125    /// * `symbol` - Optional trading pair symbol. If None, fetches all open orders.
1126    /// * `since` - Optional start timestamp in milliseconds.
1127    /// * `limit` - Optional limit on number of orders (maximum: 50).
1128    ///
1129    /// # Returns
1130    ///
1131    /// Returns a vector of open [`Order`] structures.
1132    pub async fn fetch_open_orders(
1133        &self,
1134        symbol: Option<&str>,
1135        since: Option<i64>,
1136        limit: Option<u32>,
1137    ) -> Result<Vec<Order>> {
1138        let path = Self::build_api_path("/order/realtime");
1139
1140        // Bybit maximum limit is 50
1141        let actual_limit = limit.map_or(50, |l| l.min(50));
1142
1143        let market = if let Some(sym) = symbol {
1144            Some(self.base().market(sym).await?)
1145        } else {
1146            None
1147        };
1148
1149        let mut builder = self
1150            .signed_request(&path)
1151            .param("category", self.get_category())
1152            .param("limit", actual_limit)
1153            .optional_param("startTime", since);
1154
1155        if let Some(ref m) = market {
1156            builder = builder.param("symbol", &m.id);
1157        }
1158
1159        let response = builder.execute().await?;
1160
1161        let result = response
1162            .get("result")
1163            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1164
1165        let list = result
1166            .get("list")
1167            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
1168
1169        let orders_array = list.as_array().ok_or_else(|| {
1170            Error::from(ParseError::invalid_format(
1171                "list",
1172                "Expected array of orders",
1173            ))
1174        })?;
1175
1176        let mut orders = Vec::new();
1177        for order_data in orders_array {
1178            match parser::parse_order(order_data, market.as_deref()) {
1179                Ok(order) => orders.push(order),
1180                Err(e) => {
1181                    warn!(error = %e, "Failed to parse open order");
1182                }
1183            }
1184        }
1185
1186        Ok(orders)
1187    }
1188
1189    /// Fetch closed orders.
1190    ///
1191    /// # Arguments
1192    ///
1193    /// * `symbol` - Optional trading pair symbol. If None, fetches all closed orders.
1194    /// * `since` - Optional start timestamp in milliseconds.
1195    /// * `limit` - Optional limit on number of orders (maximum: 50).
1196    ///
1197    /// # Returns
1198    ///
1199    /// Returns a vector of closed [`Order`] structures.
1200    pub async fn fetch_closed_orders(
1201        &self,
1202        symbol: Option<&str>,
1203        since: Option<i64>,
1204        limit: Option<u32>,
1205    ) -> Result<Vec<Order>> {
1206        let path = Self::build_api_path("/order/history");
1207
1208        // Bybit maximum limit is 50
1209        let actual_limit = limit.map_or(50, |l| l.min(50));
1210
1211        let market = if let Some(sym) = symbol {
1212            Some(self.base().market(sym).await?)
1213        } else {
1214            None
1215        };
1216
1217        let mut builder = self
1218            .signed_request(&path)
1219            .param("category", self.get_category())
1220            .param("limit", actual_limit)
1221            .optional_param("startTime", since);
1222
1223        if let Some(ref m) = market {
1224            builder = builder.param("symbol", &m.id);
1225        }
1226
1227        let response = builder.execute().await?;
1228
1229        let result = response
1230            .get("result")
1231            .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1232
1233        let list = result
1234            .get("list")
1235            .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
1236
1237        let orders_array = list.as_array().ok_or_else(|| {
1238            Error::from(ParseError::invalid_format(
1239                "list",
1240                "Expected array of orders",
1241            ))
1242        })?;
1243
1244        let mut orders = Vec::new();
1245        for order_data in orders_array {
1246            match parser::parse_order(order_data, market.as_deref()) {
1247                Ok(order) => orders.push(order),
1248                Err(e) => {
1249                    warn!(error = %e, "Failed to parse closed order");
1250                }
1251            }
1252        }
1253
1254        Ok(orders)
1255    }
1256}
1257
1258#[cfg(test)]
1259mod tests {
1260    use super::*;
1261
1262    #[test]
1263    fn test_build_api_path() {
1264        let path = Bybit::build_api_path("/market/instruments-info");
1265        assert_eq!(path, "/v5/market/instruments-info");
1266    }
1267
1268    #[test]
1269    fn test_get_category_spot() {
1270        let bybit = Bybit::builder().build().unwrap();
1271        let category = bybit.get_category();
1272        assert_eq!(category, "spot");
1273    }
1274
1275    #[test]
1276    fn test_get_category_linear() {
1277        let bybit = Bybit::builder().account_type("LINEAR").build().unwrap();
1278        let category = bybit.get_category();
1279        assert_eq!(category, "linear");
1280    }
1281
1282    #[test]
1283    fn test_get_timestamp() {
1284        let _bybit = Bybit::builder().build().unwrap();
1285        let ts = Bybit::get_timestamp();
1286
1287        // Should be a valid timestamp string
1288        assert!(!ts.is_empty());
1289        let parsed: i64 = ts.parse().unwrap();
1290        assert!(parsed > 0);
1291    }
1292}