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