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