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