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