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        ohlcv_data
138            .into_iter()
139            .map(|o| -> ccxt_core::Result<Ohlcv> {
140                Ok(Ohlcv {
141                    timestamp: o.timestamp,
142                    open: Price(Decimal::try_from(o.open).map_err(|e| {
143                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
144                            "OHLCV open",
145                            format!("{e}"),
146                        ))
147                    })?),
148                    high: Price(Decimal::try_from(o.high).map_err(|e| {
149                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
150                            "OHLCV high",
151                            format!("{e}"),
152                        ))
153                    })?),
154                    low: Price(Decimal::try_from(o.low).map_err(|e| {
155                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
156                            "OHLCV low",
157                            format!("{e}"),
158                        ))
159                    })?),
160                    close: Price(Decimal::try_from(o.close).map_err(|e| {
161                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
162                            "OHLCV close",
163                            format!("{e}"),
164                        ))
165                    })?),
166                    volume: Amount(Decimal::try_from(o.volume).map_err(|e| {
167                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
168                            "OHLCV volume",
169                            format!("{e}"),
170                        ))
171                    })?),
172                })
173            })
174            .collect::<ccxt_core::Result<Vec<Ohlcv>>>()
175            .map_err(|e| e.context("Failed to convert Bitget OHLCV data"))
176    }
177
178    // ==================== Trading (Private API) ====================
179
180    async fn create_order(
181        &self,
182        symbol: &str,
183        order_type: OrderType,
184        side: OrderSide,
185        amount: Amount,
186        price: Option<Price>,
187    ) -> Result<Order> {
188        // Direct delegation - no type conversion needed
189        #[allow(deprecated)]
190        Bitget::create_order(self, symbol, order_type, side, amount, price).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<Arc<Market>> {
250        let cache = self.base().market_cache.read().await;
251
252        if !cache.is_loaded() {
253            return Err(ccxt_core::Error::exchange(
254                "-1",
255                "Markets not loaded. Call load_markets() first.",
256            ));
257        }
258
259        cache
260            .get_market(symbol)
261            .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
262    }
263
264    async fn markets(&self) -> Arc<HashMap<String, Arc<Market>>> {
265        let cache = self.base().market_cache.read().await;
266        cache.markets()
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use ccxt_core::ExchangeConfig;
274
275    #[test]
276    fn test_bitget_exchange_trait_metadata() {
277        let config = ExchangeConfig::default();
278        let bitget = Bitget::new(config).unwrap();
279
280        // Test metadata methods via Exchange trait
281        let exchange: &dyn Exchange = &bitget;
282
283        assert_eq!(exchange.id(), "bitget");
284        assert_eq!(exchange.name(), "Bitget");
285        assert_eq!(exchange.version(), "v2");
286        assert!(!exchange.certified());
287        assert!(exchange.has_websocket());
288    }
289
290    #[test]
291    fn test_bitget_exchange_trait_capabilities() {
292        let config = ExchangeConfig::default();
293        let bitget = Bitget::new(config).unwrap();
294
295        let exchange: &dyn Exchange = &bitget;
296        let caps = exchange.capabilities();
297
298        // Public API capabilities
299        assert!(caps.fetch_markets());
300        assert!(caps.fetch_ticker());
301        assert!(caps.fetch_tickers());
302        assert!(caps.fetch_order_book());
303        assert!(caps.fetch_trades());
304        assert!(caps.fetch_ohlcv());
305
306        // Private API capabilities
307        assert!(caps.create_order());
308        assert!(caps.cancel_order());
309        assert!(caps.fetch_order());
310        assert!(caps.fetch_open_orders());
311        assert!(caps.fetch_closed_orders());
312        assert!(caps.fetch_balance());
313        assert!(caps.fetch_my_trades());
314
315        // WebSocket capabilities
316        assert!(caps.websocket());
317        assert!(caps.watch_ticker());
318        assert!(caps.watch_order_book());
319        assert!(caps.watch_trades());
320
321        // Not implemented capabilities
322        assert!(!caps.edit_order());
323        assert!(!caps.cancel_all_orders());
324        assert!(!caps.fetch_currencies());
325    }
326
327    #[test]
328    fn test_bitget_exchange_trait_timeframes() {
329        let config = ExchangeConfig::default();
330        let bitget = Bitget::new(config).unwrap();
331
332        let exchange: &dyn Exchange = &bitget;
333        let timeframes = exchange.timeframes();
334
335        assert!(!timeframes.is_empty());
336        assert!(timeframes.contains(&Timeframe::M1));
337        assert!(timeframes.contains(&Timeframe::M5));
338        assert!(timeframes.contains(&Timeframe::M15));
339        assert!(timeframes.contains(&Timeframe::H1));
340        assert!(timeframes.contains(&Timeframe::H4));
341        assert!(timeframes.contains(&Timeframe::D1));
342        assert!(timeframes.contains(&Timeframe::W1));
343        assert!(timeframes.contains(&Timeframe::Mon1));
344    }
345
346    #[test]
347    fn test_bitget_exchange_trait_rate_limit() {
348        let config = ExchangeConfig::default();
349        let bitget = Bitget::new(config).unwrap();
350
351        let exchange: &dyn Exchange = &bitget;
352        assert_eq!(exchange.rate_limit(), 20);
353    }
354
355    #[test]
356    fn test_bitget_exchange_trait_object_safety() {
357        let config = ExchangeConfig::default();
358        let bitget = Bitget::new(config).unwrap();
359
360        // Test that we can create a trait object (Box<dyn Exchange>)
361        let exchange: Box<dyn Exchange> = Box::new(bitget);
362
363        assert_eq!(exchange.id(), "bitget");
364        assert_eq!(exchange.name(), "Bitget");
365        assert_eq!(exchange.rate_limit(), 20);
366    }
367
368    #[test]
369    fn test_bitget_exchange_trait_polymorphic_usage() {
370        let config = ExchangeConfig::default();
371        let bitget = Bitget::new(config).unwrap();
372
373        // Test polymorphic usage with &dyn Exchange
374        fn check_exchange_metadata(exchange: &dyn Exchange) -> (&str, &str, bool) {
375            (exchange.id(), exchange.name(), exchange.has_websocket())
376        }
377
378        let (id, name, has_ws) = check_exchange_metadata(&bitget);
379        assert_eq!(id, "bitget");
380        assert_eq!(name, "Bitget");
381        assert!(has_ws);
382    }
383
384    #[test]
385    fn test_bitget_capabilities_has_method() {
386        let config = ExchangeConfig::default();
387        let bitget = Bitget::new(config).unwrap();
388
389        let exchange: &dyn Exchange = &bitget;
390        let caps = exchange.capabilities();
391
392        // Test the has() method with CCXT-style camelCase names
393        assert!(caps.has("fetchMarkets"));
394        assert!(caps.has("fetchTicker"));
395        assert!(caps.has("fetchTickers"));
396        assert!(caps.has("fetchOrderBook"));
397        assert!(caps.has("fetchTrades"));
398        assert!(caps.has("fetchOHLCV"));
399        assert!(caps.has("createOrder"));
400        assert!(caps.has("cancelOrder"));
401        assert!(caps.has("fetchOrder"));
402        assert!(caps.has("fetchOpenOrders"));
403        assert!(caps.has("fetchClosedOrders"));
404        assert!(caps.has("fetchBalance"));
405        assert!(caps.has("fetchMyTrades"));
406        assert!(caps.has("websocket"));
407
408        // Not implemented
409        assert!(!caps.has("editOrder"));
410        assert!(!caps.has("cancelAllOrders"));
411        assert!(!caps.has("fetchCurrencies"));
412        assert!(!caps.has("unknownCapability"));
413    }
414}