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