Skip to main content

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