Skip to main content

ccxt_exchanges/bitget/
symbol.rs

1//! Bitget symbol converter implementation.
2//!
3//! This module provides conversion between unified CCXT symbols and Bitget-specific
4//! exchange IDs for spot, swap (perpetual), and futures markets.
5//!
6//! # Bitget Symbol Formats
7//!
8//! | Market Type | Unified Format | Bitget Format | Product Type |
9//! |-------------|----------------|---------------|--------------|
10//! | Spot | BTC/USDT | BTCUSDT | spot |
11//! | Linear Swap | BTC/USDT:USDT | BTCUSDT | USDT-FUTURES |
12//! | Inverse Swap | BTC/USD:BTC | BTCUSD | COIN-FUTURES |
13//!
14//! # Example
15//!
16//! ```rust
17//! use ccxt_exchanges::bitget::symbol::BitgetSymbolConverter;
18//!
19//! assert_eq!(BitgetSymbolConverter::unified_to_exchange("BTC/USDT"), "BTCUSDT");
20//! assert_eq!(BitgetSymbolConverter::unified_to_exchange("BTC/USDT:USDT"), "BTCUSDT");
21//! assert_eq!(BitgetSymbolConverter::product_type_from_symbol("BTC/USDT:USDT"), "USDT-FUTURES");
22//! assert_eq!(BitgetSymbolConverter::product_type_from_symbol("BTC/USDT"), "spot");
23//! ```
24
25/// Bitget symbol converter.
26///
27/// Provides methods to convert between unified CCXT symbols and Bitget-specific
28/// exchange IDs, and to determine the appropriate product type.
29pub struct BitgetSymbolConverter;
30
31impl BitgetSymbolConverter {
32    /// Convert a unified CCXT symbol to Bitget exchange ID.
33    ///
34    /// Strips the settlement currency suffix and removes separators:
35    /// - "BTC/USDT" → "BTCUSDT"
36    /// - "BTC/USDT:USDT" → "BTCUSDT"
37    /// - "BTC/USD:BTC" → "BTCUSD"
38    /// - "BTC/USDT:USDT-241231" → "BTCUSDT"
39    pub fn unified_to_exchange(symbol: &str) -> String {
40        // Strip settlement part (everything after ':')
41        let base_quote = if let Some(pos) = symbol.find(':') {
42            &symbol[..pos]
43        } else {
44            symbol
45        };
46
47        // Remove the '/' separator
48        base_quote.replace('/', "")
49    }
50
51    /// Determine the Bitget product type from a unified symbol.
52    ///
53    /// Returns:
54    /// - "USDT-FUTURES" for linear perpetual/futures (e.g., "BTC/USDT:USDT")
55    /// - "COIN-FUTURES" for inverse perpetual/futures (e.g., "BTC/USD:BTC")
56    /// - "spot" for spot markets (e.g., "BTC/USDT")
57    pub fn product_type_from_symbol(symbol: &str) -> &'static str {
58        if let Some(pos) = symbol.find(':') {
59            let settle_part = &symbol[pos + 1..];
60            // Strip expiry date if present
61            let settle = if let Some(dash_pos) = settle_part.find('-') {
62                &settle_part[..dash_pos]
63            } else {
64                settle_part
65            };
66
67            // Extract quote currency
68            let base_quote = &symbol[..pos];
69            let quote = if let Some(slash_pos) = base_quote.find('/') {
70                &base_quote[slash_pos + 1..]
71            } else {
72                ""
73            };
74
75            // If settle == quote, it's linear (USDT-FUTURES)
76            // If settle != quote (settle == base), it's inverse (COIN-FUTURES)
77            if settle == quote {
78                "USDT-FUTURES"
79            } else {
80                "COIN-FUTURES"
81            }
82        } else {
83            "spot"
84        }
85    }
86
87    /// Check if a symbol is a contract (has settlement currency).
88    pub fn is_contract(symbol: &str) -> bool {
89        symbol.contains(':')
90    }
91
92    /// Check if a symbol is a spot market.
93    pub fn is_spot(symbol: &str) -> bool {
94        !symbol.contains(':')
95    }
96
97    /// Convert a Bitget exchange ID back to a rough unified symbol hint.
98    ///
99    /// This is a best-effort conversion:
100    /// - "BTCUSDT" with product_type "USDT-FUTURES" → "BTC/USDT:USDT"
101    /// - "BTCUSD" with product_type "COIN-FUTURES" → "BTC/USD:BTC"
102    /// - "BTCUSDT" with product_type "spot" → "BTC/USDT"
103    pub fn exchange_to_unified_hint(exchange_id: &str, product_type: &str) -> String {
104        // Try to split the exchange ID into base and quote
105        // Common quote currencies in order of length (longest first to avoid partial matches)
106        let quote_currencies = ["USDT", "USDC", "USD", "BTC", "ETH"];
107
108        for quote in &quote_currencies {
109            if exchange_id.ends_with(quote) && exchange_id.len() > quote.len() {
110                let base = &exchange_id[..exchange_id.len() - quote.len()];
111                return match product_type {
112                    "USDT-FUTURES" | "usdt-futures" => {
113                        format!("{}/{}:{}", base, quote, quote)
114                    }
115                    "COIN-FUTURES" | "coin-futures" => {
116                        format!("{}/{}:{}", base, quote, base)
117                    }
118                    _ => {
119                        format!("{}/{}", base, quote)
120                    }
121                };
122            }
123        }
124
125        // Fallback: return as-is
126        exchange_id.to_string()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    // ========================================================================
135    // unified_to_exchange tests
136    // ========================================================================
137
138    #[test]
139    fn test_spot_to_exchange() {
140        assert_eq!(
141            BitgetSymbolConverter::unified_to_exchange("BTC/USDT"),
142            "BTCUSDT"
143        );
144        assert_eq!(
145            BitgetSymbolConverter::unified_to_exchange("ETH/USDT"),
146            "ETHUSDT"
147        );
148        assert_eq!(
149            BitgetSymbolConverter::unified_to_exchange("SOL/BTC"),
150            "SOLBTC"
151        );
152    }
153
154    #[test]
155    fn test_linear_swap_to_exchange() {
156        assert_eq!(
157            BitgetSymbolConverter::unified_to_exchange("BTC/USDT:USDT"),
158            "BTCUSDT"
159        );
160        assert_eq!(
161            BitgetSymbolConverter::unified_to_exchange("ETH/USDT:USDT"),
162            "ETHUSDT"
163        );
164    }
165
166    #[test]
167    fn test_inverse_swap_to_exchange() {
168        assert_eq!(
169            BitgetSymbolConverter::unified_to_exchange("BTC/USD:BTC"),
170            "BTCUSD"
171        );
172    }
173
174    #[test]
175    fn test_futures_with_expiry_to_exchange() {
176        assert_eq!(
177            BitgetSymbolConverter::unified_to_exchange("BTC/USDT:USDT-241231"),
178            "BTCUSDT"
179        );
180    }
181
182    // ========================================================================
183    // product_type_from_symbol tests
184    // ========================================================================
185
186    #[test]
187    fn test_product_type_spot() {
188        assert_eq!(
189            BitgetSymbolConverter::product_type_from_symbol("BTC/USDT"),
190            "spot"
191        );
192        assert_eq!(
193            BitgetSymbolConverter::product_type_from_symbol("ETH/BTC"),
194            "spot"
195        );
196    }
197
198    #[test]
199    fn test_product_type_linear() {
200        assert_eq!(
201            BitgetSymbolConverter::product_type_from_symbol("BTC/USDT:USDT"),
202            "USDT-FUTURES"
203        );
204        assert_eq!(
205            BitgetSymbolConverter::product_type_from_symbol("ETH/USDT:USDT"),
206            "USDT-FUTURES"
207        );
208    }
209
210    #[test]
211    fn test_product_type_inverse() {
212        assert_eq!(
213            BitgetSymbolConverter::product_type_from_symbol("BTC/USD:BTC"),
214            "COIN-FUTURES"
215        );
216        assert_eq!(
217            BitgetSymbolConverter::product_type_from_symbol("ETH/USD:ETH"),
218            "COIN-FUTURES"
219        );
220    }
221
222    #[test]
223    fn test_product_type_futures_with_expiry() {
224        assert_eq!(
225            BitgetSymbolConverter::product_type_from_symbol("BTC/USDT:USDT-241231"),
226            "USDT-FUTURES"
227        );
228    }
229
230    // ========================================================================
231    // Helper function tests
232    // ========================================================================
233
234    #[test]
235    fn test_is_contract() {
236        assert!(BitgetSymbolConverter::is_contract("BTC/USDT:USDT"));
237        assert!(BitgetSymbolConverter::is_contract("BTC/USD:BTC"));
238        assert!(!BitgetSymbolConverter::is_contract("BTC/USDT"));
239    }
240
241    #[test]
242    fn test_is_spot() {
243        assert!(BitgetSymbolConverter::is_spot("BTC/USDT"));
244        assert!(!BitgetSymbolConverter::is_spot("BTC/USDT:USDT"));
245    }
246
247    // ========================================================================
248    // exchange_to_unified_hint tests
249    // ========================================================================
250
251    #[test]
252    fn test_exchange_to_unified_spot() {
253        assert_eq!(
254            BitgetSymbolConverter::exchange_to_unified_hint("BTCUSDT", "spot"),
255            "BTC/USDT"
256        );
257    }
258
259    #[test]
260    fn test_exchange_to_unified_linear() {
261        assert_eq!(
262            BitgetSymbolConverter::exchange_to_unified_hint("BTCUSDT", "USDT-FUTURES"),
263            "BTC/USDT:USDT"
264        );
265    }
266
267    #[test]
268    fn test_exchange_to_unified_inverse() {
269        assert_eq!(
270            BitgetSymbolConverter::exchange_to_unified_hint("BTCUSD", "COIN-FUTURES"),
271            "BTC/USD:BTC"
272        );
273    }
274}