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