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