1use serde::{Deserialize, Serialize};
4
5use crate::client::OkxClient;
6use crate::error::Error;
7use crate::model::{InstType, NumberString};
8use crate::transport::Transport;
9
10const INSTRUMENTS: &str = "/api/v5/public/instruments";
11const SYSTEM_TIME: &str = "/api/v5/public/time";
12const OPEN_INTEREST: &str = "/api/v5/public/open-interest";
13const FUNDING_RATE: &str = "/api/v5/public/funding-rate";
14const FUNDING_RATE_HISTORY: &str = "/api/v5/public/funding-rate-history";
15const PRICE_LIMIT: &str = "/api/v5/public/price-limit";
16const MARK_PRICE: &str = "/api/v5/public/mark-price";
17const DELIVERY_EXERCISE_HISTORY: &str = "/api/v5/public/delivery-exercise-history";
18const POSITION_TIERS: &str = "/api/v5/public/position-tiers";
19const UNDERLYING: &str = "/api/v5/public/underlying";
20const INSURANCE_FUND: &str = "/api/v5/public/insurance-fund";
21const CONVERT_CONTRACT_COIN: &str = "/api/v5/public/convert-contract-coin";
22
23pub struct PublicData<'a, T> {
27 client: &'a OkxClient<T>,
28}
29
30impl<'a, T: Transport> PublicData<'a, T> {
31 pub(crate) fn new(client: &'a OkxClient<T>) -> Self {
32 Self { client }
33 }
34
35 pub async fn get_instruments(
47 &self,
48 inst_type: InstType,
49 inst_family: Option<&str>,
50 ) -> Result<Vec<Instrument>, Error> {
51 let query = InstrumentsQuery {
52 inst_type: &inst_type,
53 inst_family,
54 };
55 self.client.get(INSTRUMENTS, &query, false).await
56 }
57
58 pub async fn get_system_time(&self) -> Result<Vec<SystemTime>, Error> {
67 self.client.get(SYSTEM_TIME, &NoQuery, false).await
68 }
69
70 pub async fn get_open_interest(
78 &self,
79 request: &InstrumentFamilyRequest,
80 ) -> Result<Vec<OpenInterest>, Error> {
81 self.client.get(OPEN_INTEREST, request, false).await
82 }
83
84 pub async fn get_funding_rate(&self, inst_id: &str) -> Result<Vec<FundingRate>, Error> {
92 let query = InstIdQuery { inst_id };
93 self.client.get(FUNDING_RATE, &query, false).await
94 }
95
96 pub async fn get_funding_rate_history(
104 &self,
105 request: &FundingRateHistoryRequest,
106 ) -> Result<Vec<FundingRateHistory>, Error> {
107 self.client
108 .get(FUNDING_RATE_HISTORY, request, false)
109 .await
110 }
111
112 pub async fn get_price_limit(&self, inst_id: &str) -> Result<Vec<PriceLimit>, Error> {
120 let query = InstIdQuery { inst_id };
121 self.client.get(PRICE_LIMIT, &query, false).await
122 }
123
124 pub async fn get_mark_price(
132 &self,
133 request: &InstrumentFamilyRequest,
134 ) -> Result<Vec<MarkPrice>, Error> {
135 self.client.get(MARK_PRICE, request, false).await
136 }
137
138 pub async fn get_delivery_exercise_history(
146 &self,
147 request: &DeliveryExerciseHistoryRequest,
148 ) -> Result<Vec<DeliveryExercise>, Error> {
149 self.client
150 .get(DELIVERY_EXERCISE_HISTORY, request, false)
151 .await
152 }
153
154 pub async fn get_position_tiers(
162 &self,
163 request: &PositionTiersRequest,
164 ) -> Result<Vec<PositionTier>, Error> {
165 self.client.get(POSITION_TIERS, request, false).await
166 }
167
168 pub async fn get_underlying(&self, inst_type: InstType) -> Result<Vec<String>, Error> {
176 let query = UnderlyingQuery {
177 inst_type: &inst_type,
178 };
179 self.client.get(UNDERLYING, &query, false).await
180 }
181
182 pub async fn get_insurance_fund(
190 &self,
191 request: &InsuranceFundRequest,
192 ) -> Result<Vec<InsuranceFund>, Error> {
193 self.client.get(INSURANCE_FUND, request, false).await
194 }
195
196 pub async fn get_convert_contract_coin(
204 &self,
205 request: &ConvertContractCoinRequest,
206 ) -> Result<Vec<ConvertContractCoin>, Error> {
207 self.client.get(CONVERT_CONTRACT_COIN, request, false).await
208 }
209}
210
211#[derive(Serialize)]
212struct NoQuery;
213
214#[derive(Serialize)]
215struct InstrumentsQuery<'a> {
216 #[serde(rename = "instType")]
217 inst_type: &'a InstType,
218 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
219 inst_family: Option<&'a str>,
220}
221
222#[derive(Serialize)]
223struct InstIdQuery<'a> {
224 #[serde(rename = "instId")]
225 inst_id: &'a str,
226}
227
228#[derive(Serialize)]
229struct UnderlyingQuery<'a> {
230 #[serde(rename = "instType")]
231 inst_type: &'a InstType,
232}
233
234#[derive(Debug, Clone, Serialize)]
236pub struct InstrumentFamilyRequest {
237 #[serde(rename = "instType")]
238 inst_type: InstType,
239 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
240 underlying: Option<String>,
241 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
242 inst_id: Option<String>,
243 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
244 inst_family: Option<String>,
245}
246
247impl InstrumentFamilyRequest {
248 pub fn new(inst_type: InstType) -> Self {
250 Self {
251 inst_type,
252 underlying: None,
253 inst_id: None,
254 inst_family: None,
255 }
256 }
257
258 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
260 self.underlying = Some(underlying.into());
261 self
262 }
263
264 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
266 self.inst_id = Some(inst_id.into());
267 self
268 }
269
270 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
272 self.inst_family = Some(inst_family.into());
273 self
274 }
275}
276
277#[derive(Debug, Clone, Serialize)]
279pub struct FundingRateHistoryRequest {
280 #[serde(rename = "instId")]
281 inst_id: String,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 after: Option<String>,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 before: Option<String>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 limit: Option<u32>,
288}
289
290impl FundingRateHistoryRequest {
291 pub fn new(inst_id: impl Into<String>) -> Self {
293 Self {
294 inst_id: inst_id.into(),
295 after: None,
296 before: None,
297 limit: None,
298 }
299 }
300
301 pub fn after(mut self, after: impl Into<String>) -> Self {
303 self.after = Some(after.into());
304 self
305 }
306
307 pub fn before(mut self, before: impl Into<String>) -> Self {
309 self.before = Some(before.into());
310 self
311 }
312
313 pub fn limit(mut self, limit: u32) -> Self {
315 self.limit = Some(limit);
316 self
317 }
318}
319
320#[derive(Debug, Clone, Serialize)]
322pub struct DeliveryExerciseHistoryRequest {
323 #[serde(rename = "instType")]
324 inst_type: InstType,
325 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
326 underlying: Option<String>,
327 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
328 inst_family: Option<String>,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 after: Option<String>,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 before: Option<String>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 limit: Option<u32>,
335}
336
337impl DeliveryExerciseHistoryRequest {
338 pub fn new(inst_type: InstType) -> Self {
340 Self {
341 inst_type,
342 underlying: None,
343 inst_family: None,
344 after: None,
345 before: None,
346 limit: None,
347 }
348 }
349
350 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
352 self.underlying = Some(underlying.into());
353 self
354 }
355
356 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
358 self.inst_family = Some(inst_family.into());
359 self
360 }
361
362 pub fn after(mut self, after: impl Into<String>) -> Self {
364 self.after = Some(after.into());
365 self
366 }
367
368 pub fn before(mut self, before: impl Into<String>) -> Self {
370 self.before = Some(before.into());
371 self
372 }
373
374 pub fn limit(mut self, limit: u32) -> Self {
376 self.limit = Some(limit);
377 self
378 }
379}
380
381#[derive(Debug, Clone, Serialize)]
383pub struct PositionTiersRequest {
384 #[serde(rename = "instType")]
385 inst_type: InstType,
386 #[serde(rename = "tdMode")]
387 td_mode: String,
388 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
389 underlying: Option<String>,
390 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
391 inst_id: Option<String>,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 ccy: Option<String>,
394 #[serde(skip_serializing_if = "Option::is_none")]
395 tier: Option<String>,
396 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
397 inst_family: Option<String>,
398}
399
400impl PositionTiersRequest {
401 pub fn new(inst_type: InstType, td_mode: impl Into<String>) -> Self {
403 Self {
404 inst_type,
405 td_mode: td_mode.into(),
406 underlying: None,
407 inst_id: None,
408 ccy: None,
409 tier: None,
410 inst_family: None,
411 }
412 }
413
414 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
416 self.underlying = Some(underlying.into());
417 self
418 }
419
420 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
422 self.inst_id = Some(inst_id.into());
423 self
424 }
425
426 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
428 self.ccy = Some(ccy.into());
429 self
430 }
431
432 pub fn tier(mut self, tier: impl Into<String>) -> Self {
434 self.tier = Some(tier.into());
435 self
436 }
437
438 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
440 self.inst_family = Some(inst_family.into());
441 self
442 }
443}
444
445#[derive(Debug, Clone, Serialize)]
447pub struct InsuranceFundRequest {
448 #[serde(rename = "instType")]
449 inst_type: InstType,
450 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
451 fund_type: Option<String>,
452 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
453 underlying: Option<String>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 ccy: Option<String>,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 before: Option<String>,
458 #[serde(skip_serializing_if = "Option::is_none")]
459 after: Option<String>,
460 #[serde(skip_serializing_if = "Option::is_none")]
461 limit: Option<u32>,
462 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
463 inst_family: Option<String>,
464}
465
466impl InsuranceFundRequest {
467 pub fn new(inst_type: InstType) -> Self {
469 Self {
470 inst_type,
471 fund_type: None,
472 underlying: None,
473 ccy: None,
474 before: None,
475 after: None,
476 limit: None,
477 inst_family: None,
478 }
479 }
480
481 pub fn fund_type(mut self, fund_type: impl Into<String>) -> Self {
483 self.fund_type = Some(fund_type.into());
484 self
485 }
486
487 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
489 self.underlying = Some(underlying.into());
490 self
491 }
492
493 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
495 self.ccy = Some(ccy.into());
496 self
497 }
498
499 pub fn before(mut self, before: impl Into<String>) -> Self {
501 self.before = Some(before.into());
502 self
503 }
504
505 pub fn after(mut self, after: impl Into<String>) -> Self {
507 self.after = Some(after.into());
508 self
509 }
510
511 pub fn limit(mut self, limit: u32) -> Self {
513 self.limit = Some(limit);
514 self
515 }
516
517 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
519 self.inst_family = Some(inst_family.into());
520 self
521 }
522}
523
524#[derive(Debug, Clone, Serialize)]
526pub struct ConvertContractCoinRequest {
527 #[serde(rename = "type")]
528 conversion_type: String,
529 #[serde(rename = "instId")]
530 inst_id: String,
531 sz: String,
532 #[serde(skip_serializing_if = "Option::is_none")]
533 px: Option<String>,
534 #[serde(skip_serializing_if = "Option::is_none")]
535 unit: Option<String>,
536}
537
538impl ConvertContractCoinRequest {
539 pub fn new(
541 conversion_type: impl Into<String>,
542 inst_id: impl Into<String>,
543 sz: impl Into<String>,
544 ) -> Self {
545 Self {
546 conversion_type: conversion_type.into(),
547 inst_id: inst_id.into(),
548 sz: sz.into(),
549 px: None,
550 unit: None,
551 }
552 }
553
554 pub fn price(mut self, px: impl Into<String>) -> Self {
556 self.px = Some(px.into());
557 self
558 }
559
560 pub fn unit(mut self, unit: impl Into<String>) -> Self {
562 self.unit = Some(unit.into());
563 self
564 }
565}
566
567#[derive(Debug, Clone, Deserialize)]
572#[serde(rename_all = "camelCase")]
573#[non_exhaustive]
574pub struct Instrument {
575 pub inst_type: InstType,
577 pub inst_id: String,
579 #[serde(default)]
581 pub uly: String,
582 #[serde(default)]
584 pub inst_family: String,
585 #[serde(default)]
587 pub base_ccy: String,
588 #[serde(default)]
590 pub quote_ccy: String,
591 #[serde(default)]
593 pub settle_ccy: String,
594 #[serde(default)]
596 pub lot_sz: NumberString,
597 #[serde(default)]
599 pub tick_sz: NumberString,
600 #[serde(default)]
602 pub min_sz: NumberString,
603 #[serde(default)]
605 pub state: String,
606}
607
608#[derive(Debug, Clone, Deserialize)]
610#[serde(rename_all = "camelCase")]
611#[non_exhaustive]
612pub struct SystemTime {
613 pub ts: NumberString,
615}
616
617#[derive(Debug, Clone, Deserialize)]
619#[serde(rename_all = "camelCase")]
620#[non_exhaustive]
621pub struct OpenInterest {
622 pub inst_type: InstType,
624 pub inst_id: String,
626 #[serde(default)]
628 pub oi: NumberString,
629 #[serde(default)]
631 pub oi_ccy: NumberString,
632 #[serde(default)]
634 pub ts: NumberString,
635}
636
637#[derive(Debug, Clone, Deserialize)]
639#[serde(rename_all = "camelCase")]
640#[non_exhaustive]
641pub struct FundingRate {
642 #[serde(default)]
644 pub inst_type: String,
645 pub inst_id: String,
647 #[serde(default)]
649 pub funding_rate: NumberString,
650 #[serde(default)]
652 pub next_funding_rate: NumberString,
653 #[serde(default)]
655 pub funding_time: NumberString,
656 #[serde(default)]
658 pub next_funding_time: NumberString,
659}
660
661#[derive(Debug, Clone, Deserialize)]
663#[serde(rename_all = "camelCase")]
664#[non_exhaustive]
665pub struct FundingRateHistory {
666 pub inst_id: String,
668 #[serde(default)]
670 pub funding_rate: NumberString,
671 #[serde(default)]
673 pub realized_rate: NumberString,
674 #[serde(default)]
676 pub funding_time: NumberString,
677 #[serde(default)]
679 pub method: String,
680}
681
682#[derive(Debug, Clone, Deserialize)]
684#[serde(rename_all = "camelCase")]
685#[non_exhaustive]
686pub struct PriceLimit {
687 pub inst_id: String,
689 #[serde(default)]
691 pub buy_lmt: NumberString,
692 #[serde(default)]
694 pub sell_lmt: NumberString,
695 #[serde(default)]
697 pub ts: NumberString,
698}
699
700#[derive(Debug, Clone, Deserialize)]
702#[serde(rename_all = "camelCase")]
703#[non_exhaustive]
704pub struct MarkPrice {
705 pub inst_type: InstType,
707 pub inst_id: String,
709 #[serde(default)]
711 pub mark_px: NumberString,
712 #[serde(default)]
714 pub ts: NumberString,
715}
716
717#[derive(Debug, Clone, Deserialize)]
719#[serde(rename_all = "camelCase")]
720#[non_exhaustive]
721pub struct DeliveryExercise {
722 #[serde(default)]
724 pub inst_type: String,
725 #[serde(default)]
727 pub inst_id: String,
728 #[serde(default)]
730 pub px: NumberString,
731 #[serde(rename = "type", default)]
733 pub exercise_type: String,
734 #[serde(default)]
736 pub ts: NumberString,
737}
738
739#[derive(Debug, Clone, Deserialize)]
741#[serde(rename_all = "camelCase")]
742#[non_exhaustive]
743pub struct PositionTier {
744 pub inst_type: InstType,
746 #[serde(default)]
748 pub td_mode: String,
749 #[serde(default)]
751 pub inst_id: String,
752 #[serde(default)]
754 pub tier: String,
755 #[serde(default)]
757 pub min_sz: NumberString,
758 #[serde(default)]
760 pub max_sz: NumberString,
761 #[serde(default)]
763 pub imr: NumberString,
764 #[serde(default)]
766 pub mmr: NumberString,
767}
768
769#[derive(Debug, Clone, Deserialize)]
771#[serde(rename_all = "camelCase")]
772#[non_exhaustive]
773pub struct InsuranceFund {
774 #[serde(default)]
776 pub inst_type: String,
777 #[serde(rename = "type", default)]
779 pub fund_type: String,
780 #[serde(default)]
782 pub ccy: String,
783 #[serde(default)]
785 pub amt: NumberString,
786 #[serde(default)]
788 pub ts: NumberString,
789}
790
791#[derive(Debug, Clone, Deserialize)]
793#[serde(rename_all = "camelCase")]
794#[non_exhaustive]
795pub struct ConvertContractCoin {
796 #[serde(default)]
798 pub inst_id: String,
799 #[serde(default)]
801 pub sz: NumberString,
802 #[serde(default)]
804 pub px: NumberString,
805 #[serde(default)]
807 pub unit: String,
808}
809
810#[cfg(test)]
811mod tests {
812 use crate::OkxClient;
813 use crate::model::InstType;
814 use crate::test_util::MockTransport;
815
816 #[tokio::test]
817 async fn get_instruments_builds_request_and_parses() {
818 let body = r#"{"code":"0","msg":"","data":[
819 {"instType":"SPOT","instId":"BTC-USDT","uly":"","instFamily":"",
820 "baseCcy":"BTC","quoteCcy":"USDT","settleCcy":"","lotSz":"0.00000001",
821 "tickSz":"0.1","minSz":"0.00001","state":"live"}]}"#;
822 let mock = MockTransport::new(body);
823 let client = OkxClient::with_transport(mock.clone()).build();
824
825 let instruments = client
826 .public_data()
827 .get_instruments(InstType::Spot, None)
828 .await
829 .unwrap();
830
831 assert_eq!(instruments.len(), 1);
832 assert_eq!(instruments[0].inst_id, "BTC-USDT");
833 assert_eq!(instruments[0].base_ccy, "BTC");
834 assert_eq!(instruments[0].tick_sz.as_str(), "0.1");
835
836 let req = mock.captured();
837 assert_eq!(req.method, http::Method::GET);
838 assert!(req.uri.ends_with("/api/v5/public/instruments?instType=SPOT"));
839 assert!(!req.is_signed(), "public endpoint must not be signed");
840 }
841
842 #[tokio::test]
843 async fn get_system_time_parses_time() {
844 let body = r#"{"code":"0","msg":"","data":[{"ts":"1597026383085"}]}"#;
845 let mock = MockTransport::new(body);
846 let client = OkxClient::with_transport(mock.clone()).build();
847
848 let time = client.public_data().get_system_time().await.unwrap();
849 assert_eq!(time[0].ts.as_str(), "1597026383085");
850
851 let req = mock.captured();
852 assert!(req.uri.ends_with("/api/v5/public/time"));
853 assert_eq!(req.query(), None);
854 assert!(!req.is_signed());
855 }
856
857 #[tokio::test]
858 async fn get_open_interest_uses_family_request() {
859 let body = r#"{"code":"0","msg":"","data":[
860 {"instType":"SWAP","instId":"BTC-USDT-SWAP","oi":"10","oiCcy":"1","ts":"1597026383085"}]}"#;
861 let mock = MockTransport::new(body);
862 let client = OkxClient::with_transport(mock.clone()).build();
863 let request = super::InstrumentFamilyRequest::new(InstType::Swap).inst_id("BTC-USDT-SWAP");
864
865 let rows = client
866 .public_data()
867 .get_open_interest(&request)
868 .await
869 .unwrap();
870 assert_eq!(rows[0].oi.as_str(), "10");
871
872 let req = mock.captured();
873 assert_eq!(req.query(), Some("instType=SWAP&instId=BTC-USDT-SWAP"));
874 }
875
876 #[tokio::test]
877 async fn get_funding_rate_queries_instrument() {
878 let body = r#"{"code":"0","msg":"","data":[
879 {"instId":"BTC-USDT-SWAP","fundingRate":"0.0001","nextFundingRate":"0.0002",
880 "fundingTime":"1597026383085","nextFundingTime":"1597030000000"}]}"#;
881 let mock = MockTransport::new(body);
882 let client = OkxClient::with_transport(mock.clone()).build();
883
884 let rows = client
885 .public_data()
886 .get_funding_rate("BTC-USDT-SWAP")
887 .await
888 .unwrap();
889 assert_eq!(rows[0].funding_rate.as_str(), "0.0001");
890
891 let req = mock.captured();
892 assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP"));
893 }
894
895 #[tokio::test]
896 async fn get_funding_rate_history_uses_builder_query() {
897 let body = r#"{"code":"0","msg":"","data":[
898 {"instId":"BTC-USDT-SWAP","fundingRate":"0.0001","realizedRate":"0.0001",
899 "fundingTime":"1597026383085","method":"current_period"}]}"#;
900 let mock = MockTransport::new(body);
901 let client = OkxClient::with_transport(mock.clone()).build();
902 let request = super::FundingRateHistoryRequest::new("BTC-USDT-SWAP")
903 .before("10")
904 .limit(1);
905
906 let rows = client
907 .public_data()
908 .get_funding_rate_history(&request)
909 .await
910 .unwrap();
911 assert_eq!(rows[0].method, "current_period");
912
913 let req = mock.captured();
914 assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP&before=10&limit=1"));
915 assert!(!req.query().unwrap().contains("after"));
916 }
917
918 #[tokio::test]
919 async fn get_price_limit_queries_instrument() {
920 let body = r#"{"code":"0","msg":"","data":[
921 {"instId":"BTC-USDT-SWAP","buyLmt":"45000","sellLmt":"39000","ts":"1597026383085"}]}"#;
922 let mock = MockTransport::new(body);
923 let client = OkxClient::with_transport(mock.clone()).build();
924
925 let rows = client
926 .public_data()
927 .get_price_limit("BTC-USDT-SWAP")
928 .await
929 .unwrap();
930 assert_eq!(rows[0].buy_lmt.as_str(), "45000");
931
932 let req = mock.captured();
933 assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP"));
934 }
935
936 #[tokio::test]
937 async fn get_mark_price_uses_family_request() {
938 let body = r#"{"code":"0","msg":"","data":[
939 {"instType":"SWAP","instId":"BTC-USDT-SWAP","markPx":"42000","ts":"1597026383085"}]}"#;
940 let mock = MockTransport::new(body);
941 let client = OkxClient::with_transport(mock.clone()).build();
942 let request = super::InstrumentFamilyRequest::new(InstType::Swap).inst_family("BTC-USDT");
943
944 let rows = client.public_data().get_mark_price(&request).await.unwrap();
945 assert_eq!(rows[0].mark_px.as_str(), "42000");
946
947 let req = mock.captured();
948 assert_eq!(req.query(), Some("instType=SWAP&instFamily=BTC-USDT"));
949 }
950
951 #[tokio::test]
952 async fn get_delivery_exercise_history_uses_builder_query() {
953 let body = r#"{"code":"0","msg":"","data":[
954 {"instType":"FUTURES","instId":"BTC-USD-240628","px":"42000","type":"delivery","ts":"1597026383085"}]}"#;
955 let mock = MockTransport::new(body);
956 let client = OkxClient::with_transport(mock.clone()).build();
957 let request = super::DeliveryExerciseHistoryRequest::new(InstType::Futures)
958 .underlying("BTC-USD")
959 .limit(1);
960
961 let rows = client
962 .public_data()
963 .get_delivery_exercise_history(&request)
964 .await
965 .unwrap();
966 assert_eq!(rows[0].exercise_type, "delivery");
967
968 let req = mock.captured();
969 assert_eq!(req.query(), Some("instType=FUTURES&uly=BTC-USD&limit=1"));
970 }
971
972 #[tokio::test]
973 async fn get_position_tiers_uses_builder_query() {
974 let body = r#"{"code":"0","msg":"","data":[
975 {"instType":"SWAP","tdMode":"cross","instId":"BTC-USDT-SWAP","tier":"1",
976 "minSz":"0","maxSz":"100","imr":"0.1","mmr":"0.05"}]}"#;
977 let mock = MockTransport::new(body);
978 let client = OkxClient::with_transport(mock.clone()).build();
979 let request = super::PositionTiersRequest::new(InstType::Swap, "cross").tier("1");
980
981 let rows = client
982 .public_data()
983 .get_position_tiers(&request)
984 .await
985 .unwrap();
986 assert_eq!(rows[0].tier, "1");
987
988 let req = mock.captured();
989 assert_eq!(req.query(), Some("instType=SWAP&tdMode=cross&tier=1"));
990 }
991
992 #[tokio::test]
993 async fn get_underlying_queries_inst_type() {
994 let body = r#"{"code":"0","msg":"","data":["BTC-USD"]}"#;
995 let mock = MockTransport::new(body);
996 let client = OkxClient::with_transport(mock.clone()).build();
997
998 let rows = client.public_data().get_underlying(InstType::Swap).await.unwrap();
999 assert_eq!(rows[0], "BTC-USD");
1000
1001 let req = mock.captured();
1002 assert_eq!(req.query(), Some("instType=SWAP"));
1003 }
1004
1005 #[tokio::test]
1006 async fn get_insurance_fund_uses_builder_query() {
1007 let body = r#"{"code":"0","msg":"","data":[
1008 {"instType":"SWAP","type":"regular_update","ccy":"USDT","amt":"100","ts":"1597026383085"}]}"#;
1009 let mock = MockTransport::new(body);
1010 let client = OkxClient::with_transport(mock.clone()).build();
1011 let request = super::InsuranceFundRequest::new(InstType::Swap)
1012 .fund_type("regular_update")
1013 .currency("USDT");
1014
1015 let rows = client
1016 .public_data()
1017 .get_insurance_fund(&request)
1018 .await
1019 .unwrap();
1020 assert_eq!(rows[0].amt.as_str(), "100");
1021
1022 let req = mock.captured();
1023 assert_eq!(
1024 req.query(),
1025 Some("instType=SWAP&type=regular_update&ccy=USDT")
1026 );
1027 }
1028
1029 #[tokio::test]
1030 async fn get_convert_contract_coin_uses_builder_query() {
1031 let body = r#"{"code":"0","msg":"","data":[
1032 {"instId":"BTC-USDT-SWAP","sz":"1","px":"42000","unit":"coin"}]}"#;
1033 let mock = MockTransport::new(body);
1034 let client = OkxClient::with_transport(mock.clone()).build();
1035 let request = super::ConvertContractCoinRequest::new("1", "BTC-USDT-SWAP", "1")
1036 .price("42000")
1037 .unit("coin");
1038
1039 let rows = client
1040 .public_data()
1041 .get_convert_contract_coin(&request)
1042 .await
1043 .unwrap();
1044 assert_eq!(rows[0].unit, "coin");
1045
1046 let req = mock.captured();
1047 assert_eq!(
1048 req.query(),
1049 Some("type=1&instId=BTC-USDT-SWAP&sz=1&px=42000&unit=coin")
1050 );
1051 }
1052}