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(×tamp, method, &sign_path, &body_string);
153
154 // Build headers
155 let mut headers = HeaderMap::new();
156 auth.add_auth_headers(&mut headers, ×tamp, &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(¶ms)).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(¶ms)).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(¶ms)).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(¶ms)).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(¶ms), 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(¶ms), 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(¶ms), 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(¶ms), 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}