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