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(&self, index: &str) -> Result<Vec<IndexComponents>, Error> {
233 let query = IndexComponentsQuery { index };
234 self.client.get(INDEX_COMPONENTS, &query, false).await
235 }
236
237 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#[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 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 pub fn after(mut self, after: impl Into<String>) -> Self {
323 self.after = Some(after.into());
324 self
325 }
326
327 pub fn before(mut self, before: impl Into<String>) -> Self {
329 self.before = Some(before.into());
330 self
331 }
332
333 pub fn bar(mut self, bar: impl Into<String>) -> Self {
335 self.bar = Some(bar.into());
336 self
337 }
338
339 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#[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 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 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 pub fn after(mut self, after: impl Into<String>) -> Self {
389 self.after = Some(after.into());
390 self
391 }
392
393 pub fn before(mut self, before: impl Into<String>) -> Self {
395 self.before = Some(before.into());
396 self
397 }
398
399 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#[derive(Debug, Clone, Deserialize)]
413#[serde(rename_all = "camelCase")]
414#[non_exhaustive]
415pub struct Ticker {
416 pub inst_type: InstType,
418 pub inst_id: String,
420 pub last: NumberString,
422 #[serde(default)]
424 pub last_sz: NumberString,
425 #[serde(default)]
427 pub ask_px: NumberString,
428 #[serde(default)]
430 pub ask_sz: NumberString,
431 #[serde(default)]
433 pub bid_px: NumberString,
434 #[serde(default)]
436 pub bid_sz: NumberString,
437 #[serde(default)]
439 pub open24h: NumberString,
440 #[serde(default)]
442 pub high24h: NumberString,
443 #[serde(default)]
445 pub low24h: NumberString,
446 #[serde(default)]
448 pub vol24h: NumberString,
449 #[serde(default)]
451 pub vol_ccy24h: NumberString,
452 pub ts: NumberString,
454}
455
456#[derive(Debug, Clone, Deserialize)]
458#[serde(rename_all = "camelCase")]
459#[non_exhaustive]
460pub struct IndexTicker {
461 pub inst_id: String,
463 #[serde(default)]
465 pub idx_px: NumberString,
466 #[serde(default)]
468 pub open24h: NumberString,
469 #[serde(default)]
471 pub high24h: NumberString,
472 #[serde(default)]
474 pub low24h: NumberString,
475 #[serde(default)]
477 pub ts: NumberString,
478}
479
480#[derive(Debug, Clone, Deserialize)]
482#[non_exhaustive]
483pub struct OrderBook {
484 pub asks: Vec<OrderBookLevel>,
486 pub bids: Vec<OrderBookLevel>,
488 pub ts: NumberString,
490}
491
492#[derive(Debug, Clone, Deserialize)]
497#[serde(from = "BookLevelRaw")]
498#[non_exhaustive]
499pub struct OrderBookLevel {
500 pub price: NumberString,
502 pub size: NumberString,
504 pub deprecated: NumberString,
506 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#[derive(Debug, Clone, Deserialize)]
527#[serde(from = "CandleRaw")]
528#[non_exhaustive]
529pub struct Candle {
530 pub ts: NumberString,
532 pub open: NumberString,
534 pub high: NumberString,
536 pub low: NumberString,
538 pub close: NumberString,
540 pub vol: NumberString,
542 pub vol_ccy: NumberString,
544 pub vol_ccy_quote: NumberString,
546 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#[derive(Debug, Clone, Deserialize)]
580#[serde(rename_all = "camelCase")]
581#[non_exhaustive]
582pub struct MarketTrade {
583 pub inst_id: String,
585 #[serde(default)]
587 pub trade_id: String,
588 #[serde(default)]
590 pub px: NumberString,
591 #[serde(default)]
593 pub sz: NumberString,
594 #[serde(default)]
596 pub side: String,
597 #[serde(default)]
599 pub ts: NumberString,
600}
601
602#[derive(Debug, Clone, Deserialize)]
604#[serde(rename_all = "camelCase")]
605#[non_exhaustive]
606pub struct PlatformVolume {
607 #[serde(default)]
609 pub vol_usd: NumberString,
610 #[serde(default)]
612 pub vol_cny: NumberString,
613}
614
615#[derive(Debug, Clone, Deserialize)]
617#[serde(rename_all = "camelCase")]
618#[non_exhaustive]
619pub struct IndexComponents {
620 #[serde(default)]
622 pub index: String,
623 #[serde(default)]
625 pub components: Vec<IndexComponent>,
626 #[serde(default)]
628 pub ts: NumberString,
629}
630
631#[derive(Debug, Clone, Deserialize)]
633#[serde(rename_all = "camelCase")]
634#[non_exhaustive]
635pub struct IndexComponent {
636 #[serde(default)]
638 pub exch: String,
639 #[serde(default)]
641 pub symbol: String,
642 #[serde(default)]
644 pub sym_px: NumberString,
645 #[serde(default)]
647 pub wgt: NumberString,
648 #[serde(default)]
650 pub cnv_px: NumberString,
651}
652
653#[derive(Debug, Clone, Deserialize)]
655#[serde(rename_all = "camelCase")]
656#[non_exhaustive]
657pub struct ExchangeRate {
658 #[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}