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