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(
233        &self,
234        index: &str,
235    ) -> Result<Vec<IndexComponents>, Error> {
236        let query = IndexComponentsQuery { index };
237        self.client.get(INDEX_COMPONENTS, &query, false).await
238    }
239
240    /// Retrieve the USD/CNY exchange rate used by OKX.
241    ///
242    /// `GET /api/v5/market/exchange-rate`. Public.
243    ///
244    /// # Errors
245    ///
246    /// See [`get_ticker`](Self::get_ticker).
247    pub async fn get_exchange_rate(&self) -> Result<Vec<ExchangeRate>, Error> {
248        self.client.get(EXCHANGE_RATE, &NoQuery, false).await
249    }
250}
251
252#[derive(Serialize)]
253struct NoQuery;
254
255#[derive(Serialize)]
256struct InstIdQuery<'a> {
257    #[serde(rename = "instId")]
258    inst_id: &'a str,
259}
260
261#[derive(Serialize)]
262struct TickersQuery<'a> {
263    #[serde(rename = "instType")]
264    inst_type: &'a InstType,
265    #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
266    underlying: Option<&'a str>,
267    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
268    inst_family: Option<&'a str>,
269}
270
271#[derive(Serialize)]
272struct IndexTickersQuery<'a> {
273    #[serde(rename = "quoteCcy", skip_serializing_if = "Option::is_none")]
274    quote_ccy: Option<&'a str>,
275    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
276    inst_id: Option<&'a str>,
277}
278
279#[derive(Serialize)]
280struct OrderBookQuery<'a> {
281    #[serde(rename = "instId")]
282    inst_id: &'a str,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    sz: Option<u32>,
285}
286
287#[derive(Serialize)]
288struct CandlesQuery<'a> {
289    #[serde(rename = "instId")]
290    inst_id: &'a str,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    bar: Option<&'a str>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    limit: Option<u32>,
295}
296
297/// Query parameters for historical/index/mark-price candlestick endpoints.
298#[derive(Debug, Clone, Serialize)]
299pub struct CandlesticksRequest {
300    #[serde(rename = "instId")]
301    inst_id: String,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    after: Option<String>,
304    #[serde(skip_serializing_if = "Option::is_none")]
305    before: Option<String>,
306    #[serde(skip_serializing_if = "Option::is_none")]
307    bar: Option<String>,
308    #[serde(skip_serializing_if = "Option::is_none")]
309    limit: Option<u32>,
310}
311
312impl CandlesticksRequest {
313    /// Create a candlestick query for an instrument.
314    pub fn new(inst_id: impl Into<String>) -> Self {
315        Self {
316            inst_id: inst_id.into(),
317            after: None,
318            before: None,
319            bar: None,
320            limit: None,
321        }
322    }
323
324    /// Return records after this pagination cursor.
325    pub fn after(mut self, after: impl Into<String>) -> Self {
326        self.after = Some(after.into());
327        self
328    }
329
330    /// Return records before this pagination cursor.
331    pub fn before(mut self, before: impl Into<String>) -> Self {
332        self.before = Some(before.into());
333        self
334    }
335
336    /// Set the bar size, e.g. `1m`, `1H`, or `1D`.
337    pub fn bar(mut self, bar: impl Into<String>) -> Self {
338        self.bar = Some(bar.into());
339        self
340    }
341
342    /// Set the maximum number of rows to return.
343    pub fn limit(mut self, limit: u32) -> Self {
344        self.limit = Some(limit);
345        self
346    }
347}
348
349#[derive(Serialize)]
350struct TradesQuery<'a> {
351    #[serde(rename = "instId")]
352    inst_id: &'a str,
353    #[serde(skip_serializing_if = "Option::is_none")]
354    limit: Option<u32>,
355}
356
357/// Query parameters for historical trades.
358#[derive(Debug, Clone, Serialize)]
359pub struct HistoryTradesRequest {
360    #[serde(rename = "instId")]
361    inst_id: String,
362    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
363    trade_type: Option<String>,
364    #[serde(skip_serializing_if = "Option::is_none")]
365    after: Option<String>,
366    #[serde(skip_serializing_if = "Option::is_none")]
367    before: Option<String>,
368    #[serde(skip_serializing_if = "Option::is_none")]
369    limit: Option<u32>,
370}
371
372impl HistoryTradesRequest {
373    /// Create a historical trades query for an instrument.
374    pub fn new(inst_id: impl Into<String>) -> Self {
375        Self {
376            inst_id: inst_id.into(),
377            trade_type: None,
378            after: None,
379            before: None,
380            limit: None,
381        }
382    }
383
384    /// Set the OKX trade type filter.
385    pub fn trade_type(mut self, trade_type: impl Into<String>) -> Self {
386        self.trade_type = Some(trade_type.into());
387        self
388    }
389
390    /// Return records after this pagination cursor.
391    pub fn after(mut self, after: impl Into<String>) -> Self {
392        self.after = Some(after.into());
393        self
394    }
395
396    /// Return records before this pagination cursor.
397    pub fn before(mut self, before: impl Into<String>) -> Self {
398        self.before = Some(before.into());
399        self
400    }
401
402    /// Set the maximum number of rows to return.
403    pub fn limit(mut self, limit: u32) -> Self {
404        self.limit = Some(limit);
405        self
406    }
407}
408
409#[derive(Serialize)]
410struct IndexComponentsQuery<'a> {
411    index: &'a str,
412}
413
414/// The latest ticker snapshot for an instrument.
415#[derive(Debug, Clone, Deserialize)]
416#[serde(rename_all = "camelCase")]
417#[non_exhaustive]
418pub struct Ticker {
419    /// Instrument type.
420    pub inst_type: InstType,
421    /// Instrument ID, e.g. `BTC-USDT`.
422    pub inst_id: String,
423    /// Last traded price.
424    pub last: NumberString,
425    /// Last traded size.
426    #[serde(default)]
427    pub last_sz: NumberString,
428    /// Best ask price.
429    #[serde(default)]
430    pub ask_px: NumberString,
431    /// Best ask size.
432    #[serde(default)]
433    pub ask_sz: NumberString,
434    /// Best bid price.
435    #[serde(default)]
436    pub bid_px: NumberString,
437    /// Best bid size.
438    #[serde(default)]
439    pub bid_sz: NumberString,
440    /// Open price over the last 24 hours.
441    #[serde(default)]
442    pub open24h: NumberString,
443    /// Highest price over the last 24 hours.
444    #[serde(default)]
445    pub high24h: NumberString,
446    /// Lowest price over the last 24 hours.
447    #[serde(default)]
448    pub low24h: NumberString,
449    /// Trading volume (base ccy) over the last 24 hours.
450    #[serde(default)]
451    pub vol24h: NumberString,
452    /// Trading volume (quote ccy) over the last 24 hours.
453    #[serde(default)]
454    pub vol_ccy24h: NumberString,
455    /// Ticker timestamp (Unix milliseconds).
456    pub ts: NumberString,
457}
458
459/// The latest index ticker snapshot.
460#[derive(Debug, Clone, Deserialize)]
461#[serde(rename_all = "camelCase")]
462#[non_exhaustive]
463pub struct IndexTicker {
464    /// Index instrument ID, e.g. `BTC-USD`.
465    pub inst_id: String,
466    /// Index price.
467    #[serde(default)]
468    pub idx_px: NumberString,
469    /// Open price over the last 24 hours.
470    #[serde(default)]
471    pub open24h: NumberString,
472    /// Highest price over the last 24 hours.
473    #[serde(default)]
474    pub high24h: NumberString,
475    /// Lowest price over the last 24 hours.
476    #[serde(default)]
477    pub low24h: NumberString,
478    /// Timestamp (Unix milliseconds).
479    #[serde(default)]
480    pub ts: NumberString,
481}
482
483/// An order book snapshot.
484#[derive(Debug, Clone, Deserialize)]
485#[non_exhaustive]
486pub struct OrderBook {
487    /// Ask levels, sorted from best (lowest) price.
488    pub asks: Vec<OrderBookLevel>,
489    /// Bid levels, sorted from best (highest) price.
490    pub bids: Vec<OrderBookLevel>,
491    /// Snapshot timestamp (Unix milliseconds).
492    pub ts: NumberString,
493}
494
495/// A single price level in an [`OrderBook`].
496///
497/// On the wire OKX encodes a level as the array
498/// `[price, size, deprecated, order_count]`.
499#[derive(Debug, Clone, Deserialize)]
500#[serde(from = "BookLevelRaw")]
501#[non_exhaustive]
502pub struct OrderBookLevel {
503    /// Price at this level.
504    pub price: NumberString,
505    /// Aggregated size available at this level.
506    pub size: NumberString,
507    /// Deprecated field (always `0`), retained for wire compatibility.
508    pub deprecated: NumberString,
509    /// Number of orders aggregated at this level.
510    pub order_count: NumberString,
511}
512
513type BookLevelRaw = (NumberString, NumberString, NumberString, NumberString);
514
515impl From<BookLevelRaw> for OrderBookLevel {
516    fn from(raw: BookLevelRaw) -> Self {
517        Self {
518            price: raw.0,
519            size: raw.1,
520            deprecated: raw.2,
521            order_count: raw.3,
522        }
523    }
524}
525
526/// A single candlestick (OHLCV) bar.
527///
528/// On the wire OKX encodes a bar as a 9-element string array.
529#[derive(Debug, Clone, Deserialize)]
530#[serde(from = "CandleRaw")]
531#[non_exhaustive]
532pub struct Candle {
533    /// Opening timestamp (Unix milliseconds).
534    pub ts: NumberString,
535    /// Open price.
536    pub open: NumberString,
537    /// Highest price.
538    pub high: NumberString,
539    /// Lowest price.
540    pub low: NumberString,
541    /// Close price.
542    pub close: NumberString,
543    /// Trading volume in contracts / base currency.
544    pub vol: NumberString,
545    /// Trading volume in quote currency.
546    pub vol_ccy: NumberString,
547    /// Trading volume in quote currency (alternate calculation).
548    pub vol_ccy_quote: NumberString,
549    /// `1` if the bar is closed/confirmed, `0` otherwise.
550    pub confirm: NumberString,
551}
552
553type CandleRaw = (
554    NumberString,
555    NumberString,
556    NumberString,
557    NumberString,
558    NumberString,
559    NumberString,
560    NumberString,
561    NumberString,
562    NumberString,
563);
564
565impl From<CandleRaw> for Candle {
566    fn from(raw: CandleRaw) -> Self {
567        Self {
568            ts: raw.0,
569            open: raw.1,
570            high: raw.2,
571            low: raw.3,
572            close: raw.4,
573            vol: raw.5,
574            vol_ccy: raw.6,
575            vol_ccy_quote: raw.7,
576            confirm: raw.8,
577        }
578    }
579}
580
581/// A public trade record.
582#[derive(Debug, Clone, Deserialize)]
583#[serde(rename_all = "camelCase")]
584#[non_exhaustive]
585pub struct MarketTrade {
586    /// Instrument ID.
587    pub inst_id: String,
588    /// Trade ID.
589    #[serde(default)]
590    pub trade_id: String,
591    /// Trade price.
592    #[serde(default)]
593    pub px: NumberString,
594    /// Trade size.
595    #[serde(default)]
596    pub sz: NumberString,
597    /// Trade side (`buy` or `sell`).
598    #[serde(default)]
599    pub side: String,
600    /// Trade timestamp (Unix milliseconds).
601    #[serde(default)]
602    pub ts: NumberString,
603}
604
605/// OKX platform 24-hour trading volume.
606#[derive(Debug, Clone, Deserialize)]
607#[serde(rename_all = "camelCase")]
608#[non_exhaustive]
609pub struct PlatformVolume {
610    /// 24-hour volume in USD.
611    #[serde(default)]
612    pub vol_usd: NumberString,
613    /// 24-hour volume in CNY.
614    #[serde(default)]
615    pub vol_cny: NumberString,
616}
617
618/// The components that make up an OKX index.
619#[derive(Debug, Clone, Deserialize)]
620#[serde(rename_all = "camelCase")]
621#[non_exhaustive]
622pub struct IndexComponents {
623    /// Index name.
624    #[serde(default)]
625    pub index: String,
626    /// Component list.
627    #[serde(default)]
628    pub components: Vec<IndexComponent>,
629    /// Timestamp (Unix milliseconds).
630    #[serde(default)]
631    pub ts: NumberString,
632}
633
634/// A single index component.
635#[derive(Debug, Clone, Deserialize)]
636#[serde(rename_all = "camelCase")]
637#[non_exhaustive]
638pub struct IndexComponent {
639    /// Exchange name.
640    #[serde(default)]
641    pub exch: String,
642    /// Symbol used by the component exchange.
643    #[serde(default)]
644    pub symbol: String,
645    /// Component price.
646    #[serde(default)]
647    pub sym_px: NumberString,
648    /// Component weight.
649    #[serde(default)]
650    pub wgt: NumberString,
651    /// Conversion price.
652    #[serde(default)]
653    pub cnv_px: NumberString,
654}
655
656/// The USD/CNY exchange rate.
657#[derive(Debug, Clone, Deserialize)]
658#[serde(rename_all = "camelCase")]
659#[non_exhaustive]
660pub struct ExchangeRate {
661    /// USD/CNY rate.
662    #[serde(default)]
663    pub usd_cny: NumberString,
664}
665
666#[cfg(test)]
667mod tests {
668    use crate::OkxClient;
669    use crate::test_util::MockTransport;
670
671    #[tokio::test]
672    async fn get_ticker_builds_request_and_parses() {
673        let body = r#"{"code":"0","msg":"","data":[
674            {"instType":"SPOT","instId":"BTC-USDT","last":"42000.1","lastSz":"0.1",
675             "askPx":"42000.2","askSz":"1","bidPx":"42000.0","bidSz":"2",
676             "open24h":"41000","high24h":"43000","low24h":"40000","vol24h":"1000",
677             "volCcy24h":"42000000","ts":"1597026383085"}]}"#;
678        let mock = MockTransport::new(body);
679        let client = OkxClient::with_transport(mock.clone()).build();
680
681        let tickers = client.market().get_ticker("BTC-USDT").await.unwrap();
682        assert_eq!(tickers[0].inst_id, "BTC-USDT");
683        assert_eq!(tickers[0].last.as_str(), "42000.1");
684        assert_eq!(tickers[0].bid_px.parse::<f64>().unwrap(), 42000.0);
685
686        let req = mock.captured();
687        assert_eq!(req.method, http::Method::GET);
688        assert_eq!(req.query(), Some("instId=BTC-USDT"));
689        assert!(!req.is_signed());
690    }
691
692    #[tokio::test]
693    async fn get_tickers_builds_request_and_parses() {
694        let body = r#"{"code":"0","msg":"","data":[
695            {"instType":"SWAP","instId":"BTC-USDT-SWAP","last":"42000.1","ts":"1597026383085"}]}"#;
696        let mock = MockTransport::new(body);
697        let client = OkxClient::with_transport(mock.clone()).build();
698
699        let tickers = client
700            .market()
701            .get_tickers(crate::model::InstType::Swap, None, Some("BTC-USDT"))
702            .await
703            .unwrap();
704        assert_eq!(tickers[0].inst_id, "BTC-USDT-SWAP");
705
706        let req = mock.captured();
707        assert_eq!(req.method, http::Method::GET);
708        assert_eq!(req.query(), Some("instType=SWAP&instFamily=BTC-USDT"));
709        assert!(!req.is_signed());
710    }
711
712    #[tokio::test]
713    async fn get_index_tickers_builds_request_and_parses() {
714        let body = r#"{"code":"0","msg":"","data":[
715            {"instId":"BTC-USD","idxPx":"42000.1","open24h":"41000","high24h":"43000","low24h":"40000","ts":"1597026383085"}]}"#;
716        let mock = MockTransport::new(body);
717        let client = OkxClient::with_transport(mock.clone()).build();
718
719        let tickers = client
720            .market()
721            .get_index_tickers(Some("USD"), None)
722            .await
723            .unwrap();
724        assert_eq!(tickers[0].inst_id, "BTC-USD");
725        assert_eq!(tickers[0].idx_px.as_str(), "42000.1");
726
727        let req = mock.captured();
728        assert_eq!(req.query(), Some("quoteCcy=USD"));
729    }
730
731    #[tokio::test]
732    async fn get_orderbook_parses_levels_and_passes_depth() {
733        let body = r#"{"code":"0","msg":"","data":[
734            {"asks":[["42000.2","1","0","3"]],"bids":[["42000.0","2","0","1"]],
735             "ts":"1597026383085"}]}"#;
736        let mock = MockTransport::new(body);
737        let client = OkxClient::with_transport(mock.clone()).build();
738
739        let books = client
740            .market()
741            .get_orderbook("BTC-USDT", Some(5))
742            .await
743            .unwrap();
744        let book = &books[0];
745        assert_eq!(book.asks[0].price.as_str(), "42000.2");
746        assert_eq!(book.asks[0].order_count.as_str(), "3");
747        assert_eq!(book.bids[0].size.as_str(), "2");
748
749        let req = mock.captured();
750        assert_eq!(req.query(), Some("instId=BTC-USDT&sz=5"));
751    }
752
753    #[tokio::test]
754    async fn get_order_lite_book_queries_instrument() {
755        let body = r#"{"code":"0","msg":"","data":[
756            {"asks":[["42000.2","1","0","3"]],"bids":[["42000.0","2","0","1"]],
757             "ts":"1597026383085"}]}"#;
758        let mock = MockTransport::new(body);
759        let client = OkxClient::with_transport(mock.clone()).build();
760
761        let books = client.market().get_order_lite_book("BTC-USDT").await.unwrap();
762        assert_eq!(books[0].asks[0].price.as_str(), "42000.2");
763
764        let req = mock.captured();
765        assert!(req.uri.ends_with("/api/v5/market/books-lite?instId=BTC-USDT"));
766    }
767
768    #[tokio::test]
769    async fn get_candlesticks_parses_array_rows() {
770        let body = r#"{"code":"0","msg":"","data":[
771            ["1597026383085","42000","43000","41000","42500","100","4250000","4250000","1"]]}"#;
772        let mock = MockTransport::new(body);
773        let client = OkxClient::with_transport(mock.clone()).build();
774
775        let candles = client
776            .market()
777            .get_candlesticks("BTC-USDT", Some("1H"), Some(1))
778            .await
779            .unwrap();
780        assert_eq!(candles[0].open.as_str(), "42000");
781        assert_eq!(candles[0].close.as_str(), "42500");
782        assert_eq!(candles[0].confirm.as_str(), "1");
783
784        let req = mock.captured();
785        assert_eq!(req.query(), Some("instId=BTC-USDT&bar=1H&limit=1"));
786    }
787
788    #[tokio::test]
789    async fn get_history_candlesticks_uses_builder_query() {
790        let body = r#"{"code":"0","msg":"","data":[
791            ["1597026383085","42000","43000","41000","42500","100","4250000","4250000","1"]]}"#;
792        let mock = MockTransport::new(body);
793        let client = OkxClient::with_transport(mock.clone()).build();
794        let request = super::CandlesticksRequest::new("BTC-USDT")
795            .bar("1H")
796            .limit(2);
797
798        let candles = client
799            .market()
800            .get_history_candlesticks(&request)
801            .await
802            .unwrap();
803        assert_eq!(candles[0].high.as_str(), "43000");
804
805        let req = mock.captured();
806        assert_eq!(req.query(), Some("instId=BTC-USDT&bar=1H&limit=2"));
807        assert!(!req.query().unwrap().contains("after"));
808    }
809
810    #[tokio::test]
811    async fn get_index_candlesticks_uses_builder_query() {
812        let body = r#"{"code":"0","msg":"","data":[
813            ["1597026383085","42000","43000","41000","42500","100","4250000","4250000","1"]]}"#;
814        let mock = MockTransport::new(body);
815        let client = OkxClient::with_transport(mock.clone()).build();
816        let request = super::CandlesticksRequest::new("BTC-USD").after("10");
817
818        let candles = client
819            .market()
820            .get_index_candlesticks(&request)
821            .await
822            .unwrap();
823        assert_eq!(candles[0].low.as_str(), "41000");
824
825        let req = mock.captured();
826        assert_eq!(req.query(), Some("instId=BTC-USD&after=10"));
827    }
828
829    #[tokio::test]
830    async fn get_mark_price_candlesticks_uses_builder_query() {
831        let body = r#"{"code":"0","msg":"","data":[
832            ["1597026383085","42000","43000","41000","42500","100","4250000","4250000","1"]]}"#;
833        let mock = MockTransport::new(body);
834        let client = OkxClient::with_transport(mock.clone()).build();
835        let request = super::CandlesticksRequest::new("BTC-USDT-SWAP").before("20");
836
837        let candles = client
838            .market()
839            .get_mark_price_candlesticks(&request)
840            .await
841            .unwrap();
842        assert_eq!(candles[0].close.as_str(), "42500");
843
844        let req = mock.captured();
845        assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP&before=20"));
846    }
847
848    #[tokio::test]
849    async fn get_trades_builds_request_and_parses() {
850        let body = r#"{"code":"0","msg":"","data":[
851            {"instId":"BTC-USDT","tradeId":"1","px":"42000.1","sz":"0.02","side":"buy","ts":"1597026383085"}]}"#;
852        let mock = MockTransport::new(body);
853        let client = OkxClient::with_transport(mock.clone()).build();
854
855        let trades = client.market().get_trades("BTC-USDT", Some(1)).await.unwrap();
856        assert_eq!(trades[0].trade_id, "1");
857        assert_eq!(trades[0].px.as_str(), "42000.1");
858
859        let req = mock.captured();
860        assert_eq!(req.query(), Some("instId=BTC-USDT&limit=1"));
861    }
862
863    #[tokio::test]
864    async fn get_history_trades_uses_builder_query() {
865        let body = r#"{"code":"0","msg":"","data":[
866            {"instId":"BTC-USDT","tradeId":"1","px":"42000.1","sz":"0.02","side":"sell","ts":"1597026383085"}]}"#;
867        let mock = MockTransport::new(body);
868        let client = OkxClient::with_transport(mock.clone()).build();
869        let request = super::HistoryTradesRequest::new("BTC-USDT")
870            .trade_type("1")
871            .before("100")
872            .limit(1);
873
874        let trades = client.market().get_history_trades(&request).await.unwrap();
875        assert_eq!(trades[0].side, "sell");
876
877        let req = mock.captured();
878        assert_eq!(req.query(), Some("instId=BTC-USDT&type=1&before=100&limit=1"));
879        assert!(!req.query().unwrap().contains("after"));
880    }
881
882    #[tokio::test]
883    async fn get_platform_24_volume_parses_volume() {
884        let body = r#"{"code":"0","msg":"","data":[{"volUsd":"1000","volCny":"7100"}]}"#;
885        let mock = MockTransport::new(body);
886        let client = OkxClient::with_transport(mock.clone()).build();
887
888        let volume = client.market().get_platform_24_volume().await.unwrap();
889        assert_eq!(volume[0].vol_usd.as_str(), "1000");
890
891        let req = mock.captured();
892        assert!(req.uri.ends_with("/api/v5/market/platform-24-volume"));
893        assert_eq!(req.query(), None);
894    }
895
896    #[tokio::test]
897    async fn get_index_components_builds_request_and_parses() {
898        let body = r#"{"code":"0","msg":"","data":[
899            {"index":"BTC-USD","ts":"1597026383085","components":[
900                {"exch":"okx","symbol":"BTC-USDT","symPx":"42000","wgt":"1","cnvPx":"42000"}]}]}"#;
901        let mock = MockTransport::new(body);
902        let client = OkxClient::with_transport(mock.clone()).build();
903
904        let components = client
905            .market()
906            .get_index_components("BTC-USD")
907            .await
908            .unwrap();
909        assert_eq!(components[0].components[0].symbol, "BTC-USDT");
910
911        let req = mock.captured();
912        assert_eq!(req.query(), Some("index=BTC-USD"));
913    }
914
915    #[tokio::test]
916    async fn get_exchange_rate_parses_rate() {
917        let body = r#"{"code":"0","msg":"","data":[{"usdCny":"7.1"}]}"#;
918        let mock = MockTransport::new(body);
919        let client = OkxClient::with_transport(mock.clone()).build();
920
921        let rate = client.market().get_exchange_rate().await.unwrap();
922        assert_eq!(rate[0].usd_cny.as_str(), "7.1");
923
924        let req = mock.captured();
925        assert!(req.uri.ends_with("/api/v5/market/exchange-rate"));
926        assert_eq!(req.query(), None);
927    }
928}