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.get(FUNDING_RATE_HISTORY, request, false).await
108 }
109
110 pub async fn get_price_limit(&self, inst_id: &str) -> Result<Vec<PriceLimit>, Error> {
118 let query = InstIdQuery { inst_id };
119 self.client.get(PRICE_LIMIT, &query, false).await
120 }
121
122 pub async fn get_mark_price(
130 &self,
131 request: &InstrumentFamilyRequest,
132 ) -> Result<Vec<MarkPrice>, Error> {
133 self.client.get(MARK_PRICE, request, false).await
134 }
135
136 pub async fn get_delivery_exercise_history(
144 &self,
145 request: &DeliveryExerciseHistoryRequest,
146 ) -> Result<Vec<DeliveryExercise>, Error> {
147 self.client
148 .get(DELIVERY_EXERCISE_HISTORY, request, false)
149 .await
150 }
151
152 pub async fn get_position_tiers(
160 &self,
161 request: &PositionTiersRequest,
162 ) -> Result<Vec<PositionTier>, Error> {
163 self.client.get(POSITION_TIERS, request, false).await
164 }
165
166 pub async fn get_underlying(&self, inst_type: InstType) -> Result<Vec<String>, Error> {
174 let query = UnderlyingQuery {
175 inst_type: &inst_type,
176 };
177 self.client.get(UNDERLYING, &query, false).await
178 }
179
180 pub async fn get_insurance_fund(
188 &self,
189 request: &InsuranceFundRequest,
190 ) -> Result<Vec<InsuranceFund>, Error> {
191 self.client.get(INSURANCE_FUND, request, false).await
192 }
193
194 pub async fn get_convert_contract_coin(
202 &self,
203 request: &ConvertContractCoinRequest,
204 ) -> Result<Vec<ConvertContractCoin>, Error> {
205 self.client.get(CONVERT_CONTRACT_COIN, request, false).await
206 }
207}
208
209#[derive(Serialize)]
210struct NoQuery;
211
212#[derive(Serialize)]
213struct InstrumentsQuery<'a> {
214 #[serde(rename = "instType")]
215 inst_type: &'a InstType,
216 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
217 inst_family: Option<&'a str>,
218}
219
220#[derive(Serialize)]
221struct InstIdQuery<'a> {
222 #[serde(rename = "instId")]
223 inst_id: &'a str,
224}
225
226#[derive(Serialize)]
227struct UnderlyingQuery<'a> {
228 #[serde(rename = "instType")]
229 inst_type: &'a InstType,
230}
231
232#[derive(Debug, Clone, Serialize)]
234pub struct InstrumentFamilyRequest {
235 #[serde(rename = "instType")]
236 inst_type: InstType,
237 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
238 underlying: Option<String>,
239 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
240 inst_id: Option<String>,
241 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
242 inst_family: Option<String>,
243}
244
245impl InstrumentFamilyRequest {
246 pub fn new(inst_type: InstType) -> Self {
248 Self {
249 inst_type,
250 underlying: None,
251 inst_id: None,
252 inst_family: None,
253 }
254 }
255
256 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
258 self.underlying = Some(underlying.into());
259 self
260 }
261
262 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
264 self.inst_id = Some(inst_id.into());
265 self
266 }
267
268 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
270 self.inst_family = Some(inst_family.into());
271 self
272 }
273}
274
275#[derive(Debug, Clone, Serialize)]
277pub struct FundingRateHistoryRequest {
278 #[serde(rename = "instId")]
279 inst_id: String,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 after: Option<String>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 before: Option<String>,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 limit: Option<u32>,
286}
287
288impl FundingRateHistoryRequest {
289 pub fn new(inst_id: impl Into<String>) -> Self {
291 Self {
292 inst_id: inst_id.into(),
293 after: None,
294 before: None,
295 limit: None,
296 }
297 }
298
299 pub fn after(mut self, after: impl Into<String>) -> Self {
301 self.after = Some(after.into());
302 self
303 }
304
305 pub fn before(mut self, before: impl Into<String>) -> Self {
307 self.before = Some(before.into());
308 self
309 }
310
311 pub fn limit(mut self, limit: u32) -> Self {
313 self.limit = Some(limit);
314 self
315 }
316}
317
318#[derive(Debug, Clone, Serialize)]
320pub struct DeliveryExerciseHistoryRequest {
321 #[serde(rename = "instType")]
322 inst_type: InstType,
323 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
324 underlying: Option<String>,
325 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
326 inst_family: Option<String>,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 after: Option<String>,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 before: Option<String>,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 limit: Option<u32>,
333}
334
335impl DeliveryExerciseHistoryRequest {
336 pub fn new(inst_type: InstType) -> Self {
338 Self {
339 inst_type,
340 underlying: None,
341 inst_family: None,
342 after: None,
343 before: None,
344 limit: None,
345 }
346 }
347
348 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
350 self.underlying = Some(underlying.into());
351 self
352 }
353
354 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
356 self.inst_family = Some(inst_family.into());
357 self
358 }
359
360 pub fn after(mut self, after: impl Into<String>) -> Self {
362 self.after = Some(after.into());
363 self
364 }
365
366 pub fn before(mut self, before: impl Into<String>) -> Self {
368 self.before = Some(before.into());
369 self
370 }
371
372 pub fn limit(mut self, limit: u32) -> Self {
374 self.limit = Some(limit);
375 self
376 }
377}
378
379#[derive(Debug, Clone, Serialize)]
381pub struct PositionTiersRequest {
382 #[serde(rename = "instType")]
383 inst_type: InstType,
384 #[serde(rename = "tdMode")]
385 td_mode: String,
386 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
387 underlying: Option<String>,
388 #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
389 inst_id: Option<String>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 ccy: Option<String>,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 tier: Option<String>,
394 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
395 inst_family: Option<String>,
396}
397
398impl PositionTiersRequest {
399 pub fn new(inst_type: InstType, td_mode: impl Into<String>) -> Self {
401 Self {
402 inst_type,
403 td_mode: td_mode.into(),
404 underlying: None,
405 inst_id: None,
406 ccy: None,
407 tier: None,
408 inst_family: None,
409 }
410 }
411
412 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
414 self.underlying = Some(underlying.into());
415 self
416 }
417
418 pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
420 self.inst_id = Some(inst_id.into());
421 self
422 }
423
424 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
426 self.ccy = Some(ccy.into());
427 self
428 }
429
430 pub fn tier(mut self, tier: impl Into<String>) -> Self {
432 self.tier = Some(tier.into());
433 self
434 }
435
436 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
438 self.inst_family = Some(inst_family.into());
439 self
440 }
441}
442
443#[derive(Debug, Clone, Serialize)]
445pub struct InsuranceFundRequest {
446 #[serde(rename = "instType")]
447 inst_type: InstType,
448 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
449 fund_type: Option<String>,
450 #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
451 underlying: Option<String>,
452 #[serde(skip_serializing_if = "Option::is_none")]
453 ccy: Option<String>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 before: Option<String>,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 after: Option<String>,
458 #[serde(skip_serializing_if = "Option::is_none")]
459 limit: Option<u32>,
460 #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
461 inst_family: Option<String>,
462}
463
464impl InsuranceFundRequest {
465 pub fn new(inst_type: InstType) -> Self {
467 Self {
468 inst_type,
469 fund_type: None,
470 underlying: None,
471 ccy: None,
472 before: None,
473 after: None,
474 limit: None,
475 inst_family: None,
476 }
477 }
478
479 pub fn fund_type(mut self, fund_type: impl Into<String>) -> Self {
481 self.fund_type = Some(fund_type.into());
482 self
483 }
484
485 pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
487 self.underlying = Some(underlying.into());
488 self
489 }
490
491 pub fn currency(mut self, ccy: impl Into<String>) -> Self {
493 self.ccy = Some(ccy.into());
494 self
495 }
496
497 pub fn before(mut self, before: impl Into<String>) -> Self {
499 self.before = Some(before.into());
500 self
501 }
502
503 pub fn after(mut self, after: impl Into<String>) -> Self {
505 self.after = Some(after.into());
506 self
507 }
508
509 pub fn limit(mut self, limit: u32) -> Self {
511 self.limit = Some(limit);
512 self
513 }
514
515 pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
517 self.inst_family = Some(inst_family.into());
518 self
519 }
520}
521
522#[derive(Debug, Clone, Serialize)]
524pub struct ConvertContractCoinRequest {
525 #[serde(rename = "type")]
526 conversion_type: String,
527 #[serde(rename = "instId")]
528 inst_id: String,
529 sz: String,
530 #[serde(skip_serializing_if = "Option::is_none")]
531 px: Option<String>,
532 #[serde(skip_serializing_if = "Option::is_none")]
533 unit: Option<String>,
534}
535
536impl ConvertContractCoinRequest {
537 pub fn new(
539 conversion_type: impl Into<String>,
540 inst_id: impl Into<String>,
541 sz: impl Into<String>,
542 ) -> Self {
543 Self {
544 conversion_type: conversion_type.into(),
545 inst_id: inst_id.into(),
546 sz: sz.into(),
547 px: None,
548 unit: None,
549 }
550 }
551
552 pub fn price(mut self, px: impl Into<String>) -> Self {
554 self.px = Some(px.into());
555 self
556 }
557
558 pub fn unit(mut self, unit: impl Into<String>) -> Self {
560 self.unit = Some(unit.into());
561 self
562 }
563}
564
565#[derive(Debug, Clone, Deserialize)]
570#[serde(rename_all = "camelCase")]
571#[non_exhaustive]
572pub struct Instrument {
573 pub inst_type: InstType,
575 pub inst_id: String,
577 #[serde(default)]
579 pub uly: String,
580 #[serde(default)]
582 pub inst_family: String,
583 #[serde(default)]
585 pub base_ccy: String,
586 #[serde(default)]
588 pub quote_ccy: String,
589 #[serde(default)]
591 pub settle_ccy: String,
592 #[serde(default)]
594 pub lot_sz: NumberString,
595 #[serde(default)]
597 pub tick_sz: NumberString,
598 #[serde(default)]
600 pub min_sz: NumberString,
601 #[serde(default)]
603 pub state: String,
604}
605
606#[derive(Debug, Clone, Deserialize)]
608#[serde(rename_all = "camelCase")]
609#[non_exhaustive]
610pub struct SystemTime {
611 pub ts: NumberString,
613}
614
615#[derive(Debug, Clone, Deserialize)]
617#[serde(rename_all = "camelCase")]
618#[non_exhaustive]
619pub struct OpenInterest {
620 pub inst_type: InstType,
622 pub inst_id: String,
624 #[serde(default)]
626 pub oi: NumberString,
627 #[serde(default)]
629 pub oi_ccy: NumberString,
630 #[serde(default)]
632 pub ts: NumberString,
633}
634
635#[derive(Debug, Clone, Deserialize)]
637#[serde(rename_all = "camelCase")]
638#[non_exhaustive]
639pub struct FundingRate {
640 #[serde(default)]
642 pub inst_type: String,
643 pub inst_id: String,
645 #[serde(default)]
647 pub funding_rate: NumberString,
648 #[serde(default)]
650 pub next_funding_rate: NumberString,
651 #[serde(default)]
653 pub funding_time: NumberString,
654 #[serde(default)]
656 pub next_funding_time: NumberString,
657}
658
659#[derive(Debug, Clone, Deserialize)]
661#[serde(rename_all = "camelCase")]
662#[non_exhaustive]
663pub struct FundingRateHistory {
664 pub inst_id: String,
666 #[serde(default)]
668 pub funding_rate: NumberString,
669 #[serde(default)]
671 pub realized_rate: NumberString,
672 #[serde(default)]
674 pub funding_time: NumberString,
675 #[serde(default)]
677 pub method: String,
678}
679
680#[derive(Debug, Clone, Deserialize)]
682#[serde(rename_all = "camelCase")]
683#[non_exhaustive]
684pub struct PriceLimit {
685 pub inst_id: String,
687 #[serde(default)]
689 pub buy_lmt: NumberString,
690 #[serde(default)]
692 pub sell_lmt: NumberString,
693 #[serde(default)]
695 pub ts: NumberString,
696}
697
698#[derive(Debug, Clone, Deserialize)]
700#[serde(rename_all = "camelCase")]
701#[non_exhaustive]
702pub struct MarkPrice {
703 pub inst_type: InstType,
705 pub inst_id: String,
707 #[serde(default)]
709 pub mark_px: NumberString,
710 #[serde(default)]
712 pub ts: NumberString,
713}
714
715#[derive(Debug, Clone, Deserialize)]
717#[serde(rename_all = "camelCase")]
718#[non_exhaustive]
719pub struct DeliveryExercise {
720 #[serde(default)]
722 pub inst_type: String,
723 #[serde(default)]
725 pub inst_id: String,
726 #[serde(default)]
728 pub px: NumberString,
729 #[serde(rename = "type", default)]
731 pub exercise_type: String,
732 #[serde(default)]
734 pub ts: NumberString,
735}
736
737#[derive(Debug, Clone, Deserialize)]
739#[serde(rename_all = "camelCase")]
740#[non_exhaustive]
741pub struct PositionTier {
742 pub inst_type: InstType,
744 #[serde(default)]
746 pub td_mode: String,
747 #[serde(default)]
749 pub inst_id: String,
750 #[serde(default)]
752 pub tier: String,
753 #[serde(default)]
755 pub min_sz: NumberString,
756 #[serde(default)]
758 pub max_sz: NumberString,
759 #[serde(default)]
761 pub imr: NumberString,
762 #[serde(default)]
764 pub mmr: NumberString,
765}
766
767#[derive(Debug, Clone, Deserialize)]
769#[serde(rename_all = "camelCase")]
770#[non_exhaustive]
771pub struct InsuranceFund {
772 #[serde(default)]
774 pub inst_type: String,
775 #[serde(rename = "type", default)]
777 pub fund_type: String,
778 #[serde(default)]
780 pub ccy: String,
781 #[serde(default)]
783 pub amt: NumberString,
784 #[serde(default)]
786 pub ts: NumberString,
787}
788
789#[derive(Debug, Clone, Deserialize)]
791#[serde(rename_all = "camelCase")]
792#[non_exhaustive]
793pub struct ConvertContractCoin {
794 #[serde(default)]
796 pub inst_id: String,
797 #[serde(default)]
799 pub sz: NumberString,
800 #[serde(default)]
802 pub px: NumberString,
803 #[serde(default)]
805 pub unit: String,
806}
807
808#[cfg(test)]
809mod tests {
810 use crate::OkxClient;
811 use crate::model::InstType;
812 use crate::test_util::MockTransport;
813
814 #[tokio::test]
815 async fn get_instruments_builds_request_and_parses() {
816 let body = r#"{"code":"0","msg":"","data":[
817 {"instType":"SPOT","instId":"BTC-USDT","uly":"","instFamily":"",
818 "baseCcy":"BTC","quoteCcy":"USDT","settleCcy":"","lotSz":"0.00000001",
819 "tickSz":"0.1","minSz":"0.00001","state":"live"}]}"#;
820 let mock = MockTransport::new(body);
821 let client = OkxClient::with_transport(mock.clone()).build();
822
823 let instruments = client
824 .public_data()
825 .get_instruments(InstType::Spot, None)
826 .await
827 .unwrap();
828
829 assert_eq!(instruments.len(), 1);
830 assert_eq!(instruments[0].inst_id, "BTC-USDT");
831 assert_eq!(instruments[0].base_ccy, "BTC");
832 assert_eq!(instruments[0].tick_sz.as_str(), "0.1");
833
834 let req = mock.captured();
835 assert_eq!(req.method, http::Method::GET);
836 assert!(
837 req.uri
838 .ends_with("/api/v5/public/instruments?instType=SPOT")
839 );
840 assert!(!req.is_signed(), "public endpoint must not be signed");
841 }
842
843 #[tokio::test]
844 async fn get_system_time_parses_time() {
845 let body = r#"{"code":"0","msg":"","data":[{"ts":"1597026383085"}]}"#;
846 let mock = MockTransport::new(body);
847 let client = OkxClient::with_transport(mock.clone()).build();
848
849 let time = client.public_data().get_system_time().await.unwrap();
850 assert_eq!(time[0].ts.as_str(), "1597026383085");
851
852 let req = mock.captured();
853 assert!(req.uri.ends_with("/api/v5/public/time"));
854 assert_eq!(req.query(), None);
855 assert!(!req.is_signed());
856 }
857
858 #[tokio::test]
859 async fn get_open_interest_uses_family_request() {
860 let body = r#"{"code":"0","msg":"","data":[
861 {"instType":"SWAP","instId":"BTC-USDT-SWAP","oi":"10","oiCcy":"1","ts":"1597026383085"}]}"#;
862 let mock = MockTransport::new(body);
863 let client = OkxClient::with_transport(mock.clone()).build();
864 let request = super::InstrumentFamilyRequest::new(InstType::Swap).inst_id("BTC-USDT-SWAP");
865
866 let rows = client
867 .public_data()
868 .get_open_interest(&request)
869 .await
870 .unwrap();
871 assert_eq!(rows[0].oi.as_str(), "10");
872
873 let req = mock.captured();
874 assert_eq!(req.query(), Some("instType=SWAP&instId=BTC-USDT-SWAP"));
875 }
876
877 #[tokio::test]
878 async fn get_funding_rate_queries_instrument() {
879 let body = r#"{"code":"0","msg":"","data":[
880 {"instId":"BTC-USDT-SWAP","fundingRate":"0.0001","nextFundingRate":"0.0002",
881 "fundingTime":"1597026383085","nextFundingTime":"1597030000000"}]}"#;
882 let mock = MockTransport::new(body);
883 let client = OkxClient::with_transport(mock.clone()).build();
884
885 let rows = client
886 .public_data()
887 .get_funding_rate("BTC-USDT-SWAP")
888 .await
889 .unwrap();
890 assert_eq!(rows[0].funding_rate.as_str(), "0.0001");
891
892 let req = mock.captured();
893 assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP"));
894 }
895
896 #[tokio::test]
897 async fn get_funding_rate_history_uses_builder_query() {
898 let body = r#"{"code":"0","msg":"","data":[
899 {"instId":"BTC-USDT-SWAP","fundingRate":"0.0001","realizedRate":"0.0001",
900 "fundingTime":"1597026383085","method":"current_period"}]}"#;
901 let mock = MockTransport::new(body);
902 let client = OkxClient::with_transport(mock.clone()).build();
903 let request = super::FundingRateHistoryRequest::new("BTC-USDT-SWAP")
904 .before("10")
905 .limit(1);
906
907 let rows = client
908 .public_data()
909 .get_funding_rate_history(&request)
910 .await
911 .unwrap();
912 assert_eq!(rows[0].method, "current_period");
913
914 let req = mock.captured();
915 assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP&before=10&limit=1"));
916 assert!(!req.query().unwrap().contains("after"));
917 }
918
919 #[tokio::test]
920 async fn get_price_limit_queries_instrument() {
921 let body = r#"{"code":"0","msg":"","data":[
922 {"instId":"BTC-USDT-SWAP","buyLmt":"45000","sellLmt":"39000","ts":"1597026383085"}]}"#;
923 let mock = MockTransport::new(body);
924 let client = OkxClient::with_transport(mock.clone()).build();
925
926 let rows = client
927 .public_data()
928 .get_price_limit("BTC-USDT-SWAP")
929 .await
930 .unwrap();
931 assert_eq!(rows[0].buy_lmt.as_str(), "45000");
932
933 let req = mock.captured();
934 assert_eq!(req.query(), Some("instId=BTC-USDT-SWAP"));
935 }
936
937 #[tokio::test]
938 async fn get_mark_price_uses_family_request() {
939 let body = r#"{"code":"0","msg":"","data":[
940 {"instType":"SWAP","instId":"BTC-USDT-SWAP","markPx":"42000","ts":"1597026383085"}]}"#;
941 let mock = MockTransport::new(body);
942 let client = OkxClient::with_transport(mock.clone()).build();
943 let request = super::InstrumentFamilyRequest::new(InstType::Swap).inst_family("BTC-USDT");
944
945 let rows = client.public_data().get_mark_price(&request).await.unwrap();
946 assert_eq!(rows[0].mark_px.as_str(), "42000");
947
948 let req = mock.captured();
949 assert_eq!(req.query(), Some("instType=SWAP&instFamily=BTC-USDT"));
950 }
951
952 #[tokio::test]
953 async fn get_delivery_exercise_history_uses_builder_query() {
954 let body = r#"{"code":"0","msg":"","data":[
955 {"instType":"FUTURES","instId":"BTC-USD-240628","px":"42000","type":"delivery","ts":"1597026383085"}]}"#;
956 let mock = MockTransport::new(body);
957 let client = OkxClient::with_transport(mock.clone()).build();
958 let request = super::DeliveryExerciseHistoryRequest::new(InstType::Futures)
959 .underlying("BTC-USD")
960 .limit(1);
961
962 let rows = client
963 .public_data()
964 .get_delivery_exercise_history(&request)
965 .await
966 .unwrap();
967 assert_eq!(rows[0].exercise_type, "delivery");
968
969 let req = mock.captured();
970 assert_eq!(req.query(), Some("instType=FUTURES&uly=BTC-USD&limit=1"));
971 }
972
973 #[tokio::test]
974 async fn get_position_tiers_uses_builder_query() {
975 let body = r#"{"code":"0","msg":"","data":[
976 {"instType":"SWAP","tdMode":"cross","instId":"BTC-USDT-SWAP","tier":"1",
977 "minSz":"0","maxSz":"100","imr":"0.1","mmr":"0.05"}]}"#;
978 let mock = MockTransport::new(body);
979 let client = OkxClient::with_transport(mock.clone()).build();
980 let request = super::PositionTiersRequest::new(InstType::Swap, "cross").tier("1");
981
982 let rows = client
983 .public_data()
984 .get_position_tiers(&request)
985 .await
986 .unwrap();
987 assert_eq!(rows[0].tier, "1");
988
989 let req = mock.captured();
990 assert_eq!(req.query(), Some("instType=SWAP&tdMode=cross&tier=1"));
991 }
992
993 #[tokio::test]
994 async fn get_underlying_queries_inst_type() {
995 let body = r#"{"code":"0","msg":"","data":["BTC-USD"]}"#;
996 let mock = MockTransport::new(body);
997 let client = OkxClient::with_transport(mock.clone()).build();
998
999 let rows = client
1000 .public_data()
1001 .get_underlying(InstType::Swap)
1002 .await
1003 .unwrap();
1004 assert_eq!(rows[0], "BTC-USD");
1005
1006 let req = mock.captured();
1007 assert_eq!(req.query(), Some("instType=SWAP"));
1008 }
1009
1010 #[tokio::test]
1011 async fn get_insurance_fund_uses_builder_query() {
1012 let body = r#"{"code":"0","msg":"","data":[
1013 {"instType":"SWAP","type":"regular_update","ccy":"USDT","amt":"100","ts":"1597026383085"}]}"#;
1014 let mock = MockTransport::new(body);
1015 let client = OkxClient::with_transport(mock.clone()).build();
1016 let request = super::InsuranceFundRequest::new(InstType::Swap)
1017 .fund_type("regular_update")
1018 .currency("USDT");
1019
1020 let rows = client
1021 .public_data()
1022 .get_insurance_fund(&request)
1023 .await
1024 .unwrap();
1025 assert_eq!(rows[0].amt.as_str(), "100");
1026
1027 let req = mock.captured();
1028 assert_eq!(
1029 req.query(),
1030 Some("instType=SWAP&type=regular_update&ccy=USDT")
1031 );
1032 }
1033
1034 #[tokio::test]
1035 async fn get_convert_contract_coin_uses_builder_query() {
1036 let body = r#"{"code":"0","msg":"","data":[
1037 {"instId":"BTC-USDT-SWAP","sz":"1","px":"42000","unit":"coin"}]}"#;
1038 let mock = MockTransport::new(body);
1039 let client = OkxClient::with_transport(mock.clone()).build();
1040 let request = super::ConvertContractCoinRequest::new("1", "BTC-USDT-SWAP", "1")
1041 .price("42000")
1042 .unit("coin");
1043
1044 let rows = client
1045 .public_data()
1046 .get_convert_contract_coin(&request)
1047 .await
1048 .unwrap();
1049 assert_eq!(rows[0].unit, "coin");
1050
1051 let req = mock.captured();
1052 assert_eq!(
1053 req.query(),
1054 Some("type=1&instId=BTC-USDT-SWAP&sz=1&px=42000&unit=coin")
1055 );
1056 }
1057}