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