ccxt_exchanges/binance/
endpoint_router.rs

1//! Binance-specific endpoint router trait.
2//!
3//! This module provides the `BinanceEndpointRouter` trait for routing API requests
4//! to the correct Binance domain based on market characteristics.
5//!
6//! # Binance API Domains
7//!
8//! Binance has a complex multi-domain structure:
9//! - **Spot**: `api.binance.com` - Spot trading and margin
10//! - **Linear Futures (FAPI)**: `fapi.binance.com` - USDT-margined perpetuals/futures
11//! - **Inverse Futures (DAPI)**: `dapi.binance.com` - Coin-margined perpetuals/futures
12//! - **Options (EAPI)**: `eapi.binance.com` - Options trading
13//! - **Portfolio Margin (PAPI)**: `papi.binance.com` - Portfolio margin API
14//!
15//! # Example
16//!
17//! ```rust,no_run
18//! use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
19//! use ccxt_core::types::{EndpointType, Market, MarketType};
20//! use ccxt_core::ExchangeConfig;
21//!
22//! let binance = Binance::new(ExchangeConfig::default()).unwrap();
23//!
24//! // Get REST endpoint for a spot market
25//! let spot_market = Market::new_spot(
26//!     "BTCUSDT".to_string(),
27//!     "BTC/USDT".to_string(),
28//!     "BTC".to_string(),
29//!     "USDT".to_string(),
30//! );
31//! let url = binance.rest_endpoint(&spot_market, EndpointType::Public);
32//! assert!(url.contains("api.binance.com"));
33//!
34//! // Get default REST endpoint based on exchange options
35//! let default_url = binance.default_rest_endpoint(EndpointType::Public);
36//! ```
37
38use ccxt_core::types::{EndpointType, Market, MarketType};
39
40/// Binance-specific endpoint router trait.
41///
42/// This trait defines methods for obtaining the correct API endpoints based on
43/// market characteristics. Binance has multiple API domains for different market
44/// types (spot, linear futures, inverse futures, options).
45///
46/// # Implementation Notes
47///
48/// The routing logic follows these rules:
49/// - **Spot markets**: Use `api.binance.com` endpoints
50/// - **Linear Swap/Futures**: Use `fapi.binance.com` endpoints (USDT-margined)
51/// - **Inverse Swap/Futures**: Use `dapi.binance.com` endpoints (Coin-margined)
52/// - **Option markets**: Use `eapi.binance.com` endpoints
53///
54/// When sandbox mode is enabled, testnet URLs are returned instead.
55pub trait BinanceEndpointRouter {
56    /// Returns the REST API endpoint for a specific market.
57    ///
58    /// This method routes to the correct Binance domain based on the market's
59    /// `market_type`, `linear`, and `inverse` fields.
60    ///
61    /// # Arguments
62    ///
63    /// * `market` - Reference to the market object containing type information
64    /// * `endpoint_type` - Whether this is a public or private endpoint
65    ///
66    /// # Returns
67    ///
68    /// The REST API base URL string for the given market.
69    ///
70    /// # Routing Logic
71    ///
72    /// | Market Type | Linear | Inverse | Domain |
73    /// |-------------|--------|---------|--------|
74    /// | Spot | - | - | api.binance.com |
75    /// | Swap/Futures | true | false | fapi.binance.com |
76    /// | Swap/Futures | false | true | dapi.binance.com |
77    /// | Option | - | - | eapi.binance.com |
78    ///
79    /// # Example
80    ///
81    /// ```rust,no_run
82    /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
83    /// use ccxt_core::types::{EndpointType, Market, MarketType};
84    /// use ccxt_core::ExchangeConfig;
85    /// use rust_decimal_macros::dec;
86    ///
87    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
88    ///
89    /// // Linear futures market
90    /// let linear_market = Market::new_swap(
91    ///     "BTCUSDT".to_string(),
92    ///     "BTC/USDT:USDT".to_string(),
93    ///     "BTC".to_string(),
94    ///     "USDT".to_string(),
95    ///     "USDT".to_string(),
96    ///     dec!(1.0),
97    /// );
98    /// let url = binance.rest_endpoint(&linear_market, EndpointType::Public);
99    /// assert!(url.contains("fapi.binance.com"));
100    /// ```
101    fn rest_endpoint(&self, market: &Market, endpoint_type: EndpointType) -> String;
102
103    /// Returns the WebSocket endpoint for a specific market.
104    ///
105    /// This method routes to the correct Binance WebSocket domain based on
106    /// the market's type and settlement characteristics.
107    ///
108    /// # Arguments
109    ///
110    /// * `market` - Reference to the market object containing type information
111    ///
112    /// # Returns
113    ///
114    /// The WebSocket URL string for the given market.
115    ///
116    /// # Routing Logic
117    ///
118    /// | Market Type | Linear | Inverse | WebSocket Domain |
119    /// |-------------|--------|---------|------------------|
120    /// | Spot | - | - | stream.binance.com |
121    /// | Swap/Futures | true | false | fstream.binance.com |
122    /// | Swap/Futures | false | true | dstream.binance.com |
123    /// | Option | - | - | nbstream.binance.com |
124    ///
125    /// # Example
126    ///
127    /// ```rust,no_run
128    /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
129    /// use ccxt_core::types::Market;
130    /// use ccxt_core::ExchangeConfig;
131    ///
132    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
133    ///
134    /// let spot_market = Market::new_spot(
135    ///     "BTCUSDT".to_string(),
136    ///     "BTC/USDT".to_string(),
137    ///     "BTC".to_string(),
138    ///     "USDT".to_string(),
139    /// );
140    /// let ws_url = binance.ws_endpoint(&spot_market);
141    /// assert!(ws_url.contains("stream.binance.com"));
142    /// ```
143    fn ws_endpoint(&self, market: &Market) -> String;
144
145    /// Returns the default REST endpoint when no specific market is provided.
146    ///
147    /// This method uses the exchange's `default_type` and `default_sub_type`
148    /// options to determine which endpoint to return.
149    ///
150    /// # Arguments
151    ///
152    /// * `endpoint_type` - Whether this is a public or private endpoint
153    ///
154    /// # Returns
155    ///
156    /// The default REST API base URL string.
157    ///
158    /// # Example
159    ///
160    /// ```rust,no_run
161    /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter, BinanceOptions};
162    /// use ccxt_core::types::EndpointType;
163    /// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
164    /// use ccxt_core::ExchangeConfig;
165    ///
166    /// // Create a futures-focused Binance instance
167    /// let options = BinanceOptions {
168    ///     default_type: DefaultType::Swap,
169    ///     default_sub_type: Some(DefaultSubType::Linear),
170    ///     ..Default::default()
171    /// };
172    /// let binance = Binance::new_with_options(ExchangeConfig::default(), options).unwrap();
173    ///
174    /// let url = binance.default_rest_endpoint(EndpointType::Public);
175    /// assert!(url.contains("fapi.binance.com"));
176    /// ```
177    fn default_rest_endpoint(&self, endpoint_type: EndpointType) -> String;
178
179    /// Returns the default WebSocket endpoint when no specific market is provided.
180    ///
181    /// This method uses the exchange's `default_type` and `default_sub_type`
182    /// options to determine which WebSocket endpoint to return.
183    ///
184    /// # Returns
185    ///
186    /// The default WebSocket URL string.
187    ///
188    /// # Example
189    ///
190    /// ```rust,no_run
191    /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
192    /// use ccxt_core::ExchangeConfig;
193    ///
194    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
195    /// let ws_url = binance.default_ws_endpoint();
196    /// // Default is spot, so should be stream.binance.com
197    /// assert!(ws_url.contains("stream.binance.com"));
198    /// ```
199    fn default_ws_endpoint(&self) -> String;
200
201    /// Returns the SAPI (Spot API) endpoint.
202    ///
203    /// SAPI is used for Binance-specific spot trading features like:
204    /// - Margin trading operations
205    /// - Savings and staking
206    /// - Sub-account management
207    /// - Asset transfers
208    ///
209    /// # Returns
210    ///
211    /// The SAPI base URL string.
212    ///
213    /// # Example
214    ///
215    /// ```rust,no_run
216    /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
217    /// use ccxt_core::ExchangeConfig;
218    ///
219    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
220    /// let sapi_url = binance.sapi_endpoint();
221    /// assert!(sapi_url.contains("sapi"));
222    /// ```
223    fn sapi_endpoint(&self) -> String;
224
225    /// Returns the Portfolio Margin API (PAPI) endpoint.
226    ///
227    /// PAPI is used for portfolio margin trading which allows cross-margining
228    /// across spot, futures, and options positions.
229    ///
230    /// # Returns
231    ///
232    /// The PAPI base URL string.
233    ///
234    /// # Example
235    ///
236    /// ```rust,no_run
237    /// use ccxt_exchanges::binance::{Binance, BinanceEndpointRouter};
238    /// use ccxt_core::ExchangeConfig;
239    ///
240    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
241    /// let papi_url = binance.papi_endpoint();
242    /// assert!(papi_url.contains("papi"));
243    /// ```
244    fn papi_endpoint(&self) -> String;
245}
246
247use super::Binance;
248
249use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
250
251impl BinanceEndpointRouter for Binance {
252    fn rest_endpoint(&self, market: &Market, endpoint_type: EndpointType) -> String {
253        let urls = self.urls();
254
255        match market.market_type {
256            MarketType::Spot => match endpoint_type {
257                EndpointType::Public => urls.public.clone(),
258                EndpointType::Private => urls.private.clone(),
259            },
260            MarketType::Swap | MarketType::Futures => {
261                // Determine linear/inverse from market fields
262                // Default to linear (true) if not specified
263                let is_linear = market.linear.unwrap_or(true);
264
265                if is_linear {
266                    match endpoint_type {
267                        EndpointType::Public => urls.fapi_public.clone(),
268                        EndpointType::Private => urls.fapi_private.clone(),
269                    }
270                } else {
271                    match endpoint_type {
272                        EndpointType::Public => urls.dapi_public.clone(),
273                        EndpointType::Private => urls.dapi_private.clone(),
274                    }
275                }
276            }
277            MarketType::Option => match endpoint_type {
278                EndpointType::Public => urls.eapi_public.clone(),
279                EndpointType::Private => urls.eapi_private.clone(),
280            },
281        }
282    }
283
284    fn ws_endpoint(&self, market: &Market) -> String {
285        let urls = self.urls();
286
287        match market.market_type {
288            MarketType::Spot => urls.ws.clone(),
289            MarketType::Swap | MarketType::Futures => {
290                // Determine linear/inverse from market fields
291                // Default to linear (true) if not specified
292                let is_linear = market.linear.unwrap_or(true);
293
294                if is_linear {
295                    urls.ws_fapi.clone()
296                } else {
297                    urls.ws_dapi.clone()
298                }
299            }
300            MarketType::Option => urls.ws_eapi.clone(),
301        }
302    }
303
304    fn default_rest_endpoint(&self, endpoint_type: EndpointType) -> String {
305        let urls = self.urls();
306        let options = self.options();
307
308        match options.default_type {
309            DefaultType::Spot => match endpoint_type {
310                EndpointType::Public => urls.public.clone(),
311                EndpointType::Private => urls.private.clone(),
312            },
313            DefaultType::Margin => urls.sapi.clone(),
314            DefaultType::Swap | DefaultType::Futures => {
315                match options.default_sub_type {
316                    Some(DefaultSubType::Inverse) => match endpoint_type {
317                        EndpointType::Public => urls.dapi_public.clone(),
318                        EndpointType::Private => urls.dapi_private.clone(),
319                    },
320                    _ => match endpoint_type {
321                        // Default to FAPI (Linear)
322                        EndpointType::Public => urls.fapi_public.clone(),
323                        EndpointType::Private => urls.fapi_private.clone(),
324                    },
325                }
326            }
327            DefaultType::Option => match endpoint_type {
328                EndpointType::Public => urls.eapi_public.clone(),
329                EndpointType::Private => urls.eapi_private.clone(),
330            },
331        }
332    }
333
334    fn default_ws_endpoint(&self) -> String {
335        let urls = self.urls();
336        let options = self.options();
337
338        match options.default_type {
339            DefaultType::Swap | DefaultType::Futures => {
340                // Check sub-type for FAPI vs DAPI selection
341                match options.default_sub_type {
342                    Some(DefaultSubType::Inverse) => urls.ws_dapi.clone(),
343                    _ => urls.ws_fapi.clone(), // Default to FAPI (Linear) if not specified
344                }
345            }
346            DefaultType::Option => urls.ws_eapi.clone(),
347            _ => urls.ws.clone(), // Spot and Margin use standard WebSocket
348        }
349    }
350
351    fn sapi_endpoint(&self) -> String {
352        self.urls().sapi.clone()
353    }
354
355    fn papi_endpoint(&self) -> String {
356        self.urls().papi.clone()
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use ccxt_core::ExchangeConfig;
364    use rust_decimal_macros::dec;
365
366    fn create_test_binance() -> Binance {
367        Binance::new(ExchangeConfig::default()).unwrap()
368    }
369
370    fn create_sandbox_binance() -> Binance {
371        let config = ExchangeConfig {
372            sandbox: true,
373            ..Default::default()
374        };
375        Binance::new(config).unwrap()
376    }
377
378    // ==================== REST Endpoint Tests ====================
379
380    #[test]
381    fn test_rest_endpoint_spot_public() {
382        let binance = create_test_binance();
383        let market = Market::new_spot(
384            "BTCUSDT".to_string(),
385            "BTC/USDT".to_string(),
386            "BTC".to_string(),
387            "USDT".to_string(),
388        );
389
390        let url = binance.rest_endpoint(&market, EndpointType::Public);
391        assert!(url.contains("api.binance.com"));
392    }
393
394    #[test]
395    fn test_rest_endpoint_spot_private() {
396        let binance = create_test_binance();
397        let market = Market::new_spot(
398            "BTCUSDT".to_string(),
399            "BTC/USDT".to_string(),
400            "BTC".to_string(),
401            "USDT".to_string(),
402        );
403
404        let url = binance.rest_endpoint(&market, EndpointType::Private);
405        assert!(url.contains("api.binance.com"));
406    }
407
408    #[test]
409    fn test_rest_endpoint_linear_swap_public() {
410        let binance = create_test_binance();
411        let market = Market::new_swap(
412            "BTCUSDT".to_string(),
413            "BTC/USDT:USDT".to_string(),
414            "BTC".to_string(),
415            "USDT".to_string(),
416            "USDT".to_string(),
417            dec!(1.0),
418        );
419
420        let url = binance.rest_endpoint(&market, EndpointType::Public);
421        assert!(url.contains("fapi.binance.com"));
422    }
423
424    #[test]
425    fn test_rest_endpoint_linear_swap_private() {
426        let binance = create_test_binance();
427        let market = Market::new_swap(
428            "BTCUSDT".to_string(),
429            "BTC/USDT:USDT".to_string(),
430            "BTC".to_string(),
431            "USDT".to_string(),
432            "USDT".to_string(),
433            dec!(1.0),
434        );
435
436        let url = binance.rest_endpoint(&market, EndpointType::Private);
437        assert!(url.contains("fapi.binance.com"));
438    }
439
440    #[test]
441    fn test_rest_endpoint_inverse_swap_public() {
442        let binance = create_test_binance();
443        let mut market = Market::new_swap(
444            "BTCUSD_PERP".to_string(),
445            "BTC/USD:BTC".to_string(),
446            "BTC".to_string(),
447            "USD".to_string(),
448            "BTC".to_string(),
449            dec!(100.0),
450        );
451        // Ensure inverse is set correctly
452        market.linear = Some(false);
453        market.inverse = Some(true);
454
455        let url = binance.rest_endpoint(&market, EndpointType::Public);
456        assert!(url.contains("dapi.binance.com"));
457    }
458
459    #[test]
460    fn test_rest_endpoint_inverse_swap_private() {
461        let binance = create_test_binance();
462        let mut market = Market::new_swap(
463            "BTCUSD_PERP".to_string(),
464            "BTC/USD:BTC".to_string(),
465            "BTC".to_string(),
466            "USD".to_string(),
467            "BTC".to_string(),
468            dec!(100.0),
469        );
470        market.linear = Some(false);
471        market.inverse = Some(true);
472
473        let url = binance.rest_endpoint(&market, EndpointType::Private);
474        assert!(url.contains("dapi.binance.com"));
475    }
476
477    #[test]
478    fn test_rest_endpoint_option_public() {
479        let binance = create_test_binance();
480        let mut market = Market::default();
481        market.id = "BTC-250328-100000-C".to_string();
482        market.symbol = "BTC/USDT:USDT-250328-100000-C".to_string();
483        market.market_type = MarketType::Option;
484
485        let url = binance.rest_endpoint(&market, EndpointType::Public);
486        assert!(url.contains("eapi.binance.com"));
487    }
488
489    #[test]
490    fn test_rest_endpoint_option_private() {
491        let binance = create_test_binance();
492        let mut market = Market::default();
493        market.id = "BTC-250328-100000-C".to_string();
494        market.symbol = "BTC/USDT:USDT-250328-100000-C".to_string();
495        market.market_type = MarketType::Option;
496
497        let url = binance.rest_endpoint(&market, EndpointType::Private);
498        assert!(url.contains("eapi.binance.com"));
499    }
500
501    // ==================== WebSocket Endpoint Tests ====================
502
503    #[test]
504    fn test_ws_endpoint_spot() {
505        let binance = create_test_binance();
506        let market = Market::new_spot(
507            "BTCUSDT".to_string(),
508            "BTC/USDT".to_string(),
509            "BTC".to_string(),
510            "USDT".to_string(),
511        );
512
513        let url = binance.ws_endpoint(&market);
514        assert!(url.contains("stream.binance.com"));
515    }
516
517    #[test]
518    fn test_ws_endpoint_linear_swap() {
519        let binance = create_test_binance();
520        let market = Market::new_swap(
521            "BTCUSDT".to_string(),
522            "BTC/USDT:USDT".to_string(),
523            "BTC".to_string(),
524            "USDT".to_string(),
525            "USDT".to_string(),
526            dec!(1.0),
527        );
528
529        let url = binance.ws_endpoint(&market);
530        assert!(url.contains("fstream.binance.com"));
531    }
532
533    #[test]
534    fn test_ws_endpoint_inverse_swap() {
535        let binance = create_test_binance();
536        let mut market = Market::new_swap(
537            "BTCUSD_PERP".to_string(),
538            "BTC/USD:BTC".to_string(),
539            "BTC".to_string(),
540            "USD".to_string(),
541            "BTC".to_string(),
542            dec!(100.0),
543        );
544        market.linear = Some(false);
545        market.inverse = Some(true);
546
547        let url = binance.ws_endpoint(&market);
548        assert!(url.contains("dstream.binance.com"));
549    }
550
551    #[test]
552    fn test_ws_endpoint_option() {
553        let binance = create_test_binance();
554        let mut market = Market::default();
555        market.market_type = MarketType::Option;
556
557        let url = binance.ws_endpoint(&market);
558        assert!(url.contains("nbstream.binance.com"));
559    }
560
561    // ==================== Default Endpoint Tests ====================
562
563    #[test]
564    fn test_default_rest_endpoint_spot() {
565        let binance = create_test_binance();
566
567        let url = binance.default_rest_endpoint(EndpointType::Public);
568        assert!(url.contains("api.binance.com"));
569    }
570
571    #[test]
572    fn test_default_ws_endpoint_spot() {
573        let binance = create_test_binance();
574
575        let url = binance.default_ws_endpoint();
576        assert!(url.contains("stream.binance.com"));
577    }
578
579    // ==================== SAPI and PAPI Tests ====================
580
581    #[test]
582    fn test_sapi_endpoint() {
583        let binance = create_test_binance();
584
585        let url = binance.sapi_endpoint();
586        assert!(url.contains("sapi"));
587        assert!(url.contains("api.binance.com"));
588    }
589
590    #[test]
591    fn test_papi_endpoint() {
592        let binance = create_test_binance();
593
594        let url = binance.papi_endpoint();
595        assert!(url.contains("papi"));
596    }
597
598    // ==================== Sandbox Mode Tests ====================
599
600    #[test]
601    fn test_sandbox_rest_endpoint_spot() {
602        let binance = create_sandbox_binance();
603        let market = Market::new_spot(
604            "BTCUSDT".to_string(),
605            "BTC/USDT".to_string(),
606            "BTC".to_string(),
607            "USDT".to_string(),
608        );
609
610        let url = binance.rest_endpoint(&market, EndpointType::Public);
611        assert!(url.contains("testnet"));
612    }
613
614    #[test]
615    fn test_sandbox_ws_endpoint_spot() {
616        let binance = create_sandbox_binance();
617        let market = Market::new_spot(
618            "BTCUSDT".to_string(),
619            "BTC/USDT".to_string(),
620            "BTC".to_string(),
621            "USDT".to_string(),
622        );
623
624        let url = binance.ws_endpoint(&market);
625        assert!(url.contains("testnet"));
626    }
627
628    #[test]
629    fn test_sandbox_rest_endpoint_linear_swap() {
630        let binance = create_sandbox_binance();
631        let market = Market::new_swap(
632            "BTCUSDT".to_string(),
633            "BTC/USDT:USDT".to_string(),
634            "BTC".to_string(),
635            "USDT".to_string(),
636            "USDT".to_string(),
637            dec!(1.0),
638        );
639
640        let url = binance.rest_endpoint(&market, EndpointType::Public);
641        assert!(url.contains("testnet"));
642    }
643
644    // ==================== Edge Case Tests ====================
645
646    #[test]
647    fn test_swap_defaults_to_linear_when_not_specified() {
648        let binance = create_test_binance();
649        let mut market = Market::default();
650        market.market_type = MarketType::Swap;
651        // linear and inverse are None
652
653        let url = binance.rest_endpoint(&market, EndpointType::Public);
654        // Should default to linear (fapi)
655        assert!(url.contains("fapi.binance.com"));
656    }
657
658    #[test]
659    fn test_futures_defaults_to_linear_when_not_specified() {
660        let binance = create_test_binance();
661        let mut market = Market::default();
662        market.market_type = MarketType::Futures;
663        // linear and inverse are None
664
665        let url = binance.rest_endpoint(&market, EndpointType::Public);
666        // Should default to linear (fapi)
667        assert!(url.contains("fapi.binance.com"));
668    }
669}