ccxt_exchanges/binance/rest.rs
1//! Binance REST API implementation.
2//!
3//! Implements all REST API endpoint operations for the Binance exchange.
4
5use super::{Binance, auth::BinanceAuth, parser};
6use ccxt_core::{
7 Error, ParseError, Result,
8 types::{
9 AccountConfig, AggTrade, Balance, BatchCancelResult, BatchOrderRequest, BatchOrderResult,
10 BatchOrderUpdate, BidAsk, BorrowInterest, BorrowRateHistory, CancelAllOrdersResult,
11 CommissionRate, Currency, DepositAddress, FeeFundingRate, FeeFundingRateHistory,
12 FeeTradingFee, FundingFee, IntoTickerParams, LastPrice, LeverageTier, MarginAdjustment,
13 MarginLoan, MarginRepay, MarkPrice, Market, MarketType, MaxBorrowable, MaxLeverage,
14 MaxTransferable, NextFundingRate, OcoOrder, OpenInterest, OpenInterestHistory, Order,
15 OrderBook, OrderSide, OrderStatus, OrderType, Stats24hr, Ticker, Trade, TradingLimits,
16 Transaction, TransactionType, Transfer,
17 },
18};
19use reqwest::header::HeaderMap;
20use rust_decimal::Decimal;
21use serde_json::Value;
22use std::collections::HashMap;
23use tracing::{debug, info, warn};
24
25impl Binance {
26 /// Fetch server timestamp for internal use.
27 ///
28 /// # Returns
29 ///
30 /// Returns the server timestamp in milliseconds.
31 ///
32 /// # Errors
33 ///
34 /// Returns an error if the request fails or the response is malformed.
35 async fn fetch_time_raw(&self) -> Result<u64> {
36 let url = format!("{}/time", self.urls().public);
37 let response = self.base().http_client.get(&url, None).await?;
38
39 response["serverTime"]
40 .as_u64()
41 .ok_or_else(|| ParseError::missing_field("serverTime").into())
42 }
43
44 /// Fetch exchange system status.
45 ///
46 /// # Returns
47 ///
48 /// Returns formatted exchange status information with the following structure:
49 /// ```json
50 /// {
51 /// "status": "ok" | "maintenance",
52 /// "updated": null,
53 /// "eta": null,
54 /// "url": null,
55 /// "info": { ... }
56 /// }
57 /// ```
58 pub async fn fetch_status(&self) -> Result<Value> {
59 let url = format!("{}/system/status", self.urls().sapi);
60 let response = self.base().http_client.get(&url, None).await?;
61
62 // Response format: { "status": 0, "msg": "normal" }
63 // Status codes: 0 = normal, 1 = system maintenance
64 let status_raw = response
65 .get("status")
66 .and_then(|v| v.as_i64())
67 .ok_or_else(|| {
68 Error::from(ParseError::invalid_format(
69 "status",
70 "status field missing or not an integer",
71 ))
72 })?;
73
74 let status = match status_raw {
75 0 => "ok",
76 1 => "maintenance",
77 _ => "unknown",
78 };
79
80 // json! macro with literal values is infallible
81 #[allow(clippy::disallowed_methods)]
82 Ok(serde_json::json!({
83 "status": status,
84 "updated": null,
85 "eta": null,
86 "url": null,
87 "info": response
88 }))
89 }
90
91 /// Fetch all trading markets.
92 ///
93 /// # Returns
94 ///
95 /// Returns a vector of [`Market`] structures containing market information.
96 ///
97 /// # Errors
98 ///
99 /// Returns an error if the API request fails or response parsing fails.
100 ///
101 /// # Example
102 ///
103 /// ```no_run
104 /// # use ccxt_exchanges::binance::Binance;
105 /// # use ccxt_core::ExchangeConfig;
106 /// # async fn example() -> ccxt_core::Result<()> {
107 /// let binance = Binance::new(ExchangeConfig::default())?;
108 /// let markets = binance.fetch_markets().await?;
109 /// println!("Found {} markets", markets.len());
110 /// # Ok(())
111 /// # }
112 /// ```
113 pub async fn fetch_markets(&self) -> Result<Vec<Market>> {
114 let url = format!("{}/exchangeInfo", self.urls().public);
115 let data = self.base().http_client.get(&url, None).await?;
116
117 let symbols = data["symbols"]
118 .as_array()
119 .ok_or_else(|| Error::from(ParseError::missing_field("symbols")))?;
120
121 let mut markets = Vec::new();
122 for symbol in symbols {
123 match parser::parse_market(symbol) {
124 Ok(market) => markets.push(market),
125 Err(e) => {
126 warn!(error = %e, "Failed to parse market");
127 }
128 }
129 }
130
131 let markets = self.base().set_markets(markets, None).await?;
132
133 Ok(markets)
134 }
135 /// Load and cache market data.
136 ///
137 /// Standard CCXT method for loading all market data from the exchange.
138 /// If markets are already loaded and `reload` is false, returns cached data.
139 ///
140 /// # Arguments
141 ///
142 /// * `reload` - Whether to force reload market data from the API.
143 ///
144 /// # Returns
145 ///
146 /// Returns a `HashMap` containing all market data, keyed by symbol (e.g., "BTC/USDT").
147 ///
148 /// # Errors
149 ///
150 /// Returns an error if the API request fails or response parsing fails.
151 ///
152 /// # Example
153 ///
154 /// ```no_run
155 /// # use ccxt_exchanges::binance::Binance;
156 /// # use ccxt_core::ExchangeConfig;
157 /// # async fn example() -> ccxt_core::error::Result<()> {
158 /// let binance = Binance::new(ExchangeConfig::default())?;
159 ///
160 /// // Load markets for the first time
161 /// let markets = binance.load_markets(false).await?;
162 /// println!("Loaded {} markets", markets.len());
163 ///
164 /// // Subsequent calls use cache (no API request)
165 /// let markets = binance.load_markets(false).await?;
166 ///
167 /// // Force reload
168 /// let markets = binance.load_markets(true).await?;
169 /// # Ok(())
170 /// # }
171 /// ```
172 pub async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
173 {
174 let cache = self.base().market_cache.read().await;
175 if cache.loaded && !reload {
176 debug!(
177 "Returning cached markets for Binance ({} markets)",
178 cache.markets.len()
179 );
180 return Ok(cache.markets.clone());
181 }
182 }
183
184 info!("Loading markets for Binance (reload: {})", reload);
185 let _markets = self.fetch_markets().await?;
186
187 let cache = self.base().market_cache.read().await;
188 Ok(cache.markets.clone())
189 }
190
191 /// Fetch ticker for a single trading pair.
192 ///
193 /// # Arguments
194 ///
195 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
196 /// * `params` - Optional parameters to configure the ticker request.
197 ///
198 /// # Returns
199 ///
200 /// Returns [`Ticker`] data for the specified symbol.
201 ///
202 /// # Errors
203 ///
204 /// Returns an error if the market is not found or the API request fails.
205 pub async fn fetch_ticker(
206 &self,
207 symbol: &str,
208 params: impl IntoTickerParams,
209 ) -> Result<Ticker> {
210 let market = self.base().market(symbol).await?;
211
212 let params = params.into_ticker_params();
213 let rolling = params.rolling.unwrap_or(false);
214
215 let endpoint = if rolling { "ticker" } else { "ticker/24hr" };
216
217 let mut url = format!("{}/{}?symbol={}", self.urls().public, endpoint, market.id);
218
219 if let Some(window) = params.window_size {
220 url.push_str(&format!("&windowSize={}", window));
221 }
222
223 for (key, value) in ¶ms.extras {
224 if key != "rolling" && key != "windowSize" {
225 url.push_str(&format!("&{}={}", key, value));
226 }
227 }
228
229 let data = self.base().http_client.get(&url, None).await?;
230
231 parser::parse_ticker(&data, Some(&market))
232 }
233
234 /// Fetch tickers for multiple trading pairs.
235 ///
236 /// # Arguments
237 ///
238 /// * `symbols` - Optional list of trading pair symbols; fetches all if `None`.
239 ///
240 /// # Returns
241 ///
242 /// Returns a vector of [`Ticker`] structures.
243 ///
244 /// # Errors
245 ///
246 /// Returns an error if markets are not loaded or the API request fails.
247 pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
248 let cache = self.base().market_cache.read().await;
249 if !cache.loaded {
250 drop(cache);
251 return Err(Error::exchange(
252 "-1",
253 "Markets not loaded. Call load_markets() first.",
254 ));
255 }
256 drop(cache);
257
258 let url = format!("{}/ticker/24hr", self.urls().public);
259 let data = self.base().http_client.get(&url, None).await?;
260
261 let tickers_array = data.as_array().ok_or_else(|| {
262 Error::from(ParseError::invalid_format(
263 "response",
264 "Expected array of tickers",
265 ))
266 })?;
267
268 let mut tickers = Vec::new();
269 for ticker_data in tickers_array {
270 if let Some(binance_symbol) = ticker_data["symbol"].as_str() {
271 let cache = self.base().market_cache.read().await;
272 if let Some(market) = cache.markets_by_id.get(binance_symbol) {
273 let market_clone = market.clone();
274 drop(cache);
275
276 match parser::parse_ticker(ticker_data, Some(&market_clone)) {
277 Ok(ticker) => {
278 if let Some(ref syms) = symbols {
279 if syms.contains(&ticker.symbol) {
280 tickers.push(ticker);
281 }
282 } else {
283 tickers.push(ticker);
284 }
285 }
286 Err(e) => {
287 warn!(
288 error = %e,
289 symbol = %binance_symbol,
290 "Failed to parse ticker"
291 );
292 }
293 }
294 } else {
295 drop(cache);
296 }
297 }
298 }
299
300 Ok(tickers)
301 }
302
303 /// Fetch order book for a trading pair.
304 ///
305 /// # Arguments
306 ///
307 /// * `symbol` - Trading pair symbol.
308 /// * `limit` - Optional depth limit (valid values: 5, 10, 20, 50, 100, 500, 1000, 5000).
309 ///
310 /// # Returns
311 ///
312 /// Returns [`OrderBook`] data containing bids and asks.
313 ///
314 /// # Errors
315 ///
316 /// Returns an error if the market is not found or the API request fails.
317 pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
318 let market = self.base().market(symbol).await?;
319
320 let url = if let Some(l) = limit {
321 format!(
322 "{}/depth?symbol={}&limit={}",
323 self.urls().public,
324 market.id,
325 l
326 )
327 } else {
328 format!("{}/depth?symbol={}", self.urls().public, market.id)
329 };
330
331 let data = self.base().http_client.get(&url, None).await?;
332
333 parser::parse_orderbook(&data, market.symbol.clone())
334 }
335
336 /// Fetch recent public trades.
337 ///
338 /// # Arguments
339 ///
340 /// * `symbol` - Trading pair symbol.
341 /// * `limit` - Optional limit on number of trades (maximum: 1000).
342 ///
343 /// # Returns
344 ///
345 /// Returns a vector of [`Trade`] structures, sorted by timestamp in descending order.
346 ///
347 /// # Errors
348 ///
349 /// Returns an error if the market is not found or the API request fails.
350 pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
351 let market = self.base().market(symbol).await?;
352
353 let url = if let Some(l) = limit {
354 format!(
355 "{}/trades?symbol={}&limit={}",
356 self.urls().public,
357 market.id,
358 l
359 )
360 } else {
361 format!("{}/trades?symbol={}", self.urls().public, market.id)
362 };
363
364 let data = self.base().http_client.get(&url, None).await?;
365
366 let trades_array = data.as_array().ok_or_else(|| {
367 Error::from(ParseError::invalid_format(
368 "data",
369 "Expected array of trades",
370 ))
371 })?;
372
373 let mut trades = Vec::new();
374 for trade_data in trades_array {
375 match parser::parse_trade(trade_data, Some(&market)) {
376 Ok(trade) => trades.push(trade),
377 Err(e) => {
378 warn!(error = %e, "Failed to parse trade");
379 }
380 }
381 }
382
383 // CCXT convention: trades should be sorted by timestamp descending (newest first)
384 trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
385
386 Ok(trades)
387 }
388 /// Fetch recent public trades (alias for `fetch_trades`).
389 ///
390 /// # Arguments
391 ///
392 /// * `symbol` - Trading pair symbol.
393 /// * `limit` - Optional limit on number of trades (default: 500, maximum: 1000).
394 ///
395 /// # Returns
396 ///
397 /// Returns a vector of [`Trade`] structures for recent public trades.
398 ///
399 /// # Errors
400 ///
401 /// Returns an error if the market is not found or the API request fails.
402 ///
403 /// # Example
404 ///
405 /// ```no_run
406 /// # use ccxt_exchanges::binance::Binance;
407 /// # use ccxt_core::ExchangeConfig;
408 /// # async fn example() -> ccxt_core::Result<()> {
409 /// # let binance = Binance::new(ExchangeConfig::default())?;
410 /// let recent_trades = binance.fetch_recent_trades("BTC/USDT", Some(100)).await?;
411 /// # Ok(())
412 /// # }
413 /// ```
414 pub async fn fetch_recent_trades(
415 &self,
416 symbol: &str,
417 limit: Option<u32>,
418 ) -> Result<Vec<Trade>> {
419 self.fetch_trades(symbol, limit).await
420 }
421
422 /// Fetch authenticated user's recent trades (private API).
423 ///
424 /// # Arguments
425 ///
426 /// * `symbol` - Trading pair symbol.
427 /// * `since` - Optional start timestamp in milliseconds.
428 /// * `limit` - Optional limit on number of trades (default: 500, maximum: 1000).
429 /// * `params` - Additional parameters.
430 ///
431 /// # Returns
432 ///
433 /// Returns a vector of [`Trade`] structures for the user's trades.
434 ///
435 /// # Errors
436 ///
437 /// Returns an error if authentication fails or the API request fails.
438 ///
439 /// # Example
440 ///
441 /// ```no_run
442 /// # use ccxt_exchanges::binance::Binance;
443 /// # use ccxt_core::ExchangeConfig;
444 /// # async fn example() -> ccxt_core::Result<()> {
445 /// # let binance = Binance::new(ExchangeConfig::default())?;
446 /// let my_trades = binance.fetch_my_recent_trades("BTC/USDT", None, Some(50), None).await?;
447 /// # Ok(())
448 /// # }
449 /// ```
450 pub async fn fetch_my_recent_trades(
451 &self,
452 symbol: &str,
453 since: Option<u64>,
454 limit: Option<u32>,
455 params: Option<HashMap<String, String>>,
456 ) -> Result<Vec<Trade>> {
457 let market = self.base().market(symbol).await?;
458
459 self.check_required_credentials()?;
460
461 let mut request_params = HashMap::new();
462 request_params.insert("symbol".to_string(), market.id.clone());
463
464 if let Some(s) = since {
465 request_params.insert("startTime".to_string(), s.to_string());
466 }
467
468 if let Some(l) = limit {
469 request_params.insert("limit".to_string(), l.to_string());
470 }
471
472 if let Some(p) = params {
473 for (k, v) in p {
474 request_params.insert(k, v);
475 }
476 }
477
478 let url = format!("{}/myTrades", self.urls().private);
479 let timestamp = self.fetch_time_raw().await?;
480 let auth = self.get_auth()?;
481 let signed_params =
482 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
483
484 let mut request_url = format!("{}?", url);
485 for (key, value) in &signed_params {
486 request_url.push_str(&format!("{}={}&", key, value));
487 }
488
489 let mut headers = HeaderMap::new();
490 auth.add_auth_headers_reqwest(&mut headers);
491
492 let data = self
493 .base()
494 .http_client
495 .get(&request_url, Some(headers))
496 .await?;
497
498 let trades_array = data.as_array().ok_or_else(|| {
499 Error::from(ParseError::invalid_format(
500 "data",
501 "Expected array of trades",
502 ))
503 })?;
504
505 let mut trades = Vec::new();
506 for trade_data in trades_array {
507 match parser::parse_trade(trade_data, Some(&market)) {
508 Ok(trade) => trades.push(trade),
509 Err(e) => {
510 warn!(error = %e, "Failed to parse my trade");
511 }
512 }
513 }
514
515 Ok(trades)
516 }
517
518 /// Fetch aggregated trade data.
519 ///
520 /// # Arguments
521 ///
522 /// * `symbol` - Trading pair symbol.
523 /// * `since` - Optional start timestamp in milliseconds.
524 /// * `limit` - Optional limit on number of records (default: 500, maximum: 1000).
525 /// * `params` - Additional parameters that may include:
526 /// - `fromId`: Start from specific aggTradeId.
527 /// - `endTime`: End timestamp in milliseconds.
528 ///
529 /// # Returns
530 ///
531 /// Returns a vector of aggregated trade records.
532 ///
533 /// # Errors
534 ///
535 /// Returns an error if the market is not found or the API request fails.
536 ///
537 /// # Example
538 ///
539 /// ```no_run
540 /// # use ccxt_exchanges::binance::Binance;
541 /// # use ccxt_core::ExchangeConfig;
542 /// # async fn example() -> ccxt_core::Result<()> {
543 /// # let binance = Binance::new(ExchangeConfig::default())?;
544 /// let agg_trades = binance.fetch_agg_trades("BTC/USDT", None, Some(100), None).await?;
545 /// # Ok(())
546 /// # }
547 /// ```
548 pub async fn fetch_agg_trades(
549 &self,
550 symbol: &str,
551 since: Option<u64>,
552 limit: Option<u32>,
553 params: Option<HashMap<String, String>>,
554 ) -> Result<Vec<AggTrade>> {
555 let market = self.base().market(symbol).await?;
556
557 let mut url = format!("{}/aggTrades?symbol={}", self.urls().public, market.id);
558
559 if let Some(s) = since {
560 url.push_str(&format!("&startTime={}", s));
561 }
562
563 if let Some(l) = limit {
564 url.push_str(&format!("&limit={}", l));
565 }
566
567 if let Some(p) = params {
568 if let Some(from_id) = p.get("fromId") {
569 url.push_str(&format!("&fromId={}", from_id));
570 }
571 if let Some(end_time) = p.get("endTime") {
572 url.push_str(&format!("&endTime={}", end_time));
573 }
574 }
575
576 let data = self.base().http_client.get(&url, None).await?;
577
578 let agg_trades_array = data.as_array().ok_or_else(|| {
579 Error::from(ParseError::invalid_format(
580 "data",
581 "Expected array of agg trades",
582 ))
583 })?;
584
585 let mut agg_trades = Vec::new();
586 for agg_trade_data in agg_trades_array {
587 match parser::parse_agg_trade(agg_trade_data, Some(market.symbol.clone())) {
588 Ok(agg_trade) => agg_trades.push(agg_trade),
589 Err(e) => {
590 warn!(error = %e, "Failed to parse agg trade");
591 }
592 }
593 }
594
595 Ok(agg_trades)
596 }
597
598 /// Fetch historical trade data (requires API key but not signature).
599 ///
600 /// Note: Binance API uses `fromId` parameter instead of timestamp.
601 ///
602 /// # Arguments
603 ///
604 /// * `symbol` - Trading pair symbol.
605 /// * `_since` - Optional start timestamp (unused, Binance uses `fromId` instead).
606 /// * `limit` - Optional limit on number of records (default: 500, maximum: 1000).
607 /// * `params` - Additional parameters that may include:
608 /// - `fromId`: Start from specific tradeId.
609 ///
610 /// # Returns
611 ///
612 /// Returns a vector of historical [`Trade`] records.
613 ///
614 /// # Errors
615 ///
616 /// Returns an error if authentication fails or the API request fails.
617 ///
618 /// # Example
619 ///
620 /// ```no_run
621 /// # use ccxt_exchanges::binance::Binance;
622 /// # use ccxt_core::ExchangeConfig;
623 /// # async fn example() -> ccxt_core::Result<()> {
624 /// # let binance = Binance::new(ExchangeConfig::default())?;
625 /// let historical_trades = binance.fetch_historical_trades("BTC/USDT", None, Some(100), None).await?;
626 /// # Ok(())
627 /// # }
628 /// ```
629 pub async fn fetch_historical_trades(
630 &self,
631 symbol: &str,
632 _since: Option<u64>,
633 limit: Option<u32>,
634 params: Option<HashMap<String, String>>,
635 ) -> Result<Vec<Trade>> {
636 let market = self.base().market(symbol).await?;
637
638 self.check_required_credentials()?;
639
640 let mut url = format!(
641 "{}/historicalTrades?symbol={}",
642 self.urls().public,
643 market.id
644 );
645
646 // Binance historicalTrades endpoint uses fromId instead of timestamp
647 if let Some(p) = ¶ms {
648 if let Some(from_id) = p.get("fromId") {
649 url.push_str(&format!("&fromId={}", from_id));
650 }
651 }
652
653 if let Some(l) = limit {
654 url.push_str(&format!("&limit={}", l));
655 }
656
657 let mut headers = HeaderMap::new();
658 let auth = self.get_auth()?;
659 auth.add_auth_headers_reqwest(&mut headers);
660
661 let data = self.base().http_client.get(&url, Some(headers)).await?;
662
663 let trades_array = data.as_array().ok_or_else(|| {
664 Error::from(ParseError::invalid_format(
665 "data",
666 "Expected array of trades",
667 ))
668 })?;
669
670 let mut trades = Vec::new();
671 for trade_data in trades_array {
672 match parser::parse_trade(trade_data, Some(&market)) {
673 Ok(trade) => trades.push(trade),
674 Err(e) => {
675 warn!(error = %e, "Failed to parse historical trade");
676 }
677 }
678 }
679
680 Ok(trades)
681 }
682
683 /// Fetch 24-hour trading statistics.
684 ///
685 /// # Arguments
686 ///
687 /// * `symbol` - Optional trading pair symbol. If `None`, returns statistics for all pairs.
688 ///
689 /// # Returns
690 ///
691 /// Returns a vector of [`Stats24hr`] structures. Single symbol returns one item, all symbols return multiple items.
692 ///
693 /// # Errors
694 ///
695 /// Returns an error if the market is not found or the API request fails.
696 ///
697 /// # Example
698 ///
699 /// ```no_run
700 /// # use ccxt_exchanges::binance::Binance;
701 /// # use ccxt_core::ExchangeConfig;
702 /// # async fn example() -> ccxt_core::Result<()> {
703 /// # let binance = Binance::new(ExchangeConfig::default())?;
704 /// // Fetch statistics for a single pair
705 /// let stats = binance.fetch_24hr_stats(Some("BTC/USDT")).await?;
706 ///
707 /// // Fetch statistics for all pairs
708 /// let all_stats = binance.fetch_24hr_stats(None).await?;
709 /// # Ok(())
710 /// # }
711 /// ```
712 pub async fn fetch_24hr_stats(&self, symbol: Option<&str>) -> Result<Vec<Stats24hr>> {
713 let url = if let Some(sym) = symbol {
714 let market = self.base().market(sym).await?;
715 format!("{}/ticker/24hr?symbol={}", self.urls().public, market.id)
716 } else {
717 format!("{}/ticker/24hr", self.urls().public)
718 };
719
720 let data = self.base().http_client.get(&url, None).await?;
721
722 // Single symbol returns object, all symbols return array
723 let stats_vec = if data.is_array() {
724 let stats_array = data.as_array().ok_or_else(|| {
725 Error::from(ParseError::invalid_format(
726 "data",
727 "Expected array of 24hr stats",
728 ))
729 })?;
730
731 let mut stats = Vec::new();
732 for stats_data in stats_array {
733 match parser::parse_stats_24hr(stats_data) {
734 Ok(stat) => stats.push(stat),
735 Err(e) => {
736 warn!(error = %e, "Failed to parse 24hr stats");
737 }
738 }
739 }
740 stats
741 } else {
742 vec![parser::parse_stats_24hr(&data)?]
743 };
744
745 Ok(stats_vec)
746 }
747
748 /// Fetch trading limits information for a symbol.
749 ///
750 /// # Arguments
751 ///
752 /// * `symbol` - Trading pair symbol.
753 ///
754 /// # Returns
755 ///
756 /// Returns [`TradingLimits`] containing minimum/maximum order constraints.
757 ///
758 /// # Errors
759 ///
760 /// Returns an error if the market is not found or the API request fails.
761 ///
762 /// # Example
763 ///
764 /// ```no_run
765 /// # use ccxt_exchanges::binance::Binance;
766 /// # use ccxt_core::ExchangeConfig;
767 /// # async fn example() -> ccxt_core::Result<()> {
768 /// # let binance = Binance::new(ExchangeConfig::default())?;
769 /// let limits = binance.fetch_trading_limits("BTC/USDT").await?;
770 /// println!("Min amount: {:?}", limits.amount.min);
771 /// println!("Max amount: {:?}", limits.amount.max);
772 /// # Ok(())
773 /// # }
774 /// ```
775 pub async fn fetch_trading_limits(&self, symbol: &str) -> Result<TradingLimits> {
776 let market = self.base().market(symbol).await?;
777
778 let url = format!("{}/exchangeInfo?symbol={}", self.urls().public, market.id);
779 let data = self.base().http_client.get(&url, None).await?;
780
781 let symbols_array = data["symbols"].as_array().ok_or_else(|| {
782 Error::from(ParseError::invalid_format("data", "Expected symbols array"))
783 })?;
784
785 if symbols_array.is_empty() {
786 return Err(Error::from(ParseError::invalid_format(
787 "data",
788 format!("No symbol info found for {}", symbol),
789 )));
790 }
791
792 let symbol_data = &symbols_array[0];
793
794 parser::parse_trading_limits(symbol_data, market.symbol.clone())
795 }
796
797 /// Create a new order.
798 ///
799 /// # Arguments
800 ///
801 /// * `symbol` - Trading pair symbol.
802 /// * `order_type` - Order type (Market, Limit, StopLoss, etc.).
803 /// * `side` - Order side (Buy or Sell).
804 /// * `amount` - Order quantity.
805 /// * `price` - Optional price (required for limit orders).
806 /// * `params` - Additional parameters.
807 ///
808 /// # Returns
809 ///
810 /// Returns the created [`Order`] structure with order details.
811 ///
812 /// # Errors
813 ///
814 /// Returns an error if authentication fails, market is not found, or the API request fails.
815 pub async fn create_order(
816 &self,
817 symbol: &str,
818 order_type: OrderType,
819 side: OrderSide,
820 amount: f64,
821 price: Option<f64>,
822 params: Option<HashMap<String, String>>,
823 ) -> Result<Order> {
824 self.check_required_credentials()?;
825
826 let market = self.base().market(symbol).await?;
827 let mut request_params = HashMap::new();
828
829 request_params.insert("symbol".to_string(), market.id.clone());
830 request_params.insert(
831 "side".to_string(),
832 match side {
833 OrderSide::Buy => "BUY".to_string(),
834 OrderSide::Sell => "SELL".to_string(),
835 },
836 );
837 request_params.insert(
838 "type".to_string(),
839 match order_type {
840 OrderType::Market => "MARKET".to_string(),
841 OrderType::Limit => "LIMIT".to_string(),
842 OrderType::StopLoss => "STOP_LOSS".to_string(),
843 OrderType::StopLossLimit => "STOP_LOSS_LIMIT".to_string(),
844 OrderType::TakeProfit => "TAKE_PROFIT".to_string(),
845 OrderType::TakeProfitLimit => "TAKE_PROFIT_LIMIT".to_string(),
846 OrderType::LimitMaker => "LIMIT_MAKER".to_string(),
847 OrderType::StopMarket => "STOP_MARKET".to_string(),
848 OrderType::StopLimit => "STOP_LIMIT".to_string(),
849 OrderType::TrailingStop => "TRAILING_STOP_MARKET".to_string(),
850 },
851 );
852 request_params.insert("quantity".to_string(), amount.to_string());
853
854 if let Some(p) = price {
855 request_params.insert("price".to_string(), p.to_string());
856 }
857
858 // Limit orders require timeInForce parameter
859 if order_type == OrderType::Limit
860 || order_type == OrderType::StopLossLimit
861 || order_type == OrderType::TakeProfitLimit
862 {
863 if !request_params.contains_key("timeInForce") {
864 request_params.insert("timeInForce".to_string(), "GTC".to_string());
865 }
866 }
867
868 if let Some(extra) = params {
869 for (k, v) in extra {
870 request_params.insert(k, v);
871 }
872 }
873
874 // Handle cost parameter for market buy orders (quoteOrderQty)
875 // Convert cost parameter to Binance API's quoteOrderQty
876 if order_type == OrderType::Market && side == OrderSide::Buy {
877 if let Some(cost_str) = request_params.get("cost") {
878 request_params.insert("quoteOrderQty".to_string(), cost_str.clone());
879 // Remove quantity parameter (not needed with quoteOrderQty)
880 request_params.remove("quantity");
881 // Remove cost parameter (Binance API doesn't recognize it)
882 request_params.remove("cost");
883 }
884 }
885
886 // Handle conditional order parameters
887 // stopPrice: trigger price for stop-loss/take-profit orders
888 if matches!(
889 order_type,
890 OrderType::StopLoss
891 | OrderType::StopLossLimit
892 | OrderType::TakeProfit
893 | OrderType::TakeProfitLimit
894 | OrderType::StopMarket
895 ) {
896 // Use stopLossPrice or takeProfitPrice if stopPrice not provided
897 if !request_params.contains_key("stopPrice") {
898 if let Some(stop_loss) = request_params.get("stopLossPrice") {
899 request_params.insert("stopPrice".to_string(), stop_loss.clone());
900 } else if let Some(take_profit) = request_params.get("takeProfitPrice") {
901 request_params.insert("stopPrice".to_string(), take_profit.clone());
902 }
903 }
904 }
905
906 // trailingDelta: price offset for trailing stop (basis points)
907 // Spot market: requires trailingDelta parameter
908 // Futures market: uses callbackRate parameter
909 if order_type == OrderType::TrailingStop {
910 if market.is_spot() {
911 // Spot trailing stop: use trailingDelta
912 if !request_params.contains_key("trailingDelta") {
913 // Convert trailingPercent to trailingDelta (basis points) if provided
914 if let Some(percent_str) = request_params.get("trailingPercent") {
915 if let Ok(percent) = percent_str.parse::<f64>() {
916 let delta = (percent * 100.0) as i64;
917 request_params.insert("trailingDelta".to_string(), delta.to_string());
918 request_params.remove("trailingPercent");
919 }
920 }
921 }
922 } else if market.is_swap() || market.is_futures() {
923 // Futures trailing stop: use callbackRate
924 if !request_params.contains_key("callbackRate") {
925 if let Some(percent_str) = request_params.get("trailingPercent") {
926 request_params.insert("callbackRate".to_string(), percent_str.clone());
927 request_params.remove("trailingPercent");
928 }
929 }
930 }
931 }
932
933 // Futures order advanced parameters (Stage 24.3)
934 if market.is_swap() || market.is_futures() {
935 // reduceOnly: reduce-only flag (only reduces existing position, won't reverse)
936 if let Some(reduce_only) = request_params.get("reduceOnly") {
937 // Keep original value, Binance API accepts "true" or "false" strings
938 request_params.insert("reduceOnly".to_string(), reduce_only.clone());
939 }
940
941 // postOnly: maker-only flag (ensures order won't execute immediately)
942 if let Some(post_only) = request_params.get("postOnly") {
943 request_params.insert("postOnly".to_string(), post_only.clone());
944 }
945
946 // positionSide: position direction (LONG/SHORT/BOTH)
947 // Required in hedge mode, defaults to BOTH in one-way mode
948 if let Some(position_side) = request_params.get("positionSide") {
949 request_params.insert("positionSide".to_string(), position_side.clone());
950 }
951
952 // closePosition: close all positions flag (market orders only)
953 // When true, closes all positions; quantity can be omitted
954 if let Some(close_position) = request_params.get("closePosition") {
955 if order_type == OrderType::Market {
956 request_params.insert("closePosition".to_string(), close_position.clone());
957 }
958 }
959 }
960
961 let timestamp = self.fetch_time_raw().await?;
962 let auth = self.get_auth()?;
963 let signed_params =
964 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
965
966 let url = format!("{}/order", self.urls().private);
967 let mut headers = HeaderMap::new();
968 auth.add_auth_headers_reqwest(&mut headers);
969
970 let body = serde_json::to_value(&signed_params).map_err(|e| {
971 Error::from(ParseError::invalid_format(
972 "data",
973 format!("Failed to serialize params: {}", e),
974 ))
975 })?;
976
977 let data = self
978 .base()
979 .http_client
980 .post(&url, Some(headers), Some(body))
981 .await?;
982
983 parser::parse_order(&data, Some(&market))
984 }
985
986 /// Cancel an order.
987 ///
988 /// # Arguments
989 ///
990 /// * `id` - Order ID.
991 /// * `symbol` - Trading pair symbol.
992 ///
993 /// # Returns
994 ///
995 /// Returns the cancelled [`Order`] information.
996 ///
997 /// # Errors
998 ///
999 /// Returns an error if authentication fails, market is not found, or the API request fails.
1000 pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
1001 self.check_required_credentials()?;
1002
1003 let market = self.base().market(symbol).await?;
1004 let mut params = HashMap::new();
1005 params.insert("symbol".to_string(), market.id.clone());
1006 params.insert("orderId".to_string(), id.to_string());
1007
1008 let timestamp = self.fetch_time_raw().await?;
1009 let auth = self.get_auth()?;
1010 let signed_params =
1011 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
1012
1013 let url = format!("{}/order", self.urls().private);
1014 let mut headers = HeaderMap::new();
1015 auth.add_auth_headers_reqwest(&mut headers);
1016
1017 let body = serde_json::to_value(&signed_params).map_err(|e| {
1018 Error::from(ParseError::invalid_format(
1019 "data",
1020 format!("Failed to serialize params: {}", e),
1021 ))
1022 })?;
1023
1024 let data = self
1025 .base()
1026 .http_client
1027 .delete(&url, Some(headers), Some(body))
1028 .await?;
1029
1030 parser::parse_order(&data, Some(&market))
1031 }
1032
1033 /// Fetch order details.
1034 ///
1035 /// # Arguments
1036 ///
1037 /// * `id` - Order ID.
1038 /// * `symbol` - Trading pair symbol.
1039 ///
1040 /// # Returns
1041 ///
1042 /// Returns the [`Order`] information.
1043 ///
1044 /// # Errors
1045 ///
1046 /// Returns an error if authentication fails, market is not found, or the API request fails.
1047 pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
1048 self.check_required_credentials()?;
1049
1050 let market = self.base().market(symbol).await?;
1051 let mut params = HashMap::new();
1052 params.insert("symbol".to_string(), market.id.clone());
1053 params.insert("orderId".to_string(), id.to_string());
1054
1055 let timestamp = self.fetch_time_raw().await?;
1056 let auth = self.get_auth()?;
1057 let signed_params =
1058 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
1059
1060 let mut url = format!("{}/order?", self.urls().private);
1061 for (key, value) in &signed_params {
1062 url.push_str(&format!("{}={}&", key, value));
1063 }
1064
1065 let mut headers = HeaderMap::new();
1066 auth.add_auth_headers_reqwest(&mut headers);
1067
1068 let data = self.base().http_client.get(&url, Some(headers)).await?;
1069
1070 parser::parse_order(&data, Some(&market))
1071 }
1072
1073 /// Fetch open (unfilled) orders.
1074 ///
1075 /// # Arguments
1076 ///
1077 /// * `symbol` - Optional trading pair symbol. If `None`, fetches all open orders.
1078 ///
1079 /// # Returns
1080 ///
1081 /// Returns a vector of open [`Order`] structures.
1082 ///
1083 /// # Errors
1084 ///
1085 /// Returns an error if authentication fails or the API request fails.
1086 pub async fn fetch_open_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
1087 self.check_required_credentials()?;
1088
1089 let mut params = HashMap::new();
1090 let market = if let Some(sym) = symbol {
1091 let m = self.base().market(sym).await?;
1092 params.insert("symbol".to_string(), m.id.clone());
1093 Some(m)
1094 } else {
1095 None
1096 };
1097
1098 let timestamp = self.fetch_time_raw().await?;
1099 let auth = self.get_auth()?;
1100 let signed_params =
1101 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
1102
1103 let mut url = format!("{}/openOrders?", self.urls().private);
1104 for (key, value) in &signed_params {
1105 url.push_str(&format!("{}={}&", key, value));
1106 }
1107
1108 let mut headers = HeaderMap::new();
1109 auth.add_auth_headers_reqwest(&mut headers);
1110
1111 let data = self.base().http_client.get(&url, Some(headers)).await?;
1112
1113 let orders_array = data.as_array().ok_or_else(|| {
1114 Error::from(ParseError::invalid_format(
1115 "data",
1116 "Expected array of orders",
1117 ))
1118 })?;
1119
1120 let mut orders = Vec::new();
1121 for order_data in orders_array {
1122 match parser::parse_order(order_data, market.as_ref()) {
1123 Ok(order) => orders.push(order),
1124 Err(e) => {
1125 warn!(error = %e, "Failed to parse order");
1126 }
1127 }
1128 }
1129
1130 Ok(orders)
1131 }
1132 /// Create a stop-loss order.
1133 ///
1134 /// # Arguments
1135 ///
1136 /// * `symbol` - Trading pair symbol.
1137 /// * `side` - Order side (Buy/Sell).
1138 /// * `amount` - Order quantity.
1139 /// * `stop_price` - Stop-loss trigger price.
1140 /// * `price` - Optional limit price (if `None`, creates market stop-loss order).
1141 /// * `params` - Optional additional parameters.
1142 ///
1143 /// # Returns
1144 ///
1145 /// Returns the created stop-loss [`Order`].
1146 ///
1147 /// # Errors
1148 ///
1149 /// Returns an error if authentication fails or the API request fails.
1150 ///
1151 /// # Example
1152 ///
1153 /// ```no_run
1154 /// # use ccxt_exchanges::binance::Binance;
1155 /// # use ccxt_core::ExchangeConfig;
1156 /// # use ccxt_core::types::{OrderSide, OrderType};
1157 /// # async fn example() -> ccxt_core::Result<()> {
1158 /// # let exchange = Binance::new(ExchangeConfig::default())?;
1159 /// // Create market stop-loss order
1160 /// let order = exchange.create_stop_loss_order(
1161 /// "BTC/USDT",
1162 /// OrderSide::Sell,
1163 /// 0.1,
1164 /// 45000.0,
1165 /// None,
1166 /// None
1167 /// ).await?;
1168 ///
1169 /// // Create limit stop-loss order
1170 /// let order = exchange.create_stop_loss_order(
1171 /// "BTC/USDT",
1172 /// OrderSide::Sell,
1173 /// 0.1,
1174 /// 45000.0,
1175 /// Some(44900.0),
1176 /// None
1177 /// ).await?;
1178 /// # Ok(())
1179 /// # }
1180 /// ```
1181 pub async fn create_stop_loss_order(
1182 &self,
1183 symbol: &str,
1184 side: OrderSide,
1185 amount: f64,
1186 stop_price: f64,
1187 price: Option<f64>,
1188 params: Option<HashMap<String, String>>,
1189 ) -> Result<Order> {
1190 let mut request_params = params.unwrap_or_default();
1191
1192 request_params.insert("stopPrice".to_string(), stop_price.to_string());
1193
1194 let order_type = if price.is_some() {
1195 OrderType::StopLossLimit
1196 } else {
1197 OrderType::StopLoss
1198 };
1199
1200 self.create_order(
1201 symbol,
1202 order_type,
1203 side,
1204 amount,
1205 price,
1206 Some(request_params),
1207 )
1208 .await
1209 }
1210
1211 /// Create a take-profit order.
1212 ///
1213 /// # Arguments
1214 ///
1215 /// * `symbol` - Trading pair symbol.
1216 /// * `side` - Order side (Buy/Sell).
1217 /// * `amount` - Order quantity.
1218 /// * `take_profit_price` - Take-profit trigger price.
1219 /// * `price` - Optional limit price (if `None`, creates market take-profit order).
1220 /// * `params` - Optional additional parameters.
1221 ///
1222 /// # Returns
1223 ///
1224 /// Returns the created take-profit [`Order`].
1225 ///
1226 /// # Errors
1227 ///
1228 /// Returns an error if authentication fails or the API request fails.
1229 ///
1230 /// # Example
1231 ///
1232 /// ```no_run
1233 /// # use ccxt_exchanges::binance::Binance;
1234 /// # use ccxt_core::ExchangeConfig;
1235 /// # use ccxt_core::types::{OrderSide, OrderType};
1236 /// # async fn example() -> ccxt_core::Result<()> {
1237 /// # let exchange = Binance::new(ExchangeConfig::default())?;
1238 /// // Create market take-profit order
1239 /// let order = exchange.create_take_profit_order(
1240 /// "BTC/USDT",
1241 /// OrderSide::Sell,
1242 /// 0.1,
1243 /// 55000.0,
1244 /// None,
1245 /// None
1246 /// ).await?;
1247 ///
1248 /// // Create limit take-profit order
1249 /// let order = exchange.create_take_profit_order(
1250 /// "BTC/USDT",
1251 /// OrderSide::Sell,
1252 /// 0.1,
1253 /// 55000.0,
1254 /// Some(55100.0),
1255 /// None
1256 /// ).await?;
1257 /// # Ok(())
1258 /// # }
1259 /// ```
1260 pub async fn create_take_profit_order(
1261 &self,
1262 symbol: &str,
1263 side: OrderSide,
1264 amount: f64,
1265 take_profit_price: f64,
1266 price: Option<f64>,
1267 params: Option<HashMap<String, String>>,
1268 ) -> Result<Order> {
1269 let mut request_params = params.unwrap_or_default();
1270
1271 request_params.insert("stopPrice".to_string(), take_profit_price.to_string());
1272
1273 let order_type = if price.is_some() {
1274 OrderType::TakeProfitLimit
1275 } else {
1276 OrderType::TakeProfit
1277 };
1278
1279 self.create_order(
1280 symbol,
1281 order_type,
1282 side,
1283 amount,
1284 price,
1285 Some(request_params),
1286 )
1287 .await
1288 }
1289
1290 /// Create a trailing stop order.
1291 ///
1292 /// # Arguments
1293 ///
1294 /// * `symbol` - Trading pair symbol.
1295 /// * `side` - Order side (Buy/Sell).
1296 /// * `amount` - Order quantity.
1297 /// * `trailing_percent` - Trailing percentage (e.g., 2.0 for 2%).
1298 /// * `activation_price` - Optional activation price (not supported for spot markets).
1299 /// * `params` - Optional additional parameters.
1300 ///
1301 /// # Returns
1302 ///
1303 /// Returns the created trailing stop [`Order`].
1304 ///
1305 /// # Errors
1306 ///
1307 /// Returns an error if authentication fails or the API request fails.
1308 ///
1309 /// # Example
1310 ///
1311 /// ```no_run
1312 /// # use ccxt_exchanges::binance::Binance;
1313 /// # use ccxt_core::ExchangeConfig;
1314 /// # use ccxt_core::types::{OrderSide, OrderType};
1315 /// # async fn example() -> ccxt_core::Result<()> {
1316 /// # let exchange = Binance::new(ExchangeConfig::default())?;
1317 /// // Spot market: create trailing stop order with 2% trail
1318 /// let order = exchange.create_trailing_stop_order(
1319 /// "BTC/USDT",
1320 /// OrderSide::Sell,
1321 /// 0.1,
1322 /// 2.0,
1323 /// None,
1324 /// None
1325 /// ).await?;
1326 ///
1327 /// // Futures market: create trailing stop order with 1.5% trail, activation price 50000
1328 /// let order = exchange.create_trailing_stop_order(
1329 /// "BTC/USDT:USDT",
1330 /// OrderSide::Sell,
1331 /// 0.1,
1332 /// 1.5,
1333 /// Some(50000.0),
1334 /// None
1335 /// ).await?;
1336 /// # Ok(())
1337 /// # }
1338 /// ```
1339 pub async fn create_trailing_stop_order(
1340 &self,
1341 symbol: &str,
1342 side: OrderSide,
1343 amount: f64,
1344 trailing_percent: f64,
1345 activation_price: Option<f64>,
1346 params: Option<HashMap<String, String>>,
1347 ) -> Result<Order> {
1348 let mut request_params = params.unwrap_or_default();
1349
1350 request_params.insert("trailingPercent".to_string(), trailing_percent.to_string());
1351
1352 if let Some(activation) = activation_price {
1353 request_params.insert("activationPrice".to_string(), activation.to_string());
1354 }
1355
1356 self.create_order(
1357 symbol,
1358 OrderType::TrailingStop,
1359 side,
1360 amount,
1361 None,
1362 Some(request_params),
1363 )
1364 .await
1365 }
1366
1367 /// Fetch account balance.
1368 ///
1369 /// # Returns
1370 ///
1371 /// Returns the account [`Balance`] information.
1372 ///
1373 /// # Errors
1374 ///
1375 /// Returns an error if authentication fails or the API request fails.
1376 pub async fn fetch_balance_simple(&self) -> Result<Balance> {
1377 self.check_required_credentials()?;
1378
1379 let params = HashMap::new();
1380 let timestamp = self.fetch_time_raw().await?;
1381 let auth = self.get_auth()?;
1382 let signed_params =
1383 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
1384
1385 let mut url = format!("{}/account?", self.urls().private);
1386 for (key, value) in &signed_params {
1387 url.push_str(&format!("{}={}&", key, value));
1388 }
1389
1390 let mut headers = HeaderMap::new();
1391 auth.add_auth_headers_reqwest(&mut headers);
1392
1393 let data = self.base().http_client.get(&url, Some(headers)).await?;
1394
1395 parser::parse_balance(&data)
1396 }
1397
1398 /// Fetch user's trade history.
1399 ///
1400 /// # Arguments
1401 ///
1402 /// * `symbol` - Trading pair symbol.
1403 /// * `since` - Optional start timestamp.
1404 /// * `limit` - Optional limit on number of trades.
1405 ///
1406 /// # Returns
1407 ///
1408 /// Returns a vector of [`Trade`] structures for the user's trades.
1409 ///
1410 /// # Errors
1411 ///
1412 /// Returns an error if authentication fails, market is not found, or the API request fails.
1413 pub async fn fetch_my_trades(
1414 &self,
1415 symbol: &str,
1416 since: Option<u64>,
1417 limit: Option<u32>,
1418 ) -> Result<Vec<Trade>> {
1419 self.check_required_credentials()?;
1420
1421 let market = self.base().market(symbol).await?;
1422 let mut params = HashMap::new();
1423 params.insert("symbol".to_string(), market.id.clone());
1424
1425 if let Some(s) = since {
1426 params.insert("startTime".to_string(), s.to_string());
1427 }
1428
1429 if let Some(l) = limit {
1430 params.insert("limit".to_string(), l.to_string());
1431 }
1432
1433 let timestamp = self.fetch_time_raw().await?;
1434 let auth = self.get_auth()?;
1435 let signed_params =
1436 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
1437
1438 let mut url = format!("{}/myTrades?", self.urls().private);
1439 for (key, value) in &signed_params {
1440 url.push_str(&format!("{}={}&", key, value));
1441 }
1442
1443 let mut headers = HeaderMap::new();
1444 auth.add_auth_headers_reqwest(&mut headers);
1445
1446 let data = self.base().http_client.get(&url, Some(headers)).await?;
1447
1448 let trades_array = data.as_array().ok_or_else(|| {
1449 Error::from(ParseError::invalid_format(
1450 "data",
1451 "Expected array of trades",
1452 ))
1453 })?;
1454
1455 let mut trades = Vec::new();
1456
1457 for trade_data in trades_array {
1458 match parser::parse_trade(trade_data, Some(&market)) {
1459 Ok(trade) => trades.push(trade),
1460 Err(e) => {
1461 warn!(error = %e, "Failed to parse trade");
1462 }
1463 }
1464 }
1465
1466 Ok(trades)
1467 }
1468
1469 /// Fetch all currency information.
1470 ///
1471 /// # Returns
1472 ///
1473 /// Returns a vector of [`Currency`] structures.
1474 ///
1475 /// # Errors
1476 ///
1477 /// Returns an error if authentication fails or the API request fails.
1478 ///
1479 /// # Example
1480 ///
1481 /// ```no_run
1482 /// # use ccxt_exchanges::binance::Binance;
1483 /// # use ccxt_core::ExchangeConfig;
1484 /// # async fn example() -> ccxt_core::Result<()> {
1485 /// # let binance = Binance::new(ExchangeConfig::default())?;
1486 /// let currencies = binance.fetch_currencies().await?;
1487 /// for currency in ¤cies {
1488 /// println!("{}: {} - {}", currency.code, currency.name, currency.active);
1489 /// }
1490 /// # Ok(())
1491 /// # }
1492 /// ```
1493 pub async fn fetch_currencies(&self) -> Result<Vec<Currency>> {
1494 let url = format!("{}/capital/config/getall", self.urls().sapi);
1495
1496 // Private API requires signature
1497 self.check_required_credentials()?;
1498
1499 let params = HashMap::new();
1500 let timestamp = self.fetch_time_raw().await?;
1501 let auth = self.get_auth()?;
1502 let signed_params =
1503 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
1504
1505 let mut request_url = format!("{}?", url);
1506 for (key, value) in &signed_params {
1507 request_url.push_str(&format!("{}={}&", key, value));
1508 }
1509
1510 let mut headers = HeaderMap::new();
1511 auth.add_auth_headers_reqwest(&mut headers);
1512
1513 let data = self
1514 .base()
1515 .http_client
1516 .get(&request_url, Some(headers))
1517 .await?;
1518
1519 parser::parse_currencies(&data)
1520 }
1521
1522 /// Fetch closed (completed) orders.
1523 ///
1524 /// # Arguments
1525 ///
1526 /// * `symbol` - Optional trading pair symbol.
1527 /// * `since` - Optional start timestamp (milliseconds).
1528 /// * `limit` - Optional limit on number of orders (default 500, max 1000).
1529 ///
1530 /// # Returns
1531 ///
1532 /// Returns a vector of closed [`Order`] structures.
1533 ///
1534 /// # Errors
1535 ///
1536 /// Returns an error if authentication fails or the API request fails.
1537 ///
1538 /// # Note
1539 ///
1540 /// This method calls `fetch_orders` to get all orders, then filters for "closed" status.
1541 /// This matches the Go version's implementation logic.
1542 pub async fn fetch_closed_orders(
1543 &self,
1544 symbol: Option<&str>,
1545 since: Option<u64>,
1546 limit: Option<u32>,
1547 ) -> Result<Vec<Order>> {
1548 let all_orders = self.fetch_orders(symbol, since, None).await?;
1549
1550 let mut closed_orders: Vec<Order> = all_orders
1551 .into_iter()
1552 .filter(|order| order.status == OrderStatus::Closed)
1553 .collect();
1554
1555 if let Some(l) = limit {
1556 closed_orders.truncate(l as usize);
1557 }
1558
1559 Ok(closed_orders)
1560 }
1561
1562 /// Cancel all open orders.
1563 ///
1564 /// # Arguments
1565 ///
1566 /// * `symbol` - Trading pair symbol.
1567 ///
1568 /// # Returns
1569 ///
1570 /// Returns a vector of cancelled [`Order`] structures.
1571 ///
1572 /// # Errors
1573 ///
1574 /// Returns an error if authentication fails, market is not found, or the API request fails.
1575 pub async fn cancel_all_orders(&self, symbol: &str) -> Result<Vec<Order>> {
1576 self.check_required_credentials()?;
1577
1578 let market = self.base().market(symbol).await?;
1579 let mut params = HashMap::new();
1580 params.insert("symbol".to_string(), market.id.clone());
1581
1582 let timestamp = self.fetch_time_raw().await?;
1583 let auth = self.get_auth()?;
1584 let signed_params =
1585 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
1586
1587 let url = format!("{}/openOrders", self.urls().private);
1588 let mut headers = HeaderMap::new();
1589 auth.add_auth_headers_reqwest(&mut headers);
1590
1591 let body = serde_json::to_value(&signed_params).map_err(|e| {
1592 Error::from(ParseError::invalid_format(
1593 "data",
1594 format!("Failed to serialize params: {}", e),
1595 ))
1596 })?;
1597
1598 let data = self
1599 .base()
1600 .http_client
1601 .delete(&url, Some(headers), Some(body))
1602 .await?;
1603
1604 let orders_array = data.as_array().ok_or_else(|| {
1605 Error::from(ParseError::invalid_format(
1606 "data",
1607 "Expected array of orders",
1608 ))
1609 })?;
1610
1611 let mut orders = Vec::new();
1612 for order_data in orders_array {
1613 match parser::parse_order(order_data, Some(&market)) {
1614 Ok(order) => orders.push(order),
1615 Err(e) => {
1616 warn!(error = %e, "Failed to parse order");
1617 }
1618 }
1619 }
1620
1621 Ok(orders)
1622 }
1623
1624 /// Cancel multiple orders.
1625 ///
1626 /// # Arguments
1627 ///
1628 /// * `ids` - Vector of order IDs to cancel.
1629 /// * `symbol` - Trading pair symbol.
1630 ///
1631 /// # Returns
1632 ///
1633 /// Returns a vector of cancelled [`Order`] structures.
1634 ///
1635 /// # Errors
1636 ///
1637 /// Returns an error if authentication fails, market is not found, or the API request fails.
1638 pub async fn cancel_orders(&self, ids: Vec<String>, symbol: &str) -> Result<Vec<Order>> {
1639 self.check_required_credentials()?;
1640
1641 let market = self.base().market(symbol).await?;
1642
1643 // Binance supports batch cancellation using orderIdList parameter
1644 let mut params = HashMap::new();
1645 params.insert("symbol".to_string(), market.id.clone());
1646
1647 let order_ids_json = serde_json::to_string(&ids).map_err(|e| {
1648 Error::from(ParseError::invalid_format(
1649 "data",
1650 format!("Failed to serialize order IDs: {}", e),
1651 ))
1652 })?;
1653 params.insert("orderIdList".to_string(), order_ids_json);
1654
1655 let timestamp = self.fetch_time_raw().await?;
1656 let auth = self.get_auth()?;
1657 let signed_params =
1658 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
1659
1660 let url = format!("{}/openOrders", self.urls().private);
1661 let mut headers = HeaderMap::new();
1662 auth.add_auth_headers_reqwest(&mut headers);
1663
1664 let body = serde_json::to_value(&signed_params).map_err(|e| {
1665 Error::from(ParseError::invalid_format(
1666 "data",
1667 format!("Failed to serialize params: {}", e),
1668 ))
1669 })?;
1670
1671 let data = self
1672 .base()
1673 .http_client
1674 .delete(&url, Some(headers), Some(body))
1675 .await?;
1676
1677 let orders_array = data.as_array().ok_or_else(|| {
1678 Error::from(ParseError::invalid_format(
1679 "data",
1680 "Expected array of orders",
1681 ))
1682 })?;
1683
1684 let mut orders = Vec::new();
1685 for order_data in orders_array {
1686 match parser::parse_order(order_data, Some(&market)) {
1687 Ok(order) => orders.push(order),
1688 Err(e) => {
1689 warn!(error = %e, "Failed to parse order");
1690 }
1691 }
1692 }
1693
1694 Ok(orders)
1695 }
1696
1697 /// Fetch all orders (historical and current).
1698 ///
1699 /// # Arguments
1700 ///
1701 /// * `symbol` - Optional trading pair symbol.
1702 /// * `since` - Optional start timestamp (milliseconds).
1703 /// * `limit` - Optional limit on number of orders (default 500, max 1000).
1704 ///
1705 /// # Returns
1706 ///
1707 /// Returns a vector of all [`Order`] structures.
1708 ///
1709 /// # Errors
1710 ///
1711 /// Returns an error if authentication fails or the API request fails.
1712 pub async fn fetch_orders(
1713 &self,
1714 symbol: Option<&str>,
1715 since: Option<u64>,
1716 limit: Option<u32>,
1717 ) -> Result<Vec<Order>> {
1718 self.check_required_credentials()?;
1719
1720 let mut params = HashMap::new();
1721 let market = if let Some(sym) = symbol {
1722 let m = self.base().market(sym).await?;
1723 params.insert("symbol".to_string(), m.id.clone());
1724 Some(m)
1725 } else {
1726 None
1727 };
1728
1729 if let Some(s) = since {
1730 params.insert("startTime".to_string(), s.to_string());
1731 }
1732
1733 if let Some(l) = limit {
1734 params.insert("limit".to_string(), l.to_string());
1735 }
1736
1737 let timestamp = self.fetch_time_raw().await?;
1738 let auth = self.get_auth()?;
1739 let signed_params =
1740 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
1741
1742 let mut url = format!("{}/allOrders?", self.urls().private);
1743 for (key, value) in &signed_params {
1744 url.push_str(&format!("{}={}&", key, value));
1745 }
1746
1747 let mut headers = HeaderMap::new();
1748 auth.add_auth_headers_reqwest(&mut headers);
1749
1750 let data = self.base().http_client.get(&url, Some(headers)).await?;
1751
1752 let orders_array = data.as_array().ok_or_else(|| {
1753 Error::from(ParseError::invalid_format(
1754 "data",
1755 "Expected array of orders",
1756 ))
1757 })?;
1758
1759 let mut orders = Vec::new();
1760 for order_data in orders_array {
1761 match parser::parse_order(order_data, market.as_ref()) {
1762 Ok(order) => orders.push(order),
1763 Err(e) => {
1764 warn!(error = %e, "Failed to parse order");
1765 }
1766 }
1767 }
1768
1769 Ok(orders)
1770 }
1771
1772 /// Check required authentication credentials.
1773 fn check_required_credentials(&self) -> Result<()> {
1774 if self.base().config.api_key.is_none() || self.base().config.secret.is_none() {
1775 return Err(Error::authentication("API key and secret are required"));
1776 }
1777 Ok(())
1778 }
1779
1780 /// Get authenticator instance.
1781 fn get_auth(&self) -> Result<BinanceAuth> {
1782 let api_key = self
1783 .base()
1784 .config
1785 .api_key
1786 .as_ref()
1787 .ok_or_else(|| Error::authentication("Missing API key"))?;
1788 let secret = self
1789 .base()
1790 .config
1791 .secret
1792 .as_ref()
1793 .ok_or_else(|| Error::authentication("Missing secret"))?;
1794
1795 Ok(BinanceAuth::new(api_key, secret))
1796 }
1797 /// Fetch funding rate for a single trading pair.
1798 ///
1799 /// # Arguments
1800 ///
1801 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT:USDT").
1802 /// * `params` - Optional parameters.
1803 ///
1804 /// # Returns
1805 ///
1806 /// Returns the [`FundingRate`] information.
1807 ///
1808 /// # Errors
1809 ///
1810 /// Returns an error if the API request fails.
1811 ///
1812 /// # Example
1813 ///
1814 /// ```no_run
1815 /// # use ccxt_exchanges::binance::Binance;
1816 /// # use ccxt_core::ExchangeConfig;
1817 /// # async fn example() -> ccxt_core::Result<()> {
1818 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
1819 /// let funding_rate = binance.fetch_funding_rate("BTC/USDT:USDT", None).await?;
1820 /// # Ok(())
1821 /// # }
1822 /// ```
1823 /// Fetch funding rates for multiple trading pairs.
1824 ///
1825 /// # Arguments
1826 ///
1827 /// * `symbols` - Optional vector of trading pair symbols, fetches all if `None`.
1828 /// * `params` - Optional parameters.
1829 ///
1830 /// # Returns
1831 ///
1832 /// Returns a vector of [`FundingRate`] structures.
1833 ///
1834 /// # Errors
1835 ///
1836 /// Returns an error if the API request fails.
1837 ///
1838 /// # Example
1839 ///
1840 /// ```no_run
1841 /// # use ccxt_exchanges::binance::Binance;
1842 /// # use ccxt_core::ExchangeConfig;
1843 /// # async fn example() -> ccxt_core::Result<()> {
1844 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
1845 /// let funding_rates = binance.fetch_funding_rates(None, None).await?;
1846 /// # Ok(())
1847 /// # }
1848 /// ```
1849 /// Fetch funding rate history.
1850 ///
1851 /// # Arguments
1852 ///
1853 /// * `symbol` - Optional trading pair symbol.
1854 /// * `since` - Optional start timestamp (milliseconds).
1855 /// * `limit` - Optional limit on number of records (default 100, max 1000).
1856 /// * `params` - Optional parameters.
1857 ///
1858 /// # Returns
1859 ///
1860 /// Returns a vector of historical [`FundingRate`] structures.
1861 ///
1862 /// # Errors
1863 ///
1864 /// Returns an error if the API request fails.
1865 ///
1866 /// # Example
1867 ///
1868 /// ```no_run
1869 /// # use ccxt_exchanges::binance::Binance;
1870 /// # use ccxt_core::ExchangeConfig;
1871 /// # async fn example() -> ccxt_core::Result<()> {
1872 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
1873 /// let history = binance.fetch_funding_rate_history(
1874 /// Some("BTC/USDT:USDT"),
1875 /// None,
1876 /// Some(100),
1877 /// None
1878 /// ).await?;
1879 /// # Ok(())
1880 /// # }
1881 /// ```
1882 /// Fetch position for a single trading pair.
1883 ///
1884 /// # Arguments
1885 ///
1886 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT:USDT").
1887 /// * `params` - Optional parameters.
1888 ///
1889 /// # Returns
1890 ///
1891 /// Returns the [`Position`] information.
1892 ///
1893 /// # Errors
1894 ///
1895 /// Returns an error if authentication fails, market is not found, or the API request fails.
1896 ///
1897 /// # Example
1898 ///
1899 /// ```no_run
1900 /// # use ccxt_exchanges::binance::Binance;
1901 /// # use ccxt_core::ExchangeConfig;
1902 /// # async fn example() -> ccxt_core::Result<()> {
1903 /// let mut config = ExchangeConfig::default();
1904 /// config.api_key = Some("your_api_key".to_string());
1905 /// config.secret = Some("your_secret".to_string());
1906 /// let binance = Binance::new_futures(config)?;
1907 /// let position = binance.fetch_position("BTC/USDT:USDT", None).await?;
1908 /// # Ok(())
1909 /// # }
1910 /// ```
1911 pub async fn fetch_position(
1912 &self,
1913 symbol: &str,
1914 params: Option<Value>,
1915 ) -> Result<ccxt_core::types::Position> {
1916 self.check_required_credentials()?;
1917
1918 let market = self.base().market(symbol).await?;
1919 let mut request_params = HashMap::new();
1920 request_params.insert("symbol".to_string(), market.id.clone());
1921
1922 if let Some(params) = params {
1923 if let Some(obj) = params.as_object() {
1924 for (key, value) in obj {
1925 if let Some(v) = value.as_str() {
1926 request_params.insert(key.clone(), v.to_string());
1927 }
1928 }
1929 }
1930 }
1931
1932 let timestamp = self.fetch_time_raw().await?;
1933 let auth = self.get_auth()?;
1934 let signed_params =
1935 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
1936
1937 // Determine API endpoint based on market type
1938 let url = if market.linear.unwrap_or(true) {
1939 format!("{}/positionRisk", self.urls().fapi_private)
1940 } else {
1941 format!("{}/positionRisk", self.urls().dapi_private)
1942 };
1943
1944 let mut request_url = format!("{}?", url);
1945 for (key, value) in &signed_params {
1946 request_url.push_str(&format!("{}={}&", key, value));
1947 }
1948
1949 let mut headers = HeaderMap::new();
1950 auth.add_auth_headers_reqwest(&mut headers);
1951
1952 let data = self
1953 .base()
1954 .http_client
1955 .get(&request_url, Some(headers))
1956 .await?;
1957
1958 // API returns array, find matching symbol
1959 let positions_array = data.as_array().ok_or_else(|| {
1960 Error::from(ParseError::invalid_format(
1961 "data",
1962 "Expected array of positions",
1963 ))
1964 })?;
1965
1966 for position_data in positions_array {
1967 if let Some(pos_symbol) = position_data["symbol"].as_str() {
1968 if pos_symbol == market.id {
1969 return parser::parse_position(position_data, Some(&market));
1970 }
1971 }
1972 }
1973
1974 Err(Error::from(ParseError::missing_field_owned(format!(
1975 "Position not found for symbol: {}",
1976 symbol
1977 ))))
1978 }
1979
1980 /// Fetch all positions.
1981 ///
1982 /// # Arguments
1983 ///
1984 /// * `symbols` - Optional vector of trading pair symbols.
1985 /// * `params` - Optional parameters.
1986 ///
1987 /// # Returns
1988 ///
1989 /// Returns a vector of [`Position`] structures.
1990 ///
1991 /// # Errors
1992 ///
1993 /// Returns an error if authentication fails or the API request fails.
1994 ///
1995 /// # Example
1996 ///
1997 /// ```no_run
1998 /// # use ccxt_exchanges::binance::Binance;
1999 /// # use ccxt_core::ExchangeConfig;
2000 /// # async fn example() -> ccxt_core::Result<()> {
2001 /// let mut config = ExchangeConfig::default();
2002 /// config.api_key = Some("your_api_key".to_string());
2003 /// config.secret = Some("your_secret".to_string());
2004 /// let binance = Binance::new_futures(config)?;
2005 /// let positions = binance.fetch_positions(None, None).await?;
2006 /// # Ok(())
2007 /// # }
2008 /// ```
2009 pub async fn fetch_positions(
2010 &self,
2011 symbols: Option<Vec<String>>,
2012 params: Option<Value>,
2013 ) -> Result<Vec<ccxt_core::types::Position>> {
2014 self.check_required_credentials()?;
2015
2016 let mut request_params = HashMap::new();
2017
2018 if let Some(ref params) = params {
2019 if let Some(obj) = params.as_object() {
2020 for (key, value) in obj {
2021 if let Some(v) = value.as_str() {
2022 request_params.insert(key.clone(), v.to_string());
2023 }
2024 }
2025 }
2026 }
2027
2028 let timestamp = self.fetch_time_raw().await?;
2029 let auth = self.get_auth()?;
2030 let signed_params =
2031 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
2032 let query_string = auth.build_query_string(&signed_params);
2033 // Default to USDT-M futures endpoint
2034 let use_coin_m = params
2035 .as_ref()
2036 .and_then(|p| p.get("type"))
2037 .and_then(|v| v.as_str())
2038 .map(|t| t == "delivery" || t == "coin_m")
2039 .unwrap_or(false);
2040
2041 let url = if use_coin_m {
2042 format!("{}/positionRisk", self.urls().dapi_private)
2043 } else {
2044 format!("{}/positionRisk", self.urls().fapi_private)
2045 };
2046
2047 let request_url = format!("{}?{}", url, query_string);
2048
2049 let mut headers = HeaderMap::new();
2050 auth.add_auth_headers_reqwest(&mut headers);
2051
2052 let data = self
2053 .base()
2054 .http_client
2055 .get(&request_url, Some(headers))
2056 .await?;
2057
2058 let positions_array = data.as_array().ok_or_else(|| {
2059 Error::from(ParseError::invalid_format(
2060 "data",
2061 "Expected array of positions",
2062 ))
2063 })?;
2064
2065 let mut positions = Vec::new();
2066 for position_data in positions_array {
2067 if let Some(binance_symbol) = position_data["symbol"].as_str() {
2068 let cache = self.base().market_cache.read().await;
2069 if let Some(market) = cache.markets_by_id.get(binance_symbol) {
2070 let market_clone = market.clone();
2071 drop(cache);
2072
2073 match parser::parse_position(position_data, Some(&market_clone)) {
2074 Ok(position) => {
2075 // Only return positions with contracts > 0
2076 if position.contracts.unwrap_or(0.0) > 0.0 {
2077 // If symbols specified, only return matching ones
2078 if let Some(ref syms) = symbols {
2079 if syms.contains(&position.symbol) {
2080 positions.push(position);
2081 }
2082 } else {
2083 positions.push(position);
2084 }
2085 }
2086 }
2087 Err(e) => {
2088 warn!(
2089 error = %e,
2090 symbol = %binance_symbol,
2091 "Failed to parse position"
2092 );
2093 }
2094 }
2095 } else {
2096 drop(cache);
2097 }
2098 }
2099 }
2100
2101 Ok(positions)
2102 }
2103
2104 /// Fetch position risk information.
2105 ///
2106 /// This is an alias for [`fetch_positions`](Self::fetch_positions) provided for CCXT naming consistency.
2107 ///
2108 /// # Arguments
2109 ///
2110 /// * `symbols` - Optional list of trading pair symbols.
2111 /// * `params` - Optional additional parameters.
2112 ///
2113 /// # Returns
2114 ///
2115 /// Returns a vector of position risk information as [`Position`] structures.
2116 ///
2117 /// # Errors
2118 ///
2119 /// Returns an error if authentication fails or the API request fails.
2120 pub async fn fetch_positions_risk(
2121 &self,
2122 symbols: Option<Vec<String>>,
2123 params: Option<Value>,
2124 ) -> Result<Vec<ccxt_core::types::Position>> {
2125 self.fetch_positions(symbols, params).await
2126 }
2127
2128 /// Fetch leverage settings for multiple trading pairs.
2129 ///
2130 /// # Arguments
2131 ///
2132 /// * `symbols` - Optional list of trading pairs. `None` queries all pairs.
2133 /// * `params` - Optional parameters:
2134 /// - `portfolioMargin`: Whether to use portfolio margin account.
2135 ///
2136 /// # Returns
2137 ///
2138 /// Returns a `HashMap` of leverage information keyed by trading pair symbol.
2139 ///
2140 /// # Errors
2141 ///
2142 /// Returns an error if authentication fails or the API request fails.
2143 ///
2144 /// # Examples
2145 ///
2146 /// ```no_run
2147 /// # use ccxt_exchanges::binance::Binance;
2148 /// # use std::collections::HashMap;
2149 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2150 /// let exchange = Binance::new(Default::default());
2151 ///
2152 /// // Query leverage settings for all trading pairs
2153 /// let leverages = exchange.fetch_leverages(None, None).await?;
2154 ///
2155 /// // Query leverage settings for specific pairs
2156 /// let symbols = vec!["BTC/USDT:USDT".to_string(), "ETH/USDT:USDT".to_string()];
2157 /// let leverages = exchange.fetch_leverages(Some(symbols), None).await?;
2158 /// # Ok(())
2159 /// # }
2160 /// ```
2161 pub async fn fetch_leverages(
2162 &self,
2163 symbols: Option<Vec<String>>,
2164 params: Option<Value>,
2165 ) -> Result<HashMap<String, ccxt_core::types::Leverage>> {
2166 self.load_markets(false).await?;
2167
2168 let mut params_map = if let Some(p) = params {
2169 serde_json::from_value::<HashMap<String, String>>(p).unwrap_or_default()
2170 } else {
2171 HashMap::new()
2172 };
2173
2174 let market_type = params_map
2175 .remove("type")
2176 .or_else(|| params_map.remove("marketType"))
2177 .unwrap_or_else(|| "future".to_string());
2178
2179 let sub_type = params_map
2180 .remove("subType")
2181 .unwrap_or_else(|| "linear".to_string());
2182
2183 let is_portfolio_margin = params_map
2184 .remove("portfolioMargin")
2185 .and_then(|v| v.parse::<bool>().ok())
2186 .unwrap_or(false);
2187
2188 let timestamp = self.fetch_time_raw().await?;
2189 let auth = self.get_auth()?;
2190 let signed_params =
2191 auth.sign_with_timestamp(¶ms_map, timestamp, Some(self.options().recv_window))?;
2192
2193 let url = if market_type == "future" && sub_type == "linear" {
2194 // USDT-M futures
2195 if is_portfolio_margin {
2196 format!(
2197 "{}/account",
2198 self.urls().fapi_private.replace("/fapi/v1", "/papi/v1/um")
2199 )
2200 } else {
2201 format!("{}/symbolConfig", self.urls().fapi_private)
2202 }
2203 } else if market_type == "future" && sub_type == "inverse" {
2204 // Coin-M futures
2205 if is_portfolio_margin {
2206 format!(
2207 "{}/account",
2208 self.urls().dapi_private.replace("/dapi/v1", "/papi/v1/cm")
2209 )
2210 } else {
2211 format!("{}/account", self.urls().dapi_private)
2212 }
2213 } else {
2214 return Err(Error::invalid_request(
2215 "fetchLeverages() supports linear and inverse contracts only",
2216 ));
2217 };
2218
2219 let mut request_url = format!("{}?", url);
2220 for (key, value) in &signed_params {
2221 request_url.push_str(&format!("{}={}&", key, value));
2222 }
2223
2224 let mut headers = HeaderMap::new();
2225 auth.add_auth_headers_reqwest(&mut headers);
2226
2227 let response = self
2228 .base()
2229 .http_client
2230 .get(&request_url, Some(headers))
2231 .await?;
2232
2233 let leverages_data = if let Some(positions) = response.get("positions") {
2234 positions.as_array().cloned().unwrap_or_default()
2235 } else if response.is_array() {
2236 response.as_array().cloned().unwrap_or_default()
2237 } else {
2238 vec![]
2239 };
2240
2241 let mut leverages = HashMap::new();
2242
2243 for item in leverages_data {
2244 if let Ok(leverage) = parser::parse_leverage(&item, None) {
2245 // If symbols specified, only keep matching ones
2246 if let Some(ref filter_symbols) = symbols {
2247 if filter_symbols.contains(&leverage.symbol) {
2248 leverages.insert(leverage.symbol.clone(), leverage);
2249 }
2250 } else {
2251 leverages.insert(leverage.symbol.clone(), leverage);
2252 }
2253 }
2254 }
2255
2256 Ok(leverages)
2257 }
2258
2259 /// Fetch leverage settings for a single trading pair.
2260 ///
2261 /// # Arguments
2262 ///
2263 /// * `symbol` - Trading pair symbol.
2264 /// * `params` - Optional additional parameters.
2265 ///
2266 /// # Returns
2267 ///
2268 /// Returns leverage information for the specified trading pair.
2269 ///
2270 /// # Errors
2271 ///
2272 /// Returns an error if the symbol is not found or the API request fails.
2273 ///
2274 /// # Examples
2275 ///
2276 /// ```no_run
2277 /// # use ccxt_exchanges::binance::Binance;
2278 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2279 /// let exchange = Binance::new(Default::default());
2280 ///
2281 /// // Query leverage settings for BTC/USDT futures
2282 /// let leverage = exchange.fetch_leverage("BTC/USDT:USDT", None).await?;
2283 /// println!("Long leverage: {:?}", leverage.long_leverage);
2284 /// println!("Short leverage: {:?}", leverage.short_leverage);
2285 /// println!("Margin mode: {:?}", leverage.margin_mode);
2286 /// # Ok(())
2287 /// # }
2288 /// ```
2289 pub async fn fetch_leverage(
2290 &self,
2291 symbol: &str,
2292 params: Option<Value>,
2293 ) -> Result<ccxt_core::types::Leverage> {
2294 let symbols = Some(vec![symbol.to_string()]);
2295 let leverages = self.fetch_leverages(symbols, params).await?;
2296
2297 leverages.get(symbol).cloned().ok_or_else(|| {
2298 Error::exchange("404", format!("Leverage not found for symbol: {}", symbol))
2299 })
2300 }
2301 /// Set leverage multiplier for a trading pair.
2302 ///
2303 /// # Arguments
2304 ///
2305 /// * `symbol` - Trading pair symbol.
2306 /// * `leverage` - Leverage multiplier (1-125).
2307 /// * `params` - Optional additional parameters.
2308 ///
2309 /// # Returns
2310 ///
2311 /// Returns the operation result as a `HashMap`.
2312 ///
2313 /// # Errors
2314 ///
2315 /// Returns an error if:
2316 /// - Authentication credentials are missing
2317 /// - Leverage is outside valid range (1-125)
2318 /// - The market is not a futures/swap market
2319 /// - The API request fails
2320 ///
2321 /// # Examples
2322 ///
2323 /// ```no_run
2324 /// # use ccxt_exchanges::binance::Binance;
2325 /// # use ccxt_core::ExchangeConfig;
2326 /// # async fn example() -> ccxt_core::Result<()> {
2327 /// let mut config = ExchangeConfig::default();
2328 /// config.api_key = Some("your_api_key".to_string());
2329 /// config.secret = Some("your_secret".to_string());
2330 /// let binance = Binance::new_futures(config)?;
2331 /// let result = binance.set_leverage("BTC/USDT:USDT", 10, None).await?;
2332 /// # Ok(())
2333 /// # }
2334 /// ```
2335 pub async fn set_leverage(
2336 &self,
2337 symbol: &str,
2338 leverage: i64,
2339 params: Option<HashMap<String, String>>,
2340 ) -> Result<HashMap<String, Value>> {
2341 self.check_required_credentials()?;
2342
2343 if leverage < 1 || leverage > 125 {
2344 return Err(Error::invalid_request(
2345 "Leverage must be between 1 and 125".to_string(),
2346 ));
2347 }
2348
2349 self.load_markets(false).await?;
2350 let market = self.base().market(symbol).await?;
2351
2352 if market.market_type != MarketType::Futures && market.market_type != MarketType::Swap {
2353 return Err(Error::invalid_request(
2354 "set_leverage() supports futures and swap markets only".to_string(),
2355 ));
2356 }
2357
2358 let mut request_params = HashMap::new();
2359 request_params.insert("symbol".to_string(), market.id.clone());
2360 request_params.insert("leverage".to_string(), leverage.to_string());
2361
2362 if let Some(p) = params {
2363 for (key, value) in p {
2364 request_params.insert(key, value);
2365 }
2366 }
2367
2368 // Select API endpoint based on market type
2369 let url = if market.linear.unwrap_or(true) {
2370 format!("{}/leverage", self.urls().fapi_private)
2371 } else if market.inverse.unwrap_or(false) {
2372 format!("{}/leverage", self.urls().dapi_private)
2373 } else {
2374 return Err(Error::invalid_request(
2375 "Unknown futures market type".to_string(),
2376 ));
2377 };
2378
2379 let timestamp = self.fetch_time_raw().await?;
2380 let auth = self.get_auth()?;
2381
2382 let signed_params =
2383 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
2384
2385 let mut headers = HeaderMap::new();
2386 auth.add_auth_headers_reqwest(&mut headers);
2387
2388 let body = serde_json::to_value(&signed_params).map_err(|e| {
2389 Error::from(ParseError::invalid_format(
2390 "data",
2391 format!("Failed to serialize params: {}", e),
2392 ))
2393 })?;
2394
2395 let response = self
2396 .base()
2397 .http_client
2398 .post(&url, Some(headers), Some(body))
2399 .await?;
2400
2401 let result: HashMap<String, Value> = serde_json::from_value(response).map_err(|e| {
2402 Error::from(ParseError::invalid_format(
2403 "data",
2404 format!("Failed to parse response: {}", e),
2405 ))
2406 })?;
2407
2408 Ok(result)
2409 }
2410
2411 /// Set margin mode for a trading pair.
2412 ///
2413 /// # Arguments
2414 ///
2415 /// * `symbol` - Trading pair symbol.
2416 /// * `margin_mode` - Margin mode (`isolated` or `cross`).
2417 /// * `params` - Optional additional parameters.
2418 ///
2419 /// # Returns
2420 ///
2421 /// Returns the operation result as a `HashMap`.
2422 ///
2423 /// # Errors
2424 ///
2425 /// Returns an error if:
2426 /// - Authentication credentials are missing
2427 /// - Margin mode is invalid (must be `isolated` or `cross`)
2428 /// - The market is not a futures/swap market
2429 /// - The API request fails
2430 ///
2431 /// # Examples
2432 ///
2433 /// ```no_run
2434 /// # use ccxt_exchanges::binance::Binance;
2435 /// # use ccxt_core::ExchangeConfig;
2436 /// # async fn example() -> ccxt_core::Result<()> {
2437 /// let mut config = ExchangeConfig::default();
2438 /// config.api_key = Some("your_api_key".to_string());
2439 /// config.secret = Some("your_secret".to_string());
2440 /// let binance = Binance::new_futures(config)?;
2441 /// let result = binance.set_margin_mode("BTC/USDT:USDT", "isolated", None).await?;
2442 /// # Ok(())
2443 /// # }
2444 /// ```
2445 pub async fn set_margin_mode(
2446 &self,
2447 symbol: &str,
2448 margin_mode: &str,
2449 params: Option<HashMap<String, String>>,
2450 ) -> Result<HashMap<String, Value>> {
2451 self.check_required_credentials()?;
2452
2453 let margin_type = match margin_mode.to_uppercase().as_str() {
2454 "ISOLATED" | "ISOLATED_MARGIN" => "ISOLATED",
2455 "CROSS" | "CROSSED" | "CROSS_MARGIN" => "CROSSED",
2456 _ => {
2457 return Err(Error::invalid_request(format!(
2458 "Invalid margin mode: {}. Must be 'isolated' or 'cross'",
2459 margin_mode
2460 )));
2461 }
2462 };
2463
2464 self.load_markets(false).await?;
2465 let market = self.base().market(symbol).await?;
2466
2467 if market.market_type != MarketType::Futures && market.market_type != MarketType::Swap {
2468 return Err(Error::invalid_request(
2469 "set_margin_mode() supports futures and swap markets only".to_string(),
2470 ));
2471 }
2472
2473 let mut request_params = HashMap::new();
2474 request_params.insert("symbol".to_string(), market.id.clone());
2475 request_params.insert("marginType".to_string(), margin_type.to_string());
2476
2477 if let Some(p) = params {
2478 for (key, value) in p {
2479 request_params.insert(key, value);
2480 }
2481 }
2482
2483 // Select API endpoint based on market type
2484 let url = if market.linear.unwrap_or(true) {
2485 format!("{}/marginType", self.urls().fapi_private)
2486 } else if market.inverse.unwrap_or(false) {
2487 format!("{}/marginType", self.urls().dapi_private)
2488 } else {
2489 return Err(Error::invalid_request(
2490 "Unknown futures market type".to_string(),
2491 ));
2492 };
2493
2494 let timestamp = self.fetch_time_raw().await?;
2495 let auth = self.get_auth()?;
2496 let signed_params =
2497 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
2498
2499 let mut headers = HeaderMap::new();
2500 auth.add_auth_headers_reqwest(&mut headers);
2501
2502 let body = serde_json::to_value(&signed_params).map_err(|e| {
2503 Error::from(ParseError::invalid_format(
2504 "data",
2505 format!("Failed to serialize params: {}", e),
2506 ))
2507 })?;
2508
2509 let response = self
2510 .base()
2511 .http_client
2512 .post(&url, Some(headers), Some(body))
2513 .await?;
2514
2515 let result: HashMap<String, Value> = serde_json::from_value(response).map_err(|e| {
2516 Error::from(ParseError::invalid_format(
2517 "data",
2518 format!("Failed to parse response: {}", e),
2519 ))
2520 })?;
2521
2522 Ok(result)
2523 }
2524
2525 // ==================== P2.2: Fee Management ====================
2526
2527 /// Fetch trading fee for a single trading pair.
2528 ///
2529 /// # Arguments
2530 ///
2531 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
2532 /// * `params` - Optional parameters.
2533 ///
2534 /// # Returns
2535 ///
2536 /// Returns trading fee information.
2537 ///
2538 /// # Errors
2539 ///
2540 /// Returns an error if:
2541 /// - Authentication credentials are missing
2542 /// - The market type is not supported
2543 /// - The API request fails
2544 ///
2545 /// # Example
2546 ///
2547 /// ```no_run
2548 /// # use ccxt_exchanges::binance::Binance;
2549 /// # use ccxt_core::ExchangeConfig;
2550 /// # async fn example() -> ccxt_core::Result<()> {
2551 /// let mut config = ExchangeConfig::default();
2552 /// config.api_key = Some("your_api_key".to_string());
2553 /// config.secret = Some("your_secret".to_string());
2554 /// let binance = Binance::new(config)?;
2555 /// let fee = binance.fetch_trading_fee("BTC/USDT", None).await?;
2556 /// println!("Maker: {}, Taker: {}", fee.maker, fee.taker);
2557 /// # Ok(())
2558 /// # }
2559 /// ```
2560 pub async fn fetch_trading_fee(
2561 &self,
2562 symbol: &str,
2563 params: Option<HashMap<String, String>>,
2564 ) -> Result<FeeTradingFee> {
2565 self.check_required_credentials()?;
2566
2567 self.load_markets(false).await?;
2568 let market = self.base().market(symbol).await?;
2569
2570 let mut request_params = HashMap::new();
2571 request_params.insert("symbol".to_string(), market.id.clone());
2572
2573 let is_portfolio_margin = params
2574 .as_ref()
2575 .and_then(|p| p.get("portfolioMargin"))
2576 .map(|v| v == "true")
2577 .unwrap_or(false);
2578
2579 if let Some(p) = params {
2580 for (key, value) in p {
2581 // Do not pass portfolioMargin parameter to API
2582 if key != "portfolioMargin" {
2583 request_params.insert(key, value);
2584 }
2585 }
2586 }
2587
2588 // Select API endpoint based on market type and Portfolio Margin mode
2589 let url = match market.market_type {
2590 MarketType::Spot => format!("{}/asset/tradeFee", self.urls().sapi),
2591 MarketType::Futures | MarketType::Swap => {
2592 if is_portfolio_margin {
2593 // Portfolio Margin mode uses papi endpoints
2594 if market.is_linear() {
2595 format!("{}/um/commissionRate", self.urls().papi)
2596 } else {
2597 format!("{}/cm/commissionRate", self.urls().papi)
2598 }
2599 } else {
2600 // Standard mode
2601 if market.is_linear() {
2602 format!("{}/commissionRate", self.urls().fapi_private)
2603 } else {
2604 format!("{}/commissionRate", self.urls().dapi_private)
2605 }
2606 }
2607 }
2608 _ => {
2609 return Err(Error::invalid_request(format!(
2610 "fetch_trading_fee not supported for market type: {:?}",
2611 market.market_type
2612 )));
2613 }
2614 };
2615
2616 let timestamp = self.fetch_time_raw().await?;
2617 let auth = self.get_auth()?;
2618 let signed_params =
2619 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
2620
2621 let mut request_url = format!("{}?", url);
2622 for (key, value) in &signed_params {
2623 request_url.push_str(&format!("{}={}&", key, value));
2624 }
2625
2626 let mut headers = HeaderMap::new();
2627 auth.add_auth_headers_reqwest(&mut headers);
2628
2629 let response = self
2630 .base()
2631 .http_client
2632 .get(&request_url, Some(headers))
2633 .await?;
2634
2635 parser::parse_trading_fee(&response)
2636 }
2637
2638 /// Fetch trading fees for multiple trading pairs.
2639 ///
2640 /// # Arguments
2641 ///
2642 /// * `symbols` - Optional list of trading pairs. `None` fetches all pairs.
2643 /// * `params` - Optional parameters.
2644 ///
2645 /// # Returns
2646 ///
2647 /// Returns a `HashMap` of trading fees keyed by symbol.
2648 ///
2649 /// # Errors
2650 ///
2651 /// Returns an error if authentication fails or the API request fails.
2652 ///
2653 /// # Example
2654 ///
2655 /// ```no_run
2656 /// # use ccxt_exchanges::binance::Binance;
2657 /// # use ccxt_core::ExchangeConfig;
2658 /// # async fn example() -> ccxt_core::Result<()> {
2659 /// let mut config = ExchangeConfig::default();
2660 /// config.api_key = Some("your_api_key".to_string());
2661 /// config.secret = Some("your_secret".to_string());
2662 /// let binance = Binance::new(config)?;
2663 /// let fees = binance.fetch_trading_fees(None, None).await?;
2664 /// # Ok(())
2665 /// # }
2666 /// ```
2667 pub async fn fetch_trading_fees(
2668 &self,
2669 symbols: Option<Vec<String>>,
2670 params: Option<HashMap<String, String>>,
2671 ) -> Result<HashMap<String, FeeTradingFee>> {
2672 self.check_required_credentials()?;
2673
2674 self.load_markets(false).await?;
2675
2676 let mut request_params = HashMap::new();
2677
2678 if let Some(syms) = &symbols {
2679 let mut market_ids: Vec<String> = Vec::new();
2680 for s in syms {
2681 if let Ok(market) = self.base().market(s).await {
2682 market_ids.push(market.id);
2683 }
2684 }
2685 if !market_ids.is_empty() {
2686 request_params.insert("symbols".to_string(), market_ids.join(","));
2687 }
2688 }
2689
2690 if let Some(p) = params {
2691 for (key, value) in p {
2692 request_params.insert(key, value);
2693 }
2694 }
2695
2696 let url = format!("{}/asset/tradeFee", self.urls().sapi);
2697
2698 let timestamp = self.fetch_time_raw().await?;
2699 let auth = self.get_auth()?;
2700 let signed_params =
2701 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
2702
2703 let mut request_url = format!("{}?", url);
2704 for (key, value) in &signed_params {
2705 request_url.push_str(&format!("{}={}&", key, value));
2706 }
2707
2708 let mut headers = HeaderMap::new();
2709 auth.add_auth_headers_reqwest(&mut headers);
2710
2711 let response = self
2712 .base()
2713 .http_client
2714 .get(&request_url, Some(headers))
2715 .await?;
2716
2717 let fees_array = response.as_array().ok_or_else(|| {
2718 Error::from(ParseError::invalid_format(
2719 "data",
2720 "Expected array of trading fees",
2721 ))
2722 })?;
2723
2724 let mut fees = HashMap::new();
2725 for fee_data in fees_array {
2726 if let Ok(symbol_id) = fee_data["symbol"]
2727 .as_str()
2728 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
2729 {
2730 if let Ok(market) = self.base().market_by_id(symbol_id).await {
2731 if let Ok(fee) = parser::parse_trading_fee(fee_data) {
2732 fees.insert(market.symbol.clone(), fee);
2733 }
2734 }
2735 }
2736 }
2737
2738 Ok(fees)
2739 }
2740
2741 /// Fetch current funding rate for a single trading pair.
2742 ///
2743 /// # Arguments
2744 ///
2745 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT:USDT").
2746 /// * `params` - Optional parameters.
2747 ///
2748 /// # Returns
2749 ///
2750 /// Returns current funding rate information.
2751 ///
2752 /// # Errors
2753 ///
2754 /// Returns an error if:
2755 /// - The market is not a futures or swap market
2756 /// - The API request fails
2757 ///
2758 /// # Example
2759 ///
2760 /// ```no_run
2761 /// # use ccxt_exchanges::binance::Binance;
2762 /// # use ccxt_core::ExchangeConfig;
2763 /// # async fn example() -> ccxt_core::Result<()> {
2764 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
2765 /// let rate = binance.fetch_funding_rate("BTC/USDT:USDT", None).await?;
2766 /// println!("Funding rate: {:?}", rate.funding_rate);
2767 /// # Ok(())
2768 /// # }
2769 /// ```
2770 pub async fn fetch_funding_rate(
2771 &self,
2772 symbol: &str,
2773 params: Option<HashMap<String, String>>,
2774 ) -> Result<FeeFundingRate> {
2775 self.load_markets(false).await?;
2776 let market = self.base().market(symbol).await?;
2777
2778 if market.market_type != MarketType::Futures && market.market_type != MarketType::Swap {
2779 return Err(Error::invalid_request(
2780 "fetch_funding_rate() supports futures and swap markets only".to_string(),
2781 ));
2782 }
2783
2784 let mut request_params = HashMap::new();
2785 request_params.insert("symbol".to_string(), market.id.clone());
2786
2787 if let Some(p) = params {
2788 for (key, value) in p {
2789 request_params.insert(key, value);
2790 }
2791 }
2792
2793 let url = if market.linear.unwrap_or(true) {
2794 format!("{}/premiumIndex", self.urls().fapi_public)
2795 } else {
2796 format!("{}/premiumIndex", self.urls().dapi_public)
2797 };
2798
2799 let mut request_url = format!("{}?", url);
2800 for (key, value) in &request_params {
2801 request_url.push_str(&format!("{}={}&", key, value));
2802 }
2803
2804 let response = self.base().http_client.get(&request_url, None).await?;
2805
2806 // COIN-M futures API returns array format - extract first element
2807 let data = if !market.linear.unwrap_or(true) {
2808 response
2809 .as_array()
2810 .and_then(|arr| arr.first())
2811 .ok_or_else(|| {
2812 Error::from(ParseError::invalid_format(
2813 "data",
2814 "COIN-M funding rate response should be an array with at least one element",
2815 ))
2816 })?
2817 } else {
2818 &response
2819 };
2820
2821 parser::parse_funding_rate(data, Some(&market))
2822 }
2823
2824 /// Fetch funding rate history for a trading pair.
2825 ///
2826 /// # Arguments
2827 ///
2828 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT:USDT").
2829 /// * `since` - Optional start timestamp in milliseconds.
2830 /// * `limit` - Optional record limit (default 100, max 1000).
2831 /// * `params` - Optional parameters.
2832 ///
2833 /// # Returns
2834 ///
2835 /// Returns a vector of historical funding rate records.
2836 ///
2837 /// # Errors
2838 ///
2839 /// Returns an error if:
2840 /// - The market is not a futures or swap market
2841 /// - The API request fails
2842 ///
2843 /// # Example
2844 ///
2845 /// ```no_run
2846 /// # use ccxt_exchanges::binance::Binance;
2847 /// # use ccxt_core::ExchangeConfig;
2848 /// # async fn example() -> ccxt_core::Result<()> {
2849 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
2850 /// let history = binance.fetch_funding_rate_history("BTC/USDT:USDT", None, Some(10), None).await?;
2851 /// println!("Found {} records", history.len());
2852 /// # Ok(())
2853 /// # }
2854 /// ```
2855 pub async fn fetch_funding_rate_history(
2856 &self,
2857 symbol: &str,
2858 since: Option<u64>,
2859 limit: Option<u32>,
2860 params: Option<HashMap<String, String>>,
2861 ) -> Result<Vec<FeeFundingRateHistory>> {
2862 self.load_markets(false).await?;
2863 let market = self.base().market(symbol).await?;
2864
2865 if market.market_type != MarketType::Futures && market.market_type != MarketType::Swap {
2866 return Err(Error::invalid_request(
2867 "fetch_funding_rate_history() supports futures and swap markets only".to_string(),
2868 ));
2869 }
2870
2871 let mut request_params = HashMap::new();
2872 request_params.insert("symbol".to_string(), market.id.clone());
2873
2874 if let Some(s) = since {
2875 request_params.insert("startTime".to_string(), s.to_string());
2876 }
2877
2878 if let Some(l) = limit {
2879 request_params.insert("limit".to_string(), l.to_string());
2880 }
2881
2882 if let Some(p) = params {
2883 for (key, value) in p {
2884 request_params.insert(key, value);
2885 }
2886 }
2887
2888 let url = if market.linear.unwrap_or(true) {
2889 format!("{}/fundingRate", self.urls().fapi_public)
2890 } else {
2891 format!("{}/fundingRate", self.urls().dapi_public)
2892 };
2893
2894 let mut request_url = format!("{}?", url);
2895 for (key, value) in &request_params {
2896 request_url.push_str(&format!("{}={}&", key, value));
2897 }
2898
2899 let response = self.base().http_client.get(&request_url, None).await?;
2900
2901 let history_array = response.as_array().ok_or_else(|| {
2902 Error::from(ParseError::invalid_format(
2903 "data",
2904 "Expected array of funding rate history",
2905 ))
2906 })?;
2907
2908 let mut history = Vec::new();
2909 for history_data in history_array {
2910 match parser::parse_funding_rate_history(history_data, Some(&market)) {
2911 Ok(record) => history.push(record),
2912 Err(e) => {
2913 warn!(error = %e, "Failed to parse funding rate history");
2914 }
2915 }
2916 }
2917
2918 Ok(history)
2919 }
2920
2921 /// Fetch current funding rates for multiple trading pairs.
2922 ///
2923 /// # Arguments
2924 ///
2925 /// * `symbols` - Optional list of trading pairs. `None` fetches all pairs.
2926 /// * `params` - Optional parameters.
2927 ///
2928 /// # Returns
2929 ///
2930 /// Returns a `HashMap` of funding rates keyed by symbol.
2931 ///
2932 /// # Errors
2933 ///
2934 /// Returns an error if the API request fails.
2935 ///
2936 /// # Example
2937 ///
2938 /// ```no_run
2939 /// # use ccxt_exchanges::binance::Binance;
2940 /// # use ccxt_core::ExchangeConfig;
2941 /// # async fn example() -> ccxt_core::Result<()> {
2942 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
2943 /// let rates = binance.fetch_funding_rates(None, None).await?;
2944 /// println!("Found {} funding rates", rates.len());
2945 /// # Ok(())
2946 /// # }
2947 /// ```
2948 pub async fn fetch_funding_rates(
2949 &self,
2950 symbols: Option<Vec<String>>,
2951 params: Option<HashMap<String, String>>,
2952 ) -> Result<HashMap<String, FeeFundingRate>> {
2953 self.load_markets(false).await?;
2954
2955 let mut request_params = HashMap::new();
2956
2957 if let Some(p) = params {
2958 for (key, value) in p {
2959 request_params.insert(key, value);
2960 }
2961 }
2962
2963 let url = format!("{}/premiumIndex", self.urls().fapi_public);
2964
2965 let mut request_url = url.clone();
2966 if !request_params.is_empty() {
2967 request_url.push('?');
2968 for (key, value) in &request_params {
2969 request_url.push_str(&format!("{}={}&", key, value));
2970 }
2971 }
2972
2973 let response = self.base().http_client.get(&request_url, None).await?;
2974
2975 let mut rates = HashMap::new();
2976
2977 if let Some(rates_array) = response.as_array() {
2978 for rate_data in rates_array {
2979 if let Ok(symbol_id) = rate_data["symbol"]
2980 .as_str()
2981 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
2982 {
2983 if let Ok(market) = self.base().market_by_id(symbol_id).await {
2984 if let Some(ref syms) = symbols {
2985 if !syms.contains(&market.symbol) {
2986 continue;
2987 }
2988 }
2989
2990 if let Ok(rate) = parser::parse_funding_rate(rate_data, Some(&market)) {
2991 rates.insert(market.symbol.clone(), rate);
2992 }
2993 }
2994 }
2995 }
2996 } else {
2997 if let Ok(symbol_id) = response["symbol"]
2998 .as_str()
2999 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
3000 {
3001 if let Ok(market) = self.base().market_by_id(symbol_id).await {
3002 if let Ok(rate) = parser::parse_funding_rate(&response, Some(&market)) {
3003 rates.insert(market.symbol.clone(), rate);
3004 }
3005 }
3006 }
3007 }
3008
3009 Ok(rates)
3010 }
3011
3012 /// Fetch leverage tier information for trading pairs.
3013 ///
3014 /// # Arguments
3015 ///
3016 /// * `symbols` - Optional list of trading pairs. `None` fetches all pairs.
3017 /// * `params` - Optional parameters.
3018 ///
3019 /// # Returns
3020 ///
3021 /// Returns a `HashMap` of leverage tiers keyed by symbol.
3022 ///
3023 /// # Errors
3024 ///
3025 /// Returns an error if authentication fails or the API request fails.
3026 ///
3027 /// # Example
3028 ///
3029 /// ```no_run
3030 /// # use ccxt_exchanges::binance::Binance;
3031 /// # use ccxt_core::ExchangeConfig;
3032 /// # async fn example() -> ccxt_core::Result<()> {
3033 /// let mut config = ExchangeConfig::default();
3034 /// config.api_key = Some("your_api_key".to_string());
3035 /// config.secret = Some("your_secret".to_string());
3036 /// let binance = Binance::new_futures(config)?;
3037 /// let tiers = binance.fetch_leverage_tiers(None, None).await?;
3038 /// # Ok(())
3039 /// # }
3040 /// ```
3041 pub async fn fetch_leverage_tiers(
3042 &self,
3043 symbols: Option<Vec<String>>,
3044 params: Option<HashMap<String, String>>,
3045 ) -> Result<HashMap<String, Vec<LeverageTier>>> {
3046 self.check_required_credentials()?;
3047
3048 self.load_markets(false).await?;
3049
3050 let mut request_params = HashMap::new();
3051
3052 let is_portfolio_margin = params
3053 .as_ref()
3054 .and_then(|p| p.get("portfolioMargin"))
3055 .map(|v| v == "true")
3056 .unwrap_or(false);
3057
3058 let market = if let Some(syms) = &symbols {
3059 if let Some(first_symbol) = syms.first() {
3060 let m = self.base().market(first_symbol).await?;
3061 request_params.insert("symbol".to_string(), m.id.clone());
3062 Some(m)
3063 } else {
3064 None
3065 }
3066 } else {
3067 None
3068 };
3069
3070 if let Some(p) = params {
3071 for (key, value) in p {
3072 // Do not pass portfolioMargin parameter to API
3073 if key != "portfolioMargin" {
3074 request_params.insert(key, value);
3075 }
3076 }
3077 }
3078
3079 // Select API endpoint based on market type and Portfolio Margin mode
3080 let url = if let Some(ref m) = market {
3081 if is_portfolio_margin {
3082 // Portfolio Margin mode uses papi endpoints
3083 if m.is_linear() {
3084 format!("{}/um/leverageBracket", self.urls().papi)
3085 } else {
3086 format!("{}/cm/leverageBracket", self.urls().papi)
3087 }
3088 } else {
3089 // Standard mode
3090 if m.is_linear() {
3091 format!("{}/leverageBracket", self.urls().fapi_private)
3092 } else {
3093 format!("{}/v2/leverageBracket", self.urls().dapi_private)
3094 }
3095 }
3096 } else {
3097 format!("{}/leverageBracket", self.urls().fapi_private)
3098 };
3099
3100 let timestamp = self.fetch_time_raw().await?;
3101 let auth = self.get_auth()?;
3102 let signed_params =
3103 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
3104
3105 let mut request_url = format!("{}?", url);
3106 for (key, value) in &signed_params {
3107 request_url.push_str(&format!("{}={}&", key, value));
3108 }
3109
3110 let mut headers = HeaderMap::new();
3111 auth.add_auth_headers_reqwest(&mut headers);
3112
3113 let response = self
3114 .base()
3115 .http_client
3116 .get(&request_url, Some(headers))
3117 .await?;
3118
3119 let mut tiers = HashMap::new();
3120
3121 if let Some(tiers_array) = response.as_array() {
3122 for tier_data in tiers_array {
3123 if let Ok(symbol_id) = tier_data["symbol"]
3124 .as_str()
3125 .ok_or_else(|| Error::from(ParseError::missing_field("symbol")))
3126 {
3127 if let Ok(market) = self.base().market_by_id(symbol_id).await {
3128 if let Some(ref syms) = symbols {
3129 if !syms.contains(&market.symbol) {
3130 continue;
3131 }
3132 }
3133
3134 if let Some(brackets) = tier_data["brackets"].as_array() {
3135 let mut tier_list = Vec::new();
3136 for bracket in brackets {
3137 match parser::parse_leverage_tier(bracket, &market) {
3138 Ok(tier) => tier_list.push(tier),
3139 Err(e) => {
3140 warn!(error = %e, "Failed to parse leverage tier");
3141 }
3142 }
3143 }
3144 if !tier_list.is_empty() {
3145 tiers.insert(market.symbol.clone(), tier_list);
3146 }
3147 }
3148 }
3149 }
3150 }
3151 }
3152
3153 Ok(tiers)
3154 }
3155
3156 /// Fetch funding fee history.
3157 ///
3158 /// # Arguments
3159 ///
3160 /// * `symbol` - Optional trading pair symbol.
3161 /// * `since` - Optional start timestamp in milliseconds.
3162 /// * `limit` - Optional record limit (default 100, max 1000).
3163 /// * `params` - Optional parameters.
3164 ///
3165 /// # Returns
3166 ///
3167 /// Returns a vector of funding fee history records.
3168 ///
3169 /// # Errors
3170 ///
3171 /// Returns an error if authentication fails or the API request fails.
3172 ///
3173 /// # Example
3174 ///
3175 /// ```no_run
3176 /// # use ccxt_exchanges::binance::Binance;
3177 /// # use ccxt_core::ExchangeConfig;
3178 /// # async fn example() -> ccxt_core::Result<()> {
3179 /// let mut config = ExchangeConfig::default();
3180 /// config.api_key = Some("your_api_key".to_string());
3181 /// config.secret = Some("your_secret".to_string());
3182 /// let binance = Binance::new_futures(config)?;
3183 /// let history = binance.fetch_funding_history(
3184 /// Some("BTC/USDT:USDT"),
3185 /// None,
3186 /// Some(100),
3187 /// None
3188 /// ).await?;
3189 /// # Ok(())
3190 /// # }
3191 /// ```
3192 pub async fn fetch_funding_history(
3193 &self,
3194 symbol: Option<&str>,
3195 since: Option<u64>,
3196 limit: Option<u32>,
3197 params: Option<Value>,
3198 ) -> Result<Vec<ccxt_core::types::FundingHistory>> {
3199 self.check_required_credentials()?;
3200
3201 let mut request_params = HashMap::new();
3202
3203 request_params.insert("incomeType".to_string(), "FUNDING_FEE".to_string());
3204
3205 let market = if let Some(sym) = symbol {
3206 let m = self.base().market(sym).await?;
3207 request_params.insert("symbol".to_string(), m.id.clone());
3208 Some(m)
3209 } else {
3210 None
3211 };
3212
3213 if let Some(s) = since {
3214 request_params.insert("startTime".to_string(), s.to_string());
3215 }
3216
3217 if let Some(l) = limit {
3218 request_params.insert("limit".to_string(), l.to_string());
3219 }
3220
3221 if let Some(params) = params {
3222 if let Some(obj) = params.as_object() {
3223 for (key, value) in obj {
3224 if let Some(v) = value.as_str() {
3225 request_params.insert(key.clone(), v.to_string());
3226 } else if let Some(v) = value.as_i64() {
3227 request_params.insert(key.clone(), v.to_string());
3228 }
3229 }
3230 }
3231 }
3232
3233 let timestamp = self.fetch_time_raw().await?;
3234 let auth = self.get_auth()?;
3235 let signed_params =
3236 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
3237 let query_string = auth.build_query_string(&signed_params);
3238 let use_coin_m = market
3239 .as_ref()
3240 .map(|m| !m.linear.unwrap_or(false))
3241 .unwrap_or(false);
3242
3243 let url = if use_coin_m {
3244 format!("{}/income", self.urls().dapi_private)
3245 } else {
3246 format!("{}/income", self.urls().fapi_private)
3247 };
3248
3249 let request_url = format!("{}?{}", url, query_string);
3250
3251 let mut headers = HeaderMap::new();
3252 auth.add_auth_headers_reqwest(&mut headers);
3253
3254 let data = self
3255 .base()
3256 .http_client
3257 .get(&request_url, Some(headers))
3258 .await?;
3259
3260 let history_array = data.as_array().ok_or_else(|| {
3261 Error::from(ParseError::invalid_format(
3262 "data",
3263 "Expected array of funding history",
3264 ))
3265 })?;
3266
3267 let mut history = Vec::new();
3268 for history_data in history_array {
3269 match parser::parse_funding_history(history_data, market.as_ref()) {
3270 Ok(funding) => history.push(funding),
3271 Err(e) => {
3272 warn!(error = %e, "Failed to parse funding history");
3273 }
3274 }
3275 }
3276
3277 Ok(history)
3278 }
3279 /// Fetch my funding fee history
3280 ///
3281 /// # Arguments
3282 ///
3283 /// * `symbol` - Optional trading pair symbol.
3284 /// * `start_time` - Optional start timestamp.
3285 /// * `end_time` - Optional end timestamp.
3286 /// * `limit` - Optional record limit (default 100, max 1000).
3287 ///
3288 /// # Returns
3289 ///
3290 /// Returns a vector of funding fee history records.
3291 ///
3292 /// # Errors
3293 ///
3294 /// Returns an error if authentication fails or the API request fails.
3295 ///
3296 /// # Example
3297 ///
3298 /// ```no_run
3299 /// # use ccxt_exchanges::binance::Binance;
3300 /// # use ccxt_core::ExchangeConfig;
3301 /// # async fn example() -> ccxt_core::Result<()> {
3302 /// let mut config = ExchangeConfig::default();
3303 /// config.api_key = Some("your_api_key".to_string());
3304 /// config.secret = Some("your_secret".to_string());
3305 /// let binance = Binance::new_futures(config)?;
3306 /// let fees = binance.fetch_my_funding_history(Some("BTC/USDT:USDT"), None, None, Some(50)).await?;
3307 /// # Ok(())
3308 /// # }
3309 /// ```
3310 pub async fn fetch_my_funding_history(
3311 &self,
3312 symbol: Option<&str>,
3313 start_time: Option<u64>,
3314 end_time: Option<u64>,
3315 limit: Option<u32>,
3316 ) -> Result<Vec<FundingFee>> {
3317 self.check_required_credentials()?;
3318
3319 let mut request_params = HashMap::new();
3320
3321 request_params.insert("incomeType".to_string(), "FUNDING_FEE".to_string());
3322
3323 let market = if let Some(sym) = symbol {
3324 let m = self.base().market(sym).await?;
3325 request_params.insert("symbol".to_string(), m.id.clone());
3326 Some(m)
3327 } else {
3328 None
3329 };
3330
3331 if let Some(s) = start_time {
3332 request_params.insert("startTime".to_string(), s.to_string());
3333 }
3334
3335 if let Some(e) = end_time {
3336 request_params.insert("endTime".to_string(), e.to_string());
3337 }
3338
3339 if let Some(l) = limit {
3340 request_params.insert("limit".to_string(), l.to_string());
3341 }
3342
3343 let timestamp = self.fetch_time_raw().await?;
3344 let auth = self.get_auth()?;
3345 let signed_params =
3346 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
3347
3348 let use_coin_m = market
3349 .as_ref()
3350 .map(|m| !m.linear.unwrap_or(false))
3351 .unwrap_or(false);
3352
3353 let url = if use_coin_m {
3354 format!("{}/income", self.urls().dapi_private)
3355 } else {
3356 format!("{}/income", self.urls().fapi_private)
3357 };
3358 let query_string = auth.build_query_string(&signed_params);
3359 let request_url = format!("{}?{}", url, query_string);
3360
3361 let mut headers = HeaderMap::new();
3362 auth.add_auth_headers_reqwest(&mut headers);
3363
3364 let data = self
3365 .base()
3366 .http_client
3367 .get(&request_url, Some(headers))
3368 .await?;
3369
3370 let fees_array = data.as_array().ok_or_else(|| {
3371 Error::from(ParseError::invalid_format(
3372 "data",
3373 "Expected array of funding fees",
3374 ))
3375 })?;
3376
3377 let mut fees = Vec::new();
3378 for fee_data in fees_array {
3379 match parser::parse_funding_fee(fee_data, market.as_ref()) {
3380 Ok(fee) => fees.push(fee),
3381 Err(e) => {
3382 warn!(error = %e, "Failed to parse funding fee");
3383 }
3384 }
3385 }
3386
3387 Ok(fees)
3388 }
3389
3390 /// Fetch next funding rate (prediction) for a trading pair.
3391 ///
3392 /// # Arguments
3393 ///
3394 /// * `symbol` - Trading pair symbol.
3395 ///
3396 /// # Returns
3397 ///
3398 /// Returns next funding rate information.
3399 ///
3400 /// # Errors
3401 ///
3402 /// Returns an error if the API request fails.
3403 ///
3404 /// # Example
3405 ///
3406 /// ```no_run
3407 /// # use ccxt_exchanges::binance::Binance;
3408 /// # use ccxt_core::ExchangeConfig;
3409 /// # async fn example() -> ccxt_core::Result<()> {
3410 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
3411 /// let next_rate = binance.fetch_next_funding_rate("BTC/USDT:USDT").await?;
3412 /// # Ok(())
3413 /// # }
3414 /// ```
3415 pub async fn fetch_next_funding_rate(&self, symbol: &str) -> Result<NextFundingRate> {
3416 let market = self.base().market(symbol).await?;
3417
3418 let use_coin_m = !market.linear.unwrap_or(false);
3419
3420 let url = if use_coin_m {
3421 format!("{}/premiumIndex", self.urls().dapi_public)
3422 } else {
3423 format!("{}/premiumIndex", self.urls().fapi_public)
3424 };
3425
3426 let mut request_params = HashMap::new();
3427 request_params.insert("symbol".to_string(), market.id.clone());
3428
3429 let mut request_url = format!("{}?", url);
3430 for (key, value) in &request_params {
3431 request_url.push_str(&format!("{}={}&", key, value));
3432 }
3433
3434 let data = self.base().http_client.get(&request_url, None).await?;
3435
3436 parser::parse_next_funding_rate(&data, &market)
3437 }
3438
3439 /// Set position mode (one-way or hedge mode).
3440 ///
3441 /// # Arguments
3442 ///
3443 /// * `dual_side` - Enable hedge mode (dual-side position).
3444 /// - `true`: Hedge mode (can hold both long and short positions simultaneously).
3445 /// - `false`: One-way mode (can only hold one direction).
3446 /// * `params` - Optional parameters.
3447 ///
3448 /// # Returns
3449 ///
3450 /// Returns the operation result.
3451 ///
3452 /// # Errors
3453 ///
3454 /// Returns an error if:
3455 /// - Authentication fails
3456 /// - Positions are currently open (cannot switch modes with open positions)
3457 /// - The API request fails
3458 ///
3459 /// # Notes
3460 ///
3461 /// - This setting applies globally to the account, not per trading pair.
3462 /// - In hedge mode, orders must specify `positionSide` (LONG/SHORT).
3463 /// - In one-way mode, `positionSide` is fixed to BOTH.
3464 /// - Cannot switch modes while holding positions.
3465 ///
3466 /// # Example
3467 ///
3468 /// ```no_run
3469 /// # use ccxt_exchanges::binance::Binance;
3470 /// # use ccxt_core::ExchangeConfig;
3471 /// # async fn example() -> ccxt_core::Result<()> {
3472 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
3473 ///
3474 /// // Enable hedge mode
3475 /// let result = binance.set_position_mode(true, None).await?;
3476 ///
3477 /// // Switch back to one-way mode
3478 /// let result = binance.set_position_mode(false, None).await?;
3479 /// # Ok(())
3480 /// # }
3481 /// ```
3482 pub async fn set_position_mode(&self, dual_side: bool, params: Option<Value>) -> Result<Value> {
3483 self.check_required_credentials()?;
3484
3485 let mut request_params = HashMap::new();
3486 request_params.insert("dualSidePosition".to_string(), dual_side.to_string());
3487
3488 if let Some(params) = params {
3489 if let Some(obj) = params.as_object() {
3490 for (key, value) in obj {
3491 if let Some(v) = value.as_str() {
3492 request_params.insert(key.clone(), v.to_string());
3493 } else if let Some(v) = value.as_bool() {
3494 request_params.insert(key.clone(), v.to_string());
3495 } else if let Some(v) = value.as_i64() {
3496 request_params.insert(key.clone(), v.to_string());
3497 }
3498 }
3499 }
3500 }
3501
3502 let timestamp = self.fetch_time_raw().await?;
3503 let auth = self.get_auth()?;
3504 let signed_params =
3505 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
3506
3507 let url = format!("{}/positionSide/dual", self.urls().fapi_private);
3508
3509 let mut headers = HeaderMap::new();
3510 auth.add_auth_headers_reqwest(&mut headers);
3511
3512 let body = serde_json::to_value(&signed_params).map_err(|e| {
3513 Error::from(ParseError::invalid_format(
3514 "data",
3515 format!("Failed to serialize params: {}", e),
3516 ))
3517 })?;
3518
3519 let data = self
3520 .base()
3521 .http_client
3522 .post(&url, Some(headers), Some(body))
3523 .await?;
3524
3525 Ok(data)
3526 }
3527
3528 /// Fetch current position mode.
3529 ///
3530 /// # Arguments
3531 ///
3532 /// * `params` - Optional parameters.
3533 ///
3534 /// # Returns
3535 ///
3536 /// Returns the current position mode:
3537 /// - `true`: Hedge mode (dual-side position).
3538 /// - `false`: One-way mode.
3539 ///
3540 /// # Errors
3541 ///
3542 /// Returns an error if authentication fails or the API request fails.
3543 ///
3544 /// # Example
3545 ///
3546 /// ```no_run
3547 /// # use ccxt_exchanges::binance::Binance;
3548 /// # use ccxt_core::ExchangeConfig;
3549 /// # async fn example() -> ccxt_core::Result<()> {
3550 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
3551 ///
3552 /// let dual_side = binance.fetch_position_mode(None).await?;
3553 /// println!("Hedge mode enabled: {}", dual_side);
3554 /// # Ok(())
3555 /// # }
3556 /// ```
3557 pub async fn fetch_position_mode(&self, params: Option<Value>) -> Result<bool> {
3558 self.check_required_credentials()?;
3559
3560 let mut request_params = HashMap::new();
3561
3562 if let Some(params) = params {
3563 if let Some(obj) = params.as_object() {
3564 for (key, value) in obj {
3565 if let Some(v) = value.as_str() {
3566 request_params.insert(key.clone(), v.to_string());
3567 }
3568 }
3569 }
3570 }
3571
3572 let timestamp = self.fetch_time_raw().await?;
3573 let auth = self.get_auth()?;
3574 let signed_params =
3575 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
3576
3577 let query_string = signed_params
3578 .iter()
3579 .map(|(k, v)| format!("{}={}", k, v))
3580 .collect::<Vec<_>>()
3581 .join("&");
3582
3583 let url = format!(
3584 "{}/positionSide/dual?{}",
3585 self.urls().fapi_private,
3586 query_string
3587 );
3588
3589 let mut headers = HeaderMap::new();
3590 auth.add_auth_headers_reqwest(&mut headers);
3591
3592 let data = self.base().http_client.get(&url, Some(headers)).await?;
3593
3594 if let Some(dual_side) = data.get("dualSidePosition") {
3595 if let Some(value) = dual_side.as_bool() {
3596 return Ok(value);
3597 }
3598 if let Some(value_str) = dual_side.as_str() {
3599 return Ok(value_str.to_lowercase() == "true");
3600 }
3601 }
3602
3603 Err(Error::from(ParseError::invalid_format(
3604 "data",
3605 "Failed to parse position mode response",
3606 )))
3607 }
3608
3609 // ==================== Futures Margin Management ====================
3610
3611 /// Modify isolated position margin.
3612 ///
3613 /// # Arguments
3614 ///
3615 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
3616 /// * `amount` - Adjustment amount (positive to add, negative to reduce).
3617 /// * `params` - Optional parameters:
3618 /// - `type`: Operation type (1=add, 2=reduce). If provided, `amount` sign is ignored.
3619 /// - `positionSide`: Position side "LONG" | "SHORT" (required in hedge mode).
3620 ///
3621 /// # Returns
3622 ///
3623 /// Returns the adjustment result including the new margin amount.
3624 ///
3625 /// # Errors
3626 ///
3627 /// Returns an error if authentication fails or the API request fails.
3628 /// ```rust,no_run
3629 /// # use ccxt_exchanges::binance::Binance;
3630 /// # use ccxt_core::ExchangeConfig;
3631 /// # use rust_decimal_macros::dec;
3632 /// # #[tokio::main]
3633 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3634 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
3635 ///
3636 /// // Add 100 USDT to isolated margin
3637 /// binance.modify_isolated_position_margin("BTC/USDT", dec!(100.0), None).await?;
3638 ///
3639 /// // Reduce 50 USDT from isolated margin
3640 /// binance.modify_isolated_position_margin("BTC/USDT", dec!(-50.0), None).await?;
3641 ///
3642 /// // Adjust long position margin in hedge mode
3643 /// let params = serde_json::json!({"positionSide": "LONG"});
3644 /// binance.modify_isolated_position_margin("BTC/USDT", dec!(100.0), Some(params)).await?;
3645 /// # Ok(())
3646 /// # }
3647 /// ```
3648 pub async fn modify_isolated_position_margin(
3649 &self,
3650 symbol: &str,
3651 amount: Decimal,
3652 params: Option<Value>,
3653 ) -> Result<Value> {
3654 self.check_required_credentials()?;
3655
3656 let market = self.base().market(symbol).await?;
3657 let mut request_params = HashMap::new();
3658 request_params.insert("symbol".to_string(), market.id.clone());
3659 request_params.insert("amount".to_string(), amount.abs().to_string());
3660 request_params.insert(
3661 "type".to_string(),
3662 if amount > Decimal::ZERO {
3663 "1".to_string()
3664 } else {
3665 "2".to_string()
3666 },
3667 );
3668
3669 if let Some(params) = params {
3670 if let Some(obj) = params.as_object() {
3671 for (key, value) in obj {
3672 if let Some(v) = value.as_str() {
3673 request_params.insert(key.clone(), v.to_string());
3674 }
3675 }
3676 }
3677 }
3678
3679 let timestamp = self.fetch_time_raw().await?;
3680 let auth = self.get_auth()?;
3681 let signed_params =
3682 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
3683
3684 let url = if market.linear.unwrap_or(true) {
3685 format!("{}/positionMargin", self.urls().fapi_private)
3686 } else {
3687 format!("{}/positionMargin", self.urls().dapi_private)
3688 };
3689
3690 let mut headers = HeaderMap::new();
3691 auth.add_auth_headers_reqwest(&mut headers);
3692
3693 let body = serde_json::to_value(&signed_params).map_err(|e| {
3694 Error::from(ParseError::invalid_format(
3695 "data",
3696 format!("Failed to serialize params: {}", e),
3697 ))
3698 })?;
3699
3700 let data = self
3701 .base()
3702 .http_client
3703 .post(&url, Some(headers), Some(body))
3704 .await?;
3705
3706 Ok(data)
3707 }
3708
3709 /// Fetch position risk information.
3710 ///
3711 /// Retrieves risk information for all futures positions, including unrealized PnL,
3712 /// liquidation price, leverage, etc.
3713 ///
3714 /// # Arguments
3715 ///
3716 /// * `symbol` - Optional trading pair symbol. `None` returns all positions.
3717 /// * `params` - Optional parameters.
3718 ///
3719 /// # Returns
3720 ///
3721 /// Returns position risk information array.
3722 ///
3723 /// # Errors
3724 ///
3725 /// Returns an error if authentication fails or the API request fails.
3726 ///
3727 /// # Examples
3728 /// ```rust,no_run
3729 /// # use ccxt_exchanges::binance::Binance;
3730 /// # use ccxt_core::ExchangeConfig;
3731 /// # #[tokio::main]
3732 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3733 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
3734 ///
3735 /// // Fetch all position risks
3736 /// let risks = binance.fetch_position_risk(None, None).await?;
3737 ///
3738 /// // Fetch position risk for specific symbol
3739 /// let risk = binance.fetch_position_risk(Some("BTC/USDT"), None).await?;
3740 /// # Ok(())
3741 /// # }
3742 /// ```
3743 pub async fn fetch_position_risk(
3744 &self,
3745 symbol: Option<&str>,
3746 params: Option<Value>,
3747 ) -> Result<Value> {
3748 self.check_required_credentials()?;
3749
3750 let mut request_params = HashMap::new();
3751
3752 if let Some(sym) = symbol {
3753 let market = self.base().market(sym).await?;
3754 request_params.insert("symbol".to_string(), market.id.clone());
3755 }
3756
3757 if let Some(params) = params {
3758 if let Some(obj) = params.as_object() {
3759 for (key, value) in obj {
3760 if let Some(v) = value.as_str() {
3761 request_params.insert(key.clone(), v.to_string());
3762 }
3763 }
3764 }
3765 }
3766
3767 let timestamp = self.fetch_time_raw().await?;
3768 let auth = self.get_auth()?;
3769 let signed_params =
3770 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
3771
3772 let query_string: String = signed_params
3773 .iter()
3774 .map(|(k, v)| format!("{}={}", k, v))
3775 .collect::<Vec<_>>()
3776 .join("&");
3777
3778 let url = format!("{}/positionRisk?{}", self.urls().fapi_private, query_string);
3779
3780 let mut headers = HeaderMap::new();
3781 auth.add_auth_headers_reqwest(&mut headers);
3782
3783 let data = self.base().http_client.get(&url, Some(headers)).await?;
3784
3785 Ok(data)
3786 }
3787
3788 /// Fetch leverage bracket information.
3789 ///
3790 /// Retrieves leverage bracket information for specified or all trading pairs,
3791 /// showing maximum leverage for different notional value tiers.
3792 ///
3793 /// # Arguments
3794 ///
3795 /// * `symbol` - Optional trading pair symbol. `None` returns all pairs.
3796 /// * `params` - Optional parameters.
3797 ///
3798 /// # Returns
3799 ///
3800 /// Returns leverage bracket information including maximum notional value and
3801 /// corresponding maximum leverage for each tier.
3802 ///
3803 /// # Errors
3804 ///
3805 /// Returns an error if authentication fails or the API request fails.
3806 ///
3807 /// # Examples
3808 /// ```rust,no_run
3809 /// # use ccxt_exchanges::binance::Binance;
3810 /// # use ccxt_core::ExchangeConfig;
3811 /// # #[tokio::main]
3812 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3813 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
3814 ///
3815 /// // Fetch all leverage brackets
3816 /// let brackets = binance.fetch_leverage_bracket(None, None).await?;
3817 ///
3818 /// // Fetch bracket for specific symbol
3819 /// let bracket = binance.fetch_leverage_bracket(Some("BTC/USDT"), None).await?;
3820 /// # Ok(())
3821 /// # }
3822 /// ```
3823 ///
3824 /// # Response Example
3825 /// ```json
3826 /// [
3827 /// {
3828 /// "symbol": "BTCUSDT",
3829 /// "brackets": [
3830 /// {
3831 /// "bracket": 1,
3832 /// "initialLeverage": 125,
3833 /// "notionalCap": 50000,
3834 /// "notionalFloor": 0,
3835 /// "maintMarginRatio": 0.004,
3836 /// "cum": 0
3837 /// },
3838 /// {
3839 /// "bracket": 2,
3840 /// "initialLeverage": 100,
3841 /// "notionalCap": 250000,
3842 /// "notionalFloor": 50000,
3843 /// "maintMarginRatio": 0.005,
3844 /// "cum": 50
3845 /// }
3846 /// ]
3847 /// }
3848 /// ]
3849 /// ```
3850 pub async fn fetch_leverage_bracket(
3851 &self,
3852 symbol: Option<&str>,
3853 params: Option<Value>,
3854 ) -> Result<Value> {
3855 self.check_required_credentials()?;
3856
3857 let mut request_params = HashMap::new();
3858
3859 if let Some(sym) = symbol {
3860 let market = self.base().market(sym).await?;
3861 request_params.insert("symbol".to_string(), market.id.clone());
3862 }
3863
3864 if let Some(params) = params {
3865 if let Some(obj) = params.as_object() {
3866 for (key, value) in obj {
3867 if let Some(v) = value.as_str() {
3868 request_params.insert(key.clone(), v.to_string());
3869 }
3870 }
3871 }
3872 }
3873
3874 let timestamp = self.fetch_time_raw().await?;
3875 let auth = self.get_auth()?;
3876 let signed_params =
3877 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
3878
3879 let query_string: String = signed_params
3880 .iter()
3881 .map(|(k, v)| format!("{}={}", k, v))
3882 .collect::<Vec<_>>()
3883 .join("&");
3884
3885 let url = format!(
3886 "{}/leverageBracket?{}",
3887 self.urls().fapi_private,
3888 query_string
3889 );
3890
3891 let mut headers = HeaderMap::new();
3892 auth.add_auth_headers_reqwest(&mut headers);
3893
3894 let data = self.base().http_client.get(&url, Some(headers)).await?;
3895
3896 Ok(data)
3897 }
3898
3899 // ==================== Margin Trading ====================
3900
3901 /// Borrow funds in cross margin mode.
3902 ///
3903 /// # Arguments
3904 ///
3905 /// * `currency` - Currency code.
3906 /// * `amount` - Borrow amount.
3907 ///
3908 /// # Returns
3909 ///
3910 /// Returns a margin loan record.
3911 ///
3912 /// # Errors
3913 ///
3914 /// Returns an error if authentication fails or the API request fails.
3915 pub async fn borrow_cross_margin(&self, currency: &str, amount: f64) -> Result<MarginLoan> {
3916 let mut params = HashMap::new();
3917 params.insert("asset".to_string(), currency.to_string());
3918 params.insert("amount".to_string(), amount.to_string());
3919
3920 let timestamp = self.fetch_time_raw().await?;
3921 let auth = self.get_auth()?;
3922 let signed_params =
3923 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
3924
3925 let url = format!("{}/sapi/v1/margin/loan", self.urls().sapi);
3926
3927 let mut headers = HeaderMap::new();
3928 auth.add_auth_headers_reqwest(&mut headers);
3929
3930 let body = serde_json::to_value(&signed_params).map_err(|e| {
3931 Error::from(ParseError::invalid_format(
3932 "data",
3933 format!("Failed to serialize params: {}", e),
3934 ))
3935 })?;
3936
3937 let data = self
3938 .base()
3939 .http_client
3940 .post(&url, Some(headers), Some(body))
3941 .await?;
3942
3943 parser::parse_margin_loan(&data)
3944 }
3945
3946 /// Borrow funds in isolated margin mode.
3947 ///
3948 /// # Arguments
3949 ///
3950 /// * `symbol` - Trading pair symbol.
3951 /// * `currency` - Currency code.
3952 /// * `amount` - Borrow amount.
3953 ///
3954 /// # Returns
3955 ///
3956 /// Returns a margin loan record.
3957 ///
3958 /// # Errors
3959 ///
3960 /// Returns an error if authentication fails or the API request fails.
3961 pub async fn borrow_isolated_margin(
3962 &self,
3963 symbol: &str,
3964 currency: &str,
3965 amount: f64,
3966 ) -> Result<MarginLoan> {
3967 self.load_markets(false).await?;
3968 let market = self.base().market(symbol).await?;
3969
3970 let mut params = HashMap::new();
3971 params.insert("asset".to_string(), currency.to_string());
3972 params.insert("amount".to_string(), amount.to_string());
3973 params.insert("symbol".to_string(), market.id.clone());
3974 params.insert("isIsolated".to_string(), "TRUE".to_string());
3975
3976 let timestamp = self.fetch_time_raw().await?;
3977 let auth = self.get_auth()?;
3978 let signed_params =
3979 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
3980
3981 let url = format!("{}/sapi/v1/margin/loan", self.urls().sapi);
3982
3983 let mut headers = HeaderMap::new();
3984 auth.add_auth_headers_reqwest(&mut headers);
3985
3986 let body = serde_json::to_value(&signed_params).map_err(|e| {
3987 Error::from(ParseError::invalid_format(
3988 "data",
3989 format!("Failed to serialize params: {}", e),
3990 ))
3991 })?;
3992
3993 let data = self
3994 .base()
3995 .http_client
3996 .post(&url, Some(headers), Some(body))
3997 .await?;
3998
3999 parser::parse_margin_loan(&data)
4000 }
4001
4002 /// Repay borrowed funds in cross margin mode.
4003 ///
4004 /// # Arguments
4005 ///
4006 /// * `currency` - Currency code.
4007 /// * `amount` - Repayment amount.
4008 ///
4009 /// # Returns
4010 ///
4011 /// Returns a margin repayment record.
4012 ///
4013 /// # Errors
4014 ///
4015 /// Returns an error if authentication fails or the API request fails.
4016 pub async fn repay_cross_margin(&self, currency: &str, amount: f64) -> Result<MarginRepay> {
4017 let mut params = HashMap::new();
4018 params.insert("asset".to_string(), currency.to_string());
4019 params.insert("amount".to_string(), amount.to_string());
4020
4021 let timestamp = self.fetch_time_raw().await?;
4022 let auth = self.get_auth()?;
4023 let signed_params =
4024 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
4025
4026 let url = format!("{}/sapi/v1/margin/repay", self.urls().sapi);
4027
4028 let mut headers = HeaderMap::new();
4029 auth.add_auth_headers_reqwest(&mut headers);
4030
4031 let body = serde_json::to_value(&signed_params).map_err(|e| {
4032 Error::from(ParseError::invalid_format(
4033 "data",
4034 format!("Failed to serialize params: {}", e),
4035 ))
4036 })?;
4037
4038 let data = self
4039 .base()
4040 .http_client
4041 .post(&url, Some(headers), Some(body))
4042 .await?;
4043
4044 let loan = parser::parse_margin_loan(&data)?;
4045
4046 Ok(MarginRepay {
4047 id: loan.id,
4048 currency: loan.currency,
4049 amount: loan.amount,
4050 symbol: loan.symbol,
4051 timestamp: loan.timestamp,
4052 datetime: loan.datetime,
4053 status: loan.status,
4054 is_isolated: loan.is_isolated,
4055 info: loan.info,
4056 })
4057 }
4058
4059 /// Repay borrowed funds in isolated margin mode.
4060 ///
4061 /// # Arguments
4062 ///
4063 /// * `symbol` - Trading pair symbol.
4064 /// * `currency` - Currency code.
4065 /// * `amount` - Repayment amount.
4066 ///
4067 /// # Returns
4068 ///
4069 /// Returns a margin repayment record.
4070 ///
4071 /// # Errors
4072 ///
4073 /// Returns an error if authentication fails or the API request fails.
4074 pub async fn repay_isolated_margin(
4075 &self,
4076 symbol: &str,
4077 currency: &str,
4078 amount: f64,
4079 ) -> Result<MarginRepay> {
4080 self.load_markets(false).await?;
4081 let market = self.base().market(symbol).await?;
4082
4083 let mut params = HashMap::new();
4084 params.insert("asset".to_string(), currency.to_string());
4085 params.insert("amount".to_string(), amount.to_string());
4086 params.insert("symbol".to_string(), market.id.clone());
4087 params.insert("isIsolated".to_string(), "TRUE".to_string());
4088
4089 let timestamp = self.fetch_time_raw().await?;
4090 let auth = self.get_auth()?;
4091 let signed_params =
4092 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
4093
4094 let url = format!("{}/sapi/v1/margin/repay", self.urls().sapi);
4095
4096 let mut headers = HeaderMap::new();
4097 auth.add_auth_headers_reqwest(&mut headers);
4098
4099 let body = serde_json::to_value(&signed_params).map_err(|e| {
4100 Error::from(ParseError::invalid_format(
4101 "data",
4102 format!("Failed to serialize params: {}", e),
4103 ))
4104 })?;
4105
4106 let data = self
4107 .base()
4108 .http_client
4109 .post(&url, Some(headers), Some(body))
4110 .await?;
4111
4112 let loan = parser::parse_margin_loan(&data)?;
4113
4114 Ok(MarginRepay {
4115 id: loan.id,
4116 currency: loan.currency,
4117 amount: loan.amount,
4118 symbol: loan.symbol,
4119 timestamp: loan.timestamp,
4120 datetime: loan.datetime,
4121 status: loan.status,
4122 is_isolated: loan.is_isolated,
4123 info: loan.info,
4124 })
4125 }
4126
4127 /// Fetch margin adjustment history.
4128 ///
4129 /// Retrieves liquidation records and margin adjustment history.
4130 ///
4131 /// # Arguments
4132 /// * `symbol` - Optional trading pair symbol (required for isolated margin)
4133 /// * `since` - Start timestamp in milliseconds
4134 /// * `limit` - Maximum number of records to return
4135 ///
4136 /// # Returns
4137 /// Returns a list of margin adjustments.
4138 ///
4139 /// # Errors
4140 /// Returns an error if authentication fails or the API request fails.
4141 pub async fn fetch_margin_adjustment_history(
4142 &self,
4143 symbol: Option<&str>,
4144 since: Option<i64>,
4145 limit: Option<i64>,
4146 ) -> Result<Vec<MarginAdjustment>> {
4147 let mut params = HashMap::new();
4148
4149 if let Some(sym) = symbol {
4150 self.load_markets(false).await?;
4151 let market = self.base().market(sym).await?;
4152 params.insert("symbol".to_string(), market.id.clone());
4153 params.insert("isolatedSymbol".to_string(), market.id.clone());
4154 }
4155
4156 if let Some(start_time) = since {
4157 params.insert("startTime".to_string(), start_time.to_string());
4158 }
4159
4160 if let Some(size) = limit {
4161 params.insert("size".to_string(), size.to_string());
4162 }
4163
4164 let timestamp = self.fetch_time_raw().await?;
4165 let auth = self.get_auth()?;
4166 let _signed_params =
4167 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
4168
4169 let url = format!("{}/sapi/v1/margin/forceLiquidationRec", self.urls().sapi);
4170
4171 let mut headers = HeaderMap::new();
4172 auth.add_auth_headers_reqwest(&mut headers);
4173
4174 let data = self.base().http_client.get(&url, Some(headers)).await?;
4175
4176 let rows = data["rows"].as_array().ok_or_else(|| {
4177 Error::from(ParseError::invalid_format(
4178 "data",
4179 "Expected rows array in response",
4180 ))
4181 })?;
4182
4183 let mut results = Vec::new();
4184 for item in rows {
4185 if let Ok(adjustment) = parser::parse_margin_adjustment(item) {
4186 results.push(adjustment);
4187 }
4188 }
4189
4190 Ok(results)
4191 }
4192 /// Fetch account balance.
4193 ///
4194 /// Retrieves balance information for various account types including spot,
4195 /// margin, futures, savings, and funding accounts.
4196 ///
4197 /// # Arguments
4198 /// * `params` - Optional parameters
4199 /// - `type`: Account type ("spot", "margin", "isolated", "linear", "inverse", "savings", "funding", "papi")
4200 /// - `marginMode`: Margin mode ("cross", "isolated")
4201 /// - `symbols`: List of isolated margin trading pairs (used with "isolated" type)
4202 ///
4203 /// # Returns
4204 /// Returns account balance information.
4205 ///
4206 /// # Errors
4207 /// Returns an error if authentication fails or the API request fails.
4208 pub async fn fetch_balance(&self, params: Option<HashMap<String, Value>>) -> Result<Balance> {
4209 let params = params.unwrap_or_default();
4210
4211 let account_type = params
4212 .get("type")
4213 .and_then(|v| v.as_str())
4214 .unwrap_or("spot");
4215 let _margin_mode = params.get("marginMode").and_then(|v| v.as_str());
4216
4217 let (url, method) = match account_type {
4218 "spot" => (format!("{}/account", self.urls().public), "GET"),
4219 "margin" | "cross" => (
4220 format!("{}/sapi/v1/margin/account", self.urls().sapi),
4221 "GET",
4222 ),
4223 "isolated" => (
4224 format!("{}/sapi/v1/margin/isolated/account", self.urls().sapi),
4225 "GET",
4226 ),
4227 "linear" | "future" => (
4228 format!("{}/balance", self.urls().fapi_public.replace("/v1", "/v2")),
4229 "GET",
4230 ),
4231 "inverse" | "delivery" => (format!("{}/account", self.urls().dapi_public), "GET"),
4232 "savings" => (
4233 format!("{}/sapi/v1/lending/union/account", self.urls().sapi),
4234 "GET",
4235 ),
4236 "funding" => (
4237 format!("{}/asset/get-funding-asset", self.urls().sapi),
4238 "POST",
4239 ),
4240 "papi" => (format!("{}/papi/v1/balance", self.urls().sapi), "GET"),
4241 _ => (format!("{}/account", self.urls().public), "GET"),
4242 };
4243
4244 let mut request_params = HashMap::new();
4245
4246 if account_type == "isolated" {
4247 if let Some(symbols) = params.get("symbols").and_then(|v| v.as_array()) {
4248 let symbols_str: Vec<String> = symbols
4249 .iter()
4250 .filter_map(|v| v.as_str())
4251 .map(|s| s.to_string())
4252 .collect();
4253 if !symbols_str.is_empty() {
4254 request_params.insert("symbols".to_string(), symbols_str.join(","));
4255 }
4256 }
4257 }
4258 let timestamp = self.fetch_time_raw().await?;
4259 let auth = self.get_auth()?;
4260 let signed_params =
4261 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
4262
4263 let query_string = {
4264 let mut pairs: Vec<_> = signed_params.iter().collect();
4265 pairs.sort_by_key(|(k, _)| *k);
4266 pairs
4267 .iter()
4268 .map(|(k, v)| format!("{}={}", k, v))
4269 .collect::<Vec<_>>()
4270 .join("&")
4271 };
4272 let full_url = format!("{}?{}", url, query_string);
4273
4274 let mut headers = HeaderMap::new();
4275 auth.add_auth_headers_reqwest(&mut headers);
4276
4277 let data = if method == "POST" {
4278 self.base()
4279 .http_client
4280 .post(&full_url, Some(headers), None)
4281 .await?
4282 } else {
4283 self.base()
4284 .http_client
4285 .get(&full_url, Some(headers))
4286 .await?
4287 };
4288
4289 parser::parse_balance_with_type(&data, account_type)
4290 }
4291
4292 /// Fetch maximum borrowable amount for cross margin.
4293 ///
4294 /// Queries the maximum amount that can be borrowed for a specific currency
4295 /// in cross margin mode.
4296 ///
4297 /// # Arguments
4298 /// * `code` - Currency code (e.g., "USDT")
4299 /// * `params` - Optional parameters (reserved for future use)
4300 ///
4301 /// # Returns
4302 /// Returns maximum borrowable amount information.
4303 ///
4304 /// # Errors
4305 /// Returns an error if authentication fails or the API request fails.
4306 pub async fn fetch_cross_margin_max_borrowable(
4307 &self,
4308 code: &str,
4309 params: Option<HashMap<String, Value>>,
4310 ) -> Result<MaxBorrowable> {
4311 let _params = params.unwrap_or_default();
4312
4313 let mut request_params = HashMap::new();
4314 request_params.insert("asset".to_string(), code.to_string());
4315
4316 let timestamp = self.fetch_time_raw().await?;
4317 let auth = self.get_auth()?;
4318 let signed_params =
4319 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
4320
4321 let query_string = {
4322 let mut pairs: Vec<_> = signed_params.iter().collect();
4323 pairs.sort_by_key(|(k, _)| *k);
4324 pairs
4325 .iter()
4326 .map(|(k, v)| format!("{}={}", k, v))
4327 .collect::<Vec<_>>()
4328 .join("&")
4329 };
4330 let url = format!(
4331 "{}/sapi/v1/margin/maxBorrowable?{}",
4332 self.urls().sapi,
4333 query_string
4334 );
4335
4336 let mut headers = HeaderMap::new();
4337 auth.add_auth_headers_reqwest(&mut headers);
4338
4339 let data = self.base().http_client.get(&url, Some(headers)).await?;
4340
4341 parser::parse_max_borrowable(&data, code, None)
4342 }
4343
4344 /// Fetch maximum borrowable amount for isolated margin.
4345 ///
4346 /// Queries the maximum amount that can be borrowed for a specific currency
4347 /// in a specific isolated margin trading pair.
4348 ///
4349 /// # Arguments
4350 /// * `code` - Currency code (e.g., "USDT")
4351 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
4352 /// * `params` - Optional parameters (reserved for future use)
4353 ///
4354 /// # Returns
4355 /// Returns maximum borrowable amount information.
4356 ///
4357 /// # Errors
4358 /// Returns an error if authentication fails or the API request fails.
4359 pub async fn fetch_isolated_margin_max_borrowable(
4360 &self,
4361 code: &str,
4362 symbol: &str,
4363 params: Option<HashMap<String, Value>>,
4364 ) -> Result<MaxBorrowable> {
4365 let _params = params.unwrap_or_default();
4366
4367 self.load_markets(false).await?;
4368 let market = self.base().market(symbol).await?;
4369
4370 let mut request_params = HashMap::new();
4371 request_params.insert("asset".to_string(), code.to_string());
4372 request_params.insert("isolatedSymbol".to_string(), market.id.clone());
4373
4374 let timestamp = self.fetch_time_raw().await?;
4375 let auth = self.get_auth()?;
4376 let signed_params =
4377 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
4378
4379 let query_string = signed_params
4380 .iter()
4381 .map(|(k, v)| format!("{}={}", k, v))
4382 .collect::<Vec<_>>()
4383 .join("&");
4384 let url = format!(
4385 "{}/sapi/v1/margin/isolated/maxBorrowable?{}",
4386 self.urls().sapi,
4387 query_string
4388 );
4389
4390 let mut headers = HeaderMap::new();
4391 auth.add_auth_headers_reqwest(&mut headers);
4392
4393 let data = self.base().http_client.get(&url, Some(headers)).await?;
4394
4395 parser::parse_max_borrowable(&data, code, Some(symbol.to_string()))
4396 }
4397
4398 /// Fetch maximum transferable amount.
4399 ///
4400 /// Queries the maximum amount that can be transferred out of margin account.
4401 ///
4402 /// # Arguments
4403 /// * `code` - Currency code (e.g., "USDT")
4404 /// * `params` - Optional parameters
4405 /// - `symbol`: Isolated margin trading pair symbol (e.g., "BTC/USDT")
4406 ///
4407 /// # Returns
4408 /// Returns maximum transferable amount information.
4409 ///
4410 /// # Errors
4411 /// Returns an error if authentication fails or the API request fails.
4412 pub async fn fetch_max_transferable(
4413 &self,
4414 code: &str,
4415 params: Option<HashMap<String, Value>>,
4416 ) -> Result<MaxTransferable> {
4417 let params = params.unwrap_or_default();
4418
4419 let mut request_params = HashMap::new();
4420 request_params.insert("asset".to_string(), code.to_string());
4421
4422 let symbol_opt: Option<String> = if let Some(symbol_value) = params.get("symbol") {
4423 if let Some(symbol_str) = symbol_value.as_str() {
4424 self.load_markets(false).await?;
4425 let market = self.base().market(symbol_str).await?;
4426 request_params.insert("isolatedSymbol".to_string(), market.id.clone());
4427 Some(symbol_str.to_string())
4428 } else {
4429 None
4430 }
4431 } else {
4432 None
4433 };
4434
4435 let timestamp = self.fetch_time_raw().await?;
4436 let auth = self.get_auth()?;
4437 let signed_params =
4438 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
4439
4440 let query_string = signed_params
4441 .iter()
4442 .map(|(k, v)| format!("{}={}", k, v))
4443 .collect::<Vec<_>>()
4444 .join("&");
4445 let url = format!(
4446 "{}/sapi/v1/margin/maxTransferable?{}",
4447 self.urls().sapi,
4448 query_string
4449 );
4450
4451 let mut headers = HeaderMap::new();
4452 auth.add_auth_headers_reqwest(&mut headers);
4453
4454 let data = self.base().http_client.get(&url, Some(headers)).await?;
4455
4456 parser::parse_max_transferable(&data, code, symbol_opt)
4457 }
4458
4459 // ============================================================================
4460 // Enhanced Market Data API
4461 // ============================================================================
4462
4463 /// Fetch bid/ask prices (Book Ticker)
4464 ///
4465 /// # Arguments
4466 ///
4467 /// * `symbol` - Optional trading pair symbol; if omitted, returns all symbols
4468 ///
4469 /// # Returns
4470 ///
4471 /// Returns bid/ask price information
4472 ///
4473 /// # API Endpoint
4474 ///
4475 /// * GET `/api/v3/ticker/bookTicker`
4476 /// * Weight: 1 for single symbol, 2 for all symbols
4477 /// * Requires signature: No
4478 ///
4479 /// # Example
4480 ///
4481 /// ```no_run
4482 /// # use ccxt_exchanges::binance::Binance;
4483 /// # use ccxt_core::ExchangeConfig;
4484 /// # async fn example() -> ccxt_core::Result<()> {
4485 /// let binance = Binance::new(ExchangeConfig::default())?;
4486 ///
4487 /// // Fetch bid/ask for single symbol
4488 /// let bid_ask = binance.fetch_bids_asks(Some("BTC/USDT")).await?;
4489 /// println!("BTC/USDT bid: {}, ask: {}", bid_ask[0].bid, bid_ask[0].ask);
4490 ///
4491 /// // Fetch bid/ask for all symbols
4492 /// let all_bid_asks = binance.fetch_bids_asks(None).await?;
4493 /// println!("Total symbols: {}", all_bid_asks.len());
4494 /// # Ok(())
4495 /// # }
4496 /// ```
4497 pub async fn fetch_bids_asks(&self, symbol: Option<&str>) -> Result<Vec<BidAsk>> {
4498 self.load_markets(false).await?;
4499
4500 let url = if let Some(sym) = symbol {
4501 let market = self.base().market(sym).await?;
4502 format!(
4503 "{}/ticker/bookTicker?symbol={}",
4504 self.urls().public,
4505 market.id
4506 )
4507 } else {
4508 format!("{}/ticker/bookTicker", self.urls().public)
4509 };
4510
4511 let data = self.base().http_client.get(&url, None).await?;
4512
4513 parser::parse_bids_asks(&data)
4514 }
4515
4516 /// Fetch latest prices.
4517 ///
4518 /// Retrieves the most recent price for one or all trading pairs.
4519 ///
4520 /// # Arguments
4521 ///
4522 /// * `symbol` - Optional trading pair symbol; if omitted, returns all symbols
4523 ///
4524 /// # Returns
4525 ///
4526 /// Returns latest price information.
4527 ///
4528 /// # Errors
4529 ///
4530 /// Returns an error if the API request fails.
4531 ///
4532 /// # Examples
4533 ///
4534 /// ```no_run
4535 /// # use ccxt_exchanges::binance::Binance;
4536 /// # use ccxt_core::ExchangeConfig;
4537 /// # async fn example() -> ccxt_core::Result<()> {
4538 /// let binance = Binance::new(ExchangeConfig::default())?;
4539 ///
4540 /// // Fetch latest price for single symbol
4541 /// let price = binance.fetch_last_prices(Some("BTC/USDT")).await?;
4542 /// println!("BTC/USDT last price: {}", price[0].price);
4543 ///
4544 /// // Fetch latest prices for all symbols
4545 /// let all_prices = binance.fetch_last_prices(None).await?;
4546 /// println!("Total symbols: {}", all_prices.len());
4547 /// # Ok(())
4548 /// # }
4549 /// ```
4550 pub async fn fetch_last_prices(&self, symbol: Option<&str>) -> Result<Vec<LastPrice>> {
4551 self.load_markets(false).await?;
4552
4553 let url = if let Some(sym) = symbol {
4554 let market = self.base().market(sym).await?;
4555 format!("{}/ticker/price?symbol={}", self.urls().public, market.id)
4556 } else {
4557 format!("{}/ticker/price", self.urls().public)
4558 };
4559
4560 let data = self.base().http_client.get(&url, None).await?;
4561
4562 parser::parse_last_prices(&data)
4563 }
4564
4565 /// Fetch futures mark prices.
4566 ///
4567 /// Retrieves mark prices for futures contracts, used for calculating unrealized PnL.
4568 /// Includes funding rates and next funding time.
4569 ///
4570 /// # Arguments
4571 ///
4572 /// * `symbol` - Optional trading pair symbol; if omitted, returns all futures pairs
4573 ///
4574 /// # Returns
4575 ///
4576 /// Returns mark price information including funding rates.
4577 ///
4578 /// # Errors
4579 ///
4580 /// Returns an error if the API request fails.
4581 ///
4582 /// # Note
4583 ///
4584 /// This API only applies to futures markets.
4585 ///
4586 /// # Examples
4587 ///
4588 /// ```no_run
4589 /// # use ccxt_exchanges::binance::Binance;
4590 /// # use ccxt_core::ExchangeConfig;
4591 /// # async fn example() -> ccxt_core::Result<()> {
4592 /// let binance = Binance::new(ExchangeConfig::default())?;
4593 ///
4594 /// // Fetch mark price for single futures symbol
4595 /// let mark_price = binance.fetch_mark_price(Some("BTC/USDT")).await?;
4596 /// println!("BTC/USDT mark price: {}", mark_price[0].mark_price);
4597 /// println!("Funding rate: {}", mark_price[0].last_funding_rate);
4598 ///
4599 /// // Fetch mark prices for all futures symbols
4600 /// let all_mark_prices = binance.fetch_mark_price(None).await?;
4601 /// println!("Total futures symbols: {}", all_mark_prices.len());
4602 /// # Ok(())
4603 /// # }
4604 /// ```
4605 pub async fn fetch_mark_price(&self, symbol: Option<&str>) -> Result<Vec<MarkPrice>> {
4606 self.load_markets(false).await?;
4607
4608 let url = if let Some(sym) = symbol {
4609 let market = self.base().market(sym).await?;
4610 format!(
4611 "{}/premiumIndex?symbol={}",
4612 self.urls().fapi_public,
4613 market.id
4614 )
4615 } else {
4616 format!("{}/premiumIndex", self.urls().fapi_public)
4617 };
4618
4619 let data = self.base().http_client.get(&url, None).await?;
4620
4621 parser::parse_mark_prices(&data)
4622 }
4623 /// Parse timeframe string into seconds.
4624 ///
4625 /// Converts a timeframe string like "1m", "5m", "1h", "1d" into the equivalent number of seconds.
4626 ///
4627 /// # Arguments
4628 ///
4629 /// * `timeframe` - Timeframe string such as "1m", "5m", "1h", "1d"
4630 ///
4631 /// # Returns
4632 ///
4633 /// Returns the time interval in seconds.
4634 ///
4635 /// # Errors
4636 ///
4637 /// Returns an error if the timeframe is empty or has an invalid format.
4638 fn parse_timeframe(&self, timeframe: &str) -> Result<i64> {
4639 let unit_map = [
4640 ("s", 1),
4641 ("m", 60),
4642 ("h", 3600),
4643 ("d", 86400),
4644 ("w", 604800),
4645 ("M", 2592000),
4646 ("y", 31536000),
4647 ];
4648
4649 if timeframe.is_empty() {
4650 return Err(Error::invalid_request("timeframe cannot be empty"));
4651 }
4652
4653 let mut num_str = String::new();
4654 let mut unit_str = String::new();
4655
4656 for ch in timeframe.chars() {
4657 if ch.is_ascii_digit() {
4658 num_str.push(ch);
4659 } else {
4660 unit_str.push(ch);
4661 }
4662 }
4663
4664 let amount: i64 = if num_str.is_empty() {
4665 1
4666 } else {
4667 num_str.parse().map_err(|_| {
4668 Error::invalid_request(format!("Invalid timeframe format: {}", timeframe))
4669 })?
4670 };
4671
4672 let unit_seconds = unit_map
4673 .iter()
4674 .find(|(unit, _)| unit == &unit_str.as_str())
4675 .map(|(_, seconds)| *seconds)
4676 .ok_or_else(|| {
4677 Error::invalid_request(format!("Unsupported timeframe unit: {}", unit_str))
4678 })?;
4679
4680 Ok(amount * unit_seconds)
4681 }
4682
4683 /// Get OHLCV API endpoint based on market type and price type
4684 ///
4685 /// # Arguments
4686 /// * `market` - Market information
4687 /// * `price` - Price type: None (default) | "mark" | "index" | "premiumIndex"
4688 ///
4689 /// # Returns
4690 /// Returns tuple (base_url, endpoint, use_pair)
4691 /// * `base_url` - API base URL
4692 /// * `endpoint` - Specific endpoint path
4693 /// * `use_pair` - Whether to use pair instead of symbol (required for index price)
4694 ///
4695 /// # API Endpoint Mapping
4696 /// | Market Type | Price Type | API Endpoint |
4697 /// |-------------|------------|--------------|
4698 /// | spot | default | `/api/v3/klines` |
4699 /// | linear | default | `/fapi/v1/klines` |
4700 /// | linear | mark | `/fapi/v1/markPriceKlines` |
4701 /// | linear | index | `/fapi/v1/indexPriceKlines` |
4702 /// | linear | premiumIndex | `/fapi/v1/premiumIndexKlines` |
4703 /// | inverse | default | `/dapi/v1/klines` |
4704 /// | inverse | mark | `/dapi/v1/markPriceKlines` |
4705 /// | inverse | index | `/dapi/v1/indexPriceKlines` |
4706 /// | inverse | premiumIndex | `/dapi/v1/premiumIndexKlines` |
4707 /// | option | default | `/eapi/v1/klines` |
4708 fn get_ohlcv_endpoint(
4709 &self,
4710 market: &Market,
4711 price: Option<&str>,
4712 ) -> Result<(String, String, bool)> {
4713 use ccxt_core::types::MarketType;
4714
4715 if let Some(p) = price {
4716 if !["mark", "index", "premiumIndex"].contains(&p) {
4717 return Err(Error::invalid_request(format!(
4718 "Unsupported price type: {}. Supported types: mark, index, premiumIndex",
4719 p
4720 )));
4721 }
4722 }
4723
4724 match market.market_type {
4725 MarketType::Spot => {
4726 if let Some(p) = price {
4727 return Err(Error::invalid_request(format!(
4728 "Spot market does not support '{}' price type",
4729 p
4730 )));
4731 }
4732 Ok((self.urls().public.clone(), "/klines".to_string(), false))
4733 }
4734
4735 MarketType::Swap | MarketType::Futures => {
4736 let is_linear = market.linear.unwrap_or(false);
4737 let is_inverse = market.inverse.unwrap_or(false);
4738
4739 if is_linear {
4740 let (endpoint, use_pair) = match price {
4741 None => ("/klines".to_string(), false),
4742 Some("mark") => ("/markPriceKlines".to_string(), false),
4743 Some("index") => ("/indexPriceKlines".to_string(), true),
4744 Some("premiumIndex") => ("/premiumIndexKlines".to_string(), false),
4745 _ => unreachable!(),
4746 };
4747 Ok((self.urls().fapi_public.clone(), endpoint, use_pair))
4748 } else if is_inverse {
4749 let (endpoint, use_pair) = match price {
4750 None => ("/klines".to_string(), false),
4751 Some("mark") => ("/markPriceKlines".to_string(), false),
4752 Some("index") => ("/indexPriceKlines".to_string(), true),
4753 Some("premiumIndex") => ("/premiumIndexKlines".to_string(), false),
4754 _ => unreachable!(),
4755 };
4756 Ok((self.urls().dapi_public.clone(), endpoint, use_pair))
4757 } else {
4758 Err(Error::invalid_request(
4759 "Cannot determine futures contract type (linear or inverse)",
4760 ))
4761 }
4762 }
4763
4764 MarketType::Option => {
4765 if let Some(p) = price {
4766 return Err(Error::invalid_request(format!(
4767 "Option market does not support '{}' price type",
4768 p
4769 )));
4770 }
4771 Ok((
4772 self.urls().eapi_public.clone(),
4773 "/klines".to_string(),
4774 false,
4775 ))
4776 }
4777 }
4778 }
4779
4780 /// Fetch OHLCV (candlestick) data
4781 ///
4782 /// # Arguments
4783 ///
4784 /// * `symbol` - Trading pair symbol, e.g., "BTC/USDT"
4785 /// * `timeframe` - Time period, e.g., "1m", "5m", "1h", "1d"
4786 /// * `since` - Start timestamp in milliseconds
4787 /// * `limit` - Maximum number of candlesticks to return
4788 /// * `params` - Optional parameters
4789 /// * `price` - Price type: "mark" | "index" | "premiumIndex" (futures only)
4790 /// * `until` - End timestamp in milliseconds
4791 /// * `paginate` - Whether to automatically paginate (default: false)
4792 ///
4793 /// # Returns
4794 ///
4795 /// Returns OHLCV data array: [timestamp, open, high, low, close, volume]
4796 ///
4797 /// # Examples
4798 ///
4799 /// ```no_run
4800 /// # use ccxt_exchanges::binance::Binance;
4801 /// # use std::collections::HashMap;
4802 /// # use serde_json::json;
4803 /// # async fn example() -> ccxt_core::Result<()> {
4804 /// let binance = Binance::new(Default::default());
4805 ///
4806 /// // Basic usage
4807 /// let ohlcv = binance.fetch_ohlcv("BTC/USDT", "1h", None, Some(100), None).await?;
4808 ///
4809 /// // Using time range
4810 /// let since = 1609459200000; // 2021-01-01
4811 /// let until = 1612137600000; // 2021-02-01
4812 /// let mut params = HashMap::new();
4813 /// params.insert("until".to_string(), json!(until));
4814 /// let ohlcv = binance.fetch_ohlcv("BTC/USDT", "1h", Some(since), None, Some(params)).await?;
4815 ///
4816 /// // Futures mark price candlesticks
4817 /// let mut params = HashMap::new();
4818 /// params.insert("price".to_string(), json!("mark"));
4819 /// let mark_ohlcv = binance.fetch_ohlcv("BTC/USDT:USDT", "1h", None, Some(100), Some(params)).await?;
4820 /// # Ok(())
4821 /// # }
4822 /// ```
4823 pub async fn fetch_ohlcv(
4824 &self,
4825 symbol: &str,
4826 timeframe: &str,
4827 since: Option<i64>,
4828 limit: Option<u32>,
4829 params: Option<HashMap<String, Value>>,
4830 ) -> Result<Vec<ccxt_core::types::OHLCV>> {
4831 self.load_markets(false).await?;
4832
4833 let price = params
4834 .as_ref()
4835 .and_then(|p| p.get("price"))
4836 .and_then(|v| v.as_str())
4837 .map(|s| s.to_string());
4838
4839 let until = params
4840 .as_ref()
4841 .and_then(|p| p.get("until"))
4842 .and_then(|v| v.as_i64());
4843
4844 let paginate = params
4845 .as_ref()
4846 .and_then(|p| p.get("paginate"))
4847 .and_then(|v| v.as_bool())
4848 .unwrap_or(false);
4849
4850 // TODO: Phase 4 - Implement pagination
4851 if paginate {
4852 return Err(Error::not_implemented(
4853 "Pagination feature not yet implemented, will be added in Phase 4",
4854 ));
4855 }
4856
4857 let market = self.base().market(symbol).await?;
4858
4859 let default_limit = 500u32;
4860 let max_limit = 1500u32;
4861
4862 let adjusted_limit = if since.is_some() && until.is_some() && limit.is_none() {
4863 max_limit
4864 } else if let Some(lim) = limit {
4865 lim.min(max_limit)
4866 } else {
4867 default_limit
4868 };
4869
4870 let (base_url, endpoint, use_pair) = self.get_ohlcv_endpoint(&market, price.as_deref())?;
4871
4872 let symbol_param = if use_pair {
4873 market.symbol.replace('/', "")
4874 } else {
4875 market.id.clone()
4876 };
4877
4878 let mut url = format!(
4879 "{}{}?symbol={}&interval={}&limit={}",
4880 base_url, endpoint, symbol_param, timeframe, adjusted_limit
4881 );
4882
4883 if let Some(start_time) = since {
4884 url.push_str(&format!("&startTime={}", start_time));
4885
4886 // Calculate endTime for inverse markets (ref: Go implementation)
4887 if market.inverse.unwrap_or(false) && start_time > 0 && until.is_none() {
4888 let duration = self.parse_timeframe(timeframe)?;
4889 let calculated_end_time =
4890 start_time + (adjusted_limit as i64 * duration * 1000) - 1;
4891 // SAFETY: SystemTime::now() is always after UNIX_EPOCH on any modern system.
4892 // This can only fail if the system clock is set before 1970, which is not
4893 // a supported configuration for this library.
4894 let now = std::time::SystemTime::now()
4895 .duration_since(std::time::UNIX_EPOCH)
4896 .expect("System clock is set before UNIX_EPOCH (1970); this is not supported")
4897 .as_millis() as i64;
4898 let end_time = calculated_end_time.min(now);
4899 url.push_str(&format!("&endTime={}", end_time));
4900 }
4901 }
4902
4903 if let Some(end_time) = until {
4904 url.push_str(&format!("&endTime={}", end_time));
4905 }
4906
4907 let data = self.base().http_client.get(&url, None).await?;
4908
4909 parser::parse_ohlcvs(&data)
4910 }
4911
4912 /// Fetch trading fee for a single trading pair
4913 ///
4914 /// # Arguments
4915 /// * `symbol` - Trading pair symbol (e.g., BTC/USDT)
4916 ///
4917 /// # Binance API
4918 /// - Endpoint: GET /sapi/v1/asset/tradeFee
4919 /// - Weight: 1
4920 /// - Requires signature: Yes
4921 ///
4922 /// # Examples
4923 /// ```rust,no_run
4924 /// # use ccxt_exchanges::binance::Binance;
4925 /// # use ccxt_core::ExchangeConfig;
4926 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
4927 /// let config = ExchangeConfig {
4928 /// api_key: Some("your-api-key".to_string()),
4929 /// secret: Some("your-secret".to_string()),
4930 /// ..Default::default()
4931 /// };
4932 /// let binance = Binance::new(config)?;
4933 ///
4934 /// // Fetch trading fee for BTC/USDT
4935 /// let fee = binance.fetch_trading_fee("BTC/USDT").await?;
4936 /// println!("Maker fee: {}%, Taker fee: {}%",
4937 /// fee.maker * 100.0, fee.taker * 100.0);
4938 /// # Ok(())
4939 /// # }
4940 /// ```
4941 // pub async fn fetch_trading_fee(&self, symbol: &str) -> Result<ccxt_core::types::TradingFee> {
4942 // self.base().load_markets(false).await?;
4943 //
4944 // let market = self.base().market(symbol).await?;
4945 // let mut params = std::collections::HashMap::new();
4946 // params.insert("symbol".to_string(), market.id.clone());
4947 //
4948 // // Sign parameters
4949 // let auth = crate::binance::auth::BinanceAuth::new(
4950 // self.base().config.api_key.as_ref().ok_or_else(|| {
4951 // ccxt_core::error::Error::authentication("API key required")
4952 // })?,
4953 // self.base().config.secret.as_ref().ok_or_else(|| {
4954 // ccxt_core::error::Error::authentication("API secret required")
4955 // })?,
4956 // );
4957 //
4958 // let signed_params = auth.sign_params(¶ms)?;
4959 //
4960 // // Build URL
4961 // let query_string: Vec<String> = signed_params
4962 // .iter()
4963 // .map(|(k, v)| format!("{}={}", k, v))
4964 // .collect();
4965 // let url = format!("{}/asset/tradeFee?{}", self.urls().sapi, query_string.join("&"));
4966 //
4967 // // Add API key to headers
4968 // let mut headers = reqwest::header::HeaderMap::new();
4969 // auth.add_auth_headers_reqwest(&mut headers);
4970 //
4971 // let data = self.base().http_client.get(&url, Some(headers)).await?;
4972 //
4973 // // Parse response array and take first element
4974 // let fees = parser::parse_trading_fees(&data)?;
4975 // fees.into_iter().next().ok_or_else(|| {
4976 // ccxt_core::error::Error::invalid_request("No trading fee data returned")
4977 // })
4978 // }
4979
4980 /// Fetch trading fees for multiple trading pairs
4981 ///
4982 /// # Arguments
4983 /// * `symbols` - Trading pair symbols (e.g., vec!["BTC/USDT", "ETH/USDT"]), or None for all pairs
4984 ///
4985 /// # Binance API
4986 /// - Endpoint: GET /sapi/v1/asset/tradeFee
4987 /// - Weight: 1
4988 /// - Signature required: Yes
4989 ///
4990 /// # Examples
4991 /// ```rust,no_run
4992 /// # use ccxt_exchanges::binance::Binance;
4993 /// # use ccxt_core::ExchangeConfig;
4994 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
4995 /// let config = ExchangeConfig {
4996 /// api_key: Some("your-api-key".to_string()),
4997 /// secret: Some("your-secret".to_string()),
4998 /// ..Default::default()
4999 /// };
5000 /// let binance = Binance::new(config)?;
5001 ///
5002 /// // Fetch fees for all trading pairs
5003 /// let all_fees = binance.fetch_trading_fees(None).await?;
5004 /// println!("Total symbols with fees: {}", all_fees.len());
5005 ///
5006 /// // Fetch fees for specific trading pairs
5007 /// let fees = binance.fetch_trading_fees(Some(vec!["BTC/USDT", "ETH/USDT"])).await?;
5008 /// for fee in &fees {
5009 /// println!("{}: Maker {}%, Taker {}%",
5010 /// fee.symbol, fee.maker * 100.0, fee.taker * 100.0);
5011 /// }
5012 /// # Ok(())
5013 /// # }
5014 /// ```
5015 /// Fetch server time
5016 ///
5017 /// # Binance API
5018 /// - Endpoint: GET /api/v3/time
5019 /// - Weight: 1
5020 /// - Data source: Memory
5021 ///
5022 /// # Examples
5023 /// ```rust,no_run
5024 /// # use ccxt_exchanges::binance::Binance;
5025 /// # use ccxt_core::ExchangeConfig;
5026 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
5027 /// let config = ExchangeConfig::default();
5028 /// let binance = Binance::new(config)?;
5029 ///
5030 /// // Fetch Binance server time
5031 /// let server_time = binance.fetch_time().await?;
5032 /// println!("Server time: {} ({})", server_time.server_time, server_time.datetime);
5033 ///
5034 /// // Check local time offset
5035 /// let local_time = chrono::Utc::now().timestamp_millis();
5036 /// let offset = server_time.server_time - local_time;
5037 /// println!("Time offset: {} ms", offset);
5038 /// # Ok(())
5039 /// # }
5040 /// ```
5041 pub async fn fetch_time(&self) -> Result<ccxt_core::types::ServerTime> {
5042 let url = format!("{}/time", self.urls().public);
5043
5044 let data = self.base().http_client.get(&url, None).await?;
5045
5046 parser::parse_server_time(&data)
5047 }
5048
5049 /// Fetch canceled orders
5050 ///
5051 /// # Arguments
5052 ///
5053 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
5054 /// * `since` - Optional start timestamp in milliseconds
5055 /// * `limit` - Optional record limit (default 500, max 1000)
5056 ///
5057 /// # Returns
5058 ///
5059 /// Returns vector of canceled orders
5060 ///
5061 /// # Example
5062 ///
5063 /// ```no_run
5064 /// # use ccxt_exchanges::binance::Binance;
5065 /// # use ccxt_core::ExchangeConfig;
5066 /// # async fn example() -> ccxt_core::Result<()> {
5067 /// let binance = Binance::new(ExchangeConfig::default())?;
5068 /// let orders = binance.fetch_canceled_orders("BTC/USDT", None, Some(10)).await?;
5069 /// println!("Found {} canceled orders", orders.len());
5070 /// # Ok(())
5071 /// # }
5072 /// ```
5073 /// Fetch canceled and closed orders
5074 ///
5075 /// # Arguments
5076 ///
5077 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
5078 /// * `since` - Optional start timestamp in milliseconds
5079 /// * `limit` - Optional record limit (default 500, max 1000)
5080 ///
5081 /// # Returns
5082 ///
5083 /// Returns vector of canceled and closed orders
5084 ///
5085 /// # Example
5086 ///
5087 /// ```no_run
5088 /// # use ccxt_exchanges::binance::Binance;
5089 /// # use ccxt_core::ExchangeConfig;
5090 /// # async fn example() -> ccxt_core::Result<()> {
5091 /// let binance = Binance::new(ExchangeConfig::default())?;
5092 /// let orders = binance.fetch_canceled_and_closed_orders("BTC/USDT", None, Some(20)).await?;
5093 /// println!("Found {} closed/canceled orders", orders.len());
5094 /// # Ok(())
5095 /// # }
5096 /// ```
5097 pub async fn fetch_canceled_and_closed_orders(
5098 &self,
5099 symbol: &str,
5100 since: Option<u64>,
5101 limit: Option<u64>,
5102 ) -> Result<Vec<Order>> {
5103 self.check_required_credentials()?;
5104
5105 let market = self.base().market(symbol).await?;
5106 let market_id = &market.id;
5107
5108 let mut params = HashMap::new();
5109 params.insert("symbol".to_string(), market_id.to_string());
5110
5111 if let Some(ts) = since {
5112 params.insert("startTime".to_string(), ts.to_string());
5113 }
5114
5115 if let Some(l) = limit {
5116 params.insert("limit".to_string(), l.to_string());
5117 }
5118
5119 let timestamp = self.fetch_time_raw().await?;
5120 let auth = self.get_auth()?;
5121 let signed_params =
5122 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
5123
5124 let mut url = format!("{}/api/v3/allOrders?", self.urls().private);
5125 for (key, value) in &signed_params {
5126 url.push_str(&format!("{}={}&", key, value));
5127 }
5128
5129 let mut headers = HeaderMap::new();
5130 auth.add_auth_headers_reqwest(&mut headers);
5131
5132 let data = self.base().http_client.get(&url, Some(headers)).await?;
5133
5134 let all_orders = if let Some(array) = data.as_array() {
5135 array
5136 .iter()
5137 .filter_map(|item| parser::parse_order(item, None).ok())
5138 .collect::<Vec<_>>()
5139 } else {
5140 vec![]
5141 };
5142
5143 Ok(all_orders
5144 .into_iter()
5145 .filter(|order| {
5146 order.status == OrderStatus::Canceled || order.status == OrderStatus::Closed
5147 })
5148 .collect())
5149 }
5150
5151 /// Fetch a single open order
5152 ///
5153 /// # Arguments
5154 ///
5155 /// * `id` - Order ID
5156 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
5157 ///
5158 /// # Returns
5159 ///
5160 /// Returns order information
5161 ///
5162 /// # Example
5163 ///
5164 /// ```no_run
5165 /// # use ccxt_exchanges::binance::Binance;
5166 /// # use ccxt_core::ExchangeConfig;
5167 /// # async fn example() -> ccxt_core::Result<()> {
5168 /// let binance = Binance::new(ExchangeConfig::default())?;
5169 /// let order = binance.fetch_open_order("12345", "BTC/USDT").await?;
5170 /// println!("Order status: {:?}", order.status);
5171 /// # Ok(())
5172 /// # }
5173 /// ```
5174 pub async fn fetch_open_order(&self, id: &str, symbol: &str) -> Result<Order> {
5175 self.check_required_credentials()?;
5176
5177 let market = self.base().market(symbol).await?;
5178 let market_id = &market.id;
5179
5180 let mut params = HashMap::new();
5181 params.insert("symbol".to_string(), market_id.to_string());
5182 params.insert("orderId".to_string(), id.to_string());
5183
5184 let timestamp = self.fetch_time_raw().await?;
5185 let auth = self.get_auth()?;
5186 let signed_params =
5187 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
5188
5189 let mut url = format!("{}/api/v3/order?", self.urls().private);
5190 for (key, value) in &signed_params {
5191 url.push_str(&format!("{}={}&", key, value));
5192 }
5193
5194 let mut headers = HeaderMap::new();
5195 auth.add_auth_headers_reqwest(&mut headers);
5196
5197 let data = self.base().http_client.get(&url, Some(headers)).await?;
5198
5199 parser::parse_order(&data, None)
5200 }
5201
5202 /// Fetch trades for a specific order
5203 ///
5204 /// # Arguments
5205 ///
5206 /// * `id` - Order ID
5207 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
5208 /// * `since` - Optional start timestamp in milliseconds
5209 /// * `limit` - Optional record limit (default 500, max 1000)
5210 ///
5211 /// # Returns
5212 ///
5213 /// Returns vector of trade records
5214 ///
5215 /// # Example
5216 ///
5217 /// ```no_run
5218 /// # use ccxt_exchanges::binance::Binance;
5219 /// # use ccxt_core::ExchangeConfig;
5220 /// # async fn example() -> ccxt_core::Result<()> {
5221 /// let binance = Binance::new(ExchangeConfig::default())?;
5222 /// let trades = binance.fetch_order_trades("12345", "BTC/USDT", None, Some(50)).await?;
5223 /// println!("Order has {} trades", trades.len());
5224 /// # Ok(())
5225 /// # }
5226 /// ```
5227 /// Edit order (cancel and replace)
5228 ///
5229 /// # Arguments
5230 ///
5231 /// * `id` - Original order ID
5232 /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
5233 /// * `order_type` - Order type
5234 /// * `side` - Order side (Buy/Sell)
5235 /// * `amount` - Order quantity
5236 /// * `price` - Optional order price (required for limit orders)
5237 /// * `params` - Optional additional parameters
5238 ///
5239 /// # Returns
5240 ///
5241 /// Returns new order information
5242 ///
5243 /// # Example
5244 ///
5245 /// ```no_run
5246 /// # use ccxt_exchanges::binance::Binance;
5247 /// # use ccxt_core::{ExchangeConfig, types::{OrderType, OrderSide}};
5248 /// # use rust_decimal_macros::dec;
5249 /// # async fn example() -> ccxt_core::Result<()> {
5250 /// let binance = Binance::new(ExchangeConfig::default())?;
5251 /// let order = binance.edit_order(
5252 /// "12345",
5253 /// "BTC/USDT",
5254 /// OrderType::Limit,
5255 /// OrderSide::Buy,
5256 /// dec!(0.01),
5257 /// Some(dec!(50000.0)),
5258 /// None
5259 /// ).await?;
5260 /// println!("New order ID: {}", order.id);
5261 /// # Ok(())
5262 /// # }
5263 /// ```
5264 pub async fn edit_order(
5265 &self,
5266 id: &str,
5267 symbol: &str,
5268 order_type: OrderType,
5269 side: OrderSide,
5270 amount: rust_decimal::Decimal,
5271 price: Option<rust_decimal::Decimal>,
5272 params: Option<HashMap<String, String>>,
5273 ) -> Result<Order> {
5274 self.check_required_credentials()?;
5275
5276 let market = self.base().market(symbol).await?;
5277 let market_id = &market.id;
5278
5279 let mut request_params = params.unwrap_or_default();
5280
5281 request_params.insert("symbol".to_string(), market_id.to_string());
5282 request_params.insert("cancelOrderId".to_string(), id.to_string());
5283 request_params.insert("side".to_string(), side.to_string().to_uppercase());
5284 request_params.insert("type".to_string(), order_type.to_string().to_uppercase());
5285 request_params.insert("quantity".to_string(), amount.to_string());
5286
5287 if let Some(p) = price {
5288 request_params.insert("price".to_string(), p.to_string());
5289 }
5290
5291 if !request_params.contains_key("cancelReplaceMode") {
5292 request_params.insert(
5293 "cancelReplaceMode".to_string(),
5294 "STOP_ON_FAILURE".to_string(),
5295 );
5296 }
5297
5298 let timestamp = self.fetch_time_raw().await?;
5299 let auth = self.get_auth()?;
5300 let signed_params =
5301 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
5302
5303 let mut url = format!("{}/api/v3/order/cancelReplace?", self.urls().private);
5304 for (key, value) in &signed_params {
5305 url.push_str(&format!("{}={}&", key, value));
5306 }
5307
5308 let mut headers = HeaderMap::new();
5309 auth.add_auth_headers_reqwest(&mut headers);
5310
5311 let data = self
5312 .base()
5313 .http_client
5314 .post(&url, Some(headers), None)
5315 .await?;
5316
5317 parser::parse_edit_order_result(&data, None)
5318 }
5319
5320 /// Edit multiple orders in batch
5321 ///
5322 /// # Arguments
5323 ///
5324 /// * `orders` - Vector of order edit parameters, each element contains:
5325 /// - id: Original order ID
5326 /// - symbol: Trading pair symbol
5327 /// - order_type: Order type
5328 /// - side: Order side
5329 /// - amount: Order quantity
5330 /// - price: Optional order price
5331 ///
5332 /// # Returns
5333 ///
5334 /// Returns vector of new order information
5335 ///
5336 /// # Note
5337 ///
5338 /// Binance does not support native batch order editing API. This method implements batch editing by concurrent calls to single order edits.
5339 ///
5340 /// # Example
5341 ///
5342 /// ```no_run
5343 /// # use ccxt_exchanges::binance::Binance;
5344 /// # use ccxt_core::{ExchangeConfig, types::{OrderType, OrderSide}};
5345 /// # use rust_decimal_macros::dec;
5346 /// # async fn example() -> ccxt_core::Result<()> {
5347 /// let binance = Binance::new(ExchangeConfig::default())?;
5348 /// let order_params = vec![
5349 /// ("12345", "BTC/USDT", OrderType::Limit, OrderSide::Buy, dec!(0.01), Some(dec!(50000.0))),
5350 /// ("12346", "ETH/USDT", OrderType::Limit, OrderSide::Sell, dec!(0.1), Some(dec!(3000.0))),
5351 /// ];
5352 /// let orders = binance.edit_orders(order_params).await?;
5353 /// println!("Edited {} orders", orders.len());
5354 /// # Ok(())
5355 /// # }
5356 /// ```
5357 pub async fn edit_orders(
5358 &self,
5359 orders: Vec<(
5360 &str, // id
5361 &str, // symbol
5362 OrderType, // order_type
5363 OrderSide, // side
5364 rust_decimal::Decimal, // amount
5365 Option<rust_decimal::Decimal>, // price
5366 )>,
5367 ) -> Result<Vec<Order>> {
5368 let futures: Vec<_> = orders
5369 .into_iter()
5370 .map(|(id, symbol, order_type, side, amount, price)| {
5371 self.edit_order(id, symbol, order_type, side, amount, price, None)
5372 })
5373 .collect();
5374
5375 let results = futures::future::join_all(futures).await;
5376
5377 results.into_iter().collect()
5378 }
5379
5380 // ==================== WebSocket User Data Stream (Listen Key Management) ====================
5381
5382 /// Create listen key for user data stream
5383 ///
5384 /// Creates a listen key with 60-minute validity for subscribing to WebSocket user data stream.
5385 /// The listen key must be refreshed periodically to keep the connection active.
5386 ///
5387 /// # Returns
5388 ///
5389 /// Returns the listen key string
5390 ///
5391 /// # Errors
5392 ///
5393 /// - If API credentials are not configured
5394 /// - If API request fails
5395 ///
5396 /// # Example
5397 ///
5398 /// ```no_run
5399 /// # use ccxt_exchanges::binance::Binance;
5400 /// # use ccxt_core::ExchangeConfig;
5401 /// # async fn example() -> ccxt_core::Result<()> {
5402 /// let mut config = ExchangeConfig::default();
5403 /// config.api_key = Some("your_api_key".to_string());
5404 /// config.secret = Some("your_secret".to_string());
5405 /// let binance = Binance::new(config)?;
5406 ///
5407 /// let listen_key = binance.create_listen_key().await?;
5408 /// println!("Listen Key: {}", listen_key);
5409 /// # Ok(())
5410 /// # }
5411 /// ```
5412 pub async fn create_listen_key(&self) -> Result<String> {
5413 self.check_required_credentials()?;
5414
5415 let url = format!("{}/userDataStream", self.urls().public);
5416 let mut headers = HeaderMap::new();
5417
5418 let auth = self.get_auth()?;
5419 auth.add_auth_headers_reqwest(&mut headers);
5420
5421 let response = self
5422 .base()
5423 .http_client
5424 .post(&url, Some(headers), None)
5425 .await?;
5426
5427 response["listenKey"]
5428 .as_str()
5429 .map(|s| s.to_string())
5430 .ok_or_else(|| Error::from(ParseError::missing_field("listenKey")))
5431 }
5432
5433 /// Refresh listen key to extend validity
5434 ///
5435 /// Extends the listen key validity by 60 minutes. Recommended to call every 30 minutes to maintain the connection.
5436 ///
5437 /// # Arguments
5438 ///
5439 /// * `listen_key` - The listen key to refresh
5440 ///
5441 /// # Returns
5442 ///
5443 /// Returns `Ok(())` on success
5444 ///
5445 /// # Errors
5446 ///
5447 /// - If API credentials are not configured
5448 /// - If listen key is invalid or expired
5449 /// - If API request fails
5450 ///
5451 /// # Example
5452 ///
5453 /// ```no_run
5454 /// # use ccxt_exchanges::binance::Binance;
5455 /// # use ccxt_core::ExchangeConfig;
5456 /// # async fn example() -> ccxt_core::Result<()> {
5457 /// # let mut config = ExchangeConfig::default();
5458 /// # config.api_key = Some("your_api_key".to_string());
5459 /// # config.secret = Some("your_secret".to_string());
5460 /// # let binance = Binance::new(config)?;
5461 /// let listen_key = binance.create_listen_key().await?;
5462 ///
5463 /// // Refresh after 30 minutes
5464 /// binance.refresh_listen_key(&listen_key).await?;
5465 /// # Ok(())
5466 /// # }
5467 /// ```
5468 pub async fn refresh_listen_key(&self, listen_key: &str) -> Result<()> {
5469 self.check_required_credentials()?;
5470
5471 let url = format!(
5472 "{}/userDataStream?listenKey={}",
5473 self.urls().public,
5474 listen_key
5475 );
5476 let mut headers = HeaderMap::new();
5477
5478 let auth = self.get_auth()?;
5479 auth.add_auth_headers_reqwest(&mut headers);
5480
5481 let _response = self
5482 .base()
5483 .http_client
5484 .put(&url, Some(headers), None)
5485 .await?;
5486
5487 Ok(())
5488 }
5489
5490 /// Delete listen key to close user data stream
5491 ///
5492 /// Closes the user data stream connection and invalidates the listen key.
5493 ///
5494 /// # Arguments
5495 ///
5496 /// * `listen_key` - The listen key to delete
5497 ///
5498 /// # Returns
5499 ///
5500 /// Returns `Ok(())` on success
5501 ///
5502 /// # Errors
5503 ///
5504 /// - If API credentials are not configured
5505 /// - If API request fails
5506 ///
5507 /// # Example
5508 ///
5509 /// ```no_run
5510 /// # use ccxt_exchanges::binance::Binance;
5511 /// # use ccxt_core::ExchangeConfig;
5512 /// # async fn example() -> ccxt_core::Result<()> {
5513 /// # let mut config = ExchangeConfig::default();
5514 /// # config.api_key = Some("your_api_key".to_string());
5515 /// # config.secret = Some("your_secret".to_string());
5516 /// # let binance = Binance::new(config)?;
5517 /// let listen_key = binance.create_listen_key().await?;
5518 ///
5519 /// // Delete after use
5520 /// binance.delete_listen_key(&listen_key).await?;
5521 /// # Ok(())
5522 /// # }
5523 /// ```
5524 pub async fn delete_listen_key(&self, listen_key: &str) -> Result<()> {
5525 self.check_required_credentials()?;
5526
5527 let url = format!(
5528 "{}/userDataStream?listenKey={}",
5529 self.urls().public,
5530 listen_key
5531 );
5532 let mut headers = HeaderMap::new();
5533
5534 let auth = self.get_auth()?;
5535 auth.add_auth_headers_reqwest(&mut headers);
5536
5537 let _response = self
5538 .base()
5539 .http_client
5540 .delete(&url, Some(headers), None)
5541 .await?;
5542
5543 Ok(())
5544 }
5545 /// Create orders in batch (up to 5 orders)
5546 ///
5547 /// Creates multiple orders in a single API request to improve order placement efficiency.
5548 ///
5549 /// # Arguments
5550 /// - `orders`: List of order requests (maximum 5)
5551 /// - `params`: Optional parameters
5552 ///
5553 /// # Returns
5554 /// Returns list of batch order results
5555 ///
5556 /// # Examples
5557 /// ```rust,no_run
5558 /// # use ccxt_exchanges::binance::Binance;
5559 /// # use ccxt_core::ExchangeConfig;
5560 /// # use ccxt_core::types::order::BatchOrderRequest;
5561 /// # #[tokio::main]
5562 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
5563 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
5564 ///
5565 /// // Batch create orders
5566 /// let orders = vec![
5567 /// BatchOrderRequest {
5568 /// symbol: "BTCUSDT".to_string(),
5569 /// side: "BUY".to_string(),
5570 /// order_type: "LIMIT".to_string(),
5571 /// quantity: "0.001".to_string(),
5572 /// price: Some("40000".to_string()),
5573 /// time_in_force: Some("GTC".to_string()),
5574 /// reduce_only: None,
5575 /// position_side: Some("LONG".to_string()),
5576 /// new_client_order_id: None,
5577 /// },
5578 /// BatchOrderRequest {
5579 /// symbol: "ETHUSDT".to_string(),
5580 /// side: "SELL".to_string(),
5581 /// order_type: "LIMIT".to_string(),
5582 /// quantity: "0.01".to_string(),
5583 /// price: Some("3000".to_string()),
5584 /// time_in_force: Some("GTC".to_string()),
5585 /// reduce_only: None,
5586 /// position_side: Some("SHORT".to_string()),
5587 /// new_client_order_id: None,
5588 /// },
5589 /// ];
5590 /// let results = binance.create_orders(orders, None).await?;
5591 /// # Ok(())
5592 /// # }
5593 /// ```
5594 ///
5595 /// # API Endpoint
5596 /// - Endpoint: POST /fapi/v1/batchOrders
5597 /// - Weight: 5
5598 /// - Requires signature: Yes
5599 pub async fn create_orders(
5600 &self,
5601 orders: Vec<BatchOrderRequest>,
5602 params: Option<Value>,
5603 ) -> Result<Vec<BatchOrderResult>> {
5604 self.check_required_credentials()?;
5605
5606 if orders.is_empty() {
5607 return Err(Error::invalid_request("Orders list cannot be empty"));
5608 }
5609
5610 if orders.len() > 5 {
5611 return Err(Error::invalid_request(
5612 "Cannot create more than 5 orders at once",
5613 ));
5614 }
5615
5616 let batch_orders_json = serde_json::to_string(&orders).map_err(|e| {
5617 Error::from(ParseError::invalid_format(
5618 "data",
5619 format!("Failed to serialize orders: {}", e),
5620 ))
5621 })?;
5622
5623 let mut request_params = HashMap::new();
5624 request_params.insert("batchOrders".to_string(), batch_orders_json);
5625
5626 if let Some(params) = params {
5627 if let Some(obj) = params.as_object() {
5628 for (key, value) in obj {
5629 if let Some(v) = value.as_str() {
5630 request_params.insert(key.clone(), v.to_string());
5631 }
5632 }
5633 }
5634 }
5635
5636 let timestamp = self.fetch_time_raw().await?;
5637 let auth = self.get_auth()?;
5638 let signed_params =
5639 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
5640
5641 let url = format!("{}/batchOrders", self.urls().fapi_private);
5642
5643 let mut headers = HeaderMap::new();
5644 auth.add_auth_headers_reqwest(&mut headers);
5645
5646 let body = serde_json::to_value(&signed_params).map_err(|e| {
5647 Error::from(ParseError::invalid_format(
5648 "data",
5649 format!("Failed to serialize params: {}", e),
5650 ))
5651 })?;
5652
5653 let data = self
5654 .base()
5655 .http_client
5656 .post(&url, Some(headers), Some(body))
5657 .await?;
5658
5659 let results: Vec<BatchOrderResult> = serde_json::from_value(data).map_err(|e| {
5660 Error::from(ParseError::invalid_format(
5661 "data",
5662 format!("Failed to parse batch order results: {}", e),
5663 ))
5664 })?;
5665
5666 Ok(results)
5667 }
5668
5669 /// Modify multiple orders in batch
5670 ///
5671 /// Modifies the price and quantity of multiple orders in a single API request.
5672 ///
5673 /// # Arguments
5674 /// - `updates`: List of order update requests (maximum 5)
5675 /// - `params`: Optional parameters
5676 ///
5677 /// # Returns
5678 /// Returns list of batch order results
5679 ///
5680 /// # Examples
5681 /// ```rust,no_run
5682 /// # use ccxt_exchanges::binance::Binance;
5683 /// # use ccxt_core::ExchangeConfig;
5684 /// # use ccxt_core::types::order::BatchOrderUpdate;
5685 /// # #[tokio::main]
5686 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
5687 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
5688 ///
5689 /// // Batch modify orders
5690 /// let updates = vec![
5691 /// BatchOrderUpdate {
5692 /// order_id: Some(12345),
5693 /// orig_client_order_id: None,
5694 /// symbol: "BTCUSDT".to_string(),
5695 /// side: "BUY".to_string(),
5696 /// quantity: "0.002".to_string(),
5697 /// price: "41000".to_string(),
5698 /// },
5699 /// ];
5700 /// let results = binance.edit_orders(updates, None).await?;
5701 /// # Ok(())
5702 /// # }
5703 /// ```
5704 ///
5705 /// # API Endpoint
5706 /// - Endpoint: PUT /fapi/v1/batchOrders
5707 /// - Weight: 5
5708 /// - Requires signature: Yes
5709 pub async fn batch_edit_orders(
5710 &self,
5711 updates: Vec<BatchOrderUpdate>,
5712 params: Option<Value>,
5713 ) -> Result<Vec<BatchOrderResult>> {
5714 self.check_required_credentials()?;
5715
5716 if updates.is_empty() {
5717 return Err(Error::invalid_request("Updates list cannot be empty"));
5718 }
5719
5720 if updates.len() > 5 {
5721 return Err(Error::invalid_request(
5722 "Cannot update more than 5 orders at once",
5723 ));
5724 }
5725
5726 let batch_orders_json = serde_json::to_string(&updates).map_err(|e| {
5727 Error::from(ParseError::invalid_format(
5728 "data",
5729 format!("Failed to serialize updates: {}", e),
5730 ))
5731 })?;
5732
5733 let mut request_params = HashMap::new();
5734 request_params.insert("batchOrders".to_string(), batch_orders_json);
5735
5736 if let Some(params) = params {
5737 if let Some(obj) = params.as_object() {
5738 for (key, value) in obj {
5739 if let Some(v) = value.as_str() {
5740 request_params.insert(key.clone(), v.to_string());
5741 }
5742 }
5743 }
5744 }
5745
5746 let timestamp = self.fetch_time_raw().await?;
5747 let auth = self.get_auth()?;
5748 let signed_params =
5749 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
5750
5751 let url = format!("{}/batchOrders", self.urls().fapi_private);
5752
5753 let mut headers = HeaderMap::new();
5754 auth.add_auth_headers_reqwest(&mut headers);
5755
5756 let body = serde_json::to_value(&signed_params).map_err(|e| {
5757 Error::from(ParseError::invalid_format(
5758 "data",
5759 format!("Failed to serialize params: {}", e),
5760 ))
5761 })?;
5762
5763 let data = self
5764 .base()
5765 .http_client
5766 .put(&url, Some(headers), Some(body))
5767 .await?;
5768
5769 let results: Vec<BatchOrderResult> = serde_json::from_value(data).map_err(|e| {
5770 Error::from(ParseError::invalid_format(
5771 "data",
5772 format!("Failed to parse batch order results: {}", e),
5773 ))
5774 })?;
5775
5776 Ok(results)
5777 }
5778
5779 /// Cancel orders in batch by order IDs
5780 ///
5781 /// Cancels multiple orders in a single API request.
5782 ///
5783 /// # Arguments
5784 /// - `symbol`: Trading pair symbol
5785 /// - `order_ids`: List of order IDs (maximum 10)
5786 /// - `params`: Optional parameters
5787 /// - `origClientOrderIdList`: List of original client order IDs (alternative to order_ids)
5788 ///
5789 /// # Returns
5790 /// Returns list of batch cancel results
5791 ///
5792 /// # Examples
5793 /// ```rust,no_run
5794 /// # use ccxt_exchanges::binance::Binance;
5795 /// # use ccxt_core::ExchangeConfig;
5796 /// # #[tokio::main]
5797 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
5798 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
5799 ///
5800 /// // Batch cancel orders
5801 /// let order_ids = vec![12345, 12346, 12347];
5802 /// let results = binance.cancel_orders("BTC/USDT", order_ids, None).await?;
5803 /// # Ok(())
5804 /// # }
5805 /// ```
5806 ///
5807 /// # API Endpoint
5808 /// - Endpoint: DELETE /fapi/v1/batchOrders
5809 /// - Weight: 1
5810 /// - Requires signature: Yes
5811 pub async fn batch_cancel_orders(
5812 &self,
5813 symbol: &str,
5814 order_ids: Vec<i64>,
5815 params: Option<Value>,
5816 ) -> Result<Vec<BatchCancelResult>> {
5817 self.check_required_credentials()?;
5818
5819 if order_ids.is_empty() {
5820 return Err(Error::invalid_request("Order IDs list cannot be empty"));
5821 }
5822
5823 if order_ids.len() > 10 {
5824 return Err(Error::invalid_request(
5825 "Cannot cancel more than 10 orders at once",
5826 ));
5827 }
5828
5829 let market = self.base().market(symbol).await?;
5830
5831 let mut request_params = HashMap::new();
5832 request_params.insert("symbol".to_string(), market.id.clone());
5833
5834 let order_id_list_json = serde_json::to_string(&order_ids).map_err(|e| {
5835 Error::from(ParseError::invalid_format(
5836 "data",
5837 format!("Failed to serialize order IDs: {}", e),
5838 ))
5839 })?;
5840 request_params.insert("orderIdList".to_string(), order_id_list_json);
5841
5842 if let Some(params) = params {
5843 if let Some(obj) = params.as_object() {
5844 for (key, value) in obj {
5845 if key == "origClientOrderIdList" {
5846 if let Some(v) = value.as_str() {
5847 request_params.insert(key.clone(), v.to_string());
5848 }
5849 }
5850 }
5851 }
5852 }
5853
5854 let timestamp = self.fetch_time_raw().await?;
5855 let auth = self.get_auth()?;
5856 let signed_params =
5857 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
5858
5859 let url = if market.linear.unwrap_or(true) {
5860 format!("{}/batchOrders", self.urls().fapi_private)
5861 } else {
5862 format!("{}/batchOrders", self.urls().dapi_private)
5863 };
5864
5865 let mut headers = HeaderMap::new();
5866 auth.add_auth_headers_reqwest(&mut headers);
5867
5868 let body = serde_json::to_value(&signed_params).map_err(|e| {
5869 Error::from(ParseError::invalid_format(
5870 "data",
5871 format!("Failed to serialize params: {}", e),
5872 ))
5873 })?;
5874
5875 let data = self
5876 .base()
5877 .http_client
5878 .delete(&url, Some(headers), Some(body))
5879 .await?;
5880
5881 let results: Vec<BatchCancelResult> = serde_json::from_value(data).map_err(|e| {
5882 Error::from(ParseError::invalid_format(
5883 "data",
5884 format!("Failed to parse batch cancel results: {}", e),
5885 ))
5886 })?;
5887
5888 Ok(results)
5889 }
5890
5891 /// Cancel all open orders for a trading pair
5892 ///
5893 /// Cancels all open orders for the specified trading pair.
5894 ///
5895 /// # Arguments
5896 /// - `symbol`: Trading pair symbol
5897 /// - `params`: Optional parameters
5898 ///
5899 /// # Returns
5900 /// Returns result of canceling all orders
5901 ///
5902 /// # Examples
5903 /// ```rust,no_run
5904 /// # use ccxt_exchanges::binance::Binance;
5905 /// # use ccxt_core::ExchangeConfig;
5906 /// # #[tokio::main]
5907 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
5908 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
5909 ///
5910 /// // Cancel all open orders for BTC/USDT
5911 /// let result = binance.cancel_all_orders("BTC/USDT", None).await?;
5912 /// # Ok(())
5913 /// # }
5914 /// ```
5915 ///
5916 /// # API Endpoint
5917 /// - Endpoint: DELETE /fapi/v1/allOpenOrders
5918 /// - Weight: 1
5919 /// - Requires signature: Yes
5920 pub async fn cancel_all_orders_futures(
5921 &self,
5922 symbol: &str,
5923 params: Option<Value>,
5924 ) -> Result<CancelAllOrdersResult> {
5925 self.check_required_credentials()?;
5926
5927 let market = self.base().market(symbol).await?;
5928
5929 let mut request_params = HashMap::new();
5930 request_params.insert("symbol".to_string(), market.id.clone());
5931
5932 if let Some(params) = params {
5933 if let Some(obj) = params.as_object() {
5934 for (key, value) in obj {
5935 if let Some(v) = value.as_str() {
5936 request_params.insert(key.clone(), v.to_string());
5937 }
5938 }
5939 }
5940 }
5941
5942 let timestamp = self.fetch_time_raw().await?;
5943 let auth = self.get_auth()?;
5944 let signed_params =
5945 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
5946
5947 let url = if market.linear.unwrap_or(true) {
5948 format!("{}/allOpenOrders", self.urls().fapi_private)
5949 } else {
5950 format!("{}/allOpenOrders", self.urls().dapi_private)
5951 };
5952
5953 let mut headers = HeaderMap::new();
5954 auth.add_auth_headers_reqwest(&mut headers);
5955
5956 let body = serde_json::to_value(&signed_params).map_err(|e| {
5957 Error::from(ParseError::invalid_format(
5958 "data",
5959 format!("Failed to serialize params: {}", e),
5960 ))
5961 })?;
5962
5963 let data = self
5964 .base()
5965 .http_client
5966 .delete(&url, Some(headers), Some(body))
5967 .await?;
5968
5969 let result: CancelAllOrdersResult = serde_json::from_value(data).map_err(|e| {
5970 Error::from(ParseError::invalid_format(
5971 "data",
5972 format!("Failed to parse cancel all orders result: {}", e),
5973 ))
5974 })?;
5975
5976 Ok(result)
5977 }
5978 // ============================================================================
5979 // Account Configuration Management
5980 // ============================================================================
5981
5982 /// Fetch account configuration information
5983 ///
5984 /// Retrieves futures account configuration parameters including multi-asset mode status and fee tier.
5985 ///
5986 /// # Returns
5987 ///
5988 /// Returns account configuration information
5989 ///
5990 /// # Examples
5991 ///
5992 /// ```no_run
5993 /// # use ccxt_exchanges::binance::Binance;
5994 /// # use ccxt_core::ExchangeConfig;
5995 /// # async fn example() -> ccxt_core::Result<()> {
5996 /// let mut config = ExchangeConfig::default();
5997 /// config.api_key = Some("your_api_key".to_string());
5998 /// config.secret = Some("your_secret".to_string());
5999 /// let binance = Binance::new_futures(config)?;
6000 /// let account_config = binance.fetch_account_configuration().await?;
6001 /// println!("Multi-asset mode: {}", account_config.multi_assets_margin);
6002 /// println!("Fee tier: {}", account_config.fee_tier);
6003 /// # Ok(())
6004 /// # }
6005 /// ```
6006 pub async fn fetch_account_configuration(&self) -> Result<AccountConfig> {
6007 self.check_required_credentials()?;
6008
6009 let request_params = HashMap::new();
6010
6011 let timestamp = self.fetch_time_raw().await?;
6012 let auth = self.get_auth()?;
6013 let signed_params =
6014 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
6015
6016 let mut request_url = format!("{}/account?", self.urls().fapi_private);
6017 for (key, value) in &signed_params {
6018 request_url.push_str(&format!("{}={}&", key, value));
6019 }
6020
6021 let mut headers = HeaderMap::new();
6022 auth.add_auth_headers_reqwest(&mut headers);
6023
6024 let data = self
6025 .base()
6026 .http_client
6027 .get(&request_url, Some(headers))
6028 .await?;
6029
6030 parser::parse_account_config(&data)
6031 }
6032
6033 /// Set multi-asset mode
6034 ///
6035 /// Enable or disable multi-asset margin mode.
6036 ///
6037 /// # Arguments
6038 ///
6039 /// * `multi_assets` - `true` to enable multi-asset mode, `false` for single-asset mode
6040 ///
6041 /// # Returns
6042 ///
6043 /// Returns operation result
6044 ///
6045 /// # Examples
6046 ///
6047 /// ```no_run
6048 /// # use ccxt_exchanges::binance::Binance;
6049 /// # use ccxt_core::ExchangeConfig;
6050 /// # async fn example() -> ccxt_core::Result<()> {
6051 /// let mut config = ExchangeConfig::default();
6052 /// config.api_key = Some("your_api_key".to_string());
6053 /// config.secret = Some("your_secret".to_string());
6054 /// let binance = Binance::new_futures(config)?;
6055 /// binance.set_multi_assets_mode(true).await?;
6056 /// # Ok(())
6057 /// # }
6058 /// ```
6059 pub async fn set_multi_assets_mode(&self, multi_assets: bool) -> Result<()> {
6060 self.check_required_credentials()?;
6061
6062 let mut request_params = HashMap::new();
6063 request_params.insert("multiAssetsMargin".to_string(), multi_assets.to_string());
6064
6065 let timestamp = self.fetch_time_raw().await?;
6066 let auth = self.get_auth()?;
6067 let signed_params =
6068 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
6069
6070 let url = format!("{}/multiAssetsMargin", self.urls().fapi_private);
6071
6072 let mut headers = HeaderMap::new();
6073 auth.add_auth_headers_reqwest(&mut headers);
6074
6075 let body = serde_json::to_value(&signed_params).map_err(|e| {
6076 Error::from(ParseError::invalid_format(
6077 "data",
6078 format!("Failed to serialize params: {}", e),
6079 ))
6080 })?;
6081
6082 let _data = self
6083 .base()
6084 .http_client
6085 .post(&url, Some(headers), Some(body))
6086 .await?;
6087
6088 Ok(())
6089 }
6090
6091 /// Fetch futures commission rate
6092 ///
6093 /// Retrieves maker and taker commission rates for the specified trading pair.
6094 ///
6095 /// # Arguments
6096 ///
6097 /// * `symbol` - Trading pair symbol
6098 ///
6099 /// # Returns
6100 ///
6101 /// Returns commission rate information
6102 ///
6103 /// # Examples
6104 ///
6105 /// ```no_run
6106 /// # use ccxt_exchanges::binance::Binance;
6107 /// # use ccxt_core::ExchangeConfig;
6108 /// # async fn example() -> ccxt_core::Result<()> {
6109 /// let mut config = ExchangeConfig::default();
6110 /// config.api_key = Some("your_api_key".to_string());
6111 /// config.secret = Some("your_secret".to_string());
6112 /// let binance = Binance::new_futures(config)?;
6113 /// let rate = binance.fetch_commission_rate("BTC/USDT:USDT").await?;
6114 /// println!("Maker rate: {}", rate.maker_commission_rate);
6115 /// println!("Taker rate: {}", rate.taker_commission_rate);
6116 /// # Ok(())
6117 /// # }
6118 /// ```
6119 pub async fn fetch_commission_rate(&self, symbol: &str) -> Result<CommissionRate> {
6120 self.check_required_credentials()?;
6121
6122 let market = self.base().market(symbol).await?;
6123 let mut request_params = HashMap::new();
6124 request_params.insert("symbol".to_string(), market.id.clone());
6125
6126 let timestamp = self.fetch_time_raw().await?;
6127 let auth = self.get_auth()?;
6128 let signed_params =
6129 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
6130
6131 let mut request_url = format!("{}/commissionRate?", self.urls().fapi_private);
6132 for (key, value) in &signed_params {
6133 request_url.push_str(&format!("{}={}&", key, value));
6134 }
6135
6136 let mut headers = HeaderMap::new();
6137 auth.add_auth_headers_reqwest(&mut headers);
6138
6139 let data = self
6140 .base()
6141 .http_client
6142 .get(&request_url, Some(headers))
6143 .await?;
6144
6145 parser::parse_commission_rate(&data, &market)
6146 }
6147 // ==================== Stage 24.6: Risk and Limit Queries ====================
6148
6149 /// Fetch futures open interest
6150 ///
6151 /// Retrieves the total amount of outstanding (open) contracts.
6152 ///
6153 /// # Arguments
6154 ///
6155 /// * `symbol` - Trading pair symbol, e.g. "BTC/USDT:USDT"
6156 ///
6157 /// # Returns
6158 ///
6159 /// Returns open interest information
6160 ///
6161 /// # API Endpoint
6162 ///
6163 /// GET /fapi/v1/openInterest (MARKET_DATA)
6164 ///
6165 /// # Examples
6166 ///
6167 /// ```no_run
6168 /// # use ccxt_exchanges::binance::Binance;
6169 /// # use ccxt_core::ExchangeConfig;
6170 /// # async fn example() -> ccxt_core::Result<()> {
6171 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
6172 /// let open_interest = binance.fetch_open_interest("BTC/USDT:USDT").await?;
6173 /// println!("Open interest: {}", open_interest.open_interest);
6174 /// # Ok(())
6175 /// # }
6176 /// ```
6177 pub async fn fetch_open_interest(&self, symbol: &str) -> Result<OpenInterest> {
6178 let market = self.base().market(symbol).await?;
6179 let mut request_params = HashMap::new();
6180 request_params.insert("symbol".to_string(), market.id.clone());
6181
6182 let mut request_url = format!("{}/openInterest?", self.urls().fapi_public);
6183 for (key, value) in &request_params {
6184 request_url.push_str(&format!("{}={}&", key, value));
6185 }
6186
6187 let data = self.base().http_client.get(&request_url, None).await?;
6188
6189 parser::parse_open_interest(&data, &market)
6190 }
6191
6192 /// Fetch open interest history
6193 ///
6194 /// # Arguments
6195 ///
6196 /// * `symbol` - Trading pair symbol, e.g. "BTC/USDT:USDT"
6197 /// * `period` - Time period: "5m", "15m", "30m", "1h", "2h", "4h", "6h", "12h", "1d"
6198 /// * `limit` - Number of records to return (default 30, maximum 500)
6199 /// * `start_time` - Start timestamp in milliseconds
6200 /// * `end_time` - End timestamp in milliseconds
6201 ///
6202 /// # Returns
6203 ///
6204 /// Returns list of open interest history records
6205 ///
6206 /// # API Endpoint
6207 ///
6208 /// GET /futures/data/openInterestHist (MARKET_DATA)
6209 ///
6210 /// # Examples
6211 ///
6212 /// ```no_run
6213 /// # use ccxt_exchanges::binance::Binance;
6214 /// # use ccxt_core::ExchangeConfig;
6215 /// # async fn example() -> ccxt_core::Result<()> {
6216 /// let binance = Binance::new_futures(ExchangeConfig::default())?;
6217 /// let history = binance.fetch_open_interest_history(
6218 /// "BTC/USDT:USDT",
6219 /// "1h",
6220 /// Some(100),
6221 /// None,
6222 /// None
6223 /// ).await?;
6224 /// println!("History records: {}", history.len());
6225 /// # Ok(())
6226 /// # }
6227 /// ```
6228 pub async fn fetch_open_interest_history(
6229 &self,
6230 symbol: &str,
6231 period: &str,
6232 limit: Option<u32>,
6233 start_time: Option<u64>,
6234 end_time: Option<u64>,
6235 ) -> Result<Vec<OpenInterestHistory>> {
6236 let market = self.base().market(symbol).await?;
6237 let mut request_params = HashMap::new();
6238 request_params.insert("symbol".to_string(), market.id.clone());
6239 request_params.insert("period".to_string(), period.to_string());
6240
6241 if let Some(limit) = limit {
6242 request_params.insert("limit".to_string(), limit.to_string());
6243 }
6244 if let Some(start_time) = start_time {
6245 request_params.insert("startTime".to_string(), start_time.to_string());
6246 }
6247 if let Some(end_time) = end_time {
6248 request_params.insert("endTime".to_string(), end_time.to_string());
6249 }
6250
6251 let mut request_url = format!("{}/data/openInterestHist?", self.urls().fapi_public);
6252 for (key, value) in &request_params {
6253 request_url.push_str(&format!("{}={}&", key, value));
6254 }
6255
6256 let data = self.base().http_client.get(&request_url, None).await?;
6257
6258 parser::parse_open_interest_history(&data, &market)
6259 }
6260
6261 /// Fetch maximum available leverage for trading pair
6262 ///
6263 /// # Arguments
6264 ///
6265 /// * `symbol` - Trading pair symbol, e.g. "BTC/USDT:USDT"
6266 ///
6267 /// # Returns
6268 ///
6269 /// Returns maximum leverage information
6270 ///
6271 /// # Notes
6272 ///
6273 /// This method retrieves maximum leverage by querying leverage bracket data.
6274 ///
6275 /// # Examples
6276 ///
6277 /// ```no_run
6278 /// # use ccxt_exchanges::binance::Binance;
6279 /// # use ccxt_core::ExchangeConfig;
6280 /// # async fn example() -> ccxt_core::Result<()> {
6281 /// let mut config = ExchangeConfig::default();
6282 /// config.api_key = Some("your-api-key".to_string());
6283 /// config.secret = Some("your-secret".to_string());
6284 /// let binance = Binance::new_futures(config)?;
6285 /// let max_leverage = binance.fetch_max_leverage("BTC/USDT:USDT").await?;
6286 /// println!("Maximum leverage: {}x", max_leverage.max_leverage);
6287 /// # Ok(())
6288 /// # }
6289 /// ```
6290 pub async fn fetch_max_leverage(&self, symbol: &str) -> Result<MaxLeverage> {
6291 self.check_required_credentials()?;
6292
6293 let market = self.base().market(symbol).await?;
6294 let mut request_params = HashMap::new();
6295
6296 // Optional: if symbol not provided, returns leverage brackets for all pairs
6297 request_params.insert("symbol".to_string(), market.id.clone());
6298
6299 let timestamp = self.fetch_time_raw().await?;
6300 let auth = self.get_auth()?;
6301 let signed_params =
6302 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
6303
6304 let mut request_url = format!("{}/leverageBracket?", self.urls().fapi_private);
6305 for (key, value) in &signed_params {
6306 request_url.push_str(&format!("{}={}&", key, value));
6307 }
6308
6309 let mut headers = HeaderMap::new();
6310 auth.add_auth_headers_reqwest(&mut headers);
6311
6312 let data = self
6313 .base()
6314 .http_client
6315 .get(&request_url, Some(headers))
6316 .await?;
6317
6318 parser::parse_max_leverage(&data, &market)
6319 }
6320 // ========================================================================
6321 // Futures Market Data Queries
6322 // ========================================================================
6323
6324 /// Fetch index price
6325 ///
6326 /// # Arguments
6327 ///
6328 /// * `symbol` - Trading pair symbol, e.g. "BTC/USDT"
6329 ///
6330 /// # Returns
6331 ///
6332 /// Returns index price information
6333 ///
6334 /// # Examples
6335 ///
6336 /// ```no_run
6337 /// use ccxt_exchanges::binance::Binance;
6338 /// use ccxt_core::ExchangeConfig;
6339 ///
6340 /// #[tokio::main]
6341 /// async fn main() {
6342 /// let config = ExchangeConfig::default();
6343 /// let binance = Binance::new(config).unwrap();
6344 ///
6345 /// let index_price = binance.fetch_index_price("BTC/USDT").await.unwrap();
6346 /// println!("Index Price: {}", index_price.index_price);
6347 /// }
6348 /// ```
6349 pub async fn fetch_index_price(&self, symbol: &str) -> Result<ccxt_core::types::IndexPrice> {
6350 let market = self.base().market(symbol).await?;
6351
6352 let url = format!(
6353 "{}/premiumIndex?symbol={}",
6354 self.urls().fapi_public,
6355 market.id
6356 );
6357
6358 let data = self.base().http_client.get(&url, None).await?;
6359
6360 parser::parse_index_price(&data, &market)
6361 }
6362
6363 /// Fetch premium index including mark price and funding rate
6364 ///
6365 /// # Arguments
6366 ///
6367 /// * `symbol` - Trading pair symbol, e.g. "BTC/USDT"; if `None`, returns all trading pairs
6368 ///
6369 /// # Returns
6370 ///
6371 /// Returns premium index information (single or multiple)
6372 ///
6373 /// # Examples
6374 ///
6375 /// ```no_run
6376 /// use ccxt_exchanges::binance::Binance;
6377 /// use ccxt_core::ExchangeConfig;
6378 ///
6379 /// #[tokio::main]
6380 /// async fn main() {
6381 /// let config = ExchangeConfig::default();
6382 /// let binance = Binance::new(config).unwrap();
6383 ///
6384 /// // Query single trading pair
6385 /// let premium = binance.fetch_premium_index(Some("BTC/USDT")).await.unwrap();
6386 /// println!("Mark Price: {}", premium[0].mark_price);
6387 ///
6388 /// // Query all trading pairs
6389 /// let all_premiums = binance.fetch_premium_index(None).await.unwrap();
6390 /// println!("Total pairs: {}", all_premiums.len());
6391 /// }
6392 /// ```
6393 pub async fn fetch_premium_index(
6394 &self,
6395 symbol: Option<&str>,
6396 ) -> Result<Vec<ccxt_core::types::PremiumIndex>> {
6397 let url = if let Some(sym) = symbol {
6398 let market = self.base().market(sym).await?;
6399 format!(
6400 "{}/premiumIndex?symbol={}",
6401 self.urls().fapi_public,
6402 market.id
6403 )
6404 } else {
6405 format!("{}/premiumIndex", self.urls().fapi_public)
6406 };
6407
6408 let data = self.base().http_client.get(&url, None).await?;
6409
6410 if let Some(array) = data.as_array() {
6411 let mut results = Vec::new();
6412
6413 let cache = self.base().market_cache.read().await;
6414
6415 for item in array {
6416 if let Some(symbol_str) = item["symbol"].as_str() {
6417 if let Some(market) = cache.markets_by_id.get(symbol_str) {
6418 if let Ok(premium) = parser::parse_premium_index(item, market) {
6419 results.push(premium);
6420 }
6421 }
6422 }
6423 }
6424
6425 Ok(results)
6426 } else {
6427 // When symbol is provided, the API returns a single object (not an array).
6428 // In this case, symbol must be Some because we only reach this branch
6429 // when a specific symbol was requested.
6430 let sym = symbol.ok_or_else(|| {
6431 Error::from(ParseError::missing_field(
6432 "symbol required when API returns single object",
6433 ))
6434 })?;
6435 let market = self.base().market(sym).await?;
6436 let premium = parser::parse_premium_index(&data, &market)?;
6437 Ok(vec![premium])
6438 }
6439 }
6440
6441 /// Fetch liquidation orders
6442 ///
6443 /// # Arguments
6444 ///
6445 /// * `symbol` - Trading pair symbol, e.g. "BTC/USDT"; if `None`, returns all trading pairs
6446 /// * `start_time` - Start time (millisecond timestamp)
6447 /// * `end_time` - End time (millisecond timestamp)
6448 /// * `limit` - Quantity limit (default 100, maximum 1000)
6449 ///
6450 /// # Returns
6451 ///
6452 /// Returns list of liquidation orders
6453 ///
6454 /// # Examples
6455 ///
6456 /// ```no_run
6457 /// use ccxt_exchanges::binance::Binance;
6458 /// use ccxt_core::ExchangeConfig;
6459 ///
6460 /// #[tokio::main]
6461 /// async fn main() {
6462 /// let config = ExchangeConfig::default();
6463 /// let binance = Binance::new(config).unwrap();
6464 ///
6465 /// // Query recent liquidation orders
6466 /// let liquidations = binance.fetch_liquidations(
6467 /// Some("BTC/USDT"),
6468 /// None,
6469 /// None,
6470 /// Some(50)
6471 /// ).await.unwrap();
6472 ///
6473 /// for liq in liquidations {
6474 /// println!("Liquidation: {} {} @ {}", liq.side, liq.quantity, liq.price);
6475 /// }
6476 /// }
6477 /// ```
6478 pub async fn fetch_liquidations(
6479 &self,
6480 symbol: Option<&str>,
6481 start_time: Option<u64>,
6482 end_time: Option<u64>,
6483 limit: Option<u32>,
6484 ) -> Result<Vec<ccxt_core::types::Liquidation>> {
6485 let mut request_params = HashMap::new();
6486
6487 if let Some(sym) = symbol {
6488 let market = self.base().market(sym).await?;
6489 request_params.insert("symbol".to_string(), market.id.clone());
6490 }
6491
6492 if let Some(start) = start_time {
6493 request_params.insert("startTime".to_string(), start.to_string());
6494 }
6495
6496 if let Some(end) = end_time {
6497 request_params.insert("endTime".to_string(), end.to_string());
6498 }
6499
6500 if let Some(lim) = limit {
6501 request_params.insert("limit".to_string(), lim.to_string());
6502 }
6503
6504 let mut url = format!("{}/allForceOrders?", self.urls().fapi_public);
6505 for (key, value) in &request_params {
6506 url.push_str(&format!("{}={}&", key, value));
6507 }
6508
6509 let data = self.base().http_client.get(&url, None).await?;
6510
6511 let array = data.as_array().ok_or_else(|| {
6512 Error::from(ParseError::invalid_value("data", "Expected array response"))
6513 })?;
6514
6515 let mut results = Vec::new();
6516
6517 let cache = self.base().market_cache.read().await;
6518
6519 for item in array {
6520 // Get symbol and find corresponding market
6521 if let Some(symbol_str) = item["symbol"].as_str() {
6522 if let Some(market) = cache.markets_by_id.get(symbol_str) {
6523 if let Ok(liquidation) = parser::parse_liquidation(item, market) {
6524 results.push(liquidation);
6525 }
6526 }
6527 }
6528 }
6529
6530 Ok(results)
6531 }
6532 // ============================================================================
6533 // Internal Transfer System (P0.1)
6534 // ============================================================================
6535
6536 /// Execute internal transfer
6537 ///
6538 /// Transfer assets between different account types (spot, margin, futures, funding, etc.)
6539 ///
6540 /// # Arguments
6541 ///
6542 /// * `currency` - Asset code (e.g. "USDT", "BTC")
6543 /// * `amount` - Transfer amount
6544 /// * `from_account` - Source account type (spot/margin/future/funding, etc.)
6545 /// * `to_account` - Target account type
6546 /// * `params` - Optional additional parameters
6547 ///
6548 /// # Binance API
6549 /// - Endpoint: POST /sapi/v1/asset/transfer
6550 /// - Weight: 900 (UID)
6551 /// - Required permission: TRADE
6552 /// - Signature required: Yes
6553 ///
6554 /// # Supported Account Types
6555 /// - MAIN: Spot account
6556 /// - UMFUTURE: USDT-margined futures account
6557 /// - CMFUTURE: Coin-margined futures account
6558 /// - MARGIN: Cross margin account
6559 /// - ISOLATED_MARGIN: Isolated margin account
6560 /// - FUNDING: Funding account
6561 /// - OPTION: Options account
6562 ///
6563 /// # Examples
6564 /// ```rust,no_run
6565 /// # use ccxt_exchanges::binance::Binance;
6566 /// # use ccxt_core::ExchangeConfig;
6567 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
6568 /// let config = ExchangeConfig {
6569 /// api_key: Some("your_api_key".to_string()),
6570 /// secret: Some("your_secret".to_string()),
6571 /// ..Default::default()
6572 /// };
6573 /// let binance = Binance::new(config)?;
6574 ///
6575 /// // Transfer 100 USDT from spot to futures account
6576 /// let transfer = binance.transfer(
6577 /// "USDT",
6578 /// 100.0,
6579 /// "spot", // From spot
6580 /// "future", // To futures
6581 /// None
6582 /// ).await?;
6583 ///
6584 /// println!("Transfer ID: {:?}", transfer.id);
6585 /// println!("Status: {}", transfer.status);
6586 /// # Ok(())
6587 /// # }
6588 /// ```
6589 pub async fn transfer(
6590 &self,
6591 currency: &str,
6592 amount: f64,
6593 from_account: &str,
6594 to_account: &str,
6595 params: Option<HashMap<String, String>>,
6596 ) -> Result<Transfer> {
6597 println!(
6598 "🚨 [TRANSFER ENTRY] Method called with currency={}, amount={}, from={}, to={}",
6599 currency, amount, from_account, to_account
6600 );
6601
6602 println!("🚨 [TRANSFER STEP 1] Checking credentials...");
6603 self.check_required_credentials()?;
6604 println!("🚨 [TRANSFER STEP 1] ✅ Credentials check passed");
6605
6606 let from_upper = from_account.to_uppercase();
6607 let to_upper = to_account.to_uppercase();
6608
6609 // Normalize account names to Binance API format
6610 let from_normalized = match from_upper.as_str() {
6611 "SPOT" => "MAIN",
6612 "FUTURE" | "FUTURES" => "UMFUTURE",
6613 "MARGIN" => "MARGIN",
6614 "FUNDING" => "FUNDING",
6615 "OPTION" => "OPTION",
6616 other => other,
6617 };
6618
6619 let to_normalized = match to_upper.as_str() {
6620 "SPOT" => "MAIN",
6621 "FUTURE" | "FUTURES" => "UMFUTURE",
6622 "MARGIN" => "MARGIN",
6623 "FUNDING" => "FUNDING",
6624 "OPTION" => "OPTION",
6625 other => other,
6626 };
6627
6628 let transfer_type = format!("{}_{}", from_normalized, to_normalized);
6629
6630 let mut request_params = params.unwrap_or_default();
6631 request_params.insert("type".to_string(), transfer_type);
6632 request_params.insert("asset".to_string(), currency.to_uppercase());
6633 request_params.insert("amount".to_string(), amount.to_string());
6634
6635 println!(
6636 "🚨 [TRANSFER STEP 2] Request params prepared: {:?}",
6637 request_params
6638 );
6639
6640 // Sign request with timestamp
6641 println!("🚨 [TRANSFER STEP 3] Fetching server timestamp...");
6642 let timestamp = self.fetch_time_raw().await?;
6643 println!("🚨 [TRANSFER STEP 3] ✅ Got timestamp: {}", timestamp);
6644 println!("🔍 [TRANSFER DEBUG] Timestamp: {}", timestamp);
6645 println!(
6646 "🔍 [TRANSFER DEBUG] Request params before signing: {:?}",
6647 request_params
6648 );
6649
6650 println!("🚨 [TRANSFER STEP 4] Getting auth object...");
6651 let auth = self.get_auth()?;
6652 println!("🚨 [TRANSFER STEP 4] ✅ Got auth, now signing params...");
6653
6654 // Use new tuple return value: (HashMap, sorted query string)
6655 let signed_params =
6656 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
6657 println!("🚨 [TRANSFER STEP 4] ✅ Params signed successfully");
6658 let query_string = auth.build_query_string(&signed_params);
6659 println!(
6660 "🔍 [TRANSFER DEBUG] Query string (signed and sorted): {}",
6661 query_string
6662 );
6663
6664 // Use sorted query string from Auth module to ensure consistent parameter order
6665 let url = format!("{}/asset/transfer?{}", self.urls().sapi, &query_string);
6666
6667 println!("🔍 [TRANSFER DEBUG] Final URL: {}", url);
6668
6669 let mut headers = reqwest::header::HeaderMap::new();
6670 auth.add_auth_headers_reqwest(&mut headers);
6671
6672 println!("🚨 [TRANSFER STEP 5] Sending POST request to URL: {}", url);
6673 let data = self
6674 .base()
6675 .http_client
6676 .post(&url, Some(headers), None)
6677 .await?;
6678 println!("🚨 [TRANSFER STEP 5] ✅ Got response, parsing...");
6679
6680 println!("🚨 [TRANSFER STEP 6] Parsing transfer response...");
6681 let result = parser::parse_transfer(&data);
6682 match &result {
6683 Ok(_) => tracing::error!("🚨 [TRANSFER STEP 6] ✅ Parse successful"),
6684 Err(e) => tracing::error!("🚨 [TRANSFER STEP 6] ❌ Parse failed: {:?}", e),
6685 }
6686 result
6687 }
6688
6689 /// Fetch internal transfer history
6690 ///
6691 /// Retrieve historical records of transfers between accounts
6692 ///
6693 /// # Arguments
6694 ///
6695 /// * `currency` - Optional asset code filter
6696 /// * `since` - Optional start timestamp (milliseconds)
6697 /// * `limit` - Optional quantity limit (default 100, maximum 100)
6698 /// * `params` - Optional additional parameters (may include fromSymbol/toSymbol to specify transfer direction)
6699 ///
6700 /// # API Endpoint
6701 /// - Endpoint: GET /sapi/v1/asset/transfer
6702 /// - Weight: 1
6703 /// - Required permission: USER_DATA
6704 /// - Signature required: Yes
6705 ///
6706 /// # Examples
6707 /// ```rust,no_run
6708 /// # use ccxt_exchanges::binance::Binance;
6709 /// # use ccxt_core::ExchangeConfig;
6710 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
6711 /// let config = ExchangeConfig {
6712 /// api_key: Some("your_api_key".to_string()),
6713 /// secret: Some("your_secret".to_string()),
6714 /// ..Default::default()
6715 /// };
6716 /// let binance = Binance::new(config)?;
6717 ///
6718 /// // Query recent USDT transfer records
6719 /// let transfers = binance.fetch_transfers(
6720 /// Some("USDT"),
6721 /// None,
6722 /// Some(50),
6723 /// None
6724 /// ).await?;
6725 ///
6726 /// for transfer in transfers {
6727 /// println!("{} {} from {:?} to {:?}",
6728 /// transfer.amount,
6729 /// transfer.currency,
6730 /// transfer.from_account,
6731 /// transfer.to_account
6732 /// );
6733 /// }
6734 /// # Ok(())
6735 /// # }
6736 /// ```
6737 pub async fn fetch_transfers(
6738 &self,
6739 currency: Option<&str>,
6740 since: Option<u64>,
6741 limit: Option<u64>,
6742 params: Option<HashMap<String, String>>,
6743 ) -> Result<Vec<Transfer>> {
6744 self.check_required_credentials()?;
6745
6746 let mut request_params = params.unwrap_or_default();
6747
6748 // The 'type' parameter is required; return error if not provided in params
6749 let transfer_type = request_params
6750 .get("type")
6751 .ok_or_else(|| {
6752 Error::invalid_request(
6753 "type parameter is required for fetch_transfers. Examples: MAIN_UMFUTURE, MAIN_CMFUTURE, MAIN_MARGIN, etc."
6754 )
6755 })?
6756 .clone();
6757
6758 // Validate fromSymbol and toSymbol are provided when required
6759 // fromSymbol is required when type is ISOLATEDMARGIN_MARGIN or ISOLATEDMARGIN_ISOLATEDMARGIN
6760 if transfer_type == "ISOLATEDMARGIN_MARGIN"
6761 || transfer_type == "ISOLATEDMARGIN_ISOLATEDMARGIN"
6762 {
6763 if !request_params.contains_key("fromSymbol") {
6764 return Err(Error::invalid_request(format!(
6765 "fromSymbol is required when type is {}",
6766 transfer_type
6767 )));
6768 }
6769 }
6770
6771 // toSymbol is required when type is MARGIN_ISOLATEDMARGIN or ISOLATEDMARGIN_ISOLATEDMARGIN
6772 if transfer_type == "MARGIN_ISOLATEDMARGIN"
6773 || transfer_type == "ISOLATEDMARGIN_ISOLATEDMARGIN"
6774 {
6775 if !request_params.contains_key("toSymbol") {
6776 return Err(Error::invalid_request(format!(
6777 "toSymbol is required when type is {}",
6778 transfer_type
6779 )));
6780 }
6781 }
6782
6783 if let Some(code) = currency {
6784 request_params.insert("asset".to_string(), code.to_uppercase());
6785 }
6786
6787 if let Some(ts) = since {
6788 request_params.insert("startTime".to_string(), ts.to_string());
6789 }
6790
6791 let size = limit.unwrap_or(10).min(100);
6792 request_params.insert("size".to_string(), size.to_string());
6793
6794 if !request_params.contains_key("current") {
6795 request_params.insert("current".to_string(), "1".to_string());
6796 }
6797
6798 let timestamp = self.fetch_time_raw().await?;
6799 let auth = self.get_auth()?;
6800 let signed_params =
6801 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
6802
6803 let query_string = auth.build_query_string(&signed_params);
6804 let url = format!(
6805 "{}/sapi/v1/asset/transfer?{}",
6806 self.urls().sapi,
6807 query_string
6808 );
6809
6810 let mut headers = reqwest::header::HeaderMap::new();
6811 auth.add_auth_headers_reqwest(&mut headers);
6812
6813 let data = self.base().http_client.get(&url, Some(headers)).await?;
6814
6815 // Binance response format: { "total": 2, "rows": [...] }
6816 let rows = data["rows"]
6817 .as_array()
6818 .ok_or_else(|| Error::from(ParseError::missing_field("rows")))?;
6819
6820 let mut transfers = Vec::new();
6821 for row in rows {
6822 match parser::parse_transfer(row) {
6823 Ok(transfer) => transfers.push(transfer),
6824 Err(e) => {
6825 warn!(error = %e, "Failed to parse transfer");
6826 }
6827 }
6828 }
6829
6830 Ok(transfers)
6831 }
6832 // ============================================================================
6833 // Futures Transfer API
6834 // ============================================================================
6835
6836 /// Execute futures account transfer
6837 ///
6838 /// Transfer assets between spot account and futures accounts
6839 ///
6840 /// # Arguments
6841 ///
6842 /// * `currency` - Asset code (e.g. "USDT", "BTC")
6843 /// * `amount` - Transfer quantity
6844 /// * `transfer_type` - Transfer type:
6845 /// - 1: Spot account → USDT-M futures account
6846 /// - 2: USDT-M futures account → Spot account
6847 /// - 3: Spot account → COIN-M futures account
6848 /// - 4: COIN-M futures account → Spot account
6849 /// * `params` - Optional additional parameters
6850 ///
6851 /// # API Endpoint
6852 /// - Endpoint: POST /sapi/v1/futures/transfer
6853 /// - Weight: 1
6854 /// - Required permission: TRADE
6855 /// - Signature required: Yes
6856 ///
6857 /// # Differences from transfer()
6858 /// - `futures_transfer()`: Futures-specific API with concise parameters, only requires type (1-4)
6859 /// - `transfer()`: Generic API supporting 8 account types, requires fromSymbol/toSymbol parameters
6860 ///
6861 /// # Examples
6862 /// ```rust,no_run
6863 /// # use ccxt_exchanges::binance::Binance;
6864 /// # use ccxt_core::ExchangeConfig;
6865 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
6866 /// let config = ExchangeConfig {
6867 /// api_key: Some("your_api_key".to_string()),
6868 /// secret: Some("your_secret".to_string()),
6869 /// ..Default::default()
6870 /// };
6871 /// let binance = Binance::new(config)?;
6872 ///
6873 /// // Transfer 100 USDT from spot to USDT-M futures account
6874 /// let transfer = binance.futures_transfer(
6875 /// "USDT",
6876 /// 100.0,
6877 /// 1, // type 1 = Spot → USDT-M futures
6878 /// None
6879 /// ).await?;
6880 ///
6881 /// println!("Transfer ID: {:?}", transfer.id);
6882 /// println!("Status: {}", transfer.status);
6883 /// # Ok(())
6884 /// # }
6885 /// ```
6886 pub async fn futures_transfer(
6887 &self,
6888 currency: &str,
6889 amount: f64,
6890 transfer_type: i32,
6891 params: Option<HashMap<String, String>>,
6892 ) -> Result<Transfer> {
6893 self.check_required_credentials()?;
6894
6895 // Validate transfer_type parameter range
6896 if !(1..=4).contains(&transfer_type) {
6897 return Err(Error::invalid_request(format!(
6898 "Invalid futures transfer type: {}. Must be between 1 and 4",
6899 transfer_type
6900 )));
6901 }
6902
6903 // Use parser helper function to get account names (for response parsing)
6904 let (from_account, to_account) = parser::parse_futures_transfer_type(transfer_type)?;
6905
6906 let mut request_params = params.unwrap_or_default();
6907 request_params.insert("asset".to_string(), currency.to_uppercase());
6908 request_params.insert("amount".to_string(), amount.to_string());
6909 request_params.insert("type".to_string(), transfer_type.to_string());
6910
6911 let auth = self.get_auth()?;
6912 let signed_params = auth.sign_params(&request_params)?;
6913
6914 let query_string: Vec<String> = signed_params
6915 .iter()
6916 .map(|(k, v)| format!("{}={}", k, v))
6917 .collect();
6918 let url = format!(
6919 "{}/futures/transfer?{}",
6920 self.urls().sapi,
6921 query_string.join("&")
6922 );
6923
6924 let mut headers = reqwest::header::HeaderMap::new();
6925 auth.add_auth_headers_reqwest(&mut headers);
6926
6927 let data = self
6928 .base()
6929 .http_client
6930 .post(&url, Some(headers), None)
6931 .await?;
6932
6933 // Binance response format: { "tranId": 100000001 }
6934 // Manually construct Transfer object
6935 let tran_id = data["tranId"]
6936 .as_i64()
6937 .ok_or_else(|| Error::from(ParseError::missing_field("tranId")))?;
6938
6939 let timestamp = chrono::Utc::now().timestamp_millis();
6940 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
6941 .map(|dt| dt.to_rfc3339())
6942 .unwrap_or_default();
6943
6944 Ok(Transfer {
6945 id: Some(tran_id.to_string()),
6946 timestamp: timestamp as u64,
6947 datetime,
6948 currency: currency.to_uppercase(),
6949 amount,
6950 from_account: Some(from_account.to_string()),
6951 to_account: Some(to_account.to_string()),
6952 status: "pending".to_string(), // Transfer request submitted, but status unknown
6953 info: Some(data),
6954 })
6955 }
6956
6957 /// Fetch futures transfer history
6958 ///
6959 /// Retrieve historical records of transfers between spot and futures accounts
6960 ///
6961 /// # Arguments
6962 ///
6963 /// * `currency` - Asset code (e.g. "USDT", "BTC")
6964 /// * `since` - Optional start timestamp (milliseconds)
6965 /// * `limit` - Optional quantity limit (default 10, maximum 100)
6966 /// * `params` - Optional additional parameters
6967 /// - `endTime`: End timestamp (milliseconds)
6968 /// - `current`: Current page number (default 1)
6969 ///
6970 /// # API Endpoint
6971 /// - Endpoint: GET /sapi/v1/futures/transfer
6972 /// - Weight: 10
6973 /// - Required permission: USER_DATA
6974 /// - Signature required: Yes
6975 ///
6976 /// # Return Fields
6977 /// - tranId: Transfer ID
6978 /// - asset: Asset code
6979 /// - amount: Transfer quantity
6980 /// - type: Transfer type (1-4)
6981 /// - timestamp: Transfer time
6982 /// - status: Transfer status (CONFIRMED/FAILED/PENDING)
6983 ///
6984 /// # Examples
6985 /// ```rust,no_run
6986 /// # use ccxt_exchanges::binance::Binance;
6987 /// # use ccxt_core::ExchangeConfig;
6988 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
6989 /// let config = ExchangeConfig {
6990 /// api_key: Some("your_api_key".to_string()),
6991 /// secret: Some("your_secret".to_string()),
6992 /// ..Default::default()
6993 /// };
6994 /// let binance = Binance::new(config)?;
6995 ///
6996 /// // Query USDT transfer records from the last 30 days
6997 /// let since = chrono::Utc::now().timestamp_millis() as u64 - 30 * 24 * 60 * 60 * 1000;
6998 /// let transfers = binance.fetch_futures_transfers(
6999 /// "USDT",
7000 /// Some(since),
7001 /// Some(50),
7002 /// None
7003 /// ).await?;
7004 ///
7005 /// for transfer in transfers {
7006 /// println!("{} {} from {:?} to {:?} - Status: {}",
7007 /// transfer.amount,
7008 /// transfer.currency,
7009 /// transfer.from_account,
7010 /// transfer.to_account,
7011 /// transfer.status
7012 /// );
7013 /// }
7014 /// # Ok(())
7015 /// # }
7016 /// ```
7017 pub async fn fetch_futures_transfers(
7018 &self,
7019 currency: &str,
7020 since: Option<u64>,
7021 limit: Option<u64>,
7022 params: Option<HashMap<String, String>>,
7023 ) -> Result<Vec<Transfer>> {
7024 self.check_required_credentials()?;
7025
7026 let mut request_params = params.unwrap_or_default();
7027
7028 request_params.insert("asset".to_string(), currency.to_uppercase());
7029
7030 if let Some(ts) = since {
7031 request_params.insert("startTime".to_string(), ts.to_string());
7032 }
7033
7034 // Binance limit: size maximum 100
7035 let size = limit.unwrap_or(10).min(100);
7036 request_params.insert("size".to_string(), size.to_string());
7037
7038 if !request_params.contains_key("current") {
7039 request_params.insert("current".to_string(), "1".to_string());
7040 }
7041
7042 let auth = self.get_auth()?;
7043 let signed_params = auth.sign_params(&request_params)?;
7044
7045 let query_string: Vec<String> = signed_params
7046 .iter()
7047 .map(|(k, v)| format!("{}={}", k, v))
7048 .collect();
7049 let url = format!(
7050 "{}/futures/transfer?{}",
7051 self.urls().sapi,
7052 query_string.join("&")
7053 );
7054
7055 let mut headers = reqwest::header::HeaderMap::new();
7056 auth.add_auth_headers_reqwest(&mut headers);
7057
7058 let data = self.base().http_client.get(&url, Some(headers)).await?;
7059
7060 // Binance response format: { "total": 2, "rows": [...] }
7061 let rows = data["rows"]
7062 .as_array()
7063 .ok_or_else(|| Error::from(ParseError::missing_field("rows")))?;
7064
7065 let mut transfers = Vec::new();
7066 for row in rows {
7067 // Parse type field and convert to account names
7068 if let Some(transfer_type) = row["type"].as_i64() {
7069 if let Ok((from_account, to_account)) =
7070 parser::parse_futures_transfer_type(transfer_type as i32)
7071 {
7072 let tran_id = row["tranId"].as_i64().map(|id| id.to_string());
7073
7074 let timestamp = row["timestamp"]
7075 .as_i64()
7076 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
7077
7078 let datetime = chrono::DateTime::from_timestamp_millis(timestamp)
7079 .map(|dt| dt.to_rfc3339())
7080 .unwrap_or_default();
7081
7082 let asset = row["asset"].as_str().unwrap_or(currency).to_string();
7083
7084 let amount = if let Some(amount_str) = row["amount"].as_str() {
7085 amount_str.parse::<f64>().unwrap_or(0.0)
7086 } else {
7087 row["amount"].as_f64().unwrap_or(0.0)
7088 };
7089
7090 let status = row["status"].as_str().unwrap_or("SUCCESS").to_lowercase();
7091
7092 transfers.push(Transfer {
7093 id: tran_id,
7094 timestamp: timestamp as u64,
7095 datetime,
7096 currency: asset,
7097 amount,
7098 from_account: Some(from_account.to_string()),
7099 to_account: Some(to_account.to_string()),
7100 status,
7101 info: Some(row.clone()),
7102 });
7103 } else {
7104 warn!(
7105 transfer_type = transfer_type,
7106 "Invalid futures transfer type"
7107 );
7108 }
7109 }
7110 }
7111
7112 Ok(transfers)
7113 }
7114
7115 /// Fetch deposit and withdrawal fee information
7116 ///
7117 /// Query deposit and withdrawal fee configuration for all assets
7118 ///
7119 /// # Arguments
7120 ///
7121 /// * `currency` - Optional asset code filter (returns all assets if not specified)
7122 /// * `params` - Optional additional parameters
7123 ///
7124 /// # API Endpoint
7125 /// - Endpoint: GET /sapi/v1/capital/config/getall
7126 /// - Weight: 10
7127 /// - Required permission: USER_DATA
7128 /// - Signature required: Yes
7129 ///
7130 /// # Return Information
7131 /// - Withdrawal fee
7132 /// - Minimum/maximum withdrawal amount
7133 /// - Deposit/withdrawal support status
7134 /// - Network configurations (BTC, ETH, BSC, etc.)
7135 /// - Confirmation requirements
7136 ///
7137 /// # Examples
7138 /// ```rust,no_run
7139 /// # use ccxt_exchanges::binance::Binance;
7140 /// # use ccxt_core::ExchangeConfig;
7141 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
7142 /// let config = ExchangeConfig {
7143 /// api_key: Some("your_api_key".to_string()),
7144 /// secret: Some("your_secret".to_string()),
7145 /// ..Default::default()
7146 /// };
7147 /// let binance = Binance::new(config)?;
7148 ///
7149 /// // Fetch fee information for all assets
7150 /// let fees = binance.fetch_deposit_withdraw_fees(None, None).await?;
7151 ///
7152 /// // Fetch fee information for specific asset (e.g. USDT)
7153 /// let usdt_fees = binance.fetch_deposit_withdraw_fees(Some("USDT"), None).await?;
7154 ///
7155 /// for fee in usdt_fees {
7156 /// println!("{}: withdraw fee = {}, min = {}, max = {}",
7157 /// fee.currency,
7158 /// fee.withdraw_fee,
7159 /// fee.withdraw_min,
7160 /// fee.withdraw_max
7161 /// );
7162 ///
7163 /// for network in &fee.networks {
7164 /// println!(" Network {}: fee = {}", network.network, network.withdraw_fee);
7165 /// }
7166 /// }
7167 /// # Ok(())
7168 /// # }
7169 /// ```
7170 pub async fn fetch_deposit_withdraw_fees(
7171 &self,
7172 currency: Option<&str>,
7173 params: Option<HashMap<String, String>>,
7174 ) -> Result<Vec<ccxt_core::types::DepositWithdrawFee>> {
7175 self.check_required_credentials()?;
7176
7177 let request_params = params.unwrap_or_default();
7178
7179 // Note: Binance /sapi/v1/capital/config/getall endpoint returns all currencies
7180 // If currency is specified, client-side filtering is required
7181
7182 let auth = self.get_auth()?;
7183 let signed_params = auth.sign_params(&request_params)?;
7184
7185 let query_string: Vec<String> = signed_params
7186 .iter()
7187 .map(|(k, v)| format!("{}={}", k, v))
7188 .collect();
7189 let url = format!(
7190 "{}/capital/config/getall?{}",
7191 self.urls().sapi,
7192 query_string.join("&")
7193 );
7194
7195 let mut headers = reqwest::header::HeaderMap::new();
7196 auth.add_auth_headers_reqwest(&mut headers);
7197
7198 let data = self.base().http_client.get(&url, Some(headers)).await?;
7199
7200 let all_fees = parser::parse_deposit_withdraw_fees(&data)?;
7201
7202 // Filter results if currency is specified
7203 if let Some(code) = currency {
7204 let code_upper = code.to_uppercase();
7205 Ok(all_fees
7206 .into_iter()
7207 .filter(|fee| fee.currency == code_upper)
7208 .collect())
7209 } else {
7210 Ok(all_fees)
7211 }
7212 }
7213
7214 // ============================================================================
7215 // Deposit/Withdrawal API - P1.1
7216 // ============================================================================
7217
7218 /// Withdraw to external address
7219 ///
7220 /// # Arguments
7221 /// * `code` - Currency code (e.g. "BTC", "USDT")
7222 /// * `amount` - Withdrawal amount
7223 /// * `address` - Withdrawal address
7224 /// * `params` - Optional parameters
7225 /// - `tag`: Address tag (e.g. XRP tag)
7226 /// - `network`: Network type (e.g. "ETH", "BSC", "TRX")
7227 /// - `addressTag`: Address tag (alias)
7228 /// - `name`: Address memo name
7229 /// - `walletType`: Wallet type (0=spot, 1=funding)
7230 ///
7231 /// # API Endpoint
7232 /// - Endpoint: POST /sapi/v1/capital/withdraw/apply
7233 /// - Required permission: API key with withdrawal enabled
7234 ///
7235 /// # Examples
7236 /// ```no_run
7237 /// # use ccxt_exchanges::binance::Binance;
7238 /// # use ccxt_core::ExchangeConfig;
7239 /// # use std::collections::HashMap;
7240 /// # async fn example() -> ccxt_core::Result<()> {
7241 /// let binance = Binance::new(ExchangeConfig::default())?;
7242 ///
7243 /// // Basic withdrawal
7244 /// let tx = binance.withdraw("USDT", "100.0", "TXxxx...", None).await?;
7245 /// println!("Withdrawal ID: {}", tx.id);
7246 ///
7247 /// // Withdrawal with specific network
7248 /// let mut params = HashMap::new();
7249 /// params.insert("network".to_string(), "TRX".to_string());
7250 /// let tx = binance.withdraw("USDT", "100.0", "TXxxx...", Some(params)).await?;
7251 /// # Ok(())
7252 /// # }
7253 /// ```
7254 pub async fn withdraw(
7255 &self,
7256 code: &str,
7257 amount: &str,
7258 address: &str,
7259 params: Option<HashMap<String, String>>,
7260 ) -> Result<Transaction> {
7261 self.check_required_credentials()?;
7262
7263 let mut request_params = params.unwrap_or_default();
7264
7265 request_params.insert("coin".to_string(), code.to_uppercase());
7266 request_params.insert("address".to_string(), address.to_string());
7267 request_params.insert("amount".to_string(), amount.to_string());
7268
7269 // Handle optional tag parameter (supports both 'tag' and 'addressTag' names)
7270 if let Some(tag) = request_params.get("tag").cloned() {
7271 request_params.insert("addressTag".to_string(), tag.clone());
7272 }
7273
7274 let auth = self.get_auth()?;
7275 let signed_params = auth.sign_params(&request_params)?;
7276
7277 let query_string: Vec<String> = signed_params
7278 .iter()
7279 .map(|(k, v)| format!("{}={}", k, v))
7280 .collect();
7281 let url = format!(
7282 "{}/capital/withdraw/apply?{}",
7283 self.urls().sapi,
7284 query_string.join("&")
7285 );
7286
7287 let mut headers = reqwest::header::HeaderMap::new();
7288 auth.add_auth_headers_reqwest(&mut headers);
7289
7290 let data = self
7291 .base()
7292 .http_client
7293 .post(&url, Some(headers), None)
7294 .await?;
7295
7296 parser::parse_transaction(&data, TransactionType::Withdrawal)
7297 }
7298
7299 /// Fetch deposit history
7300 ///
7301 /// # Arguments
7302 /// * `code` - Optional currency code (e.g. "BTC", "USDT")
7303 /// * `since` - Optional start timestamp (milliseconds)
7304 /// * `limit` - Optional quantity limit
7305 /// * `params` - Optional parameters
7306 /// - `coin`: Currency (overrides code parameter)
7307 /// - `status`: Status filter (0=pending, 6=credited, 1=success)
7308 /// - `startTime`: Start timestamp (milliseconds)
7309 /// - `endTime`: End timestamp (milliseconds)
7310 /// - `offset`: Offset (for pagination)
7311 /// - `txId`: Transaction ID filter
7312 ///
7313 /// # API Endpoint
7314 /// - Endpoint: GET /sapi/v1/capital/deposit/hisrec
7315 /// - Required permission: API key
7316 ///
7317 /// # Time Range Limit
7318 /// - Maximum query range: 90 days
7319 /// - Default returns last 90 days if no time range provided
7320 ///
7321 /// # Examples
7322 /// ```no_run
7323 /// # use ccxt_exchanges::binance::Binance;
7324 /// # use ccxt_core::ExchangeConfig;
7325 /// # async fn example() -> ccxt_core::Result<()> {
7326 /// let binance = Binance::new(ExchangeConfig::default())?;
7327 ///
7328 /// // Query all deposits
7329 /// let deposits = binance.fetch_deposits(None, None, Some(100), None).await?;
7330 /// println!("Total deposits: {}", deposits.len());
7331 ///
7332 /// // Query BTC deposits
7333 /// let btc_deposits = binance.fetch_deposits(Some("BTC"), None, None, None).await?;
7334 /// # Ok(())
7335 /// # }
7336 /// ```
7337 pub async fn fetch_deposits(
7338 &self,
7339 code: Option<&str>,
7340 since: Option<i64>,
7341 limit: Option<i64>,
7342 params: Option<HashMap<String, String>>,
7343 ) -> Result<Vec<Transaction>> {
7344 self.check_required_credentials()?;
7345
7346 let mut request_params = params.unwrap_or_default();
7347
7348 if let Some(coin) = code {
7349 request_params
7350 .entry("coin".to_string())
7351 .or_insert_with(|| coin.to_uppercase());
7352 }
7353
7354 if let Some(start_time) = since {
7355 request_params
7356 .entry("startTime".to_string())
7357 .or_insert_with(|| start_time.to_string());
7358 }
7359
7360 if let Some(lim) = limit {
7361 request_params.insert("limit".to_string(), lim.to_string());
7362 }
7363
7364 let auth = self.get_auth()?;
7365 let signed_params = auth.sign_params(&request_params)?;
7366
7367 let query_string: Vec<String> = signed_params
7368 .iter()
7369 .map(|(k, v)| format!("{}={}", k, v))
7370 .collect();
7371 let url = format!(
7372 "{}/capital/deposit/hisrec?{}",
7373 self.urls().sapi,
7374 query_string.join("&")
7375 );
7376
7377 let mut headers = reqwest::header::HeaderMap::new();
7378 auth.add_auth_headers_reqwest(&mut headers);
7379
7380 let data = self.base().http_client.get(&url, Some(headers)).await?;
7381
7382 if let Some(arr) = data.as_array() {
7383 arr.iter()
7384 .map(|item| parser::parse_transaction(item, TransactionType::Deposit))
7385 .collect()
7386 } else {
7387 Err(Error::invalid_request("Expected array response"))
7388 }
7389 }
7390
7391 /// Fetch withdrawal history
7392 ///
7393 /// # Arguments
7394 /// * `code` - Optional currency code (e.g. "BTC", "USDT")
7395 /// * `since` - Optional start timestamp (milliseconds)
7396 /// * `limit` - Optional quantity limit
7397 /// * `params` - Optional parameters
7398 /// - `coin`: Currency (overrides code parameter)
7399 /// - `withdrawOrderId`: Withdrawal order ID
7400 /// - `status`: Status filter (0=email sent, 1=cancelled, 2=awaiting approval, 3=rejected, 4=processing, 5=failure, 6=completed)
7401 /// - `startTime`: Start timestamp (milliseconds)
7402 /// - `endTime`: End timestamp (milliseconds)
7403 /// - `offset`: Offset (for pagination)
7404 ///
7405 /// # API Endpoint
7406 /// - Endpoint: GET /sapi/v1/capital/withdraw/history
7407 /// - Required permission: API key
7408 ///
7409 /// # Time Range Limit
7410 /// - Maximum query range: 90 days
7411 /// - Default returns last 90 days if no time range provided
7412 ///
7413 /// # Examples
7414 /// ```no_run
7415 /// # use ccxt_exchanges::binance::Binance;
7416 /// # use ccxt_core::ExchangeConfig;
7417 /// # async fn example() -> ccxt_core::Result<()> {
7418 /// let binance = Binance::new(ExchangeConfig::default())?;
7419 ///
7420 /// // Query all withdrawals
7421 /// let withdrawals = binance.fetch_withdrawals(None, None, Some(100), None).await?;
7422 /// println!("Total withdrawals: {}", withdrawals.len());
7423 ///
7424 /// // Query USDT withdrawals
7425 /// let usdt_withdrawals = binance.fetch_withdrawals(Some("USDT"), None, None, None).await?;
7426 /// # Ok(())
7427 /// # }
7428 /// ```
7429 pub async fn fetch_withdrawals(
7430 &self,
7431 code: Option<&str>,
7432 since: Option<i64>,
7433 limit: Option<i64>,
7434 params: Option<HashMap<String, String>>,
7435 ) -> Result<Vec<Transaction>> {
7436 self.check_required_credentials()?;
7437
7438 let mut request_params = params.unwrap_or_default();
7439
7440 if let Some(coin) = code {
7441 request_params
7442 .entry("coin".to_string())
7443 .or_insert_with(|| coin.to_uppercase());
7444 }
7445
7446 if let Some(start_time) = since {
7447 request_params
7448 .entry("startTime".to_string())
7449 .or_insert_with(|| start_time.to_string());
7450 }
7451
7452 if let Some(lim) = limit {
7453 request_params.insert("limit".to_string(), lim.to_string());
7454 }
7455
7456 let auth = self.get_auth()?;
7457 let signed_params = auth.sign_params(&request_params)?;
7458
7459 let query_string: Vec<String> = signed_params
7460 .iter()
7461 .map(|(k, v)| format!("{}={}", k, v))
7462 .collect();
7463 let url = format!(
7464 "{}/capital/withdraw/history?{}",
7465 self.urls().sapi,
7466 query_string.join("&")
7467 );
7468
7469 let mut headers = reqwest::header::HeaderMap::new();
7470 auth.add_auth_headers_reqwest(&mut headers);
7471
7472 let data = self.base().http_client.get(&url, Some(headers)).await?;
7473
7474 if let Some(arr) = data.as_array() {
7475 arr.iter()
7476 .map(|item| parser::parse_transaction(item, TransactionType::Withdrawal))
7477 .collect()
7478 } else {
7479 Err(Error::invalid_request("Expected array response"))
7480 }
7481 }
7482
7483 /// Fetch deposit address
7484 ///
7485 /// # Arguments
7486 /// * `code` - Currency code (e.g. "BTC", "USDT")
7487 /// * `params` - Optional parameters
7488 /// - `network`: Network type (e.g. "ETH", "BSC", "TRX")
7489 /// - `coin`: Currency (overrides code parameter)
7490 ///
7491 /// # API Endpoint
7492 /// - Endpoint: GET /sapi/v1/capital/deposit/address
7493 /// - Required permission: API key
7494 ///
7495 /// # Notes
7496 /// - Binance automatically generates new address if none exists
7497 /// - Some currencies require network parameter (e.g. USDT can be ETH/BSC/TRX network)
7498 ///
7499 /// # Examples
7500 /// ```no_run
7501 /// # use ccxt_exchanges::binance::Binance;
7502 /// # use ccxt_core::ExchangeConfig;
7503 /// # use std::collections::HashMap;
7504 /// # async fn example() -> ccxt_core::Result<()> {
7505 /// let binance = Binance::new(ExchangeConfig::default())?;
7506 ///
7507 /// // Fetch BTC deposit address
7508 /// let addr = binance.fetch_deposit_address("BTC", None).await?;
7509 /// println!("BTC address: {}", addr.address);
7510 ///
7511 /// // Fetch USDT deposit address on TRX network
7512 /// let mut params = HashMap::new();
7513 /// params.insert("network".to_string(), "TRX".to_string());
7514 /// let addr = binance.fetch_deposit_address("USDT", Some(params)).await?;
7515 /// println!("USDT-TRX address: {}", addr.address);
7516 /// # Ok(())
7517 /// # }
7518 /// ```
7519 pub async fn fetch_deposit_address(
7520 &self,
7521 code: &str,
7522 params: Option<HashMap<String, String>>,
7523 ) -> Result<DepositAddress> {
7524 self.check_required_credentials()?;
7525
7526 let mut request_params = params.unwrap_or_default();
7527
7528 request_params
7529 .entry("coin".to_string())
7530 .or_insert_with(|| code.to_uppercase());
7531
7532 let auth = self.get_auth()?;
7533 let signed_params = auth.sign_params(&request_params)?;
7534
7535 let query_string: Vec<String> = signed_params
7536 .iter()
7537 .map(|(k, v)| format!("{}={}", k, v))
7538 .collect();
7539 let url = format!(
7540 "{}/capital/deposit/address?{}",
7541 self.urls().sapi,
7542 query_string.join("&")
7543 );
7544
7545 let mut headers = reqwest::header::HeaderMap::new();
7546 auth.add_auth_headers_reqwest(&mut headers);
7547
7548 let data = self.base().http_client.get(&url, Some(headers)).await?;
7549
7550 parser::parse_deposit_address(&data)
7551 }
7552
7553 // ============================================================================
7554 // OCO Order Management Methods (P1.3)
7555 // ============================================================================
7556
7557 /// Create OCO order (One-Cancels-the-Other)
7558 ///
7559 /// OCO order contains two orders: take-profit order (limit) + stop-loss order (stop-limit).
7560 /// When one order is filled, the other is automatically cancelled.
7561 ///
7562 /// # Arguments
7563 ///
7564 /// * `symbol` - Trading pair symbol, e.g. "BTC/USDT"
7565 /// * `side` - Order side (Buy or Sell)
7566 /// * `amount` - Order quantity
7567 /// * `price` - Take-profit price (limit order price)
7568 /// * `stop_price` - Stop-loss trigger price
7569 /// * `stop_limit_price` - Optional stop-limit order price, defaults to stop_price
7570 /// * `params` - Optional additional parameters
7571 ///
7572 /// # Returns
7573 ///
7574 /// Returns created OCO order information
7575 ///
7576 /// # Example
7577 ///
7578 /// ```no_run
7579 /// # use ccxt_exchanges::binance::Binance;
7580 /// # use ccxt_core::{ExchangeConfig, types::OrderSide};
7581 /// # async fn example() -> ccxt_core::Result<()> {
7582 /// let binance = Binance::new(ExchangeConfig::default())?;
7583 /// // Long BTC position: take profit at 50000, stop loss at 45000
7584 /// let oco = binance.create_oco_order(
7585 /// "BTC/USDT",
7586 /// OrderSide::Sell,
7587 /// 0.1,
7588 /// 50000.0,
7589 /// 45000.0,
7590 /// Some(44500.0),
7591 /// None
7592 /// ).await?;
7593 /// println!("OCO order ID: {}", oco.order_list_id);
7594 /// # Ok(())
7595 /// # }
7596 /// ```
7597 pub async fn create_oco_order(
7598 &self,
7599 symbol: &str,
7600 side: OrderSide,
7601 amount: f64,
7602 price: f64,
7603 stop_price: f64,
7604 stop_limit_price: Option<f64>,
7605 params: Option<HashMap<String, String>>,
7606 ) -> Result<OcoOrder> {
7607 self.check_required_credentials()?;
7608
7609 let market = self.base().market(symbol).await?;
7610 let mut request_params = HashMap::new();
7611
7612 request_params.insert("symbol".to_string(), market.id.clone());
7613 request_params.insert(
7614 "side".to_string(),
7615 match side {
7616 OrderSide::Buy => "BUY".to_string(),
7617 OrderSide::Sell => "SELL".to_string(),
7618 },
7619 );
7620 request_params.insert("quantity".to_string(), amount.to_string());
7621 request_params.insert("price".to_string(), price.to_string());
7622 request_params.insert("stopPrice".to_string(), stop_price.to_string());
7623
7624 // Default stop-limit price to stop_price if not specified
7625 let stop_limit = stop_limit_price.unwrap_or(stop_price);
7626 request_params.insert("stopLimitPrice".to_string(), stop_limit.to_string());
7627
7628 request_params.insert("stopLimitTimeInForce".to_string(), "GTC".to_string());
7629
7630 if let Some(extra) = params {
7631 for (k, v) in extra {
7632 request_params.insert(k, v);
7633 }
7634 }
7635
7636 let timestamp = self.fetch_time_raw().await?;
7637 let auth = self.get_auth()?;
7638 let signed_params =
7639 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
7640
7641 let url = format!("{}/order/oco", self.urls().private);
7642 let mut headers = HeaderMap::new();
7643 auth.add_auth_headers_reqwest(&mut headers);
7644
7645 let body = serde_json::to_value(&signed_params).map_err(|e| {
7646 Error::from(ParseError::invalid_format(
7647 "data",
7648 format!("Failed to serialize params: {}", e),
7649 ))
7650 })?;
7651
7652 let data = self
7653 .base()
7654 .http_client
7655 .post(&url, Some(headers), Some(body))
7656 .await?;
7657
7658 parser::parse_oco_order(&data)
7659 }
7660
7661 /// Fetch single OCO order
7662 ///
7663 /// # Arguments
7664 ///
7665 /// * `order_list_id` - OCO order list ID
7666 /// * `symbol` - Trading pair symbol
7667 /// * `params` - Optional additional parameters
7668 ///
7669 /// # Returns
7670 ///
7671 /// Returns OCO order information
7672 pub async fn fetch_oco_order(
7673 &self,
7674 order_list_id: i64,
7675 symbol: &str,
7676 params: Option<HashMap<String, String>>,
7677 ) -> Result<OcoOrder> {
7678 self.check_required_credentials()?;
7679
7680 let market = self.base().market(symbol).await?;
7681 let mut request_params = HashMap::new();
7682
7683 request_params.insert("orderListId".to_string(), order_list_id.to_string());
7684 request_params.insert("symbol".to_string(), market.id.clone());
7685
7686 if let Some(extra) = params {
7687 for (k, v) in extra {
7688 request_params.insert(k, v);
7689 }
7690 }
7691
7692 let timestamp = self.fetch_time_raw().await?;
7693 let auth = self.get_auth()?;
7694 let signed_params =
7695 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
7696
7697 let mut url = format!("{}/orderList?", self.urls().private);
7698 for (key, value) in &signed_params {
7699 url.push_str(&format!("{}={}&", key, value));
7700 }
7701
7702 let mut headers = HeaderMap::new();
7703 auth.add_auth_headers_reqwest(&mut headers);
7704
7705 let data = self.base().http_client.get(&url, Some(headers)).await?;
7706
7707 parser::parse_oco_order(&data)
7708 }
7709
7710 /// Fetch all OCO orders
7711 ///
7712 /// # Arguments
7713 ///
7714 /// * `symbol` - Trading pair symbol
7715 /// * `since` - Optional start timestamp (milliseconds)
7716 /// * `limit` - Optional record quantity limit (default 500, maximum 1000)
7717 /// * `params` - Optional additional parameters
7718 ///
7719 /// # Returns
7720 ///
7721 /// Returns list of OCO orders
7722 pub async fn fetch_oco_orders(
7723 &self,
7724 symbol: &str,
7725 since: Option<u64>,
7726 limit: Option<u32>,
7727 params: Option<HashMap<String, String>>,
7728 ) -> Result<Vec<OcoOrder>> {
7729 self.check_required_credentials()?;
7730
7731 let market = self.base().market(symbol).await?;
7732 let mut request_params = HashMap::new();
7733
7734 request_params.insert("symbol".to_string(), market.id.clone());
7735
7736 if let Some(s) = since {
7737 request_params.insert("startTime".to_string(), s.to_string());
7738 }
7739
7740 if let Some(l) = limit {
7741 request_params.insert("limit".to_string(), l.to_string());
7742 }
7743
7744 if let Some(extra) = params {
7745 for (k, v) in extra {
7746 request_params.insert(k, v);
7747 }
7748 }
7749
7750 let timestamp = self.fetch_time_raw().await?;
7751 let auth = self.get_auth()?;
7752 let signed_params =
7753 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
7754
7755 let mut url = format!("{}/allOrderList?", self.urls().private);
7756 for (key, value) in &signed_params {
7757 url.push_str(&format!("{}={}&", key, value));
7758 }
7759
7760 let mut headers = HeaderMap::new();
7761 auth.add_auth_headers_reqwest(&mut headers);
7762
7763 let data = self.base().http_client.get(&url, Some(headers)).await?;
7764
7765 let oco_array = data.as_array().ok_or_else(|| {
7766 Error::from(ParseError::invalid_format(
7767 "data",
7768 "Expected array of OCO orders",
7769 ))
7770 })?;
7771
7772 let mut oco_orders = Vec::new();
7773 for oco_data in oco_array {
7774 match parser::parse_oco_order(oco_data) {
7775 Ok(oco) => oco_orders.push(oco),
7776 Err(e) => {
7777 warn!(error = %e, "Failed to parse OCO order");
7778 }
7779 }
7780 }
7781
7782 Ok(oco_orders)
7783 }
7784
7785 /// Cancel OCO order
7786 ///
7787 /// # Arguments
7788 ///
7789 /// * `order_list_id` - OCO order list ID
7790 /// * `symbol` - Trading pair symbol
7791 /// * `params` - Optional additional parameters
7792 ///
7793 /// # Returns
7794 ///
7795 /// Returns cancelled OCO order information
7796 pub async fn cancel_oco_order(
7797 &self,
7798 order_list_id: i64,
7799 symbol: &str,
7800 params: Option<HashMap<String, String>>,
7801 ) -> Result<OcoOrder> {
7802 self.check_required_credentials()?;
7803
7804 let market = self.base().market(symbol).await?;
7805 let mut request_params = HashMap::new();
7806
7807 request_params.insert("symbol".to_string(), market.id.clone());
7808 request_params.insert("orderListId".to_string(), order_list_id.to_string());
7809
7810 if let Some(extra) = params {
7811 for (k, v) in extra {
7812 request_params.insert(k, v);
7813 }
7814 }
7815
7816 let timestamp = self.fetch_time_raw().await?;
7817 let auth = self.get_auth()?;
7818 let signed_params =
7819 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
7820
7821 let url = format!("{}/orderList", self.urls().private);
7822 let mut headers = HeaderMap::new();
7823 auth.add_auth_headers_reqwest(&mut headers);
7824
7825 let body = serde_json::to_value(&signed_params).map_err(|e| {
7826 Error::from(ParseError::invalid_format(
7827 "data",
7828 format!("Failed to serialize params: {}", e),
7829 ))
7830 })?;
7831
7832 let data = self
7833 .base()
7834 .http_client
7835 .delete(&url, Some(headers), Some(body))
7836 .await?;
7837
7838 parser::parse_oco_order(&data)
7839 }
7840
7841 /// Create test order (does not place actual order)
7842 ///
7843 /// Used to test if order parameters are correct without submitting to exchange.
7844 ///
7845 /// # Arguments
7846 ///
7847 /// * `symbol` - Trading pair symbol
7848 /// * `order_type` - Order type
7849 /// * `side` - Order side (Buy/Sell)
7850 /// * `amount` - Order quantity
7851 /// * `price` - Optional price
7852 /// * `params` - Optional additional parameters
7853 ///
7854 /// # Returns
7855 ///
7856 /// Returns empty order information (validation result only)
7857 ///
7858 /// # Example
7859 ///
7860 /// ```no_run
7861 /// # use ccxt_exchanges::binance::Binance;
7862 /// # use ccxt_core::{ExchangeConfig, types::{OrderType, OrderSide}};
7863 /// # async fn example() -> ccxt_core::Result<()> {
7864 /// let binance = Binance::new(ExchangeConfig::default())?;
7865 /// // Test if order parameters are correct
7866 /// let test = binance.create_test_order(
7867 /// "BTC/USDT",
7868 /// OrderType::Limit,
7869 /// OrderSide::Buy,
7870 /// 0.001,
7871 /// 40000.0,
7872 /// None
7873 /// ).await?;
7874 /// println!("Order parameters validated successfully");
7875 /// # Ok(())
7876 /// # }
7877 /// ```
7878 pub async fn create_test_order(
7879 &self,
7880 symbol: &str,
7881 order_type: OrderType,
7882 side: OrderSide,
7883 amount: f64,
7884 price: Option<f64>,
7885 params: Option<HashMap<String, String>>,
7886 ) -> Result<Order> {
7887 self.check_required_credentials()?;
7888
7889 let market = self.base().market(symbol).await?;
7890 let mut request_params = HashMap::new();
7891
7892 request_params.insert("symbol".to_string(), market.id.clone());
7893 request_params.insert(
7894 "side".to_string(),
7895 match side {
7896 OrderSide::Buy => "BUY".to_string(),
7897 OrderSide::Sell => "SELL".to_string(),
7898 },
7899 );
7900 request_params.insert(
7901 "type".to_string(),
7902 match order_type {
7903 OrderType::Market => "MARKET".to_string(),
7904 OrderType::Limit => "LIMIT".to_string(),
7905 OrderType::StopLoss => "STOP_LOSS".to_string(),
7906 OrderType::StopLossLimit => "STOP_LOSS_LIMIT".to_string(),
7907 OrderType::TakeProfit => "TAKE_PROFIT".to_string(),
7908 OrderType::TakeProfitLimit => "TAKE_PROFIT_LIMIT".to_string(),
7909 OrderType::LimitMaker => "LIMIT_MAKER".to_string(),
7910 OrderType::StopMarket => "STOP_MARKET".to_string(),
7911 OrderType::StopLimit => "STOP_LIMIT".to_string(),
7912 OrderType::TrailingStop => "TRAILING_STOP_MARKET".to_string(),
7913 },
7914 );
7915 request_params.insert("quantity".to_string(), amount.to_string());
7916
7917 if let Some(p) = price {
7918 request_params.insert("price".to_string(), p.to_string());
7919 }
7920
7921 // Limit orders require timeInForce
7922 if order_type == OrderType::Limit
7923 || order_type == OrderType::StopLossLimit
7924 || order_type == OrderType::TakeProfitLimit
7925 {
7926 if !request_params.contains_key("timeInForce") {
7927 request_params.insert("timeInForce".to_string(), "GTC".to_string());
7928 }
7929 }
7930
7931 if let Some(extra) = params {
7932 for (k, v) in extra {
7933 request_params.insert(k, v);
7934 }
7935 }
7936
7937 let timestamp = self.fetch_time_raw().await?;
7938 let auth = self.get_auth()?;
7939 let signed_params =
7940 auth.sign_with_timestamp(&request_params, timestamp, Some(self.options().recv_window))?;
7941
7942 // Use test endpoint
7943 let url = format!("{}/order/test", self.urls().private);
7944 let mut headers = HeaderMap::new();
7945 auth.add_auth_headers_reqwest(&mut headers);
7946
7947 let body = serde_json::to_value(&signed_params).map_err(|e| {
7948 Error::from(ParseError::invalid_format(
7949 "data",
7950 format!("Failed to serialize params: {}", e),
7951 ))
7952 })?;
7953
7954 let data = self
7955 .base()
7956 .http_client
7957 .post(&url, Some(headers), Some(body))
7958 .await?;
7959
7960 // Test order returns empty object {}, return a dummy order
7961 Ok(Order {
7962 id: "test".to_string(),
7963 client_order_id: None,
7964 timestamp: Some(timestamp as i64),
7965 datetime: Some(
7966 chrono::DateTime::from_timestamp(timestamp as i64 / 1000, 0)
7967 .map(|dt| dt.to_rfc3339())
7968 .unwrap_or_default(),
7969 ),
7970 last_trade_timestamp: None,
7971 status: OrderStatus::Open,
7972 symbol: symbol.to_string(),
7973 order_type,
7974 time_in_force: Some("GTC".to_string()),
7975 side,
7976 price: price.map(Decimal::from_f64_retain).flatten(),
7977 average: None,
7978 amount: Decimal::from_f64_retain(amount)
7979 .ok_or_else(|| Error::from(ParseError::missing_field("amount")))?,
7980 filled: Some(Decimal::ZERO),
7981 remaining: Decimal::from_f64_retain(amount).map(Some).unwrap_or(None),
7982 cost: None,
7983 trades: None,
7984 fee: None,
7985 post_only: None,
7986 reduce_only: None,
7987 trigger_price: None,
7988 stop_price: None,
7989 take_profit_price: None,
7990 stop_loss_price: None,
7991 trailing_delta: None,
7992 trailing_percent: None,
7993 activation_price: None,
7994 callback_rate: None,
7995 working_type: None,
7996 fees: Some(Vec::new()),
7997 info: data
7998 .as_object()
7999 .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
8000 .unwrap_or_default(),
8001 })
8002 }
8003
8004 /// Fetch all isolated margin borrow rates
8005 ///
8006 /// # Arguments
8007 /// * `params` - Optional parameters
8008 ///
8009 /// # Returns
8010 /// HashMap<Symbol, IsolatedBorrowRate>
8011 ///
8012 /// # API Endpoint
8013 /// GET /sapi/v1/margin/isolatedMarginData
8014 ///
8015 /// # Examples
8016 /// ```no_run
8017 /// # use ccxt_exchanges::binance::Binance;
8018 /// # use ccxt_core::ExchangeConfig;
8019 /// # async fn example() -> ccxt_core::Result<()> {
8020 /// let binance = Binance::new(ExchangeConfig::default())?;
8021 /// let rates = binance.fetch_isolated_borrow_rates(None).await?;
8022 /// println!("Got {} symbols", rates.len());
8023 /// # Ok(())
8024 /// # }
8025 /// ```
8026 pub async fn fetch_isolated_borrow_rates(
8027 &self,
8028 params: Option<HashMap<String, String>>,
8029 ) -> Result<HashMap<String, ccxt_core::types::IsolatedBorrowRate>> {
8030 self.check_required_credentials()?;
8031
8032 let request_params = params.unwrap_or_default();
8033
8034 let auth = self.get_auth()?;
8035 let signed_params = auth.sign_params(&request_params)?;
8036
8037 let query_string: Vec<String> = signed_params
8038 .iter()
8039 .map(|(k, v)| format!("{}={}", k, v))
8040 .collect();
8041 let url = format!(
8042 "{}/margin/isolatedMarginData?{}",
8043 self.urls().sapi,
8044 query_string.join("&")
8045 );
8046
8047 let mut headers = reqwest::header::HeaderMap::new();
8048 auth.add_auth_headers_reqwest(&mut headers);
8049
8050 let response = self.base().http_client.get(&url, Some(headers)).await?;
8051
8052 parser::parse_isolated_borrow_rates(&response)
8053 }
8054
8055 /// Fetch borrow interest history
8056 ///
8057 /// # Arguments
8058 /// * `code` - Asset code (optional, e.g., "BTC")
8059 /// * `symbol` - Trading pair symbol (optional, for isolated margin only, e.g., "BTC/USDT")
8060 /// * `since` - Start timestamp (milliseconds)
8061 /// * `limit` - Number of records to return (default 10, max 100)
8062 /// * `params` - Optional parameters
8063 /// - `endTime`: End timestamp
8064 /// - `isolatedSymbol`: Isolated margin trading pair (overrides symbol parameter)
8065 /// - `archived`: Whether to query archived data (default false)
8066 ///
8067 /// # API Endpoint
8068 /// - Spot Margin: GET /sapi/v1/margin/interestHistory
8069 /// - Portfolio Margin: GET /papi/v1/margin/marginInterestHistory
8070 ///
8071 /// # Examples
8072 /// ```no_run
8073 /// # use ccxt_exchanges::binance::Binance;
8074 /// # use ccxt_core::ExchangeConfig;
8075 /// # async fn example() -> ccxt_core::Result<()> {
8076 /// let binance = Binance::new(ExchangeConfig::default())?;
8077 ///
8078 /// // Fetch cross-margin BTC borrow interest
8079 /// let interests = binance.fetch_borrow_interest(
8080 /// Some("BTC"),
8081 /// None,
8082 /// None,
8083 /// Some(10),
8084 /// None
8085 /// ).await?;
8086 ///
8087 /// // Fetch isolated margin BTC/USDT borrow interest
8088 /// let interests = binance.fetch_borrow_interest(
8089 /// None,
8090 /// Some("BTC/USDT"),
8091 /// None,
8092 /// Some(10),
8093 /// None
8094 /// ).await?;
8095 /// # Ok(())
8096 /// # }
8097 /// ```
8098 pub async fn fetch_borrow_interest(
8099 &self,
8100 code: Option<&str>,
8101 symbol: Option<&str>,
8102 since: Option<u64>,
8103 limit: Option<u64>,
8104 params: Option<HashMap<String, String>>,
8105 ) -> Result<Vec<BorrowInterest>> {
8106 self.check_required_credentials()?;
8107
8108 let mut request_params = params.unwrap_or_default();
8109
8110 if let Some(c) = code {
8111 request_params.insert("asset".to_string(), c.to_string());
8112 }
8113
8114 if let Some(s) = symbol {
8115 self.load_markets(false).await?;
8116 let market = self.base().market(s).await?;
8117 request_params.insert("isolatedSymbol".to_string(), market.id.clone());
8118 }
8119
8120 if let Some(st) = since {
8121 request_params.insert("startTime".to_string(), st.to_string());
8122 }
8123
8124 if let Some(l) = limit {
8125 request_params.insert("size".to_string(), l.to_string());
8126 }
8127
8128 let is_portfolio = request_params
8129 .get("type")
8130 .map(|t| t == "PORTFOLIO")
8131 .unwrap_or(false);
8132
8133 let auth = self.get_auth()?;
8134 let signed_params = auth.sign_params(&request_params)?;
8135
8136 let query_string: Vec<String> = signed_params
8137 .iter()
8138 .map(|(k, v)| format!("{}={}", k, v))
8139 .collect();
8140
8141 let url = if is_portfolio {
8142 format!(
8143 "{}/margin/marginInterestHistory?{}",
8144 self.urls().papi,
8145 query_string.join("&")
8146 )
8147 } else {
8148 format!(
8149 "{}/margin/interestHistory?{}",
8150 self.urls().sapi,
8151 query_string.join("&")
8152 )
8153 };
8154
8155 let mut headers = reqwest::header::HeaderMap::new();
8156 auth.add_auth_headers_reqwest(&mut headers);
8157
8158 let response = self.base().http_client.get(&url, Some(headers)).await?;
8159
8160 parser::parse_borrow_interests(&response)
8161 }
8162
8163 /// Fetch margin borrow rate history
8164 ///
8165 /// # Arguments
8166 /// * `code` - Asset code (e.g., "USDT")
8167 /// * `since` - Start timestamp in milliseconds
8168 /// * `limit` - Number of records to return (default 10, max 100, but actual limit is 92)
8169 /// * `params` - Optional parameters
8170 /// - `endTime`: End timestamp
8171 /// - `vipLevel`: VIP level
8172 ///
8173 /// # Notes
8174 /// - Binance API has special limit restriction, cannot exceed 92
8175 /// - For more data, use multiple requests with pagination
8176 ///
8177 /// # API Endpoint
8178 /// GET /sapi/v1/margin/interestRateHistory
8179 ///
8180 /// # Examples
8181 /// ```no_run
8182 /// # use ccxt_exchanges::binance::Binance;
8183 /// # use ccxt_core::ExchangeConfig;
8184 /// # async fn example() -> ccxt_core::Result<()> {
8185 /// let binance = Binance::new(ExchangeConfig::default())?;
8186 /// let history = binance.fetch_borrow_rate_history(
8187 /// "USDT",
8188 /// None,
8189 /// Some(50),
8190 /// None
8191 /// ).await?;
8192 /// println!("Got {} records", history.len());
8193 /// # Ok(())
8194 /// # }
8195 /// ```
8196 pub async fn fetch_borrow_rate_history(
8197 &self,
8198 code: &str,
8199 since: Option<u64>,
8200 limit: Option<u64>,
8201 params: Option<HashMap<String, String>>,
8202 ) -> Result<Vec<BorrowRateHistory>> {
8203 self.check_required_credentials()?;
8204
8205 let mut request_params = params.unwrap_or_default();
8206 request_params.insert("asset".to_string(), code.to_string());
8207
8208 // Binance API limit: cannot exceed 92
8209 let actual_limit = limit.unwrap_or(10).min(92);
8210
8211 if let Some(st) = since {
8212 request_params.insert("startTime".to_string(), st.to_string());
8213
8214 // Binance requires limited time range per request
8215 if !request_params.contains_key("endTime") {
8216 // Each record represents 1 day, so calculate endTime = startTime + (limit * 1 day)
8217 let end_time = st + (actual_limit * 86400000); // 86400000 ms = 1 day
8218 request_params.insert("endTime".to_string(), end_time.to_string());
8219 }
8220 }
8221
8222 let auth = self.get_auth()?;
8223 let signed_params = auth.sign_params(&request_params)?;
8224
8225 let query_string: Vec<String> = signed_params
8226 .iter()
8227 .map(|(k, v)| format!("{}={}", k, v))
8228 .collect();
8229 let url = format!(
8230 "{}/margin/interestRateHistory?{}",
8231 self.urls().sapi,
8232 query_string.join("&")
8233 );
8234
8235 let mut headers = reqwest::header::HeaderMap::new();
8236 auth.add_auth_headers_reqwest(&mut headers);
8237
8238 let response = self.base().http_client.get(&url, Some(headers)).await?;
8239
8240 let records = response
8241 .as_array()
8242 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Expected array")))?;
8243
8244 let mut history = Vec::new();
8245 for record in records {
8246 let rate = parser::parse_borrow_rate_history(record, code)?;
8247 history.push(rate);
8248 }
8249
8250 Ok(history)
8251 }
8252
8253 // ========================================================================
8254 // Ledger Methods
8255 // ========================================================================
8256
8257 /// Fetch ledger history
8258 ///
8259 /// **Important Notes**:
8260 /// - Only supports futures wallets (option, USDT-M, COIN-M)
8261 /// - Spot wallet is not supported, returns NotSupported error
8262 /// - For portfolioMargin accounts, uses unified account API
8263 ///
8264 /// # API Endpoint Mapping
8265 ///
8266 /// | Market Type | Endpoint | Field Format |
8267 /// |-------------|----------|--------------|
8268 /// | Option | GET /eapi/v1/bill | id, asset, amount, type |
8269 /// | USDT-M (linear) | GET /fapi/v1/income | tranId, asset, income, incomeType |
8270 /// | USDT-M+Portfolio | GET /papi/v1/um/income | tranId, asset, income, incomeType |
8271 /// | COIN-M (inverse) | GET /dapi/v1/income | tranId, asset, income, incomeType |
8272 /// | COIN-M+Portfolio | GET /papi/v1/cm/income | tranId, asset, income, incomeType |
8273 ///
8274 /// # Arguments
8275 ///
8276 /// * `code` - Currency code (required for option market)
8277 /// * `since` - Start timestamp in milliseconds
8278 /// * `limit` - Maximum number of records to return
8279 /// * `params` - Additional parameters:
8280 /// - `until`: End timestamp in milliseconds
8281 /// - `portfolioMargin`: Whether to use portfolio margin account (unified account)
8282 /// - `type`: Market type ("spot", "margin", "future", "swap", "option")
8283 /// - `subType`: Sub-type ("linear", "inverse")
8284 ///
8285 /// # Returns
8286 ///
8287 /// Returns list of LedgerEntry, sorted by time in descending order
8288 ///
8289 /// # Errors
8290 ///
8291 /// - Returns NotSupported error if market type is spot
8292 /// - Returns ArgumentsRequired error if code parameter is not provided for option market
8293 ///
8294 /// # Examples
8295 ///
8296 /// ```rust,no_run
8297 /// use ccxt_exchanges::binance::Binance;
8298 /// use ccxt_core::ExchangeConfig;
8299 /// use std::collections::HashMap;
8300 ///
8301 /// #[tokio::main]
8302 /// async fn main() {
8303 /// let mut config = ExchangeConfig::default();
8304 /// config.api_key = Some("your-api-key".to_string());
8305 /// config.secret = Some("your-api-secret".to_string());
8306 ///
8307 /// let binance = Binance::new(config).unwrap();
8308 ///
8309 /// // USDT-M futures ledger
8310 /// let mut params = HashMap::new();
8311 /// params.insert("type".to_string(), "future".to_string());
8312 /// params.insert("subType".to_string(), "linear".to_string());
8313 /// let ledger = binance.fetch_ledger(
8314 /// Some("USDT".to_string()),
8315 /// None,
8316 /// Some(100),
8317 /// Some(params)
8318 /// ).await.unwrap();
8319 ///
8320 /// // Option ledger (code is required)
8321 /// let mut params = HashMap::new();
8322 /// params.insert("type".to_string(), "option".to_string());
8323 /// let ledger = binance.fetch_ledger(
8324 /// Some("USDT".to_string()),
8325 /// None,
8326 /// Some(100),
8327 /// Some(params)
8328 /// ).await.unwrap();
8329 /// }
8330 /// ```
8331 pub async fn fetch_ledger(
8332 &self,
8333 code: Option<String>,
8334 since: Option<i64>,
8335 limit: Option<i64>,
8336 params: Option<HashMap<String, String>>,
8337 ) -> Result<Vec<ccxt_core::types::LedgerEntry>> {
8338 self.check_required_credentials()?;
8339
8340 let params = params.unwrap_or_default();
8341
8342 let market_type = params.get("type").map(|s| s.as_str()).unwrap_or("future");
8343 let sub_type = params.get("subType").map(|s| s.as_str());
8344 let portfolio_margin = params
8345 .get("portfolioMargin")
8346 .and_then(|s| s.parse::<bool>().ok())
8347 .unwrap_or(false);
8348
8349 if market_type == "spot" {
8350 return Err(Error::not_implemented(
8351 "fetch_ledger() is not supported for spot market, only for futures/swap/option",
8352 ));
8353 }
8354
8355 let (endpoint_base, path) = if market_type == "option" {
8356 if code.is_none() {
8357 return Err(Error::invalid_request(
8358 "fetch_ledger() requires code for option market",
8359 ));
8360 }
8361 (self.urls().eapi.clone(), "/eapi/v1/bill")
8362 } else if portfolio_margin {
8363 if sub_type == Some("inverse") {
8364 (self.urls().papi.clone(), "/papi/v1/cm/income")
8365 } else {
8366 (self.urls().papi.clone(), "/papi/v1/um/income")
8367 }
8368 } else if sub_type == Some("inverse") {
8369 (self.urls().dapi.clone(), "/dapi/v1/income")
8370 } else {
8371 (self.urls().fapi.clone(), "/fapi/v1/income")
8372 };
8373
8374 let mut request_params = HashMap::new();
8375
8376 if let Some(currency) = &code {
8377 request_params.insert("asset".to_string(), currency.clone());
8378 }
8379
8380 if let Some(start_time) = since {
8381 request_params.insert("startTime".to_string(), start_time.to_string());
8382 }
8383 if let Some(end_time_str) = params.get("until") {
8384 request_params.insert("endTime".to_string(), end_time_str.clone());
8385 }
8386
8387 if let Some(limit_val) = limit {
8388 request_params.insert("limit".to_string(), limit_val.to_string());
8389 }
8390
8391 let auth = self.get_auth()?;
8392 let signed_params = auth.sign_params(&request_params)?;
8393
8394 let query_string: Vec<String> = signed_params
8395 .iter()
8396 .map(|(k, v)| format!("{}={}", k, v))
8397 .collect();
8398 let url = format!("{}{}?{}", endpoint_base, path, query_string.join("&"));
8399
8400 let mut headers = reqwest::header::HeaderMap::new();
8401 auth.add_auth_headers_reqwest(&mut headers);
8402
8403 let response = self.base().http_client.get(&url, Some(headers)).await?;
8404
8405 let records = response
8406 .as_array()
8407 .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Expected array")))?;
8408
8409 let mut ledger_entries = Vec::new();
8410 for record in records {
8411 let entry = parser::parse_ledger_entry(record)?;
8412 ledger_entries.push(entry);
8413 }
8414
8415 Ok(ledger_entries)
8416 }
8417 /// Fetch trades for a specific order
8418 ///
8419 /// # Arguments
8420 ///
8421 /// * `id` - Order ID
8422 /// * `symbol` - Trading pair symbol (required)
8423 /// * `since` - Optional start timestamp in milliseconds
8424 /// * `limit` - Optional maximum number of records
8425 ///
8426 /// # Returns
8427 ///
8428 /// Returns all trades for the specified order
8429 ///
8430 /// # Notes
8431 ///
8432 /// - `symbol` parameter is required
8433 /// - Only supports spot markets
8434 /// - Internally calls fetch_my_trades method with orderId filter
8435 /// - API endpoint: GET /api/v3/myTrades
8436 ///
8437 /// # Examples
8438 ///
8439 /// ```no_run
8440 /// # use ccxt_exchanges::binance::Binance;
8441 /// # use ccxt_core::ExchangeConfig;
8442 /// # async fn example() -> ccxt_core::Result<()> {
8443 /// let mut config = ExchangeConfig::default();
8444 /// config.api_key = Some("your-api-key".to_string());
8445 /// config.secret = Some("your-secret".to_string());
8446 /// let binance = Binance::new(config)?;
8447 ///
8448 /// let trades = binance.fetch_order_trades("123456789", "BTC/USDT", None, None).await?;
8449 /// println!("Order has {} trades", trades.len());
8450 /// # Ok(())
8451 /// # }
8452 /// ```
8453 pub async fn fetch_order_trades(
8454 &self,
8455 id: &str,
8456 symbol: &str,
8457 since: Option<u64>,
8458 limit: Option<u32>,
8459 ) -> Result<Vec<Trade>> {
8460 self.check_required_credentials()?;
8461
8462 let market = self.base().market(symbol).await?;
8463 if !market.is_spot() {
8464 return Err(Error::not_implemented(
8465 "fetch_order_trades() supports spot markets only",
8466 ));
8467 }
8468
8469 let mut params = HashMap::new();
8470 params.insert("symbol".to_string(), market.id.clone());
8471 params.insert("orderId".to_string(), id.to_string());
8472
8473 if let Some(s) = since {
8474 params.insert("startTime".to_string(), s.to_string());
8475 }
8476
8477 if let Some(l) = limit {
8478 params.insert("limit".to_string(), l.to_string());
8479 }
8480
8481 let timestamp = self.fetch_time_raw().await?;
8482 let auth = self.get_auth()?;
8483 let signed_params =
8484 auth.sign_with_timestamp(¶ms, timestamp, Some(self.options().recv_window))?;
8485
8486 let mut url = format!("{}/myTrades?", self.urls().private);
8487 for (key, value) in &signed_params {
8488 url.push_str(&format!("{}={}&", key, value));
8489 }
8490
8491 let mut headers = HeaderMap::new();
8492 auth.add_auth_headers_reqwest(&mut headers);
8493
8494 let data = self.base().http_client.get(&url, Some(headers)).await?;
8495
8496 let trades_array = data.as_array().ok_or_else(|| {
8497 Error::from(ParseError::invalid_format(
8498 "data",
8499 "Expected array of trades",
8500 ))
8501 })?;
8502
8503 let mut trades = Vec::new();
8504 for trade_data in trades_array {
8505 match parser::parse_trade(trade_data, Some(&market)) {
8506 Ok(trade) => trades.push(trade),
8507 Err(e) => {
8508 warn!(error = %e, "Failed to parse trade");
8509 }
8510 }
8511 }
8512
8513 Ok(trades)
8514 }
8515
8516 /// Fetch canceled orders
8517 ///
8518 /// # Arguments
8519 ///
8520 /// * `symbol` - Trading pair symbol (required)
8521 /// * `since` - Optional start timestamp in milliseconds
8522 /// * `limit` - Optional maximum number of records
8523 ///
8524 /// # Returns
8525 ///
8526 /// Returns list of canceled orders
8527 ///
8528 /// # Notes
8529 ///
8530 /// - `symbol` parameter is required
8531 /// - Calls fetch_orders to get all orders, then filters for canceled status
8532 /// - `limit` parameter is applied client-side (fetch all orders first, then take first N)
8533 /// - Implementation logic matches Go version
8534 ///
8535 /// # Examples
8536 ///
8537 /// ```no_run
8538 /// # use ccxt_exchanges::binance::Binance;
8539 /// # use ccxt_core::ExchangeConfig;
8540 /// # async fn example() -> ccxt_core::Result<()> {
8541 /// let mut config = ExchangeConfig::default();
8542 /// config.api_key = Some("your-api-key".to_string());
8543 /// config.secret = Some("your-secret".to_string());
8544 /// let binance = Binance::new(config)?;
8545 ///
8546 /// let canceled_orders = binance.fetch_canceled_orders("BTC/USDT", None, Some(10)).await?;
8547 /// println!("Found {} canceled orders", canceled_orders.len());
8548 /// # Ok(())
8549 /// # }
8550 /// ```
8551 pub async fn fetch_canceled_orders(
8552 &self,
8553 symbol: &str,
8554 since: Option<u64>,
8555 limit: Option<u32>,
8556 ) -> Result<Vec<Order>> {
8557 let all_orders = self.fetch_orders(Some(symbol), since, None).await?;
8558
8559 let mut canceled_orders: Vec<Order> = all_orders
8560 .into_iter()
8561 .filter(|order| order.status == OrderStatus::Canceled)
8562 .collect();
8563
8564 if let Some(since_time) = since {
8565 canceled_orders.retain(|order| {
8566 order
8567 .timestamp
8568 .map(|ts| ts >= since_time as i64)
8569 .unwrap_or(true)
8570 });
8571 }
8572
8573 if let Some(limit_val) = limit {
8574 canceled_orders.truncate(limit_val as usize);
8575 }
8576
8577 Ok(canceled_orders)
8578 }
8579
8580 /// Create market buy order by cost amount
8581 ///
8582 /// # Arguments
8583 ///
8584 /// * `symbol` - Trading pair symbol
8585 /// * `cost` - Amount in quote currency to spend (e.g., spend 100 USDT to buy BTC)
8586 /// * `params` - Optional additional parameters
8587 ///
8588 /// # Returns
8589 ///
8590 /// Returns created order information
8591 ///
8592 /// # Notes
8593 ///
8594 /// - Only supports spot markets
8595 /// - `cost` parameter is converted to Binance API's `quoteOrderQty` parameter
8596 /// - This is a convenience method to simplify market buy order creation
8597 /// - Internally calls create_order method
8598 ///
8599 /// # Examples
8600 ///
8601 /// ```no_run
8602 /// # use ccxt_exchanges::binance::Binance;
8603 /// # use ccxt_core::ExchangeConfig;
8604 /// # async fn example() -> ccxt_core::Result<()> {
8605 /// let mut config = ExchangeConfig::default();
8606 /// config.api_key = Some("your-api-key".to_string());
8607 /// config.secret = Some("your-secret".to_string());
8608 /// let binance = Binance::new(config)?;
8609 ///
8610 /// // Spend 100 USDT to buy BTC (market order)
8611 /// let order = binance.create_market_buy_order_with_cost("BTC/USDT", 100.0, None).await?;
8612 /// println!("Order created: {:?}", order);
8613 /// # Ok(())
8614 /// # }
8615 /// ```
8616 pub async fn create_market_buy_order_with_cost(
8617 &self,
8618 symbol: &str,
8619 cost: f64,
8620 params: Option<HashMap<String, String>>,
8621 ) -> Result<Order> {
8622 self.check_required_credentials()?;
8623
8624 let market = self.base().market(symbol).await?;
8625 if !market.is_spot() {
8626 return Err(Error::not_implemented(
8627 "create_market_buy_order_with_cost() supports spot orders only",
8628 ));
8629 }
8630
8631 let mut order_params = params.unwrap_or_default();
8632 order_params.insert("cost".to_string(), cost.to_string());
8633
8634 // The create_order method detects the cost parameter and converts it to quoteOrderQty
8635 self.create_order(
8636 symbol,
8637 OrderType::Market,
8638 OrderSide::Buy,
8639 cost,
8640 None,
8641 Some(order_params),
8642 )
8643 .await
8644 }
8645}
8646
8647#[cfg(test)]
8648mod tests {
8649 use super::*;
8650 use ccxt_core::ExchangeConfig;
8651
8652 #[tokio::test]
8653 #[ignore] // Requires actual API connection
8654 async fn test_fetch_markets() {
8655 let config = ExchangeConfig::default();
8656 let binance = Binance::new(config).unwrap();
8657
8658 let markets = binance.fetch_markets().await;
8659 assert!(markets.is_ok());
8660
8661 let markets = markets.unwrap();
8662 assert!(!markets.is_empty());
8663 }
8664
8665 #[tokio::test]
8666 #[ignore] // Requires actual API connection
8667 async fn test_fetch_ticker() {
8668 let config = ExchangeConfig::default();
8669 let binance = Binance::new(config).unwrap();
8670
8671 let _ = binance.fetch_markets().await;
8672
8673 let ticker = binance
8674 .fetch_ticker("BTC/USDT", ccxt_core::types::TickerParams::default())
8675 .await;
8676 assert!(ticker.is_ok());
8677
8678 let ticker = ticker.unwrap();
8679 assert_eq!(ticker.symbol, "BTC/USDT");
8680 assert!(ticker.last.is_some());
8681 }
8682
8683 #[tokio::test]
8684 #[ignore] // Requires actual API connection
8685 async fn test_fetch_order_book() {
8686 let config = ExchangeConfig::default();
8687 let binance = Binance::new(config).unwrap();
8688
8689 let _ = binance.fetch_markets().await;
8690
8691 let orderbook = binance.fetch_order_book("BTC/USDT", Some(10)).await;
8692 assert!(orderbook.is_ok());
8693
8694 let orderbook = orderbook.unwrap();
8695 assert_eq!(orderbook.symbol, "BTC/USDT");
8696 assert!(!orderbook.bids.is_empty());
8697 assert!(!orderbook.asks.is_empty());
8698 }
8699
8700 #[tokio::test]
8701 #[ignore] // Requires actual API connection
8702 async fn test_fetch_trades() {
8703 let config = ExchangeConfig::default();
8704 let binance = Binance::new(config).unwrap();
8705
8706 let _ = binance.fetch_markets().await;
8707
8708 let trades = binance.fetch_trades("BTC/USDT", Some(10)).await;
8709 assert!(trades.is_ok());
8710
8711 let trades = trades.unwrap();
8712 assert!(!trades.is_empty());
8713 assert!(trades.len() <= 10);
8714 }
8715}