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}