ccxt_exchanges/binance/rest/futures.rs
1//! Binance perpetual futures (FAPI) operations.
2//!
3//! This module contains all FAPI (USDT-margined perpetual futures) specific methods
4//! including position management, leverage, funding rates, and position mode.
5
6use super::super::{Binance, parser, signed_request::HttpMethod};
7use ccxt_core::{
8 Error, ParseError, Result,
9 types::{FeeFundingRate, FeeFundingRateHistory, LeverageTier, MarketType, Position},
10};
11use reqwest::header::HeaderMap;
12use rust_decimal::Decimal;
13use serde_json::Value;
14use std::collections::{BTreeMap, HashMap};
15use tracing::warn;
16
17impl Binance {
18 // ==================== Position Methods ====================
19
20 /// Fetch a single position for a trading pair.
21 ///
22 /// # Arguments
23 ///
24 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT:USDT").
25 /// * `params` - Optional additional parameters.
26 ///
27 /// # Returns
28 ///
29 /// Returns a [`Position`] structure for the specified symbol.
30 ///
31 /// # Errors
32 ///
33 /// Returns an error if authentication fails, the market is not found,
34 /// or the API request fails.
35 ///
36 /// # Example
37 ///
38 /// ```no_run
39 /// # use ccxt_exchanges::binance::Binance;
40 /// # use ccxt_core::ExchangeConfig;
41 /// # async fn example() -> ccxt_core::Result<()> {
42 /// let mut config = ExchangeConfig::default();
43 /// config.api_key = Some("your_api_key".to_string());
44 /// config.secret = Some("your_secret".to_string());
45 /// let binance = Binance::new_swap(config)?;
46 /// let position = binance.fetch_position("BTC/USDT:USDT", None).await?;
47 /// println!("Position: {:?}", position);
48 /// # Ok(())
49 /// # }
50 /// ```
51 pub async fn fetch_position(&self, symbol: &str, params: Option<Value>) -> Result<Position> {
52 let market = self.base().market(symbol).await?;
53
54 // Determine API endpoint based on market type (linear vs inverse)
55 let url = if market.linear.unwrap_or(true) {
56 format!("{}/positionRisk", self.urls().fapi_private)
57 } else {
58 format!("{}/positionRisk", self.urls().dapi_private)
59 };
60
61 let data = self
62 .signed_request(url)
63 .param("symbol", &market.id)
64 .merge_json_params(params)
65 .execute()
66 .await?;
67
68 // API returns array, find matching symbol
69 let positions_array = data.as_array().ok_or_else(|| {
70 Error::from(ParseError::invalid_format(
71 "data",
72 "Expected array of positions",
73 ))
74 })?;
75
76 for position_data in positions_array {
77 if let Some(pos_symbol) = position_data["symbol"].as_str() {
78 if pos_symbol == market.id {
79 return parser::parse_position(position_data, Some(&market));
80 }
81 }
82 }
83
84 Err(Error::from(ParseError::missing_field_owned(format!(
85 "Position not found for symbol: {}",
86 symbol
87 ))))
88 }
89
90 /// Fetch all positions.
91 ///
92 /// # Arguments
93 ///
94 /// * `symbols` - Optional vector of trading pair symbols.
95 /// * `params` - Optional parameters.
96 ///
97 /// # Returns
98 ///
99 /// Returns a vector of [`Position`] structures.
100 ///
101 /// # Errors
102 ///
103 /// Returns an error if authentication fails or the API request fails.
104 ///
105 /// # Example
106 ///
107 /// ```no_run
108 /// # use ccxt_exchanges::binance::Binance;
109 /// # use ccxt_core::ExchangeConfig;
110 /// # async fn example() -> ccxt_core::Result<()> {
111 /// let mut config = ExchangeConfig::default();
112 /// config.api_key = Some("your_api_key".to_string());
113 /// config.secret = Some("your_secret".to_string());
114 /// let binance = Binance::new_swap(config)?;
115 /// let positions = binance.fetch_positions(None, None).await?;
116 /// # Ok(())
117 /// # }
118 /// ```
119 pub async fn fetch_positions(
120 &self,
121 symbols: Option<Vec<String>>,
122 params: Option<Value>,
123 ) -> Result<Vec<Position>> {
124 // Default to USDT-M futures endpoint
125 let use_coin_m = params
126 .as_ref()
127 .and_then(|p| p.get("type"))
128 .and_then(|v| v.as_str())
129 .map(|t| t == "delivery" || t == "coin_m")
130 .unwrap_or(false);
131
132 let url = if use_coin_m {
133 format!("{}/positionRisk", self.urls().dapi_private)
134 } else {
135 format!("{}/positionRisk", self.urls().fapi_private)
136 };
137
138 let data = self
139 .signed_request(url)
140 .merge_json_params(params)
141 .execute()
142 .await?;
143
144 let positions_array = data.as_array().ok_or_else(|| {
145 Error::from(ParseError::invalid_format(
146 "data",
147 "Expected array of positions",
148 ))
149 })?;
150
151 // Clone markets_by_id map once before the loop to avoid lock contention
152 let markets_by_id = {
153 let cache = self.base().market_cache.read().await;
154 cache.markets_by_id.clone()
155 };
156
157 let mut positions = Vec::new();
158 for position_data in positions_array {
159 if let Some(binance_symbol) = position_data["symbol"].as_str() {
160 if let Some(market) = markets_by_id.get(binance_symbol) {
161 match parser::parse_position(position_data, Some(market)) {
162 Ok(position) => {
163 // Only return positions with contracts > 0
164 if position.contracts.unwrap_or(0.0) > 0.0 {
165 // If symbols specified, only return matching ones
166 if let Some(ref syms) = symbols {
167 if syms.contains(&position.symbol) {
168 positions.push(position);
169 }
170 } else {
171 positions.push(position);
172 }
173 }
174 }
175 Err(e) => {
176 warn!(
177 error = %e,
178 symbol = %binance_symbol,
179 "Failed to parse position"
180 );
181 }
182 }
183 }
184 }
185 }
186
187 Ok(positions)
188 }
189
190 /// Fetch position risk information.
191 ///
192 /// This is an alias for [`fetch_positions`](Self::fetch_positions) provided for CCXT naming consistency.
193 ///
194 /// # Arguments
195 ///
196 /// * `symbols` - Optional list of trading pair symbols.
197 /// * `params` - Optional additional parameters.
198 ///
199 /// # Returns
200 ///
201 /// Returns a vector of position risk information as [`Position`] structures.
202 ///
203 /// # Errors
204 ///
205 /// Returns an error if authentication fails or the API request fails.
206 pub async fn fetch_positions_risk(
207 &self,
208 symbols: Option<Vec<String>>,
209 params: Option<Value>,
210 ) -> Result<Vec<Position>> {
211 self.fetch_positions(symbols, params).await
212 }
213
214 /// Fetch position risk information (raw JSON).
215 ///
216 /// Retrieves risk information for all futures positions, including unrealized PnL,
217 /// liquidation price, leverage, etc.
218 ///
219 /// # Arguments
220 ///
221 /// * `symbol` - Optional trading pair symbol. `None` returns all positions.
222 /// * `params` - Optional parameters.
223 ///
224 /// # Returns
225 ///
226 /// Returns position risk information as raw JSON.
227 ///
228 /// # Errors
229 ///
230 /// Returns an error if authentication fails or the API request fails.
231 pub async fn fetch_position_risk(
232 &self,
233 symbol: Option<&str>,
234 params: Option<Value>,
235 ) -> Result<Value> {
236 let market_id = if let Some(sym) = symbol {
237 let market = self.base().market(sym).await?;
238 Some(market.id.clone())
239 } else {
240 None
241 };
242
243 let url = format!("{}/positionRisk", self.urls().fapi_private);
244
245 self.signed_request(url)
246 .optional_param("symbol", market_id)
247 .merge_json_params(params)
248 .execute()
249 .await
250 }
251
252 // ==================== Leverage Methods ====================
253
254 /// Fetch leverage settings for multiple trading pairs.
255 ///
256 /// # Arguments
257 ///
258 /// * `symbols` - Optional list of trading pairs. `None` queries all pairs.
259 /// * `params` - Optional parameters:
260 /// - `portfolioMargin`: Whether to use portfolio margin account.
261 ///
262 /// # Returns
263 ///
264 /// Returns a `HashMap` of leverage information keyed by trading pair symbol.
265 ///
266 /// # Errors
267 ///
268 /// Returns an error if authentication fails or the API request fails.
269 ///
270 /// # Examples
271 ///
272 /// ```no_run
273 /// # use ccxt_exchanges::binance::Binance;
274 /// # use ccxt_core::ExchangeConfig;
275 /// # async fn example() -> ccxt_core::Result<()> {
276 /// # let exchange = Binance::new(ExchangeConfig::default())?;
277 /// // Query leverage settings for all trading pairs
278 /// let leverages = exchange.fetch_leverages(None, None).await?;
279 ///
280 /// // Query leverage settings for specific pairs
281 /// let symbols = vec!["BTC/USDT:USDT".to_string(), "ETH/USDT:USDT".to_string()];
282 /// let leverages = exchange.fetch_leverages(Some(symbols), None).await?;
283 /// # Ok(())
284 /// # }
285 /// ```
286 pub async fn fetch_leverages(
287 &self,
288 symbols: Option<Vec<String>>,
289 params: Option<Value>,
290 ) -> Result<BTreeMap<String, ccxt_core::types::Leverage>> {
291 self.load_markets(false).await?;
292
293 let mut params_map = if let Some(p) = params {
294 serde_json::from_value::<BTreeMap<String, String>>(p).unwrap_or_default()
295 } else {
296 BTreeMap::new()
297 };
298
299 let market_type = params_map
300 .remove("type")
301 .or_else(|| params_map.remove("marketType"))
302 .unwrap_or_else(|| "future".to_string());
303
304 let sub_type = params_map
305 .remove("subType")
306 .unwrap_or_else(|| "linear".to_string());
307
308 let is_portfolio_margin = params_map
309 .remove("portfolioMargin")
310 .and_then(|v| v.parse::<bool>().ok())
311 .unwrap_or(false);
312
313 let url = if market_type == "future" && sub_type == "linear" {
314 // USDT-M futures
315 if is_portfolio_margin {
316 format!(
317 "{}/account",
318 self.urls().fapi_private.replace("/fapi/v1", "/papi/v1/um")
319 )
320 } else {
321 format!("{}/symbolConfig", self.urls().fapi_private)
322 }
323 } else if market_type == "future" && sub_type == "inverse" {
324 // Coin-M futures
325 if is_portfolio_margin {
326 format!(
327 "{}/account",
328 self.urls().dapi_private.replace("/dapi/v1", "/papi/v1/cm")
329 )
330 } else {
331 format!("{}/account", self.urls().dapi_private)
332 }
333 } else {
334 return Err(Error::invalid_request(
335 "fetchLeverages() supports linear and inverse contracts only",
336 ));
337 };
338
339 let response = self
340 .signed_request(url)
341 .params(params_map)
342 .execute()
343 .await?;
344
345 let leverages_data = if let Some(positions) = response.get("positions") {
346 positions.as_array().cloned().unwrap_or_default()
347 } else if response.is_array() {
348 response.as_array().cloned().unwrap_or_default()
349 } else {
350 vec![]
351 };
352
353 let mut leverages = BTreeMap::new();
354
355 for item in leverages_data {
356 if let Ok(leverage) = parser::parse_leverage(&item, None) {
357 // If symbols specified, only keep matching ones
358 if let Some(ref filter_symbols) = symbols {
359 if filter_symbols.contains(&leverage.symbol) {
360 leverages.insert(leverage.symbol.clone(), leverage);
361 }
362 } else {
363 leverages.insert(leverage.symbol.clone(), leverage);
364 }
365 }
366 }
367
368 Ok(leverages)
369 }
370
371 /// Fetch leverage settings for a single trading pair.
372 ///
373 /// # Arguments
374 ///
375 /// * `symbol` - Trading pair symbol.
376 /// * `params` - Optional additional parameters.
377 ///
378 /// # Returns
379 ///
380 /// Returns leverage information for the specified trading pair.
381 ///
382 /// # Errors
383 ///
384 /// Returns an error if the symbol is not found or the API request fails.
385 ///
386 /// # Examples
387 ///
388 /// ```no_run
389 /// # use ccxt_exchanges::binance::Binance;
390 /// # use ccxt_core::ExchangeConfig;
391 /// # async fn example() -> ccxt_core::Result<()> {
392 /// # let exchange = Binance::new(ExchangeConfig::default())?;
393 /// // Query leverage settings for BTC/USDT futures
394 /// let leverage = exchange.fetch_leverage("BTC/USDT:USDT", None).await?;
395 /// println!("Long leverage: {:?}", leverage.long_leverage);
396 /// println!("Short leverage: {:?}", leverage.short_leverage);
397 /// println!("Margin mode: {:?}", leverage.margin_mode);
398 /// # Ok(())
399 /// # }
400 /// ```
401 pub async fn fetch_leverage(
402 &self,
403 symbol: &str,
404 params: Option<Value>,
405 ) -> Result<ccxt_core::types::Leverage> {
406 let symbols = Some(vec![symbol.to_string()]);
407 let leverages = self.fetch_leverages(symbols, params).await?;
408
409 leverages.get(symbol).cloned().ok_or_else(|| {
410 Error::exchange("404", format!("Leverage not found for symbol: {}", symbol))
411 })
412 }
413
414 /// Set leverage multiplier for a trading pair.
415 ///
416 /// # Arguments
417 ///
418 /// * `symbol` - Trading pair symbol.
419 /// * `leverage` - Leverage multiplier (1-125).
420 /// * `params` - Optional additional parameters.
421 ///
422 /// # Returns
423 ///
424 /// Returns the operation result as a `HashMap`.
425 ///
426 /// # Errors
427 ///
428 /// Returns an error if:
429 /// - Authentication credentials are missing
430 /// - Leverage is outside valid range (1-125)
431 /// - The market is not a futures/swap market
432 /// - The API request fails
433 ///
434 /// # Examples
435 ///
436 /// ```no_run
437 /// # use ccxt_exchanges::binance::Binance;
438 /// # use ccxt_core::ExchangeConfig;
439 /// # async fn example() -> ccxt_core::Result<()> {
440 /// let mut config = ExchangeConfig::default();
441 /// config.api_key = Some("your_api_key".to_string());
442 /// config.secret = Some("your_secret".to_string());
443 /// let binance = Binance::new_swap(config)?;
444 /// let result = binance.set_leverage("BTC/USDT:USDT", 10, None).await?;
445 /// # Ok(())
446 /// # }
447 /// ```
448 pub async fn set_leverage(
449 &self,
450 symbol: &str,
451 leverage: i64,
452 params: Option<HashMap<String, String>>,
453 ) -> Result<HashMap<String, Value>> {
454 if leverage < 1 || leverage > 125 {
455 return Err(Error::invalid_request(
456 "Leverage must be between 1 and 125".to_string(),
457 ));
458 }
459
460 self.load_markets(false).await?;
461 let market = self.base().market(symbol).await?;
462
463 if market.market_type != MarketType::Futures && market.market_type != MarketType::Swap {
464 return Err(Error::invalid_request(
465 "set_leverage() supports futures and swap markets only".to_string(),
466 ));
467 }
468
469 // Select API endpoint based on market type
470 let url = if market.linear.unwrap_or(true) {
471 format!("{}/leverage", self.urls().fapi_private)
472 } else if market.inverse.unwrap_or(false) {
473 format!("{}/leverage", self.urls().dapi_private)
474 } else {
475 return Err(Error::invalid_request(
476 "Unknown futures market type".to_string(),
477 ));
478 };
479
480 let mut builder = self
481 .signed_request(url)
482 .method(HttpMethod::Post)
483 .param("symbol", &market.id)
484 .param("leverage", leverage);
485
486 if let Some(p) = params {
487 let params_map: BTreeMap<String, String> = p.into_iter().collect();
488 builder = builder.params(params_map);
489 }
490
491 let response = builder.execute().await?;
492
493 let result: HashMap<String, Value> = serde_json::from_value(response).map_err(|e| {
494 Error::from(ParseError::invalid_format(
495 "data",
496 format!("Failed to parse response: {}", e),
497 ))
498 })?;
499
500 Ok(result)
501 }
502
503 /// Fetch leverage bracket information.
504 ///
505 /// Retrieves leverage bracket information for specified or all trading pairs,
506 /// showing maximum leverage for different notional value tiers.
507 ///
508 /// # Arguments
509 ///
510 /// * `symbol` - Optional trading pair symbol. `None` returns all pairs.
511 /// * `params` - Optional parameters.
512 ///
513 /// # Returns
514 ///
515 /// Returns leverage bracket information including maximum notional value and
516 /// corresponding maximum leverage for each tier.
517 ///
518 /// # Errors
519 ///
520 /// Returns an error if authentication fails or the API request fails.
521 pub async fn fetch_leverage_bracket(
522 &self,
523 symbol: Option<&str>,
524 params: Option<Value>,
525 ) -> Result<Value> {
526 let market_id = if let Some(sym) = symbol {
527 let market = self.base().market(sym).await?;
528 Some(market.id.clone())
529 } else {
530 None
531 };
532
533 let url = format!("{}/leverageBracket", self.urls().fapi_private);
534
535 self.signed_request(url)
536 .optional_param("symbol", market_id)
537 .merge_json_params(params)
538 .execute()
539 .await
540 }
541
542 /// Fetch leverage tier information for trading pairs.
543 ///
544 /// # Arguments
545 ///
546 /// * `symbols` - Optional list of trading pairs. `None` fetches all pairs.
547 /// * `params` - Optional parameters.
548 ///
549 /// # Returns
550 ///
551 /// Returns a `HashMap` of leverage tiers keyed by symbol.
552 ///
553 /// # Errors
554 ///
555 /// Returns an error if authentication fails or the API request fails.
556 ///
557 /// # Example
558 ///
559 /// ```no_run
560 /// # use ccxt_exchanges::binance::Binance;
561 /// # use ccxt_core::ExchangeConfig;
562 /// # async fn example() -> ccxt_core::Result<()> {
563 /// let mut config = ExchangeConfig::default();
564 /// config.api_key = Some("your_api_key".to_string());
565 /// config.secret = Some("your_secret".to_string());
566 /// let binance = Binance::new_swap(config)?;
567 /// let tiers = binance.fetch_leverage_tiers(None, None).await?;
568 /// # Ok(())
569 /// # }
570 /// ```
571 pub async fn fetch_leverage_tiers(
572 &self,
573 symbols: Option<Vec<String>>,
574 params: Option<HashMap<String, String>>,
575 ) -> Result<BTreeMap<String, Vec<LeverageTier>>> {
576 self.load_markets(false).await?;
577
578 let is_portfolio_margin = params
579 .as_ref()
580 .and_then(|p| p.get("portfolioMargin"))
581 .map(|v| v == "true")
582 .unwrap_or(false);
583
584 let (market, market_id) = if let Some(syms) = &symbols {
585 if let Some(first_symbol) = syms.first() {
586 let m = self.base().market(first_symbol).await?;
587 let id = m.id.clone();
588 (Some(m), Some(id))
589 } else {
590 (None, None)
591 }
592 } else {
593 (None, None)
594 };
595
596 // Select API endpoint based on market type and Portfolio Margin mode
597 let url = if let Some(ref m) = market {
598 if is_portfolio_margin {
599 // Portfolio Margin mode uses papi endpoints
600 if m.is_linear() {
601 format!("{}/um/leverageBracket", self.urls().papi)
602 } else {
603 format!("{}/cm/leverageBracket", self.urls().papi)
604 }
605 } else {
606 // Standard mode
607 if m.is_linear() {
608 format!("{}/leverageBracket", self.urls().fapi_private)
609 } else {
610 format!("{}/v2/leverageBracket", self.urls().dapi_private)
611 }
612 }
613 } else {
614 format!("{}/leverageBracket", self.urls().fapi_private)
615 };
616
617 // Build request params, filtering out portfolioMargin
618 let filtered_params: Option<BTreeMap<String, String>> = params.map(|p| {
619 p.into_iter()
620 .filter(|(k, _)| k != "portfolioMargin")
621 .collect()
622 });
623
624 let response = self
625 .signed_request(url)
626 .optional_param("symbol", market_id)
627 .params(filtered_params.unwrap_or_default())
628 .execute()
629 .await?;
630
631 let mut tiers_map: BTreeMap<String, Vec<LeverageTier>> = BTreeMap::new();
632
633 // Response can be array of symbols with brackets
634 if let Some(symbols_array) = response.as_array() {
635 for symbol_data in symbols_array {
636 if let (Some(symbol_id), Some(brackets)) = (
637 symbol_data["symbol"].as_str(),
638 symbol_data["brackets"].as_array(),
639 ) {
640 // Try to get the market from cache
641 if let Ok(market) = self.base().market_by_id(symbol_id).await {
642 let mut tier_list = Vec::new();
643 for bracket in brackets {
644 if let Ok(tier) = parser::parse_leverage_tier(bracket, &market) {
645 tier_list.push(tier);
646 }
647 }
648
649 if !tier_list.is_empty() {
650 // Filter by requested symbols if provided
651 if let Some(ref filter_symbols) = symbols {
652 if filter_symbols.contains(&market.symbol) {
653 tiers_map.insert(market.symbol.clone(), tier_list);
654 }
655 } else {
656 tiers_map.insert(market.symbol.clone(), tier_list);
657 }
658 }
659 }
660 }
661 }
662 }
663
664 Ok(tiers_map)
665 }
666
667 // ==================== Margin Mode Methods ====================
668
669 /// Set margin mode for a trading pair.
670 ///
671 /// # Arguments
672 ///
673 /// * `symbol` - Trading pair symbol.
674 /// * `margin_mode` - Margin mode (`isolated` or `cross`).
675 /// * `params` - Optional additional parameters.
676 ///
677 /// # Returns
678 ///
679 /// Returns the operation result as a `HashMap`.
680 ///
681 /// # Errors
682 ///
683 /// Returns an error if:
684 /// - Authentication credentials are missing
685 /// - Margin mode is invalid (must be `isolated` or `cross`)
686 /// - The market is not a futures/swap market
687 /// - The API request fails
688 ///
689 /// # Examples
690 ///
691 /// ```no_run
692 /// # use ccxt_exchanges::binance::Binance;
693 /// # use ccxt_core::ExchangeConfig;
694 /// # async fn example() -> ccxt_core::Result<()> {
695 /// let mut config = ExchangeConfig::default();
696 /// config.api_key = Some("your_api_key".to_string());
697 /// config.secret = Some("your_secret".to_string());
698 /// let binance = Binance::new_swap(config)?;
699 /// let result = binance.set_margin_mode("BTC/USDT:USDT", "isolated", None).await?;
700 /// # Ok(())
701 /// # }
702 /// ```
703 pub async fn set_margin_mode(
704 &self,
705 symbol: &str,
706 margin_mode: &str,
707 params: Option<HashMap<String, String>>,
708 ) -> Result<HashMap<String, Value>> {
709 let margin_type = match margin_mode.to_uppercase().as_str() {
710 "ISOLATED" | "ISOLATED_MARGIN" => "ISOLATED",
711 "CROSS" | "CROSSED" | "CROSS_MARGIN" => "CROSSED",
712 _ => {
713 return Err(Error::invalid_request(format!(
714 "Invalid margin mode: {}. Must be 'isolated' or 'cross'",
715 margin_mode
716 )));
717 }
718 };
719
720 self.load_markets(false).await?;
721 let market = self.base().market(symbol).await?;
722
723 if market.market_type != MarketType::Futures && market.market_type != MarketType::Swap {
724 return Err(Error::invalid_request(
725 "set_margin_mode() supports futures and swap markets only".to_string(),
726 ));
727 }
728
729 // Select API endpoint based on market type
730 let url = if market.linear.unwrap_or(true) {
731 format!("{}/marginType", self.urls().fapi_private)
732 } else if market.inverse.unwrap_or(false) {
733 format!("{}/marginType", self.urls().dapi_private)
734 } else {
735 return Err(Error::invalid_request(
736 "Unknown futures market type".to_string(),
737 ));
738 };
739
740 let mut builder = self
741 .signed_request(url)
742 .method(HttpMethod::Post)
743 .param("symbol", &market.id)
744 .param("marginType", margin_type);
745
746 if let Some(p) = params {
747 let params_map: BTreeMap<String, String> = p.into_iter().collect();
748 builder = builder.params(params_map);
749 }
750
751 let response = builder.execute().await?;
752
753 let result: HashMap<String, Value> = serde_json::from_value(response).map_err(|e| {
754 Error::from(ParseError::invalid_format(
755 "data",
756 format!("Failed to parse response: {}", e),
757 ))
758 })?;
759
760 Ok(result)
761 }
762
763 // ==================== Position Mode Methods ====================
764
765 /// Set position mode (hedge mode or one-way mode).
766 ///
767 /// This method supports both FAPI (USDT-margined) and DAPI (coin-margined) futures.
768 /// By default, it uses the FAPI endpoint. To set position mode for coin-margined
769 /// futures, pass `type: "delivery"` or `type: "coin_m"` in the params.
770 ///
771 /// # Arguments
772 ///
773 /// * `dual_side` - `true` for hedge mode (dual-side position), `false` for one-way mode.
774 /// * `params` - Optional parameters:
775 /// - `type`: Set to `"delivery"` or `"coin_m"` to use DAPI endpoint for coin-margined futures.
776 /// Defaults to FAPI endpoint if not specified.
777 ///
778 /// # Returns
779 ///
780 /// Returns the API response.
781 ///
782 /// # Errors
783 ///
784 /// Returns an error if authentication fails or the API request fails.
785 ///
786 /// # Example
787 ///
788 /// ```no_run
789 /// # use ccxt_exchanges::binance::Binance;
790 /// # use ccxt_core::ExchangeConfig;
791 /// # use serde_json::json;
792 /// # async fn example() -> ccxt_core::Result<()> {
793 /// let binance = Binance::new_swap(ExchangeConfig::default())?;
794 ///
795 /// // Enable hedge mode for USDT-margined futures (FAPI)
796 /// let result = binance.set_position_mode(true, None).await?;
797 ///
798 /// // Switch back to one-way mode for USDT-margined futures
799 /// let result = binance.set_position_mode(false, None).await?;
800 ///
801 /// // Enable hedge mode for coin-margined futures (DAPI)
802 /// let params = json!({"type": "delivery"});
803 /// let result = binance.set_position_mode(true, Some(params)).await?;
804 ///
805 /// // Alternative: use "coin_m" type
806 /// let params = json!({"type": "coin_m"});
807 /// let result = binance.set_position_mode(false, Some(params)).await?;
808 /// # Ok(())
809 /// # }
810 /// ```
811 pub async fn set_position_mode(&self, dual_side: bool, params: Option<Value>) -> Result<Value> {
812 self.check_required_credentials()?;
813
814 // Check if we should use DAPI endpoint based on type parameter
815 let use_dapi = params
816 .as_ref()
817 .and_then(|p| p.get("type"))
818 .and_then(|v| v.as_str())
819 .map(|t| t == "delivery" || t == "coin_m")
820 .unwrap_or(false);
821
822 let mut request_params = BTreeMap::new();
823 request_params.insert("dualSidePosition".to_string(), dual_side.to_string());
824
825 if let Some(params) = params {
826 if let Some(obj) = params.as_object() {
827 for (key, value) in obj {
828 // Skip the "type" parameter as it's only used for routing
829 if key == "type" {
830 continue;
831 }
832 if let Some(v) = value.as_str() {
833 request_params.insert(key.clone(), v.to_string());
834 } else if let Some(v) = value.as_bool() {
835 request_params.insert(key.clone(), v.to_string());
836 } else if let Some(v) = value.as_i64() {
837 request_params.insert(key.clone(), v.to_string());
838 }
839 }
840 }
841 }
842
843 let timestamp = self.get_signing_timestamp().await?;
844 let auth = self.get_auth()?;
845 let signed_params =
846 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
847
848 // Route to DAPI or FAPI endpoint based on type parameter
849 let url = if use_dapi {
850 format!("{}/positionSide/dual", self.urls().dapi_private)
851 } else {
852 format!("{}/positionSide/dual", self.urls().fapi_private)
853 };
854
855 let mut headers = HeaderMap::new();
856 auth.add_auth_headers_reqwest(&mut headers);
857
858 let body = serde_json::to_value(&signed_params).map_err(|e| {
859 Error::from(ParseError::invalid_format(
860 "data",
861 format!("Failed to serialize params: {}", e),
862 ))
863 })?;
864
865 let data = self
866 .base()
867 .http_client
868 .post(&url, Some(headers), Some(body))
869 .await?;
870
871 Ok(data)
872 }
873
874 /// Fetch current position mode.
875 ///
876 /// This method supports both FAPI (USDT-margined) and DAPI (coin-margined) futures.
877 /// By default, it uses the FAPI endpoint. To fetch position mode for coin-margined
878 /// futures, pass `type: "delivery"` or `type: "coin_m"` in the params.
879 ///
880 /// # Arguments
881 ///
882 /// * `params` - Optional parameters:
883 /// - `type`: Set to `"delivery"` or `"coin_m"` to use DAPI endpoint for coin-margined futures.
884 /// Defaults to FAPI endpoint if not specified.
885 ///
886 /// # Returns
887 ///
888 /// Returns the current position mode:
889 /// - `true`: Hedge mode (dual-side position).
890 /// - `false`: One-way mode.
891 ///
892 /// # Errors
893 ///
894 /// Returns an error if authentication fails or the API request fails.
895 ///
896 /// # Example
897 ///
898 /// ```no_run
899 /// # use ccxt_exchanges::binance::Binance;
900 /// # use ccxt_core::ExchangeConfig;
901 /// # use serde_json::json;
902 /// # async fn example() -> ccxt_core::Result<()> {
903 /// let binance = Binance::new_swap(ExchangeConfig::default())?;
904 ///
905 /// // Fetch position mode for USDT-margined futures (FAPI)
906 /// let dual_side = binance.fetch_position_mode(None).await?;
907 /// println!("FAPI Hedge mode enabled: {}", dual_side);
908 ///
909 /// // Fetch position mode for coin-margined futures (DAPI)
910 /// let params = json!({"type": "delivery"});
911 /// let dual_side = binance.fetch_position_mode(Some(params)).await?;
912 /// println!("DAPI Hedge mode enabled: {}", dual_side);
913 ///
914 /// // Alternative: use "coin_m" type
915 /// let params = json!({"type": "coin_m"});
916 /// let dual_side = binance.fetch_position_mode(Some(params)).await?;
917 /// # Ok(())
918 /// # }
919 /// ```
920 pub async fn fetch_position_mode(&self, params: Option<Value>) -> Result<bool> {
921 self.check_required_credentials()?;
922
923 // Check if we should use DAPI endpoint based on type parameter
924 let use_dapi = params
925 .as_ref()
926 .and_then(|p| p.get("type"))
927 .and_then(|v| v.as_str())
928 .map(|t| t == "delivery" || t == "coin_m")
929 .unwrap_or(false);
930
931 let mut request_params = BTreeMap::new();
932
933 if let Some(params) = params {
934 if let Some(obj) = params.as_object() {
935 for (key, value) in obj {
936 // Skip the "type" parameter as it's only used for routing
937 if key == "type" {
938 continue;
939 }
940 if let Some(v) = value.as_str() {
941 request_params.insert(key.clone(), v.to_string());
942 }
943 }
944 }
945 }
946
947 let timestamp = self.get_signing_timestamp().await?;
948 let auth = self.get_auth()?;
949 let signed_params =
950 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
951
952 let query_string = signed_params
953 .iter()
954 .map(|(k, v)| format!("{}={}", k, v))
955 .collect::<Vec<_>>()
956 .join("&");
957
958 // Route to DAPI or FAPI endpoint based on type parameter
959 let url = if use_dapi {
960 format!(
961 "{}/positionSide/dual?{}",
962 self.urls().dapi_private,
963 query_string
964 )
965 } else {
966 format!(
967 "{}/positionSide/dual?{}",
968 self.urls().fapi_private,
969 query_string
970 )
971 };
972
973 let mut headers = HeaderMap::new();
974 auth.add_auth_headers_reqwest(&mut headers);
975
976 let data = self.base().http_client.get(&url, Some(headers)).await?;
977
978 if let Some(dual_side) = data.get("dualSidePosition") {
979 if let Some(value) = dual_side.as_bool() {
980 return Ok(value);
981 }
982 if let Some(value_str) = dual_side.as_str() {
983 return Ok(value_str.to_lowercase() == "true");
984 }
985 }
986
987 Err(Error::from(ParseError::invalid_format(
988 "data",
989 "Failed to parse position mode response",
990 )))
991 }
992
993 /// Modify isolated position margin.
994 ///
995 /// # Arguments
996 ///
997 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
998 /// * `amount` - Adjustment amount (positive to add, negative to reduce).
999 /// * `params` - Optional parameters:
1000 /// - `type`: Operation type (1=add, 2=reduce). If provided, `amount` sign is ignored.
1001 /// - `positionSide`: Position side "LONG" | "SHORT" (required in hedge mode).
1002 ///
1003 /// # Returns
1004 ///
1005 /// Returns the adjustment result including the new margin amount.
1006 ///
1007 /// # Errors
1008 ///
1009 /// Returns an error if authentication fails or the API request fails.
1010 pub async fn modify_isolated_position_margin(
1011 &self,
1012 symbol: &str,
1013 amount: Decimal,
1014 params: Option<Value>,
1015 ) -> Result<Value> {
1016 self.check_required_credentials()?;
1017
1018 let market = self.base().market(symbol).await?;
1019 let mut request_params = BTreeMap::new();
1020 request_params.insert("symbol".to_string(), market.id.clone());
1021 request_params.insert("amount".to_string(), amount.abs().to_string());
1022 request_params.insert(
1023 "type".to_string(),
1024 if amount > Decimal::ZERO {
1025 "1".to_string()
1026 } else {
1027 "2".to_string()
1028 },
1029 );
1030
1031 if let Some(params) = params {
1032 if let Some(obj) = params.as_object() {
1033 for (key, value) in obj {
1034 if let Some(v) = value.as_str() {
1035 request_params.insert(key.clone(), v.to_string());
1036 }
1037 }
1038 }
1039 }
1040
1041 let timestamp = self.get_signing_timestamp().await?;
1042 let auth = self.get_auth()?;
1043 let signed_params =
1044 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
1045
1046 let url = if market.linear.unwrap_or(true) {
1047 format!("{}/positionMargin", self.urls().fapi_private)
1048 } else {
1049 format!("{}/positionMargin", self.urls().dapi_private)
1050 };
1051
1052 let mut headers = HeaderMap::new();
1053 auth.add_auth_headers_reqwest(&mut headers);
1054
1055 let body = serde_json::to_value(&signed_params).map_err(|e| {
1056 Error::from(ParseError::invalid_format(
1057 "data",
1058 format!("Failed to serialize params: {}", e),
1059 ))
1060 })?;
1061
1062 let data = self
1063 .base()
1064 .http_client
1065 .post(&url, Some(headers), Some(body))
1066 .await?;
1067
1068 Ok(data)
1069 }
1070
1071 // ==================== Funding Rate Methods ====================
1072
1073 /// Fetch current funding rate for a trading pair.
1074 ///
1075 /// # Arguments
1076 ///
1077 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT:USDT").
1078 /// * `params` - Optional parameters.
1079 ///
1080 /// # Returns
1081 ///
1082 /// Returns the current funding rate information.
1083 ///
1084 /// # Errors
1085 ///
1086 /// Returns an error if:
1087 /// - The market is not a futures or swap market
1088 /// - The API request fails
1089 ///
1090 /// # Example
1091 ///
1092 /// ```no_run
1093 /// # use ccxt_exchanges::binance::Binance;
1094 /// # use ccxt_core::ExchangeConfig;
1095 /// # async fn example() -> ccxt_core::Result<()> {
1096 /// let binance = Binance::new_swap(ExchangeConfig::default())?;
1097 /// let rate = binance.fetch_funding_rate("BTC/USDT:USDT", None).await?;
1098 /// println!("Funding rate: {:?}", rate.funding_rate);
1099 /// # Ok(())
1100 /// # }
1101 /// ```
1102 pub async fn fetch_funding_rate(
1103 &self,
1104 symbol: &str,
1105 params: Option<HashMap<String, String>>,
1106 ) -> Result<FeeFundingRate> {
1107 self.load_markets(false).await?;
1108 let market = self.base().market(symbol).await?;
1109
1110 if market.market_type != MarketType::Futures && market.market_type != MarketType::Swap {
1111 return Err(Error::invalid_request(
1112 "fetch_funding_rate() supports futures and swap markets only".to_string(),
1113 ));
1114 }
1115
1116 let mut request_params = BTreeMap::new();
1117 request_params.insert("symbol".to_string(), market.id.clone());
1118
1119 if let Some(p) = params {
1120 for (key, value) in p {
1121 request_params.insert(key, value);
1122 }
1123 }
1124
1125 let url = if market.linear.unwrap_or(true) {
1126 format!("{}/premiumIndex", self.urls().fapi_public)
1127 } else {
1128 format!("{}/premiumIndex", self.urls().dapi_public)
1129 };
1130
1131 let mut request_url = format!("{}?", url);
1132 for (key, value) in &request_params {
1133 request_url.push_str(&format!("{}={}&", key, value));
1134 }
1135
1136 let response = self.base().http_client.get(&request_url, None).await?;
1137
1138 // COIN-M futures API returns array format - extract first element
1139 let data = if !market.linear.unwrap_or(true) {
1140 response
1141 .as_array()
1142 .and_then(|arr| arr.first())
1143 .ok_or_else(|| {
1144 Error::from(ParseError::invalid_format(
1145 "data",
1146 "COIN-M funding rate response should be an array with at least one element",
1147 ))
1148 })?
1149 } else {
1150 &response
1151 };
1152
1153 parser::parse_funding_rate(data, Some(&market))
1154 }
1155
1156 /// Fetch current funding rates for multiple trading pairs.
1157 ///
1158 /// # Arguments
1159 ///
1160 /// * `symbols` - Optional list of trading pairs. `None` fetches all pairs.
1161 /// * `params` - Optional parameters.
1162 ///
1163 /// # Returns
1164 ///
1165 /// Returns a `HashMap` of funding rates keyed by symbol.
1166 ///
1167 /// # Errors
1168 ///
1169 /// Returns an error if the API request fails.
1170 ///
1171 /// # Example
1172 ///
1173 /// ```no_run
1174 /// # use ccxt_exchanges::binance::Binance;
1175 /// # use ccxt_core::ExchangeConfig;
1176 /// # async fn example() -> ccxt_core::Result<()> {
1177 /// let binance = Binance::new_swap(ExchangeConfig::default())?;
1178 /// let rates = binance.fetch_funding_rates(None, None).await?;
1179 /// println!("Found {} funding rates", rates.len());
1180 /// # Ok(())
1181 /// # }
1182 /// ```
1183 pub async fn fetch_funding_rates(
1184 &self,
1185 symbols: Option<Vec<String>>,
1186 params: Option<BTreeMap<String, String>>,
1187 ) -> Result<BTreeMap<String, FeeFundingRate>> {
1188 self.load_markets(false).await?;
1189
1190 let mut request_params = BTreeMap::new();
1191
1192 if let Some(p) = params {
1193 for (key, value) in p {
1194 request_params.insert(key, value);
1195 }
1196 }
1197
1198 let url = format!("{}/premiumIndex", self.urls().fapi_public);
1199
1200 let mut request_url = url.clone();
1201 if !request_params.is_empty() {
1202 request_url.push('?');
1203 for (key, value) in &request_params {
1204 request_url.push_str(&format!("{}={}&", key, value));
1205 }
1206 }
1207
1208 let response = self.base().http_client.get(&request_url, None).await?;
1209
1210 let mut rates = BTreeMap::new();
1211
1212 if let Some(rates_array) = response.as_array() {
1213 for rate_data in rates_array {
1214 if let Ok(symbol_id) = rate_data["symbol"]
1215 .as_str()
1216 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
1217 {
1218 if let Ok(market) = self.base().market_by_id(symbol_id).await {
1219 if let Some(ref syms) = symbols {
1220 if !syms.contains(&market.symbol) {
1221 continue;
1222 }
1223 }
1224
1225 if let Ok(rate) = parser::parse_funding_rate(rate_data, Some(&market)) {
1226 rates.insert(market.symbol.clone(), rate);
1227 }
1228 }
1229 }
1230 }
1231 } else {
1232 if let Ok(symbol_id) = response["symbol"]
1233 .as_str()
1234 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
1235 {
1236 if let Ok(market) = self.base().market_by_id(symbol_id).await {
1237 if let Ok(rate) = parser::parse_funding_rate(&response, Some(&market)) {
1238 rates.insert(market.symbol.clone(), rate);
1239 }
1240 }
1241 }
1242 }
1243
1244 Ok(rates)
1245 }
1246
1247 /// Fetch funding rate history for a trading pair.
1248 ///
1249 /// # Arguments
1250 ///
1251 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT:USDT").
1252 /// * `since` - Optional start timestamp in milliseconds.
1253 /// * `limit` - Optional record limit (default 100, max 1000).
1254 /// * `params` - Optional parameters.
1255 ///
1256 /// # Returns
1257 ///
1258 /// Returns a vector of historical funding rate records.
1259 ///
1260 /// # Errors
1261 ///
1262 /// Returns an error if:
1263 /// - The market is not a futures or swap market
1264 /// - The API request fails
1265 ///
1266 /// # Example
1267 ///
1268 /// ```no_run
1269 /// # use ccxt_exchanges::binance::Binance;
1270 /// # use ccxt_core::ExchangeConfig;
1271 /// # async fn example() -> ccxt_core::Result<()> {
1272 /// let binance = Binance::new_swap(ExchangeConfig::default())?;
1273 /// let history = binance.fetch_funding_rate_history("BTC/USDT:USDT", None, Some(10), None).await?;
1274 /// println!("Found {} records", history.len());
1275 /// # Ok(())
1276 /// # }
1277 /// ```
1278 pub async fn fetch_funding_rate_history(
1279 &self,
1280 symbol: &str,
1281 since: Option<i64>,
1282 limit: Option<u32>,
1283 params: Option<HashMap<String, String>>,
1284 ) -> Result<Vec<FeeFundingRateHistory>> {
1285 self.load_markets(false).await?;
1286 let market = self.base().market(symbol).await?;
1287
1288 if market.market_type != MarketType::Futures && market.market_type != MarketType::Swap {
1289 return Err(Error::invalid_request(
1290 "fetch_funding_rate_history() supports futures and swap markets only".to_string(),
1291 ));
1292 }
1293
1294 let mut request_params = BTreeMap::new();
1295 request_params.insert("symbol".to_string(), market.id.clone());
1296
1297 if let Some(s) = since {
1298 request_params.insert("startTime".to_string(), s.to_string());
1299 }
1300
1301 if let Some(l) = limit {
1302 request_params.insert("limit".to_string(), l.to_string());
1303 }
1304
1305 if let Some(p) = params {
1306 for (key, value) in p {
1307 request_params.insert(key, value);
1308 }
1309 }
1310
1311 let url = if market.linear.unwrap_or(true) {
1312 format!("{}/fundingRate", self.urls().fapi_public)
1313 } else {
1314 format!("{}/fundingRate", self.urls().dapi_public)
1315 };
1316
1317 let mut request_url = format!("{}?", url);
1318 for (key, value) in &request_params {
1319 request_url.push_str(&format!("{}={}&", key, value));
1320 }
1321
1322 let response = self.base().http_client.get(&request_url, None).await?;
1323
1324 let history_array = response.as_array().ok_or_else(|| {
1325 Error::from(ParseError::invalid_format(
1326 "data",
1327 "Expected array of funding rate history",
1328 ))
1329 })?;
1330
1331 let mut history = Vec::new();
1332 for history_data in history_array {
1333 match parser::parse_funding_rate_history(history_data, Some(&market)) {
1334 Ok(record) => history.push(record),
1335 Err(e) => {
1336 warn!(error = %e, "Failed to parse funding rate history");
1337 }
1338 }
1339 }
1340
1341 Ok(history)
1342 }
1343
1344 /// Fetch funding payment history for a trading pair.
1345 ///
1346 /// # Arguments
1347 ///
1348 /// * `symbol` - Optional trading pair symbol. `None` returns all.
1349 /// * `since` - Optional start timestamp in milliseconds.
1350 /// * `limit` - Optional record limit.
1351 /// * `params` - Optional parameters.
1352 ///
1353 /// # Returns
1354 ///
1355 /// Returns funding payment history as raw JSON.
1356 ///
1357 /// # Errors
1358 ///
1359 /// Returns an error if authentication fails or the API request fails.
1360 pub async fn fetch_funding_history(
1361 &self,
1362 symbol: Option<&str>,
1363 since: Option<i64>,
1364 limit: Option<u32>,
1365 params: Option<HashMap<String, String>>,
1366 ) -> Result<Value> {
1367 self.check_required_credentials()?;
1368 self.load_markets(false).await?;
1369
1370 let mut request_params = BTreeMap::new();
1371
1372 if let Some(sym) = symbol {
1373 let market = self.base().market(sym).await?;
1374 request_params.insert("symbol".to_string(), market.id.clone());
1375 }
1376
1377 if let Some(s) = since {
1378 request_params.insert("startTime".to_string(), s.to_string());
1379 }
1380
1381 if let Some(l) = limit {
1382 request_params.insert("limit".to_string(), l.to_string());
1383 }
1384
1385 if let Some(p) = params {
1386 for (key, value) in p {
1387 request_params.insert(key, value);
1388 }
1389 }
1390
1391 let timestamp = self.get_signing_timestamp().await?;
1392 let auth = self.get_auth()?;
1393 let signed_params =
1394 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
1395
1396 let query_string: String = signed_params
1397 .iter()
1398 .map(|(k, v)| format!("{}={}", k, v))
1399 .collect::<Vec<_>>()
1400 .join("&");
1401
1402 let url = format!("{}/income?{}", self.urls().fapi_private, query_string);
1403
1404 let mut headers = HeaderMap::new();
1405 auth.add_auth_headers_reqwest(&mut headers);
1406
1407 let data = self.base().http_client.get(&url, Some(headers)).await?;
1408
1409 Ok(data)
1410 }
1411}