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