Skip to main content

ccxt_exchanges/binance/
mod.rs

1//! Binance exchange implementation.
2//!
3//! Supports spot trading, futures trading, and options trading with complete REST API and WebSocket support.
4
5use ccxt_core::types::EndpointType;
6use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
7use ccxt_core::{BaseExchange, ExchangeConfig, Result};
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::RwLock;
11
12pub mod auth;
13pub mod builder;
14pub mod constants;
15pub mod endpoint_router;
16pub mod error;
17mod exchange_impl;
18pub mod options;
19pub mod parser;
20pub mod rest;
21pub mod signed_request;
22pub mod signing_strategy;
23pub mod symbol;
24pub mod time_sync;
25pub mod urls;
26pub mod ws;
27mod ws_exchange_impl;
28
29pub use builder::BinanceBuilder;
30pub use endpoint_router::BinanceEndpointRouter;
31pub use options::BinanceOptions;
32pub use signed_request::{HttpMethod, SignedRequestBuilder};
33pub use time_sync::{TimeSyncConfig, TimeSyncManager};
34pub use urls::BinanceUrls;
35/// Binance exchange structure.
36#[derive(Debug, Clone)]
37pub struct Binance {
38    /// Base exchange instance.
39    base: BaseExchange,
40    /// Binance-specific options.
41    options: BinanceOptions,
42    /// Time synchronization manager for caching server time offset.
43    time_sync: Arc<TimeSyncManager>,
44    /// Persistent WebSocket connection reference for state tracking.
45    #[deprecated(note = "Use connection_manager instead")]
46    ws_connection: Arc<RwLock<Option<ws::BinanceWs>>>,
47    /// Centralized WebSocket connection manager
48    pub connection_manager: Arc<ws::BinanceConnectionManager>,
49}
50
51#[allow(deprecated)]
52impl Binance {
53    /// Creates a new Binance instance using the builder pattern.
54    ///
55    /// This is the recommended way to create a Binance instance.
56    ///
57    /// # Example
58    ///
59    /// ```no_run
60    /// use ccxt_exchanges::binance::Binance;
61    ///
62    /// let binance = Binance::builder()
63    ///     .api_key("your-api-key")
64    ///     .secret("your-secret")
65    ///     .sandbox(true)
66    ///     .build()
67    ///     .unwrap();
68    /// ```
69    pub fn builder() -> BinanceBuilder {
70        BinanceBuilder::new()
71    }
72
73    /// Creates a new Binance instance.
74    ///
75    /// # Arguments
76    ///
77    /// * `config` - Exchange configuration.
78    ///
79    /// # Example
80    ///
81    /// ```no_run
82    /// use ccxt_exchanges::binance::Binance;
83    /// use ccxt_core::ExchangeConfig;
84    ///
85    /// let config = ExchangeConfig {
86    ///     id: "binance".to_string(),
87    ///     name: "Binance".to_string(),
88    ///     api_key: Some("your-api-key".to_string()),
89    ///     secret: Some("your-secret".to_string()),
90    ///     ..Default::default()
91    /// };
92    ///
93    /// let binance = Binance::new(config).unwrap();
94    /// ```
95    pub fn new(config: ExchangeConfig) -> Result<Self> {
96        let base = BaseExchange::new(config)?;
97        let options = BinanceOptions::default();
98        let time_sync = Arc::new(TimeSyncManager::new());
99
100        // Determine WS URL for ConnectionManager
101        let urls = if base.config.sandbox {
102            BinanceUrls::testnet()
103        } else {
104            BinanceUrls::production()
105        };
106        // Apply overrides logic similar to self.urls()
107        let ws_url = if let Some(url) = base.config.url_overrides.get("ws") {
108            url.clone()
109        } else {
110            urls.ws.clone()
111        };
112
113        let connection_manager = Arc::new(ws::BinanceConnectionManager::new(ws_url));
114
115        Ok(Self {
116            base,
117            options,
118            time_sync,
119            ws_connection: Arc::new(RwLock::new(None)),
120            connection_manager,
121        })
122    }
123
124    /// Creates a new Binance instance with custom options.
125    ///
126    /// This is used internally by the builder pattern.
127    ///
128    /// # Arguments
129    ///
130    /// * `config` - Exchange configuration.
131    /// * `options` - Binance-specific options.
132    ///
133    /// # Example
134    ///
135    /// ```no_run
136    /// use ccxt_exchanges::binance::{Binance, BinanceOptions};
137    /// use ccxt_core::ExchangeConfig;
138    /// use ccxt_core::types::default_type::DefaultType;
139    ///
140    /// let config = ExchangeConfig::default();
141    /// let options = BinanceOptions {
142    ///     default_type: DefaultType::Swap,
143    ///     ..Default::default()
144    /// };
145    ///
146    /// let binance = Binance::new_with_options(config, options).unwrap();
147    /// ```
148    pub fn new_with_options(config: ExchangeConfig, options: BinanceOptions) -> Result<Self> {
149        let base = BaseExchange::new(config)?;
150
151        // Create TimeSyncManager with configuration from options
152        let time_sync_config = TimeSyncConfig {
153            sync_interval: Duration::from_secs(options.time_sync_interval_secs),
154            auto_sync: options.auto_time_sync,
155            max_offset_drift: options.recv_window as i64,
156        };
157        let time_sync = Arc::new(TimeSyncManager::with_config(time_sync_config));
158
159        // Determine WS URL
160        let mut urls = if base.config.sandbox {
161            BinanceUrls::testnet()
162        } else {
163            BinanceUrls::production()
164        };
165
166        // Apply overrides
167        if let Some(ws_url) = base.config.url_overrides.get("ws") {
168            urls.ws.clone_from(ws_url);
169        }
170        if let Some(ws_fapi_url) = base.config.url_overrides.get("wsFapi") {
171            urls.ws_fapi.clone_from(ws_fapi_url);
172        }
173        if let Some(ws_dapi_url) = base.config.url_overrides.get("wsDapi") {
174            urls.ws_dapi.clone_from(ws_dapi_url);
175        }
176        if let Some(ws_eapi_url) = base.config.url_overrides.get("wsEapi") {
177            urls.ws_eapi.clone_from(ws_eapi_url);
178        }
179
180        let ws_url = match options.default_type {
181            DefaultType::Swap | DefaultType::Futures => match options.default_sub_type {
182                Some(DefaultSubType::Inverse) => urls.ws_dapi.clone(),
183                _ => urls.ws_fapi.clone(),
184            },
185            DefaultType::Option => urls.ws_eapi.clone(),
186            _ => urls.ws.clone(),
187        };
188
189        let connection_manager = Arc::new(ws::BinanceConnectionManager::new(ws_url));
190
191        Ok(Self {
192            base,
193            options,
194            time_sync,
195            ws_connection: Arc::new(RwLock::new(None)),
196            connection_manager,
197        })
198    }
199
200    /// Creates a new Binance futures instance for perpetual contracts.
201    ///
202    /// # Arguments
203    ///
204    /// * `config` - Exchange configuration.
205    ///
206    /// # Example
207    ///
208    /// ```no_run
209    /// use ccxt_exchanges::binance::Binance;
210    /// use ccxt_core::ExchangeConfig;
211    ///
212    /// let config = ExchangeConfig::default();
213    /// let futures = Binance::new_swap(config).unwrap();
214    /// ```
215    pub fn new_swap(config: ExchangeConfig) -> Result<Self> {
216        let base = BaseExchange::new(config)?;
217        let options = BinanceOptions {
218            default_type: DefaultType::Swap, // Perpetual futures
219            ..Default::default()
220        };
221
222        // Create TimeSyncManager with configuration from options
223        let time_sync_config = TimeSyncConfig {
224            sync_interval: Duration::from_secs(options.time_sync_interval_secs),
225            auto_sync: options.auto_time_sync,
226            max_offset_drift: options.recv_window as i64,
227        };
228        let time_sync = Arc::new(TimeSyncManager::with_config(time_sync_config));
229
230        // Determine WS URL (Futures/Swap)
231        let mut urls = if base.config.sandbox {
232            BinanceUrls::testnet()
233        } else {
234            BinanceUrls::production()
235        };
236
237        if let Some(ws_fapi_url) = base.config.url_overrides.get("wsFapi") {
238            urls.ws_fapi.clone_from(ws_fapi_url);
239        }
240
241        // Default to FAPI/Linear for new_swap if not configured otherwise
242        let ws_url = urls.ws_fapi.clone();
243
244        let connection_manager = Arc::new(ws::BinanceConnectionManager::new(ws_url));
245
246        Ok(Self {
247            base,
248            options,
249            time_sync,
250            ws_connection: Arc::new(RwLock::new(None)),
251            connection_manager,
252        })
253    }
254
255    /// Returns a reference to the base exchange.
256    pub fn base(&self) -> &BaseExchange {
257        &self.base
258    }
259
260    /// Returns a mutable reference to the base exchange.
261    pub fn base_mut(&mut self) -> &mut BaseExchange {
262        &mut self.base
263    }
264
265    /// Returns the Binance options.
266    pub fn options(&self) -> &BinanceOptions {
267        &self.options
268    }
269
270    /// Sets the Binance options.
271    pub fn set_options(&mut self, options: BinanceOptions) {
272        self.options = options;
273    }
274
275    /// Returns a reference to the persistent WebSocket connection.
276    ///
277    /// This allows access to the stored WebSocket instance for state tracking.
278    /// The connection is stored when `ws_connect()` is called and cleared
279    /// when `ws_disconnect()` is called.
280    ///
281    /// # Example
282    ///
283    /// ```no_run
284    /// use ccxt_exchanges::binance::Binance;
285    /// use ccxt_core::ExchangeConfig;
286    ///
287    /// # async fn example() -> ccxt_core::Result<()> {
288    /// let binance = Binance::new(ExchangeConfig::default())?;
289    /// let ws_conn = binance.ws_connection();
290    /// let guard = ws_conn.read().await;
291    /// if guard.is_some() {
292    ///     println!("WebSocket is connected");
293    /// }
294    /// # Ok(())
295    /// # }
296    /// ```
297    pub fn ws_connection(&self) -> &Arc<RwLock<Option<ws::BinanceWs>>> {
298        &self.ws_connection
299    }
300
301    /// Returns a reference to the time synchronization manager.
302    ///
303    /// The `TimeSyncManager` caches the time offset between local system time
304    /// and Binance server time, reducing network round-trips for signed requests.
305    ///
306    /// # Example
307    ///
308    /// ```no_run
309    /// use ccxt_exchanges::binance::Binance;
310    /// use ccxt_core::ExchangeConfig;
311    ///
312    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
313    /// let time_sync = binance.time_sync();
314    /// println!("Time sync initialized: {}", time_sync.is_initialized());
315    /// ```
316    pub fn time_sync(&self) -> &Arc<TimeSyncManager> {
317        &self.time_sync
318    }
319
320    /// Creates a new signed request builder for the given endpoint.
321    ///
322    /// This is the recommended way to make authenticated API requests.
323    /// The builder handles credential validation, timestamp generation,
324    /// parameter signing, and request execution.
325    ///
326    /// # Arguments
327    ///
328    /// * `endpoint` - Full API endpoint URL
329    ///
330    /// # Example
331    ///
332    /// ```no_run
333    /// use ccxt_exchanges::binance::{Binance, HttpMethod};
334    /// use ccxt_core::ExchangeConfig;
335    ///
336    /// # async fn example() -> ccxt_core::Result<()> {
337    /// let binance = Binance::new(ExchangeConfig::default())?;
338    ///
339    /// // Simple GET request
340    /// let response = binance.signed_request("https://api.binance.com/api/v3/account")
341    ///     .execute()
342    ///     .await?;
343    ///
344    /// // POST request with parameters
345    /// let response = binance.signed_request("https://api.binance.com/api/v3/order")
346    ///     .method(HttpMethod::Post)
347    ///     .param("symbol", "BTCUSDT")
348    ///     .param("side", "BUY")
349    ///     .execute()
350    ///     .await?;
351    /// # Ok(())
352    /// # }
353    /// ```
354    pub fn signed_request(&self, endpoint: impl Into<String>) -> SignedRequestBuilder<'_> {
355        SignedRequestBuilder::new(self, endpoint)
356    }
357
358    /// Returns the exchange ID.
359    pub fn id(&self) -> &'static str {
360        "binance"
361    }
362
363    /// Returns the exchange name.
364    pub fn name(&self) -> &'static str {
365        "Binance"
366    }
367
368    /// Returns the API version.
369    pub fn version(&self) -> &'static str {
370        "v3"
371    }
372
373    /// Returns `true` if the exchange is CCXT-certified.
374    pub fn certified(&self) -> bool {
375        true
376    }
377
378    /// Returns `true` if Pro version (WebSocket) is supported.
379    pub fn pro(&self) -> bool {
380        true
381    }
382
383    /// Returns the rate limit in requests per second.
384    pub fn rate_limit(&self) -> u32 {
385        50
386    }
387
388    /// Returns `true` if sandbox/testnet mode is enabled.
389    ///
390    /// Sandbox mode is enabled when either:
391    /// - `config.sandbox` is set to `true`
392    /// - `options.test` is set to `true`
393    ///
394    /// # Returns
395    ///
396    /// `true` if sandbox mode is enabled, `false` otherwise.
397    ///
398    /// # Example
399    ///
400    /// ```no_run
401    /// use ccxt_exchanges::binance::Binance;
402    /// use ccxt_core::ExchangeConfig;
403    ///
404    /// let config = ExchangeConfig {
405    ///     sandbox: true,
406    ///     ..Default::default()
407    /// };
408    /// let binance = Binance::new(config).unwrap();
409    /// assert!(binance.is_sandbox());
410    /// ```
411    pub fn is_sandbox(&self) -> bool {
412        self.base().config.sandbox || self.options.test
413    }
414
415    /// Returns the supported timeframes.
416    pub fn timeframes(&self) -> std::collections::HashMap<String, String> {
417        constants::timeframes()
418    }
419
420    /// Returns the API URLs.
421    pub fn urls(&self) -> BinanceUrls {
422        let mut urls = if self.base().config.sandbox {
423            BinanceUrls::testnet()
424        } else {
425            BinanceUrls::production()
426        };
427
428        // Apply URL overrides if present
429        if let Some(public_url) = self.base().config.url_overrides.get("public") {
430            urls.public.clone_from(public_url);
431        }
432        if let Some(private_url) = self.base().config.url_overrides.get("private") {
433            urls.private.clone_from(private_url);
434        }
435        if let Some(fapi_public_url) = self.base().config.url_overrides.get("fapiPublic") {
436            urls.fapi_public.clone_from(fapi_public_url);
437        }
438        if let Some(fapi_private_url) = self.base().config.url_overrides.get("fapiPrivate") {
439            urls.fapi_private.clone_from(fapi_private_url);
440        }
441        if let Some(dapi_public_url) = self.base().config.url_overrides.get("dapiPublic") {
442            urls.dapi_public.clone_from(dapi_public_url);
443        }
444        if let Some(dapi_private_url) = self.base().config.url_overrides.get("dapiPrivate") {
445            urls.dapi_private.clone_from(dapi_private_url);
446        }
447        // WebSocket URL overrides
448        if let Some(ws_url) = self.base().config.url_overrides.get("ws") {
449            urls.ws.clone_from(ws_url);
450        }
451        if let Some(ws_fapi_url) = self.base().config.url_overrides.get("wsFapi") {
452            urls.ws_fapi.clone_from(ws_fapi_url);
453        }
454        if let Some(ws_dapi_url) = self.base().config.url_overrides.get("wsDapi") {
455            urls.ws_dapi.clone_from(ws_dapi_url);
456        }
457        if let Some(ws_eapi_url) = self.base().config.url_overrides.get("wsEapi") {
458            urls.ws_eapi.clone_from(ws_eapi_url);
459        }
460
461        urls
462    }
463
464    /// Determines the WebSocket URL based on default_type and default_sub_type.
465    ///
466    /// This method implements the endpoint routing logic according to:
467    /// - Spot/Margin: Uses the standard WebSocket endpoint
468    /// - Swap/Futures with Linear sub-type: Uses FAPI WebSocket endpoint
469    /// - Swap/Futures with Inverse sub-type: Uses DAPI WebSocket endpoint
470    /// - Option: Uses EAPI WebSocket endpoint
471    ///
472    /// # Returns
473    ///
474    /// The appropriate WebSocket URL string.
475    ///
476    /// # Note
477    ///
478    /// This method delegates to `BinanceEndpointRouter::default_ws_endpoint()`.
479    /// The routing logic is centralized in the trait implementation.
480    pub fn get_ws_url(&self) -> String {
481        BinanceEndpointRouter::default_ws_endpoint(self)
482    }
483
484    /// Returns the public REST API base URL based on default_type and default_sub_type.
485    ///
486    /// This method implements the endpoint routing logic for public REST API calls:
487    /// - Spot: Uses the public API endpoint (api.binance.com)
488    /// - Margin: Uses the SAPI endpoint (api.binance.com/sapi)
489    /// - Swap/Futures with Linear sub-type: Uses FAPI endpoint (fapi.binance.com)
490    /// - Swap/Futures with Inverse sub-type: Uses DAPI endpoint (dapi.binance.com)
491    /// - Option: Uses EAPI endpoint (eapi.binance.com)
492    ///
493    /// # Returns
494    ///
495    /// The appropriate REST API base URL string.
496    ///
497    /// # Note
498    ///
499    /// This method delegates to `BinanceEndpointRouter::default_rest_endpoint(EndpointType::Public)`.
500    /// The routing logic is centralized in the trait implementation.
501    ///
502    /// # Example
503    ///
504    /// ```no_run
505    /// use ccxt_exchanges::binance::{Binance, BinanceOptions};
506    /// use ccxt_core::ExchangeConfig;
507    /// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
508    ///
509    /// let options = BinanceOptions {
510    ///     default_type: DefaultType::Swap,
511    ///     default_sub_type: Some(DefaultSubType::Linear),
512    ///     ..Default::default()
513    /// };
514    /// let binance = Binance::new_with_options(ExchangeConfig::default(), options).unwrap();
515    /// let url = binance.get_rest_url_public();
516    /// assert!(url.contains("fapi.binance.com"));
517    /// ```
518    pub fn get_rest_url_public(&self) -> String {
519        BinanceEndpointRouter::default_rest_endpoint(self, EndpointType::Public)
520    }
521
522    /// Returns the private REST API base URL based on default_type and default_sub_type.
523    ///
524    /// This method implements the endpoint routing logic for private REST API calls:
525    /// - Spot: Uses the private API endpoint (api.binance.com)
526    /// - Margin: Uses the SAPI endpoint (api.binance.com/sapi)
527    /// - Swap/Futures with Linear sub-type: Uses FAPI endpoint (fapi.binance.com)
528    /// - Swap/Futures with Inverse sub-type: Uses DAPI endpoint (dapi.binance.com)
529    /// - Option: Uses EAPI endpoint (eapi.binance.com)
530    ///
531    /// # Returns
532    ///
533    /// The appropriate REST API base URL string.
534    ///
535    /// # Note
536    ///
537    /// This method delegates to `BinanceEndpointRouter::default_rest_endpoint(EndpointType::Private)`.
538    /// The routing logic is centralized in the trait implementation.
539    ///
540    /// # Example
541    ///
542    /// ```no_run
543    /// use ccxt_exchanges::binance::{Binance, BinanceOptions};
544    /// use ccxt_core::ExchangeConfig;
545    /// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
546    ///
547    /// let options = BinanceOptions {
548    ///     default_type: DefaultType::Swap,
549    ///     default_sub_type: Some(DefaultSubType::Inverse),
550    ///     ..Default::default()
551    /// };
552    /// let binance = Binance::new_with_options(ExchangeConfig::default(), options).unwrap();
553    /// let url = binance.get_rest_url_private();
554    /// assert!(url.contains("dapi.binance.com"));
555    /// ```
556    pub fn get_rest_url_private(&self) -> String {
557        BinanceEndpointRouter::default_rest_endpoint(self, EndpointType::Private)
558    }
559
560    /// Checks if the current default_type is a contract type (Swap, Futures, or Option).
561    ///
562    /// This is useful for determining whether contract-specific API endpoints should be used.
563    ///
564    /// # Returns
565    ///
566    /// `true` if the default_type is Swap, Futures, or Option; `false` otherwise.
567    pub fn is_contract_type(&self) -> bool {
568        self.options.default_type.is_contract()
569    }
570
571    /// Checks if the current configuration uses inverse (coin-margined) contracts.
572    ///
573    /// # Returns
574    ///
575    /// `true` if default_sub_type is Inverse; `false` otherwise.
576    pub fn is_inverse(&self) -> bool {
577        matches!(self.options.default_sub_type, Some(DefaultSubType::Inverse))
578    }
579
580    /// Checks if the current configuration uses linear (USDT-margined) contracts.
581    ///
582    /// # Returns
583    ///
584    /// `true` if default_sub_type is Linear or not specified (defaults to Linear); `false` otherwise.
585    pub fn is_linear(&self) -> bool {
586        !self.is_inverse()
587    }
588
589    /// Creates a WebSocket client for public data streams.
590    ///
591    /// Used for subscribing to public data streams (ticker, orderbook, trades, etc.).
592    /// The WebSocket endpoint is automatically selected based on `default_type` and `default_sub_type`:
593    /// - Spot/Margin: `wss://stream.binance.com:9443/ws`
594    /// - Swap/Futures (Linear): `wss://fstream.binance.com/ws`
595    /// - Swap/Futures (Inverse): `wss://dstream.binance.com/ws`
596    /// - Option: `wss://nbstream.binance.com/eoptions/ws`
597    ///
598    /// # Returns
599    ///
600    /// Returns a `BinanceWs` instance.
601    ///
602    /// # Example
603    /// ```no_run
604    /// use ccxt_exchanges::binance::Binance;
605    /// use ccxt_core::ExchangeConfig;
606    ///
607    /// # async fn example() -> ccxt_core::error::Result<()> {
608    /// let binance = Binance::new(ExchangeConfig::default())?;
609    /// let ws = binance.create_ws();
610    /// ws.connect().await?;
611    /// # Ok(())
612    /// # }
613    /// ```
614    pub fn create_ws(&self) -> ws::BinanceWs {
615        let ws_url = self.get_ws_url();
616        ws::BinanceWs::new(ws_url)
617    }
618
619    /// Creates an authenticated WebSocket client for user data streams.
620    ///
621    /// Used for subscribing to private data streams (account balance, order updates, trade history, etc.).
622    /// Requires API key configuration.
623    /// The WebSocket endpoint is automatically selected based on `default_type` and `default_sub_type`.
624    ///
625    /// # Returns
626    ///
627    /// Returns a `BinanceWs` instance with listen key manager.
628    ///
629    /// # Example
630    /// ```no_run
631    /// use ccxt_exchanges::binance::Binance;
632    /// use ccxt_core::ExchangeConfig;
633    /// use std::sync::Arc;
634    ///
635    /// # async fn example() -> ccxt_core::error::Result<()> {
636    /// let config = ExchangeConfig {
637    ///     api_key: Some("your-api-key".to_string()),
638    ///     secret: Some("your-secret".to_string()),
639    ///     ..Default::default()
640    /// };
641    /// let binance = Arc::new(Binance::new(config)?);
642    /// let ws = binance.create_authenticated_ws();
643    /// ws.connect_user_stream().await?;
644    /// # Ok(())
645    /// # }
646    /// ```
647    pub fn create_authenticated_ws(self: &std::sync::Arc<Self>) -> ws::BinanceWs {
648        let ws_url = self.get_ws_url();
649        ws::BinanceWs::new_with_auth(ws_url, self.clone())
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    #![allow(clippy::disallowed_methods)]
656    use super::*;
657
658    #[test]
659    fn test_binance_creation() {
660        let config = ExchangeConfig {
661            id: "binance".to_string(),
662            name: "Binance".to_string(),
663            ..Default::default()
664        };
665
666        let binance = Binance::new(config);
667        assert!(binance.is_ok());
668
669        let binance = binance.unwrap();
670        assert_eq!(binance.id(), "binance");
671        assert_eq!(binance.name(), "Binance");
672        assert_eq!(binance.version(), "v3");
673        assert!(binance.certified());
674        assert!(binance.pro());
675    }
676
677    #[test]
678    fn test_timeframes() {
679        let config = ExchangeConfig::default();
680        let binance = Binance::new(config).unwrap();
681        let timeframes = binance.timeframes();
682
683        assert!(timeframes.contains_key("1m"));
684        assert!(timeframes.contains_key("1h"));
685        assert!(timeframes.contains_key("1d"));
686        assert_eq!(timeframes.len(), 16);
687    }
688
689    #[test]
690    fn test_urls() {
691        let config = ExchangeConfig::default();
692        let binance = Binance::new(config).unwrap();
693        let urls = binance.urls();
694
695        assert!(urls.public.contains("api.binance.com"));
696        assert!(urls.ws.contains("stream.binance.com"));
697    }
698
699    #[test]
700    fn test_sandbox_urls() {
701        let config = ExchangeConfig {
702            sandbox: true,
703            ..Default::default()
704        };
705        let binance = Binance::new(config).unwrap();
706        let urls = binance.urls();
707
708        assert!(urls.public.contains("testnet"));
709    }
710
711    #[test]
712    fn test_is_sandbox_with_config_sandbox() {
713        let config = ExchangeConfig {
714            sandbox: true,
715            ..Default::default()
716        };
717        let binance = Binance::new(config).unwrap();
718        assert!(binance.is_sandbox());
719    }
720
721    #[test]
722    fn test_is_sandbox_with_options_test() {
723        let config = ExchangeConfig::default();
724        let options = BinanceOptions {
725            test: true,
726            ..Default::default()
727        };
728        let binance = Binance::new_with_options(config, options).unwrap();
729        assert!(binance.is_sandbox());
730    }
731
732    #[test]
733    fn test_is_sandbox_false_by_default() {
734        let config = ExchangeConfig::default();
735        let binance = Binance::new(config).unwrap();
736        assert!(!binance.is_sandbox());
737    }
738
739    #[test]
740    fn test_binance_options_default() {
741        let options = BinanceOptions::default();
742        assert_eq!(options.default_type, DefaultType::Spot);
743        assert_eq!(options.default_sub_type, None);
744        assert!(!options.adjust_for_time_difference);
745        assert_eq!(options.recv_window, 5000);
746        assert!(!options.test);
747        assert_eq!(options.time_sync_interval_secs, 30);
748        assert!(options.auto_time_sync);
749    }
750
751    #[test]
752    fn test_binance_options_with_default_type() {
753        let options = BinanceOptions {
754            default_type: DefaultType::Swap,
755            default_sub_type: Some(DefaultSubType::Linear),
756            ..Default::default()
757        };
758        assert_eq!(options.default_type, DefaultType::Swap);
759        assert_eq!(options.default_sub_type, Some(DefaultSubType::Linear));
760    }
761
762    #[test]
763    fn test_binance_options_serialization() {
764        let options = BinanceOptions {
765            default_type: DefaultType::Swap,
766            default_sub_type: Some(DefaultSubType::Linear),
767            ..Default::default()
768        };
769        let json = serde_json::to_string(&options).unwrap();
770        assert!(json.contains("\"default_type\":\"swap\""));
771        assert!(json.contains("\"default_sub_type\":\"linear\""));
772        assert!(json.contains("\"time_sync_interval_secs\":30"));
773        assert!(json.contains("\"auto_time_sync\":true"));
774    }
775
776    #[test]
777    fn test_binance_options_deserialization_with_enum() {
778        let json = r#"{
779            "adjust_for_time_difference": false,
780            "recv_window": 5000,
781            "default_type": "swap",
782            "default_sub_type": "linear",
783            "test": false,
784            "time_sync_interval_secs": 60,
785            "auto_time_sync": false
786        }"#;
787        let options: BinanceOptions = serde_json::from_str(json).unwrap();
788        assert_eq!(options.default_type, DefaultType::Swap);
789        assert_eq!(options.default_sub_type, Some(DefaultSubType::Linear));
790        assert_eq!(options.time_sync_interval_secs, 60);
791        assert!(!options.auto_time_sync);
792    }
793
794    #[test]
795    fn test_binance_options_deserialization_legacy_future() {
796        // Test backward compatibility with legacy "future" value
797        let json = r#"{
798            "adjust_for_time_difference": false,
799            "recv_window": 5000,
800            "default_type": "future",
801            "test": false
802        }"#;
803        let options: BinanceOptions = serde_json::from_str(json).unwrap();
804        assert_eq!(options.default_type, DefaultType::Swap);
805        // Verify defaults are applied for missing fields
806        assert_eq!(options.time_sync_interval_secs, 30);
807        assert!(options.auto_time_sync);
808    }
809
810    #[test]
811    fn test_binance_options_deserialization_legacy_delivery() {
812        // Test backward compatibility with legacy "delivery" value
813        let json = r#"{
814            "adjust_for_time_difference": false,
815            "recv_window": 5000,
816            "default_type": "delivery",
817            "test": false
818        }"#;
819        let options: BinanceOptions = serde_json::from_str(json).unwrap();
820        assert_eq!(options.default_type, DefaultType::Futures);
821    }
822
823    #[test]
824    fn test_binance_options_deserialization_without_sub_type() {
825        let json = r#"{
826            "adjust_for_time_difference": false,
827            "recv_window": 5000,
828            "default_type": "spot",
829            "test": false
830        }"#;
831        let options: BinanceOptions = serde_json::from_str(json).unwrap();
832        assert_eq!(options.default_type, DefaultType::Spot);
833        assert_eq!(options.default_sub_type, None);
834    }
835
836    #[test]
837    fn test_binance_options_deserialization_case_insensitive() {
838        // Test case-insensitive deserialization
839        let json = r#"{
840            "adjust_for_time_difference": false,
841            "recv_window": 5000,
842            "default_type": "SWAP",
843            "test": false
844        }"#;
845        let options: BinanceOptions = serde_json::from_str(json).unwrap();
846        assert_eq!(options.default_type, DefaultType::Swap);
847
848        // Test mixed case
849        let json = r#"{
850            "adjust_for_time_difference": false,
851            "recv_window": 5000,
852            "default_type": "FuTuReS",
853            "test": false
854        }"#;
855        let options: BinanceOptions = serde_json::from_str(json).unwrap();
856        assert_eq!(options.default_type, DefaultType::Futures);
857    }
858
859    #[test]
860    fn test_new_futures_uses_swap_type() {
861        let config = ExchangeConfig::default();
862        let binance = Binance::new_swap(config).unwrap();
863        assert_eq!(binance.options().default_type, DefaultType::Swap);
864    }
865
866    #[test]
867    fn test_get_ws_url_spot() {
868        let config = ExchangeConfig::default();
869        let options = BinanceOptions {
870            default_type: DefaultType::Spot,
871            ..Default::default()
872        };
873        let binance = Binance::new_with_options(config, options).unwrap();
874        let ws_url = binance.get_ws_url();
875        assert!(ws_url.contains("stream.binance.com"));
876    }
877
878    #[test]
879    fn test_get_ws_url_margin() {
880        let config = ExchangeConfig::default();
881        let options = BinanceOptions {
882            default_type: DefaultType::Margin,
883            ..Default::default()
884        };
885        let binance = Binance::new_with_options(config, options).unwrap();
886        let ws_url = binance.get_ws_url();
887        // Margin uses the same WebSocket as Spot
888        assert!(ws_url.contains("stream.binance.com"));
889    }
890
891    #[test]
892    fn test_get_ws_url_swap_linear() {
893        let config = ExchangeConfig::default();
894        let options = BinanceOptions {
895            default_type: DefaultType::Swap,
896            default_sub_type: Some(DefaultSubType::Linear),
897            ..Default::default()
898        };
899        let binance = Binance::new_with_options(config, options).unwrap();
900        let ws_url = binance.get_ws_url();
901        assert!(ws_url.contains("fstream.binance.com"));
902    }
903
904    #[test]
905    fn test_get_ws_url_swap_inverse() {
906        let config = ExchangeConfig::default();
907        let options = BinanceOptions {
908            default_type: DefaultType::Swap,
909            default_sub_type: Some(DefaultSubType::Inverse),
910            ..Default::default()
911        };
912        let binance = Binance::new_with_options(config, options).unwrap();
913        let ws_url = binance.get_ws_url();
914        assert!(ws_url.contains("dstream.binance.com"));
915    }
916
917    #[test]
918    fn test_get_ws_url_swap_default_sub_type() {
919        // When sub_type is not specified, should default to Linear (FAPI)
920        let config = ExchangeConfig::default();
921        let options = BinanceOptions {
922            default_type: DefaultType::Swap,
923            default_sub_type: None,
924            ..Default::default()
925        };
926        let binance = Binance::new_with_options(config, options).unwrap();
927        let ws_url = binance.get_ws_url();
928        assert!(ws_url.contains("fstream.binance.com"));
929    }
930
931    #[test]
932    fn test_get_ws_url_futures_linear() {
933        let config = ExchangeConfig::default();
934        let options = BinanceOptions {
935            default_type: DefaultType::Futures,
936            default_sub_type: Some(DefaultSubType::Linear),
937            ..Default::default()
938        };
939        let binance = Binance::new_with_options(config, options).unwrap();
940        let ws_url = binance.get_ws_url();
941        assert!(ws_url.contains("fstream.binance.com"));
942    }
943
944    #[test]
945    fn test_get_ws_url_futures_inverse() {
946        let config = ExchangeConfig::default();
947        let options = BinanceOptions {
948            default_type: DefaultType::Futures,
949            default_sub_type: Some(DefaultSubType::Inverse),
950            ..Default::default()
951        };
952        let binance = Binance::new_with_options(config, options).unwrap();
953        let ws_url = binance.get_ws_url();
954        assert!(ws_url.contains("dstream.binance.com"));
955    }
956
957    #[test]
958    fn test_get_ws_url_option() {
959        let config = ExchangeConfig::default();
960        let options = BinanceOptions {
961            default_type: DefaultType::Option,
962            ..Default::default()
963        };
964        let binance = Binance::new_with_options(config, options).unwrap();
965        let ws_url = binance.get_ws_url();
966        assert!(ws_url.contains("nbstream.binance.com") || ws_url.contains("eoptions"));
967    }
968
969    #[test]
970    fn test_binance_urls_has_all_ws_endpoints() {
971        let urls = BinanceUrls::production();
972        assert!(!urls.ws.is_empty());
973        assert!(!urls.ws_fapi.is_empty());
974        assert!(!urls.ws_dapi.is_empty());
975        assert!(!urls.ws_eapi.is_empty());
976    }
977
978    #[test]
979    fn test_binance_urls_testnet_has_all_ws_endpoints() {
980        let urls = BinanceUrls::testnet();
981        assert!(!urls.ws.is_empty());
982        assert!(!urls.ws_fapi.is_empty());
983        assert!(!urls.ws_dapi.is_empty());
984        assert!(!urls.ws_eapi.is_empty());
985    }
986
987    #[test]
988    fn test_binance_urls_demo_has_all_ws_endpoints() {
989        let urls = BinanceUrls::demo();
990        assert!(!urls.ws.is_empty());
991        assert!(!urls.ws_fapi.is_empty());
992        assert!(!urls.ws_dapi.is_empty());
993        assert!(!urls.ws_eapi.is_empty());
994    }
995
996    #[test]
997    fn test_get_rest_url_public_spot() {
998        let config = ExchangeConfig::default();
999        let options = BinanceOptions {
1000            default_type: DefaultType::Spot,
1001            ..Default::default()
1002        };
1003        let binance = Binance::new_with_options(config, options).unwrap();
1004        let url = binance.get_rest_url_public();
1005        assert!(url.contains("api.binance.com"));
1006        assert!(url.contains("/api/v3"));
1007    }
1008
1009    #[test]
1010    fn test_get_rest_url_public_margin() {
1011        let config = ExchangeConfig::default();
1012        let options = BinanceOptions {
1013            default_type: DefaultType::Margin,
1014            ..Default::default()
1015        };
1016        let binance = Binance::new_with_options(config, options).unwrap();
1017        let url = binance.get_rest_url_public();
1018        assert!(url.contains("api.binance.com"));
1019        assert!(url.contains("/sapi/"));
1020    }
1021
1022    #[test]
1023    fn test_get_rest_url_public_swap_linear() {
1024        let config = ExchangeConfig::default();
1025        let options = BinanceOptions {
1026            default_type: DefaultType::Swap,
1027            default_sub_type: Some(DefaultSubType::Linear),
1028            ..Default::default()
1029        };
1030        let binance = Binance::new_with_options(config, options).unwrap();
1031        let url = binance.get_rest_url_public();
1032        assert!(url.contains("fapi.binance.com"));
1033    }
1034
1035    #[test]
1036    fn test_get_rest_url_public_swap_inverse() {
1037        let config = ExchangeConfig::default();
1038        let options = BinanceOptions {
1039            default_type: DefaultType::Swap,
1040            default_sub_type: Some(DefaultSubType::Inverse),
1041            ..Default::default()
1042        };
1043        let binance = Binance::new_with_options(config, options).unwrap();
1044        let url = binance.get_rest_url_public();
1045        assert!(url.contains("dapi.binance.com"));
1046    }
1047
1048    #[test]
1049    fn test_get_rest_url_public_futures_linear() {
1050        let config = ExchangeConfig::default();
1051        let options = BinanceOptions {
1052            default_type: DefaultType::Futures,
1053            default_sub_type: Some(DefaultSubType::Linear),
1054            ..Default::default()
1055        };
1056        let binance = Binance::new_with_options(config, options).unwrap();
1057        let url = binance.get_rest_url_public();
1058        assert!(url.contains("fapi.binance.com"));
1059    }
1060
1061    #[test]
1062    fn test_get_rest_url_public_futures_inverse() {
1063        let config = ExchangeConfig::default();
1064        let options = BinanceOptions {
1065            default_type: DefaultType::Futures,
1066            default_sub_type: Some(DefaultSubType::Inverse),
1067            ..Default::default()
1068        };
1069        let binance = Binance::new_with_options(config, options).unwrap();
1070        let url = binance.get_rest_url_public();
1071        assert!(url.contains("dapi.binance.com"));
1072    }
1073
1074    #[test]
1075    fn test_get_rest_url_public_option() {
1076        let config = ExchangeConfig::default();
1077        let options = BinanceOptions {
1078            default_type: DefaultType::Option,
1079            ..Default::default()
1080        };
1081        let binance = Binance::new_with_options(config, options).unwrap();
1082        let url = binance.get_rest_url_public();
1083        assert!(url.contains("eapi.binance.com"));
1084    }
1085
1086    #[test]
1087    fn test_get_rest_url_private_swap_linear() {
1088        let config = ExchangeConfig::default();
1089        let options = BinanceOptions {
1090            default_type: DefaultType::Swap,
1091            default_sub_type: Some(DefaultSubType::Linear),
1092            ..Default::default()
1093        };
1094        let binance = Binance::new_with_options(config, options).unwrap();
1095        let url = binance.get_rest_url_private();
1096        assert!(url.contains("fapi.binance.com"));
1097    }
1098
1099    #[test]
1100    fn test_get_rest_url_private_swap_inverse() {
1101        let config = ExchangeConfig::default();
1102        let options = BinanceOptions {
1103            default_type: DefaultType::Swap,
1104            default_sub_type: Some(DefaultSubType::Inverse),
1105            ..Default::default()
1106        };
1107        let binance = Binance::new_with_options(config, options).unwrap();
1108        let url = binance.get_rest_url_private();
1109        assert!(url.contains("dapi.binance.com"));
1110    }
1111
1112    #[test]
1113    fn test_is_contract_type() {
1114        let config = ExchangeConfig::default();
1115
1116        // Spot is not a contract type
1117        let options = BinanceOptions {
1118            default_type: DefaultType::Spot,
1119            ..Default::default()
1120        };
1121        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1122        assert!(!binance.is_contract_type());
1123
1124        // Margin is not a contract type
1125        let options = BinanceOptions {
1126            default_type: DefaultType::Margin,
1127            ..Default::default()
1128        };
1129        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1130        assert!(!binance.is_contract_type());
1131
1132        // Swap is a contract type
1133        let options = BinanceOptions {
1134            default_type: DefaultType::Swap,
1135            ..Default::default()
1136        };
1137        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1138        assert!(binance.is_contract_type());
1139
1140        // Futures is a contract type
1141        let options = BinanceOptions {
1142            default_type: DefaultType::Futures,
1143            ..Default::default()
1144        };
1145        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1146        assert!(binance.is_contract_type());
1147
1148        // Option is a contract type
1149        let options = BinanceOptions {
1150            default_type: DefaultType::Option,
1151            ..Default::default()
1152        };
1153        let binance = Binance::new_with_options(config, options).unwrap();
1154        assert!(binance.is_contract_type());
1155    }
1156
1157    #[test]
1158    fn test_is_linear_and_is_inverse() {
1159        let config = ExchangeConfig::default();
1160
1161        // No sub-type specified defaults to linear
1162        let options = BinanceOptions {
1163            default_type: DefaultType::Swap,
1164            default_sub_type: None,
1165            ..Default::default()
1166        };
1167        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1168        assert!(binance.is_linear());
1169        assert!(!binance.is_inverse());
1170
1171        // Explicit linear
1172        let options = BinanceOptions {
1173            default_type: DefaultType::Swap,
1174            default_sub_type: Some(DefaultSubType::Linear),
1175            ..Default::default()
1176        };
1177        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1178        assert!(binance.is_linear());
1179        assert!(!binance.is_inverse());
1180
1181        // Explicit inverse
1182        let options = BinanceOptions {
1183            default_type: DefaultType::Swap,
1184            default_sub_type: Some(DefaultSubType::Inverse),
1185            ..Default::default()
1186        };
1187        let binance = Binance::new_with_options(config, options).unwrap();
1188        assert!(!binance.is_linear());
1189        assert!(binance.is_inverse());
1190    }
1191
1192    // ============================================================
1193    // Sandbox Mode Market Type URL Selection Tests
1194    // ============================================================
1195
1196    #[test]
1197    fn test_sandbox_market_type_spot() {
1198        let config = ExchangeConfig {
1199            sandbox: true,
1200            ..Default::default()
1201        };
1202        let options = BinanceOptions {
1203            default_type: DefaultType::Spot,
1204            ..Default::default()
1205        };
1206        let binance = Binance::new_with_options(config, options).unwrap();
1207
1208        assert!(binance.is_sandbox());
1209        let url = binance.get_rest_url_public();
1210        assert!(
1211            url.contains("testnet.binance.vision"),
1212            "Spot sandbox URL should contain testnet.binance.vision, got: {}",
1213            url
1214        );
1215        assert!(
1216            url.contains("/api/v3"),
1217            "Spot sandbox URL should contain /api/v3, got: {}",
1218            url
1219        );
1220    }
1221
1222    #[test]
1223    fn test_sandbox_market_type_swap_linear() {
1224        let config = ExchangeConfig {
1225            sandbox: true,
1226            ..Default::default()
1227        };
1228        let options = BinanceOptions {
1229            default_type: DefaultType::Swap,
1230            default_sub_type: Some(DefaultSubType::Linear),
1231            ..Default::default()
1232        };
1233        let binance = Binance::new_with_options(config, options).unwrap();
1234
1235        assert!(binance.is_sandbox());
1236        let url = binance.get_rest_url_public();
1237        assert!(
1238            url.contains("testnet.binancefuture.com"),
1239            "Linear sandbox URL should contain testnet.binancefuture.com, got: {}",
1240            url
1241        );
1242        assert!(
1243            url.contains("/fapi/"),
1244            "Linear sandbox URL should contain /fapi/, got: {}",
1245            url
1246        );
1247    }
1248
1249    #[test]
1250    fn test_sandbox_market_type_swap_inverse() {
1251        let config = ExchangeConfig {
1252            sandbox: true,
1253            ..Default::default()
1254        };
1255        let options = BinanceOptions {
1256            default_type: DefaultType::Swap,
1257            default_sub_type: Some(DefaultSubType::Inverse),
1258            ..Default::default()
1259        };
1260        let binance = Binance::new_with_options(config, options).unwrap();
1261
1262        assert!(binance.is_sandbox());
1263        let url = binance.get_rest_url_public();
1264        assert!(
1265            url.contains("testnet.binancefuture.com"),
1266            "Inverse sandbox URL should contain testnet.binancefuture.com, got: {}",
1267            url
1268        );
1269        assert!(
1270            url.contains("/dapi/"),
1271            "Inverse sandbox URL should contain /dapi/, got: {}",
1272            url
1273        );
1274    }
1275
1276    #[test]
1277    fn test_sandbox_market_type_option() {
1278        let config = ExchangeConfig {
1279            sandbox: true,
1280            ..Default::default()
1281        };
1282        let options = BinanceOptions {
1283            default_type: DefaultType::Option,
1284            ..Default::default()
1285        };
1286        let binance = Binance::new_with_options(config, options).unwrap();
1287
1288        assert!(binance.is_sandbox());
1289        let url = binance.get_rest_url_public();
1290        assert!(
1291            url.contains("testnet.binanceops.com"),
1292            "Option sandbox URL should contain testnet.binanceops.com, got: {}",
1293            url
1294        );
1295        assert!(
1296            url.contains("/eapi/"),
1297            "Option sandbox URL should contain /eapi/, got: {}",
1298            url
1299        );
1300    }
1301
1302    #[test]
1303    fn test_sandbox_market_type_futures_linear() {
1304        let config = ExchangeConfig {
1305            sandbox: true,
1306            ..Default::default()
1307        };
1308        let options = BinanceOptions {
1309            default_type: DefaultType::Futures,
1310            default_sub_type: Some(DefaultSubType::Linear),
1311            ..Default::default()
1312        };
1313        let binance = Binance::new_with_options(config, options).unwrap();
1314
1315        assert!(binance.is_sandbox());
1316        let url = binance.get_rest_url_public();
1317        assert!(
1318            url.contains("testnet.binancefuture.com"),
1319            "Futures Linear sandbox URL should contain testnet.binancefuture.com, got: {}",
1320            url
1321        );
1322        assert!(
1323            url.contains("/fapi/"),
1324            "Futures Linear sandbox URL should contain /fapi/, got: {}",
1325            url
1326        );
1327    }
1328
1329    #[test]
1330    fn test_sandbox_market_type_futures_inverse() {
1331        let config = ExchangeConfig {
1332            sandbox: true,
1333            ..Default::default()
1334        };
1335        let options = BinanceOptions {
1336            default_type: DefaultType::Futures,
1337            default_sub_type: Some(DefaultSubType::Inverse),
1338            ..Default::default()
1339        };
1340        let binance = Binance::new_with_options(config, options).unwrap();
1341
1342        assert!(binance.is_sandbox());
1343        let url = binance.get_rest_url_public();
1344        assert!(
1345            url.contains("testnet.binancefuture.com"),
1346            "Futures Inverse sandbox URL should contain testnet.binancefuture.com, got: {}",
1347            url
1348        );
1349        assert!(
1350            url.contains("/dapi/"),
1351            "Futures Inverse sandbox URL should contain /dapi/, got: {}",
1352            url
1353        );
1354    }
1355
1356    #[test]
1357    fn test_sandbox_websocket_url_spot() {
1358        // Verify WebSocket URL selection in sandbox mode for Spot
1359        let config = ExchangeConfig {
1360            sandbox: true,
1361            ..Default::default()
1362        };
1363        let options = BinanceOptions {
1364            default_type: DefaultType::Spot,
1365            ..Default::default()
1366        };
1367        let binance = Binance::new_with_options(config, options).unwrap();
1368
1369        let urls = binance.urls();
1370        assert!(
1371            urls.ws.contains("testnet.binance.vision"),
1372            "Spot WS sandbox URL should contain testnet.binance.vision, got: {}",
1373            urls.ws
1374        );
1375    }
1376
1377    #[test]
1378    fn test_sandbox_websocket_url_fapi() {
1379        // Verify WebSocket URL selection in sandbox mode for FAPI (Linear)
1380        let config = ExchangeConfig {
1381            sandbox: true,
1382            ..Default::default()
1383        };
1384        let options = BinanceOptions {
1385            default_type: DefaultType::Swap,
1386            default_sub_type: Some(DefaultSubType::Linear),
1387            ..Default::default()
1388        };
1389        let binance = Binance::new_with_options(config, options).unwrap();
1390
1391        let urls = binance.urls();
1392        assert!(
1393            urls.ws_fapi.contains("binancefuture.com"),
1394            "FAPI WS sandbox URL should contain binancefuture.com, got: {}",
1395            urls.ws_fapi
1396        );
1397    }
1398
1399    #[test]
1400    fn test_sandbox_websocket_url_dapi() {
1401        // Verify WebSocket URL selection in sandbox mode for DAPI (Inverse)
1402        let config = ExchangeConfig {
1403            sandbox: true,
1404            ..Default::default()
1405        };
1406        let options = BinanceOptions {
1407            default_type: DefaultType::Swap,
1408            default_sub_type: Some(DefaultSubType::Inverse),
1409            ..Default::default()
1410        };
1411        let binance = Binance::new_with_options(config, options).unwrap();
1412
1413        let urls = binance.urls();
1414        assert!(
1415            urls.ws_dapi.contains("dstream.binancefuture.com"),
1416            "DAPI WS sandbox URL should contain dstream.binancefuture.com, got: {}",
1417            urls.ws_dapi
1418        );
1419    }
1420
1421    #[test]
1422    fn test_sandbox_websocket_url_eapi() {
1423        // Verify WebSocket URL selection in sandbox mode for EAPI (Options)
1424        let config = ExchangeConfig {
1425            sandbox: true,
1426            ..Default::default()
1427        };
1428        let options = BinanceOptions {
1429            default_type: DefaultType::Option,
1430            ..Default::default()
1431        };
1432        let binance = Binance::new_with_options(config, options).unwrap();
1433
1434        let urls = binance.urls();
1435        assert!(
1436            urls.ws_eapi.contains("testnet.binanceops.com"),
1437            "EAPI WS sandbox URL should contain testnet.binanceops.com, got: {}",
1438            urls.ws_eapi
1439        );
1440    }
1441}