Skip to main content

ccxt_exchanges/binance/rest/
market_data.rs

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