ccxt_exchanges/bitget/
rest.rs

1//! Bitget REST API implementation.
2//!
3//! Implements all REST API endpoint operations for the Bitget exchange.
4
5use super::{Bitget, BitgetAuth, 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 Bitget {
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<BitgetAuth> {
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        let passphrase = config
51            .password
52            .as_ref()
53            .ok_or_else(|| Error::authentication("Passphrase is required"))?;
54
55        Ok(BitgetAuth::new(
56            api_key.expose_secret().to_string(),
57            secret.expose_secret().to_string(),
58            passphrase.expose_secret().to_string(),
59        ))
60    }
61
62    /// Check that required credentials are configured.
63    pub fn check_required_credentials(&self) -> Result<()> {
64        self.base().check_required_credentials()?;
65        if self.base().config.password.is_none() {
66            return Err(Error::authentication("Passphrase is required for Bitget"));
67        }
68        Ok(())
69    }
70
71    /// Build the API path with product type prefix.
72    ///
73    /// Uses the effective product type derived from `default_type` and `default_sub_type`
74    /// to determine the correct API endpoint:
75    /// - "spot" -> /api/v2/spot
76    /// - "umcbl" (USDT-M) -> /api/v2/mix
77    /// - "dmcbl" (Coin-M) -> /api/v2/mix
78    fn build_api_path(&self, endpoint: &str) -> String {
79        let product_type = self.options().effective_product_type();
80        match product_type {
81            "umcbl" | "usdt-futures" | "dmcbl" | "coin-futures" => {
82                format!("/api/v2/mix{}", endpoint)
83            }
84            _ => format!("/api/v2/spot{}", endpoint),
85        }
86    }
87
88    /// Make a public API request (no authentication required).
89    async fn public_request(
90        &self,
91        method: &str,
92        path: &str,
93        params: Option<&HashMap<String, String>>,
94    ) -> Result<Value> {
95        let urls = self.urls();
96        let mut url = format!("{}{}", urls.rest, path);
97
98        if let Some(p) = params {
99            if !p.is_empty() {
100                let query: Vec<String> = p
101                    .iter()
102                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
103                    .collect();
104                url = format!("{}?{}", url, query.join("&"));
105            }
106        }
107
108        debug!("Bitget public request: {} {}", method, url);
109
110        let response = match method.to_uppercase().as_str() {
111            "GET" => self.base().http_client.get(&url, None).await?,
112            "POST" => self.base().http_client.post(&url, None, None).await?,
113            _ => {
114                return Err(Error::invalid_request(format!(
115                    "Unsupported HTTP method: {}",
116                    method
117                )));
118            }
119        };
120
121        // Check for Bitget error response
122        if error::is_error_response(&response) {
123            return Err(error::parse_error(&response));
124        }
125
126        Ok(response)
127    }
128
129    /// Make a private API request (authentication required).
130    ///
131    /// # Deprecated
132    ///
133    /// This method is deprecated. Use [`signed_request()`](Self::signed_request) instead.
134    /// The `signed_request()` builder provides a cleaner, more maintainable API for
135    /// constructing authenticated requests.
136    #[deprecated(
137        since = "0.1.0",
138        note = "Use `signed_request()` builder instead for cleaner, more maintainable code"
139    )]
140    #[allow(dead_code)]
141    #[allow(deprecated)]
142    async fn private_request(
143        &self,
144        method: &str,
145        path: &str,
146        params: Option<&HashMap<String, String>>,
147        body: Option<&Value>,
148    ) -> Result<Value> {
149        self.check_required_credentials()?;
150
151        let auth = self.get_auth()?;
152        let urls = self.urls();
153        let timestamp = Self::get_timestamp();
154
155        // Build query string for GET requests
156        let query_string = if let Some(p) = params {
157            if p.is_empty() {
158                String::new()
159            } else {
160                let query: Vec<String> = p
161                    .iter()
162                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
163                    .collect();
164                format!("?{}", query.join("&"))
165            }
166        } else {
167            String::new()
168        };
169
170        // Build body string for POST requests
171        let body_string = body
172            .map(|b| serde_json::to_string(b).unwrap_or_default())
173            .unwrap_or_default();
174
175        // Sign the request
176        let sign_path = format!("{}{}", path, query_string);
177        let signature = auth.sign(&timestamp, method, &sign_path, &body_string);
178
179        // Build headers
180        let mut headers = HeaderMap::new();
181        auth.add_auth_headers(&mut headers, &timestamp, &signature);
182        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
183
184        let url = format!("{}{}{}", urls.rest, path, query_string);
185        debug!("Bitget private request: {} {}", method, url);
186
187        let response = match method.to_uppercase().as_str() {
188            "GET" => self.base().http_client.get(&url, Some(headers)).await?,
189            "POST" => {
190                let body_value = body.cloned();
191                self.base()
192                    .http_client
193                    .post(&url, Some(headers), body_value)
194                    .await?
195            }
196            "DELETE" => {
197                self.base()
198                    .http_client
199                    .delete(&url, Some(headers), None)
200                    .await?
201            }
202            _ => {
203                return Err(Error::invalid_request(format!(
204                    "Unsupported HTTP method: {}",
205                    method
206                )));
207            }
208        };
209
210        // Check for Bitget error response
211        if error::is_error_response(&response) {
212            return Err(error::parse_error(&response));
213        }
214
215        Ok(response)
216    }
217
218    // ============================================================================
219    // Public API Methods - Market Data
220    // ============================================================================
221
222    /// Fetch all trading markets.
223    ///
224    /// # Returns
225    ///
226    /// Returns a vector of [`Market`] structures containing market information.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if the API request fails or response parsing fails.
231    ///
232    /// # Example
233    ///
234    /// ```no_run
235    /// # use ccxt_exchanges::bitget::Bitget;
236    /// # async fn example() -> ccxt_core::Result<()> {
237    /// let bitget = Bitget::builder().build()?;
238    /// let markets = bitget.fetch_markets().await?;
239    /// println!("Found {} markets", markets.len());
240    /// # Ok(())
241    /// # }
242    /// ```
243    pub async fn fetch_markets(&self) -> Result<Arc<HashMap<String, Arc<Market>>>> {
244        let path = self.build_api_path("/public/symbols");
245        let response = self.public_request("GET", &path, None).await?;
246
247        let data = response
248            .get("data")
249            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
250
251        let symbols = data.as_array().ok_or_else(|| {
252            Error::from(ParseError::invalid_format(
253                "data",
254                "Expected array of symbols",
255            ))
256        })?;
257
258        let mut markets = Vec::new();
259        for symbol in symbols {
260            match parser::parse_market(symbol) {
261                Ok(market) => markets.push(market),
262                Err(e) => {
263                    warn!(error = %e, "Failed to parse market");
264                }
265            }
266        }
267
268        // Cache the markets and preserve ownership for the caller
269        let result = self.base().set_markets(markets, None).await?;
270
271        info!("Loaded {} markets for Bitget", result.len());
272        Ok(result)
273    }
274
275    /// Load and cache market data.
276    ///
277    /// If markets are already loaded and `reload` is false, returns cached data.
278    ///
279    /// # Arguments
280    ///
281    /// * `reload` - Whether to force reload market data from the API.
282    ///
283    /// # Returns
284    ///
285    /// Returns a `HashMap` containing all market data, keyed by symbol (e.g., "BTC/USDT").
286    ///
287    /// # Errors
288    ///
289    /// Returns an error if the API request fails or response parsing fails.
290    ///
291    /// # Example
292    ///
293    /// ```no_run
294    /// # use ccxt_exchanges::bitget::Bitget;
295    /// # async fn example() -> ccxt_core::Result<()> {
296    /// let bitget = Bitget::builder().build()?;
297    ///
298    /// // Load markets for the first time
299    /// let markets = bitget.load_markets(false).await?;
300    /// println!("Loaded {} markets", markets.len());
301    ///
302    /// // Subsequent calls use cache (no API request)
303    /// let markets = bitget.load_markets(false).await?;
304    ///
305    /// // Force reload
306    /// let markets = bitget.load_markets(true).await?;
307    /// # Ok(())
308    /// # }
309    /// ```
310    pub async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
311        // Acquire the loading lock to serialize concurrent load_markets calls
312        // This prevents multiple tasks from making duplicate API calls
313        let _loading_guard = self.base().market_loading_lock.lock().await;
314
315        // Check cache status while holding the lock
316        {
317            let cache = self.base().market_cache.read().await;
318            if cache.loaded && !reload {
319                debug!(
320                    "Returning cached markets for Bitget ({} markets)",
321                    cache.markets.len()
322                );
323                return Ok(cache.markets.clone());
324            }
325        }
326
327        info!("Loading markets for Bitget (reload: {})", reload);
328        let _markets = self.fetch_markets().await?;
329
330        let cache = self.base().market_cache.read().await;
331        Ok(cache.markets.clone())
332    }
333
334    /// Fetch ticker for a single trading pair.
335    ///
336    /// # Arguments
337    ///
338    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
339    ///
340    /// # Returns
341    ///
342    /// Returns [`Ticker`] data for the specified symbol.
343    ///
344    /// # Errors
345    ///
346    /// Returns an error if the market is not found or the API request fails.
347    ///
348    /// # Example
349    ///
350    /// ```no_run
351    /// # use ccxt_exchanges::bitget::Bitget;
352    /// # async fn example() -> ccxt_core::Result<()> {
353    /// let bitget = Bitget::builder().build()?;
354    /// bitget.load_markets(false).await?;
355    /// let ticker = bitget.fetch_ticker("BTC/USDT").await?;
356    /// println!("BTC/USDT last price: {:?}", ticker.last);
357    /// # Ok(())
358    /// # }
359    /// ```
360    pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
361        let market = self.base().market(symbol).await?;
362
363        let path = self.build_api_path("/market/tickers");
364        let mut params = HashMap::new();
365        params.insert("symbol".to_string(), market.id.clone());
366
367        let response = self.public_request("GET", &path, Some(&params)).await?;
368
369        let data = response
370            .get("data")
371            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
372
373        // Bitget returns an array even for single ticker
374        let tickers = data.as_array().ok_or_else(|| {
375            Error::from(ParseError::invalid_format(
376                "data",
377                "Expected array of tickers",
378            ))
379        })?;
380
381        if tickers.is_empty() {
382            return Err(Error::bad_symbol(format!("No ticker data for {}", symbol)));
383        }
384
385        parser::parse_ticker(&tickers[0], Some(&market))
386    }
387
388    /// Fetch tickers for multiple trading pairs.
389    ///
390    /// # Arguments
391    ///
392    /// * `symbols` - Optional list of trading pair symbols; fetches all if `None`.
393    ///
394    /// # Returns
395    ///
396    /// Returns a vector of [`Ticker`] structures.
397    ///
398    /// # Errors
399    ///
400    /// Returns an error if markets are not loaded or the API request fails.
401    ///
402    /// # Example
403    ///
404    /// ```no_run
405    /// # use ccxt_exchanges::bitget::Bitget;
406    /// # async fn example() -> ccxt_core::Result<()> {
407    /// let bitget = Bitget::builder().build()?;
408    /// bitget.load_markets(false).await?;
409    ///
410    /// // Fetch all tickers
411    /// let all_tickers = bitget.fetch_tickers(None).await?;
412    ///
413    /// // Fetch specific tickers
414    /// let tickers = bitget.fetch_tickers(Some(vec!["BTC/USDT".to_string(), "ETH/USDT".to_string()])).await?;
415    /// # Ok(())
416    /// # }
417    /// ```
418    pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
419        let cache = self.base().market_cache.read().await;
420        if !cache.loaded {
421            drop(cache);
422            return Err(Error::exchange(
423                "-1",
424                "Markets not loaded. Call load_markets() first.",
425            ));
426        }
427        drop(cache);
428
429        let path = self.build_api_path("/market/tickers");
430        let response = self.public_request("GET", &path, None).await?;
431
432        let data = response
433            .get("data")
434            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
435
436        let tickers_array = data.as_array().ok_or_else(|| {
437            Error::from(ParseError::invalid_format(
438                "data",
439                "Expected array of tickers",
440            ))
441        })?;
442
443        let mut tickers = Vec::new();
444        for ticker_data in tickers_array {
445            if let Some(bitget_symbol) = ticker_data["symbol"].as_str() {
446                let cache = self.base().market_cache.read().await;
447                if let Some(market) = cache.markets_by_id.get(bitget_symbol) {
448                    let market_clone = market.clone();
449                    drop(cache);
450
451                    match parser::parse_ticker(ticker_data, Some(&market_clone)) {
452                        Ok(ticker) => {
453                            if let Some(ref syms) = symbols {
454                                if syms.contains(&ticker.symbol) {
455                                    tickers.push(ticker);
456                                }
457                            } else {
458                                tickers.push(ticker);
459                            }
460                        }
461                        Err(e) => {
462                            warn!(
463                                error = %e,
464                                symbol = %bitget_symbol,
465                                "Failed to parse ticker"
466                            );
467                        }
468                    }
469                } else {
470                    drop(cache);
471                }
472            }
473        }
474
475        Ok(tickers)
476    }
477
478    // ============================================================================
479    // Public API Methods - Order Book and Trades
480    // ============================================================================
481
482    /// Fetch order book for a trading pair.
483    ///
484    /// # Arguments
485    ///
486    /// * `symbol` - Trading pair symbol.
487    /// * `limit` - Optional depth limit (valid values: 1, 5, 15, 50, 100; default: 100).
488    ///
489    /// # Returns
490    ///
491    /// Returns [`OrderBook`] data containing bids and asks.
492    ///
493    /// # Errors
494    ///
495    /// Returns an error if the market is not found or the API request fails.
496    ///
497    /// # Example
498    ///
499    /// ```no_run
500    /// # use ccxt_exchanges::bitget::Bitget;
501    /// # async fn example() -> ccxt_core::Result<()> {
502    /// let bitget = Bitget::builder().build()?;
503    /// bitget.load_markets(false).await?;
504    /// let orderbook = bitget.fetch_order_book("BTC/USDT", Some(50)).await?;
505    /// println!("Best bid: {:?}", orderbook.bids.first());
506    /// println!("Best ask: {:?}", orderbook.asks.first());
507    /// # Ok(())
508    /// # }
509    /// ```
510    pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
511        let market = self.base().market(symbol).await?;
512
513        let path = self.build_api_path("/market/orderbook");
514        let mut params = HashMap::new();
515        params.insert("symbol".to_string(), market.id.clone());
516
517        // Bitget valid limits: 1, 5, 15, 50, 100
518        // Cap to maximum allowed value
519        let actual_limit = limit.map_or(100, |l| l.min(100));
520        params.insert("limit".to_string(), actual_limit.to_string());
521
522        let response = self.public_request("GET", &path, Some(&params)).await?;
523
524        let data = response
525            .get("data")
526            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
527
528        parser::parse_orderbook(data, market.symbol.clone())
529    }
530
531    /// Fetch recent public trades.
532    ///
533    /// # Arguments
534    ///
535    /// * `symbol` - Trading pair symbol.
536    /// * `limit` - Optional limit on number of trades (maximum: 500).
537    ///
538    /// # Returns
539    ///
540    /// Returns a vector of [`Trade`] structures, sorted by timestamp in descending order.
541    ///
542    /// # Errors
543    ///
544    /// Returns an error if the market is not found or the API request fails.
545    ///
546    /// # Example
547    ///
548    /// ```no_run
549    /// # use ccxt_exchanges::bitget::Bitget;
550    /// # async fn example() -> ccxt_core::Result<()> {
551    /// let bitget = Bitget::builder().build()?;
552    /// bitget.load_markets(false).await?;
553    /// let trades = bitget.fetch_trades("BTC/USDT", Some(100)).await?;
554    /// for trade in trades.iter().take(5) {
555    ///     println!("Trade: {:?} @ {:?}", trade.amount, trade.price);
556    /// }
557    /// # Ok(())
558    /// # }
559    /// ```
560    pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
561        let market = self.base().market(symbol).await?;
562
563        let path = self.build_api_path("/market/fills");
564        let mut params = HashMap::new();
565        params.insert("symbol".to_string(), market.id.clone());
566
567        // Bitget maximum limit is 500
568        let actual_limit = limit.map_or(100, |l| l.min(500));
569        params.insert("limit".to_string(), actual_limit.to_string());
570
571        let response = self.public_request("GET", &path, Some(&params)).await?;
572
573        let data = response
574            .get("data")
575            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
576
577        let trades_array = data.as_array().ok_or_else(|| {
578            Error::from(ParseError::invalid_format(
579                "data",
580                "Expected array of trades",
581            ))
582        })?;
583
584        let mut trades = Vec::new();
585        for trade_data in trades_array {
586            match parser::parse_trade(trade_data, Some(&market)) {
587                Ok(trade) => trades.push(trade),
588                Err(e) => {
589                    warn!(error = %e, "Failed to parse trade");
590                }
591            }
592        }
593
594        // Sort by timestamp descending (newest first)
595        trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
596
597        Ok(trades)
598    }
599
600    /// Fetch OHLCV (candlestick) data.
601    ///
602    /// # Arguments
603    ///
604    /// * `symbol` - Trading pair symbol.
605    /// * `timeframe` - Candlestick timeframe (e.g., "1m", "5m", "1h", "1d").
606    /// * `since` - Optional start timestamp in milliseconds.
607    /// * `limit` - Optional limit on number of candles (maximum: 1000).
608    ///
609    /// # Returns
610    ///
611    /// Returns a vector of [`OHLCV`] structures.
612    ///
613    /// # Errors
614    ///
615    /// Returns an error if the market is not found or the API request fails.
616    ///
617    /// # Example
618    ///
619    /// ```no_run
620    /// # use ccxt_exchanges::bitget::Bitget;
621    /// # async fn example() -> ccxt_core::Result<()> {
622    /// let bitget = Bitget::builder().build()?;
623    /// bitget.load_markets(false).await?;
624    /// let ohlcv = bitget.fetch_ohlcv("BTC/USDT", "1h", None, Some(100)).await?;
625    /// for candle in ohlcv.iter().take(5) {
626    ///     println!("Open: {}, Close: {}", candle.open, candle.close);
627    /// }
628    /// # Ok(())
629    /// # }
630    /// ```
631    ///
632    /// _Requirements: 2.3, 2.6_
633    pub async fn fetch_ohlcv_v2(&self, request: OhlcvRequest) -> Result<Vec<OHLCV>> {
634        let market = self.base().market(&request.symbol).await?;
635
636        // Convert timeframe to Bitget format
637        let timeframes = self.timeframes();
638        let bitget_timeframe = timeframes.get(&request.timeframe).ok_or_else(|| {
639            Error::invalid_request(format!("Unsupported timeframe: {}", request.timeframe))
640        })?;
641
642        let path = self.build_api_path("/market/candles");
643        let mut params = HashMap::new();
644        params.insert("symbol".to_string(), market.id.clone());
645        params.insert("granularity".to_string(), bitget_timeframe.clone());
646
647        // Bitget maximum limit is 1000
648        let actual_limit = request.limit.map_or(100, |l| l.min(1000));
649        params.insert("limit".to_string(), actual_limit.to_string());
650
651        if let Some(start_time) = request.since {
652            params.insert("startTime".to_string(), start_time.to_string());
653        }
654
655        if let Some(end_time) = request.until {
656            params.insert("endTime".to_string(), end_time.to_string());
657        }
658
659        let response = self.public_request("GET", &path, Some(&params)).await?;
660
661        let data = response
662            .get("data")
663            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
664
665        let candles_array = data.as_array().ok_or_else(|| {
666            Error::from(ParseError::invalid_format(
667                "data",
668                "Expected array of candles",
669            ))
670        })?;
671
672        let mut ohlcv = Vec::new();
673        for candle_data in candles_array {
674            match parser::parse_ohlcv(candle_data) {
675                Ok(candle) => ohlcv.push(candle),
676                Err(e) => {
677                    warn!(error = %e, "Failed to parse OHLCV");
678                }
679            }
680        }
681
682        // Sort by timestamp ascending (oldest first)
683        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
684
685        Ok(ohlcv)
686    }
687
688    /// Fetch OHLCV (candlestick) data (deprecated).
689    ///
690    /// # Deprecated
691    ///
692    /// This method is deprecated. Use [`fetch_ohlcv_v2`](Self::fetch_ohlcv_v2) with
693    /// [`OhlcvRequest::builder()`] instead for a more ergonomic API.
694    ///
695    /// # Arguments
696    ///
697    /// * `symbol` - Trading pair symbol.
698    /// * `timeframe` - Candlestick timeframe (e.g., "1m", "5m", "1h", "1d").
699    /// * `since` - Optional start timestamp in milliseconds.
700    /// * `limit` - Optional limit on number of candles (maximum: 1000).
701    ///
702    /// # Returns
703    ///
704    /// Returns a vector of [`OHLCV`] structures.
705    ///
706    /// # Example
707    ///
708    /// ```no_run
709    /// # use ccxt_exchanges::bitget::Bitget;
710    /// # async fn example() -> ccxt_core::Result<()> {
711    /// let bitget = Bitget::builder().build()?;
712    /// bitget.load_markets(false).await?;
713    /// let ohlcv = bitget.fetch_ohlcv("BTC/USDT", "1h", None, Some(100)).await?;
714    /// for candle in ohlcv.iter().take(5) {
715    ///     println!("Open: {}, Close: {}", candle.open, candle.close);
716    /// }
717    /// # Ok(())
718    /// # }
719    /// ```
720    #[deprecated(
721        since = "0.2.0",
722        note = "Use fetch_ohlcv_v2 with OhlcvRequest::builder() instead"
723    )]
724    pub async fn fetch_ohlcv(
725        &self,
726        symbol: &str,
727        timeframe: &str,
728        since: Option<i64>,
729        limit: Option<u32>,
730    ) -> Result<Vec<OHLCV>> {
731        let market = self.base().market(symbol).await?;
732
733        // Convert timeframe to Bitget format
734        let timeframes = self.timeframes();
735        let bitget_timeframe = timeframes.get(timeframe).ok_or_else(|| {
736            Error::invalid_request(format!("Unsupported timeframe: {}", timeframe))
737        })?;
738
739        let path = self.build_api_path("/market/candles");
740        let mut params = HashMap::new();
741        params.insert("symbol".to_string(), market.id.clone());
742        params.insert("granularity".to_string(), bitget_timeframe.clone());
743
744        // Bitget maximum limit is 1000
745        let actual_limit = limit.map_or(100, |l| l.min(1000));
746        params.insert("limit".to_string(), actual_limit.to_string());
747
748        if let Some(start_time) = since {
749            params.insert("startTime".to_string(), start_time.to_string());
750        }
751
752        let response = self.public_request("GET", &path, Some(&params)).await?;
753
754        let data = response
755            .get("data")
756            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
757
758        let candles_array = data.as_array().ok_or_else(|| {
759            Error::from(ParseError::invalid_format(
760                "data",
761                "Expected array of candles",
762            ))
763        })?;
764
765        let mut ohlcv = Vec::new();
766        for candle_data in candles_array {
767            match parser::parse_ohlcv(candle_data) {
768                Ok(candle) => ohlcv.push(candle),
769                Err(e) => {
770                    warn!(error = %e, "Failed to parse OHLCV");
771                }
772            }
773        }
774
775        // Sort by timestamp ascending (oldest first)
776        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
777
778        Ok(ohlcv)
779    }
780
781    // ============================================================================
782    // Private API Methods - Account
783    // ============================================================================
784
785    /// Fetch account balances.
786    ///
787    /// # Returns
788    ///
789    /// Returns a [`Balance`] structure with all currency balances.
790    ///
791    /// # Errors
792    ///
793    /// Returns an error if authentication fails or the API request fails.
794    ///
795    /// # Example
796    ///
797    /// ```no_run
798    /// # use ccxt_exchanges::bitget::Bitget;
799    /// # async fn example() -> ccxt_core::Result<()> {
800    /// let bitget = Bitget::builder()
801    ///     .api_key("your-api-key")
802    ///     .secret("your-secret")
803    ///     .passphrase("your-passphrase")
804    ///     .build()?;
805    /// let balance = bitget.fetch_balance().await?;
806    /// if let Some(btc) = balance.get("BTC") {
807    ///     println!("BTC balance: free={}, used={}, total={}", btc.free, btc.used, btc.total);
808    /// }
809    /// # Ok(())
810    /// # }
811    /// ```
812    pub async fn fetch_balance(&self) -> Result<Balance> {
813        let path = self.build_api_path("/account/assets");
814        let response = self.signed_request(&path).execute().await?;
815
816        let data = response
817            .get("data")
818            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
819
820        parser::parse_balance(data)
821    }
822
823    /// Fetch user's trade history.
824    ///
825    /// # Arguments
826    ///
827    /// * `symbol` - Trading pair symbol.
828    /// * `since` - Optional start timestamp in milliseconds.
829    /// * `limit` - Optional limit on number of trades (maximum: 500).
830    ///
831    /// # Returns
832    ///
833    /// Returns a vector of [`Trade`] structures representing user's trade history.
834    ///
835    /// # Errors
836    ///
837    /// Returns an error if authentication fails or the API request fails.
838    ///
839    /// # Example
840    ///
841    /// ```no_run
842    /// # use ccxt_exchanges::bitget::Bitget;
843    /// # async fn example() -> ccxt_core::Result<()> {
844    /// let bitget = Bitget::builder()
845    ///     .api_key("your-api-key")
846    ///     .secret("your-secret")
847    ///     .passphrase("your-passphrase")
848    ///     .build()?;
849    /// bitget.load_markets(false).await?;
850    /// let my_trades = bitget.fetch_my_trades("BTC/USDT", None, Some(50)).await?;
851    /// # Ok(())
852    /// # }
853    /// ```
854    pub async fn fetch_my_trades(
855        &self,
856        symbol: &str,
857        since: Option<i64>,
858        limit: Option<u32>,
859    ) -> Result<Vec<Trade>> {
860        let market = self.base().market(symbol).await?;
861
862        let path = self.build_api_path("/trade/fills");
863
864        // Bitget maximum limit is 500
865        let actual_limit = limit.map_or(100, |l| l.min(500));
866
867        let mut builder = self
868            .signed_request(&path)
869            .param("symbol", &market.id)
870            .param("limit", actual_limit);
871
872        if let Some(start_time) = since {
873            builder = builder.param("startTime", start_time);
874        }
875
876        let response = builder.execute().await?;
877
878        let data = response
879            .get("data")
880            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
881
882        let trades_array = data.as_array().ok_or_else(|| {
883            Error::from(ParseError::invalid_format(
884                "data",
885                "Expected array of trades",
886            ))
887        })?;
888
889        let mut trades = Vec::new();
890        for trade_data in trades_array {
891            match parser::parse_trade(trade_data, Some(&market)) {
892                Ok(trade) => trades.push(trade),
893                Err(e) => {
894                    warn!(error = %e, "Failed to parse my trade");
895                }
896            }
897        }
898
899        Ok(trades)
900    }
901
902    // ============================================================================
903    // Private API Methods - Order Management
904    // ============================================================================
905
906    /// Create a new order.
907    ///
908    /// # Arguments
909    ///
910    /// * `symbol` - Trading pair symbol.
911    /// * `order_type` - Order type (Market, Limit).
912    /// * `side` - Order side (Buy or Sell).
913    /// * `amount` - Order quantity as [`Amount`] type.
914    /// * `price` - Optional price as [`Price`] type (required for limit orders).
915    ///
916    /// # Returns
917    ///
918    /// Returns the created [`Order`] structure with order details.
919    ///
920    /// # Errors
921    ///
922    /// Returns an error if authentication fails, market is not found, or the API request fails.
923    ///
924    /// # Example
925    ///
926    /// ```no_run
927    /// # use ccxt_exchanges::bitget::Bitget;
928    /// # use ccxt_core::types::{OrderType, OrderSide, Amount, Price};
929    /// # use rust_decimal_macros::dec;
930    /// # async fn example() -> ccxt_core::Result<()> {
931    /// let bitget = Bitget::builder()
932    ///     .api_key("your-api-key")
933    ///     .secret("your-secret")
934    ///     .passphrase("your-passphrase")
935    ///     .build()?;
936    /// bitget.load_markets(false).await?;
937    ///
938    /// // Create a limit buy order
939    /// let order = bitget.create_order(
940    ///     "BTC/USDT",
941    ///     OrderType::Limit,
942    ///     OrderSide::Buy,
943    ///     Amount::new(dec!(0.001)),
944    ///     Some(Price::new(dec!(50000.0))),
945    /// ).await?;
946    /// println!("Order created: {}", order.id);
947    /// # Ok(())
948    /// # }
949    /// ```
950    ///
951    /// _Requirements: 2.2, 2.6_
952    pub async fn create_order_v2(&self, request: OrderRequest) -> Result<Order> {
953        let market = self.base().market(&request.symbol).await?;
954
955        let path = self.build_api_path("/trade/place-order");
956
957        // Build order body
958        let mut map = serde_json::Map::new();
959        map.insert(
960            "symbol".to_string(),
961            serde_json::Value::String(market.id.clone()),
962        );
963        map.insert(
964            "side".to_string(),
965            serde_json::Value::String(match request.side {
966                OrderSide::Buy => "buy".to_string(),
967                OrderSide::Sell => "sell".to_string(),
968            }),
969        );
970        map.insert(
971            "orderType".to_string(),
972            serde_json::Value::String(match request.order_type {
973                OrderType::LimitMaker => "limit_maker".to_string(),
974                OrderType::Market
975                | OrderType::StopLoss
976                | OrderType::StopMarket
977                | OrderType::TakeProfit
978                | OrderType::TrailingStop => "market".to_string(),
979                _ => "limit".to_string(),
980            }),
981        );
982        map.insert(
983            "size".to_string(),
984            serde_json::Value::String(request.amount.to_string()),
985        );
986
987        // Handle time in force
988        let force = if let Some(tif) = request.time_in_force {
989            match tif {
990                TimeInForce::GTC => "gtc",
991                TimeInForce::IOC => "ioc",
992                TimeInForce::FOK => "fok",
993                TimeInForce::PO => "post_only",
994            }
995        } else if request.post_only == Some(true) {
996            "post_only"
997        } else {
998            "gtc"
999        };
1000        map.insert(
1001            "force".to_string(),
1002            serde_json::Value::String(force.to_string()),
1003        );
1004
1005        // Add price for limit orders
1006        if let Some(p) = request.price {
1007            if request.order_type == OrderType::Limit || request.order_type == OrderType::LimitMaker
1008            {
1009                map.insert(
1010                    "price".to_string(),
1011                    serde_json::Value::String(p.to_string()),
1012                );
1013            }
1014        }
1015
1016        // Handle client order ID
1017        if let Some(client_id) = request.client_order_id {
1018            map.insert(
1019                "clientOid".to_string(),
1020                serde_json::Value::String(client_id),
1021            );
1022        }
1023
1024        // Handle reduce only
1025        if let Some(reduce_only) = request.reduce_only {
1026            map.insert(
1027                "reduceOnly".to_string(),
1028                serde_json::Value::Bool(reduce_only),
1029            );
1030        }
1031
1032        // Handle stop price / trigger price
1033        if let Some(trigger) = request.trigger_price.or(request.stop_price) {
1034            map.insert(
1035                "triggerPrice".to_string(),
1036                serde_json::Value::String(trigger.to_string()),
1037            );
1038        }
1039
1040        // Handle take profit price
1041        if let Some(tp) = request.take_profit_price {
1042            map.insert(
1043                "presetTakeProfitPrice".to_string(),
1044                serde_json::Value::String(tp.to_string()),
1045            );
1046        }
1047
1048        // Handle stop loss price
1049        if let Some(sl) = request.stop_loss_price {
1050            map.insert(
1051                "presetStopLossPrice".to_string(),
1052                serde_json::Value::String(sl.to_string()),
1053            );
1054        }
1055
1056        let body = serde_json::Value::Object(map);
1057
1058        let response = self
1059            .signed_request(&path)
1060            .method(crate::bitget::signed_request::HttpMethod::Post)
1061            .body(body)
1062            .execute()
1063            .await?;
1064
1065        let data = response
1066            .get("data")
1067            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1068
1069        parser::parse_order(data, Some(&market))
1070    }
1071
1072    /// Create a new order (deprecated).
1073    ///
1074    /// # Deprecated
1075    ///
1076    /// This method is deprecated. Use [`create_order_v2`](Self::create_order_v2) with
1077    /// [`OrderRequest::builder()`] instead for a more ergonomic API.
1078    ///
1079    /// # Arguments
1080    ///
1081    /// * `symbol` - Trading pair symbol.
1082    /// * `order_type` - Order type (Market, Limit).
1083    /// * `side` - Order side (Buy or Sell).
1084    /// * `amount` - Order quantity as [`Amount`] type.
1085    /// * `price` - Optional price as [`Price`] type (required for limit orders).
1086    ///
1087    /// # Returns
1088    ///
1089    /// Returns the created [`Order`] structure with order details.
1090    ///
1091    /// # Errors
1092    ///
1093    /// Returns an error if authentication fails or the API request fails.
1094    ///
1095    /// # Example
1096    ///
1097    /// ```no_run
1098    /// # use rust_decimal_macros::dec;
1099    /// # use ccxt_core::types::{Amount, Price, OrderType, OrderSide};
1100    /// # use ccxt_exchanges::bitget::Bitget;
1101    /// # async fn example() -> ccxt_core::Result<()> {
1102    /// let bitget = Bitget::builder()
1103    ///     .api_key("your-api-key")
1104    ///     .secret("your-secret")
1105    ///     .passphrase("your-passphrase")
1106    ///     .build()?;
1107    /// bitget.load_markets(false).await?;
1108    ///
1109    /// // Create a limit buy order
1110    /// let order = bitget.create_order(
1111    ///     "BTC/USDT",
1112    ///     OrderType::Limit,
1113    ///     OrderSide::Buy,
1114    ///     Amount::new(dec!(0.001)),
1115    ///     Some(Price::new(dec!(50000.0))),
1116    /// ).await?;
1117    /// println!("Order created: {}", order.id);
1118    /// # Ok(())
1119    /// # }
1120    /// ```
1121    #[deprecated(
1122        since = "0.2.0",
1123        note = "Use create_order_v2 with OrderRequest::builder() instead"
1124    )]
1125    pub async fn create_order(
1126        &self,
1127        symbol: &str,
1128        order_type: OrderType,
1129        side: OrderSide,
1130        amount: Amount,
1131        price: Option<Price>,
1132    ) -> Result<Order> {
1133        let market = self.base().market(symbol).await?;
1134
1135        let path = self.build_api_path("/trade/place-order");
1136
1137        // Build order body
1138        let mut map = serde_json::Map::new();
1139        map.insert(
1140            "symbol".to_string(),
1141            serde_json::Value::String(market.id.clone()),
1142        );
1143        map.insert(
1144            "side".to_string(),
1145            serde_json::Value::String(match side {
1146                OrderSide::Buy => "buy".to_string(),
1147                OrderSide::Sell => "sell".to_string(),
1148            }),
1149        );
1150        map.insert(
1151            "orderType".to_string(),
1152            serde_json::Value::String(match order_type {
1153                OrderType::Market => "market".to_string(),
1154                OrderType::LimitMaker => "limit_maker".to_string(),
1155                _ => "limit".to_string(),
1156            }),
1157        );
1158        map.insert(
1159            "size".to_string(),
1160            serde_json::Value::String(amount.to_string()),
1161        );
1162        map.insert(
1163            "force".to_string(),
1164            serde_json::Value::String("gtc".to_string()),
1165        );
1166
1167        // Add price for limit orders
1168        if let Some(p) = price {
1169            if order_type == OrderType::Limit || order_type == OrderType::LimitMaker {
1170                map.insert(
1171                    "price".to_string(),
1172                    serde_json::Value::String(p.to_string()),
1173                );
1174            }
1175        }
1176        let body = serde_json::Value::Object(map);
1177
1178        let response = self
1179            .signed_request(&path)
1180            .method(crate::bitget::signed_request::HttpMethod::Post)
1181            .body(body)
1182            .execute()
1183            .await?;
1184
1185        let data = response
1186            .get("data")
1187            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1188
1189        parser::parse_order(data, Some(&market))
1190    }
1191
1192    /// Cancel an existing order.
1193    ///
1194    /// # Arguments
1195    ///
1196    /// * `id` - Order ID to cancel.
1197    /// * `symbol` - Trading pair symbol.
1198    ///
1199    /// # Returns
1200    ///
1201    /// Returns the canceled [`Order`] structure.
1202    ///
1203    /// # Errors
1204    ///
1205    /// Returns an error if authentication fails or the API request fails.
1206    ///
1207    /// # Example
1208    ///
1209    /// ```no_run
1210    /// # use ccxt_exchanges::bitget::Bitget;
1211    /// # async fn example() -> ccxt_core::Result<()> {
1212    /// let bitget = Bitget::builder()
1213    ///     .api_key("your-api-key")
1214    ///     .secret("your-secret")
1215    ///     .passphrase("your-passphrase")
1216    ///     .build()?;
1217    /// bitget.load_markets(false).await?;
1218    /// let order = bitget.cancel_order("123456789", "BTC/USDT").await?;
1219    /// # Ok(())
1220    /// # }
1221    /// ```
1222    pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
1223        let market = self.base().market(symbol).await?;
1224
1225        let path = self.build_api_path("/trade/cancel-order");
1226
1227        let mut map = serde_json::Map::new();
1228        map.insert(
1229            "symbol".to_string(),
1230            serde_json::Value::String(market.id.clone()),
1231        );
1232        map.insert(
1233            "orderId".to_string(),
1234            serde_json::Value::String(id.to_string()),
1235        );
1236        let body = serde_json::Value::Object(map);
1237
1238        let response = self
1239            .signed_request(&path)
1240            .method(crate::bitget::signed_request::HttpMethod::Post)
1241            .body(body)
1242            .execute()
1243            .await?;
1244
1245        let data = response
1246            .get("data")
1247            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1248
1249        parser::parse_order(data, Some(&market))
1250    }
1251
1252    /// Fetch a single order by ID.
1253    ///
1254    /// # Arguments
1255    ///
1256    /// * `id` - Order ID to fetch.
1257    /// * `symbol` - Trading pair symbol.
1258    ///
1259    /// # Returns
1260    ///
1261    /// Returns the [`Order`] structure with current status.
1262    ///
1263    /// # Errors
1264    ///
1265    /// Returns an error if authentication fails or the API request fails.
1266    ///
1267    /// # Example
1268    ///
1269    /// ```no_run
1270    /// # use ccxt_exchanges::bitget::Bitget;
1271    /// # async fn example() -> ccxt_core::Result<()> {
1272    /// let bitget = Bitget::builder()
1273    ///     .api_key("your-api-key")
1274    ///     .secret("your-secret")
1275    ///     .passphrase("your-passphrase")
1276    ///     .build()?;
1277    /// bitget.load_markets(false).await?;
1278    /// let order = bitget.fetch_order("123456789", "BTC/USDT").await?;
1279    /// println!("Order status: {:?}", order.status);
1280    /// # Ok(())
1281    /// # }
1282    /// ```
1283    pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
1284        let market = self.base().market(symbol).await?;
1285
1286        let path = self.build_api_path("/trade/orderInfo");
1287
1288        let response = self
1289            .signed_request(&path)
1290            .param("symbol", &market.id)
1291            .param("orderId", id)
1292            .execute()
1293            .await?;
1294
1295        let data = response
1296            .get("data")
1297            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1298
1299        // Bitget may return an array with single order
1300        let order_data = if data.is_array() {
1301            data.as_array()
1302                .and_then(|arr| arr.first())
1303                .ok_or_else(|| Error::exchange("40007", "Order not found"))?
1304        } else {
1305            data
1306        };
1307
1308        parser::parse_order(order_data, Some(&market))
1309    }
1310
1311    /// Fetch open orders.
1312    ///
1313    /// # Arguments
1314    ///
1315    /// * `symbol` - Optional trading pair symbol. If None, fetches all open orders.
1316    /// * `since` - Optional start timestamp in milliseconds.
1317    /// * `limit` - Optional limit on number of orders (maximum: 500).
1318    ///
1319    /// # Returns
1320    ///
1321    /// Returns a vector of open [`Order`] structures.
1322    ///
1323    /// # Errors
1324    ///
1325    /// Returns an error if authentication fails or the API request fails.
1326    ///
1327    /// # Example
1328    ///
1329    /// ```no_run
1330    /// # use ccxt_exchanges::bitget::Bitget;
1331    /// # async fn example() -> ccxt_core::Result<()> {
1332    /// let bitget = Bitget::builder()
1333    ///     .api_key("your-api-key")
1334    ///     .secret("your-secret")
1335    ///     .passphrase("your-passphrase")
1336    ///     .build()?;
1337    /// bitget.load_markets(false).await?;
1338    ///
1339    /// // Fetch all open orders
1340    /// let all_open = bitget.fetch_open_orders(None, None, None).await?;
1341    ///
1342    /// // Fetch open orders for specific symbol
1343    /// let btc_open = bitget.fetch_open_orders(Some("BTC/USDT"), None, Some(50)).await?;
1344    /// # Ok(())
1345    /// # }
1346    /// ```
1347    pub async fn fetch_open_orders(
1348        &self,
1349        symbol: Option<&str>,
1350        since: Option<i64>,
1351        limit: Option<u32>,
1352    ) -> Result<Vec<Order>> {
1353        let path = self.build_api_path("/trade/unfilled-orders");
1354
1355        let market = if let Some(sym) = symbol {
1356            Some(self.base().market(sym).await?)
1357        } else {
1358            None
1359        };
1360
1361        // Bitget maximum limit is 500
1362        let actual_limit = limit.map_or(100, |l| l.min(500));
1363
1364        let mut builder = self.signed_request(&path).param("limit", actual_limit);
1365
1366        if let Some(m) = &market {
1367            builder = builder.param("symbol", &m.id);
1368        }
1369
1370        if let Some(start_time) = since {
1371            builder = builder.param("startTime", start_time);
1372        }
1373
1374        let response = builder.execute().await?;
1375
1376        let data = response
1377            .get("data")
1378            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1379
1380        let orders_array = data.as_array().ok_or_else(|| {
1381            Error::from(ParseError::invalid_format(
1382                "data",
1383                "Expected array of orders",
1384            ))
1385        })?;
1386
1387        let mut orders = Vec::new();
1388        for order_data in orders_array {
1389            match parser::parse_order(order_data, market.as_deref()) {
1390                Ok(order) => orders.push(order),
1391                Err(e) => {
1392                    warn!(error = %e, "Failed to parse open order");
1393                }
1394            }
1395        }
1396
1397        Ok(orders)
1398    }
1399
1400    /// Fetch closed orders.
1401    ///
1402    /// # Arguments
1403    ///
1404    /// * `symbol` - Optional trading pair symbol. If None, fetches all closed orders.
1405    /// * `since` - Optional start timestamp in milliseconds.
1406    /// * `limit` - Optional limit on number of orders (maximum: 500).
1407    ///
1408    /// # Returns
1409    ///
1410    /// Returns a vector of closed [`Order`] structures.
1411    ///
1412    /// # Errors
1413    ///
1414    /// Returns an error if authentication fails or the API request fails.
1415    ///
1416    /// # Example
1417    ///
1418    /// ```no_run
1419    /// # use ccxt_exchanges::bitget::Bitget;
1420    /// # async fn example() -> ccxt_core::Result<()> {
1421    /// let bitget = Bitget::builder()
1422    ///     .api_key("your-api-key")
1423    ///     .secret("your-secret")
1424    ///     .passphrase("your-passphrase")
1425    ///     .build()?;
1426    /// bitget.load_markets(false).await?;
1427    ///
1428    /// // Fetch closed orders for specific symbol
1429    /// let closed = bitget.fetch_closed_orders(Some("BTC/USDT"), None, Some(50)).await?;
1430    /// # Ok(())
1431    /// # }
1432    /// ```
1433    pub async fn fetch_closed_orders(
1434        &self,
1435        symbol: Option<&str>,
1436        since: Option<i64>,
1437        limit: Option<u32>,
1438    ) -> Result<Vec<Order>> {
1439        let path = self.build_api_path("/trade/history-orders");
1440
1441        let market = if let Some(sym) = symbol {
1442            Some(self.base().market(sym).await?)
1443        } else {
1444            None
1445        };
1446
1447        // Bitget maximum limit is 500
1448        let actual_limit = limit.map_or(100, |l| l.min(500));
1449
1450        let mut builder = self.signed_request(&path).param("limit", actual_limit);
1451
1452        if let Some(m) = &market {
1453            builder = builder.param("symbol", &m.id);
1454        }
1455
1456        if let Some(start_time) = since {
1457            builder = builder.param("startTime", start_time);
1458        }
1459
1460        let response = builder.execute().await?;
1461
1462        let data = response
1463            .get("data")
1464            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1465
1466        let orders_array = data.as_array().ok_or_else(|| {
1467            Error::from(ParseError::invalid_format(
1468                "data",
1469                "Expected array of orders",
1470            ))
1471        })?;
1472
1473        let mut orders = Vec::new();
1474        for order_data in orders_array {
1475            match parser::parse_order(order_data, market.as_deref()) {
1476                Ok(order) => orders.push(order),
1477                Err(e) => {
1478                    warn!(error = %e, "Failed to parse closed order");
1479                }
1480            }
1481        }
1482
1483        Ok(orders)
1484    }
1485}
1486
1487#[cfg(test)]
1488mod tests {
1489    use super::*;
1490    use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
1491
1492    #[test]
1493    fn test_build_api_path_spot() {
1494        let bitget = Bitget::builder().build().unwrap();
1495        let path = bitget.build_api_path("/public/symbols");
1496        assert_eq!(path, "/api/v2/spot/public/symbols");
1497    }
1498
1499    #[test]
1500    fn test_build_api_path_futures_legacy() {
1501        // Legacy test using product_type directly
1502        // Note: product_type is kept for backward compatibility but
1503        // effective_product_type() now derives from default_type/default_sub_type
1504        // This test verifies that using default_type achieves the same result
1505        let bitget = Bitget::builder()
1506            .default_type(DefaultType::Swap)
1507            .default_sub_type(DefaultSubType::Linear)
1508            .build()
1509            .unwrap();
1510        let path = bitget.build_api_path("/public/symbols");
1511        assert_eq!(path, "/api/v2/mix/public/symbols");
1512    }
1513
1514    #[test]
1515    fn test_build_api_path_with_default_type_spot() {
1516        let bitget = Bitget::builder()
1517            .default_type(DefaultType::Spot)
1518            .build()
1519            .unwrap();
1520        let path = bitget.build_api_path("/public/symbols");
1521        assert_eq!(path, "/api/v2/spot/public/symbols");
1522    }
1523
1524    #[test]
1525    fn test_build_api_path_with_default_type_swap_linear() {
1526        let bitget = Bitget::builder()
1527            .default_type(DefaultType::Swap)
1528            .default_sub_type(DefaultSubType::Linear)
1529            .build()
1530            .unwrap();
1531        let path = bitget.build_api_path("/public/symbols");
1532        assert_eq!(path, "/api/v2/mix/public/symbols");
1533    }
1534
1535    #[test]
1536    fn test_build_api_path_with_default_type_swap_inverse() {
1537        let bitget = Bitget::builder()
1538            .default_type(DefaultType::Swap)
1539            .default_sub_type(DefaultSubType::Inverse)
1540            .build()
1541            .unwrap();
1542        let path = bitget.build_api_path("/public/symbols");
1543        assert_eq!(path, "/api/v2/mix/public/symbols");
1544    }
1545
1546    #[test]
1547    fn test_build_api_path_with_default_type_futures() {
1548        let bitget = Bitget::builder()
1549            .default_type(DefaultType::Futures)
1550            .build()
1551            .unwrap();
1552        let path = bitget.build_api_path("/public/symbols");
1553        // Futures defaults to Linear (umcbl) which uses mix API
1554        assert_eq!(path, "/api/v2/mix/public/symbols");
1555    }
1556
1557    #[test]
1558    fn test_build_api_path_with_default_type_margin() {
1559        let bitget = Bitget::builder()
1560            .default_type(DefaultType::Margin)
1561            .build()
1562            .unwrap();
1563        let path = bitget.build_api_path("/public/symbols");
1564        // Margin uses spot API
1565        assert_eq!(path, "/api/v2/spot/public/symbols");
1566    }
1567
1568    #[test]
1569    fn test_get_timestamp() {
1570        let _bitget = Bitget::builder().build().unwrap();
1571        let ts = Bitget::get_timestamp();
1572
1573        // Should be a valid timestamp string
1574        let parsed: i64 = ts.parse().unwrap();
1575        assert!(parsed > 0);
1576
1577        // Should be close to current time (within 1 second)
1578        let now = chrono::Utc::now().timestamp_millis();
1579        assert!((now - parsed).abs() < 1000);
1580    }
1581}