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 ¶ms.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) = ¶ms {
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}