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