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 rust_decimal::prelude::ToPrimitive;
16use std::collections::HashMap;
17
18use super::Bitget;
19
20#[async_trait]
21impl Exchange for Bitget {
22    // ==================== Metadata ====================
23
24    fn id(&self) -> &str {
25        "bitget"
26    }
27
28    fn name(&self) -> &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.into_values().map(|v| (*v).clone()).collect())
102    }
103
104    async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
105        let arc_markets = Bitget::load_markets(self, reload).await?;
106        Ok(arc_markets
107            .into_iter()
108            .map(|(k, v)| (k, (*v).clone()))
109            .collect())
110    }
111
112    async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
113        Bitget::fetch_ticker(self, symbol).await
114    }
115
116    async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
117        let symbols_vec = symbols.map(|s| s.to_vec());
118        Bitget::fetch_tickers(self, symbols_vec).await
119    }
120
121    async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
122        Bitget::fetch_order_book(self, symbol, limit).await
123    }
124
125    async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
126        Bitget::fetch_trades(self, symbol, limit).await
127    }
128
129    async fn fetch_ohlcv(
130        &self,
131        symbol: &str,
132        timeframe: Timeframe,
133        since: Option<i64>,
134        limit: Option<u32>,
135    ) -> Result<Vec<Ohlcv>> {
136        let timeframe_str = timeframe.to_string();
137        let ohlcv_data = Bitget::fetch_ohlcv(self, symbol, &timeframe_str, since, limit).await?;
138
139        // Convert OHLCV to Ohlcv with proper type conversions
140        Ok(ohlcv_data
141            .into_iter()
142            .map(|o| Ohlcv {
143                timestamp: o.timestamp,
144                open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
145                high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
146                low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
147                close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
148                volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
149            })
150            .collect())
151    }
152
153    // ==================== Trading (Private API) ====================
154
155    async fn create_order(
156        &self,
157        symbol: &str,
158        order_type: OrderType,
159        side: OrderSide,
160        amount: Decimal,
161        price: Option<Decimal>,
162    ) -> Result<Order> {
163        let amount_f64 = amount
164            .to_f64()
165            .ok_or_else(|| ccxt_core::Error::invalid_request("Failed to convert amount to f64"))?;
166        let price_f64 = match price {
167            Some(p) => Some(p.to_f64().ok_or_else(|| {
168                ccxt_core::Error::invalid_request("Failed to convert price to f64")
169            })?),
170            None => None,
171        };
172
173        Bitget::create_order(self, symbol, order_type, side, amount_f64, price_f64).await
174    }
175
176    async fn cancel_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 cancel_order on Bitget")
179        })?;
180        Bitget::cancel_order(self, id, symbol_str).await
181    }
182
183    async fn cancel_all_orders(&self, _symbol: Option<&str>) -> Result<Vec<Order>> {
184        Err(ccxt_core::Error::not_implemented("cancel_all_orders"))
185    }
186
187    async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
188        let symbol_str = symbol.ok_or_else(|| {
189            ccxt_core::Error::invalid_request("Symbol is required for fetch_order on Bitget")
190        })?;
191        Bitget::fetch_order(self, id, symbol_str).await
192    }
193
194    async fn fetch_open_orders(
195        &self,
196        symbol: Option<&str>,
197        since: Option<i64>,
198        limit: Option<u32>,
199    ) -> Result<Vec<Order>> {
200        Bitget::fetch_open_orders(self, symbol, since, limit).await
201    }
202
203    async fn fetch_closed_orders(
204        &self,
205        symbol: Option<&str>,
206        since: Option<i64>,
207        limit: Option<u32>,
208    ) -> Result<Vec<Order>> {
209        Bitget::fetch_closed_orders(self, symbol, since, limit).await
210    }
211
212    // ==================== Account (Private API) ====================
213
214    async fn fetch_balance(&self) -> Result<Balance> {
215        Bitget::fetch_balance(self).await
216    }
217
218    async fn fetch_my_trades(
219        &self,
220        symbol: Option<&str>,
221        since: Option<i64>,
222        limit: Option<u32>,
223    ) -> Result<Vec<Trade>> {
224        let symbol_str = symbol.ok_or_else(|| {
225            ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on Bitget")
226        })?;
227        Bitget::fetch_my_trades(self, symbol_str, since, limit).await
228    }
229
230    // ==================== Helper Methods ====================
231
232    async fn market(&self, symbol: &str) -> Result<Market> {
233        let cache = self.base().market_cache.read().await;
234
235        if !cache.loaded {
236            return Err(ccxt_core::Error::exchange(
237                "-1",
238                "Markets not loaded. Call load_markets() first.",
239            ));
240        }
241
242        cache
243            .markets
244            .get(symbol)
245            .map(|v| (**v).clone())
246            .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
247    }
248
249    async fn markets(&self) -> HashMap<String, Market> {
250        let cache = self.base().market_cache.read().await;
251        cache
252            .markets
253            .iter()
254            .map(|(k, v)| (k.clone(), (**v).clone()))
255            .collect()
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use ccxt_core::ExchangeConfig;
263
264    #[test]
265    fn test_bitget_exchange_trait_metadata() {
266        let config = ExchangeConfig::default();
267        let bitget = Bitget::new(config).unwrap();
268
269        // Test metadata methods via Exchange trait
270        let exchange: &dyn Exchange = &bitget;
271
272        assert_eq!(exchange.id(), "bitget");
273        assert_eq!(exchange.name(), "Bitget");
274        assert_eq!(exchange.version(), "v2");
275        assert!(!exchange.certified());
276        assert!(exchange.has_websocket());
277    }
278
279    #[test]
280    fn test_bitget_exchange_trait_capabilities() {
281        let config = ExchangeConfig::default();
282        let bitget = Bitget::new(config).unwrap();
283
284        let exchange: &dyn Exchange = &bitget;
285        let caps = exchange.capabilities();
286
287        // Public API capabilities
288        assert!(caps.fetch_markets());
289        assert!(caps.fetch_ticker());
290        assert!(caps.fetch_tickers());
291        assert!(caps.fetch_order_book());
292        assert!(caps.fetch_trades());
293        assert!(caps.fetch_ohlcv());
294
295        // Private API capabilities
296        assert!(caps.create_order());
297        assert!(caps.cancel_order());
298        assert!(caps.fetch_order());
299        assert!(caps.fetch_open_orders());
300        assert!(caps.fetch_closed_orders());
301        assert!(caps.fetch_balance());
302        assert!(caps.fetch_my_trades());
303
304        // WebSocket capabilities
305        assert!(caps.websocket());
306        assert!(caps.watch_ticker());
307        assert!(caps.watch_order_book());
308        assert!(caps.watch_trades());
309
310        // Not implemented capabilities
311        assert!(!caps.edit_order());
312        assert!(!caps.cancel_all_orders());
313        assert!(!caps.fetch_currencies());
314    }
315
316    #[test]
317    fn test_bitget_exchange_trait_timeframes() {
318        let config = ExchangeConfig::default();
319        let bitget = Bitget::new(config).unwrap();
320
321        let exchange: &dyn Exchange = &bitget;
322        let timeframes = exchange.timeframes();
323
324        assert!(!timeframes.is_empty());
325        assert!(timeframes.contains(&Timeframe::M1));
326        assert!(timeframes.contains(&Timeframe::M5));
327        assert!(timeframes.contains(&Timeframe::M15));
328        assert!(timeframes.contains(&Timeframe::H1));
329        assert!(timeframes.contains(&Timeframe::H4));
330        assert!(timeframes.contains(&Timeframe::D1));
331        assert!(timeframes.contains(&Timeframe::W1));
332        assert!(timeframes.contains(&Timeframe::Mon1));
333    }
334
335    #[test]
336    fn test_bitget_exchange_trait_rate_limit() {
337        let config = ExchangeConfig::default();
338        let bitget = Bitget::new(config).unwrap();
339
340        let exchange: &dyn Exchange = &bitget;
341        assert_eq!(exchange.rate_limit(), 20);
342    }
343
344    #[test]
345    fn test_bitget_exchange_trait_object_safety() {
346        let config = ExchangeConfig::default();
347        let bitget = Bitget::new(config).unwrap();
348
349        // Test that we can create a trait object (Box<dyn Exchange>)
350        let exchange: Box<dyn Exchange> = Box::new(bitget);
351
352        assert_eq!(exchange.id(), "bitget");
353        assert_eq!(exchange.name(), "Bitget");
354        assert_eq!(exchange.rate_limit(), 20);
355    }
356
357    #[test]
358    fn test_bitget_exchange_trait_polymorphic_usage() {
359        let config = ExchangeConfig::default();
360        let bitget = Bitget::new(config).unwrap();
361
362        // Test polymorphic usage with &dyn Exchange
363        fn check_exchange_metadata(exchange: &dyn Exchange) -> (&str, &str, bool) {
364            (exchange.id(), exchange.name(), exchange.has_websocket())
365        }
366
367        let (id, name, has_ws) = check_exchange_metadata(&bitget);
368        assert_eq!(id, "bitget");
369        assert_eq!(name, "Bitget");
370        assert!(has_ws);
371    }
372
373    #[test]
374    fn test_bitget_capabilities_has_method() {
375        let config = ExchangeConfig::default();
376        let bitget = Bitget::new(config).unwrap();
377
378        let exchange: &dyn Exchange = &bitget;
379        let caps = exchange.capabilities();
380
381        // Test the has() method with CCXT-style camelCase names
382        assert!(caps.has("fetchMarkets"));
383        assert!(caps.has("fetchTicker"));
384        assert!(caps.has("fetchTickers"));
385        assert!(caps.has("fetchOrderBook"));
386        assert!(caps.has("fetchTrades"));
387        assert!(caps.has("fetchOHLCV"));
388        assert!(caps.has("createOrder"));
389        assert!(caps.has("cancelOrder"));
390        assert!(caps.has("fetchOrder"));
391        assert!(caps.has("fetchOpenOrders"));
392        assert!(caps.has("fetchClosedOrders"));
393        assert!(caps.has("fetchBalance"));
394        assert!(caps.has("fetchMyTrades"));
395        assert!(caps.has("websocket"));
396
397        // Not implemented
398        assert!(!caps.has("editOrder"));
399        assert!(!caps.has("cancelAllOrders"));
400        assert!(!caps.has("fetchCurrencies"));
401        assert!(!caps.has("unknownCapability"));
402    }
403}