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