ccxt_core/traits/market_data.rs
1//! MarketData trait definition.
2//!
3//! The `MarketData` trait provides methods for fetching public market data
4//! that doesn't require authentication. This includes markets, tickers,
5//! order books, trades, and OHLCV candlestick data.
6//!
7//! # Timestamp Format
8//!
9//! All timestamp parameters and return values in this trait use the standardized format:
10//! - **Type**: `i64`
11//! - **Unit**: Milliseconds since Unix epoch (January 1, 1970, 00:00:00 UTC)
12//! - **Range**: Supports dates from 1970 to approximately year 294,276
13//!
14//! # Object Safety
15//!
16//! This trait is designed to be object-safe, allowing for dynamic dispatch via
17//! trait objects (`dyn MarketData`). All async methods return boxed futures
18//! to maintain object safety.
19//!
20//! # Example
21//!
22//! ```rust,ignore
23//! use ccxt_core::traits::MarketData;
24//! use ccxt_core::types::{Timeframe, params::OhlcvParams};
25//!
26//! async fn fetch_data(exchange: &dyn MarketData) -> Result<(), ccxt_core::Error> {
27//! // Fetch markets
28//! let markets = exchange.fetch_markets().await?;
29//!
30//! // Fetch ticker
31//! let ticker = exchange.fetch_ticker("BTC/USDT").await?;
32//!
33//! // Fetch OHLCV with i64 timestamp parameters
34//! let since: i64 = chrono::Utc::now().timestamp_millis() - 3600000; // 1 hour ago
35//! let candles = exchange.fetch_ohlcv_with_params(
36//! "BTC/USDT",
37//! OhlcvParams::new(Timeframe::H1).since(since).limit(100)
38//! ).await?;
39//!
40//! Ok(())
41//! }
42//! ```
43
44use async_trait::async_trait;
45use std::collections::HashMap;
46
47use crate::error::Result;
48use crate::traits::PublicExchange;
49use crate::types::{
50 Market, Ohlcv, OrderBook, Ticker, Timeframe, Trade,
51 params::{OhlcvParams, OrderBookParams},
52};
53
54/// Trait for fetching public market data.
55///
56/// This trait provides methods for accessing public market information
57/// that doesn't require authentication. All methods are async and return
58/// `Result<T>` for proper error handling.
59///
60/// # Timestamp Format
61///
62/// All timestamp parameters and fields in returned data structures use:
63/// - **Type**: `i64`
64/// - **Unit**: Milliseconds since Unix epoch (January 1, 1970, 00:00:00 UTC)
65/// - **Example**: `1609459200000` represents January 1, 2021, 00:00:00 UTC
66///
67/// # Supertrait
68///
69/// Requires `PublicExchange` as a supertrait to access exchange metadata
70/// and capabilities.
71///
72/// # Thread Safety
73///
74/// This trait requires `Send + Sync` bounds (inherited from `PublicExchange`)
75/// to ensure safe usage across thread boundaries in async contexts.
76#[async_trait]
77pub trait MarketData: PublicExchange {
78 // ========================================================================
79 // Markets
80 // ========================================================================
81
82 /// Fetch all available markets from the exchange.
83 ///
84 /// Returns a vector of `Market` structures containing information about
85 /// each trading pair including symbols, precision, limits, and fees.
86 ///
87 /// # Example
88 ///
89 /// ```rust,ignore
90 /// let markets = exchange.fetch_markets().await?;
91 /// for market in markets {
92 /// println!("{}: {} ({})", market.symbol, market.base, market.quote);
93 /// }
94 /// ```
95 async fn fetch_markets(&self) -> Result<Vec<Market>>;
96
97 /// Load and cache markets.
98 ///
99 /// This method fetches markets if not already cached, or returns the
100 /// cached markets. Use `reload_markets()` to force a refresh.
101 ///
102 /// # Example
103 ///
104 /// ```rust,ignore
105 /// // First call loads from API
106 /// let markets = exchange.load_markets().await?;
107 ///
108 /// // Subsequent calls use cache
109 /// let markets = exchange.load_markets().await?;
110 /// ```
111 async fn load_markets(
112 &self,
113 ) -> Result<std::sync::Arc<HashMap<String, std::sync::Arc<Market>>>> {
114 self.load_markets_with_reload(false).await
115 }
116
117 /// Force reload markets from the API.
118 ///
119 /// Clears the cache and fetches fresh market data.
120 async fn reload_markets(
121 &self,
122 ) -> Result<std::sync::Arc<HashMap<String, std::sync::Arc<Market>>>> {
123 self.load_markets_with_reload(true).await
124 }
125
126 /// Internal method to load markets with optional reload.
127 ///
128 /// Implementations should cache markets and only fetch from the API
129 /// when `reload` is `true` or the cache is empty.
130 async fn load_markets_with_reload(
131 &self,
132 reload: bool,
133 ) -> Result<std::sync::Arc<HashMap<String, std::sync::Arc<Market>>>>;
134
135 /// Get a specific market by symbol.
136 ///
137 /// Returns the market information for the given symbol, or an error
138 /// if the symbol is not found.
139 async fn market(&self, symbol: &str) -> Result<std::sync::Arc<Market>>;
140
141 /// Get all cached markets.
142 ///
143 /// Returns the currently cached markets without fetching from the API.
144 async fn markets(&self) -> std::sync::Arc<HashMap<String, std::sync::Arc<Market>>>;
145
146 /// Check if a symbol exists and is active.
147 ///
148 /// Returns `true` if the symbol exists in the market cache and is active.
149 async fn has_symbol(&self, symbol: &str) -> bool {
150 self.market(symbol).await.map(|m| m.active).unwrap_or(false)
151 }
152
153 // ========================================================================
154 // Tickers
155 // ========================================================================
156
157 /// Fetch ticker for a single symbol.
158 ///
159 /// Returns current price and volume information for the specified symbol.
160 ///
161 /// # Arguments
162 ///
163 /// * `symbol` - The trading pair symbol (e.g., "BTC/USDT")
164 ///
165 /// # Example
166 ///
167 /// ```rust,ignore
168 /// let ticker = exchange.fetch_ticker("BTC/USDT").await?;
169 /// println!("Last price: {:?}", ticker.last);
170 /// ```
171 async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker>;
172
173 /// Fetch tickers for multiple symbols.
174 ///
175 /// Returns tickers for the specified symbols. If `symbols` is empty,
176 /// returns tickers for all available symbols.
177 ///
178 /// # Arguments
179 ///
180 /// * `symbols` - Slice of trading pair symbols
181 ///
182 /// # Example
183 ///
184 /// ```rust,ignore
185 /// // Fetch specific symbols
186 /// let tickers = exchange.fetch_tickers(&["BTC/USDT", "ETH/USDT"]).await?;
187 ///
188 /// // Fetch all tickers
189 /// let all_tickers = exchange.fetch_all_tickers().await?;
190 /// ```
191 async fn fetch_tickers(&self, symbols: &[&str]) -> Result<Vec<Ticker>>;
192
193 /// Fetch all tickers.
194 ///
195 /// Convenience method that calls `fetch_tickers` with an empty slice.
196 async fn fetch_all_tickers(&self) -> Result<Vec<Ticker>> {
197 self.fetch_tickers(&[]).await
198 }
199
200 // ========================================================================
201 // Order Book
202 // ========================================================================
203
204 /// Fetch order book with default depth.
205 ///
206 /// Returns the current order book (bids and asks) for the specified symbol.
207 ///
208 /// # Arguments
209 ///
210 /// * `symbol` - The trading pair symbol
211 ///
212 /// # Example
213 ///
214 /// ```rust,ignore
215 /// let orderbook = exchange.fetch_order_book("BTC/USDT").await?;
216 /// println!("Best bid: {:?}", orderbook.bids.first());
217 /// println!("Best ask: {:?}", orderbook.asks.first());
218 /// ```
219 async fn fetch_order_book(&self, symbol: &str) -> Result<OrderBook> {
220 self.fetch_order_book_with_params(symbol, OrderBookParams::default())
221 .await
222 }
223
224 /// Fetch order book with custom depth.
225 ///
226 /// # Arguments
227 ///
228 /// * `symbol` - The trading pair symbol
229 /// * `limit` - Maximum number of price levels to return
230 async fn fetch_order_book_with_depth(&self, symbol: &str, limit: u32) -> Result<OrderBook> {
231 self.fetch_order_book_with_params(symbol, OrderBookParams::default().limit(limit))
232 .await
233 }
234
235 /// Fetch order book with full parameters.
236 ///
237 /// # Arguments
238 ///
239 /// * `symbol` - The trading pair symbol
240 /// * `params` - Order book parameters including depth limit
241 async fn fetch_order_book_with_params(
242 &self,
243 symbol: &str,
244 params: OrderBookParams,
245 ) -> Result<OrderBook>;
246
247 // ========================================================================
248 // Trades
249 // ========================================================================
250
251 /// Fetch recent public trades.
252 ///
253 /// Returns recent trades for the specified symbol.
254 ///
255 /// # Arguments
256 ///
257 /// * `symbol` - The trading pair symbol
258 ///
259 /// # Example
260 ///
261 /// ```rust,ignore
262 /// let trades = exchange.fetch_trades("BTC/USDT").await?;
263 /// for trade in trades {
264 /// println!("{}: {} @ {}", trade.side, trade.amount, trade.price);
265 /// }
266 /// ```
267 async fn fetch_trades(&self, symbol: &str) -> Result<Vec<Trade>> {
268 self.fetch_trades_with_limit(symbol, None, None).await
269 }
270
271 /// Fetch recent trades with limit only.
272 ///
273 /// # Arguments
274 ///
275 /// * `symbol` - The trading pair symbol
276 /// * `limit` - Maximum number of trades to return
277 async fn fetch_trades_with_limit_only(
278 &self,
279 symbol: &str,
280 limit: Option<u32>,
281 ) -> Result<Vec<Trade>> {
282 self.fetch_trades_with_limit(symbol, None, limit).await
283 }
284
285 /// Fetch recent trades with limit and optional timestamp filtering.
286 ///
287 /// Returns recent trades for the specified symbol, optionally filtered by timestamp.
288 ///
289 /// # Arguments
290 ///
291 /// * `symbol` - The trading pair symbol (e.g., "BTC/USDT")
292 /// * `since` - Optional start timestamp in milliseconds (i64) since Unix epoch
293 /// * `limit` - Maximum number of trades to return
294 ///
295 /// # Timestamp Format
296 ///
297 /// The `since` parameter uses `i64` milliseconds since Unix epoch:
298 /// - `1609459200000` = January 1, 2021, 00:00:00 UTC
299 /// - `chrono::Utc::now().timestamp_millis()` = Current time
300 /// - `chrono::Utc::now().timestamp_millis() - 3600000` = 1 hour ago
301 ///
302 /// # Example
303 ///
304 /// ```rust,ignore
305 /// // Fetch recent trades without timestamp filter
306 /// let trades = exchange.fetch_trades_with_limit("BTC/USDT", None, Some(100)).await?;
307 ///
308 /// // Fetch trades from the last hour
309 /// let since = chrono::Utc::now().timestamp_millis() - 3600000;
310 /// let trades = exchange.fetch_trades_with_limit("BTC/USDT", Some(since), Some(50)).await?;
311 /// ```
312 async fn fetch_trades_with_limit(
313 &self,
314 symbol: &str,
315 since: Option<i64>,
316 limit: Option<u32>,
317 ) -> Result<Vec<Trade>>;
318
319 // ========================================================================
320 // Deprecated u64 Wrapper Methods (Backward Compatibility)
321 // ========================================================================
322
323 /// Fetch OHLCV candlestick data with u64 timestamps (deprecated).
324 ///
325 /// **DEPRECATED**: Use `fetch_ohlcv_with_params` with i64 timestamps instead.
326 /// This method is provided for backward compatibility during migration.
327 ///
328 /// # Migration
329 ///
330 /// ```rust,ignore
331 /// // Old code (deprecated)
332 /// let candles = exchange.fetch_ohlcv_u64("BTC/USDT", Timeframe::H1, Some(1609459200000u64), Some(100), None).await?;
333 ///
334 /// // New code (recommended)
335 /// let params = OhlcvParams::new(Timeframe::H1).since(1609459200000i64).limit(100);
336 /// let candles = exchange.fetch_ohlcv_with_params("BTC/USDT", params).await?;
337 /// ```
338 #[deprecated(
339 since = "0.1.0",
340 note = "Use fetch_ohlcv_with_params with i64 timestamps. Convert using TimestampUtils::u64_to_i64()"
341 )]
342 async fn fetch_ohlcv_u64(
343 &self,
344 symbol: &str,
345 timeframe: Timeframe,
346 since: Option<u64>,
347 limit: Option<u32>,
348 until: Option<u64>,
349 ) -> Result<Vec<Ohlcv>> {
350 use crate::time::TimestampConversion;
351 use crate::types::params::OhlcvParams;
352
353 let since_i64 = since.to_i64()?;
354 let until_i64 = until.to_i64()?;
355
356 let mut params = OhlcvParams::new(timeframe);
357 if let Some(ts) = since_i64 {
358 params = params.since(ts);
359 }
360 if let Some(ts) = until_i64 {
361 params = params.until(ts);
362 }
363 if let Some(n) = limit {
364 params = params.limit(n);
365 }
366
367 self.fetch_ohlcv_with_params(symbol, params).await
368 }
369
370 /// Fetch recent trades with u64 timestamp filtering (deprecated).
371 ///
372 /// **DEPRECATED**: Use `fetch_trades_with_limit` with i64 timestamps instead.
373 /// This method is provided for backward compatibility during migration.
374 ///
375 /// # Migration
376 ///
377 /// ```rust,ignore
378 /// // Old code (deprecated)
379 /// let trades = exchange.fetch_trades_u64("BTC/USDT", Some(1609459200000u64), Some(100)).await?;
380 ///
381 /// // New code (recommended)
382 /// let trades = exchange.fetch_trades_with_limit("BTC/USDT", Some(1609459200000i64), Some(100)).await?;
383 /// ```
384 #[deprecated(
385 since = "0.1.0",
386 note = "Use fetch_trades_with_limit with i64 timestamps. Convert using TimestampUtils::u64_to_i64()"
387 )]
388 async fn fetch_trades_u64(
389 &self,
390 symbol: &str,
391 since: Option<u64>,
392 limit: Option<u32>,
393 ) -> Result<Vec<Trade>> {
394 use crate::time::TimestampConversion;
395
396 let since_i64 = since.to_i64()?;
397 self.fetch_trades_with_limit(symbol, since_i64, limit).await
398 }
399
400 // ========================================================================
401 // OHLCV (Candlesticks)
402 // ========================================================================
403
404 /// Fetch OHLCV candlestick data.
405 ///
406 /// Returns candlestick data for the specified symbol and timeframe.
407 ///
408 /// # Arguments
409 ///
410 /// * `symbol` - The trading pair symbol
411 /// * `timeframe` - The candlestick timeframe
412 ///
413 /// # Example
414 ///
415 /// ```rust,ignore
416 /// let candles = exchange.fetch_ohlcv("BTC/USDT", Timeframe::H1).await?;
417 /// for candle in candles {
418 /// println!("O: {} H: {} L: {} C: {}",
419 /// candle.open, candle.high, candle.low, candle.close);
420 /// }
421 /// ```
422 async fn fetch_ohlcv(&self, symbol: &str, timeframe: Timeframe) -> Result<Vec<Ohlcv>> {
423 self.fetch_ohlcv_with_params(symbol, OhlcvParams::new(timeframe))
424 .await
425 }
426
427 /// Fetch OHLCV with full parameters.
428 ///
429 /// Allows specifying additional parameters like start time, limit, and
430 /// price type (for futures).
431 ///
432 /// # Arguments
433 ///
434 /// * `symbol` - The trading pair symbol (e.g., "BTC/USDT")
435 /// * `params` - OHLCV parameters including timeframe, since, limit, until
436 ///
437 /// # Timestamp Format
438 ///
439 /// All timestamp parameters in `OhlcvParams` use `i64` milliseconds since Unix epoch:
440 /// - `since`: Start time for historical data
441 /// - `until`: End time for historical data
442 /// - Returned `Ohlcv` structures have `timestamp` field as `i64`
443 ///
444 /// # Example
445 ///
446 /// ```rust,ignore
447 /// use ccxt_core::types::params::OhlcvParams;
448 ///
449 /// // Fetch recent candles
450 /// let candles = exchange.fetch_ohlcv_with_params(
451 /// "BTC/USDT",
452 /// OhlcvParams::new(Timeframe::H1).limit(100)
453 /// ).await?;
454 ///
455 /// // Fetch historical candles with timestamp range
456 /// let since = chrono::Utc::now().timestamp_millis() - (7 * 24 * 60 * 60 * 1000); // 7 days ago
457 /// let until = chrono::Utc::now().timestamp_millis();
458 /// let candles = exchange.fetch_ohlcv_with_params(
459 /// "BTC/USDT",
460 /// OhlcvParams::new(Timeframe::D1)
461 /// .since(since)
462 /// .until(until)
463 /// .limit(7)
464 /// ).await?;
465 /// ```
466 async fn fetch_ohlcv_with_params(
467 &self,
468 symbol: &str,
469 params: OhlcvParams,
470 ) -> Result<Vec<Ohlcv>>;
471}
472
473/// Type alias for boxed MarketData trait object.
474pub type BoxedMarketData = Box<dyn MarketData>;
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::capability::ExchangeCapabilities;
480
481 // Mock implementation for testing trait object safety
482 struct MockExchange;
483
484 impl PublicExchange for MockExchange {
485 fn id(&self) -> &str {
486 "mock"
487 }
488 fn name(&self) -> &str {
489 "Mock Exchange"
490 }
491 fn capabilities(&self) -> ExchangeCapabilities {
492 ExchangeCapabilities::public_only()
493 }
494 fn timeframes(&self) -> Vec<Timeframe> {
495 vec![Timeframe::H1, Timeframe::D1]
496 }
497 }
498
499 #[async_trait]
500 impl MarketData for MockExchange {
501 async fn fetch_markets(&self) -> Result<Vec<Market>> {
502 Ok(vec![])
503 }
504
505 async fn load_markets_with_reload(
506 &self,
507 _reload: bool,
508 ) -> Result<std::sync::Arc<HashMap<String, std::sync::Arc<Market>>>> {
509 Ok(std::sync::Arc::new(HashMap::new()))
510 }
511
512 async fn market(&self, _symbol: &str) -> Result<std::sync::Arc<Market>> {
513 Err(crate::Error::invalid_request("Not found"))
514 }
515
516 async fn markets(&self) -> std::sync::Arc<HashMap<String, std::sync::Arc<Market>>> {
517 std::sync::Arc::new(HashMap::new())
518 }
519
520 async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
521 Ok(Ticker::new(symbol.to_string(), 0))
522 }
523
524 async fn fetch_tickers(&self, _symbols: &[&str]) -> Result<Vec<Ticker>> {
525 Ok(vec![])
526 }
527
528 async fn fetch_order_book_with_params(
529 &self,
530 symbol: &str,
531 _params: OrderBookParams,
532 ) -> Result<OrderBook> {
533 Ok(OrderBook::new(symbol.to_string(), 0))
534 }
535
536 async fn fetch_trades_with_limit(
537 &self,
538 _symbol: &str,
539 _since: Option<i64>,
540 _limit: Option<u32>,
541 ) -> Result<Vec<Trade>> {
542 Ok(vec![])
543 }
544
545 async fn fetch_ohlcv_with_params(
546 &self,
547 _symbol: &str,
548 _params: OhlcvParams,
549 ) -> Result<Vec<Ohlcv>> {
550 Ok(vec![])
551 }
552 }
553
554 #[test]
555 fn test_trait_object_safety() {
556 // Verify trait is object-safe by creating a trait object
557 let _exchange: BoxedMarketData = Box::new(MockExchange);
558 }
559
560 #[tokio::test]
561 async fn test_default_implementations() {
562 let exchange = MockExchange;
563
564 // Test load_markets default
565 let markets = exchange.load_markets().await.unwrap();
566 assert!(markets.is_empty());
567
568 // Test reload_markets default
569 let markets = exchange.reload_markets().await.unwrap();
570 assert!(markets.is_empty());
571
572 // Test has_symbol default
573 let has = exchange.has_symbol("BTC/USDT").await;
574 assert!(!has);
575
576 // Test fetch_all_tickers default
577 let tickers = exchange.fetch_all_tickers().await.unwrap();
578 assert!(tickers.is_empty());
579
580 // Test fetch_order_book default
581 let orderbook = exchange.fetch_order_book("BTC/USDT").await.unwrap();
582 assert_eq!(orderbook.symbol, "BTC/USDT");
583
584 // Test fetch_order_book_with_depth default
585 let orderbook = exchange
586 .fetch_order_book_with_depth("BTC/USDT", 100)
587 .await
588 .unwrap();
589 assert_eq!(orderbook.symbol, "BTC/USDT");
590
591 // Test fetch_trades default
592 let trades = exchange.fetch_trades("BTC/USDT").await.unwrap();
593 assert!(trades.is_empty());
594
595 // Test fetch_ohlcv default
596 let candles = exchange
597 .fetch_ohlcv("BTC/USDT", Timeframe::H1)
598 .await
599 .unwrap();
600 assert!(candles.is_empty());
601 }
602}