Skip to main content

ccxt_exchanges/binance/rest/
account.rs

1//! Binance account operations.
2//!
3//! This module contains all account-related methods including balance,
4//! trade history, account configuration, and user data stream management.
5
6use super::super::{Binance, BinanceEndpointRouter, parser};
7use ccxt_core::types::AccountType;
8use ccxt_core::types::default_type::DefaultSubType;
9use ccxt_core::{
10    Error, ParseError, Result,
11    types::{Balance, Currency, EndpointType, FeeTradingFee, MarketType, Trade},
12};
13use std::collections::HashMap;
14use tracing::warn;
15
16/// Balance fetch parameters for Binance.
17#[derive(Debug, Clone, Default)]
18pub struct BalanceFetchParams {
19    /// Account type to query (spot, margin, futures, etc.).
20    pub account_type: Option<AccountType>,
21    /// Margin mode: "cross" or "isolated".
22    pub margin_mode: Option<String>,
23    /// Symbols for isolated margin (e.g., `["BTC/USDT", "ETH/USDT"]`).
24    pub symbols: Option<Vec<String>>,
25    /// Whether to use Portfolio Margin API.
26    pub portfolio_margin: bool,
27    /// Sub-type for futures: "linear" or "inverse".
28    pub sub_type: Option<String>,
29}
30
31impl BalanceFetchParams {
32    /// Create params for spot balance.
33    pub fn spot() -> Self {
34        Self {
35            account_type: Some(AccountType::Spot),
36            ..Default::default()
37        }
38    }
39
40    /// Create params for cross margin balance.
41    pub fn cross_margin() -> Self {
42        Self {
43            account_type: Some(AccountType::Margin),
44            margin_mode: Some("cross".to_string()),
45            ..Default::default()
46        }
47    }
48
49    /// Create params for isolated margin balance.
50    pub fn isolated_margin(symbols: Option<Vec<String>>) -> Self {
51        Self {
52            account_type: Some(AccountType::IsolatedMargin),
53            margin_mode: Some("isolated".to_string()),
54            symbols,
55            ..Default::default()
56        }
57    }
58
59    /// Create params for USDT-margined futures (linear).
60    pub fn linear_futures() -> Self {
61        Self {
62            account_type: Some(AccountType::Futures),
63            sub_type: Some("linear".to_string()),
64            ..Default::default()
65        }
66    }
67
68    /// Create params for coin-margined futures (inverse/delivery).
69    pub fn inverse_futures() -> Self {
70        Self {
71            account_type: Some(AccountType::Delivery),
72            sub_type: Some("inverse".to_string()),
73            ..Default::default()
74        }
75    }
76
77    /// Create params for funding wallet.
78    pub fn funding() -> Self {
79        Self {
80            account_type: Some(AccountType::Funding),
81            ..Default::default()
82        }
83    }
84
85    /// Create params for options account.
86    pub fn option() -> Self {
87        Self {
88            account_type: Some(AccountType::Option),
89            ..Default::default()
90        }
91    }
92
93    /// Create params for portfolio margin.
94    pub fn portfolio_margin() -> Self {
95        Self {
96            portfolio_margin: true,
97            ..Default::default()
98        }
99    }
100}
101
102impl Binance {
103    /// Fetch account balance.
104    ///
105    /// # Returns
106    ///
107    /// Returns the account [`Balance`] information.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if authentication fails or the API request fails.
112    pub async fn fetch_balance_simple(&self) -> Result<Balance> {
113        let url = format!("{}/account", self.urls().private);
114        let data = self.signed_request(url).execute().await?;
115        parser::parse_balance(&data)
116    }
117
118    /// Fetch account balance with optional account type parameter.
119    ///
120    /// Query for balance and get the amount of funds available for trading or funds locked in orders.
121    ///
122    /// # Arguments
123    ///
124    /// * `account_type` - Optional account type. Supported values:
125    ///   - `Spot` - Spot trading account (default)
126    ///   - `Margin` - Cross margin account
127    ///   - `IsolatedMargin` - Isolated margin account
128    ///   - `Futures` - USDT-margined futures (linear)
129    ///   - `Delivery` - Coin-margined futures (inverse)
130    ///   - `Funding` - Funding wallet
131    ///   - `Option` - Options account
132    ///
133    /// # Returns
134    ///
135    /// Returns the account [`Balance`] information.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if authentication fails or the API request fails.
140    ///
141    /// # Example
142    ///
143    /// ```no_run
144    /// # use ccxt_exchanges::binance::Binance;
145    /// # use ccxt_core::ExchangeConfig;
146    /// # use ccxt_core::types::AccountType;
147    /// # async fn example() -> ccxt_core::Result<()> {
148    /// let mut config = ExchangeConfig::default();
149    /// config.api_key = Some("your_api_key".to_string());
150    /// config.secret = Some("your_secret".to_string());
151    /// let binance = Binance::new(config)?;
152    ///
153    /// // Fetch spot balance (default)
154    /// let balance = binance.fetch_balance(None).await?;
155    ///
156    /// // Fetch futures balance
157    /// let futures_balance = binance.fetch_balance(Some(AccountType::Futures)).await?;
158    /// # Ok(())
159    /// # }
160    /// ```
161    pub async fn fetch_balance(&self, account_type: Option<AccountType>) -> Result<Balance> {
162        let params = BalanceFetchParams {
163            account_type,
164            ..Default::default()
165        };
166        self.fetch_balance_with_params(params).await
167    }
168
169    /// Fetch account balance with detailed parameters.
170    ///
171    /// This method provides full control over balance fetching, supporting all Binance account types
172    /// and margin modes.
173    ///
174    /// # Arguments
175    ///
176    /// * `params` - Balance fetch parameters including:
177    ///   - `account_type` - Account type (spot, margin, futures, etc.)
178    ///   - `margin_mode` - "cross" or "isolated" for margin trading
179    ///   - `symbols` - Symbols for isolated margin queries
180    ///   - `portfolio_margin` - Use Portfolio Margin API
181    ///   - `sub_type` - "linear" or "inverse" for futures
182    ///
183    /// # Returns
184    ///
185    /// Returns the account [`Balance`] information.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if authentication fails or the API request fails.
190    ///
191    /// # API Endpoints
192    ///
193    /// - Spot: `GET /api/v3/account`
194    /// - Cross Margin: `GET /sapi/v1/margin/account`
195    /// - Isolated Margin: `GET /sapi/v1/margin/isolated/account`
196    /// - Funding Wallet: `POST /sapi/v1/asset/get-funding-asset`
197    /// - USDT-M Futures: `GET /fapi/v2/balance`
198    /// - COIN-M Futures: `GET /dapi/v1/balance`
199    /// - Options: `GET /eapi/v1/account`
200    /// - Portfolio Margin: `GET /papi/v1/balance`
201    ///
202    /// # Example
203    ///
204    /// ```no_run
205    /// # use ccxt_exchanges::binance::Binance;
206    /// # use ccxt_exchanges::binance::rest::account::BalanceFetchParams;
207    /// # use ccxt_core::ExchangeConfig;
208    /// # async fn example() -> ccxt_core::Result<()> {
209    /// let mut config = ExchangeConfig::default();
210    /// config.api_key = Some("your_api_key".to_string());
211    /// config.secret = Some("your_secret".to_string());
212    /// let binance = Binance::new(config)?;
213    ///
214    /// // Fetch cross margin balance
215    /// let margin_balance = binance.fetch_balance_with_params(
216    ///     BalanceFetchParams::cross_margin()
217    /// ).await?;
218    ///
219    /// // Fetch isolated margin balance for specific symbols
220    /// let isolated_balance = binance.fetch_balance_with_params(
221    ///     BalanceFetchParams::isolated_margin(Some(vec!["BTC/USDT".to_string()]))
222    /// ).await?;
223    ///
224    /// // Fetch USDT-margined futures balance
225    /// let futures_balance = binance.fetch_balance_with_params(
226    ///     BalanceFetchParams::linear_futures()
227    /// ).await?;
228    ///
229    /// // Fetch portfolio margin balance
230    /// let pm_balance = binance.fetch_balance_with_params(
231    ///     BalanceFetchParams::portfolio_margin()
232    /// ).await?;
233    /// # Ok(())
234    /// # }
235    /// ```
236    pub async fn fetch_balance_with_params(&self, params: BalanceFetchParams) -> Result<Balance> {
237        // Handle portfolio margin first
238        if params.portfolio_margin {
239            return self.fetch_portfolio_margin_balance().await;
240        }
241
242        let account_type = params.account_type.unwrap_or(AccountType::Spot);
243
244        match account_type {
245            AccountType::Spot => self.fetch_spot_balance().await,
246            AccountType::Margin => {
247                let margin_mode = params.margin_mode.as_deref().unwrap_or("cross");
248                if margin_mode == "isolated" {
249                    self.fetch_isolated_margin_balance(params.symbols).await
250                } else {
251                    self.fetch_cross_margin_balance().await
252                }
253            }
254            AccountType::IsolatedMargin => self.fetch_isolated_margin_balance(params.symbols).await,
255            AccountType::Futures => {
256                let sub_type = params.sub_type.as_deref().unwrap_or("linear");
257                if sub_type == "inverse" {
258                    self.fetch_delivery_balance().await
259                } else {
260                    self.fetch_futures_balance().await
261                }
262            }
263            AccountType::Delivery => self.fetch_delivery_balance().await,
264            AccountType::Funding => self.fetch_funding_balance().await,
265            AccountType::Option => self.fetch_option_balance().await,
266        }
267    }
268
269    /// Fetch spot account balance.
270    ///
271    /// Uses the `/api/v3/account` endpoint.
272    async fn fetch_spot_balance(&self) -> Result<Balance> {
273        let url = format!("{}/account", self.urls().private);
274        let data = self.signed_request(url).execute().await?;
275        parser::parse_balance_with_type(&data, "spot")
276    }
277
278    /// Fetch cross margin account balance.
279    ///
280    /// Uses the `/sapi/v1/margin/account` endpoint.
281    async fn fetch_cross_margin_balance(&self) -> Result<Balance> {
282        let url = format!("{}/margin/account", self.urls().sapi);
283        let data = self.signed_request(url).execute().await?;
284        parser::parse_balance_with_type(&data, "margin")
285    }
286
287    /// Fetch isolated margin account balance.
288    ///
289    /// Uses the `/sapi/v1/margin/isolated/account` endpoint.
290    ///
291    /// # Arguments
292    ///
293    /// * `symbols` - Optional list of symbols to query. If None, fetches all isolated margin pairs.
294    async fn fetch_isolated_margin_balance(&self, symbols: Option<Vec<String>>) -> Result<Balance> {
295        let url = format!("{}/margin/isolated/account", self.urls().sapi);
296
297        let mut request = self.signed_request(url);
298
299        // Add symbols parameter if provided
300        if let Some(syms) = symbols {
301            // Convert unified symbols to market IDs
302            let mut market_ids = Vec::new();
303            for sym in &syms {
304                if let Ok(market) = self.base().market(sym).await {
305                    market_ids.push(market.id.clone());
306                } else {
307                    // If market not found, use the symbol as-is (might be already in exchange format)
308                    market_ids.push(sym.replace('/', ""));
309                }
310            }
311            if !market_ids.is_empty() {
312                request = request.param("symbols", market_ids.join(","));
313            }
314        }
315
316        let data = request.execute().await?;
317        parser::parse_balance_with_type(&data, "isolated")
318    }
319
320    /// Fetch funding wallet balance.
321    ///
322    /// Uses the `/sapi/v1/asset/get-funding-asset` endpoint.
323    async fn fetch_funding_balance(&self) -> Result<Balance> {
324        use super::super::signed_request::HttpMethod;
325
326        let url = format!("{}/asset/get-funding-asset", self.urls().sapi);
327        let data = self
328            .signed_request(url)
329            .method(HttpMethod::Post)
330            .execute()
331            .await?;
332        parser::parse_balance_with_type(&data, "funding")
333    }
334
335    /// Fetch USDT-margined futures balance.
336    ///
337    /// Uses the `/fapi/v2/balance` endpoint.
338    async fn fetch_futures_balance(&self) -> Result<Balance> {
339        // Use v2 endpoint for better data
340        let url = format!("{}/balance", self.urls().fapi_private.replace("/v1", "/v2"));
341        let data = self.signed_request(url).execute().await?;
342        parser::parse_balance_with_type(&data, "linear")
343    }
344
345    /// Fetch coin-margined futures (delivery) balance.
346    ///
347    /// Uses the `/dapi/v1/balance` endpoint.
348    async fn fetch_delivery_balance(&self) -> Result<Balance> {
349        let url = format!("{}/balance", self.urls().dapi_private);
350        let data = self.signed_request(url).execute().await?;
351        parser::parse_balance_with_type(&data, "inverse")
352    }
353
354    /// Fetch options account balance.
355    ///
356    /// Uses the `/eapi/v1/account` endpoint.
357    async fn fetch_option_balance(&self) -> Result<Balance> {
358        let url = format!("{}/account", self.urls().eapi_private);
359        let data = self.signed_request(url).execute().await?;
360        parser::parse_balance_with_type(&data, "option")
361    }
362
363    /// Fetch portfolio margin account balance.
364    ///
365    /// Uses the `/papi/v1/balance` endpoint.
366    async fn fetch_portfolio_margin_balance(&self) -> Result<Balance> {
367        let url = format!("{}/balance", self.urls().papi);
368        let data = self.signed_request(url).execute().await?;
369        parser::parse_balance_with_type(&data, "portfolio")
370    }
371
372    /// Fetch user's trade history.
373    ///
374    /// # Arguments
375    ///
376    /// * `symbol` - Trading pair symbol.
377    /// * `since` - Optional start timestamp.
378    /// * `limit` - Optional limit on number of trades.
379    ///
380    /// # Returns
381    ///
382    /// Returns a vector of [`Trade`] structures for the user's trades.
383    ///
384    /// # Errors
385    ///
386    /// Returns an error if authentication fails, market is not found, or the API request fails.
387    pub async fn fetch_my_trades(
388        &self,
389        symbol: &str,
390        since: Option<i64>,
391        limit: Option<u32>,
392    ) -> Result<Vec<Trade>> {
393        let market = self.base().market(symbol).await?;
394        // Use market-type-aware endpoint routing
395        let base_url = self.rest_endpoint(&market, EndpointType::Private);
396        let url = format!("{}/myTrades", base_url);
397
398        let data = self
399            .signed_request(url)
400            .param("symbol", &market.id)
401            .optional_param("startTime", since)
402            .optional_param("limit", limit)
403            .execute()
404            .await?;
405
406        let trades_array = data.as_array().ok_or_else(|| {
407            Error::from(ParseError::invalid_format(
408                "data",
409                "Expected array of trades",
410            ))
411        })?;
412
413        let mut trades = Vec::new();
414
415        for trade_data in trades_array {
416            match parser::parse_trade(trade_data, Some(&market)) {
417                Ok(trade) => trades.push(trade),
418                Err(e) => {
419                    warn!(error = %e, "Failed to parse trade");
420                }
421            }
422        }
423
424        Ok(trades)
425    }
426
427    /// Fetch user's recent trade history with additional parameters.
428    ///
429    /// This method is similar to `fetch_my_trades` but accepts additional parameters
430    /// for more flexible querying.
431    ///
432    /// # Arguments
433    ///
434    /// * `symbol` - Trading pair symbol.
435    /// * `since` - Optional start timestamp in milliseconds.
436    /// * `limit` - Optional limit on number of trades (default: 500, max: 1000).
437    /// * `params` - Optional additional parameters that may include:
438    ///   - `orderId`: Filter by order ID.
439    ///   - `fromId`: Start from specific trade ID.
440    ///   - `endTime`: End timestamp in milliseconds.
441    ///
442    /// # Returns
443    ///
444    /// Returns a vector of [`Trade`] structures for the user's trades.
445    ///
446    /// # Errors
447    ///
448    /// Returns an error if authentication fails, market is not found, or the API request fails.
449    ///
450    /// # Example
451    ///
452    /// ```no_run
453    /// # use ccxt_exchanges::binance::Binance;
454    /// # use ccxt_core::ExchangeConfig;
455    /// # async fn example() -> ccxt_core::Result<()> {
456    /// let mut config = ExchangeConfig::default();
457    /// config.api_key = Some("your_api_key".to_string());
458    /// config.secret = Some("your_secret".to_string());
459    /// let binance = Binance::new(config)?;
460    /// let my_trades = binance.fetch_my_recent_trades("BTC/USDT", None, Some(50), None).await?;
461    /// for trade in &my_trades {
462    ///     println!("Trade: {} {} @ {}", trade.side, trade.amount, trade.price);
463    /// }
464    /// # Ok(())
465    /// # }
466    /// ```
467    pub async fn fetch_my_recent_trades(
468        &self,
469        symbol: &str,
470        since: Option<i64>,
471        limit: Option<u32>,
472        params: Option<HashMap<String, String>>,
473    ) -> Result<Vec<Trade>> {
474        let market = self.base().market(symbol).await?;
475        // Use market-type-aware endpoint routing
476        let base_url = self.rest_endpoint(&market, EndpointType::Private);
477        let url = format!("{}/myTrades", base_url);
478
479        // Convert HashMap to serde_json::Value for merge_json_params
480        let json_params = params.map(|p| {
481            serde_json::Value::Object(
482                p.into_iter()
483                    .map(|(k, v)| (k, serde_json::Value::String(v)))
484                    .collect(),
485            )
486        });
487
488        let data = self
489            .signed_request(url)
490            .param("symbol", &market.id)
491            .optional_param("startTime", since)
492            .optional_param("limit", limit)
493            .merge_json_params(json_params)
494            .execute()
495            .await?;
496
497        let trades_array = data.as_array().ok_or_else(|| {
498            Error::from(ParseError::invalid_format(
499                "data",
500                "Expected array of trades",
501            ))
502        })?;
503
504        let mut trades = Vec::new();
505        for trade_data in trades_array {
506            match parser::parse_trade(trade_data, Some(&market)) {
507                Ok(trade) => trades.push(trade),
508                Err(e) => {
509                    warn!(error = %e, "Failed to parse my trade");
510                }
511            }
512        }
513
514        Ok(trades)
515    }
516
517    /// Fetch all currency information.
518    ///
519    /// # Returns
520    ///
521    /// Returns a vector of [`Currency`] structures.
522    ///
523    /// # Errors
524    ///
525    /// Returns an error if authentication fails or the API request fails.
526    pub async fn fetch_currencies(&self) -> Result<Vec<Currency>> {
527        let url = format!("{}/capital/config/getall", self.urls().sapi);
528        let data = self.signed_request(url).execute().await?;
529        parser::parse_currencies(&data)
530    }
531
532    /// Fetch trading fees for a symbol.
533    ///
534    /// # Arguments
535    ///
536    /// * `symbol` - Trading pair symbol.
537    /// * `params` - Optional parameters. Supports `portfolioMargin` key for Portfolio Margin mode.
538    ///
539    /// # Returns
540    ///
541    /// Returns trading fee information for the symbol as a [`FeeTradingFee`] structure.
542    ///
543    /// # Errors
544    ///
545    /// Returns an error if authentication fails, market is not found, or the API request fails.
546    ///
547    /// # Example
548    ///
549    /// ```no_run
550    /// # use ccxt_exchanges::binance::Binance;
551    /// # use ccxt_core::ExchangeConfig;
552    /// # async fn example() -> ccxt_core::Result<()> {
553    /// let mut config = ExchangeConfig::default();
554    /// config.api_key = Some("your_api_key".to_string());
555    /// config.secret = Some("your_secret".to_string());
556    /// let binance = Binance::new(config)?;
557    /// let fee = binance.fetch_trading_fee("BTC/USDT", None).await?;
558    /// println!("Maker: {}, Taker: {}", fee.maker, fee.taker);
559    /// # Ok(())
560    /// # }
561    /// ```
562    pub async fn fetch_trading_fee(
563        &self,
564        symbol: &str,
565        params: Option<HashMap<String, String>>,
566    ) -> Result<FeeTradingFee> {
567        self.load_markets(false).await?;
568        let market = self.base().market(symbol).await?;
569
570        let is_portfolio_margin = params
571            .as_ref()
572            .and_then(|p| p.get("portfolioMargin"))
573            .is_some_and(|v| v == "true");
574
575        // Select API endpoint based on market type and Portfolio Margin mode
576        let url = match market.market_type {
577            MarketType::Spot => format!("{}/asset/tradeFee", self.urls().sapi),
578            MarketType::Futures | MarketType::Swap => {
579                if is_portfolio_margin {
580                    // Portfolio Margin mode uses papi endpoints
581                    if market.is_linear() {
582                        format!("{}/um/commissionRate", self.urls().papi)
583                    } else {
584                        format!("{}/cm/commissionRate", self.urls().papi)
585                    }
586                } else {
587                    // Standard mode
588                    if market.is_linear() {
589                        format!("{}/commissionRate", self.urls().fapi_private)
590                    } else {
591                        format!("{}/commissionRate", self.urls().dapi_private)
592                    }
593                }
594            }
595            MarketType::Option => {
596                return Err(Error::invalid_request(format!(
597                    "fetch_trading_fee not supported for market type: {:?}",
598                    market.market_type
599                )));
600            }
601        };
602
603        // Convert HashMap to serde_json::Value, filtering out portfolioMargin
604        let json_params = params.map(|p| {
605            serde_json::Value::Object(
606                p.into_iter()
607                    .filter(|(k, _)| k != "portfolioMargin")
608                    .map(|(k, v)| (k, serde_json::Value::String(v)))
609                    .collect(),
610            )
611        });
612
613        let response = self
614            .signed_request(url)
615            .param("symbol", &market.id)
616            .merge_json_params(json_params)
617            .execute()
618            .await?;
619
620        parser::parse_trading_fee(&response)
621    }
622
623    /// Fetch trading fees for multiple symbols.
624    ///
625    /// # Arguments
626    ///
627    /// * `symbols` - Optional list of trading pair symbols. `None` fetches all pairs.
628    /// * `params` - Optional parameters.
629    ///
630    /// # Returns
631    ///
632    /// Returns a `HashMap` of trading fees keyed by symbol.
633    ///
634    /// # Errors
635    ///
636    /// Returns an error if authentication fails or the API request fails.
637    ///
638    /// # Example
639    ///
640    /// ```no_run
641    /// # use ccxt_exchanges::binance::Binance;
642    /// # use ccxt_core::ExchangeConfig;
643    /// # async fn example() -> ccxt_core::Result<()> {
644    /// let mut config = ExchangeConfig::default();
645    /// config.api_key = Some("your_api_key".to_string());
646    /// config.secret = Some("your_secret".to_string());
647    /// let binance = Binance::new(config)?;
648    /// let fees = binance.fetch_trading_fees(None, None).await?;
649    /// for (symbol, fee) in &fees {
650    ///     println!("{}: maker={}, taker={}", symbol, fee.maker, fee.taker);
651    /// }
652    /// # Ok(())
653    /// # }
654    /// ```
655    pub async fn fetch_trading_fees(
656        &self,
657        symbols: Option<Vec<String>>,
658        params: Option<HashMap<String, String>>,
659    ) -> Result<HashMap<String, FeeTradingFee>> {
660        self.load_markets(false).await?;
661
662        let url = format!("{}/asset/tradeFee", self.urls().sapi);
663
664        // Build symbols parameter if provided
665        let symbols_param = if let Some(syms) = &symbols {
666            let mut market_ids: Vec<String> = Vec::new();
667            for s in syms {
668                if let Ok(market) = self.base().market(s).await {
669                    market_ids.push(market.id.clone());
670                }
671            }
672            if market_ids.is_empty() {
673                None
674            } else {
675                Some(market_ids.join(","))
676            }
677        } else {
678            None
679        };
680
681        // Convert HashMap to serde_json::Value for merge_json_params
682        let json_params = params.map(|p| {
683            serde_json::Value::Object(
684                p.into_iter()
685                    .map(|(k, v)| (k, serde_json::Value::String(v)))
686                    .collect(),
687            )
688        });
689
690        let response = self
691            .signed_request(url)
692            .optional_param("symbols", symbols_param)
693            .merge_json_params(json_params)
694            .execute()
695            .await?;
696
697        let fees_array = response.as_array().ok_or_else(|| {
698            Error::from(ParseError::invalid_format(
699                "data",
700                "Expected array of trading fees",
701            ))
702        })?;
703
704        let mut fees = HashMap::new();
705        for fee_data in fees_array {
706            if let Ok(symbol_id) = fee_data["symbol"]
707                .as_str()
708                .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
709            {
710                if let Ok(market) = self.base().market_by_id(symbol_id).await {
711                    if let Ok(fee) = parser::parse_trading_fee(fee_data) {
712                        fees.insert(market.symbol.clone(), fee);
713                    }
714                }
715            }
716        }
717
718        Ok(fees)
719    }
720
721    /// Create a listen key for user data stream (spot market).
722    ///
723    /// Creates a new listen key that can be used to subscribe to user data streams
724    /// via WebSocket. The listen key is valid for 60 minutes.
725    ///
726    /// # Returns
727    ///
728    /// Returns the listen key string.
729    ///
730    /// # Errors
731    ///
732    /// Returns an error if:
733    /// - API credentials are not configured
734    /// - API request fails
735    ///
736    /// # Example
737    ///
738    /// ```no_run
739    /// # use ccxt_exchanges::binance::Binance;
740    /// # use ccxt_core::ExchangeConfig;
741    /// # async fn example() -> ccxt_core::Result<()> {
742    /// let mut config = ExchangeConfig::default();
743    /// config.api_key = Some("your_api_key".to_string());
744    /// config.secret = Some("your_secret".to_string());
745    /// let binance = Binance::new(config)?;
746    ///
747    /// let listen_key = binance.create_listen_key().await?;
748    /// println!("Listen Key: {}", listen_key);
749    /// # Ok(())
750    /// # }
751    /// ```
752    pub async fn create_listen_key(&self) -> Result<String> {
753        self.create_listen_key_for_market(None).await
754    }
755
756    /// Create a listen key for user data stream with market type routing.
757    ///
758    /// Creates a new listen key for the specified market type. Different market types
759    /// use different API endpoints:
760    /// - Spot: `/api/v3/userDataStream`
761    /// - USDT-M Futures: `/fapi/v1/listenKey`
762    /// - COIN-M Futures: `/dapi/v1/listenKey`
763    /// - Options: `/eapi/v1/listenKey`
764    ///
765    /// # Arguments
766    ///
767    /// * `market_type` - Optional market type. If None, defaults to Spot.
768    ///
769    /// # Returns
770    ///
771    /// Returns the listen key string.
772    ///
773    /// # Errors
774    ///
775    /// Returns an error if:
776    /// - API credentials are not configured
777    /// - API request fails
778    ///
779    /// # Example
780    ///
781    /// ```no_run
782    /// # use ccxt_exchanges::binance::Binance;
783    /// # use ccxt_core::ExchangeConfig;
784    /// # use ccxt_core::types::MarketType;
785    /// # async fn example() -> ccxt_core::Result<()> {
786    /// let mut config = ExchangeConfig::default();
787    /// config.api_key = Some("your_api_key".to_string());
788    /// config.secret = Some("your_secret".to_string());
789    /// let binance = Binance::new(config)?;
790    ///
791    /// // Create listen key for futures
792    /// let listen_key = binance.create_listen_key_for_market(Some(MarketType::Swap)).await?;
793    /// println!("Futures Listen Key: {}", listen_key);
794    /// # Ok(())
795    /// # }
796    /// ```
797    pub async fn create_listen_key_for_market(
798        &self,
799        market_type: Option<MarketType>,
800    ) -> Result<String> {
801        let url = self.get_listen_key_url(market_type, None);
802
803        let response = self
804            .api_key_request(url)
805            .method(crate::binance::signed_request::HttpMethod::Post)
806            .execute()
807            .await?;
808
809        response["listenKey"]
810            .as_str()
811            .map(ToString::to_string)
812            .ok_or_else(|| Error::from(ParseError::missing_field("listenKey")))
813    }
814
815    /// Refresh listen key to extend validity (spot market).
816    ///
817    /// Extends the listen key validity by 60 minutes. Recommended to call
818    /// every 30 minutes to maintain the connection.
819    ///
820    /// # Arguments
821    ///
822    /// * `listen_key` - The listen key to refresh.
823    ///
824    /// # Returns
825    ///
826    /// Returns `Ok(())` on success.
827    ///
828    /// # Errors
829    ///
830    /// Returns an error if:
831    /// - API credentials are not configured
832    /// - Listen key is invalid or expired
833    /// - API request fails
834    pub async fn refresh_listen_key(&self, listen_key: &str) -> Result<()> {
835        self.refresh_listen_key_for_market(listen_key, None).await
836    }
837
838    /// Refresh listen key with market type routing.
839    ///
840    /// Extends the listen key validity by 60 minutes for the specified market type.
841    ///
842    /// # Arguments
843    ///
844    /// * `listen_key` - The listen key to refresh.
845    /// * `market_type` - Optional market type. If None, defaults to Spot.
846    ///
847    /// # Returns
848    ///
849    /// Returns `Ok(())` on success.
850    ///
851    /// # Errors
852    ///
853    /// Returns an error if:
854    /// - API credentials are not configured
855    /// - Listen key is invalid or expired
856    /// - API request fails
857    pub async fn refresh_listen_key_for_market(
858        &self,
859        listen_key: &str,
860        market_type: Option<MarketType>,
861    ) -> Result<()> {
862        let url = self.get_listen_key_url(market_type, Some(listen_key));
863
864        self.api_key_request(url)
865            .method(crate::binance::signed_request::HttpMethod::Put)
866            .execute()
867            .await?;
868
869        Ok(())
870    }
871
872    /// Delete listen key to close user data stream (spot market).
873    ///
874    /// Closes the user data stream connection and invalidates the listen key.
875    ///
876    /// # Arguments
877    ///
878    /// * `listen_key` - The listen key to delete.
879    ///
880    /// # Returns
881    ///
882    /// Returns `Ok(())` on success.
883    ///
884    /// # Errors
885    ///
886    /// Returns an error if:
887    /// - API credentials are not configured
888    /// - API request fails
889    pub async fn delete_listen_key(&self, listen_key: &str) -> Result<()> {
890        self.delete_listen_key_for_market(listen_key, None).await
891    }
892
893    /// Delete listen key with market type routing.
894    ///
895    /// Closes the user data stream connection for the specified market type.
896    ///
897    /// # Arguments
898    ///
899    /// * `listen_key` - The listen key to delete.
900    /// * `market_type` - Optional market type. If None, defaults to Spot.
901    ///
902    /// # Returns
903    ///
904    /// Returns `Ok(())` on success.
905    ///
906    /// # Errors
907    ///
908    /// Returns an error if:
909    /// - API credentials are not configured
910    /// - API request fails
911    pub async fn delete_listen_key_for_market(
912        &self,
913        listen_key: &str,
914        market_type: Option<MarketType>,
915    ) -> Result<()> {
916        let url = self.get_listen_key_url(market_type, Some(listen_key));
917
918        self.api_key_request(url)
919            .method(crate::binance::signed_request::HttpMethod::Delete)
920            .execute()
921            .await?;
922
923        Ok(())
924    }
925
926    /// Get the listen key URL for the specified market type.
927    ///
928    /// # Arguments
929    ///
930    /// * `market_type` - Optional market type. If None, defaults to Spot.
931    /// * `listen_key` - Optional listen key to include in the URL (for refresh/delete).
932    ///
933    /// # Returns
934    ///
935    /// Returns the appropriate URL for the listen key operation.
936    fn get_listen_key_url(
937        &self,
938        market_type: Option<MarketType>,
939        listen_key: Option<&str>,
940    ) -> String {
941        let urls = self.urls();
942        let (base_url, endpoint) = match market_type {
943            Some(MarketType::Swap | MarketType::Futures) => {
944                // Distinguish between linear (USDT-M) and inverse (COIN-M)
945                if self.options().default_sub_type == Some(DefaultSubType::Inverse) {
946                    (urls.dapi_public.clone(), "listenKey")
947                } else {
948                    (urls.fapi_public.clone(), "listenKey")
949                }
950            }
951            Some(MarketType::Option) => (urls.eapi_public.clone(), "listenKey"),
952            // Spot is the default
953            None | Some(MarketType::Spot) => (urls.public.clone(), "userDataStream"),
954        };
955
956        match listen_key {
957            Some(key) => format!("{}/{}?listenKey={}", base_url, endpoint, key),
958            None => format!("{}/{}", base_url, endpoint),
959        }
960    }
961
962    /// Get the listen key URL for linear (USDT-M) futures.
963    ///
964    /// # Arguments
965    ///
966    /// * `listen_key` - Optional listen key to include in the URL.
967    ///
968    /// # Returns
969    ///
970    /// Returns the URL for linear futures listen key operations.
971    pub fn get_linear_listen_key_url(&self, listen_key: Option<&str>) -> String {
972        let base_url = &self.urls().fapi_public;
973        match listen_key {
974            Some(key) => format!("{}/listenKey?listenKey={}", base_url, key),
975            None => format!("{}/listenKey", base_url),
976        }
977    }
978
979    /// Get the listen key URL for inverse (COIN-M) futures.
980    ///
981    /// # Arguments
982    ///
983    /// * `listen_key` - Optional listen key to include in the URL.
984    ///
985    /// # Returns
986    ///
987    /// Returns the URL for inverse futures listen key operations.
988    pub fn get_inverse_listen_key_url(&self, listen_key: Option<&str>) -> String {
989        let base_url = &self.urls().dapi_public;
990        match listen_key {
991            Some(key) => format!("{}/listenKey?listenKey={}", base_url, key),
992            None => format!("{}/listenKey", base_url),
993        }
994    }
995
996    /// Create a listen key for linear (USDT-M) futures.
997    ///
998    /// # Returns
999    ///
1000    /// Returns the listen key string.
1001    ///
1002    /// # Errors
1003    ///
1004    /// Returns an error if API credentials are not configured or the request fails.
1005    pub async fn create_linear_listen_key(&self) -> Result<String> {
1006        let url = self.get_linear_listen_key_url(None);
1007
1008        let response = self
1009            .api_key_request(url)
1010            .method(crate::binance::signed_request::HttpMethod::Post)
1011            .execute()
1012            .await?;
1013
1014        response["listenKey"]
1015            .as_str()
1016            .map(ToString::to_string)
1017            .ok_or_else(|| Error::from(ParseError::missing_field("listenKey")))
1018    }
1019
1020    /// Create a listen key for inverse (COIN-M) futures.
1021    ///
1022    /// # Returns
1023    ///
1024    /// Returns the listen key string.
1025    ///
1026    /// # Errors
1027    ///
1028    /// Returns an error if API credentials are not configured or the request fails.
1029    pub async fn create_inverse_listen_key(&self) -> Result<String> {
1030        let url = self.get_inverse_listen_key_url(None);
1031
1032        let response = self
1033            .api_key_request(url)
1034            .method(crate::binance::signed_request::HttpMethod::Post)
1035            .execute()
1036            .await?;
1037
1038        response["listenKey"]
1039            .as_str()
1040            .map(ToString::to_string)
1041            .ok_or_else(|| Error::from(ParseError::missing_field("listenKey")))
1042    }
1043
1044    /// Refresh a linear (USDT-M) futures listen key.
1045    ///
1046    /// # Arguments
1047    ///
1048    /// * `listen_key` - The listen key to refresh.
1049    ///
1050    /// # Returns
1051    ///
1052    /// Returns `Ok(())` on success.
1053    pub async fn refresh_linear_listen_key(&self, listen_key: &str) -> Result<()> {
1054        let url = self.get_linear_listen_key_url(Some(listen_key));
1055
1056        self.api_key_request(url)
1057            .method(crate::binance::signed_request::HttpMethod::Put)
1058            .execute()
1059            .await?;
1060
1061        Ok(())
1062    }
1063
1064    /// Refresh an inverse (COIN-M) futures listen key.
1065    ///
1066    /// # Arguments
1067    ///
1068    /// * `listen_key` - The listen key to refresh.
1069    ///
1070    /// # Returns
1071    ///
1072    /// Returns `Ok(())` on success.
1073    pub async fn refresh_inverse_listen_key(&self, listen_key: &str) -> Result<()> {
1074        let url = self.get_inverse_listen_key_url(Some(listen_key));
1075
1076        self.api_key_request(url)
1077            .method(crate::binance::signed_request::HttpMethod::Put)
1078            .execute()
1079            .await?;
1080
1081        Ok(())
1082    }
1083
1084    /// Delete a linear (USDT-M) futures listen key.
1085    ///
1086    /// # Arguments
1087    ///
1088    /// * `listen_key` - The listen key to delete.
1089    ///
1090    /// # Returns
1091    ///
1092    /// Returns `Ok(())` on success.
1093    pub async fn delete_linear_listen_key(&self, listen_key: &str) -> Result<()> {
1094        let url = self.get_linear_listen_key_url(Some(listen_key));
1095
1096        self.api_key_request(url)
1097            .method(crate::binance::signed_request::HttpMethod::Delete)
1098            .execute()
1099            .await?;
1100
1101        Ok(())
1102    }
1103
1104    /// Delete an inverse (COIN-M) futures listen key.
1105    ///
1106    /// # Arguments
1107    ///
1108    /// * `listen_key` - The listen key to delete.
1109    ///
1110    /// # Returns
1111    ///
1112    /// Returns `Ok(())` on success.
1113    pub async fn delete_inverse_listen_key(&self, listen_key: &str) -> Result<()> {
1114        let url = self.get_inverse_listen_key_url(Some(listen_key));
1115
1116        self.api_key_request(url)
1117            .method(crate::binance::signed_request::HttpMethod::Delete)
1118            .execute()
1119            .await?;
1120
1121        Ok(())
1122    }
1123}