ccxt_exchanges/bitget/
mod.rs

1//! Bitget exchange implementation.
2//!
3//! Supports spot trading and futures trading (USDT-M and Coin-M) with REST API and WebSocket support.
4
5use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
6use ccxt_core::{BaseExchange, ExchangeConfig, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10pub mod auth;
11pub mod builder;
12pub mod endpoint_router;
13pub mod error;
14mod exchange_impl;
15pub mod parser;
16pub mod rest;
17pub mod signed_request;
18pub mod ws;
19mod ws_exchange_impl;
20
21pub use auth::BitgetAuth;
22pub use builder::BitgetBuilder;
23pub use endpoint_router::BitgetEndpointRouter;
24pub use error::{BitgetErrorCode, is_error_response, parse_error};
25pub use parser::{
26    datetime_to_timestamp, parse_balance, parse_market, parse_ohlcv, parse_order,
27    parse_order_status, parse_orderbook, parse_ticker, parse_trade, timestamp_to_datetime,
28};
29
30/// Bitget exchange structure.
31#[derive(Debug)]
32pub struct Bitget {
33    /// Base exchange instance.
34    base: BaseExchange,
35    /// Bitget-specific options.
36    options: BitgetOptions,
37}
38
39/// Bitget-specific options.
40///
41/// # Example
42///
43/// ```rust
44/// use ccxt_exchanges::bitget::BitgetOptions;
45/// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
46///
47/// let options = BitgetOptions {
48///     default_type: DefaultType::Swap,
49///     default_sub_type: Some(DefaultSubType::Linear),
50///     ..Default::default()
51/// };
52/// ```
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct BitgetOptions {
55    /// Product type: spot, umcbl (USDT-M), dmcbl (Coin-M).
56    ///
57    /// This is kept for backward compatibility with existing configurations.
58    /// When using `default_type` and `default_sub_type`, this field is automatically
59    /// derived from those values.
60    pub product_type: String,
61    /// Default trading type (spot/swap/futures/option).
62    ///
63    /// This determines which product type to use for API calls.
64    /// Bitget uses product_type-based filtering:
65    /// - `Spot` -> product_type=spot
66    /// - `Swap` + Linear -> product_type=umcbl (USDT-M)
67    /// - `Swap` + Inverse -> product_type=dmcbl (Coin-M)
68    /// - `Futures` + Linear -> product_type=umcbl (USDT-M futures)
69    /// - `Futures` + Inverse -> product_type=dmcbl (Coin-M futures)
70    #[serde(default)]
71    pub default_type: DefaultType,
72    /// Default sub-type for contract settlement (linear/inverse).
73    ///
74    /// - `Linear`: USDT-margined contracts (product_type=umcbl)
75    /// - `Inverse`: Coin-margined contracts (product_type=dmcbl)
76    ///
77    /// Only applicable when `default_type` is `Swap` or `Futures`.
78    /// Ignored for `Spot` type.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub default_sub_type: Option<DefaultSubType>,
81    /// Receive window in milliseconds.
82    pub recv_window: u64,
83    /// Enables testnet environment.
84    pub testnet: bool,
85}
86
87impl Default for BitgetOptions {
88    fn default() -> Self {
89        Self {
90            product_type: "spot".to_string(),
91            default_type: DefaultType::default(), // Defaults to Spot
92            default_sub_type: None,
93            recv_window: 5000,
94            testnet: false,
95        }
96    }
97}
98
99impl BitgetOptions {
100    /// Returns the effective product_type based on default_type and default_sub_type.
101    ///
102    /// This method maps the DefaultType and DefaultSubType to Bitget's product_type:
103    /// - `Spot` -> "spot"
104    /// - `Swap` + Linear (or None) -> "umcbl" (USDT-M perpetuals)
105    /// - `Swap` + Inverse -> "dmcbl" (Coin-M perpetuals)
106    /// - `Futures` + Linear (or None) -> "umcbl" (USDT-M futures)
107    /// - `Futures` + Inverse -> "dmcbl" (Coin-M futures)
108    /// - `Margin` -> "spot" (margin uses spot markets)
109    /// - `Option` -> "spot" (options not fully supported, fallback to spot)
110    ///
111    /// # Example
112    ///
113    /// ```rust
114    /// use ccxt_exchanges::bitget::BitgetOptions;
115    /// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
116    ///
117    /// let mut options = BitgetOptions::default();
118    /// options.default_type = DefaultType::Swap;
119    /// options.default_sub_type = Some(DefaultSubType::Linear);
120    /// assert_eq!(options.effective_product_type(), "umcbl");
121    ///
122    /// options.default_sub_type = Some(DefaultSubType::Inverse);
123    /// assert_eq!(options.effective_product_type(), "dmcbl");
124    /// ```
125    pub fn effective_product_type(&self) -> &str {
126        match self.default_type {
127            DefaultType::Swap | DefaultType::Futures => {
128                match self.default_sub_type.unwrap_or(DefaultSubType::Linear) {
129                    DefaultSubType::Linear => "umcbl",  // USDT-M
130                    DefaultSubType::Inverse => "dmcbl", // Coin-M
131                }
132            }
133            _ => "spot", // Margin, Spot and Option fallback to spot
134        }
135    }
136}
137
138impl Bitget {
139    /// Creates a new Bitget instance using the builder pattern.
140    ///
141    /// This is the recommended way to create a Bitget instance.
142    ///
143    /// # Example
144    ///
145    /// ```no_run
146    /// use ccxt_exchanges::bitget::Bitget;
147    ///
148    /// let bitget = Bitget::builder()
149    ///     .api_key("your-api-key")
150    ///     .secret("your-secret")
151    ///     .passphrase("your-passphrase")
152    ///     .sandbox(true)
153    ///     .build()
154    ///     .unwrap();
155    /// ```
156    pub fn builder() -> BitgetBuilder {
157        BitgetBuilder::new()
158    }
159
160    /// Creates a new Bitget instance.
161    ///
162    /// # Arguments
163    ///
164    /// * `config` - Exchange configuration.
165    pub fn new(config: ExchangeConfig) -> Result<Self> {
166        let base = BaseExchange::new(config)?;
167        let options = BitgetOptions::default();
168
169        Ok(Self { base, options })
170    }
171
172    /// Creates a new Bitget instance with custom options.
173    ///
174    /// This is used internally by the builder pattern.
175    ///
176    /// # Arguments
177    ///
178    /// * `config` - Exchange configuration.
179    /// * `options` - Bitget-specific options.
180    pub fn new_with_options(config: ExchangeConfig, options: BitgetOptions) -> Result<Self> {
181        let base = BaseExchange::new(config)?;
182        Ok(Self { base, options })
183    }
184
185    /// Returns a reference to the base exchange.
186    pub fn base(&self) -> &BaseExchange {
187        &self.base
188    }
189
190    /// Returns a mutable reference to the base exchange.
191    pub fn base_mut(&mut self) -> &mut BaseExchange {
192        &mut self.base
193    }
194
195    /// Returns the Bitget options.
196    pub fn options(&self) -> &BitgetOptions {
197        &self.options
198    }
199
200    /// Sets the Bitget options.
201    pub fn set_options(&mut self, options: BitgetOptions) {
202        self.options = options;
203    }
204
205    /// Returns the exchange ID.
206    pub fn id(&self) -> &'static str {
207        "bitget"
208    }
209
210    /// Returns the exchange name.
211    pub fn name(&self) -> &'static str {
212        "Bitget"
213    }
214
215    /// Returns the API version.
216    pub fn version(&self) -> &'static str {
217        "v2"
218    }
219
220    /// Returns `true` if the exchange is CCXT-certified.
221    pub fn certified(&self) -> bool {
222        false
223    }
224
225    /// Returns `true` if Pro version (WebSocket) is supported.
226    pub fn pro(&self) -> bool {
227        true
228    }
229
230    /// Returns the rate limit in requests per second.
231    pub fn rate_limit(&self) -> u32 {
232        20
233    }
234
235    /// Returns `true` if sandbox/testnet mode is enabled.
236    ///
237    /// Sandbox mode is enabled when either:
238    /// - `config.sandbox` is set to `true`
239    /// - `options.testnet` is set to `true`
240    ///
241    /// # Returns
242    ///
243    /// `true` if sandbox mode is enabled, `false` otherwise.
244    ///
245    /// # Example
246    ///
247    /// ```no_run
248    /// use ccxt_exchanges::bitget::Bitget;
249    /// use ccxt_core::ExchangeConfig;
250    ///
251    /// let config = ExchangeConfig {
252    ///     sandbox: true,
253    ///     ..Default::default()
254    /// };
255    /// let bitget = Bitget::new(config).unwrap();
256    /// assert!(bitget.is_sandbox());
257    /// ```
258    pub fn is_sandbox(&self) -> bool {
259        self.base().config.sandbox || self.options.testnet
260    }
261
262    /// Returns the supported timeframes.
263    pub fn timeframes(&self) -> HashMap<String, String> {
264        let mut timeframes = HashMap::new();
265        timeframes.insert("1m".to_string(), "1m".to_string());
266        timeframes.insert("5m".to_string(), "5m".to_string());
267        timeframes.insert("15m".to_string(), "15m".to_string());
268        timeframes.insert("30m".to_string(), "30m".to_string());
269        timeframes.insert("1h".to_string(), "1H".to_string());
270        timeframes.insert("4h".to_string(), "4H".to_string());
271        timeframes.insert("6h".to_string(), "6H".to_string());
272        timeframes.insert("12h".to_string(), "12H".to_string());
273        timeframes.insert("1d".to_string(), "1D".to_string());
274        timeframes.insert("3d".to_string(), "3D".to_string());
275        timeframes.insert("1w".to_string(), "1W".to_string());
276        timeframes.insert("1M".to_string(), "1M".to_string());
277        timeframes
278    }
279
280    /// Returns the API URLs.
281    ///
282    /// Returns testnet URLs when sandbox mode is enabled (via `config.sandbox` or `options.testnet`),
283    /// otherwise returns production URLs.
284    ///
285    /// # Returns
286    ///
287    /// - `BitgetUrls::testnet()` when sandbox mode is enabled
288    /// - `BitgetUrls::production()` when sandbox mode is disabled
289    pub fn urls(&self) -> BitgetUrls {
290        if self.base().config.sandbox || self.options.testnet {
291            BitgetUrls::testnet()
292        } else {
293            BitgetUrls::production()
294        }
295    }
296
297    /// Creates a public WebSocket client.
298    ///
299    /// # Returns
300    ///
301    /// Returns a `BitgetWs` instance for public data streams.
302    ///
303    /// # Example
304    /// ```no_run
305    /// use ccxt_exchanges::bitget::Bitget;
306    /// use ccxt_core::ExchangeConfig;
307    ///
308    /// # async fn example() -> ccxt_core::error::Result<()> {
309    /// let bitget = Bitget::new(ExchangeConfig::default())?;
310    /// let ws = bitget.create_ws();
311    /// ws.connect().await?;
312    /// # Ok(())
313    /// # }
314    /// ```
315    pub fn create_ws(&self) -> ws::BitgetWs {
316        let urls = self.urls();
317        ws::BitgetWs::new(urls.ws_public)
318    }
319
320    /// Creates a signed request builder for authenticated API requests.
321    ///
322    /// # Arguments
323    ///
324    /// * `endpoint` - API endpoint path (e.g., "/api/v2/spot/account/assets")
325    ///
326    /// # Returns
327    ///
328    /// Returns a `BitgetSignedRequestBuilder` for fluent API construction.
329    ///
330    /// # Example
331    ///
332    /// ```no_run
333    /// use ccxt_exchanges::bitget::Bitget;
334    /// use ccxt_exchanges::bitget::signed_request::HttpMethod;
335    /// use ccxt_core::ExchangeConfig;
336    ///
337    /// # async fn example() -> ccxt_core::Result<()> {
338    /// let bitget = Bitget::new(ExchangeConfig::default())?;
339    ///
340    /// let data = bitget.signed_request("/api/v2/spot/account/assets")
341    ///     .execute()
342    ///     .await?;
343    /// # Ok(())
344    /// # }
345    /// ```
346    pub fn signed_request(
347        &self,
348        endpoint: impl Into<String>,
349    ) -> signed_request::BitgetSignedRequestBuilder<'_> {
350        signed_request::BitgetSignedRequestBuilder::new(self, endpoint)
351    }
352}
353
354/// Bitget API URLs.
355#[derive(Debug, Clone)]
356pub struct BitgetUrls {
357    /// REST API base URL.
358    pub rest: String,
359    /// Public WebSocket URL.
360    pub ws_public: String,
361    /// Private WebSocket URL.
362    pub ws_private: String,
363}
364
365impl BitgetUrls {
366    /// Returns production environment URLs.
367    pub fn production() -> Self {
368        Self {
369            rest: "https://api.bitget.com".to_string(),
370            ws_public: "wss://ws.bitget.com/v2/ws/public".to_string(),
371            ws_private: "wss://ws.bitget.com/v2/ws/private".to_string(),
372        }
373    }
374
375    /// Returns testnet environment URLs.
376    ///
377    /// Testnet uses completely isolated domains for both REST and WebSocket APIs.
378    /// This is the recommended environment for testing without risking real funds.
379    ///
380    /// # URLs
381    ///
382    /// - REST: `https://api-testnet.bitget.com`
383    /// - WS Public: `wss://ws-testnet.bitget.com/v2/ws/public`
384    /// - WS Private: `wss://ws-testnet.bitget.com/v2/ws/private`
385    pub fn testnet() -> Self {
386        Self {
387            rest: "https://api-testnet.bitget.com".to_string(),
388            ws_public: "wss://ws-testnet.bitget.com/v2/ws/public".to_string(),
389            ws_private: "wss://ws-testnet.bitget.com/v2/ws/private".to_string(),
390        }
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn test_bitget_creation() {
400        let config = ExchangeConfig {
401            id: "bitget".to_string(),
402            name: "Bitget".to_string(),
403            ..Default::default()
404        };
405
406        let bitget = Bitget::new(config);
407        assert!(bitget.is_ok());
408
409        let bitget = bitget.unwrap();
410        assert_eq!(bitget.id(), "bitget");
411        assert_eq!(bitget.name(), "Bitget");
412        assert_eq!(bitget.version(), "v2");
413        assert!(!bitget.certified());
414        assert!(bitget.pro());
415    }
416
417    #[test]
418    fn test_timeframes() {
419        let config = ExchangeConfig::default();
420        let bitget = Bitget::new(config).unwrap();
421        let timeframes = bitget.timeframes();
422
423        assert!(timeframes.contains_key("1m"));
424        assert!(timeframes.contains_key("1h"));
425        assert!(timeframes.contains_key("1d"));
426        assert_eq!(timeframes.len(), 12);
427    }
428
429    #[test]
430    fn test_urls() {
431        let config = ExchangeConfig::default();
432        let bitget = Bitget::new(config).unwrap();
433        let urls = bitget.urls();
434
435        assert!(urls.rest.contains("api.bitget.com"));
436        assert!(urls.ws_public.contains("ws.bitget.com"));
437    }
438
439    #[test]
440    fn test_sandbox_urls() {
441        let config = ExchangeConfig {
442            sandbox: true,
443            ..Default::default()
444        };
445        let bitget = Bitget::new(config).unwrap();
446        let urls = bitget.urls();
447
448        // Sandbox mode should use testnet URLs
449        assert_eq!(urls.rest, "https://api-testnet.bitget.com");
450        assert_eq!(urls.ws_public, "wss://ws-testnet.bitget.com/v2/ws/public");
451        assert_eq!(urls.ws_private, "wss://ws-testnet.bitget.com/v2/ws/private");
452    }
453
454    #[test]
455    fn test_bitget_urls_testnet() {
456        let urls = BitgetUrls::testnet();
457        assert_eq!(urls.rest, "https://api-testnet.bitget.com");
458        assert_eq!(urls.ws_public, "wss://ws-testnet.bitget.com/v2/ws/public");
459        assert_eq!(urls.ws_private, "wss://ws-testnet.bitget.com/v2/ws/private");
460    }
461
462    #[test]
463    fn test_sandbox_urls_with_testnet_option() {
464        let config = ExchangeConfig::default();
465        let options = BitgetOptions {
466            testnet: true,
467            ..Default::default()
468        };
469        let bitget = Bitget::new_with_options(config, options).unwrap();
470        let urls = bitget.urls();
471
472        // Testnet option should also use testnet URLs
473        assert_eq!(urls.rest, "https://api-testnet.bitget.com");
474        assert_eq!(urls.ws_public, "wss://ws-testnet.bitget.com/v2/ws/public");
475        assert_eq!(urls.ws_private, "wss://ws-testnet.bitget.com/v2/ws/private");
476    }
477
478    #[test]
479    fn test_is_sandbox_with_config_sandbox() {
480        let config = ExchangeConfig {
481            sandbox: true,
482            ..Default::default()
483        };
484        let bitget = Bitget::new(config).unwrap();
485        assert!(bitget.is_sandbox());
486    }
487
488    #[test]
489    fn test_is_sandbox_with_options_testnet() {
490        let config = ExchangeConfig::default();
491        let options = BitgetOptions {
492            testnet: true,
493            ..Default::default()
494        };
495        let bitget = Bitget::new_with_options(config, options).unwrap();
496        assert!(bitget.is_sandbox());
497    }
498
499    #[test]
500    fn test_is_sandbox_false_by_default() {
501        let config = ExchangeConfig::default();
502        let bitget = Bitget::new(config).unwrap();
503        assert!(!bitget.is_sandbox());
504    }
505
506    #[test]
507    fn test_default_options() {
508        let options = BitgetOptions::default();
509        assert_eq!(options.product_type, "spot");
510        assert_eq!(options.default_type, DefaultType::Spot);
511        assert_eq!(options.default_sub_type, None);
512        assert_eq!(options.recv_window, 5000);
513        assert!(!options.testnet);
514    }
515
516    #[test]
517    fn test_effective_product_type_spot() {
518        let mut options = BitgetOptions::default();
519        options.default_type = DefaultType::Spot;
520        assert_eq!(options.effective_product_type(), "spot");
521    }
522
523    #[test]
524    fn test_effective_product_type_swap_linear() {
525        let mut options = BitgetOptions::default();
526        options.default_type = DefaultType::Swap;
527        options.default_sub_type = Some(DefaultSubType::Linear);
528        assert_eq!(options.effective_product_type(), "umcbl");
529    }
530
531    #[test]
532    fn test_effective_product_type_swap_inverse() {
533        let mut options = BitgetOptions::default();
534        options.default_type = DefaultType::Swap;
535        options.default_sub_type = Some(DefaultSubType::Inverse);
536        assert_eq!(options.effective_product_type(), "dmcbl");
537    }
538
539    #[test]
540    fn test_effective_product_type_swap_default_sub_type() {
541        let mut options = BitgetOptions::default();
542        options.default_type = DefaultType::Swap;
543        // No sub_type specified, should default to Linear (umcbl)
544        assert_eq!(options.effective_product_type(), "umcbl");
545    }
546
547    #[test]
548    fn test_effective_product_type_futures_linear() {
549        let mut options = BitgetOptions::default();
550        options.default_type = DefaultType::Futures;
551        options.default_sub_type = Some(DefaultSubType::Linear);
552        assert_eq!(options.effective_product_type(), "umcbl");
553    }
554
555    #[test]
556    fn test_effective_product_type_futures_inverse() {
557        let mut options = BitgetOptions::default();
558        options.default_type = DefaultType::Futures;
559        options.default_sub_type = Some(DefaultSubType::Inverse);
560        assert_eq!(options.effective_product_type(), "dmcbl");
561    }
562
563    #[test]
564    fn test_effective_product_type_margin() {
565        let mut options = BitgetOptions::default();
566        options.default_type = DefaultType::Margin;
567        assert_eq!(options.effective_product_type(), "spot");
568    }
569
570    #[test]
571    fn test_effective_product_type_option() {
572        let mut options = BitgetOptions::default();
573        options.default_type = DefaultType::Option;
574        assert_eq!(options.effective_product_type(), "spot");
575    }
576}