ccxt_exchanges/okx/rest/
market_data.rs

1//! Market data endpoints for OKX REST API.
2
3use super::super::{Okx, parser};
4use ccxt_core::{
5    Error, ParseError, Result,
6    types::{Market, OHLCV, OhlcvRequest, OrderBook, Ticker, Trade},
7};
8use std::{collections::HashMap, sync::Arc};
9use tracing::{info, warn};
10
11impl Okx {
12    /// Fetch all trading markets.
13    ///
14    /// # Returns
15    ///
16    /// Returns a vector of [`Market`] structures containing market information.
17    ///
18    /// # Errors
19    ///
20    /// Returns an error if the API request fails or response parsing fails.
21    ///
22    /// # Example
23    ///
24    /// ```no_run
25    /// # use ccxt_exchanges::okx::Okx;
26    /// # async fn example() -> ccxt_core::Result<()> {
27    /// let okx = Okx::builder().build()?;
28    /// let markets = okx.fetch_markets().await?;
29    /// println!("Found {} markets", markets.len());
30    /// # Ok(())
31    /// # }
32    /// ```
33    pub async fn fetch_markets(&self) -> Result<Arc<HashMap<String, Arc<Market>>>> {
34        let path = Self::build_api_path("/public/instruments");
35        let mut params = HashMap::new();
36        params.insert("instType".to_string(), self.get_inst_type().to_string());
37
38        let response = self.public_request("GET", &path, Some(&params)).await?;
39
40        let data = response
41            .get("data")
42            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
43
44        let instruments = data.as_array().ok_or_else(|| {
45            Error::from(ParseError::invalid_format(
46                "data",
47                "Expected array of instruments",
48            ))
49        })?;
50
51        let mut markets = Vec::new();
52        for instrument in instruments {
53            match parser::parse_market(instrument) {
54                Ok(market) => markets.push(market),
55                Err(e) => {
56                    warn!(error = %e, "Failed to parse market");
57                }
58            }
59        }
60
61        let result = self.base().set_markets(markets, None).await?;
62
63        info!("Loaded {} markets for OKX", result.len());
64        Ok(result)
65    }
66
67    /// Load and cache market data.
68    ///
69    /// If markets are already loaded and `reload` is false, returns cached data.
70    ///
71    /// # Arguments
72    ///
73    /// * `reload` - Whether to force reload market data from the API.
74    ///
75    /// # Returns
76    ///
77    /// Returns a `HashMap` containing all market data, keyed by symbol (e.g., "BTC/USDT").
78    pub async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
79        let _loading_guard = self.base().market_loading_lock.lock().await;
80
81        {
82            let cache = self.base().market_cache.read().await;
83            if cache.is_loaded() && !reload {
84                return Ok(cache.markets());
85            }
86        }
87
88        info!("Loading markets for OKX (reload: {})", reload);
89        let _markets = self.fetch_markets().await?;
90
91        let cache = self.base().market_cache.read().await;
92        Ok(cache.markets())
93    }
94
95    /// Fetch ticker for a single trading pair.
96    ///
97    /// # Arguments
98    ///
99    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
100    ///
101    /// # Returns
102    ///
103    /// Returns [`Ticker`] data for the specified symbol.
104    pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
105        let market = self.base().market(symbol).await?;
106
107        let path = Self::build_api_path("/market/ticker");
108        let mut params = HashMap::new();
109        params.insert("instId".to_string(), market.id.clone());
110
111        let response = self.public_request("GET", &path, Some(&params)).await?;
112
113        let data = response
114            .get("data")
115            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
116
117        let tickers = data.as_array().ok_or_else(|| {
118            Error::from(ParseError::invalid_format(
119                "data",
120                "Expected array of tickers",
121            ))
122        })?;
123
124        if tickers.is_empty() {
125            return Err(Error::bad_symbol(format!("No ticker data for {}", symbol)));
126        }
127
128        parser::parse_ticker(&tickers[0], Some(&market))
129    }
130
131    /// Fetch tickers for multiple trading pairs.
132    ///
133    /// # Arguments
134    ///
135    /// * `symbols` - Optional list of trading pair symbols; fetches all if `None`.
136    ///
137    /// # Returns
138    ///
139    /// Returns a vector of [`Ticker`] structures.
140    pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
141        let cache = self.base().market_cache.read().await;
142        if !cache.is_loaded() {
143            drop(cache);
144            return Err(Error::exchange(
145                "-1",
146                "Markets not loaded. Call load_markets() first.",
147            ));
148        }
149        // Build a snapshot of markets by ID for efficient lookup
150        let markets_snapshot: std::collections::HashMap<String, Arc<Market>> = cache
151            .iter_markets()
152            .map(|(_, m)| (m.id.clone(), m))
153            .collect();
154        drop(cache);
155
156        let path = Self::build_api_path("/market/tickers");
157        let mut params = HashMap::new();
158        params.insert("instType".to_string(), self.get_inst_type().to_string());
159
160        let response = self.public_request("GET", &path, Some(&params)).await?;
161
162        let data = response
163            .get("data")
164            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
165
166        let tickers_array = data.as_array().ok_or_else(|| {
167            Error::from(ParseError::invalid_format(
168                "data",
169                "Expected array of tickers",
170            ))
171        })?;
172
173        let mut tickers = Vec::new();
174        for ticker_data in tickers_array {
175            if let Some(inst_id) = ticker_data["instId"].as_str() {
176                if let Some(market) = markets_snapshot.get(inst_id) {
177                    match parser::parse_ticker(ticker_data, Some(market)) {
178                        Ok(ticker) => {
179                            if let Some(ref syms) = symbols {
180                                if syms.contains(&ticker.symbol) {
181                                    tickers.push(ticker);
182                                }
183                            } else {
184                                tickers.push(ticker);
185                            }
186                        }
187                        Err(e) => {
188                            warn!(
189                                error = %e,
190                                symbol = %inst_id,
191                                "Failed to parse ticker"
192                            );
193                        }
194                    }
195                }
196            }
197        }
198
199        Ok(tickers)
200    }
201
202    /// Fetch order book for a trading pair.
203    ///
204    /// # Arguments
205    ///
206    /// * `symbol` - Trading pair symbol.
207    /// * `limit` - Optional depth limit (valid values: 1-400; default: 100).
208    ///
209    /// # Returns
210    ///
211    /// Returns [`OrderBook`] data containing bids and asks.
212    pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
213        let market = self.base().market(symbol).await?;
214
215        let path = Self::build_api_path("/market/books");
216        let mut params = HashMap::new();
217        params.insert("instId".to_string(), market.id.clone());
218
219        let actual_limit = limit.map_or(100, |l| l.min(400));
220        params.insert("sz".to_string(), actual_limit.to_string());
221
222        let response = self.public_request("GET", &path, Some(&params)).await?;
223
224        let data = response
225            .get("data")
226            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
227
228        let books = data.as_array().ok_or_else(|| {
229            Error::from(ParseError::invalid_format(
230                "data",
231                "Expected array of orderbooks",
232            ))
233        })?;
234
235        if books.is_empty() {
236            return Err(Error::bad_symbol(format!(
237                "No orderbook data for {}",
238                symbol
239            )));
240        }
241
242        parser::parse_orderbook(&books[0], market.symbol.clone())
243    }
244
245    /// Fetch recent public trades.
246    ///
247    /// # Arguments
248    ///
249    /// * `symbol` - Trading pair symbol.
250    /// * `limit` - Optional limit on number of trades (maximum: 500).
251    ///
252    /// # Returns
253    ///
254    /// Returns a vector of [`Trade`] structures, sorted by timestamp in descending order.
255    pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
256        let market = self.base().market(symbol).await?;
257
258        let path = Self::build_api_path("/market/trades");
259        let mut params = HashMap::new();
260        params.insert("instId".to_string(), market.id.clone());
261
262        let actual_limit = limit.map_or(100, |l| l.min(500));
263        params.insert("limit".to_string(), actual_limit.to_string());
264
265        let response = self.public_request("GET", &path, Some(&params)).await?;
266
267        let data = response
268            .get("data")
269            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
270
271        let trades_array = data.as_array().ok_or_else(|| {
272            Error::from(ParseError::invalid_format(
273                "data",
274                "Expected array of trades",
275            ))
276        })?;
277
278        let mut trades = Vec::new();
279        for trade_data in trades_array {
280            match parser::parse_trade(trade_data, Some(&market)) {
281                Ok(trade) => trades.push(trade),
282                Err(e) => {
283                    warn!(error = %e, "Failed to parse trade");
284                }
285            }
286        }
287
288        trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
289
290        Ok(trades)
291    }
292
293    /// Fetch OHLCV (candlestick) data using the builder pattern.
294    ///
295    /// This is the preferred method for fetching OHLCV data. It accepts an [`OhlcvRequest`]
296    /// built using the builder pattern, which provides validation and a more ergonomic API.
297    ///
298    /// # Arguments
299    ///
300    /// * `request` - OHLCV request built via [`OhlcvRequest::builder()`]
301    ///
302    /// # Returns
303    ///
304    /// Returns a vector of [`OHLCV`] structures.
305    ///
306    /// # Errors
307    ///
308    /// Returns an error if the market is not found or the API request fails.
309    ///
310    /// _Requirements: 2.3, 2.6_
311    pub async fn fetch_ohlcv_v2(&self, request: OhlcvRequest) -> Result<Vec<OHLCV>> {
312        let market = self.base().market(&request.symbol).await?;
313
314        let timeframes = self.timeframes();
315        let okx_timeframe = timeframes.get(&request.timeframe).ok_or_else(|| {
316            Error::invalid_request(format!("Unsupported timeframe: {}", request.timeframe))
317        })?;
318
319        let path = Self::build_api_path("/market/candles");
320        let mut params = HashMap::new();
321        params.insert("instId".to_string(), market.id.clone());
322        params.insert("bar".to_string(), okx_timeframe.clone());
323
324        let actual_limit = request.limit.map_or(100, |l| l.min(300));
325        params.insert("limit".to_string(), actual_limit.to_string());
326
327        if let Some(start_time) = request.since {
328            params.insert("after".to_string(), start_time.to_string());
329        }
330
331        if let Some(end_time) = request.until {
332            params.insert("before".to_string(), end_time.to_string());
333        }
334
335        let response = self.public_request("GET", &path, Some(&params)).await?;
336
337        let data = response
338            .get("data")
339            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
340
341        let candles_array = data.as_array().ok_or_else(|| {
342            Error::from(ParseError::invalid_format(
343                "data",
344                "Expected array of candles",
345            ))
346        })?;
347
348        let mut ohlcv = Vec::new();
349        for candle_data in candles_array {
350            match parser::parse_ohlcv(candle_data) {
351                Ok(candle) => ohlcv.push(candle),
352                Err(e) => {
353                    warn!(error = %e, "Failed to parse OHLCV");
354                }
355            }
356        }
357
358        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
359
360        Ok(ohlcv)
361    }
362
363    /// Fetch OHLCV (candlestick) data (deprecated).
364    ///
365    /// # Deprecated
366    ///
367    /// This method is deprecated. Use [`fetch_ohlcv_v2`](Self::fetch_ohlcv_v2) with
368    /// [`OhlcvRequest::builder()`] instead for a more ergonomic API.
369    ///
370    /// # Arguments
371    ///
372    /// * `symbol` - Trading pair symbol.
373    /// * `timeframe` - Candlestick timeframe (e.g., "1m", "5m", "1h", "1d").
374    /// * `since` - Optional start timestamp in milliseconds.
375    /// * `limit` - Optional limit on number of candles (maximum: 300).
376    ///
377    /// # Returns
378    ///
379    /// Returns a vector of [`OHLCV`] structures.
380    #[deprecated(
381        since = "0.2.0",
382        note = "Use fetch_ohlcv_v2 with OhlcvRequest::builder() instead"
383    )]
384    pub async fn fetch_ohlcv(
385        &self,
386        symbol: &str,
387        timeframe: &str,
388        since: Option<i64>,
389        limit: Option<u32>,
390    ) -> Result<Vec<OHLCV>> {
391        let market = self.base().market(symbol).await?;
392
393        let timeframes = self.timeframes();
394        let okx_timeframe = timeframes.get(timeframe).ok_or_else(|| {
395            Error::invalid_request(format!("Unsupported timeframe: {}", timeframe))
396        })?;
397
398        let path = Self::build_api_path("/market/candles");
399        let mut params = HashMap::new();
400        params.insert("instId".to_string(), market.id.clone());
401        params.insert("bar".to_string(), okx_timeframe.clone());
402
403        let actual_limit = limit.map_or(100, |l| l.min(300));
404        params.insert("limit".to_string(), actual_limit.to_string());
405
406        if let Some(start_time) = since {
407            params.insert("after".to_string(), start_time.to_string());
408        }
409
410        let response = self.public_request("GET", &path, Some(&params)).await?;
411
412        let data = response
413            .get("data")
414            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
415
416        let candles_array = data.as_array().ok_or_else(|| {
417            Error::from(ParseError::invalid_format(
418                "data",
419                "Expected array of candles",
420            ))
421        })?;
422
423        let mut ohlcv = Vec::new();
424        for candle_data in candles_array {
425            match parser::parse_ohlcv(candle_data) {
426                Ok(candle) => ohlcv.push(candle),
427                Err(e) => {
428                    warn!(error = %e, "Failed to parse OHLCV");
429                }
430            }
431        }
432
433        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
434
435        Ok(ohlcv)
436    }
437}