Skip to main content

rust_okx/model/
mod.rs

1//! Shared data types: the [`NumberString`] wrapper, the response envelope, and
2//! the common string enums used across the API modules.
3
4use std::fmt;
5use std::str::FromStr;
6
7use serde::ser::{SerializeMap, SerializeSeq};
8use serde::{Deserialize, Serialize};
9
10/// Deserialize an OKX array field that may be encoded as an empty string when
11/// no entries are available.
12///
13/// A few OKX REST endpoints document these fields as arrays but return `""`
14/// for accounts/currencies without data. Treat only the empty string and
15/// `null` as an empty vector; non-empty strings remain decode errors.
16pub(crate) fn deserialize_vec_or_empty_string<'de, D, T>(
17    deserializer: D,
18) -> Result<Vec<T>, D::Error>
19where
20    D: serde::Deserializer<'de>,
21    T: Deserialize<'de>,
22{
23    #[derive(Deserialize)]
24    #[serde(untagged)]
25    enum WireValue<T> {
26        Sequence(Vec<T>),
27        String(String),
28        Null(()),
29    }
30
31    match WireValue::<T>::deserialize(deserializer)? {
32        WireValue::Sequence(values) => Ok(values),
33        WireValue::String(value) if value.is_empty() => Ok(Vec::new()),
34        WireValue::String(value) => Err(serde::de::Error::custom(format!(
35            "expected an array or empty string, got {value:?}"
36        ))),
37        WireValue::Null(()) => Ok(Vec::new()),
38    }
39}
40
41/// The OKX response envelope: `{ "code": "...", "msg": "...", "data": [...] }`.
42///
43/// Internal — the client unwraps it and returns `data` (or an [`Error::Api`]).
44///
45/// [`Error::Api`]: crate::Error::Api
46#[derive(Debug, Clone, Deserialize)]
47pub(crate) struct OkxResponse<D> {
48    pub code: String,
49    pub msg: String,
50    pub data: D,
51}
52
53/// A numeric value returned by OKX as a JSON string.
54///
55/// OKX encodes all prices, sizes, and balances as strings to avoid floating
56/// point precision loss. `NumberString` preserves the exact wire representation
57/// and lets the caller decide how to interpret it:
58///
59/// ```
60/// use rust_okx::NumberString;
61/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
62/// let px = NumberString::from("42000.1");
63/// assert_eq!(px.as_str(), "42000.1");
64/// let as_f64: f64 = px.parse()?;
65/// assert_eq!(as_f64, 42000.1);
66/// # Ok(())
67/// # }
68/// ```
69#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
70#[serde(transparent)]
71pub struct NumberString(String);
72
73impl NumberString {
74    /// Borrow the raw string value.
75    pub fn as_str(&self) -> &str {
76        &self.0
77    }
78
79    /// Returns `true` if the value is the empty string (OKX uses `""` for
80    /// "not applicable" fields).
81    pub fn is_empty(&self) -> bool {
82        self.0.is_empty()
83    }
84
85    /// Parse the value into any [`FromStr`] type, e.g. `f64`, `i64`, or
86    /// `rust_decimal::Decimal`.
87    pub fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
88        self.0.parse()
89    }
90
91    /// Consume the wrapper and return the inner [`String`].
92    pub fn into_string(self) -> String {
93        self.0
94    }
95
96    /// Parse the value as a [`rust_decimal::Decimal`].
97    #[cfg(feature = "rust-decimal")]
98    pub fn to_decimal(&self) -> Result<rust_decimal::Decimal, rust_decimal::Error> {
99        self.0.parse()
100    }
101}
102
103impl From<String> for NumberString {
104    fn from(s: String) -> Self {
105        NumberString(s)
106    }
107}
108
109impl From<&str> for NumberString {
110    fn from(s: &str) -> Self {
111        NumberString(s.to_owned())
112    }
113}
114
115impl AsRef<str> for NumberString {
116    fn as_ref(&self) -> &str {
117        &self.0
118    }
119}
120
121impl fmt::Display for NumberString {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.write_str(&self.0)
124    }
125}
126
127/// A flexible, untyped request-parameter builder for unsupported or newly
128/// introduced OKX fields.
129///
130/// Prefer endpoint-specific request types whenever one exists. Adding the same
131/// key more than once replaces its previous value while preserving insertion
132/// order, preventing duplicate JSON object keys.
133#[derive(Debug, Clone, Default)]
134pub struct RawRequestParams {
135    fields: Vec<(String, ParamValue)>,
136}
137
138impl RawRequestParams {
139    /// Create an empty parameter set.
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Add or replace a string parameter.
145    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
146        self.set(key.into(), ParamValue::String(value.into()));
147        self
148    }
149
150    /// Add or replace a boolean parameter.
151    pub fn bool_param(mut self, key: impl Into<String>, value: bool) -> Self {
152        self.set(key.into(), ParamValue::Bool(value));
153        self
154    }
155
156    /// Add or replace an array-of-strings parameter.
157    pub fn string_list<I, S>(mut self, key: impl Into<String>, values: I) -> Self
158    where
159        I: IntoIterator<Item = S>,
160        S: Into<String>,
161    {
162        self.set(
163            key.into(),
164            ParamValue::StringList(values.into_iter().map(Into::into).collect()),
165        );
166        self
167    }
168
169    /// Return true when no parameters are set.
170    pub fn is_empty(&self) -> bool {
171        self.fields.is_empty()
172    }
173
174    fn set(&mut self, key: String, value: ParamValue) {
175        if let Some((_, existing)) = self.fields.iter_mut().find(|(name, _)| name == &key) {
176            *existing = value;
177        } else {
178            self.fields.push((key, value));
179        }
180    }
181}
182
183impl Serialize for RawRequestParams {
184    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
185        let mut map = serializer.serialize_map(Some(self.fields.len()))?;
186        for (key, value) in &self.fields {
187            map.serialize_entry(key, value)?;
188        }
189        map.end()
190    }
191}
192
193/// Backward-compatible name for [`RawRequestParams`].
194///
195/// New endpoint implementations should expose typed request structs instead of
196/// accepting this alias directly.
197pub type RequestParams = RawRequestParams;
198
199#[derive(Debug, Clone)]
200enum ParamValue {
201    String(String),
202    Bool(bool),
203    StringList(Vec<String>),
204}
205
206impl Serialize for ParamValue {
207    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
208        match self {
209            Self::String(value) => serializer.serialize_str(value),
210            Self::Bool(value) => serializer.serialize_bool(*value),
211            Self::StringList(values) => {
212                let mut seq = serializer.serialize_seq(Some(values.len()))?;
213                for value in values {
214                    seq.serialize_element(value)?;
215                }
216                seq.end()
217            }
218        }
219    }
220}
221
222/// A broad OKX response row for low-frequency endpoints.
223///
224/// OKX edge endpoints often return sparse, feature-dependent objects. Fields
225/// default when absent so deserialization remains forward-compatible.
226#[derive(Debug, Clone, Default, Deserialize)]
227#[serde(rename_all = "camelCase")]
228#[non_exhaustive]
229pub struct RestRow {
230    /// Instrument type.
231    #[serde(default, rename = "instType")]
232    pub inst_type: String,
233    /// Instrument ID.
234    #[serde(default, rename = "instId")]
235    pub inst_id: String,
236    /// Instrument family.
237    #[serde(default, rename = "instFamily")]
238    pub inst_family: String,
239    /// Currency code.
240    #[serde(default)]
241    pub ccy: String,
242    /// Order ID.
243    #[serde(default, rename = "ordId")]
244    pub ord_id: String,
245    /// Client order ID.
246    #[serde(default, rename = "clOrdId")]
247    pub cl_ord_id: String,
248    /// Algo order ID.
249    #[serde(default, rename = "algoId")]
250    pub algo_id: String,
251    /// Client algo order ID.
252    #[serde(default, rename = "algoClOrdId")]
253    pub algo_cl_ord_id: String,
254    /// Quote ID.
255    #[serde(default, rename = "quoteId")]
256    pub quote_id: String,
257    /// Request ID.
258    #[serde(default, rename = "reqId")]
259    pub req_id: String,
260    /// Product ID.
261    #[serde(default, rename = "productId")]
262    pub product_id: String,
263    /// Operation type.
264    #[serde(default, rename = "type")]
265    pub row_type: String,
266    /// State or status.
267    #[serde(default)]
268    pub state: String,
269    /// Status.
270    #[serde(default)]
271    pub status: String,
272    /// Side.
273    #[serde(default)]
274    pub side: String,
275    /// Amount.
276    #[serde(default)]
277    pub amt: NumberString,
278    /// Size.
279    #[serde(default)]
280    pub sz: NumberString,
281    /// Price.
282    #[serde(default)]
283    pub px: NumberString,
284    /// Rate.
285    #[serde(default)]
286    pub rate: NumberString,
287    /// Balance.
288    #[serde(default)]
289    pub bal: NumberString,
290    /// Available balance.
291    #[serde(default)]
292    pub avail_bal: NumberString,
293    /// Timestamp.
294    #[serde(default)]
295    pub ts: NumberString,
296    /// Success code.
297    #[serde(default, rename = "sCode")]
298    pub s_code: String,
299    /// Success message.
300    #[serde(default, rename = "sMsg")]
301    pub s_msg: String,
302}
303
304/// Defines a string-backed enum that round-trips through the OKX wire format and
305/// tolerates unknown values via an `Unknown(String)` fallback variant. This
306/// keeps response deserialization non-breaking when OKX adds new values.
307macro_rules! string_enum {
308    (
309        $(#[$meta:meta])*
310        $vis:vis enum $name:ident {
311            $( $(#[$vmeta:meta])* $variant:ident = $wire:literal ),* $(,)?
312        }
313    ) => {
314        $(#[$meta])*
315        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
316        #[non_exhaustive]
317        $vis enum $name {
318            $( $(#[$vmeta])* $variant, )*
319            /// A value not modeled by this version of the crate; the raw string
320            /// is preserved.
321            Unknown(String),
322        }
323
324        impl $name {
325            /// The OKX wire representation of this value.
326            pub fn as_str(&self) -> &str {
327                match self {
328                    $( $name::$variant => $wire, )*
329                    $name::Unknown(s) => s.as_str(),
330                }
331            }
332        }
333
334        impl ::core::convert::From<&str> for $name {
335            fn from(s: &str) -> Self {
336                match s {
337                    $( $wire => $name::$variant, )*
338                    other => $name::Unknown(other.to_owned()),
339                }
340            }
341        }
342
343        impl ::core::fmt::Display for $name {
344            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
345                f.write_str(self.as_str())
346            }
347        }
348
349        impl ::serde::Serialize for $name {
350            fn serialize<S: ::serde::Serializer>(&self, ser: S) -> ::core::result::Result<S::Ok, S::Error> {
351                ser.serialize_str(self.as_str())
352            }
353        }
354
355        impl<'de> ::serde::Deserialize<'de> for $name {
356            fn deserialize<D: ::serde::Deserializer<'de>>(de: D) -> ::core::result::Result<Self, D::Error> {
357                let s = <::std::string::String as ::serde::Deserialize>::deserialize(de)?;
358                ::core::result::Result::Ok($name::from(s.as_str()))
359            }
360        }
361    };
362}
363
364string_enum! {
365    /// Instrument type.
366    pub enum InstType {
367        /// Spot.
368        Spot = "SPOT",
369        /// Margin.
370        Margin = "MARGIN",
371        /// Perpetual Futures.
372        Swap = "SWAP",
373        /// Expiry Futures.
374        Futures = "FUTURES",
375        /// Option.
376        Option = "OPTION",
377        /// Event Contracts
378        Events = "EVENTS",
379    }
380}
381
382string_enum! {
383    /// Order side.
384    pub enum OrderSide {
385        /// Buy.
386        Buy = "buy",
387        /// Sell.
388        Sell = "sell",
389    }
390}
391
392string_enum! {
393    /// Order type.
394    pub enum OrderType {
395        /// Market order.
396        Market = "market",
397        /// Limit order.
398        Limit = "limit",
399        /// Post-only order.
400        PostOnly = "post_only",
401        /// Fill-or-kill order.
402        Fok = "fok",
403        /// Immediate-or-cancel order.
404        Ioc = "ioc",
405        /// Market order with immediate-or-cancel (futures/swap).
406        OptimalLimitIoc = "optimal_limit_ioc",
407    }
408}
409
410string_enum! {
411    /// Trade (margin) mode.
412    pub enum TradeMode {
413        /// Non-margin (cash).
414        Cash = "cash",
415        /// Cross margin.
416        Cross = "cross",
417        /// Isolated margin.
418        Isolated = "isolated",
419        /// Spot isolated margin mode.
420        SpotIsolated = "spot_isolated",
421    }
422}
423
424string_enum! {
425    /// Position side.
426    pub enum PositionSide {
427        /// Long position.
428        Long = "long",
429        /// Short position.
430        Short = "short",
431        /// Net position.
432        Net = "net",
433    }
434}
435
436string_enum! {
437    /// Order lifecycle state.
438    pub enum OrderState {
439        /// Resting on the book.
440        Live = "live",
441        /// Partially filled.
442        PartiallyFilled = "partially_filled",
443        /// Fully filled.
444        Filled = "filled",
445        /// Canceled.
446        Canceled = "canceled",
447        /// Canceled by market maker protection.
448        MmpCanceled = "mmp_canceled",
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn number_string_parses_and_preserves() {
458        let n = NumberString::from("1.005");
459        assert_eq!(n.as_str(), "1.005");
460        assert_eq!(n.parse::<f64>().unwrap(), 1.005);
461        assert_eq!(n.into_string(), "1.005");
462    }
463
464    #[test]
465    fn known_enum_value_round_trips() {
466        let v: InstType = serde_json::from_str("\"SWAP\"").unwrap();
467        assert_eq!(v, InstType::Swap);
468        assert_eq!(serde_json::to_string(&v).unwrap(), "\"SWAP\"");
469    }
470
471    #[test]
472    fn unknown_enum_value_is_preserved_not_an_error() {
473        let v: InstType = serde_json::from_str("\"FUTURE_THING\"").unwrap();
474        assert_eq!(v, InstType::Unknown("FUTURE_THING".to_owned()));
475        // And serializes back to the original wire value.
476        assert_eq!(serde_json::to_string(&v).unwrap(), "\"FUTURE_THING\"");
477    }
478
479    #[test]
480    fn raw_request_params_replace_duplicate_keys() {
481        let params = RawRequestParams::new()
482            .param("ccy", "BTC")
483            .param("ccy", "ETH");
484
485        assert_eq!(
486            serde_json::to_value(params).unwrap(),
487            serde_json::json!({"ccy": "ETH"})
488        );
489    }
490}