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