ccxt_exchanges/bitget/
exchange_impl.rs

1//! Exchange trait implementation for Bitget
2//!
3//! This module implements the unified `Exchange` trait from `ccxt-core` for Bitget.
4
5use async_trait::async_trait;
6use ccxt_core::{
7    Result,
8    exchange::{Exchange, ExchangeCapabilities},
9    types::{
10        Amount, Balance, Market, Ohlcv, Order, OrderBook, OrderSide, OrderType, Price, Ticker,
11        Timeframe, Trade,
12    },
13};
14use rust_decimal::Decimal;
15use std::collections::HashMap;
16
17use super::Bitget;
18
19#[async_trait]
20impl Exchange for Bitget {
21    // ==================== Metadata ====================
22
23    fn id(&self) -> &str {
24        "bitget"
25    }
26
27    fn name(&self) -> &str {
28        "Bitget"
29    }
30
31    fn version(&self) -> &'static str {
32        "v2"
33    }
34
35    fn certified(&self) -> bool {
36        false
37    }
38
39    fn has_websocket(&self) -> bool {
40        true
41    }
42
43    fn capabilities(&self) -> ExchangeCapabilities {
44        ExchangeCapabilities {
45            // Market Data (Public API)
46            fetch_markets: true,
47            fetch_currencies: false,
48            fetch_ticker: true,
49            fetch_tickers: true,
50            fetch_order_book: true,
51            fetch_trades: true,
52            fetch_ohlcv: true,
53            fetch_status: false,
54            fetch_time: false,
55
56            // Trading (Private API)
57            create_order: true,
58            create_market_order: true,
59            create_limit_order: true,
60            cancel_order: true,
61            cancel_all_orders: false, // Not implemented yet
62            edit_order: false,
63            fetch_order: true,
64            fetch_orders: false,
65            fetch_open_orders: true,
66            fetch_closed_orders: true,
67            fetch_canceled_orders: false,
68
69            // Account (Private API)
70            fetch_balance: true,
71            fetch_my_trades: true,
72            fetch_deposits: false,
73            fetch_withdrawals: false,
74            fetch_transactions: false,
75            fetch_ledger: false,
76
77            // Funding
78            fetch_deposit_address: false,
79            create_deposit_address: false,
80            withdraw: false,
81            transfer: false,
82
83            // Margin Trading
84            fetch_borrow_rate: false,
85            fetch_borrow_rates: false,
86            fetch_funding_rate: false,
87            fetch_funding_rates: false,
88            fetch_positions: false,
89            set_leverage: false,
90            set_margin_mode: false,
91
92            // WebSocket
93            websocket: true,
94            watch_ticker: true,
95            watch_tickers: false,
96            watch_order_book: true,
97            watch_trades: true,
98            watch_ohlcv: false,
99            watch_balance: false,
100            watch_orders: false,
101            watch_my_trades: false,
102        }
103    }
104
105    fn timeframes(&self) -> Vec<Timeframe> {
106        vec![
107            Timeframe::M1,
108            Timeframe::M5,
109            Timeframe::M15,
110            Timeframe::M30,
111            Timeframe::H1,
112            Timeframe::H4,
113            Timeframe::H6,
114            Timeframe::H12,
115            Timeframe::D1,
116            Timeframe::D3,
117            Timeframe::W1,
118            Timeframe::Mon1,
119        ]
120    }
121
122    fn rate_limit(&self) -> f64 {
123        20.0
124    }
125
126    // ==================== Market Data (Public API) ====================
127
128    async fn fetch_markets(&self) -> Result<Vec<Market>> {
129        Bitget::fetch_markets(self).await
130    }
131
132    async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
133        Bitget::load_markets(self, reload).await
134    }
135
136    async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
137        Bitget::fetch_ticker(self, symbol).await
138    }
139
140    async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
141        let symbols_vec = symbols.map(|s| s.to_vec());
142        Bitget::fetch_tickers(self, symbols_vec).await
143    }
144
145    async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
146        Bitget::fetch_order_book(self, symbol, limit).await
147    }
148
149    async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
150        Bitget::fetch_trades(self, symbol, limit).await
151    }
152
153    async fn fetch_ohlcv(
154        &self,
155        symbol: &str,
156        timeframe: Timeframe,
157        since: Option<i64>,
158        limit: Option<u32>,
159    ) -> Result<Vec<Ohlcv>> {
160        let timeframe_str = timeframe.to_string();
161        let ohlcv_data = Bitget::fetch_ohlcv(self, symbol, &timeframe_str, since, limit).await?;
162
163        // Convert OHLCV to Ohlcv with proper type conversions
164        Ok(ohlcv_data
165            .into_iter()
166            .map(|o| Ohlcv {
167                timestamp: o.timestamp,
168                open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
169                high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
170                low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
171                close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
172                volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
173            })
174            .collect())
175    }
176
177    // ==================== Trading (Private API) ====================
178
179    async fn create_order(
180        &self,
181        symbol: &str,
182        order_type: OrderType,
183        side: OrderSide,
184        amount: Decimal,
185        price: Option<Decimal>,
186    ) -> Result<Order> {
187        let amount_f64 = amount.to_string().parse::<f64>().unwrap_or(0.0);
188        let price_f64 = price.map(|p| p.to_string().parse::<f64>().unwrap_or(0.0));
189
190        Bitget::create_order(self, symbol, order_type, side, amount_f64, price_f64).await
191    }
192
193    async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
194        let symbol_str = symbol.ok_or_else(|| {
195            ccxt_core::Error::invalid_request("Symbol is required for cancel_order on Bitget")
196        })?;
197        Bitget::cancel_order(self, id, symbol_str).await
198    }
199
200    async fn cancel_all_orders(&self, _symbol: Option<&str>) -> Result<Vec<Order>> {
201        Err(ccxt_core::Error::not_implemented("cancel_all_orders"))
202    }
203
204    async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
205        let symbol_str = symbol.ok_or_else(|| {
206            ccxt_core::Error::invalid_request("Symbol is required for fetch_order on Bitget")
207        })?;
208        Bitget::fetch_order(self, id, symbol_str).await
209    }
210
211    async fn fetch_open_orders(
212        &self,
213        symbol: Option<&str>,
214        since: Option<i64>,
215        limit: Option<u32>,
216    ) -> Result<Vec<Order>> {
217        Bitget::fetch_open_orders(self, symbol, since, limit).await
218    }
219
220    async fn fetch_closed_orders(
221        &self,
222        symbol: Option<&str>,
223        since: Option<i64>,
224        limit: Option<u32>,
225    ) -> Result<Vec<Order>> {
226        Bitget::fetch_closed_orders(self, symbol, since, limit).await
227    }
228
229    // ==================== Account (Private API) ====================
230
231    async fn fetch_balance(&self) -> Result<Balance> {
232        Bitget::fetch_balance(self).await
233    }
234
235    async fn fetch_my_trades(
236        &self,
237        symbol: Option<&str>,
238        since: Option<i64>,
239        limit: Option<u32>,
240    ) -> Result<Vec<Trade>> {
241        let symbol_str = symbol.ok_or_else(|| {
242            ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on Bitget")
243        })?;
244        Bitget::fetch_my_trades(self, symbol_str, since, limit).await
245    }
246
247    // ==================== Helper Methods ====================
248
249    async fn market(&self, symbol: &str) -> Result<Market> {
250        let cache = self.base().market_cache.read().await;
251
252        if !cache.loaded {
253            return Err(ccxt_core::Error::exchange(
254                "-1",
255                "Markets not loaded. Call load_markets() first.",
256            ));
257        }
258
259        cache
260            .markets
261            .get(symbol)
262            .cloned()
263            .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
264    }
265
266    async fn markets(&self) -> HashMap<String, Market> {
267        let cache = self.base().market_cache.read().await;
268        cache.markets.clone()
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use ccxt_core::ExchangeConfig;
276
277    #[test]
278    fn test_bitget_exchange_trait_metadata() {
279        let config = ExchangeConfig::default();
280        let bitget = Bitget::new(config).unwrap();
281
282        // Test metadata methods via Exchange trait
283        let exchange: &dyn Exchange = &bitget;
284
285        assert_eq!(exchange.id(), "bitget");
286        assert_eq!(exchange.name(), "Bitget");
287        assert_eq!(exchange.version(), "v2");
288        assert!(!exchange.certified());
289        assert!(exchange.has_websocket());
290    }
291
292    #[test]
293    fn test_bitget_exchange_trait_capabilities() {
294        let config = ExchangeConfig::default();
295        let bitget = Bitget::new(config).unwrap();
296
297        let exchange: &dyn Exchange = &bitget;
298        let caps = exchange.capabilities();
299
300        // Public API capabilities
301        assert!(caps.fetch_markets);
302        assert!(caps.fetch_ticker);
303        assert!(caps.fetch_tickers);
304        assert!(caps.fetch_order_book);
305        assert!(caps.fetch_trades);
306        assert!(caps.fetch_ohlcv);
307
308        // Private API capabilities
309        assert!(caps.create_order);
310        assert!(caps.cancel_order);
311        assert!(caps.fetch_order);
312        assert!(caps.fetch_open_orders);
313        assert!(caps.fetch_closed_orders);
314        assert!(caps.fetch_balance);
315        assert!(caps.fetch_my_trades);
316
317        // WebSocket capabilities
318        assert!(caps.websocket);
319        assert!(caps.watch_ticker);
320        assert!(caps.watch_order_book);
321        assert!(caps.watch_trades);
322
323        // Not implemented capabilities
324        assert!(!caps.edit_order);
325        assert!(!caps.cancel_all_orders);
326        assert!(!caps.fetch_currencies);
327    }
328
329    #[test]
330    fn test_bitget_exchange_trait_timeframes() {
331        let config = ExchangeConfig::default();
332        let bitget = Bitget::new(config).unwrap();
333
334        let exchange: &dyn Exchange = &bitget;
335        let timeframes = exchange.timeframes();
336
337        assert!(!timeframes.is_empty());
338        assert!(timeframes.contains(&Timeframe::M1));
339        assert!(timeframes.contains(&Timeframe::M5));
340        assert!(timeframes.contains(&Timeframe::M15));
341        assert!(timeframes.contains(&Timeframe::H1));
342        assert!(timeframes.contains(&Timeframe::H4));
343        assert!(timeframes.contains(&Timeframe::D1));
344        assert!(timeframes.contains(&Timeframe::W1));
345        assert!(timeframes.contains(&Timeframe::Mon1));
346    }
347
348    #[test]
349    fn test_bitget_exchange_trait_rate_limit() {
350        let config = ExchangeConfig::default();
351        let bitget = Bitget::new(config).unwrap();
352
353        let exchange: &dyn Exchange = &bitget;
354        assert!((exchange.rate_limit() - 20.0).abs() < f64::EPSILON);
355    }
356
357    #[test]
358    fn test_bitget_exchange_trait_object_safety() {
359        let config = ExchangeConfig::default();
360        let bitget = Bitget::new(config).unwrap();
361
362        // Test that we can create a trait object (Box<dyn Exchange>)
363        let exchange: Box<dyn Exchange> = Box::new(bitget);
364
365        assert_eq!(exchange.id(), "bitget");
366        assert_eq!(exchange.name(), "Bitget");
367        assert!((exchange.rate_limit() - 20.0).abs() < f64::EPSILON);
368    }
369
370    #[test]
371    fn test_bitget_exchange_trait_polymorphic_usage() {
372        let config = ExchangeConfig::default();
373        let bitget = Bitget::new(config).unwrap();
374
375        // Test polymorphic usage with &dyn Exchange
376        fn check_exchange_metadata(exchange: &dyn Exchange) -> (&str, &str, bool) {
377            (exchange.id(), exchange.name(), exchange.has_websocket())
378        }
379
380        let (id, name, has_ws) = check_exchange_metadata(&bitget);
381        assert_eq!(id, "bitget");
382        assert_eq!(name, "Bitget");
383        assert!(has_ws);
384    }
385
386    #[test]
387    fn test_bitget_capabilities_has_method() {
388        let config = ExchangeConfig::default();
389        let bitget = Bitget::new(config).unwrap();
390
391        let exchange: &dyn Exchange = &bitget;
392        let caps = exchange.capabilities();
393
394        // Test the has() method with CCXT-style camelCase names
395        assert!(caps.has("fetchMarkets"));
396        assert!(caps.has("fetchTicker"));
397        assert!(caps.has("fetchTickers"));
398        assert!(caps.has("fetchOrderBook"));
399        assert!(caps.has("fetchTrades"));
400        assert!(caps.has("fetchOHLCV"));
401        assert!(caps.has("createOrder"));
402        assert!(caps.has("cancelOrder"));
403        assert!(caps.has("fetchOrder"));
404        assert!(caps.has("fetchOpenOrders"));
405        assert!(caps.has("fetchClosedOrders"));
406        assert!(caps.has("fetchBalance"));
407        assert!(caps.has("fetchMyTrades"));
408        assert!(caps.has("websocket"));
409
410        // Not implemented
411        assert!(!caps.has("editOrder"));
412        assert!(!caps.has("cancelAllOrders"));
413        assert!(!caps.has("fetchCurrencies"));
414        assert!(!caps.has("unknownCapability"));
415    }
416}