Skip to main content

binance_api_client/api/
market.rs

1//! Market data API endpoints.
2//!
3//! This module provides access to public market data endpoints that don't
4//! require authentication.
5
6use serde_json::Value;
7
8use crate::Result;
9use crate::client::Client;
10use crate::models::{
11    AggTrade, AveragePrice, BookTicker, ExchangeInfo, Kline, OrderBook, RollingWindowTicker,
12    RollingWindowTickerMini, ServerTime, Ticker24h, TickerPrice, Trade, TradingDayTicker,
13    TradingDayTickerMini,
14};
15use crate::types::{KlineInterval, SymbolStatus, TickerType};
16
17// API endpoints
18const API_V3_PING: &str = "/api/v3/ping";
19const API_V3_TIME: &str = "/api/v3/time";
20const API_V3_EXCHANGE_INFO: &str = "/api/v3/exchangeInfo";
21const API_V3_DEPTH: &str = "/api/v3/depth";
22const API_V3_TRADES: &str = "/api/v3/trades";
23const API_V3_HISTORICAL_TRADES: &str = "/api/v3/historicalTrades";
24const API_V3_AGG_TRADES: &str = "/api/v3/aggTrades";
25const API_V3_KLINES: &str = "/api/v3/klines";
26const API_V3_UI_KLINES: &str = "/api/v3/uiKlines";
27const API_V3_AVG_PRICE: &str = "/api/v3/avgPrice";
28const API_V3_TICKER_24HR: &str = "/api/v3/ticker/24hr";
29const API_V3_TICKER_TRADING_DAY: &str = "/api/v3/ticker/tradingDay";
30const API_V3_TICKER_PRICE: &str = "/api/v3/ticker/price";
31const API_V3_TICKER_BOOK_TICKER: &str = "/api/v3/ticker/bookTicker";
32const API_V3_TICKER: &str = "/api/v3/ticker";
33
34/// Market data API client.
35///
36/// Provides access to public market data endpoints.
37#[derive(Clone)]
38pub struct Market {
39    client: Client,
40}
41
42impl Market {
43    /// Create a new Market API client.
44    pub(crate) fn new(client: Client) -> Self {
45        Self { client }
46    }
47
48    /// Test connectivity to the API.
49    ///
50    /// # Example
51    ///
52    /// ```rust,ignore
53    /// let client = Binance::new_unauthenticated()?;
54    /// client.market().ping().await?;
55    /// ```
56    pub async fn ping(&self) -> Result<()> {
57        let _: Value = self.client.get(API_V3_PING, None).await?;
58        Ok(())
59    }
60
61    /// Get the current server time.
62    ///
63    /// # Example
64    ///
65    /// ```rust,ignore
66    /// let client = Binance::new_unauthenticated()?;
67    /// let time = client.market().server_time().await?;
68    /// println!("Server time: {}", time.server_time);
69    /// ```
70    pub async fn server_time(&self) -> Result<ServerTime> {
71        self.client.get(API_V3_TIME, None).await
72    }
73
74    /// Get exchange information (trading rules and symbol info).
75    ///
76    /// # Example
77    ///
78    /// ```rust,ignore
79    /// let client = Binance::new_unauthenticated()?;
80    /// let info = client.market().exchange_info().await?;
81    /// for symbol in info.symbols {
82    ///     println!("{}: {}", symbol.symbol, symbol.status);
83    /// }
84    /// ```
85    pub async fn exchange_info(&self) -> Result<ExchangeInfo> {
86        self.client.get(API_V3_EXCHANGE_INFO, None).await
87    }
88
89    /// Get exchange information for specific symbols.
90    ///
91    /// # Arguments
92    ///
93    /// * `symbols` - List of symbols to get info for
94    ///
95    /// # Example
96    ///
97    /// ```rust,ignore
98    /// let client = Binance::new_unauthenticated()?;
99    /// let info = client.market().exchange_info_for_symbols(&["BTCUSDT", "ETHUSDT"]).await?;
100    /// ```
101    pub async fn exchange_info_for_symbols(&self, symbols: &[&str]) -> Result<ExchangeInfo> {
102        let symbols_json = serde_json::to_string(symbols).unwrap_or_default();
103        let query = format!("symbols={}", urlencoding::encode(&symbols_json));
104        self.client.get(API_V3_EXCHANGE_INFO, Some(&query)).await
105    }
106
107    /// Get order book depth.
108    ///
109    /// # Arguments
110    ///
111    /// * `symbol` - Trading pair symbol (e.g., "BTCUSDT")
112    /// * `limit` - Number of entries to return. Valid limits: 5, 10, 20, 50, 100, 500, 1000, 5000.
113    ///   Default is 100; max is 5000.
114    ///
115    /// # Example
116    ///
117    /// ```rust,ignore
118    /// let client = Binance::new_unauthenticated()?;
119    /// let depth = client.market().depth("BTCUSDT", Some(10)).await?;
120    /// for bid in depth.bids {
121    ///     println!("Bid: {} @ {}", bid.quantity, bid.price);
122    /// }
123    /// ```
124    pub async fn depth(&self, symbol: &str, limit: Option<u16>) -> Result<OrderBook> {
125        let mut query = format!("symbol={}", symbol);
126        if let Some(l) = limit {
127            query.push_str(&format!("&limit={}", l));
128        }
129        self.client.get(API_V3_DEPTH, Some(&query)).await
130    }
131
132    /// Get recent trades.
133    ///
134    /// # Arguments
135    ///
136    /// * `symbol` - Trading pair symbol
137    /// * `limit` - Number of trades to return. Default 500; max 1000.
138    ///
139    /// # Example
140    ///
141    /// ```rust,ignore
142    /// let client = Binance::new_unauthenticated()?;
143    /// let trades = client.market().trades("BTCUSDT", Some(10)).await?;
144    /// ```
145    pub async fn trades(&self, symbol: &str, limit: Option<u16>) -> Result<Vec<Trade>> {
146        let mut query = format!("symbol={}", symbol);
147        if let Some(l) = limit {
148            query.push_str(&format!("&limit={}", l));
149        }
150        self.client.get(API_V3_TRADES, Some(&query)).await
151    }
152
153    /// Get older/historical trades.
154    ///
155    /// This endpoint requires an API key but not a signature.
156    ///
157    /// # Arguments
158    ///
159    /// * `symbol` - Trading pair symbol
160    /// * `from_id` - Trade ID to fetch from
161    /// * `limit` - Number of trades to return. Default 500; max 1000.
162    ///
163    /// # Example
164    ///
165    /// ```rust,ignore
166    /// let client = Binance::new("api_key", "secret_key")?;
167    /// let trades = client.market().historical_trades("BTCUSDT", Some(12345), Some(100)).await?;
168    /// ```
169    pub async fn historical_trades(
170        &self,
171        symbol: &str,
172        from_id: Option<u64>,
173        limit: Option<u16>,
174    ) -> Result<Vec<Trade>> {
175        let mut query = format!("symbol={}", symbol);
176        if let Some(id) = from_id {
177            query.push_str(&format!("&fromId={}", id));
178        }
179        if let Some(l) = limit {
180            query.push_str(&format!("&limit={}", l));
181        }
182        // This endpoint requires API key but not signature
183        self.client
184            .get_with_api_key(API_V3_HISTORICAL_TRADES, Some(&query))
185            .await
186    }
187
188    /// Get compressed/aggregate trades.
189    ///
190    /// Trades that fill at the same time, from the same order, with the same
191    /// price will have the aggregate quantity added.
192    ///
193    /// # Arguments
194    ///
195    /// * `symbol` - Trading pair symbol
196    /// * `from_id` - Aggregate trade ID to get from
197    /// * `start_time` - Start time in milliseconds
198    /// * `end_time` - End time in milliseconds
199    /// * `limit` - Default 500; max 1000
200    ///
201    /// # Example
202    ///
203    /// ```rust,ignore
204    /// let client = Binance::new_unauthenticated()?;
205    /// let trades = client.market().agg_trades("BTCUSDT", None, None, None, Some(10)).await?;
206    /// ```
207    pub async fn agg_trades(
208        &self,
209        symbol: &str,
210        from_id: Option<u64>,
211        start_time: Option<u64>,
212        end_time: Option<u64>,
213        limit: Option<u16>,
214    ) -> Result<Vec<AggTrade>> {
215        let mut query = format!("symbol={}", symbol);
216        if let Some(id) = from_id {
217            query.push_str(&format!("&fromId={}", id));
218        }
219        if let Some(start) = start_time {
220            query.push_str(&format!("&startTime={}", start));
221        }
222        if let Some(end) = end_time {
223            query.push_str(&format!("&endTime={}", end));
224        }
225        if let Some(l) = limit {
226            query.push_str(&format!("&limit={}", l));
227        }
228        self.client.get(API_V3_AGG_TRADES, Some(&query)).await
229    }
230
231    /// Get kline/candlestick data.
232    ///
233    /// # Arguments
234    ///
235    /// * `symbol` - Trading pair symbol
236    /// * `interval` - Kline interval
237    /// * `start_time` - Start time in milliseconds
238    /// * `end_time` - End time in milliseconds
239    /// * `limit` - Default 500; max 1000
240    ///
241    /// # Example
242    ///
243    /// ```rust,ignore
244    /// use binance_api_client::KlineInterval;
245    ///
246    /// let client = Binance::new_unauthenticated()?;
247    /// let klines = client.market().klines("BTCUSDT", KlineInterval::Hours1, None, None, Some(10)).await?;
248    /// for kline in klines {
249    ///     println!("Open: {}, Close: {}", kline.open, kline.close);
250    /// }
251    /// ```
252    pub async fn klines(
253        &self,
254        symbol: &str,
255        interval: KlineInterval,
256        start_time: Option<u64>,
257        end_time: Option<u64>,
258        limit: Option<u16>,
259    ) -> Result<Vec<Kline>> {
260        let mut query = format!("symbol={}&interval={}", symbol, interval);
261        if let Some(start) = start_time {
262            query.push_str(&format!("&startTime={}", start));
263        }
264        if let Some(end) = end_time {
265            query.push_str(&format!("&endTime={}", end));
266        }
267        if let Some(l) = limit {
268            query.push_str(&format!("&limit={}", l));
269        }
270
271        // Klines come as arrays, need to parse manually
272        let raw: Vec<Vec<Value>> = self.client.get(API_V3_KLINES, Some(&query)).await?;
273
274        Ok(parse_klines(raw))
275    }
276
277    /// Get UI optimized kline/candlestick data.
278    ///
279    /// This endpoint mirrors the `/api/v3/klines` response format.
280    ///
281    /// # Arguments
282    ///
283    /// * `symbol` - Trading pair symbol
284    /// * `interval` - Kline interval
285    /// * `start_time` - Start time in milliseconds
286    /// * `end_time` - End time in milliseconds
287    /// * `limit` - Default 500; max 1000
288    ///
289    /// # Example
290    ///
291    /// ```rust,ignore
292    /// use binance_api_client::KlineInterval;
293    ///
294    /// let client = Binance::new_unauthenticated()?;
295    /// let klines = client
296    ///     .market()
297    ///     .ui_klines("BTCUSDT", KlineInterval::Hours1, None, None, Some(10))
298    ///     .await?;
299    /// ```
300    pub async fn ui_klines(
301        &self,
302        symbol: &str,
303        interval: KlineInterval,
304        start_time: Option<u64>,
305        end_time: Option<u64>,
306        limit: Option<u16>,
307    ) -> Result<Vec<Kline>> {
308        let mut query = format!("symbol={}&interval={}", symbol, interval);
309        if let Some(start) = start_time {
310            query.push_str(&format!("&startTime={}", start));
311        }
312        if let Some(end) = end_time {
313            query.push_str(&format!("&endTime={}", end));
314        }
315        if let Some(l) = limit {
316            query.push_str(&format!("&limit={}", l));
317        }
318
319        let raw: Vec<Vec<Value>> = self.client.get(API_V3_UI_KLINES, Some(&query)).await?;
320
321        Ok(parse_klines(raw))
322    }
323
324    /// Get current average price for a symbol.
325    ///
326    /// # Arguments
327    ///
328    /// * `symbol` - Trading pair symbol
329    ///
330    /// # Example
331    ///
332    /// ```rust,ignore
333    /// let client = Binance::new_unauthenticated()?;
334    /// let avg = client.market().avg_price("BTCUSDT").await?;
335    /// println!("Average price over {} mins: {}", avg.mins, avg.price);
336    /// ```
337    pub async fn avg_price(&self, symbol: &str) -> Result<AveragePrice> {
338        let query = format!("symbol={}", symbol);
339        self.client.get(API_V3_AVG_PRICE, Some(&query)).await
340    }
341
342    /// Get 24hr ticker price change statistics.
343    ///
344    /// # Arguments
345    ///
346    /// * `symbol` - Trading pair symbol
347    ///
348    /// # Example
349    ///
350    /// ```rust,ignore
351    /// let client = Binance::new_unauthenticated()?;
352    /// let ticker = client.market().ticker_24h("BTCUSDT").await?;
353    /// println!("Price change: {}%", ticker.price_change_percent);
354    /// ```
355    pub async fn ticker_24h(&self, symbol: &str) -> Result<Ticker24h> {
356        let query = format!("symbol={}", symbol);
357        self.client.get(API_V3_TICKER_24HR, Some(&query)).await
358    }
359
360    /// Get 24hr ticker price change statistics for all symbols.
361    ///
362    /// # Example
363    ///
364    /// ```rust,ignore
365    /// let client = Binance::new_unauthenticated()?;
366    /// let tickers = client.market().ticker_24h_all().await?;
367    /// ```
368    pub async fn ticker_24h_all(&self) -> Result<Vec<Ticker24h>> {
369        self.client.get(API_V3_TICKER_24HR, None).await
370    }
371
372    /// Get trading day ticker statistics (FULL).
373    ///
374    /// # Arguments
375    ///
376    /// * `symbol` - Trading pair symbol
377    /// * `time_zone` - Optional timezone (e.g., "0" or "-1:00")
378    /// * `symbol_status` - Optional symbol trading status filter
379    pub async fn trading_day_ticker(
380        &self,
381        symbol: &str,
382        time_zone: Option<&str>,
383        symbol_status: Option<SymbolStatus>,
384    ) -> Result<TradingDayTicker> {
385        let mut params: Vec<(&str, String)> = vec![("symbol", symbol.to_string())];
386
387        if let Some(tz) = time_zone {
388            params.push(("timeZone", tz.to_string()));
389        }
390        if let Some(status) = symbol_status {
391            params.push(("symbolStatus", status.to_string()));
392        }
393
394        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
395        self.client
396            .get_with_params(API_V3_TICKER_TRADING_DAY, &params_ref)
397            .await
398    }
399
400    /// Get trading day ticker statistics (MINI).
401    pub async fn trading_day_ticker_mini(
402        &self,
403        symbol: &str,
404        time_zone: Option<&str>,
405        symbol_status: Option<SymbolStatus>,
406    ) -> Result<TradingDayTickerMini> {
407        let mut params: Vec<(&str, String)> = vec![("symbol", symbol.to_string())];
408
409        params.push(("type", TickerType::Mini.to_string()));
410
411        if let Some(tz) = time_zone {
412            params.push(("timeZone", tz.to_string()));
413        }
414        if let Some(status) = symbol_status {
415            params.push(("symbolStatus", status.to_string()));
416        }
417
418        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
419        self.client
420            .get_with_params(API_V3_TICKER_TRADING_DAY, &params_ref)
421            .await
422    }
423
424    /// Get trading day ticker statistics (FULL) for multiple symbols.
425    pub async fn trading_day_tickers(
426        &self,
427        symbols: &[&str],
428        time_zone: Option<&str>,
429        symbol_status: Option<SymbolStatus>,
430    ) -> Result<Vec<TradingDayTicker>> {
431        let symbols_json = serde_json::to_string(symbols).unwrap_or_default();
432        let mut params: Vec<(&str, String)> =
433            vec![("symbols", urlencoding::encode(&symbols_json).into_owned())];
434
435        if let Some(tz) = time_zone {
436            params.push(("timeZone", tz.to_string()));
437        }
438        if let Some(status) = symbol_status {
439            params.push(("symbolStatus", status.to_string()));
440        }
441
442        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
443        self.client
444            .get_with_params(API_V3_TICKER_TRADING_DAY, &params_ref)
445            .await
446    }
447
448    /// Get trading day ticker statistics (MINI) for multiple symbols.
449    pub async fn trading_day_tickers_mini(
450        &self,
451        symbols: &[&str],
452        time_zone: Option<&str>,
453        symbol_status: Option<SymbolStatus>,
454    ) -> Result<Vec<TradingDayTickerMini>> {
455        let symbols_json = serde_json::to_string(symbols).unwrap_or_default();
456        let mut params: Vec<(&str, String)> =
457            vec![("symbols", urlencoding::encode(&symbols_json).into_owned())];
458
459        params.push(("type", TickerType::Mini.to_string()));
460
461        if let Some(tz) = time_zone {
462            params.push(("timeZone", tz.to_string()));
463        }
464        if let Some(status) = symbol_status {
465            params.push(("symbolStatus", status.to_string()));
466        }
467
468        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
469        self.client
470            .get_with_params(API_V3_TICKER_TRADING_DAY, &params_ref)
471            .await
472    }
473
474    /// Get rolling window ticker statistics (FULL).
475    ///
476    /// # Arguments
477    ///
478    /// * `symbol` - Trading pair symbol
479    /// * `window_size` - Optional window size (e.g., "1d", "15m")
480    /// * `symbol_status` - Optional symbol trading status filter
481    pub async fn rolling_window_ticker(
482        &self,
483        symbol: &str,
484        window_size: Option<&str>,
485        symbol_status: Option<SymbolStatus>,
486    ) -> Result<RollingWindowTicker> {
487        let mut params: Vec<(&str, String)> = vec![("symbol", symbol.to_string())];
488
489        if let Some(window) = window_size {
490            params.push(("windowSize", window.to_string()));
491        }
492        if let Some(status) = symbol_status {
493            params.push(("symbolStatus", status.to_string()));
494        }
495
496        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
497        self.client
498            .get_with_params(API_V3_TICKER, &params_ref)
499            .await
500    }
501
502    /// Get rolling window ticker statistics (MINI).
503    pub async fn rolling_window_ticker_mini(
504        &self,
505        symbol: &str,
506        window_size: Option<&str>,
507        symbol_status: Option<SymbolStatus>,
508    ) -> Result<RollingWindowTickerMini> {
509        let mut params: Vec<(&str, String)> = vec![("symbol", symbol.to_string())];
510
511        params.push(("type", TickerType::Mini.to_string()));
512
513        if let Some(window) = window_size {
514            params.push(("windowSize", window.to_string()));
515        }
516        if let Some(status) = symbol_status {
517            params.push(("symbolStatus", status.to_string()));
518        }
519
520        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
521        self.client
522            .get_with_params(API_V3_TICKER, &params_ref)
523            .await
524    }
525
526    /// Get rolling window ticker statistics (FULL) for multiple symbols.
527    pub async fn rolling_window_tickers(
528        &self,
529        symbols: &[&str],
530        window_size: Option<&str>,
531        symbol_status: Option<SymbolStatus>,
532    ) -> Result<Vec<RollingWindowTicker>> {
533        let symbols_json = serde_json::to_string(symbols).unwrap_or_default();
534        let mut params: Vec<(&str, String)> =
535            vec![("symbols", urlencoding::encode(&symbols_json).into_owned())];
536
537        if let Some(window) = window_size {
538            params.push(("windowSize", window.to_string()));
539        }
540        if let Some(status) = symbol_status {
541            params.push(("symbolStatus", status.to_string()));
542        }
543
544        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
545        self.client
546            .get_with_params(API_V3_TICKER, &params_ref)
547            .await
548    }
549
550    /// Get rolling window ticker statistics (MINI) for multiple symbols.
551    pub async fn rolling_window_tickers_mini(
552        &self,
553        symbols: &[&str],
554        window_size: Option<&str>,
555        symbol_status: Option<SymbolStatus>,
556    ) -> Result<Vec<RollingWindowTickerMini>> {
557        let symbols_json = serde_json::to_string(symbols).unwrap_or_default();
558        let mut params: Vec<(&str, String)> =
559            vec![("symbols", urlencoding::encode(&symbols_json).into_owned())];
560
561        params.push(("type", TickerType::Mini.to_string()));
562
563        if let Some(window) = window_size {
564            params.push(("windowSize", window.to_string()));
565        }
566        if let Some(status) = symbol_status {
567            params.push(("symbolStatus", status.to_string()));
568        }
569
570        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
571        self.client
572            .get_with_params(API_V3_TICKER, &params_ref)
573            .await
574    }
575
576    /// Get latest price for a symbol.
577    ///
578    /// # Arguments
579    ///
580    /// * `symbol` - Trading pair symbol
581    ///
582    /// # Example
583    ///
584    /// ```rust,ignore
585    /// let client = Binance::new_unauthenticated()?;
586    /// let price = client.market().price("BTCUSDT").await?;
587    /// println!("BTC/USDT: {}", price.price);
588    /// ```
589    pub async fn price(&self, symbol: &str) -> Result<TickerPrice> {
590        let query = format!("symbol={}", symbol);
591        self.client.get(API_V3_TICKER_PRICE, Some(&query)).await
592    }
593
594    /// Get latest prices for all symbols.
595    ///
596    /// # Example
597    ///
598    /// ```rust,ignore
599    /// let client = Binance::new_unauthenticated()?;
600    /// let prices = client.market().prices().await?;
601    /// for price in prices {
602    ///     println!("{}: {}", price.symbol, price.price);
603    /// }
604    /// ```
605    pub async fn prices(&self) -> Result<Vec<TickerPrice>> {
606        self.client.get(API_V3_TICKER_PRICE, None).await
607    }
608
609    /// Get latest prices for specific symbols.
610    ///
611    /// # Arguments
612    ///
613    /// * `symbols` - List of symbols
614    ///
615    /// # Example
616    ///
617    /// ```rust,ignore
618    /// let client = Binance::new_unauthenticated()?;
619    /// let prices = client.market().prices_for(&["BTCUSDT", "ETHUSDT"]).await?;
620    /// ```
621    pub async fn prices_for(&self, symbols: &[&str]) -> Result<Vec<TickerPrice>> {
622        let symbols_json = serde_json::to_string(symbols).unwrap_or_default();
623        let query = format!("symbols={}", urlencoding::encode(&symbols_json));
624        self.client.get(API_V3_TICKER_PRICE, Some(&query)).await
625    }
626
627    /// Get best price/qty on the order book for a symbol.
628    ///
629    /// # Arguments
630    ///
631    /// * `symbol` - Trading pair symbol
632    ///
633    /// # Example
634    ///
635    /// ```rust,ignore
636    /// let client = Binance::new_unauthenticated()?;
637    /// let ticker = client.market().book_ticker("BTCUSDT").await?;
638    /// println!("Best bid: {} @ {}", ticker.bid_qty, ticker.bid_price);
639    /// println!("Best ask: {} @ {}", ticker.ask_qty, ticker.ask_price);
640    /// ```
641    pub async fn book_ticker(&self, symbol: &str) -> Result<BookTicker> {
642        let query = format!("symbol={}", symbol);
643        self.client
644            .get(API_V3_TICKER_BOOK_TICKER, Some(&query))
645            .await
646    }
647
648    /// Get best price/qty on the order book for all symbols.
649    ///
650    /// # Example
651    ///
652    /// ```rust,ignore
653    /// let client = Binance::new_unauthenticated()?;
654    /// let tickers = client.market().book_tickers().await?;
655    /// ```
656    pub async fn book_tickers(&self) -> Result<Vec<BookTicker>> {
657        self.client.get(API_V3_TICKER_BOOK_TICKER, None).await
658    }
659
660    /// Get best price/qty on the order book for specific symbols.
661    ///
662    /// # Arguments
663    ///
664    /// * `symbols` - List of symbols
665    ///
666    /// # Example
667    ///
668    /// ```rust,ignore
669    /// let client = Binance::new_unauthenticated()?;
670    /// let tickers = client.market().book_tickers_for(&["BTCUSDT", "ETHUSDT"]).await?;
671    /// ```
672    pub async fn book_tickers_for(&self, symbols: &[&str]) -> Result<Vec<BookTicker>> {
673        let symbols_json = serde_json::to_string(symbols).unwrap_or_default();
674        let query = format!("symbols={}", urlencoding::encode(&symbols_json));
675        self.client
676            .get(API_V3_TICKER_BOOK_TICKER, Some(&query))
677            .await
678    }
679}
680
681/// Parse a serde_json::Value as f64, handling both strings and numbers.
682fn parse_value_as_f64(value: &Value) -> f64 {
683    match value {
684        Value::String(s) => s.parse().unwrap_or_default(),
685        Value::Number(n) => n.as_f64().unwrap_or_default(),
686        _ => 0.0,
687    }
688}
689
690fn parse_klines(raw: Vec<Vec<Value>>) -> Vec<Kline> {
691    raw.into_iter()
692        .map(|row| Kline {
693            open_time: row[0].as_i64().unwrap_or_default(),
694            open: parse_value_as_f64(&row[1]),
695            high: parse_value_as_f64(&row[2]),
696            low: parse_value_as_f64(&row[3]),
697            close: parse_value_as_f64(&row[4]),
698            volume: parse_value_as_f64(&row[5]),
699            close_time: row[6].as_i64().unwrap_or_default(),
700            quote_asset_volume: parse_value_as_f64(&row[7]),
701            number_of_trades: row[8].as_i64().unwrap_or_default(),
702            taker_buy_base_asset_volume: parse_value_as_f64(&row[9]),
703            taker_buy_quote_asset_volume: parse_value_as_f64(&row[10]),
704        })
705        .collect()
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711
712    #[test]
713    fn test_parse_value_as_f64_string() {
714        let value = Value::String("123.456".to_string());
715        assert_eq!(parse_value_as_f64(&value), 123.456);
716    }
717
718    #[test]
719    fn test_parse_value_as_f64_number() {
720        let value = serde_json::json!(123.456);
721        assert_eq!(parse_value_as_f64(&value), 123.456);
722    }
723
724    #[test]
725    fn test_parse_value_as_f64_invalid() {
726        let value = Value::Null;
727        assert_eq!(parse_value_as_f64(&value), 0.0);
728    }
729}