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