Skip to main content

rust_okx/api/
public_data.rs

1//! Public reference-data endpoints (`/api/v5/public/*`).
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::OkxClient;
6use crate::error::Error;
7use crate::model::{InstType, NumberString};
8use crate::transport::Transport;
9
10const 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
23/// Accessor for the public reference-data endpoints.
24///
25/// Obtain one via [`OkxClient::public_data`](crate::OkxClient::public_data).
26pub 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    /// Retrieve the list of tradable instruments.
36    ///
37    /// `GET /api/v5/public/instruments`
38    ///
39    /// `inst_family` is required for `FUTURES`, `SWAP`, and `OPTION` and ignored
40    /// for `SPOT`/`MARGIN`. This endpoint is public (unauthenticated).
41    ///
42    /// # Errors
43    ///
44    /// Returns [`Error::Api`] if OKX rejects the request, or
45    /// [`Error::Transport`]/[`Error::Decode`] on transport/parsing failure.
46    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    /// Retrieve OKX system time.
59    ///
60    /// `GET /api/v5/public/time`. Public.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`Error::Api`] if OKX rejects the request, or transport/decode
65    /// errors.
66    pub async fn get_system_time(&self) -> Result<Vec<SystemTime>, Error> {
67        self.client.get(SYSTEM_TIME, &NoQuery, false).await
68    }
69
70    /// Retrieve open interest.
71    ///
72    /// `GET /api/v5/public/open-interest`. Public.
73    ///
74    /// # Errors
75    ///
76    /// See [`get_system_time`](Self::get_system_time).
77    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    /// Retrieve the current funding rate for a derivatives instrument.
85    ///
86    /// `GET /api/v5/public/funding-rate`. Public.
87    ///
88    /// # Errors
89    ///
90    /// See [`get_system_time`](Self::get_system_time).
91    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    /// Retrieve historical funding rates.
97    ///
98    /// `GET /api/v5/public/funding-rate-history`. Public.
99    ///
100    /// # Errors
101    ///
102    /// See [`get_system_time`](Self::get_system_time).
103    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    /// Retrieve the price limit for an instrument.
111    ///
112    /// `GET /api/v5/public/price-limit`. Public.
113    ///
114    /// # Errors
115    ///
116    /// See [`get_system_time`](Self::get_system_time).
117    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    /// Retrieve mark prices.
123    ///
124    /// `GET /api/v5/public/mark-price`. Public.
125    ///
126    /// # Errors
127    ///
128    /// See [`get_system_time`](Self::get_system_time).
129    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    /// Retrieve delivery/exercise history.
137    ///
138    /// `GET /api/v5/public/delivery-exercise-history`. Public.
139    ///
140    /// # Errors
141    ///
142    /// See [`get_system_time`](Self::get_system_time).
143    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    /// Retrieve position tiers.
153    ///
154    /// `GET /api/v5/public/position-tiers`. Public.
155    ///
156    /// # Errors
157    ///
158    /// See [`get_system_time`](Self::get_system_time).
159    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    /// Retrieve underlying values for an instrument type.
167    ///
168    /// `GET /api/v5/public/underlying`. Public.
169    ///
170    /// # Errors
171    ///
172    /// See [`get_system_time`](Self::get_system_time).
173    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    /// Retrieve insurance-fund snapshots.
181    ///
182    /// `GET /api/v5/public/insurance-fund`. Public.
183    ///
184    /// # Errors
185    ///
186    /// See [`get_system_time`](Self::get_system_time).
187    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    /// Convert between contract count and coin amount.
195    ///
196    /// `GET /api/v5/public/convert-contract-coin`. Public.
197    ///
198    /// # Errors
199    ///
200    /// See [`get_system_time`](Self::get_system_time).
201    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/// Query parameters for public endpoints filtered by instrument family.
233#[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    /// Create a query for an instrument type.
247    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    /// Set the underlying filter.
257    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
258        self.underlying = Some(underlying.into());
259        self
260    }
261
262    /// Set the instrument ID filter.
263    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    /// Set the instrument family filter.
269    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/// Query parameters for funding-rate history.
276#[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    /// Create a funding-rate history query.
290    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    /// Return records after this pagination cursor.
300    pub fn after(mut self, after: impl Into<String>) -> Self {
301        self.after = Some(after.into());
302        self
303    }
304
305    /// Return records before this pagination cursor.
306    pub fn before(mut self, before: impl Into<String>) -> Self {
307        self.before = Some(before.into());
308        self
309    }
310
311    /// Set the maximum number of rows to return.
312    pub fn limit(mut self, limit: u32) -> Self {
313        self.limit = Some(limit);
314        self
315    }
316}
317
318/// Query parameters for delivery/exercise history.
319#[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    /// Create a delivery/exercise history query.
337    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    /// Set the underlying filter.
349    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
350        self.underlying = Some(underlying.into());
351        self
352    }
353
354    /// Set the instrument family filter.
355    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    /// Return records after this pagination cursor.
361    pub fn after(mut self, after: impl Into<String>) -> Self {
362        self.after = Some(after.into());
363        self
364    }
365
366    /// Return records before this pagination cursor.
367    pub fn before(mut self, before: impl Into<String>) -> Self {
368        self.before = Some(before.into());
369        self
370    }
371
372    /// Set the maximum number of rows to return.
373    pub fn limit(mut self, limit: u32) -> Self {
374        self.limit = Some(limit);
375        self
376    }
377}
378
379/// Query parameters for public position tiers.
380#[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    /// Create a position-tiers query.
400    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    /// Set the underlying filter.
413    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
414        self.underlying = Some(underlying.into());
415        self
416    }
417
418    /// Set the instrument ID filter.
419    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    /// Set the currency filter.
425    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
426        self.ccy = Some(ccy.into());
427        self
428    }
429
430    /// Set the tier filter.
431    pub fn tier(mut self, tier: impl Into<String>) -> Self {
432        self.tier = Some(tier.into());
433        self
434    }
435
436    /// Set the instrument family filter.
437    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/// Query parameters for insurance fund snapshots.
444#[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    /// Create an insurance fund query.
466    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    /// Set the OKX fund type filter.
480    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    /// Set the underlying filter.
486    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
487        self.underlying = Some(underlying.into());
488        self
489    }
490
491    /// Set the currency filter.
492    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
493        self.ccy = Some(ccy.into());
494        self
495    }
496
497    /// Return records before this pagination cursor.
498    pub fn before(mut self, before: impl Into<String>) -> Self {
499        self.before = Some(before.into());
500        self
501    }
502
503    /// Return records after this pagination cursor.
504    pub fn after(mut self, after: impl Into<String>) -> Self {
505        self.after = Some(after.into());
506        self
507    }
508
509    /// Set the maximum number of rows to return.
510    pub fn limit(mut self, limit: u32) -> Self {
511        self.limit = Some(limit);
512        self
513    }
514
515    /// Set the instrument family filter.
516    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/// Query parameters for contract/coin conversion.
523#[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    /// Create a contract/coin conversion query.
538    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    /// Set the price used for conversion.
553    pub fn price(mut self, px: impl Into<String>) -> Self {
554        self.px = Some(px.into());
555        self
556    }
557
558    /// Set the unit used for conversion.
559    pub fn unit(mut self, unit: impl Into<String>) -> Self {
560        self.unit = Some(unit.into());
561        self
562    }
563}
564
565/// A tradable instrument.
566///
567/// Only commonly used fields are modeled; the struct is `#[non_exhaustive]` and
568/// unknown JSON fields are ignored, so OKX additions are non-breaking.
569#[derive(Debug, Clone, Deserialize)]
570#[serde(rename_all = "camelCase")]
571#[non_exhaustive]
572pub struct Instrument {
573    /// Instrument type.
574    pub inst_type: InstType,
575    /// Instrument ID, e.g. `BTC-USDT`.
576    pub inst_id: String,
577    /// Underlying, e.g. `BTC-USD` (derivatives only).
578    #[serde(default)]
579    pub uly: String,
580    /// Instrument family, e.g. `BTC-USD` (derivatives only).
581    #[serde(default)]
582    pub inst_family: String,
583    /// Base currency, e.g. `BTC` (spot/margin only).
584    #[serde(default)]
585    pub base_ccy: String,
586    /// Quote currency, e.g. `USDT` (spot/margin only).
587    #[serde(default)]
588    pub quote_ccy: String,
589    /// Settlement currency (derivatives only).
590    #[serde(default)]
591    pub settle_ccy: String,
592    /// Lot size (order size increment).
593    #[serde(default)]
594    pub lot_sz: NumberString,
595    /// Tick size (price increment).
596    #[serde(default)]
597    pub tick_sz: NumberString,
598    /// Minimum order size.
599    #[serde(default)]
600    pub min_sz: NumberString,
601    /// Instrument lifecycle state, e.g. `live`, `suspend`.
602    #[serde(default)]
603    pub state: String,
604}
605
606/// OKX system time.
607#[derive(Debug, Clone, Deserialize)]
608#[serde(rename_all = "camelCase")]
609#[non_exhaustive]
610pub struct SystemTime {
611    /// Current OKX system timestamp in Unix milliseconds.
612    pub ts: NumberString,
613}
614
615/// Open interest for an instrument.
616#[derive(Debug, Clone, Deserialize)]
617#[serde(rename_all = "camelCase")]
618#[non_exhaustive]
619pub struct OpenInterest {
620    /// Instrument type.
621    pub inst_type: InstType,
622    /// Instrument ID.
623    pub inst_id: String,
624    /// Open interest in contracts.
625    #[serde(default)]
626    pub oi: NumberString,
627    /// Open interest in coin/currency units.
628    #[serde(default)]
629    pub oi_ccy: NumberString,
630    /// Timestamp (Unix milliseconds).
631    #[serde(default)]
632    pub ts: NumberString,
633}
634
635/// Current funding-rate information.
636#[derive(Debug, Clone, Deserialize)]
637#[serde(rename_all = "camelCase")]
638#[non_exhaustive]
639pub struct FundingRate {
640    /// Instrument type.
641    #[serde(default)]
642    pub inst_type: String,
643    /// Instrument ID.
644    pub inst_id: String,
645    /// Current funding rate.
646    #[serde(default)]
647    pub funding_rate: NumberString,
648    /// Next estimated funding rate.
649    #[serde(default)]
650    pub next_funding_rate: NumberString,
651    /// Funding time (Unix milliseconds).
652    #[serde(default)]
653    pub funding_time: NumberString,
654    /// Next funding time (Unix milliseconds).
655    #[serde(default)]
656    pub next_funding_time: NumberString,
657}
658
659/// Historical funding-rate row.
660#[derive(Debug, Clone, Deserialize)]
661#[serde(rename_all = "camelCase")]
662#[non_exhaustive]
663pub struct FundingRateHistory {
664    /// Instrument ID.
665    pub inst_id: String,
666    /// Funding rate.
667    #[serde(default)]
668    pub funding_rate: NumberString,
669    /// Realized funding rate.
670    #[serde(default)]
671    pub realized_rate: NumberString,
672    /// Funding time (Unix milliseconds).
673    #[serde(default)]
674    pub funding_time: NumberString,
675    /// Funding method.
676    #[serde(default)]
677    pub method: String,
678}
679
680/// Price-limit information for an instrument.
681#[derive(Debug, Clone, Deserialize)]
682#[serde(rename_all = "camelCase")]
683#[non_exhaustive]
684pub struct PriceLimit {
685    /// Instrument ID.
686    pub inst_id: String,
687    /// Highest buy price.
688    #[serde(default)]
689    pub buy_lmt: NumberString,
690    /// Lowest sell price.
691    #[serde(default)]
692    pub sell_lmt: NumberString,
693    /// Timestamp (Unix milliseconds).
694    #[serde(default)]
695    pub ts: NumberString,
696}
697
698/// Mark-price information.
699#[derive(Debug, Clone, Deserialize)]
700#[serde(rename_all = "camelCase")]
701#[non_exhaustive]
702pub struct MarkPrice {
703    /// Instrument type.
704    pub inst_type: InstType,
705    /// Instrument ID.
706    pub inst_id: String,
707    /// Mark price.
708    #[serde(default)]
709    pub mark_px: NumberString,
710    /// Timestamp (Unix milliseconds).
711    #[serde(default)]
712    pub ts: NumberString,
713}
714
715/// Delivery/exercise history row.
716#[derive(Debug, Clone, Deserialize)]
717#[serde(rename_all = "camelCase")]
718#[non_exhaustive]
719pub struct DeliveryExercise {
720    /// Instrument type.
721    #[serde(default)]
722    pub inst_type: String,
723    /// Instrument ID.
724    #[serde(default)]
725    pub inst_id: String,
726    /// Delivery/exercise price.
727    #[serde(default)]
728    pub px: NumberString,
729    /// Delivery/exercise type.
730    #[serde(rename = "type", default)]
731    pub exercise_type: String,
732    /// Timestamp (Unix milliseconds).
733    #[serde(default)]
734    pub ts: NumberString,
735}
736
737/// Public position-tier information.
738#[derive(Debug, Clone, Deserialize)]
739#[serde(rename_all = "camelCase")]
740#[non_exhaustive]
741pub struct PositionTier {
742    /// Instrument type.
743    pub inst_type: InstType,
744    /// Trade mode.
745    #[serde(default)]
746    pub td_mode: String,
747    /// Instrument ID.
748    #[serde(default)]
749    pub inst_id: String,
750    /// Tier.
751    #[serde(default)]
752    pub tier: String,
753    /// Minimum size.
754    #[serde(default)]
755    pub min_sz: NumberString,
756    /// Maximum size.
757    #[serde(default)]
758    pub max_sz: NumberString,
759    /// Initial margin rate.
760    #[serde(default)]
761    pub imr: NumberString,
762    /// Maintenance margin rate.
763    #[serde(default)]
764    pub mmr: NumberString,
765}
766
767/// Insurance-fund snapshot.
768#[derive(Debug, Clone, Deserialize)]
769#[serde(rename_all = "camelCase")]
770#[non_exhaustive]
771pub struct InsuranceFund {
772    /// Instrument type.
773    #[serde(default)]
774    pub inst_type: String,
775    /// Fund type.
776    #[serde(rename = "type", default)]
777    pub fund_type: String,
778    /// Currency.
779    #[serde(default)]
780    pub ccy: String,
781    /// Balance amount.
782    #[serde(default)]
783    pub amt: NumberString,
784    /// Timestamp (Unix milliseconds).
785    #[serde(default)]
786    pub ts: NumberString,
787}
788
789/// Contract/coin conversion result.
790#[derive(Debug, Clone, Deserialize)]
791#[serde(rename_all = "camelCase")]
792#[non_exhaustive]
793pub struct ConvertContractCoin {
794    /// Instrument ID.
795    #[serde(default)]
796    pub inst_id: String,
797    /// Converted size.
798    #[serde(default)]
799    pub sz: NumberString,
800    /// Conversion price.
801    #[serde(default)]
802    pub px: NumberString,
803    /// Conversion unit.
804    #[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}