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