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}