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//! as well as the new decomposed traits (PublicExchange, MarketData, Trading, Account, Margin, Funding).
5//!
6//! # Backward Compatibility
7//!
8//! Binance implements both the legacy `Exchange` trait and the new modular traits.
9//! This ensures that existing code using `dyn Exchange` continues to work, while
10//! new code can use the more granular trait interfaces.
11//!
12//! # Trait Implementations
13//!
14//! - `Exchange`: Unified interface (backward compatible)
15//! - `PublicExchange`: Metadata and capabilities
16//! - `MarketData`: Public market data (via traits module)
17//! - `Trading`: Order management (via traits module)
18//! - `Account`: Balance and trade history (via traits module)
19//! - `Margin`: Positions, leverage, funding (via traits module)
20//! - `Funding`: Deposits, withdrawals, transfers (via traits module)
21
22use async_trait::async_trait;
23use ccxt_core::{
24    Result,
25    exchange::{Capability, Exchange, ExchangeCapabilities},
26    traits::PublicExchange,
27    types::{
28        Amount, Balance, Market, Ohlcv, Order, OrderBook, OrderSide, OrderType, Price, Ticker,
29        Timeframe, Trade,
30    },
31};
32use rust_decimal::Decimal;
33use std::collections::HashMap;
34use std::sync::Arc;
35
36use super::Binance;
37
38// Re-export ExchangeExt for use in tests
39#[cfg(test)]
40use ccxt_core::exchange::ExchangeExt;
41
42#[async_trait]
43impl Exchange for Binance {
44    // ==================== Metadata ====================
45
46    fn id(&self) -> &'static str {
47        "binance"
48    }
49
50    fn name(&self) -> &'static str {
51        "Binance"
52    }
53
54    fn version(&self) -> &'static str {
55        "v3"
56    }
57
58    fn certified(&self) -> bool {
59        true
60    }
61
62    fn has_websocket(&self) -> bool {
63        true
64    }
65
66    fn capabilities(&self) -> ExchangeCapabilities {
67        // Binance supports almost all capabilities except:
68        // - EditOrder: Binance doesn't support order editing
69        // - FetchCanceledOrders: Binance doesn't have a separate API for canceled orders
70        ExchangeCapabilities::builder()
71            .all()
72            .without_capability(Capability::EditOrder)
73            .without_capability(Capability::FetchCanceledOrders)
74            .build()
75    }
76
77    fn timeframes(&self) -> Vec<Timeframe> {
78        vec![
79            Timeframe::M1,
80            Timeframe::M3,
81            Timeframe::M5,
82            Timeframe::M15,
83            Timeframe::M30,
84            Timeframe::H1,
85            Timeframe::H2,
86            Timeframe::H4,
87            Timeframe::H6,
88            Timeframe::H8,
89            Timeframe::H12,
90            Timeframe::D1,
91            Timeframe::D3,
92            Timeframe::W1,
93            Timeframe::Mon1,
94        ]
95    }
96
97    fn rate_limit(&self) -> u32 {
98        self.options.rate_limit
99    }
100
101    // ==================== Market Data (Public API) ====================
102
103    async fn fetch_markets(&self) -> Result<Vec<Market>> {
104        let arc_markets = Binance::fetch_markets(self).await?;
105        Ok(arc_markets.values().map(|v| (**v).clone()).collect())
106    }
107
108    async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
109        Binance::load_markets(self, reload).await
110    }
111
112    async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
113        // Delegate to existing implementation using default parameters
114        Binance::fetch_ticker(self, symbol, ()).await
115    }
116
117    async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
118        // Convert slice to Vec for existing implementation
119        let symbols_vec = symbols.map(<[String]>::to_vec);
120        Binance::fetch_tickers(self, symbols_vec).await
121    }
122
123    async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
124        // Delegate to existing implementation
125        Binance::fetch_order_book(self, symbol, limit).await
126    }
127
128    async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
129        // Delegate to existing implementation
130        Binance::fetch_trades(self, symbol, limit).await
131    }
132
133    async fn fetch_ohlcv(
134        &self,
135        symbol: &str,
136        timeframe: Timeframe,
137        since: Option<i64>,
138        limit: Option<u32>,
139    ) -> Result<Vec<Ohlcv>> {
140        use ccxt_core::types::{Amount, Price};
141
142        // Convert Timeframe enum to string for existing implementation
143        let timeframe_str = timeframe.to_string();
144        // Use i64 directly for the updated method signature
145        #[allow(deprecated)]
146        let ohlcv_data =
147            Binance::fetch_ohlcv(self, symbol, &timeframe_str, since, limit, None).await?;
148
149        // Convert OHLCV to Ohlcv with proper type conversions
150        Ok(ohlcv_data
151            .into_iter()
152            .map(|o| Ohlcv {
153                timestamp: o.timestamp,
154                open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
155                high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
156                low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
157                close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
158                volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
159            })
160            .collect())
161    }
162
163    // ==================== Trading (Private API) ====================
164
165    async fn create_order(
166        &self,
167        symbol: &str,
168        order_type: OrderType,
169        side: OrderSide,
170        amount: Amount,
171        price: Option<Price>,
172    ) -> Result<Order> {
173        // Direct delegation - no type conversion needed
174        #[allow(deprecated)]
175        Binance::create_order(self, symbol, order_type, side, amount, price, None).await
176    }
177
178    async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
179        // Delegate to existing implementation
180        // Note: Binance requires symbol for cancel_order
181        let symbol_str = symbol.ok_or_else(|| {
182            ccxt_core::Error::invalid_request("Symbol is required for cancel_order on Binance")
183        })?;
184        Binance::cancel_order(self, id, symbol_str).await
185    }
186
187    async fn cancel_all_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
188        // Delegate to existing implementation
189        // Note: Binance requires symbol for cancel_all_orders
190        let symbol_str = symbol.ok_or_else(|| {
191            ccxt_core::Error::invalid_request("Symbol is required for cancel_all_orders on Binance")
192        })?;
193        Binance::cancel_all_orders(self, symbol_str).await
194    }
195
196    async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
197        // Delegate to existing implementation
198        // Note: Binance requires symbol for fetch_order
199        let symbol_str = symbol.ok_or_else(|| {
200            ccxt_core::Error::invalid_request("Symbol is required for fetch_order on Binance")
201        })?;
202        Binance::fetch_order(self, id, symbol_str).await
203    }
204
205    async fn fetch_open_orders(
206        &self,
207        symbol: Option<&str>,
208        _since: Option<i64>,
209        _limit: Option<u32>,
210    ) -> Result<Vec<Order>> {
211        // Delegate to existing implementation
212        // Note: Binance's fetch_open_orders doesn't support since/limit parameters
213        Binance::fetch_open_orders(self, symbol).await
214    }
215
216    async fn fetch_closed_orders(
217        &self,
218        symbol: Option<&str>,
219        since: Option<i64>,
220        limit: Option<u32>,
221    ) -> Result<Vec<Order>> {
222        // Delegate to existing implementation
223        // Use i64 directly for since parameter
224        Binance::fetch_closed_orders(self, symbol, since, limit).await
225    }
226
227    // ==================== Account (Private API) ====================
228
229    async fn fetch_balance(&self) -> Result<Balance> {
230        // Delegate to existing implementation
231        Binance::fetch_balance(self, None).await
232    }
233
234    async fn fetch_my_trades(
235        &self,
236        symbol: Option<&str>,
237        since: Option<i64>,
238        limit: Option<u32>,
239    ) -> Result<Vec<Trade>> {
240        // Delegate to existing implementation
241        // Note: Binance's fetch_my_trades requires a symbol
242        let symbol_str = symbol.ok_or_else(|| {
243            ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on Binance")
244        })?;
245        // Use i64 directly for the updated method signature
246        Binance::fetch_my_trades(self, symbol_str, since, limit).await
247    }
248
249    // ==================== Helper Methods ====================
250
251    async fn market(&self, symbol: &str) -> Result<Arc<Market>> {
252        // Use async read for async method
253        let cache = self.base().market_cache.read().await;
254
255        if !cache.loaded {
256            return Err(ccxt_core::Error::exchange(
257                "-1",
258                "Markets not loaded. Call load_markets() first.",
259            ));
260        }
261
262        cache
263            .markets
264            .get(symbol)
265            .cloned()
266            .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
267    }
268
269    async fn markets(&self) -> Arc<HashMap<String, Arc<Market>>> {
270        let cache = self.base().market_cache.read().await;
271        cache.markets.clone()
272    }
273}
274
275// ==================== PublicExchange Trait Implementation ====================
276
277#[async_trait]
278impl PublicExchange for Binance {
279    fn id(&self) -> &'static str {
280        "binance"
281    }
282
283    fn name(&self) -> &'static str {
284        "Binance"
285    }
286
287    fn version(&self) -> &'static str {
288        "v3"
289    }
290
291    fn certified(&self) -> bool {
292        true
293    }
294
295    fn capabilities(&self) -> ExchangeCapabilities {
296        // Binance supports almost all capabilities except:
297        // - EditOrder: Binance doesn't support order editing
298        // - FetchCanceledOrders: Binance doesn't have a separate API for canceled orders
299        ExchangeCapabilities::builder()
300            .all()
301            .without_capability(Capability::EditOrder)
302            .without_capability(Capability::FetchCanceledOrders)
303            .build()
304    }
305
306    fn timeframes(&self) -> Vec<Timeframe> {
307        vec![
308            Timeframe::M1,
309            Timeframe::M3,
310            Timeframe::M5,
311            Timeframe::M15,
312            Timeframe::M30,
313            Timeframe::H1,
314            Timeframe::H2,
315            Timeframe::H4,
316            Timeframe::H6,
317            Timeframe::H8,
318            Timeframe::H12,
319            Timeframe::D1,
320            Timeframe::D3,
321            Timeframe::W1,
322            Timeframe::Mon1,
323        ]
324    }
325
326    fn rate_limit(&self) -> u32 {
327        self.options.rate_limit
328    }
329
330    fn has_websocket(&self) -> bool {
331        true
332    }
333}
334
335// Helper methods for REST API operations
336impl Binance {
337    /// Check required authentication credentials.
338    pub(crate) fn check_required_credentials(&self) -> ccxt_core::Result<()> {
339        if self.base().config.api_key.is_none() || self.base().config.secret.is_none() {
340            return Err(ccxt_core::Error::authentication(
341                "API key and secret are required",
342            ));
343        }
344        Ok(())
345    }
346
347    /// Get authenticator instance.
348    pub(crate) fn get_auth(&self) -> ccxt_core::Result<super::auth::BinanceAuth> {
349        let api_key = self
350            .base()
351            .config
352            .api_key
353            .as_ref()
354            .ok_or_else(|| ccxt_core::Error::authentication("API key is required"))?
355            .clone();
356
357        let secret = self
358            .base()
359            .config
360            .secret
361            .as_ref()
362            .ok_or_else(|| ccxt_core::Error::authentication("Secret is required"))?
363            .clone();
364
365        Ok(super::auth::BinanceAuth::new(
366            api_key.expose_secret(),
367            secret.expose_secret(),
368        ))
369    }
370
371    // ==================== Time Sync Helper Methods ====================
372
373    /// Gets the server timestamp for signing requests.
374    ///
375    /// This method uses the cached time offset if available, otherwise fetches
376    /// the server time directly. It also triggers a background resync if needed.
377    ///
378    /// # Optimization
379    ///
380    /// By caching the time offset between local and server time, this method
381    /// reduces the number of network round-trips for signed API requests from 2 to 1.
382    /// Instead of fetching server time before every signed request, we calculate
383    /// the server timestamp locally using: `server_timestamp = local_time + cached_offset`
384    ///
385    /// # Returns
386    ///
387    /// Returns the estimated server timestamp in milliseconds.
388    ///
389    /// # Errors
390    ///
391    /// Returns an error if:
392    /// - The time sync manager is not initialized and the server time fetch fails
393    /// - Network errors occur during time synchronization
394    ///
395    /// # Example
396    ///
397    /// ```no_run
398    /// # use ccxt_exchanges::binance::Binance;
399    /// # use ccxt_core::ExchangeConfig;
400    /// # async fn example() -> ccxt_core::Result<()> {
401    /// let binance = Binance::new(ExchangeConfig::default())?;
402    ///
403    /// // Get timestamp for signing (uses cached offset if available)
404    /// let timestamp = binance.get_signing_timestamp().await?;
405    /// println!("Server timestamp: {}", timestamp);
406    /// # Ok(())
407    /// # }
408    /// ```
409    ///
410    /// _Requirements: 1.2, 2.3, 6.4_
411    pub async fn get_signing_timestamp(&self) -> ccxt_core::Result<i64> {
412        // Check if we need to sync (not initialized or sync interval elapsed)
413        if self.time_sync.needs_resync() {
414            // Attempt to sync time
415            if let Err(e) = self.sync_time().await {
416                // If sync fails and we're not initialized, we must fail
417                if !self.time_sync.is_initialized() {
418                    return Err(e);
419                }
420                // If sync fails but we have a cached offset, log and continue
421                tracing::warn!(
422                    error = %e,
423                    "Time sync failed, using cached offset"
424                );
425            }
426        }
427
428        // If still not initialized after sync attempt, fall back to direct fetch
429        if !self.time_sync.is_initialized() {
430            return self.fetch_time_raw().await;
431        }
432
433        // Return the estimated server timestamp using cached offset
434        Ok(self.time_sync.get_server_timestamp())
435    }
436
437    /// Synchronizes local time with Binance server time.
438    ///
439    /// This method fetches the current server time from Binance and updates
440    /// the cached time offset. The offset is calculated as:
441    /// `offset = server_time - local_time`
442    ///
443    /// # When to Use
444    ///
445    /// - Called automatically by `get_signing_timestamp()` when resync is needed
446    /// - Can be called manually to force a time synchronization
447    /// - Useful after receiving timestamp-related errors from the API
448    ///
449    /// # Returns
450    ///
451    /// Returns `Ok(())` on successful synchronization.
452    ///
453    /// # Errors
454    ///
455    /// Returns an error if the server time fetch fails due to network issues.
456    ///
457    /// # Example
458    ///
459    /// ```no_run
460    /// # use ccxt_exchanges::binance::Binance;
461    /// # use ccxt_core::ExchangeConfig;
462    /// # async fn example() -> ccxt_core::Result<()> {
463    /// let binance = Binance::new(ExchangeConfig::default())?;
464    ///
465    /// // Manually sync time with server
466    /// binance.sync_time().await?;
467    ///
468    /// // Check the current offset
469    /// let offset = binance.time_sync().get_offset();
470    /// println!("Time offset: {}ms", offset);
471    /// # Ok(())
472    /// # }
473    /// ```
474    ///
475    /// _Requirements: 2.1, 2.2, 6.3_
476    pub async fn sync_time(&self) -> ccxt_core::Result<()> {
477        let server_time = self.fetch_time_raw().await?;
478        self.time_sync.update_offset(server_time);
479        tracing::debug!(
480            server_time = server_time,
481            offset = self.time_sync.get_offset(),
482            "Time synchronized with Binance server"
483        );
484        Ok(())
485    }
486
487    /// Checks if an error is related to timestamp validation.
488    ///
489    /// Binance returns specific error codes and messages when the request
490    /// timestamp is outside the acceptable window (recvWindow). This method
491    /// detects such errors to enable automatic retry with a fresh timestamp.
492    ///
493    /// # Binance Timestamp Error Codes
494    ///
495    /// | Error Code | Message | Meaning |
496    /// |------------|---------|---------|
497    /// | -1021 | "Timestamp for this request is outside of the recvWindow" | Timestamp too old or too new |
498    /// | -1022 | "Signature for this request is not valid" | May be caused by wrong timestamp |
499    ///
500    /// # Arguments
501    ///
502    /// * `error` - The error to check
503    ///
504    /// # Returns
505    ///
506    /// Returns `true` if the error is related to timestamp validation, `false` otherwise.
507    ///
508    /// # Example
509    ///
510    /// ```rust
511    /// use ccxt_exchanges::binance::Binance;
512    /// use ccxt_core::{ExchangeConfig, Error};
513    ///
514    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
515    ///
516    /// // Simulate a timestamp error
517    /// let err = Error::exchange("-1021", "Timestamp for this request is outside of the recvWindow");
518    /// assert!(binance.is_timestamp_error(&err));
519    ///
520    /// // Non-timestamp error
521    /// let err = Error::exchange("-1100", "Illegal characters found in parameter");
522    /// assert!(!binance.is_timestamp_error(&err));
523    /// ```
524    ///
525    /// _Requirements: 4.1_
526    pub fn is_timestamp_error(&self, error: &ccxt_core::Error) -> bool {
527        let error_str = error.to_string().to_lowercase();
528
529        // Check for timestamp-related keywords in the error message
530        let has_timestamp_keyword = error_str.contains("timestamp");
531        let has_recv_window = error_str.contains("recvwindow");
532        let has_ahead = error_str.contains("ahead");
533        let has_behind = error_str.contains("behind");
534
535        // Timestamp error if it mentions timestamp AND one of the time-related issues
536        if has_timestamp_keyword && (has_recv_window || has_ahead || has_behind) {
537            return true;
538        }
539
540        // Also check for specific Binance error codes
541        // -1021: Timestamp for this request is outside of the recvWindow
542        // -1022: Signature for this request is not valid (may be timestamp-related)
543        if error_str.contains("-1021") {
544            return true;
545        }
546
547        // Check for error code in Exchange variant
548        if let ccxt_core::Error::Exchange(details) = error {
549            if details.code == "-1021" {
550                return true;
551            }
552        }
553
554        false
555    }
556
557    /// Executes a signed request with automatic timestamp error recovery.
558    ///
559    /// This method wraps a signed request operation and automatically handles
560    /// timestamp-related errors by resyncing time and retrying the request once.
561    ///
562    /// # Error Recovery Flow
563    ///
564    /// 1. Execute the request with the current timestamp
565    /// 2. If the request fails with a timestamp error:
566    ///    a. Resync time with the server
567    ///    b. Retry the request with a fresh timestamp
568    /// 3. If the retry also fails, return the error
569    ///
570    /// # Retry Limit
571    ///
572    /// To prevent infinite retry loops, this method limits automatic retries to
573    /// exactly 1 retry per request. If the retry also fails, the error is returned.
574    ///
575    /// # Type Parameters
576    ///
577    /// * `T` - The return type of the request
578    /// * `F` - The async function that performs the signed request
579    ///
580    /// # Arguments
581    ///
582    /// * `request_fn` - An async function that takes a timestamp (i64) and returns
583    ///   a `Result<T>`. This function should perform the actual signed API request.
584    ///
585    /// # Returns
586    ///
587    /// Returns `Ok(T)` on success, or `Err` if the request fails after retry.
588    ///
589    /// # Example
590    ///
591    /// ```no_run
592    /// # use ccxt_exchanges::binance::Binance;
593    /// # use ccxt_core::ExchangeConfig;
594    /// # async fn example() -> ccxt_core::Result<()> {
595    /// let binance = Binance::new(ExchangeConfig::default())?;
596    ///
597    /// // Execute a signed request with automatic retry on timestamp error
598    /// let result = binance.execute_signed_request_with_retry(|timestamp| {
599    ///     Box::pin(async move {
600    ///         // Perform signed request using the provided timestamp
601    ///         // ... actual request logic here ...
602    ///         Ok(())
603    ///     })
604    /// }).await?;
605    /// # Ok(())
606    /// # }
607    /// ```
608    ///
609    /// _Requirements: 4.1, 4.2, 4.3, 4.4_
610    pub async fn execute_signed_request_with_retry<T, F, Fut>(
611        &self,
612        request_fn: F,
613    ) -> ccxt_core::Result<T>
614    where
615        F: Fn(i64) -> Fut,
616        Fut: std::future::Future<Output = ccxt_core::Result<T>>,
617    {
618        // Get the initial timestamp
619        let timestamp = self.get_signing_timestamp().await?;
620
621        // Execute the request
622        match request_fn(timestamp).await {
623            Ok(result) => Ok(result),
624            Err(e) if self.is_timestamp_error(&e) => {
625                // Log the timestamp error and retry
626                tracing::warn!(
627                    error = %e,
628                    "Timestamp error detected, resyncing time and retrying request"
629                );
630
631                // Force resync time with server
632                if let Err(sync_err) = self.sync_time().await {
633                    tracing::error!(
634                        error = %sync_err,
635                        "Failed to resync time after timestamp error"
636                    );
637                    // Return the original error if sync fails
638                    return Err(e);
639                }
640
641                // Get fresh timestamp after resync
642                let new_timestamp = self.time_sync.get_server_timestamp();
643
644                tracing::debug!(
645                    old_timestamp = timestamp,
646                    new_timestamp = new_timestamp,
647                    offset = self.time_sync.get_offset(),
648                    "Retrying request with fresh timestamp"
649                );
650
651                // Retry the request once with the new timestamp
652                request_fn(new_timestamp).await
653            }
654            Err(e) => Err(e),
655        }
656    }
657
658    /// Helper method to handle timestamp errors and trigger resync.
659    ///
660    /// This method checks if an error is a timestamp error and, if so,
661    /// triggers a time resync. It's useful for manual error handling
662    /// when you want more control over the retry logic.
663    ///
664    /// # Arguments
665    ///
666    /// * `error` - The error to check
667    ///
668    /// # Returns
669    ///
670    /// Returns `true` if the error was a timestamp error and resync was triggered,
671    /// `false` otherwise.
672    ///
673    /// # Example
674    ///
675    /// ```no_run
676    /// # use ccxt_exchanges::binance::Binance;
677    /// # use ccxt_core::ExchangeConfig;
678    /// # async fn example() -> ccxt_core::Result<()> {
679    /// let binance = Binance::new(ExchangeConfig::default())?;
680    ///
681    /// // Manual error handling with resync
682    /// let result = some_signed_request().await;
683    /// if let Err(ref e) = result {
684    ///     if binance.handle_timestamp_error_and_resync(e).await {
685    ///         // Timestamp error detected and resync triggered
686    ///         // You can now retry the request
687    ///     }
688    /// }
689    /// # async fn some_signed_request() -> ccxt_core::Result<()> { Ok(()) }
690    /// # Ok(())
691    /// # }
692    /// ```
693    ///
694    /// _Requirements: 4.1, 4.2_
695    pub async fn handle_timestamp_error_and_resync(&self, error: &ccxt_core::Error) -> bool {
696        if self.is_timestamp_error(error) {
697            tracing::warn!(
698                error = %error,
699                "Timestamp error detected, triggering time resync"
700            );
701
702            if let Err(sync_err) = self.sync_time().await {
703                tracing::error!(
704                    error = %sync_err,
705                    "Failed to resync time after timestamp error"
706                );
707                return false;
708            }
709
710            tracing::debug!(
711                offset = self.time_sync.get_offset(),
712                "Time resync completed after timestamp error"
713            );
714
715            return true;
716        }
717
718        false
719    }
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725    use ccxt_core::ExchangeConfig;
726
727    #[test]
728    fn test_binance_exchange_trait_metadata() {
729        let config = ExchangeConfig::default();
730        let binance = Binance::new(config).unwrap();
731
732        // Test metadata methods via Exchange trait
733        let exchange: &dyn Exchange = &binance;
734
735        assert_eq!(exchange.id(), "binance");
736        assert_eq!(exchange.name(), "Binance");
737        assert_eq!(exchange.version(), "v3");
738        assert!(exchange.certified());
739        assert!(exchange.has_websocket());
740    }
741
742    #[test]
743    fn test_binance_exchange_trait_capabilities() {
744        let config = ExchangeConfig::default();
745        let binance = Binance::new(config).unwrap();
746
747        let exchange: &dyn Exchange = &binance;
748        let caps = exchange.capabilities();
749
750        assert!(caps.fetch_markets());
751        assert!(caps.fetch_ticker());
752        assert!(caps.create_order());
753        assert!(caps.websocket());
754        assert!(!caps.edit_order()); // Binance doesn't support order editing
755    }
756
757    #[test]
758    fn test_binance_exchange_trait_timeframes() {
759        let config = ExchangeConfig::default();
760        let binance = Binance::new(config).unwrap();
761
762        let exchange: &dyn Exchange = &binance;
763        let timeframes = exchange.timeframes();
764
765        assert!(!timeframes.is_empty());
766        assert!(timeframes.contains(&Timeframe::M1));
767        assert!(timeframes.contains(&Timeframe::H1));
768        assert!(timeframes.contains(&Timeframe::D1));
769    }
770
771    #[test]
772    fn test_binance_exchange_trait_object_safety() {
773        let config = ExchangeConfig::default();
774        let binance = Binance::new(config).unwrap();
775
776        // Test that we can create a trait object
777        let exchange: Box<dyn Exchange> = Box::new(binance);
778
779        assert_eq!(exchange.id(), "binance");
780        assert_eq!(exchange.rate_limit(), 50);
781    }
782
783    #[test]
784    fn test_binance_exchange_ext_trait() {
785        let config = ExchangeConfig::default();
786        let binance = Binance::new(config).unwrap();
787
788        // Test ExchangeExt methods
789        let exchange: &dyn Exchange = &binance;
790
791        // Binance supports all capabilities
792        assert!(
793            exchange.supports_market_data(),
794            "Binance should support market data"
795        );
796        assert!(
797            exchange.supports_trading(),
798            "Binance should support trading"
799        );
800        assert!(
801            exchange.supports_account(),
802            "Binance should support account operations"
803        );
804        assert!(
805            exchange.supports_margin(),
806            "Binance should support margin operations"
807        );
808        assert!(
809            exchange.supports_funding(),
810            "Binance should support funding operations"
811        );
812    }
813
814    #[test]
815    fn test_binance_implements_both_exchange_and_public_exchange() {
816        let config = ExchangeConfig::default();
817        let binance = Binance::new(config).unwrap();
818
819        // Test that Binance can be used as both Exchange and PublicExchange
820        let exchange: &dyn Exchange = &binance;
821        let public_exchange: &dyn PublicExchange = &binance;
822
823        // Both should return the same values
824        assert_eq!(exchange.id(), public_exchange.id());
825        assert_eq!(exchange.name(), public_exchange.name());
826        assert_eq!(exchange.version(), public_exchange.version());
827        assert_eq!(exchange.certified(), public_exchange.certified());
828        assert_eq!(exchange.rate_limit(), public_exchange.rate_limit());
829        assert_eq!(exchange.has_websocket(), public_exchange.has_websocket());
830        assert_eq!(exchange.timeframes(), public_exchange.timeframes());
831    }
832
833    // ==================== Time Sync Helper Tests ====================
834
835    #[test]
836    fn test_is_timestamp_error_with_recv_window_message() {
837        let config = ExchangeConfig::default();
838        let binance = Binance::new(config).unwrap();
839
840        // Test with recvWindow error message
841        let err = ccxt_core::Error::exchange(
842            "-1021",
843            "Timestamp for this request is outside of the recvWindow",
844        );
845        assert!(
846            binance.is_timestamp_error(&err),
847            "Should detect recvWindow timestamp error"
848        );
849    }
850
851    #[test]
852    fn test_is_timestamp_error_with_ahead_message() {
853        let config = ExchangeConfig::default();
854        let binance = Binance::new(config).unwrap();
855
856        // Test with "ahead" error message
857        let err = ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time");
858        assert!(
859            binance.is_timestamp_error(&err),
860            "Should detect 'ahead' timestamp error"
861        );
862    }
863
864    #[test]
865    fn test_is_timestamp_error_with_behind_message() {
866        let config = ExchangeConfig::default();
867        let binance = Binance::new(config).unwrap();
868
869        // Test with "behind" error message
870        let err = ccxt_core::Error::exchange("-1021", "Timestamp is behind server time");
871        assert!(
872            binance.is_timestamp_error(&err),
873            "Should detect 'behind' timestamp error"
874        );
875    }
876
877    #[test]
878    fn test_is_timestamp_error_with_error_code_only() {
879        let config = ExchangeConfig::default();
880        let binance = Binance::new(config).unwrap();
881
882        // Test with error code -1021 in message
883        let err = ccxt_core::Error::exchange("-1021", "Some error message");
884        assert!(
885            binance.is_timestamp_error(&err),
886            "Should detect error code -1021"
887        );
888    }
889
890    #[test]
891    fn test_is_timestamp_error_non_timestamp_error() {
892        let config = ExchangeConfig::default();
893        let binance = Binance::new(config).unwrap();
894
895        // Test with non-timestamp error
896        let err = ccxt_core::Error::exchange("-1100", "Illegal characters found in parameter");
897        assert!(
898            !binance.is_timestamp_error(&err),
899            "Should not detect non-timestamp error"
900        );
901
902        // Test with authentication error
903        let err = ccxt_core::Error::authentication("Invalid API key");
904        assert!(
905            !binance.is_timestamp_error(&err),
906            "Should not detect authentication error as timestamp error"
907        );
908
909        // Test with network error
910        let err = ccxt_core::Error::network("Connection refused");
911        assert!(
912            !binance.is_timestamp_error(&err),
913            "Should not detect network error as timestamp error"
914        );
915    }
916
917    #[test]
918    fn test_is_timestamp_error_case_insensitive() {
919        let config = ExchangeConfig::default();
920        let binance = Binance::new(config).unwrap();
921
922        // Test case insensitivity
923        let err = ccxt_core::Error::exchange(
924            "-1021",
925            "TIMESTAMP for this request is outside of the RECVWINDOW",
926        );
927        assert!(
928            binance.is_timestamp_error(&err),
929            "Should detect timestamp error case-insensitively"
930        );
931    }
932
933    #[test]
934    fn test_time_sync_manager_accessible() {
935        let config = ExchangeConfig::default();
936        let binance = Binance::new(config).unwrap();
937
938        // Test that time_sync() returns a reference to the manager
939        let time_sync = binance.time_sync();
940        assert!(
941            !time_sync.is_initialized(),
942            "Time sync should not be initialized initially"
943        );
944        assert!(
945            time_sync.needs_resync(),
946            "Time sync should need resync initially"
947        );
948    }
949
950    #[test]
951    fn test_time_sync_manager_update_offset() {
952        let config = ExchangeConfig::default();
953        let binance = Binance::new(config).unwrap();
954
955        // Simulate updating the offset
956        let server_time = ccxt_core::time::TimestampUtils::now_ms() + 100;
957        binance.time_sync().update_offset(server_time);
958
959        assert!(
960            binance.time_sync().is_initialized(),
961            "Time sync should be initialized after update"
962        );
963        assert!(
964            !binance.time_sync().needs_resync(),
965            "Time sync should not need resync immediately after update"
966        );
967
968        // Offset should be approximately 100ms
969        let offset = binance.time_sync().get_offset();
970        assert!(
971            offset >= 90 && offset <= 110,
972            "Offset should be approximately 100ms, got {}",
973            offset
974        );
975    }
976
977    // ==================== Error Recovery Tests ====================
978
979    #[tokio::test]
980    async fn test_execute_signed_request_with_retry_success() {
981        let config = ExchangeConfig::default();
982        let binance = Binance::new(config).unwrap();
983
984        // Initialize time sync
985        let server_time = ccxt_core::time::TimestampUtils::now_ms();
986        binance.time_sync().update_offset(server_time);
987
988        // Test successful request (no retry needed)
989        let result = binance
990            .execute_signed_request_with_retry(|timestamp| async move {
991                assert!(timestamp > 0, "Timestamp should be positive");
992                Ok::<_, ccxt_core::Error>(42)
993            })
994            .await;
995
996        assert!(result.is_ok(), "Request should succeed");
997        assert_eq!(result.unwrap(), 42);
998    }
999
1000    #[tokio::test]
1001    async fn test_execute_signed_request_with_retry_non_timestamp_error() {
1002        let config = ExchangeConfig::default();
1003        let binance = Binance::new(config).unwrap();
1004
1005        // Initialize time sync
1006        let server_time = ccxt_core::time::TimestampUtils::now_ms();
1007        binance.time_sync().update_offset(server_time);
1008
1009        // Test non-timestamp error (should not retry)
1010        let result = binance
1011            .execute_signed_request_with_retry(|_timestamp| async move {
1012                Err::<i32, _>(ccxt_core::Error::exchange("-1100", "Invalid parameter"))
1013            })
1014            .await;
1015
1016        assert!(result.is_err(), "Request should fail");
1017        let err = result.unwrap_err();
1018        assert!(
1019            err.to_string().contains("-1100"),
1020            "Error should contain original error code"
1021        );
1022    }
1023
1024    #[test]
1025    fn test_handle_timestamp_error_detection() {
1026        let config = ExchangeConfig::default();
1027        let binance = Binance::new(config).unwrap();
1028
1029        // Test various timestamp error formats
1030        let timestamp_errors = vec![
1031            ccxt_core::Error::exchange(
1032                "-1021",
1033                "Timestamp for this request is outside of the recvWindow",
1034            ),
1035            ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time"),
1036            ccxt_core::Error::exchange("-1021", "Timestamp is behind server time"),
1037            ccxt_core::Error::exchange("-1021", "Some error with timestamp and recvwindow"),
1038        ];
1039
1040        for err in timestamp_errors {
1041            assert!(
1042                binance.is_timestamp_error(&err),
1043                "Should detect timestamp error: {}",
1044                err
1045            );
1046        }
1047
1048        // Test non-timestamp errors
1049        let non_timestamp_errors = vec![
1050            ccxt_core::Error::exchange("-1100", "Invalid parameter"),
1051            ccxt_core::Error::exchange("-1000", "Unknown error"),
1052            ccxt_core::Error::authentication("Invalid API key"),
1053            ccxt_core::Error::network("Connection refused"),
1054            ccxt_core::Error::timeout("Request timed out"),
1055        ];
1056
1057        for err in non_timestamp_errors {
1058            assert!(
1059                !binance.is_timestamp_error(&err),
1060                "Should not detect as timestamp error: {}",
1061                err
1062            );
1063        }
1064    }
1065
1066    // ==================== Property-Based Tests ====================
1067
1068    mod property_tests {
1069        use super::*;
1070        use proptest::prelude::*;
1071
1072        // Strategy to generate various ExchangeConfig configurations
1073        fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
1074            (
1075                prop::bool::ANY,                                                      // sandbox
1076                prop::option::of(any::<u64>().prop_map(|n| format!("key_{}", n))),    // api_key
1077                prop::option::of(any::<u64>().prop_map(|n| format!("secret_{}", n))), // secret
1078            )
1079                .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
1080                    sandbox,
1081                    api_key: api_key.map(ccxt_core::SecretString::new),
1082                    secret: secret.map(ccxt_core::SecretString::new),
1083                    ..Default::default()
1084                })
1085        }
1086
1087        proptest! {
1088            #![proptest_config(ProptestConfig::with_cases(100))]
1089
1090            /// **Feature: unified-exchange-trait, Property 8: Timeframes Non-Empty**
1091            ///
1092            /// *For any* exchange configuration, calling `timeframes()` should return
1093            /// a non-empty vector of valid `Timeframe` values.
1094            ///
1095            /// **Validates: Requirements 8.4**
1096            #[test]
1097            fn prop_timeframes_non_empty(config in arb_exchange_config()) {
1098                let binance = Binance::new(config).expect("Should create Binance instance");
1099                let exchange: &dyn Exchange = &binance;
1100
1101                let timeframes = exchange.timeframes();
1102
1103                // Property: timeframes should never be empty
1104                prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
1105
1106                // Property: all timeframes should be valid (no duplicates)
1107                let mut seen = std::collections::HashSet::new();
1108                for tf in &timeframes {
1109                    prop_assert!(
1110                        seen.insert(tf.clone()),
1111                        "Timeframes should not contain duplicates: {:?}",
1112                        tf
1113                    );
1114                }
1115
1116                // Property: should contain common timeframes
1117                prop_assert!(
1118                    timeframes.contains(&Timeframe::M1),
1119                    "Should contain 1-minute timeframe"
1120                );
1121                prop_assert!(
1122                    timeframes.contains(&Timeframe::H1),
1123                    "Should contain 1-hour timeframe"
1124                );
1125                prop_assert!(
1126                    timeframes.contains(&Timeframe::D1),
1127                    "Should contain 1-day timeframe"
1128                );
1129            }
1130
1131            /// **Feature: unified-exchange-trait, Property 7: Backward Compatibility**
1132            ///
1133            /// *For any* exchange configuration, metadata methods called through the Exchange trait
1134            /// should return the same values as calling them directly on Binance.
1135            ///
1136            /// **Validates: Requirements 3.2, 3.4**
1137            #[test]
1138            fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
1139                let binance = Binance::new(config).expect("Should create Binance instance");
1140
1141                // Get trait object reference
1142                let exchange: &dyn Exchange = &binance;
1143
1144                // Property: id() should be consistent
1145                prop_assert_eq!(
1146                    exchange.id(),
1147                    Binance::id(&binance),
1148                    "id() should be consistent between trait and direct call"
1149                );
1150
1151                // Property: name() should be consistent
1152                prop_assert_eq!(
1153                    exchange.name(),
1154                    Binance::name(&binance),
1155                    "name() should be consistent between trait and direct call"
1156                );
1157
1158                // Property: version() should be consistent
1159                prop_assert_eq!(
1160                    exchange.version(),
1161                    Binance::version(&binance),
1162                    "version() should be consistent between trait and direct call"
1163                );
1164
1165                // Property: certified() should be consistent
1166                prop_assert_eq!(
1167                    exchange.certified(),
1168                    Binance::certified(&binance),
1169                    "certified() should be consistent between trait and direct call"
1170                );
1171
1172                // Property: rate_limit() should be consistent
1173                prop_assert_eq!(
1174                    exchange.rate_limit(),
1175                    Binance::rate_limit(&binance),
1176                    "rate_limit() should be consistent between trait and direct call"
1177                );
1178
1179                // Property: capabilities should be consistent
1180                let trait_caps = exchange.capabilities();
1181                prop_assert!(trait_caps.fetch_markets(), "Should support fetch_markets");
1182                prop_assert!(trait_caps.fetch_ticker(), "Should support fetch_ticker");
1183                prop_assert!(trait_caps.websocket(), "Should support websocket");
1184            }
1185        }
1186    }
1187}