1use 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
25pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[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 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 pub fn after(mut self, after: impl Into<String>) -> Self {
326 self.after = Some(after.into());
327 self
328 }
329
330 pub fn before(mut self, before: impl Into<String>) -> Self {
332 self.before = Some(before.into());
333 self
334 }
335
336 pub fn bar(mut self, bar: impl Into<String>) -> Self {
338 self.bar = Some(bar.into());
339 self
340 }
341
342 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#[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 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 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 pub fn after(mut self, after: impl Into<String>) -> Self {
392 self.after = Some(after.into());
393 self
394 }
395
396 pub fn before(mut self, before: impl Into<String>) -> Self {
398 self.before = Some(before.into());
399 self
400 }
401
402 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#[derive(Debug, Clone, Deserialize)]
416#[serde(rename_all = "camelCase")]
417#[non_exhaustive]
418pub struct Ticker {
419 pub inst_type: InstType,
421 pub inst_id: String,
423 pub last: NumberString,
425 #[serde(default)]
427 pub last_sz: NumberString,
428 #[serde(default)]
430 pub ask_px: NumberString,
431 #[serde(default)]
433 pub ask_sz: NumberString,
434 #[serde(default)]
436 pub bid_px: NumberString,
437 #[serde(default)]
439 pub bid_sz: NumberString,
440 #[serde(default)]
442 pub open24h: NumberString,
443 #[serde(default)]
445 pub high24h: NumberString,
446 #[serde(default)]
448 pub low24h: NumberString,
449 #[serde(default)]
451 pub vol24h: NumberString,
452 #[serde(default)]
454 pub vol_ccy24h: NumberString,
455 pub ts: NumberString,
457}
458
459#[derive(Debug, Clone, Deserialize)]
461#[serde(rename_all = "camelCase")]
462#[non_exhaustive]
463pub struct IndexTicker {
464 pub inst_id: String,
466 #[serde(default)]
468 pub idx_px: NumberString,
469 #[serde(default)]
471 pub open24h: NumberString,
472 #[serde(default)]
474 pub high24h: NumberString,
475 #[serde(default)]
477 pub low24h: NumberString,
478 #[serde(default)]
480 pub ts: NumberString,
481}
482
483#[derive(Debug, Clone, Deserialize)]
485#[non_exhaustive]
486pub struct OrderBook {
487 pub asks: Vec<OrderBookLevel>,
489 pub bids: Vec<OrderBookLevel>,
491 pub ts: NumberString,
493}
494
495#[derive(Debug, Clone, Deserialize)]
500#[serde(from = "BookLevelRaw")]
501#[non_exhaustive]
502pub struct OrderBookLevel {
503 pub price: NumberString,
505 pub size: NumberString,
507 pub deprecated: NumberString,
509 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#[derive(Debug, Clone, Deserialize)]
530#[serde(from = "CandleRaw")]
531#[non_exhaustive]
532pub struct Candle {
533 pub ts: NumberString,
535 pub open: NumberString,
537 pub high: NumberString,
539 pub low: NumberString,
541 pub close: NumberString,
543 pub vol: NumberString,
545 pub vol_ccy: NumberString,
547 pub vol_ccy_quote: NumberString,
549 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#[derive(Debug, Clone, Deserialize)]
583#[serde(rename_all = "camelCase")]
584#[non_exhaustive]
585pub struct MarketTrade {
586 pub inst_id: String,
588 #[serde(default)]
590 pub trade_id: String,
591 #[serde(default)]
593 pub px: NumberString,
594 #[serde(default)]
596 pub sz: NumberString,
597 #[serde(default)]
599 pub side: String,
600 #[serde(default)]
602 pub ts: NumberString,
603}
604
605#[derive(Debug, Clone, Deserialize)]
607#[serde(rename_all = "camelCase")]
608#[non_exhaustive]
609pub struct PlatformVolume {
610 #[serde(default)]
612 pub vol_usd: NumberString,
613 #[serde(default)]
615 pub vol_cny: NumberString,
616}
617
618#[derive(Debug, Clone, Deserialize)]
620#[serde(rename_all = "camelCase")]
621#[non_exhaustive]
622pub struct IndexComponents {
623 #[serde(default)]
625 pub index: String,
626 #[serde(default)]
628 pub components: Vec<IndexComponent>,
629 #[serde(default)]
631 pub ts: NumberString,
632}
633
634#[derive(Debug, Clone, Deserialize)]
636#[serde(rename_all = "camelCase")]
637#[non_exhaustive]
638pub struct IndexComponent {
639 #[serde(default)]
641 pub exch: String,
642 #[serde(default)]
644 pub symbol: String,
645 #[serde(default)]
647 pub sym_px: NumberString,
648 #[serde(default)]
650 pub wgt: NumberString,
651 #[serde(default)]
653 pub cnv_px: NumberString,
654}
655
656#[derive(Debug, Clone, Deserialize)]
658#[serde(rename_all = "camelCase")]
659#[non_exhaustive]
660pub struct ExchangeRate {
661 #[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}