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
108            .get(FUNDING_RATE_HISTORY, request, false)
109            .await
110    }
111
112    /// Retrieve the price limit for an instrument.
113    ///
114    /// `GET /api/v5/public/price-limit`. Public.
115    ///
116    /// # Errors
117    ///
118    /// See [`get_system_time`](Self::get_system_time).
119    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    /// Retrieve mark prices.
125    ///
126    /// `GET /api/v5/public/mark-price`. Public.
127    ///
128    /// # Errors
129    ///
130    /// See [`get_system_time`](Self::get_system_time).
131    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    /// Retrieve delivery/exercise history.
139    ///
140    /// `GET /api/v5/public/delivery-exercise-history`. Public.
141    ///
142    /// # Errors
143    ///
144    /// See [`get_system_time`](Self::get_system_time).
145    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    /// Retrieve position tiers.
155    ///
156    /// `GET /api/v5/public/position-tiers`. Public.
157    ///
158    /// # Errors
159    ///
160    /// See [`get_system_time`](Self::get_system_time).
161    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    /// Retrieve underlying values for an instrument type.
169    ///
170    /// `GET /api/v5/public/underlying`. Public.
171    ///
172    /// # Errors
173    ///
174    /// See [`get_system_time`](Self::get_system_time).
175    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    /// Retrieve insurance-fund snapshots.
183    ///
184    /// `GET /api/v5/public/insurance-fund`. Public.
185    ///
186    /// # Errors
187    ///
188    /// See [`get_system_time`](Self::get_system_time).
189    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    /// Convert between contract count and coin amount.
197    ///
198    /// `GET /api/v5/public/convert-contract-coin`. Public.
199    ///
200    /// # Errors
201    ///
202    /// See [`get_system_time`](Self::get_system_time).
203    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/// Query parameters for public endpoints filtered by instrument family.
235#[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    /// Create a query for an instrument type.
249    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    /// Set the underlying filter.
259    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
260        self.underlying = Some(underlying.into());
261        self
262    }
263
264    /// Set the instrument ID filter.
265    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    /// Set the instrument family filter.
271    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/// Query parameters for funding-rate history.
278#[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    /// Create a funding-rate history query.
292    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    /// Return records after this pagination cursor.
302    pub fn after(mut self, after: impl Into<String>) -> Self {
303        self.after = Some(after.into());
304        self
305    }
306
307    /// Return records before this pagination cursor.
308    pub fn before(mut self, before: impl Into<String>) -> Self {
309        self.before = Some(before.into());
310        self
311    }
312
313    /// Set the maximum number of rows to return.
314    pub fn limit(mut self, limit: u32) -> Self {
315        self.limit = Some(limit);
316        self
317    }
318}
319
320/// Query parameters for delivery/exercise history.
321#[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    /// Create a delivery/exercise history query.
339    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    /// Set the underlying filter.
351    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
352        self.underlying = Some(underlying.into());
353        self
354    }
355
356    /// Set the instrument family filter.
357    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    /// Return records after this pagination cursor.
363    pub fn after(mut self, after: impl Into<String>) -> Self {
364        self.after = Some(after.into());
365        self
366    }
367
368    /// Return records before this pagination cursor.
369    pub fn before(mut self, before: impl Into<String>) -> Self {
370        self.before = Some(before.into());
371        self
372    }
373
374    /// Set the maximum number of rows to return.
375    pub fn limit(mut self, limit: u32) -> Self {
376        self.limit = Some(limit);
377        self
378    }
379}
380
381/// Query parameters for public position tiers.
382#[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    /// Create a position-tiers query.
402    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    /// Set the underlying filter.
415    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
416        self.underlying = Some(underlying.into());
417        self
418    }
419
420    /// Set the instrument ID filter.
421    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    /// Set the currency filter.
427    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
428        self.ccy = Some(ccy.into());
429        self
430    }
431
432    /// Set the tier filter.
433    pub fn tier(mut self, tier: impl Into<String>) -> Self {
434        self.tier = Some(tier.into());
435        self
436    }
437
438    /// Set the instrument family filter.
439    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/// Query parameters for insurance fund snapshots.
446#[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    /// Create an insurance fund query.
468    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    /// Set the OKX fund type filter.
482    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    /// Set the underlying filter.
488    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
489        self.underlying = Some(underlying.into());
490        self
491    }
492
493    /// Set the currency filter.
494    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
495        self.ccy = Some(ccy.into());
496        self
497    }
498
499    /// Return records before this pagination cursor.
500    pub fn before(mut self, before: impl Into<String>) -> Self {
501        self.before = Some(before.into());
502        self
503    }
504
505    /// Return records after this pagination cursor.
506    pub fn after(mut self, after: impl Into<String>) -> Self {
507        self.after = Some(after.into());
508        self
509    }
510
511    /// Set the maximum number of rows to return.
512    pub fn limit(mut self, limit: u32) -> Self {
513        self.limit = Some(limit);
514        self
515    }
516
517    /// Set the instrument family filter.
518    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/// Query parameters for contract/coin conversion.
525#[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    /// Create a contract/coin conversion query.
540    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    /// Set the price used for conversion.
555    pub fn price(mut self, px: impl Into<String>) -> Self {
556        self.px = Some(px.into());
557        self
558    }
559
560    /// Set the unit used for conversion.
561    pub fn unit(mut self, unit: impl Into<String>) -> Self {
562        self.unit = Some(unit.into());
563        self
564    }
565}
566
567/// A tradable instrument.
568///
569/// Only commonly used fields are modeled; the struct is `#[non_exhaustive]` and
570/// unknown JSON fields are ignored, so OKX additions are non-breaking.
571#[derive(Debug, Clone, Deserialize)]
572#[serde(rename_all = "camelCase")]
573#[non_exhaustive]
574pub struct Instrument {
575    /// Instrument type.
576    pub inst_type: InstType,
577    /// Instrument ID, e.g. `BTC-USDT`.
578    pub inst_id: String,
579    /// Underlying, e.g. `BTC-USD` (derivatives only).
580    #[serde(default)]
581    pub uly: String,
582    /// Instrument family, e.g. `BTC-USD` (derivatives only).
583    #[serde(default)]
584    pub inst_family: String,
585    /// Base currency, e.g. `BTC` (spot/margin only).
586    #[serde(default)]
587    pub base_ccy: String,
588    /// Quote currency, e.g. `USDT` (spot/margin only).
589    #[serde(default)]
590    pub quote_ccy: String,
591    /// Settlement currency (derivatives only).
592    #[serde(default)]
593    pub settle_ccy: String,
594    /// Lot size (order size increment).
595    #[serde(default)]
596    pub lot_sz: NumberString,
597    /// Tick size (price increment).
598    #[serde(default)]
599    pub tick_sz: NumberString,
600    /// Minimum order size.
601    #[serde(default)]
602    pub min_sz: NumberString,
603    /// Instrument lifecycle state, e.g. `live`, `suspend`.
604    #[serde(default)]
605    pub state: String,
606}
607
608/// OKX system time.
609#[derive(Debug, Clone, Deserialize)]
610#[serde(rename_all = "camelCase")]
611#[non_exhaustive]
612pub struct SystemTime {
613    /// Current OKX system timestamp in Unix milliseconds.
614    pub ts: NumberString,
615}
616
617/// Open interest for an instrument.
618#[derive(Debug, Clone, Deserialize)]
619#[serde(rename_all = "camelCase")]
620#[non_exhaustive]
621pub struct OpenInterest {
622    /// Instrument type.
623    pub inst_type: InstType,
624    /// Instrument ID.
625    pub inst_id: String,
626    /// Open interest in contracts.
627    #[serde(default)]
628    pub oi: NumberString,
629    /// Open interest in coin/currency units.
630    #[serde(default)]
631    pub oi_ccy: NumberString,
632    /// Timestamp (Unix milliseconds).
633    #[serde(default)]
634    pub ts: NumberString,
635}
636
637/// Current funding-rate information.
638#[derive(Debug, Clone, Deserialize)]
639#[serde(rename_all = "camelCase")]
640#[non_exhaustive]
641pub struct FundingRate {
642    /// Instrument type.
643    #[serde(default)]
644    pub inst_type: String,
645    /// Instrument ID.
646    pub inst_id: String,
647    /// Current funding rate.
648    #[serde(default)]
649    pub funding_rate: NumberString,
650    /// Next estimated funding rate.
651    #[serde(default)]
652    pub next_funding_rate: NumberString,
653    /// Funding time (Unix milliseconds).
654    #[serde(default)]
655    pub funding_time: NumberString,
656    /// Next funding time (Unix milliseconds).
657    #[serde(default)]
658    pub next_funding_time: NumberString,
659}
660
661/// Historical funding-rate row.
662#[derive(Debug, Clone, Deserialize)]
663#[serde(rename_all = "camelCase")]
664#[non_exhaustive]
665pub struct FundingRateHistory {
666    /// Instrument ID.
667    pub inst_id: String,
668    /// Funding rate.
669    #[serde(default)]
670    pub funding_rate: NumberString,
671    /// Realized funding rate.
672    #[serde(default)]
673    pub realized_rate: NumberString,
674    /// Funding time (Unix milliseconds).
675    #[serde(default)]
676    pub funding_time: NumberString,
677    /// Funding method.
678    #[serde(default)]
679    pub method: String,
680}
681
682/// Price-limit information for an instrument.
683#[derive(Debug, Clone, Deserialize)]
684#[serde(rename_all = "camelCase")]
685#[non_exhaustive]
686pub struct PriceLimit {
687    /// Instrument ID.
688    pub inst_id: String,
689    /// Highest buy price.
690    #[serde(default)]
691    pub buy_lmt: NumberString,
692    /// Lowest sell price.
693    #[serde(default)]
694    pub sell_lmt: NumberString,
695    /// Timestamp (Unix milliseconds).
696    #[serde(default)]
697    pub ts: NumberString,
698}
699
700/// Mark-price information.
701#[derive(Debug, Clone, Deserialize)]
702#[serde(rename_all = "camelCase")]
703#[non_exhaustive]
704pub struct MarkPrice {
705    /// Instrument type.
706    pub inst_type: InstType,
707    /// Instrument ID.
708    pub inst_id: String,
709    /// Mark price.
710    #[serde(default)]
711    pub mark_px: NumberString,
712    /// Timestamp (Unix milliseconds).
713    #[serde(default)]
714    pub ts: NumberString,
715}
716
717/// Delivery/exercise history row.
718#[derive(Debug, Clone, Deserialize)]
719#[serde(rename_all = "camelCase")]
720#[non_exhaustive]
721pub struct DeliveryExercise {
722    /// Instrument type.
723    #[serde(default)]
724    pub inst_type: String,
725    /// Instrument ID.
726    #[serde(default)]
727    pub inst_id: String,
728    /// Delivery/exercise price.
729    #[serde(default)]
730    pub px: NumberString,
731    /// Delivery/exercise type.
732    #[serde(rename = "type", default)]
733    pub exercise_type: String,
734    /// Timestamp (Unix milliseconds).
735    #[serde(default)]
736    pub ts: NumberString,
737}
738
739/// Public position-tier information.
740#[derive(Debug, Clone, Deserialize)]
741#[serde(rename_all = "camelCase")]
742#[non_exhaustive]
743pub struct PositionTier {
744    /// Instrument type.
745    pub inst_type: InstType,
746    /// Trade mode.
747    #[serde(default)]
748    pub td_mode: String,
749    /// Instrument ID.
750    #[serde(default)]
751    pub inst_id: String,
752    /// Tier.
753    #[serde(default)]
754    pub tier: String,
755    /// Minimum size.
756    #[serde(default)]
757    pub min_sz: NumberString,
758    /// Maximum size.
759    #[serde(default)]
760    pub max_sz: NumberString,
761    /// Initial margin rate.
762    #[serde(default)]
763    pub imr: NumberString,
764    /// Maintenance margin rate.
765    #[serde(default)]
766    pub mmr: NumberString,
767}
768
769/// Insurance-fund snapshot.
770#[derive(Debug, Clone, Deserialize)]
771#[serde(rename_all = "camelCase")]
772#[non_exhaustive]
773pub struct InsuranceFund {
774    /// Instrument type.
775    #[serde(default)]
776    pub inst_type: String,
777    /// Fund type.
778    #[serde(rename = "type", default)]
779    pub fund_type: String,
780    /// Currency.
781    #[serde(default)]
782    pub ccy: String,
783    /// Balance amount.
784    #[serde(default)]
785    pub amt: NumberString,
786    /// Timestamp (Unix milliseconds).
787    #[serde(default)]
788    pub ts: NumberString,
789}
790
791/// Contract/coin conversion result.
792#[derive(Debug, Clone, Deserialize)]
793#[serde(rename_all = "camelCase")]
794#[non_exhaustive]
795pub struct ConvertContractCoin {
796    /// Instrument ID.
797    #[serde(default)]
798    pub inst_id: String,
799    /// Converted size.
800    #[serde(default)]
801    pub sz: NumberString,
802    /// Conversion price.
803    #[serde(default)]
804    pub px: NumberString,
805    /// Conversion unit.
806    #[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}