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 "e_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}