ccxt_exchanges/binance/
mod.rs

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