Skip to main content

rust_okx/api/
market.rs

1//! Public market-data endpoints (`/api/v5/market/*`).
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::OkxClient;
6use crate::error::Error;
7use crate::model::{InstType, NumberString};
8use crate::transport::Transport;
9
10const TICKER: &str = "/api/v5/market/ticker";
11const TICKERS: &str = "/api/v5/market/tickers";
12const INDEX_TICKERS: &str = "/api/v5/market/index-tickers";
13const BOOKS: &str = "/api/v5/market/books";
14const BOOKS_LITE: &str = "/api/v5/market/books-lite";
15const CANDLES: &str = "/api/v5/market/candles";
16const HISTORY_CANDLES: &str = "/api/v5/market/history-candles";
17const INDEX_CANDLES: &str = "/api/v5/market/index-candles";
18const MARK_PRICE_CANDLES: &str = "/api/v5/market/mark-price-candles";
19const TRADES: &str = "/api/v5/market/trades";
20const HISTORY_TRADES: &str = "/api/v5/market/history-trades";
21const PLATFORM_24_VOLUME: &str = "/api/v5/market/platform-24-volume";
22const INDEX_COMPONENTS: &str = "/api/v5/market/index-components";
23const EXCHANGE_RATE: &str = "/api/v5/market/exchange-rate";
24
25/// Accessor for the public market-data endpoints.
26///
27/// Obtain one via [`OkxClient::market`](crate::OkxClient::market).
28pub struct Market<'a, T> {
29    client: &'a OkxClient<T>,
30}
31
32impl<'a, T: Transport> Market<'a, T> {
33    pub(crate) fn new(client: &'a OkxClient<T>) -> Self {
34        Self { client }
35    }
36
37    /// Retrieve the latest ticker for a single instrument.
38    ///
39    /// `GET /api/v5/market/ticker`. Public (unauthenticated). The returned
40    /// vector contains exactly one [`Ticker`].
41    ///
42    /// # Errors
43    ///
44    /// Returns [`Error::Api`] on a non-zero OKX code, or
45    /// [`Error::Transport`]/[`Error::Decode`] on transport/parsing failure.
46    pub async fn get_ticker(&self, inst_id: &str) -> Result<Vec<Ticker>, Error> {
47        let query = InstIdQuery { inst_id };
48        self.client.get(TICKER, &query, false).await
49    }
50
51    /// Retrieve tickers for an instrument type.
52    ///
53    /// `GET /api/v5/market/tickers`. Public. `underlying` and `inst_family`
54    /// are useful for derivatives and omitted when `None`.
55    ///
56    /// # Errors
57    ///
58    /// See [`get_ticker`](Self::get_ticker).
59    pub async fn get_tickers(
60        &self,
61        inst_type: InstType,
62        underlying: Option<&str>,
63        inst_family: Option<&str>,
64    ) -> Result<Vec<Ticker>, Error> {
65        let query = TickersQuery {
66            inst_type: &inst_type,
67            underlying,
68            inst_family,
69        };
70        self.client.get(TICKERS, &query, false).await
71    }
72
73    /// Retrieve index tickers.
74    ///
75    /// `GET /api/v5/market/index-tickers`. Public. Filter by quote currency,
76    /// index instrument ID, or neither.
77    ///
78    /// # Errors
79    ///
80    /// See [`get_ticker`](Self::get_ticker).
81    pub async fn get_index_tickers(
82        &self,
83        quote_ccy: Option<&str>,
84        inst_id: Option<&str>,
85    ) -> Result<Vec<IndexTicker>, Error> {
86        let query = IndexTickersQuery { quote_ccy, inst_id };
87        self.client.get(INDEX_TICKERS, &query, false).await
88    }
89
90    /// Retrieve the order book for an instrument.
91    ///
92    /// `GET /api/v5/market/books`. `depth` is the number of levels per side
93    /// (OKX default 1, max 400). Public.
94    ///
95    /// # Errors
96    ///
97    /// See [`get_ticker`](Self::get_ticker).
98    pub async fn get_orderbook(
99        &self,
100        inst_id: &str,
101        depth: Option<u32>,
102    ) -> Result<Vec<OrderBook>, Error> {
103        let query = OrderBookQuery { inst_id, sz: depth };
104        self.client.get(BOOKS, &query, false).await
105    }
106
107    /// Retrieve the lightweight order book for an instrument.
108    ///
109    /// `GET /api/v5/market/books-lite`. Public.
110    ///
111    /// # Errors
112    ///
113    /// See [`get_ticker`](Self::get_ticker).
114    pub async fn get_order_lite_book(&self, inst_id: &str) -> Result<Vec<OrderBook>, Error> {
115        let query = InstIdQuery { inst_id };
116        self.client.get(BOOKS_LITE, &query, false).await
117    }
118
119    /// Retrieve candlestick (OHLCV) data.
120    ///
121    /// `GET /api/v5/market/candles`. `bar` is the bar size, e.g. `1m`, `1H`,
122    /// `1D` (OKX default `1m`). `limit` caps the number of bars (max 300).
123    /// Public.
124    ///
125    /// # Errors
126    ///
127    /// See [`get_ticker`](Self::get_ticker).
128    pub async fn get_candlesticks(
129        &self,
130        inst_id: &str,
131        bar: Option<&str>,
132        limit: Option<u32>,
133    ) -> Result<Vec<Candle>, Error> {
134        let query = CandlesQuery {
135            inst_id,
136            bar,
137            limit,
138        };
139        self.client.get(CANDLES, &query, false).await
140    }
141
142    /// Retrieve historical candlestick data for top currencies.
143    ///
144    /// `GET /api/v5/market/history-candles`. Public.
145    ///
146    /// # Errors
147    ///
148    /// See [`get_ticker`](Self::get_ticker).
149    pub async fn get_history_candlesticks(
150        &self,
151        request: &CandlesticksRequest,
152    ) -> Result<Vec<Candle>, Error> {
153        self.client.get(HISTORY_CANDLES, request, false).await
154    }
155
156    /// Retrieve index candlestick data.
157    ///
158    /// `GET /api/v5/market/index-candles`. Public.
159    ///
160    /// # Errors
161    ///
162    /// See [`get_ticker`](Self::get_ticker).
163    pub async fn get_index_candlesticks(
164        &self,
165        request: &CandlesticksRequest,
166    ) -> Result<Vec<Candle>, Error> {
167        self.client.get(INDEX_CANDLES, request, false).await
168    }
169
170    /// Retrieve mark-price candlestick data.
171    ///
172    /// `GET /api/v5/market/mark-price-candles`. Public.
173    ///
174    /// # Errors
175    ///
176    /// See [`get_ticker`](Self::get_ticker).
177    pub async fn get_mark_price_candlesticks(
178        &self,
179        request: &CandlesticksRequest,
180    ) -> Result<Vec<Candle>, Error> {
181        self.client.get(MARK_PRICE_CANDLES, request, false).await
182    }
183
184    /// Retrieve recent trades for an instrument.
185    ///
186    /// `GET /api/v5/market/trades`. Public.
187    ///
188    /// # Errors
189    ///
190    /// See [`get_ticker`](Self::get_ticker).
191    pub async fn get_trades(
192        &self,
193        inst_id: &str,
194        limit: Option<u32>,
195    ) -> Result<Vec<MarketTrade>, Error> {
196        let query = TradesQuery { inst_id, limit };
197        self.client.get(TRADES, &query, false).await
198    }
199
200    /// Retrieve historical trades for an instrument.
201    ///
202    /// `GET /api/v5/market/history-trades`. Public.
203    ///
204    /// # Errors
205    ///
206    /// See [`get_ticker`](Self::get_ticker).
207    pub async fn get_history_trades(
208        &self,
209        request: &HistoryTradesRequest,
210    ) -> Result<Vec<MarketTrade>, Error> {
211        self.client.get(HISTORY_TRADES, request, false).await
212    }
213
214    /// Retrieve OKX platform 24-hour volume.
215    ///
216    /// `GET /api/v5/market/platform-24-volume`. Public.
217    ///
218    /// # Errors
219    ///
220    /// See [`get_ticker`](Self::get_ticker).
221    pub async fn get_platform_24_volume(&self) -> Result<Vec<PlatformVolume>, Error> {
222        self.client.get(PLATFORM_24_VOLUME, &NoQuery, false).await
223    }
224
225    /// Retrieve index components.
226    ///
227    /// `GET /api/v5/market/index-components`. Public.
228    ///
229    /// # Errors
230    ///
231    /// See [`get_ticker`](Self::get_ticker).
232    pub async fn get_index_components(&self, index: &str) -> Result<Vec<IndexComponents>, Error> {
233        let query = IndexComponentsQuery { index };
234        self.client.get(INDEX_COMPONENTS, &query, false).await
235    }
236
237    /// Retrieve the USD/CNY exchange rate used by OKX.
238    ///
239    /// `GET /api/v5/market/exchange-rate`. Public.
240    ///
241    /// # Errors
242    ///
243    /// See [`get_ticker`](Self::get_ticker).
244    pub async fn get_exchange_rate(&self) -> Result<Vec<ExchangeRate>, Error> {
245        self.client.get(EXCHANGE_RATE, &NoQuery, false).await
246    }
247}
248
249#[derive(Serialize)]
250struct NoQuery;
251
252#[derive(Serialize)]
253struct InstIdQuery<'a> {
254    #[serde(rename = "instId")]
255    inst_id: &'a str,
256}
257
258#[derive(Serialize)]
259struct TickersQuery<'a> {
260    #[serde(rename = "instType")]
261    inst_type: &'a InstType,
262    #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
263    underlying: Option<&'a str>,
264    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
265    inst_family: Option<&'a str>,
266}
267
268#[derive(Serialize)]
269struct IndexTickersQuery<'a> {
270    #[serde(rename = "quoteCcy", skip_serializing_if = "Option::is_none")]
271    quote_ccy: Option<&'a str>,
272    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
273    inst_id: Option<&'a str>,
274}
275
276#[derive(Serialize)]
277struct OrderBookQuery<'a> {
278    #[serde(rename = "instId")]
279    inst_id: &'a str,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    sz: Option<u32>,
282}
283
284#[derive(Serialize)]
285struct CandlesQuery<'a> {
286    #[serde(rename = "instId")]
287    inst_id: &'a str,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    bar: Option<&'a str>,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    limit: Option<u32>,
292}
293
294/// Query parameters for historical/index/mark-price candlestick endpoints.
295#[derive(Debug, Clone, Serialize)]
296pub struct CandlesticksRequest {
297    #[serde(rename = "instId")]
298    inst_id: String,
299    #[serde(skip_serializing_if = "Option::is_none")]
300    after: Option<String>,
301    #[serde(skip_serializing_if = "Option::is_none")]
302    before: Option<String>,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    bar: Option<String>,
305    #[serde(skip_serializing_if = "Option::is_none")]
306    limit: Option<u32>,
307}
308
309impl CandlesticksRequest {
310    /// Create a candlestick query for an instrument.
311    pub fn new(inst_id: impl Into<String>) -> Self {
312        Self {
313            inst_id: inst_id.into(),
314            after: None,
315            before: None,
316            bar: None,
317            limit: None,
318        }
319    }
320
321    /// Return records after this pagination cursor.
322    pub fn after(mut self, after: impl Into<String>) -> Self {
323        self.after = Some(after.into());
324        self
325    }
326
327    /// Return records before this pagination cursor.
328    pub fn before(mut self, before: impl Into<String>) -> Self {
329        self.before = Some(before.into());
330        self
331    }
332
333    /// Set the bar size, e.g. `1m`, `1H`, or `1D`.
334    pub fn bar(mut self, bar: impl Into<String>) -> Self {
335        self.bar = Some(bar.into());
336        self
337    }
338
339    /// Set the maximum number of rows to return.
340    pub fn limit(mut self, limit: u32) -> Self {
341        self.limit = Some(limit);
342        self
343    }
344}
345
346#[derive(Serialize)]
347struct TradesQuery<'a> {
348    #[serde(rename = "instId")]
349    inst_id: &'a str,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    limit: Option<u32>,
352}
353
354/// Query parameters for historical trades.
355#[derive(Debug, Clone, Serialize)]
356pub struct HistoryTradesRequest {
357    #[serde(rename = "instId")]
358    inst_id: String,
359    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
360    trade_type: Option<String>,
361    #[serde(skip_serializing_if = "Option::is_none")]
362    after: Option<String>,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    before: Option<String>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    limit: Option<u32>,
367}
368
369impl HistoryTradesRequest {
370    /// Create a historical trades query for an instrument.
371    pub fn new(inst_id: impl Into<String>) -> Self {
372        Self {
373            inst_id: inst_id.into(),
374            trade_type: None,
375            after: None,
376            before: None,
377            limit: None,
378        }
379    }
380
381    /// Set the OKX trade type filter.
382    pub fn trade_type(mut self, trade_type: impl Into<String>) -> Self {
383        self.trade_type = Some(trade_type.into());
384        self
385    }
386
387    /// Return records after this pagination cursor.
388    pub fn after(mut self, after: impl Into<String>) -> Self {
389        self.after = Some(after.into());
390        self
391    }
392
393    /// Return records before this pagination cursor.
394    pub fn before(mut self, before: impl Into<String>) -> Self {
395        self.before = Some(before.into());
396        self
397    }
398
399    /// Set the maximum number of rows to return.
400    pub fn limit(mut self, limit: u32) -> Self {
401        self.limit = Some(limit);
402        self
403    }
404}
405
406#[derive(Serialize)]
407struct IndexComponentsQuery<'a> {
408    index: &'a str,
409}
410
411/// The latest ticker snapshot for an instrument.
412#[derive(Debug, Clone, Deserialize)]
413#[serde(rename_all = "camelCase")]
414#[non_exhaustive]
415pub struct Ticker {
416    /// Instrument type.
417    pub inst_type: InstType,
418    /// Instrument ID, e.g. `BTC-USDT`.
419    pub inst_id: String,
420    /// Last traded price.
421    pub last: NumberString,
422    /// Last traded size.
423    #[serde(default)]
424    pub last_sz: NumberString,
425    /// Best ask price.
426    #[serde(default)]
427    pub ask_px: NumberString,
428    /// Best ask size.
429    #[serde(default)]
430    pub ask_sz: NumberString,
431    /// Best bid price.
432    #[serde(default)]
433    pub bid_px: NumberString,
434    /// Best bid size.
435    #[serde(default)]
436    pub bid_sz: NumberString,
437    /// Open price over the last 24 hours.
438    #[serde(default)]
439    pub open24h: NumberString,
440    /// Highest price over the last 24 hours.
441    #[serde(default)]
442    pub high24h: NumberString,
443    /// Lowest price over the last 24 hours.
444    #[serde(default)]
445    pub low24h: NumberString,
446    /// Trading volume (base ccy) over the last 24 hours.
447    #[serde(default)]
448    pub vol24h: NumberString,
449    /// Trading volume (quote ccy) over the last 24 hours.
450    #[serde(default)]
451    pub vol_ccy24h: NumberString,
452    /// Ticker timestamp (Unix milliseconds).
453    pub ts: NumberString,
454}
455
456/// The latest index ticker snapshot.
457#[derive(Debug, Clone, Deserialize)]
458#[serde(rename_all = "camelCase")]
459#[non_exhaustive]
460pub struct IndexTicker {
461    /// Index instrument ID, e.g. `BTC-USD`.
462    pub inst_id: String,
463    /// Index price.
464    #[serde(default)]
465    pub idx_px: NumberString,
466    /// Open price over the last 24 hours.
467    #[serde(default)]
468    pub open24h: NumberString,
469    /// Highest price over the last 24 hours.
470    #[serde(default)]
471    pub high24h: NumberString,
472    /// Lowest price over the last 24 hours.
473    #[serde(default)]
474    pub low24h: NumberString,
475    /// Timestamp (Unix milliseconds).
476    #[serde(default)]
477    pub ts: NumberString,
478}
479
480/// An order book snapshot.
481#[derive(Debug, Clone, Deserialize)]
482#[non_exhaustive]
483pub struct OrderBook {
484    /// Ask levels, sorted from best (lowest) price.
485    pub asks: Vec<OrderBookLevel>,
486    /// Bid levels, sorted from best (highest) price.
487    pub bids: Vec<OrderBookLevel>,
488    /// Snapshot timestamp (Unix milliseconds).
489    pub ts: NumberString,
490}
491
492/// A single price level in an [`OrderBook`].
493///
494/// On the wire OKX encodes a level as the array
495/// `[price, size, deprecated, order_count]`.
496#[derive(Debug, Clone, Deserialize)]
497#[serde(from = "BookLevelRaw")]
498#[non_exhaustive]
499pub struct OrderBookLevel {
500    /// Price at this level.
501    pub price: NumberString,
502    /// Aggregated size available at this level.
503    pub size: NumberString,
504    /// Deprecated field (always `0`), retained for wire compatibility.
505    pub deprecated: NumberString,
506    /// Number of orders aggregated at this level.
507    pub order_count: NumberString,
508}
509
510type BookLevelRaw = (NumberString, NumberString, NumberString, NumberString);
511
512impl From<BookLevelRaw> for OrderBookLevel {
513    fn from(raw: BookLevelRaw) -> Self {
514        Self {
515            price: raw.0,
516            size: raw.1,
517            deprecated: raw.2,
518            order_count: raw.3,
519        }
520    }
521}
522
523/// A single candlestick (OHLCV) bar.
524///
525/// On the wire OKX encodes a bar as a 9-element string array.
526#[derive(Debug, Clone, Deserialize)]
527#[serde(from = "CandleRaw")]
528#[non_exhaustive]
529pub struct Candle {
530    /// Opening timestamp (Unix milliseconds).
531    pub ts: NumberString,
532    /// Open price.
533    pub open: NumberString,
534    /// Highest price.
535    pub high: NumberString,
536    /// Lowest price.
537    pub low: NumberString,
538    /// Close price.
539    pub close: NumberString,
540    /// Trading volume in contracts / base currency.
541    pub vol: NumberString,
542    /// Trading volume in quote currency.
543    pub vol_ccy: NumberString,
544    /// Trading volume in quote currency (alternate calculation).
545    pub vol_ccy_quote: NumberString,
546    /// `1` if the bar is closed/confirmed, `0` otherwise.
547    pub confirm: NumberString,
548}
549
550type CandleRaw = (
551    NumberString,
552    NumberString,
553    NumberString,
554    NumberString,
555    NumberString,
556    NumberString,
557    NumberString,
558    NumberString,
559    NumberString,
560);
561
562impl From<CandleRaw> for Candle {
563    fn from(raw: CandleRaw) -> Self {
564        Self {
565            ts: raw.0,
566            open: raw.1,
567            high: raw.2,
568            low: raw.3,
569            close: raw.4,
570            vol: raw.5,
571            vol_ccy: raw.6,
572            vol_ccy_quote: raw.7,
573            confirm: raw.8,
574        }
575    }
576}
577
578/// A public trade record.
579#[derive(Debug, Clone, Deserialize)]
580#[serde(rename_all = "camelCase")]
581#[non_exhaustive]
582pub struct MarketTrade {
583    /// Instrument ID.
584    pub inst_id: String,
585    /// Trade ID.
586    #[serde(default)]
587    pub trade_id: String,
588    /// Trade price.
589    #[serde(default)]
590    pub px: NumberString,
591    /// Trade size.
592    #[serde(default)]
593    pub sz: NumberString,
594    /// Trade side (`buy` or `sell`).
595    #[serde(default)]
596    pub side: String,
597    /// Trade timestamp (Unix milliseconds).
598    #[serde(default)]
599    pub ts: NumberString,
600}
601
602/// OKX platform 24-hour trading volume.
603#[derive(Debug, Clone, Deserialize)]
604#[serde(rename_all = "camelCase")]
605#[non_exhaustive]
606pub struct PlatformVolume {
607    /// 24-hour volume in USD.
608    #[serde(default)]
609    pub vol_usd: NumberString,
610    /// 24-hour volume in CNY.
611    #[serde(default)]
612    pub vol_cny: NumberString,
613}
614
615/// The components that make up an OKX index.
616#[derive(Debug, Clone, Deserialize)]
617#[serde(rename_all = "camelCase")]
618#[non_exhaustive]
619pub struct IndexComponents {
620    /// Index name.
621    #[serde(default)]
622    pub index: String,
623    /// Component list.
624    #[serde(default)]
625    pub components: Vec<IndexComponent>,
626    /// Timestamp (Unix milliseconds).
627    #[serde(default)]
628    pub ts: NumberString,
629}
630
631/// A single index component.
632#[derive(Debug, Clone, Deserialize)]
633#[serde(rename_all = "camelCase")]
634#[non_exhaustive]
635pub struct IndexComponent {
636    /// Exchange name.
637    #[serde(default)]
638    pub exch: String,
639    /// Symbol used by the component exchange.
640    #[serde(default)]
641    pub symbol: String,
642    /// Component price.
643    #[serde(default)]
644    pub sym_px: NumberString,
645    /// Component weight.
646    #[serde(default)]
647    pub wgt: NumberString,
648    /// Conversion price.
649    #[serde(default)]
650    pub cnv_px: NumberString,
651}
652
653/// The USD/CNY exchange rate.
654#[derive(Debug, Clone, Deserialize)]
655#[serde(rename_all = "camelCase")]
656#[non_exhaustive]
657pub struct ExchangeRate {
658    /// USD/CNY rate.
659    #[serde(default)]
660    pub usd_cny: NumberString,
661}
662
663#[cfg(test)]
664mod tests {
665    use crate::OkxClient;
666    use crate::test_util::MockTransport;
667
668    #[tokio::test]
669    async fn get_ticker_builds_request_and_parses() {
670        let body = r#"{"code":"0","msg":"","data":[
671            {"instType":"SPOT","instId":"BTC-USDT","last":"42000.1","lastSz":"0.1",
672             "askPx":"42000.2","askSz":"1","bidPx":"42000.0","bidSz":"2",
673             "open24h":"41000","high24h":"43000","low24h":"40000","vol24h":"1000",
674             "volCcy24h":"42000000","ts":"1597026383085"}]}"#;
675        let mock = MockTransport::new(body);
676        let client = OkxClient::with_transport(mock.clone()).build();
677
678        let tickers = client.market().get_ticker("BTC-USDT").await.unwrap();
679        assert_eq!(tickers[0].inst_id, "BTC-USDT");
680        assert_eq!(tickers[0].last.as_str(), "42000.1");
681        assert_eq!(tickers[0].bid_px.parse::<f64>().unwrap(), 42000.0);
682
683        let req = mock.captured();
684        assert_eq!(req.method, http::Method::GET);
685        assert_eq!(req.query(), Some("instId=BTC-USDT"));
686        assert!(!req.is_signed());
687    }
688
689    #[tokio::test]
690    async fn get_tickers_builds_request_and_parses() {
691        let body = r#"{"code":"0","msg":"","data":[
692            {"instType":"SWAP","instId":"BTC-USDT-SWAP","last":"42000.1","ts":"1597026383085"}]}"#;
693        let mock = MockTransport::new(body);
694        let client = OkxClient::with_transport(mock.clone()).build();
695
696        let tickers = client
697            .market()
698            .get_tickers(crate::model::InstType::Swap, None, Some("BTC-USDT"))
699            .await
700            .unwrap();
701        assert_eq!(tickers[0].inst_id, "BTC-USDT-SWAP");
702
703        let req = mock.captured();
704        assert_eq!(req.method, http::Method::GET);
705        assert_eq!(req.query(), Some("instType=SWAP&instFamily=BTC-USDT"));
706        assert!(!req.is_signed());
707    }
708
709    #[tokio::test]
710    async fn get_index_tickers_builds_request_and_parses() {
711        let body = r#"{"code":"0","msg":"","data":[
712            {"instId":"BTC-USD","idxPx":"42000.1","open24h":"41000","high24h":"43000","low24h":"40000","ts":"1597026383085"}]}"#;
713        let mock = MockTransport::new(body);
714        let client = OkxClient::with_transport(mock.clone()).build();
715
716        let tickers = client
717            .market()
718            .get_index_tickers(Some("USD"), None)
719            .await
720            .unwrap();
721        assert_eq!(tickers[0].inst_id, "BTC-USD");
722        assert_eq!(tickers[0].idx_px.as_str(), "42000.1");
723
724        let req = mock.captured();
725        assert_eq!(req.query(), Some("quoteCcy=USD"));
726    }
727
728    #[tokio::test]
729    async fn get_orderbook_parses_levels_and_passes_depth() {
730        let body = r#"{"code":"0","msg":"","data":[
731            {"asks":[["42000.2","1","0","3"]],"bids":[["42000.0","2","0","1"]],
732             "ts":"1597026383085"}]}"#;
733        let mock = MockTransport::new(body);
734        let client = OkxClient::with_transport(mock.clone()).build();
735
736        let books = client
737            .market()
738            .get_orderbook("BTC-USDT", Some(5))
739            .await
740            .unwrap();
741        let book = &books[0];
742        assert_eq!(book.asks[0].price.as_str(), "42000.2");
743        assert_eq!(book.asks[0].order_count.as_str(), "3");
744        assert_eq!(book.bids[0].size.as_str(), "2");
745
746        let req = mock.captured();
747        assert_eq!(req.query(), Some("instId=BTC-USDT&sz=5"));
748    }
749
750    #[tokio::test]
751    async fn get_order_lite_book_queries_instrument() {
752        let body = r#"{"code":"0","msg":"","data":[
753            {"asks":[["42000.2","1","0","3"]],"bids":[["42000.0","2","0","1"]],
754             "ts":"1597026383085"}]}"#;
755        let mock = MockTransport::new(body);
756        let client = OkxClient::with_transport(mock.clone()).build();
757
758        let books = client
759            .market()
760            .get_order_lite_book("BTC-USDT")
761            .await
762            .unwrap();
763        assert_eq!(books[0].asks[0].price.as_str(), "42000.2");
764
765        let req = mock.captured();
766        assert!(
767            req.uri
768                .ends_with("/api/v5/market/books-lite?instId=BTC-USDT")
769        );
770    }
771
772    #[tokio::test]
773    async fn get_candlesticks_parses_array_rows() {
774        let body = r#"{"code":"0","msg":"","data":[
775            ["1597026383085","42000","43000","41000","42500","100","4250000","4250000","1"]]}"#;
776        let mock = MockTransport::new(body);
777        let client = OkxClient::with_transport(mock.clone()).build();
778
779        let candles = client
780            .market()
781            .get_candlesticks("BTC-USDT", Some("1H"), Some(1))
782            .await
783            .unwrap();
784        assert_eq!(candles[0].open.as_str(), "42000");
785        assert_eq!(candles[0].close.as_str(), "42500");
786        assert_eq!(candles[0].confirm.as_str(), "1");
787
788        let req = mock.captured();
789        assert_eq!(req.query(), Some("instId=BTC-USDT&bar=1H&limit=1"));
790    }
791
792    #[tokio::test]
793    async fn get_history_candlesticks_uses_builder_query() {
794        let body = r#"{"code":"0","msg":"","data":[
795            ["1597026383085","42000","43000","41000","42500","100","4250000","4250000","1"]]}"#;
796        let mock = MockTransport::new(body);
797        let client = OkxClient::with_transport(mock.clone()).build();
798        let request = super::CandlesticksRequest::new("BTC-USDT")
799            .bar("1H")
800            .limit(2);
801
802        let candles = client
803            .market()
804            .get_history_candlesticks(&request)
805            .await
806            .unwrap();
807        assert_eq!(candles[0].high.as_str(), "43000");
808
809        let req = mock.captured();
810        assert_eq!(req.query(), Some("instId=BTC-USDT&bar=1H&limit=2"));
811        assert!(!req.query().unwrap().contains("after"));
812    }
813
814    #[tokio::test]
815    async fn get_index_candlesticks_uses_builder_query() {
816        let body = r#"{"code":"0","msg":"","data":[
817            ["1597026383085","42000","43000","41000","42500","100","4250000","4250000","1"]]}"#;
818        let mock = MockTransport::new(body);
819        let client = OkxClient::with_transport(mock.clone()).build();
820        let request = super::CandlesticksRequest::new("BTC-USD").after("10");
821
822        let candles = client
823            .market()
824            .get_index_candlesticks(&request)
825            .await
826            .unwrap();
827        assert_eq!(candles[0].low.as_str(), "41000");
828
829        let req = mock.captured();
830        assert_eq!(req.query(), Some("instId=BTC-USD&after=10"));
831    }
832
833    #[tokio::test]
834    async fn get_mark_price_candlesticks_uses_builder_query() {
835        let body = r#"{"code":"0","msg":"","data":[
836            ["1597026383085","42000","43000","41000","42500","100","4250000","4250000","1"]]}"#;
837        let mock = MockTransport::new(body);
838        let client = OkxClient::with_transport(mock.clone()).build();
839        let request = super::CandlesticksRequest::new("BTC-USDT-SWAP").before("20");
840
841        let candles = client
842            .market()
843            .get_mark_price_candlesticks(&request)
844            .await
845            .unwrap();
846        assert_eq!(candles[0].close.as_str(), "42500");
847
848        let req = mock.captured();
849        assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP&before=20"));
850    }
851
852    #[tokio::test]
853    async fn get_trades_builds_request_and_parses() {
854        let body = r#"{"code":"0","msg":"","data":[
855            {"instId":"BTC-USDT","tradeId":"1","px":"42000.1","sz":"0.02","side":"buy","ts":"1597026383085"}]}"#;
856        let mock = MockTransport::new(body);
857        let client = OkxClient::with_transport(mock.clone()).build();
858
859        let trades = client
860            .market()
861            .get_trades("BTC-USDT", Some(1))
862            .await
863            .unwrap();
864        assert_eq!(trades[0].trade_id, "1");
865        assert_eq!(trades[0].px.as_str(), "42000.1");
866
867        let req = mock.captured();
868        assert_eq!(req.query(), Some("instId=BTC-USDT&limit=1"));
869    }
870
871    #[tokio::test]
872    async fn get_history_trades_uses_builder_query() {
873        let body = r#"{"code":"0","msg":"","data":[
874            {"instId":"BTC-USDT","tradeId":"1","px":"42000.1","sz":"0.02","side":"sell","ts":"1597026383085"}]}"#;
875        let mock = MockTransport::new(body);
876        let client = OkxClient::with_transport(mock.clone()).build();
877        let request = super::HistoryTradesRequest::new("BTC-USDT")
878            .trade_type("1")
879            .before("100")
880            .limit(1);
881
882        let trades = client.market().get_history_trades(&request).await.unwrap();
883        assert_eq!(trades[0].side, "sell");
884
885        let req = mock.captured();
886        assert_eq!(
887            req.query(),
888            Some("instId=BTC-USDT&type=1&before=100&limit=1")
889        );
890        assert!(!req.query().unwrap().contains("after"));
891    }
892
893    #[tokio::test]
894    async fn get_platform_24_volume_parses_volume() {
895        let body = r#"{"code":"0","msg":"","data":[{"volUsd":"1000","volCny":"7100"}]}"#;
896        let mock = MockTransport::new(body);
897        let client = OkxClient::with_transport(mock.clone()).build();
898
899        let volume = client.market().get_platform_24_volume().await.unwrap();
900        assert_eq!(volume[0].vol_usd.as_str(), "1000");
901
902        let req = mock.captured();
903        assert!(req.uri.ends_with("/api/v5/market/platform-24-volume"));
904        assert_eq!(req.query(), None);
905    }
906
907    #[tokio::test]
908    async fn get_index_components_builds_request_and_parses() {
909        let body = r#"{"code":"0","msg":"","data":[
910            {"index":"BTC-USD","ts":"1597026383085","components":[
911                {"exch":"okx","symbol":"BTC-USDT","symPx":"42000","wgt":"1","cnvPx":"42000"}]}]}"#;
912        let mock = MockTransport::new(body);
913        let client = OkxClient::with_transport(mock.clone()).build();
914
915        let components = client
916            .market()
917            .get_index_components("BTC-USD")
918            .await
919            .unwrap();
920        assert_eq!(components[0].components[0].symbol, "BTC-USDT");
921
922        let req = mock.captured();
923        assert_eq!(req.query(), Some("index=BTC-USD"));
924    }
925
926    #[tokio::test]
927    async fn get_exchange_rate_parses_rate() {
928        let body = r#"{"code":"0","msg":"","data":[{"usdCny":"7.1"}]}"#;
929        let mock = MockTransport::new(body);
930        let client = OkxClient::with_transport(mock.clone()).build();
931
932        let rate = client.market().get_exchange_rate().await.unwrap();
933        assert_eq!(rate[0].usd_cny.as_str(), "7.1");
934
935        let req = mock.captured();
936        assert!(req.uri.ends_with("/api/v5/market/exchange-rate"));
937        assert_eq!(req.query(), None);
938    }
939}