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 market = Market {
481 id: "BTC-250328-100000-C".to_string(),
482 symbol: "BTC/USDT:USDT-250328-100000-C".to_string(),
483 market_type: MarketType::Option,
484 ..Default::default()
485 };
486
487 let url = binance.rest_endpoint(&market, EndpointType::Public);
488 assert!(url.contains("eapi.binance.com"));
489 }
490
491 #[test]
492 fn test_rest_endpoint_option_private() {
493 let binance = create_test_binance();
494 let market = Market {
495 id: "BTC-250328-100000-C".to_string(),
496 symbol: "BTC/USDT:USDT-250328-100000-C".to_string(),
497 market_type: MarketType::Option,
498 ..Default::default()
499 };
500
501 let url = binance.rest_endpoint(&market, EndpointType::Private);
502 assert!(url.contains("eapi.binance.com"));
503 }
504
505 // ==================== WebSocket Endpoint Tests ====================
506
507 #[test]
508 fn test_ws_endpoint_spot() {
509 let binance = create_test_binance();
510 let market = Market::new_spot(
511 "BTCUSDT".to_string(),
512 "BTC/USDT".to_string(),
513 "BTC".to_string(),
514 "USDT".to_string(),
515 );
516
517 let url = binance.ws_endpoint(&market);
518 assert!(url.contains("stream.binance.com"));
519 }
520
521 #[test]
522 fn test_ws_endpoint_linear_swap() {
523 let binance = create_test_binance();
524 let market = Market::new_swap(
525 "BTCUSDT".to_string(),
526 "BTC/USDT:USDT".to_string(),
527 "BTC".to_string(),
528 "USDT".to_string(),
529 "USDT".to_string(),
530 dec!(1.0),
531 );
532
533 let url = binance.ws_endpoint(&market);
534 assert!(url.contains("fstream.binance.com"));
535 }
536
537 #[test]
538 fn test_ws_endpoint_inverse_swap() {
539 let binance = create_test_binance();
540 let mut market = Market::new_swap(
541 "BTCUSD_PERP".to_string(),
542 "BTC/USD:BTC".to_string(),
543 "BTC".to_string(),
544 "USD".to_string(),
545 "BTC".to_string(),
546 dec!(100.0),
547 );
548 market.linear = Some(false);
549 market.inverse = Some(true);
550
551 let url = binance.ws_endpoint(&market);
552 assert!(url.contains("dstream.binance.com"));
553 }
554
555 #[test]
556 fn test_ws_endpoint_option() {
557 let binance = create_test_binance();
558 let mut market = Market::default();
559 market.market_type = MarketType::Option;
560
561 let url = binance.ws_endpoint(&market);
562 assert!(url.contains("nbstream.binance.com"));
563 }
564
565 // ==================== Default Endpoint Tests ====================
566
567 #[test]
568 fn test_default_rest_endpoint_spot() {
569 let binance = create_test_binance();
570
571 let url = binance.default_rest_endpoint(EndpointType::Public);
572 assert!(url.contains("api.binance.com"));
573 }
574
575 #[test]
576 fn test_default_ws_endpoint_spot() {
577 let binance = create_test_binance();
578
579 let url = binance.default_ws_endpoint();
580 assert!(url.contains("stream.binance.com"));
581 }
582
583 // ==================== SAPI and PAPI Tests ====================
584
585 #[test]
586 fn test_sapi_endpoint() {
587 let binance = create_test_binance();
588
589 let url = binance.sapi_endpoint();
590 assert!(url.contains("sapi"));
591 assert!(url.contains("api.binance.com"));
592 }
593
594 #[test]
595 fn test_papi_endpoint() {
596 let binance = create_test_binance();
597
598 let url = binance.papi_endpoint();
599 assert!(url.contains("papi"));
600 }
601
602 // ==================== Sandbox Mode Tests ====================
603
604 #[test]
605 fn test_sandbox_rest_endpoint_spot() {
606 let binance = create_sandbox_binance();
607 let market = Market::new_spot(
608 "BTCUSDT".to_string(),
609 "BTC/USDT".to_string(),
610 "BTC".to_string(),
611 "USDT".to_string(),
612 );
613
614 let url = binance.rest_endpoint(&market, EndpointType::Public);
615 assert!(url.contains("testnet"));
616 }
617
618 #[test]
619 fn test_sandbox_ws_endpoint_spot() {
620 let binance = create_sandbox_binance();
621 let market = Market::new_spot(
622 "BTCUSDT".to_string(),
623 "BTC/USDT".to_string(),
624 "BTC".to_string(),
625 "USDT".to_string(),
626 );
627
628 let url = binance.ws_endpoint(&market);
629 assert!(url.contains("testnet"));
630 }
631
632 #[test]
633 fn test_sandbox_rest_endpoint_linear_swap() {
634 let binance = create_sandbox_binance();
635 let market = Market::new_swap(
636 "BTCUSDT".to_string(),
637 "BTC/USDT:USDT".to_string(),
638 "BTC".to_string(),
639 "USDT".to_string(),
640 "USDT".to_string(),
641 dec!(1.0),
642 );
643
644 let url = binance.rest_endpoint(&market, EndpointType::Public);
645 assert!(url.contains("testnet"));
646 }
647
648 // ==================== Edge Case Tests ====================
649
650 #[test]
651 fn test_swap_defaults_to_linear_when_not_specified() {
652 let binance = create_test_binance();
653 let market = Market {
654 market_type: MarketType::Swap,
655 ..Default::default()
656 };
657 // linear and inverse are None
658
659 let url = binance.rest_endpoint(&market, EndpointType::Public);
660 // Should default to linear (fapi)
661 assert!(url.contains("fapi.binance.com"));
662 }
663
664 #[test]
665 fn test_futures_defaults_to_linear_when_not_specified() {
666 let binance = create_test_binance();
667 let market = Market {
668 market_type: MarketType::Futures,
669 ..Default::default()
670 };
671 // linear and inverse are None
672
673 let url = binance.rest_endpoint(&market, EndpointType::Public);
674 // Should default to linear (fapi)
675 assert!(url.contains("fapi.binance.com"));
676 }
677}