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