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