Skip to main content

ccxt_exchanges/binance/rest/
market_data.rs

1//! Binance public market data operations.
2//!
3//! This module contains all public market data methods that don't require authentication.
4//! These include ticker data, order books, trades, OHLCV data, and market statistics.
5
6use super::super::{Binance, BinanceEndpointRouter, constants::endpoints, parser};
7use ccxt_core::{
8    Error, ParseError, Result,
9    time::TimestampUtils,
10    types::{
11        AggTrade, BidAsk, EndpointType, IntoTickerParams, LastPrice, MarkPrice, OhlcvRequest,
12        ServerTime, Stats24hr, Ticker, Trade, TradingLimits,
13    },
14};
15use reqwest::header::HeaderMap;
16use serde::{Deserialize, Serialize};
17use std::sync::Arc;
18use tracing::warn;
19use url::Url;
20
21/// System status structure.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SystemStatus {
24    /// Status: "ok" or "maintenance"
25    pub status: String,
26    /// Last updated timestamp
27    pub updated: Option<i64>,
28    /// Estimated time of arrival (recovery)
29    pub eta: Option<i64>,
30    /// Status URL
31    pub url: Option<String>,
32    /// Raw info from exchange
33    pub info: serde_json::Value,
34}
35
36impl Binance {
37    /// Fetch server timestamp for internal use.
38    ///
39    /// # Returns
40    ///
41    /// Returns the server timestamp in milliseconds.
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if the request fails or the response is malformed.
46    pub(crate) async fn fetch_time_raw(&self) -> Result<i64> {
47        let url = format!("{}{}", self.get_rest_url_public(), endpoints::TIME);
48        let response = self.public_get(&url, None).await?;
49
50        response["serverTime"]
51            .as_i64()
52            .ok_or_else(|| ParseError::missing_field("serverTime").into())
53    }
54
55    /// Fetch exchange system status.
56    ///
57    /// # Returns
58    ///
59    /// Returns formatted exchange status information with the following structure:
60    /// ```json
61    /// {
62    ///     "status": "ok" | "maintenance",
63    ///     "updated": null,
64    ///     "eta": null,
65    ///     "url": null,
66    ///     "info": { ... }
67    /// }
68    /// ```
69    pub async fn fetch_status(&self) -> Result<SystemStatus> {
70        // System status is specific to Spot/Margin (SAPI)
71        let url = format!("{}{}", self.sapi_endpoint(), endpoints::SYSTEM_STATUS);
72        let response = self.public_get(&url, None).await?;
73
74        // Response format: { "status": 0, "msg": "normal" }
75        // Status codes: 0 = normal, 1 = system maintenance
76        let status_raw = response
77            .get("status")
78            .and_then(serde_json::Value::as_i64)
79            .ok_or_else(|| {
80                Error::from(ParseError::invalid_format(
81                    "status",
82                    "status field missing or not an integer",
83                ))
84            })?;
85
86        let status = match status_raw {
87            0 => "ok",
88            1 => "maintenance",
89            _ => "unknown",
90        };
91
92        Ok(SystemStatus {
93            status: status.to_string(),
94            updated: None,
95            eta: None,
96            url: None,
97            info: response,
98        })
99    }
100
101    /// Fetch all trading markets.
102    ///
103    /// # Returns
104    ///
105    /// Returns a HashMap of [`Market`] structures containing market information.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if the API request fails or response parsing fails.
110    ///
111    /// # Example
112    ///
113    /// ```no_run
114    /// # use ccxt_exchanges::binance::Binance;
115    /// # use ccxt_core::ExchangeConfig;
116    /// # async fn example() -> ccxt_core::Result<()> {
117    /// let binance = Binance::new(ExchangeConfig::default())?;
118    /// let markets = binance.fetch_markets().await?;
119    /// println!("Found {} markets", markets.len());
120    /// # Ok(())
121    /// # }
122    /// ```
123    pub async fn fetch_markets(
124        &self,
125    ) -> Result<Arc<std::collections::HashMap<String, Arc<ccxt_core::types::Market>>>> {
126        let url = format!("{}{}", self.get_rest_url_public(), endpoints::EXCHANGE_INFO);
127        let data = self.public_get(&url, None).await?;
128
129        let symbols = data["symbols"]
130            .as_array()
131            .ok_or_else(|| Error::from(ParseError::missing_field("symbols")))?;
132
133        let mut markets = Vec::new();
134        for symbol in symbols {
135            match parser::parse_market(symbol) {
136                Ok(market) => markets.push(market),
137                Err(e) => {
138                    warn!(error = %e, "Failed to parse market");
139                }
140            }
141        }
142
143        self.base().set_markets(markets, None).await
144    }
145
146    /// Load and cache market data.
147    ///
148    /// Standard CCXT method for loading all market data from the exchange.
149    /// If markets are already loaded and `reload` is false, returns cached data.
150    ///
151    /// # Arguments
152    ///
153    /// * `reload` - Whether to force reload market data from the API.
154    ///
155    /// # Returns
156    ///
157    /// Returns a `HashMap` containing all market data, keyed by symbol (e.g., "BTC/USDT").
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if the API request fails or response parsing fails.
162    ///
163    /// # Example
164    ///
165    /// ```no_run
166    /// # use ccxt_exchanges::binance::Binance;
167    /// # use ccxt_core::ExchangeConfig;
168    /// # async fn example() -> ccxt_core::error::Result<()> {
169    /// let binance = Binance::new(ExchangeConfig::default())?;
170    ///
171    /// // Load markets for the first time
172    /// let markets = binance.load_markets(false).await?;
173    /// println!("Loaded {} markets", markets.len());
174    ///
175    /// // Subsequent calls use cache (no API request)
176    /// let markets = binance.load_markets(false).await?;
177    ///
178    /// // Force reload
179    /// let markets = binance.load_markets(true).await?;
180    /// # Ok(())
181    /// # }
182    /// ```
183    pub async fn load_markets(
184        &self,
185        reload: bool,
186    ) -> Result<Arc<std::collections::HashMap<String, Arc<ccxt_core::types::Market>>>> {
187        // Acquire the loading lock to serialize concurrent load_markets calls
188        // This prevents multiple tasks from making duplicate API calls
189        let _loading_guard = self.base().market_loading_lock.lock().await;
190
191        // Check cache status while holding the lock
192        {
193            let cache = self.base().market_cache.read().await;
194            if cache.is_loaded() && !reload {
195                tracing::debug!(
196                    "Returning cached markets for Binance ({} markets)",
197                    cache.market_count()
198                );
199                return Ok(cache.markets());
200            }
201        }
202
203        tracing::info!("Loading markets for Binance (reload: {})", reload);
204        let _markets = self.fetch_markets().await?;
205
206        let cache = self.base().market_cache.read().await;
207        Ok(cache.markets())
208    }
209
210    /// Fetch ticker for a single trading pair.
211    ///
212    /// # Arguments
213    ///
214    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
215    /// * `params` - Optional parameters to configure the ticker request.
216    ///
217    /// # Returns
218    ///
219    /// Returns [`Ticker`] data for the specified symbol.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if the market is not found or the API request fails.
224    pub async fn fetch_ticker(
225        &self,
226        symbol: &str,
227        params: impl IntoTickerParams,
228    ) -> Result<Ticker> {
229        let market = self.base().market(symbol).await?;
230
231        let params = params.into_ticker_params();
232        let rolling = params.rolling.unwrap_or(false);
233
234        let endpoint = if rolling {
235            endpoints::TICKER_ROLLING
236        } else {
237            endpoints::TICKER_24HR
238        };
239
240        let full_url = format!(
241            "{}{}",
242            self.rest_endpoint(&market, EndpointType::Public),
243            endpoint
244        );
245        let mut url =
246            Url::parse(&full_url).map_err(|e| Error::exchange("Invalid URL", e.to_string()))?;
247
248        {
249            let mut query = url.query_pairs_mut();
250            query.append_pair("symbol", &market.id);
251
252            if let Some(window) = params.window_size {
253                query.append_pair("windowSize", &window.to_string());
254            }
255
256            for (key, value) in &params.extras {
257                if key != "rolling" && key != "windowSize" {
258                    let value_str = match value {
259                        serde_json::Value::String(s) => s.clone(),
260                        _ => value.to_string(),
261                    };
262                    query.append_pair(key, &value_str);
263                }
264            }
265        }
266
267        let data = self.public_get(url.as_str(), None).await?;
268
269        parser::parse_ticker(&data, Some(&market))
270    }
271
272    /// Fetch tickers for multiple trading pairs.
273    ///
274    /// # Arguments
275    ///
276    /// * `symbols` - Optional list of trading pair symbols; fetches all if `None`.
277    ///
278    /// # Returns
279    ///
280    /// Returns a vector of [`Ticker`] structures.
281    ///
282    /// # Errors
283    ///
284    /// Returns an error if markets are not loaded or the API request fails.
285    pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
286        // Acquire read lock once and clone the necessary data to avoid lock contention in the loop
287        let cache = self.base().market_cache.read().await;
288        if !cache.is_loaded() {
289            return Err(Error::exchange(
290                "-1",
291                "Markets not loaded. Call load_markets() first.",
292            ));
293        }
294        // Get an iterator over markets by ID for efficient lookup
295        let markets_snapshot: std::collections::HashMap<String, Arc<ccxt_core::types::Market>> =
296            cache
297                .iter_markets()
298                .map(|(_, m)| (m.id.clone(), m))
299                .collect();
300        drop(cache);
301
302        let url = if let Some(ref syms) = symbols {
303            // Convert CCXT symbols to Binance market IDs for server-side filtering
304            let binance_ids: Vec<String> = syms
305                .iter()
306                .filter_map(|s| {
307                    markets_snapshot
308                        .iter()
309                        .find(|(_, m)| m.symbol == *s)
310                        .map(|(id, _)| format!("\"{}\"", id))
311                })
312                .collect();
313            if binance_ids.is_empty() {
314                format!("{}{}", self.get_rest_url_public(), endpoints::TICKER_24HR)
315            } else {
316                let symbols_param = format!("[{}]", binance_ids.join(","));
317                let encoded = urlencoding::encode(&symbols_param);
318                format!(
319                    "{}{}?symbols={}",
320                    self.get_rest_url_public(),
321                    endpoints::TICKER_24HR,
322                    encoded
323                )
324            }
325        } else {
326            format!("{}{}", self.get_rest_url_public(), endpoints::TICKER_24HR)
327        };
328        let data = self.public_get(&url, None).await?;
329
330        let tickers_array = data.as_array().ok_or_else(|| {
331            Error::from(ParseError::invalid_format(
332                "response",
333                "Expected array of tickers",
334            ))
335        })?;
336
337        let mut tickers = Vec::new();
338        for ticker_data in tickers_array {
339            if let Some(binance_symbol) = ticker_data["symbol"].as_str() {
340                // Use the pre-cloned map instead of acquiring a lock on each iteration
341                if let Some(market) = markets_snapshot.get(binance_symbol) {
342                    match parser::parse_ticker(ticker_data, Some(market)) {
343                        Ok(ticker) => {
344                            if let Some(ref syms) = symbols {
345                                if syms.contains(&ticker.symbol) {
346                                    tickers.push(ticker);
347                                }
348                            } else {
349                                tickers.push(ticker);
350                            }
351                        }
352                        Err(e) => {
353                            warn!(
354                                error = %e,
355                                symbol = %binance_symbol,
356                                "Failed to parse ticker"
357                            );
358                        }
359                    }
360                }
361            }
362        }
363
364        Ok(tickers)
365    }
366
367    /// Fetch order book for a trading pair.
368    ///
369    /// # Arguments
370    ///
371    /// * `symbol` - Trading pair symbol.
372    /// * `limit` - Optional depth limit (valid values: 5, 10, 20, 50, 100, 500, 1000, 5000).
373    ///
374    /// # Returns
375    ///
376    /// Returns [`OrderBook`] data containing bids and asks.
377    ///
378    /// # Errors
379    ///
380    /// Returns an error if the market is not found or the API request fails.
381    pub async fn fetch_order_book(
382        &self,
383        symbol: &str,
384        limit: Option<u32>,
385    ) -> Result<ccxt_core::types::OrderBook> {
386        // Validate depth limit if provided
387        const VALID_DEPTH_LIMITS: &[u32] = &[5, 10, 20, 50, 100, 500, 1000, 5000];
388        if let Some(l) = limit {
389            if !VALID_DEPTH_LIMITS.contains(&l) {
390                return Err(Error::invalid_request(format!(
391                    "Invalid depth limit: {}. Valid values: {:?}",
392                    l, VALID_DEPTH_LIMITS
393                )));
394            }
395        }
396
397        let market = self.base().market(symbol).await?;
398
399        let full_url = format!(
400            "{}{}",
401            self.rest_endpoint(&market, EndpointType::Public),
402            endpoints::DEPTH
403        );
404        let mut url =
405            Url::parse(&full_url).map_err(|e| Error::exchange("Invalid URL", e.to_string()))?;
406
407        {
408            let mut query = url.query_pairs_mut();
409            query.append_pair("symbol", &market.id);
410            if let Some(l) = limit {
411                query.append_pair("limit", &l.to_string());
412            }
413        }
414
415        let data = self.public_get(url.as_str(), None).await?;
416
417        parser::parse_orderbook(&data, market.symbol.clone())
418    }
419
420    /// Fetch recent public trades.
421    ///
422    /// # Arguments
423    ///
424    /// * `symbol` - Trading pair symbol.
425    /// * `limit` - Optional limit on number of trades (maximum: 1000).
426    ///
427    /// # Returns
428    ///
429    /// Returns a vector of [`Trade`] structures, sorted by timestamp in descending order.
430    ///
431    /// # Errors
432    ///
433    /// Returns an error if the market is not found or the API request fails.
434    pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
435        let market = self.base().market(symbol).await?;
436
437        let full_url = format!(
438            "{}{}",
439            self.rest_endpoint(&market, EndpointType::Public),
440            endpoints::TRADES
441        );
442        let mut url =
443            Url::parse(&full_url).map_err(|e| Error::exchange("Invalid URL", e.to_string()))?;
444        {
445            let mut query = url.query_pairs_mut();
446            query.append_pair("symbol", &market.id);
447            if let Some(l) = limit {
448                query.append_pair("limit", &l.to_string());
449            }
450        }
451        let url = url.to_string();
452
453        let data = self.public_get(&url, None).await?;
454
455        let trades_array = data.as_array().ok_or_else(|| {
456            Error::from(ParseError::invalid_format(
457                "data",
458                "Expected array of trades",
459            ))
460        })?;
461
462        let mut trades = Vec::new();
463        for trade_data in trades_array {
464            match parser::parse_trade(trade_data, Some(&market)) {
465                Ok(trade) => trades.push(trade),
466                Err(e) => {
467                    warn!(error = %e, "Failed to parse trade");
468                }
469            }
470        }
471
472        // CCXT convention: trades should be sorted by timestamp descending (newest first)
473        trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
474
475        Ok(trades)
476    }
477
478    /// Fetch recent public trades (alias for `fetch_trades`).
479    ///
480    /// # Arguments
481    ///
482    /// * `symbol` - Trading pair symbol.
483    /// * `limit` - Optional limit on number of trades (default: 500, maximum: 1000).
484    ///
485    /// # Returns
486    ///
487    /// Returns a vector of [`Trade`] structures for recent public trades.
488    ///
489    /// # Errors
490    ///
491    /// Returns an error if the market is not found or the API request fails.
492    pub async fn fetch_recent_trades(
493        &self,
494        symbol: &str,
495        limit: Option<u32>,
496    ) -> Result<Vec<Trade>> {
497        self.fetch_trades(symbol, limit).await
498    }
499
500    /// Fetch aggregated trade data.
501    ///
502    /// # Arguments
503    ///
504    /// * `symbol` - Trading pair symbol.
505    /// * `since` - Optional start timestamp in milliseconds.
506    /// * `limit` - Optional limit on number of records (default: 500, maximum: 1000).
507    /// * `params` - Additional parameters that may include:
508    ///   - `fromId`: Start from specific aggTradeId.
509    ///   - `endTime`: End timestamp in milliseconds.
510    ///
511    /// # Returns
512    ///
513    /// Returns a vector of aggregated trade records.
514    ///
515    /// # Errors
516    ///
517    /// Returns an error if the market is not found or the API request fails.
518    pub async fn fetch_agg_trades(
519        &self,
520        symbol: &str,
521        since: Option<i64>,
522        limit: Option<u32>,
523        params: Option<std::collections::HashMap<String, String>>,
524    ) -> Result<Vec<AggTrade>> {
525        let market = self.base().market(symbol).await?;
526
527        let full_url = format!(
528            "{}{}",
529            self.rest_endpoint(&market, EndpointType::Public),
530            endpoints::AGG_TRADES
531        );
532        let mut url_parsed =
533            Url::parse(&full_url).map_err(|e| Error::exchange("Invalid URL", e.to_string()))?;
534        {
535            let mut query = url_parsed.query_pairs_mut();
536            query.append_pair("symbol", &market.id);
537            if let Some(s) = since {
538                query.append_pair("startTime", &s.to_string());
539            }
540            if let Some(l) = limit {
541                query.append_pair("limit", &l.to_string());
542            }
543            if let Some(ref p) = params {
544                if let Some(from_id) = p.get("fromId") {
545                    query.append_pair("fromId", from_id);
546                }
547                if let Some(end_time) = p.get("endTime") {
548                    query.append_pair("endTime", end_time);
549                }
550            }
551        }
552        let url = url_parsed.to_string();
553
554        let data = self.public_get(&url, None).await?;
555
556        let agg_trades_array = data.as_array().ok_or_else(|| {
557            Error::from(ParseError::invalid_format(
558                "data",
559                "Expected array of agg trades",
560            ))
561        })?;
562
563        let mut agg_trades = Vec::new();
564        for agg_trade_data in agg_trades_array {
565            match parser::parse_agg_trade(agg_trade_data, Some(market.symbol.clone())) {
566                Ok(agg_trade) => agg_trades.push(agg_trade),
567                Err(e) => {
568                    warn!(error = %e, "Failed to parse agg trade");
569                }
570            }
571        }
572
573        Ok(agg_trades)
574    }
575
576    /// Fetch historical trade data (requires API key but not signature).
577    ///
578    /// Note: Binance API uses `fromId` parameter instead of timestamp.
579    ///
580    /// # Arguments
581    ///
582    /// * `symbol` - Trading pair symbol.
583    /// * `_since` - Optional start timestamp (unused, Binance uses `fromId` instead).
584    /// * `limit` - Optional limit on number of records (default: 500, maximum: 1000).
585    /// * `params` - Additional parameters that may include:
586    ///   - `fromId`: Start from specific tradeId.
587    ///
588    /// # Returns
589    ///
590    /// Returns a vector of historical [`Trade`] records.
591    ///
592    /// # Errors
593    ///
594    /// Returns an error if authentication fails or the API request fails.
595    pub async fn fetch_historical_trades(
596        &self,
597        symbol: &str,
598        _since: Option<i64>,
599        limit: Option<u32>,
600        params: Option<std::collections::HashMap<String, String>>,
601    ) -> Result<Vec<Trade>> {
602        let market = self.base().market(symbol).await?;
603
604        self.check_api_key()?;
605
606        let full_url = format!(
607            "{}{}",
608            self.rest_endpoint(&market, EndpointType::Public),
609            endpoints::HISTORICAL_TRADES
610        );
611        let mut url_parsed =
612            Url::parse(&full_url).map_err(|e| Error::exchange("Invalid URL", e.to_string()))?;
613        {
614            let mut query = url_parsed.query_pairs_mut();
615            query.append_pair("symbol", &market.id);
616            // Binance historicalTrades endpoint uses fromId instead of timestamp
617            if let Some(p) = &params {
618                if let Some(from_id) = p.get("fromId") {
619                    query.append_pair("fromId", from_id);
620                }
621            }
622            if let Some(l) = limit {
623                query.append_pair("limit", &l.to_string());
624            }
625        }
626        let url = url_parsed.to_string();
627
628        let mut headers = HeaderMap::new();
629        let auth = self.get_auth()?;
630        auth.add_auth_headers_reqwest(&mut headers);
631
632        let data = self.public_get(&url, Some(headers)).await?;
633
634        let trades_array = data.as_array().ok_or_else(|| {
635            Error::from(ParseError::invalid_format(
636                "data",
637                "Expected array of trades",
638            ))
639        })?;
640
641        let mut trades = Vec::new();
642        for trade_data in trades_array {
643            match parser::parse_trade(trade_data, Some(&market)) {
644                Ok(trade) => trades.push(trade),
645                Err(e) => {
646                    warn!(error = %e, "Failed to parse historical trade");
647                }
648            }
649        }
650
651        Ok(trades)
652    }
653
654    /// Fetch 24-hour trading statistics.
655    ///
656    /// # Arguments
657    ///
658    /// * `symbol` - Optional trading pair symbol. If `None`, returns statistics for all pairs.
659    ///
660    /// # Returns
661    ///
662    /// Returns a vector of [`Stats24hr`] structures. Single symbol returns one item, all symbols return multiple items.
663    ///
664    /// # Errors
665    ///
666    /// Returns an error if the market is not found or the API request fails.
667    pub async fn fetch_24hr_stats(&self, symbol: Option<&str>) -> Result<Vec<Stats24hr>> {
668        let url = if let Some(sym) = symbol {
669            let market = self.base().market(sym).await?;
670            format!(
671                "{}{}?symbol={}",
672                self.rest_endpoint(&market, EndpointType::Public),
673                endpoints::TICKER_24HR,
674                market.id
675            )
676        } else {
677            format!("{}{}", self.get_rest_url_public(), endpoints::TICKER_24HR)
678        };
679
680        let data = self.public_get(&url, None).await?;
681
682        // Single symbol returns object, all symbols return array
683        let stats_vec = if data.is_array() {
684            let stats_array = data.as_array().ok_or_else(|| {
685                Error::from(ParseError::invalid_format(
686                    "data",
687                    "Expected array of 24hr stats",
688                ))
689            })?;
690
691            let mut stats = Vec::new();
692            for stats_data in stats_array {
693                match parser::parse_stats_24hr(stats_data) {
694                    Ok(stat) => stats.push(stat),
695                    Err(e) => {
696                        warn!(error = %e, "Failed to parse 24hr stats");
697                    }
698                }
699            }
700            stats
701        } else {
702            vec![parser::parse_stats_24hr(&data)?]
703        };
704
705        Ok(stats_vec)
706    }
707
708    /// Fetch trading limits information for a symbol.
709    ///
710    /// # Arguments
711    ///
712    /// * `symbol` - Trading pair symbol.
713    ///
714    /// # Returns
715    ///
716    /// Returns [`TradingLimits`] containing minimum/maximum order constraints.
717    ///
718    /// # Errors
719    ///
720    /// Returns an error if the market is not found or the API request fails.
721    pub async fn fetch_trading_limits(&self, symbol: &str) -> Result<TradingLimits> {
722        let market = self.base().market(symbol).await?;
723
724        let url = format!(
725            "{}{}?symbol={}",
726            self.rest_endpoint(&market, EndpointType::Public),
727            endpoints::EXCHANGE_INFO,
728            market.id
729        );
730        let data = self.public_get(&url, None).await?;
731
732        let symbols_array = data["symbols"].as_array().ok_or_else(|| {
733            Error::from(ParseError::invalid_format("data", "Expected symbols array"))
734        })?;
735
736        if symbols_array.is_empty() {
737            return Err(Error::from(ParseError::invalid_format(
738                "data",
739                format!("No symbol info found for {}", symbol),
740            )));
741        }
742
743        let symbol_data = &symbols_array[0];
744
745        parser::parse_trading_limits(symbol_data, market.symbol.clone())
746    }
747
748    /// Parse timeframe string into seconds.
749    ///
750    /// Converts a timeframe string like "1m", "5m", "1h", "1d" into the equivalent number of seconds.
751    ///
752    /// # Arguments
753    ///
754    /// * `timeframe` - Timeframe string such as "1m", "5m", "1h", "1d"
755    ///
756    /// # Returns
757    ///
758    /// Returns the time interval in seconds.
759    ///
760    /// # Errors
761    ///
762    /// Returns an error if the timeframe is empty or has an invalid format.
763    fn parse_timeframe(timeframe: &str) -> Result<i64> {
764        let unit_map = [
765            ("s", 1),
766            ("m", 60),
767            ("h", 3600),
768            ("d", 86400),
769            ("w", 604800),
770            ("M", 2592000),
771            ("y", 31536000),
772        ];
773
774        if timeframe.is_empty() {
775            return Err(Error::invalid_request("timeframe cannot be empty"));
776        }
777
778        let mut num_str = String::new();
779        let mut unit_str = String::new();
780
781        for ch in timeframe.chars() {
782            if ch.is_ascii_digit() {
783                num_str.push(ch);
784            } else {
785                unit_str.push(ch);
786            }
787        }
788
789        let amount: i64 = if num_str.is_empty() {
790            1
791        } else {
792            num_str.parse().map_err(|_| {
793                Error::invalid_request(format!("Invalid timeframe format: {}", timeframe))
794            })?
795        };
796
797        let unit_seconds = unit_map
798            .iter()
799            .find(|(unit, _)| unit == &unit_str.as_str())
800            .map(|(_, seconds)| *seconds)
801            .ok_or_else(|| {
802                Error::invalid_request(format!("Unsupported timeframe unit: {}", unit_str))
803            })?;
804
805        Ok(amount * unit_seconds)
806    }
807
808    /// Get OHLCV API endpoint based on market type and price type.
809    ///
810    /// # Arguments
811    /// * `market` - Market information
812    /// * `price` - Price type: None (default) | "mark" | "index" | "premiumIndex"
813    ///
814    /// # Returns
815    /// Returns tuple (base_url, endpoint, use_pair)
816    fn get_ohlcv_endpoint(
817        &self,
818        market: &std::sync::Arc<ccxt_core::types::Market>,
819        price: Option<&str>,
820    ) -> Result<(String, String, bool)> {
821        use ccxt_core::types::MarketType;
822
823        if let Some(p) = price {
824            if !["mark", "index", "premiumIndex"].contains(&p) {
825                return Err(Error::invalid_request(format!(
826                    "Unsupported price type: {}. Supported types: mark, index, premiumIndex",
827                    p
828                )));
829            }
830        }
831
832        match market.market_type {
833            MarketType::Spot => {
834                if let Some(p) = price {
835                    return Err(Error::invalid_request(format!(
836                        "Spot market does not support '{}' price type",
837                        p
838                    )));
839                }
840                Ok((
841                    self.urls().public.clone(),
842                    endpoints::KLINES.to_string(),
843                    false,
844                ))
845            }
846
847            MarketType::Swap | MarketType::Futures => {
848                let is_linear = market.linear.unwrap_or(false);
849                let is_inverse = market.inverse.unwrap_or(false);
850
851                if is_linear {
852                    let (endpoint, use_pair) = match price {
853                        None => (endpoints::KLINES.to_string(), false),
854                        Some("mark") => ("/markPriceKlines".to_string(), false),
855                        Some("index") => ("/indexPriceKlines".to_string(), true),
856                        Some("premiumIndex") => ("/premiumIndexKlines".to_string(), false),
857                        _ => unreachable!(),
858                    };
859                    Ok((self.urls().fapi_public.clone(), endpoint, use_pair))
860                } else if is_inverse {
861                    let (endpoint, use_pair) = match price {
862                        None => (endpoints::KLINES.to_string(), false),
863                        Some("mark") => ("/markPriceKlines".to_string(), false),
864                        Some("index") => ("/indexPriceKlines".to_string(), true),
865                        Some("premiumIndex") => ("/premiumIndexKlines".to_string(), false),
866                        _ => unreachable!(),
867                    };
868                    Ok((self.urls().dapi_public.clone(), endpoint, use_pair))
869                } else {
870                    Err(Error::invalid_request(
871                        "Cannot determine futures contract type (linear or inverse)",
872                    ))
873                }
874            }
875
876            MarketType::Option => {
877                if let Some(p) = price {
878                    return Err(Error::invalid_request(format!(
879                        "Option market does not support '{}' price type",
880                        p
881                    )));
882                }
883                Ok((
884                    self.urls().eapi_public.clone(),
885                    endpoints::KLINES.to_string(),
886                    false,
887                ))
888            }
889        }
890    }
891
892    /// Fetch OHLCV (candlestick) data using the builder pattern.
893    ///
894    /// This is the preferred method for fetching OHLCV data. It accepts an [`OhlcvRequest`]
895    /// built using the builder pattern, which provides validation and a more ergonomic API.
896    ///
897    /// # Arguments
898    ///
899    /// * `request` - OHLCV request built via [`OhlcvRequest::builder()`]
900    ///
901    /// # Returns
902    ///
903    /// Returns OHLCV data array: [timestamp, open, high, low, close, volume]
904    ///
905    /// # Errors
906    ///
907    /// Returns an error if the market is not found or the API request fails.
908    ///
909    /// # Example
910    ///
911    /// ```no_run
912    /// use ccxt_exchanges::binance::Binance;
913    /// use ccxt_core::{ExchangeConfig, types::OhlcvRequest};
914    ///
915    /// # async fn example() -> ccxt_core::Result<()> {
916    /// let binance = Binance::new(ExchangeConfig::default())?;
917    ///
918    /// // Fetch OHLCV data using the builder
919    /// let request = OhlcvRequest::builder()
920    ///     .symbol("BTC/USDT")
921    ///     .timeframe("1h")
922    ///     .limit(100)
923    ///     .build()?;
924    ///
925    /// let ohlcv = binance.fetch_ohlcv_v2(request).await?;
926    /// println!("Fetched {} candles", ohlcv.len());
927    /// # Ok(())
928    /// # }
929    /// ```
930    ///
931    /// _Requirements: 2.3, 2.6_
932    pub async fn fetch_ohlcv_v2(
933        &self,
934        request: OhlcvRequest,
935    ) -> Result<Vec<ccxt_core::types::OHLCV>> {
936        self.load_markets(false).await?;
937
938        let market = self.base().market(&request.symbol).await?;
939
940        let default_limit = 500u32;
941        let max_limit = 1500u32;
942
943        let adjusted_limit =
944            if request.since.is_some() && request.until.is_some() && request.limit.is_none() {
945                max_limit
946            } else if let Some(lim) = request.limit {
947                lim.min(max_limit)
948            } else {
949                default_limit
950            };
951
952        // For v2, we don't support price type parameter (use fetch_ohlcv for that)
953        let (base_url, endpoint, use_pair) = self.get_ohlcv_endpoint(&market, None)?;
954
955        let symbol_param = if use_pair {
956            market.symbol.replace('/', "")
957        } else {
958            market.id.clone()
959        };
960
961        let mut url = format!(
962            "{}{}?symbol={}&interval={}&limit={}",
963            base_url, endpoint, symbol_param, request.timeframe, adjusted_limit
964        );
965
966        if let Some(start_time) = request.since {
967            use std::fmt::Write;
968            let _ = write!(url, "&startTime={}", start_time);
969
970            // Calculate endTime for inverse markets
971            if market.inverse.unwrap_or(false) && start_time > 0 && request.until.is_none() {
972                let duration = Self::parse_timeframe(&request.timeframe)?;
973                let calculated_end_time =
974                    start_time + (adjusted_limit as i64 * duration * 1000) - 1;
975                let now = TimestampUtils::now_ms();
976                let end_time = calculated_end_time.min(now);
977                let _ = write!(url, "&endTime={}", end_time);
978            }
979        }
980
981        if let Some(end_time) = request.until {
982            use std::fmt::Write;
983            let _ = write!(url, "&endTime={}", end_time);
984        }
985
986        let data = self.public_get(&url, None).await?;
987
988        parser::parse_ohlcvs(&data)
989    }
990
991    /// Fetch OHLCV (candlestick) data (deprecated).
992    ///
993    /// # Deprecated
994    ///
995    /// This method is deprecated. Use [`fetch_ohlcv_v2`](Self::fetch_ohlcv_v2) with
996    /// [`OhlcvRequest::builder()`] instead for a more ergonomic API.
997    ///
998    /// # Arguments
999    ///
1000    /// * `symbol` - Trading pair symbol, e.g., "BTC/USDT"
1001    /// * `timeframe` - Time period, e.g., "1m", "5m", "1h", "1d"
1002    /// * `since` - Start timestamp in milliseconds
1003    /// * `limit` - Maximum number of candlesticks to return
1004    /// * `params` - Optional parameters
1005    ///   * `price` - Price type: "mark" | "index" | "premiumIndex" (futures only)
1006    ///   * `until` - End timestamp in milliseconds
1007    ///
1008    /// # Returns
1009    ///
1010    /// Returns OHLCV data array: [timestamp, open, high, low, close, volume]
1011    #[deprecated(
1012        since = "0.2.0",
1013        note = "Use fetch_ohlcv_v2 with OhlcvRequest::builder() instead"
1014    )]
1015    pub async fn fetch_ohlcv(
1016        &self,
1017        symbol: &str,
1018        timeframe: &str,
1019        since: Option<i64>,
1020        limit: Option<u32>,
1021        params: Option<std::collections::HashMap<String, serde_json::Value>>,
1022    ) -> Result<Vec<ccxt_core::types::OHLCV>> {
1023        self.load_markets(false).await?;
1024
1025        let price = params
1026            .as_ref()
1027            .and_then(|p| p.get("price"))
1028            .and_then(serde_json::Value::as_str)
1029            .map(ToString::to_string);
1030
1031        let until = params
1032            .as_ref()
1033            .and_then(|p| p.get("until"))
1034            .and_then(serde_json::Value::as_i64);
1035
1036        let market = self.base().market(symbol).await?;
1037
1038        let default_limit = 500u32;
1039        let max_limit = 1500u32;
1040
1041        let adjusted_limit = if since.is_some() && until.is_some() && limit.is_none() {
1042            max_limit
1043        } else if let Some(lim) = limit {
1044            lim.min(max_limit)
1045        } else {
1046            default_limit
1047        };
1048
1049        let (base_url, endpoint, use_pair) = self.get_ohlcv_endpoint(&market, price.as_deref())?;
1050
1051        let symbol_param = if use_pair {
1052            market.symbol.replace('/', "")
1053        } else {
1054            market.id.clone()
1055        };
1056
1057        let mut url = format!(
1058            "{}{}?symbol={}&interval={}&limit={}",
1059            base_url, endpoint, symbol_param, timeframe, adjusted_limit
1060        );
1061
1062        if let Some(start_time) = since {
1063            use std::fmt::Write;
1064            let _ = write!(url, "&startTime={}", start_time);
1065
1066            // Calculate endTime for inverse markets
1067            if market.inverse.unwrap_or(false) && start_time > 0 && until.is_none() {
1068                let duration = Self::parse_timeframe(timeframe)?;
1069                let calculated_end_time =
1070                    start_time + (adjusted_limit as i64 * duration * 1000) - 1;
1071                let now = TimestampUtils::now_ms();
1072                let end_time = calculated_end_time.min(now);
1073                let _ = write!(url, "&endTime={}", end_time);
1074            }
1075        }
1076
1077        if let Some(end_time) = until {
1078            use std::fmt::Write;
1079            let _ = write!(url, "&endTime={}", end_time);
1080        }
1081
1082        let data = self.public_get(&url, None).await?;
1083
1084        parser::parse_ohlcvs(&data)
1085    }
1086
1087    /// Fetch server time.
1088    ///
1089    /// Retrieves the current server timestamp from the exchange.
1090    ///
1091    /// # Returns
1092    ///
1093    /// Returns [`ServerTime`] containing the server timestamp and formatted datetime.
1094    ///
1095    /// # Errors
1096    ///
1097    /// Returns an error if the API request fails.
1098    ///
1099    /// # Example
1100    ///
1101    /// ```no_run
1102    /// # use ccxt_exchanges::binance::Binance;
1103    /// # use ccxt_core::ExchangeConfig;
1104    /// # async fn example() -> ccxt_core::Result<()> {
1105    /// let binance = Binance::new(ExchangeConfig::default())?;
1106    /// let server_time = binance.fetch_time().await?;
1107    /// println!("Server time: {} ({})", server_time.server_time, server_time.datetime);
1108    /// # Ok(())
1109    /// # }
1110    /// ```
1111    pub async fn fetch_time(&self) -> Result<ServerTime> {
1112        let timestamp = self.fetch_time_raw().await?;
1113        Ok(ServerTime::new(timestamp))
1114    }
1115
1116    /// Fetch best bid/ask prices.
1117    ///
1118    /// Retrieves the best bid and ask prices for one or all trading pairs.
1119    ///
1120    /// # Arguments
1121    ///
1122    /// * `symbol` - Optional trading pair symbol; if omitted, returns all symbols
1123    ///
1124    /// # Returns
1125    ///
1126    /// Returns a vector of [`BidAsk`] structures containing bid/ask prices.
1127    ///
1128    /// # API Endpoint
1129    ///
1130    /// * GET `/api/v3/ticker/bookTicker`
1131    /// * Weight: 1 for single symbol, 2 for all symbols
1132    /// * Requires signature: No
1133    ///
1134    /// # Errors
1135    ///
1136    /// Returns an error if the API request fails.
1137    ///
1138    /// # Example
1139    ///
1140    /// ```no_run
1141    /// # use ccxt_exchanges::binance::Binance;
1142    /// # use ccxt_core::ExchangeConfig;
1143    /// # async fn example() -> ccxt_core::Result<()> {
1144    /// let binance = Binance::new(ExchangeConfig::default())?;
1145    ///
1146    /// // Fetch bid/ask for single symbol
1147    /// let bid_ask = binance.fetch_bids_asks(Some("BTC/USDT")).await?;
1148    /// println!("BTC/USDT bid: {}, ask: {}", bid_ask[0].bid_price, bid_ask[0].ask_price);
1149    ///
1150    /// // Fetch bid/ask for all symbols
1151    /// let all_bid_asks = binance.fetch_bids_asks(None).await?;
1152    /// println!("Total symbols: {}", all_bid_asks.len());
1153    /// # Ok(())
1154    /// # }
1155    /// ```
1156    pub async fn fetch_bids_asks(&self, symbol: Option<&str>) -> Result<Vec<BidAsk>> {
1157        self.load_markets(false).await?;
1158
1159        let url = if let Some(sym) = symbol {
1160            let market = self.base().market(sym).await?;
1161            format!(
1162                "{}/ticker/bookTicker?symbol={}",
1163                self.rest_endpoint(&market, EndpointType::Public),
1164                market.id
1165            )
1166        } else {
1167            format!("{}/ticker/bookTicker", self.get_rest_url_public())
1168        };
1169
1170        let data = self.public_get(&url, None).await?;
1171
1172        parser::parse_bids_asks(&data)
1173    }
1174
1175    /// Fetch latest prices.
1176    ///
1177    /// Retrieves the most recent price for one or all trading pairs.
1178    ///
1179    /// # Arguments
1180    ///
1181    /// * `symbol` - Optional trading pair symbol; if omitted, returns all symbols
1182    ///
1183    /// # Returns
1184    ///
1185    /// Returns a vector of [`LastPrice`] structures containing the latest prices.
1186    ///
1187    /// # API Endpoint
1188    ///
1189    /// * GET `/api/v3/ticker/price`
1190    /// * Weight: 1 for single symbol, 2 for all symbols
1191    /// * Requires signature: No
1192    ///
1193    /// # Errors
1194    ///
1195    /// Returns an error if the API request fails.
1196    ///
1197    /// # Example
1198    ///
1199    /// ```no_run
1200    /// # use ccxt_exchanges::binance::Binance;
1201    /// # use ccxt_core::ExchangeConfig;
1202    /// # async fn example() -> ccxt_core::Result<()> {
1203    /// let binance = Binance::new(ExchangeConfig::default())?;
1204    ///
1205    /// // Fetch latest price for single symbol
1206    /// let price = binance.fetch_last_prices(Some("BTC/USDT")).await?;
1207    /// println!("BTC/USDT last price: {}", price[0].price);
1208    ///
1209    /// // Fetch latest prices for all symbols
1210    /// let all_prices = binance.fetch_last_prices(None).await?;
1211    /// println!("Total symbols: {}", all_prices.len());
1212    /// # Ok(())
1213    /// # }
1214    /// ```
1215    pub async fn fetch_last_prices(&self, symbol: Option<&str>) -> Result<Vec<LastPrice>> {
1216        self.load_markets(false).await?;
1217
1218        let url = if let Some(sym) = symbol {
1219            let market = self.base().market(sym).await?;
1220            format!(
1221                "{}/ticker/price?symbol={}",
1222                self.rest_endpoint(&market, EndpointType::Public),
1223                market.id
1224            )
1225        } else {
1226            format!("{}/ticker/price", self.get_rest_url_public())
1227        };
1228
1229        let data = self.public_get(&url, None).await?;
1230
1231        parser::parse_last_prices(&data)
1232    }
1233
1234    /// Fetch futures mark prices.
1235    ///
1236    /// Retrieves mark prices for futures contracts, used for calculating unrealized PnL.
1237    /// Includes funding rates and next funding time.
1238    ///
1239    /// # Arguments
1240    ///
1241    /// * `symbol` - Optional trading pair symbol; if omitted, returns all futures pairs
1242    ///
1243    /// # Returns
1244    ///
1245    /// Returns a vector of [`MarkPrice`] structures containing mark prices and funding rates.
1246    ///
1247    /// # API Endpoint
1248    ///
1249    /// * GET `/fapi/v1/premiumIndex`
1250    /// * Weight: 1 for single symbol, 10 for all symbols
1251    /// * Requires signature: No
1252    ///
1253    /// # Note
1254    ///
1255    /// This API only applies to futures markets (USDT-margined perpetual contracts).
1256    ///
1257    /// # Errors
1258    ///
1259    /// Returns an error if the API request fails.
1260    ///
1261    /// # Example
1262    ///
1263    /// ```no_run
1264    /// # use ccxt_exchanges::binance::Binance;
1265    /// # use ccxt_core::ExchangeConfig;
1266    /// # async fn example() -> ccxt_core::Result<()> {
1267    /// let binance = Binance::new(ExchangeConfig::default())?;
1268    ///
1269    /// // Fetch mark price for single futures symbol
1270    /// let mark_price = binance.fetch_mark_price(Some("BTC/USDT:USDT")).await?;
1271    /// println!("BTC/USDT mark price: {}", mark_price[0].mark_price);
1272    /// println!("Funding rate: {:?}", mark_price[0].last_funding_rate);
1273    ///
1274    /// // Fetch mark prices for all futures symbols
1275    /// let all_mark_prices = binance.fetch_mark_price(None).await?;
1276    /// println!("Total futures symbols: {}", all_mark_prices.len());
1277    /// # Ok(())
1278    /// # }
1279    /// ```
1280    pub async fn fetch_mark_price(&self, symbol: Option<&str>) -> Result<Vec<MarkPrice>> {
1281        self.load_markets(false).await?;
1282
1283        let url = if let Some(sym) = symbol {
1284            let market = self.base().market(sym).await?;
1285            // Use rest_endpoint to correctly select FAPI/DAPI
1286            format!(
1287                "{}/premiumIndex?symbol={}",
1288                self.rest_endpoint(&market, EndpointType::Public),
1289                market.id
1290            )
1291        } else {
1292            // Default to the exchange's configured futures type (FAPI/DAPI)
1293            format!(
1294                "{}/premiumIndex",
1295                self.default_rest_endpoint(EndpointType::Public)
1296            )
1297        };
1298
1299        let data = self.public_get(&url, None).await?;
1300
1301        parser::parse_mark_prices(&data)
1302    }
1303}