ccxt_exchanges/binance/
exchange_impl.rs

1//! Exchange trait implementation for Binance
2//!
3//! This module implements the unified `Exchange` trait from `ccxt-core` for Binance.
4
5use async_trait::async_trait;
6use ccxt_core::{
7    Result,
8    exchange::{Exchange, ExchangeCapabilities},
9    types::{
10        Balance, Market, Ohlcv, Order, OrderBook, OrderSide, OrderType, Ticker, Timeframe, Trade,
11    },
12};
13use rust_decimal::Decimal;
14use std::collections::HashMap;
15
16use super::Binance;
17
18#[async_trait]
19impl Exchange for Binance {
20    // ==================== Metadata ====================
21
22    fn id(&self) -> &str {
23        "binance"
24    }
25
26    fn name(&self) -> &str {
27        "Binance"
28    }
29
30    fn version(&self) -> &'static str {
31        "v3"
32    }
33
34    fn certified(&self) -> bool {
35        true
36    }
37
38    fn has_websocket(&self) -> bool {
39        true
40    }
41
42    fn capabilities(&self) -> ExchangeCapabilities {
43        ExchangeCapabilities {
44            // Market Data (Public API)
45            fetch_markets: true,
46            fetch_currencies: true,
47            fetch_ticker: true,
48            fetch_tickers: true,
49            fetch_order_book: true,
50            fetch_trades: true,
51            fetch_ohlcv: true,
52            fetch_status: true,
53            fetch_time: true,
54
55            // Trading (Private API)
56            create_order: true,
57            create_market_order: true,
58            create_limit_order: true,
59            cancel_order: true,
60            cancel_all_orders: true,
61            edit_order: false, // Binance doesn't support order editing
62            fetch_order: true,
63            fetch_orders: true,
64            fetch_open_orders: true,
65            fetch_closed_orders: true,
66            fetch_canceled_orders: false,
67
68            // Account (Private API)
69            fetch_balance: true,
70            fetch_my_trades: true,
71            fetch_deposits: true,
72            fetch_withdrawals: true,
73            fetch_transactions: true,
74            fetch_ledger: true,
75
76            // Funding
77            fetch_deposit_address: true,
78            create_deposit_address: true,
79            withdraw: true,
80            transfer: true,
81
82            // Margin Trading
83            fetch_borrow_rate: true,
84            fetch_borrow_rates: true,
85            fetch_funding_rate: true,
86            fetch_funding_rates: true,
87            fetch_positions: true,
88            set_leverage: true,
89            set_margin_mode: true,
90
91            // WebSocket
92            websocket: true,
93            watch_ticker: true,
94            watch_tickers: true,
95            watch_order_book: true,
96            watch_trades: true,
97            watch_ohlcv: true,
98            watch_balance: true,
99            watch_orders: true,
100            watch_my_trades: true,
101        }
102    }
103
104    fn timeframes(&self) -> Vec<Timeframe> {
105        vec![
106            Timeframe::M1,
107            Timeframe::M3,
108            Timeframe::M5,
109            Timeframe::M15,
110            Timeframe::M30,
111            Timeframe::H1,
112            Timeframe::H2,
113            Timeframe::H4,
114            Timeframe::H6,
115            Timeframe::H8,
116            Timeframe::H12,
117            Timeframe::D1,
118            Timeframe::D3,
119            Timeframe::W1,
120            Timeframe::Mon1,
121        ]
122    }
123
124    fn rate_limit(&self) -> f64 {
125        50.0
126    }
127
128    // ==================== Market Data (Public API) ====================
129
130    async fn fetch_markets(&self) -> Result<Vec<Market>> {
131        // Delegate to existing implementation
132        Binance::fetch_markets(self).await
133    }
134
135    async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
136        // Delegate to existing implementation
137        Binance::load_markets(self, reload).await
138    }
139
140    async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
141        // Delegate to existing implementation using default parameters
142        Binance::fetch_ticker(self, symbol, ()).await
143    }
144
145    async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
146        // Convert slice to Vec for existing implementation
147        let symbols_vec = symbols.map(|s| s.to_vec());
148        Binance::fetch_tickers(self, symbols_vec).await
149    }
150
151    async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
152        // Delegate to existing implementation
153        Binance::fetch_order_book(self, symbol, limit).await
154    }
155
156    async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
157        // Delegate to existing implementation
158        Binance::fetch_trades(self, symbol, limit).await
159    }
160
161    async fn fetch_ohlcv(
162        &self,
163        symbol: &str,
164        timeframe: Timeframe,
165        since: Option<i64>,
166        limit: Option<u32>,
167    ) -> Result<Vec<Ohlcv>> {
168        use ccxt_core::types::{Amount, Price};
169
170        // Convert Timeframe enum to string for existing implementation
171        let timeframe_str = timeframe.to_string();
172        let ohlcv_data =
173            Binance::fetch_ohlcv(self, symbol, &timeframe_str, since, limit, None).await?;
174
175        // Convert OHLCV to Ohlcv with proper type conversions
176        Ok(ohlcv_data
177            .into_iter()
178            .map(|o| Ohlcv {
179                timestamp: o.timestamp,
180                open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
181                high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
182                low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
183                close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
184                volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
185            })
186            .collect())
187    }
188
189    // ==================== Trading (Private API) ====================
190
191    async fn create_order(
192        &self,
193        symbol: &str,
194        order_type: OrderType,
195        side: OrderSide,
196        amount: Decimal,
197        price: Option<Decimal>,
198    ) -> Result<Order> {
199        // Convert Decimal to f64 for existing implementation
200        let amount_f64 = amount.to_string().parse::<f64>().unwrap_or(0.0);
201        let price_f64 = price.map(|p| p.to_string().parse::<f64>().unwrap_or(0.0));
202
203        Binance::create_order(self, symbol, order_type, side, amount_f64, price_f64, None).await
204    }
205
206    async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
207        // Delegate to existing implementation
208        // Note: Binance requires symbol for cancel_order
209        let symbol_str = symbol.ok_or_else(|| {
210            ccxt_core::Error::invalid_request("Symbol is required for cancel_order on Binance")
211        })?;
212        Binance::cancel_order(self, id, symbol_str).await
213    }
214
215    async fn cancel_all_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
216        // Delegate to existing implementation
217        // Note: Binance requires symbol for cancel_all_orders
218        let symbol_str = symbol.ok_or_else(|| {
219            ccxt_core::Error::invalid_request("Symbol is required for cancel_all_orders on Binance")
220        })?;
221        Binance::cancel_all_orders(self, symbol_str).await
222    }
223
224    async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
225        // Delegate to existing implementation
226        // Note: Binance requires symbol for fetch_order
227        let symbol_str = symbol.ok_or_else(|| {
228            ccxt_core::Error::invalid_request("Symbol is required for fetch_order on Binance")
229        })?;
230        Binance::fetch_order(self, id, symbol_str).await
231    }
232
233    async fn fetch_open_orders(
234        &self,
235        symbol: Option<&str>,
236        _since: Option<i64>,
237        _limit: Option<u32>,
238    ) -> Result<Vec<Order>> {
239        // Delegate to existing implementation
240        // Note: Binance's fetch_open_orders doesn't support since/limit parameters
241        Binance::fetch_open_orders(self, symbol).await
242    }
243
244    async fn fetch_closed_orders(
245        &self,
246        symbol: Option<&str>,
247        since: Option<i64>,
248        limit: Option<u32>,
249    ) -> Result<Vec<Order>> {
250        // Delegate to existing implementation
251        // Convert i64 to u64 for since parameter
252        let since_u64 = since.map(|s| s as u64);
253        Binance::fetch_closed_orders(self, symbol, since_u64, limit).await
254    }
255
256    // ==================== Account (Private API) ====================
257
258    async fn fetch_balance(&self) -> Result<Balance> {
259        // Delegate to existing implementation
260        Binance::fetch_balance(self, None).await
261    }
262
263    async fn fetch_my_trades(
264        &self,
265        symbol: Option<&str>,
266        since: Option<i64>,
267        limit: Option<u32>,
268    ) -> Result<Vec<Trade>> {
269        // Delegate to existing implementation
270        // Note: Binance's fetch_my_trades requires a symbol
271        let symbol_str = symbol.ok_or_else(|| {
272            ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on Binance")
273        })?;
274        let since_u64 = since.map(|s| s as u64);
275        Binance::fetch_my_trades(self, symbol_str, since_u64, limit).await
276    }
277
278    // ==================== Helper Methods ====================
279
280    async fn market(&self, symbol: &str) -> Result<Market> {
281        // Use async read for async method
282        let cache = self.base().market_cache.read().await;
283
284        if !cache.loaded {
285            return Err(ccxt_core::Error::exchange(
286                "-1",
287                "Markets not loaded. Call load_markets() first.",
288            ));
289        }
290
291        cache
292            .markets
293            .get(symbol)
294            .cloned()
295            .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
296    }
297
298    async fn markets(&self) -> HashMap<String, Market> {
299        // Use async read for async method
300        let cache = self.base().market_cache.read().await;
301        cache.markets.clone()
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use ccxt_core::ExchangeConfig;
309
310    #[test]
311    fn test_binance_exchange_trait_metadata() {
312        let config = ExchangeConfig::default();
313        let binance = Binance::new(config).unwrap();
314
315        // Test metadata methods via Exchange trait
316        let exchange: &dyn Exchange = &binance;
317
318        assert_eq!(exchange.id(), "binance");
319        assert_eq!(exchange.name(), "Binance");
320        assert_eq!(exchange.version(), "v3");
321        assert!(exchange.certified());
322        assert!(exchange.has_websocket());
323    }
324
325    #[test]
326    fn test_binance_exchange_trait_capabilities() {
327        let config = ExchangeConfig::default();
328        let binance = Binance::new(config).unwrap();
329
330        let exchange: &dyn Exchange = &binance;
331        let caps = exchange.capabilities();
332
333        assert!(caps.fetch_markets);
334        assert!(caps.fetch_ticker);
335        assert!(caps.create_order);
336        assert!(caps.websocket);
337        assert!(!caps.edit_order); // Binance doesn't support order editing
338    }
339
340    #[test]
341    fn test_binance_exchange_trait_timeframes() {
342        let config = ExchangeConfig::default();
343        let binance = Binance::new(config).unwrap();
344
345        let exchange: &dyn Exchange = &binance;
346        let timeframes = exchange.timeframes();
347
348        assert!(!timeframes.is_empty());
349        assert!(timeframes.contains(&Timeframe::M1));
350        assert!(timeframes.contains(&Timeframe::H1));
351        assert!(timeframes.contains(&Timeframe::D1));
352    }
353
354    #[test]
355    fn test_binance_exchange_trait_object_safety() {
356        let config = ExchangeConfig::default();
357        let binance = Binance::new(config).unwrap();
358
359        // Test that we can create a trait object
360        let exchange: Box<dyn Exchange> = Box::new(binance);
361
362        assert_eq!(exchange.id(), "binance");
363        assert_eq!(exchange.rate_limit(), 50.0);
364    }
365
366    // ==================== Property-Based Tests ====================
367
368    mod property_tests {
369        use super::*;
370        use proptest::prelude::*;
371
372        // Strategy to generate various ExchangeConfig configurations
373        fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
374            (
375                prop::bool::ANY,                                                      // sandbox
376                prop::option::of(any::<u64>().prop_map(|n| format!("key_{}", n))),    // api_key
377                prop::option::of(any::<u64>().prop_map(|n| format!("secret_{}", n))), // secret
378            )
379                .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
380                    sandbox,
381                    api_key,
382                    secret,
383                    ..Default::default()
384                })
385        }
386
387        proptest! {
388            #![proptest_config(ProptestConfig::with_cases(100))]
389
390            /// **Feature: unified-exchange-trait, Property 8: Timeframes Non-Empty**
391            ///
392            /// *For any* exchange configuration, calling `timeframes()` should return
393            /// a non-empty vector of valid `Timeframe` values.
394            ///
395            /// **Validates: Requirements 8.4**
396            #[test]
397            fn prop_timeframes_non_empty(config in arb_exchange_config()) {
398                let binance = Binance::new(config).expect("Should create Binance instance");
399                let exchange: &dyn Exchange = &binance;
400
401                let timeframes = exchange.timeframes();
402
403                // Property: timeframes should never be empty
404                prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
405
406                // Property: all timeframes should be valid (no duplicates)
407                let mut seen = std::collections::HashSet::new();
408                for tf in &timeframes {
409                    prop_assert!(
410                        seen.insert(tf.clone()),
411                        "Timeframes should not contain duplicates: {:?}",
412                        tf
413                    );
414                }
415
416                // Property: should contain common timeframes
417                prop_assert!(
418                    timeframes.contains(&Timeframe::M1),
419                    "Should contain 1-minute timeframe"
420                );
421                prop_assert!(
422                    timeframes.contains(&Timeframe::H1),
423                    "Should contain 1-hour timeframe"
424                );
425                prop_assert!(
426                    timeframes.contains(&Timeframe::D1),
427                    "Should contain 1-day timeframe"
428                );
429            }
430
431            /// **Feature: unified-exchange-trait, Property 7: Backward Compatibility**
432            ///
433            /// *For any* exchange configuration, metadata methods called through the Exchange trait
434            /// should return the same values as calling them directly on Binance.
435            ///
436            /// **Validates: Requirements 3.2, 3.4**
437            #[test]
438            fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
439                let binance = Binance::new(config).expect("Should create Binance instance");
440
441                // Get trait object reference
442                let exchange: &dyn Exchange = &binance;
443
444                // Property: id() should be consistent
445                prop_assert_eq!(
446                    exchange.id(),
447                    Binance::id(&binance),
448                    "id() should be consistent between trait and direct call"
449                );
450
451                // Property: name() should be consistent
452                prop_assert_eq!(
453                    exchange.name(),
454                    Binance::name(&binance),
455                    "name() should be consistent between trait and direct call"
456                );
457
458                // Property: version() should be consistent
459                prop_assert_eq!(
460                    exchange.version(),
461                    Binance::version(&binance),
462                    "version() should be consistent between trait and direct call"
463                );
464
465                // Property: certified() should be consistent
466                prop_assert_eq!(
467                    exchange.certified(),
468                    Binance::certified(&binance),
469                    "certified() should be consistent between trait and direct call"
470                );
471
472                // Property: rate_limit() should be consistent
473                prop_assert!(
474                    (exchange.rate_limit() - Binance::rate_limit(&binance)).abs() < f64::EPSILON,
475                    "rate_limit() should be consistent between trait and direct call"
476                );
477
478                // Property: capabilities should be consistent
479                let trait_caps = exchange.capabilities();
480                prop_assert!(trait_caps.fetch_markets, "Should support fetch_markets");
481                prop_assert!(trait_caps.fetch_ticker, "Should support fetch_ticker");
482                prop_assert!(trait_caps.websocket, "Should support websocket");
483            }
484        }
485    }
486}