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