nautilus_okx/http/
query.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Strongly-typed request parameter structures for the OKX **v5 REST API**.
17//!
18//! Each struct corresponds 1-to-1 with an OKX REST endpoint and is annotated
19//! using `serde` so that it can be serialized directly into the query string
20//! or request body expected by the exchange.
21//!
22//! The inline documentation repeats the required/optional fields described in
23//! the [official OKX documentation](https://www.okx.com/docs-v5/en/) and, where
24//! beneficial, links to the exact endpoint section.  All links point to the
25//! English version.
26//!
27//! Example – building a request for historical trades:
28//! ```rust
29//! use nautilus_okx::http::query::{GetTradesParams, GetTradesParamsBuilder};
30//!
31//! let params = GetTradesParamsBuilder::default()
32//!     .inst_id("BTC-USDT")
33//!     .limit(200)
34//!     .build()
35//!     .unwrap();
36//! ```
37//!
38//! Once built these parameter structs are passed to `OKXHttpClient::get`/`post`
39//! where they are automatically serialized.
40
41use derive_builder::Builder;
42use serde::{self, Deserialize, Serialize};
43
44use crate::{
45    common::enums::{
46        OKXInstrumentType, OKXOrderStatus, OKXPositionMode, OKXPositionSide, OKXTradeMode,
47    },
48    http::error::BuildError,
49};
50
51#[allow(dead_code)] // Under development
52fn serialize_string_vec<S>(values: &Option<Vec<String>>, serializer: S) -> Result<S::Ok, S::Error>
53where
54    S: serde::Serializer,
55{
56    match values {
57        Some(vec) => serializer.serialize_str(&vec.join(",")),
58        None => serializer.serialize_none(),
59    }
60}
61
62/// Parameters for the POST /api/v5/account/set-position-mode endpoint.
63#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
64#[builder(setter(into, strip_option))]
65#[serde(rename_all = "camelCase")]
66pub struct SetPositionModeParams {
67    /// Position mode: "net_mode" or "long_short_mode".
68    #[serde(rename = "posMode")]
69    pub pos_mode: OKXPositionMode,
70}
71
72/// Parameters for the GET /api/v5/public/position-tiers endpoint.
73#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
74#[builder(default)]
75#[builder(setter(into, strip_option))]
76#[serde(rename_all = "camelCase")]
77pub struct GetPositionTiersParams {
78    /// Instrument type: MARGIN, SWAP, FUTURES, OPTION.
79    pub inst_type: OKXInstrumentType,
80    /// Trading mode, valid values: cross, isolated.
81    pub td_mode: OKXTradeMode,
82    /// Underlying, required for SWAP/FUTURES/OPTION
83    /// Single underlying or multiple underlyings (no more than 3) separated with comma.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub uly: Option<String>,
86    /// Instrument family, required for SWAP/FUTURES/OPTION
87    /// Single instrument family or multiple families (no more than 5) separated with comma.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub inst_family: Option<String>,
90    /// Specific instrument ID.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub inst_id: Option<String>,
93    /// Margin currency, only applicable to cross MARGIN.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub ccy: Option<String>,
96    /// Tiers.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub tier: Option<String>,
99}
100
101/// Parameters for the GET /api/v5/public/instruments endpoint.
102#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
103#[builder(default)]
104#[builder(setter(into, strip_option))]
105#[serde(rename_all = "camelCase")]
106pub struct GetInstrumentsParams {
107    /// Instrument type: SPOT, MARGIN, SWAP, FUTURES, OPTION.
108    pub inst_type: OKXInstrumentType,
109    /// Underlying. Only applicable to FUTURES/SWAP/OPTION.
110    /// If instType is OPTION, either uly or instFamily is required.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub uly: Option<String>,
113    /// Instrument family. Only applicable to FUTURES/SWAP/OPTION.
114    /// If instType is OPTION, either uly or instFamily is required.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub inst_family: Option<String>,
117    /// Instrument ID, e.g. BTC-USD-SWAP.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub inst_id: Option<String>,
120}
121
122/// Parameters for the GET /api/v5/market/history-trades endpoint.
123#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
124#[builder(default)]
125#[builder(setter(into, strip_option))]
126#[serde(rename_all = "camelCase")]
127pub struct GetTradesParams {
128    /// Instrument ID, e.g. "BTC-USDT".
129    pub inst_id: String,
130    /// Pagination: fetch records after this timestamp.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub after: Option<String>,
133    /// Pagination: fetch records before this timestamp.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub before: Option<String>,
136    /// Maximum number of records to return (default 100, max 1000).
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub limit: Option<u32>,
139}
140
141/// Parameters for the GET /api/v5/market/history-candles endpoint.
142#[derive(Clone, Debug, Deserialize, Serialize)]
143#[serde(rename_all = "camelCase")]
144pub struct GetCandlesticksParams {
145    /// Instrument ID, e.g. "BTC-USDT".
146    pub inst_id: String,
147    /// Time interval, e.g. "1m", "5m", "1H".
148    pub bar: String,
149    /// Pagination: fetch records after this timestamp (milliseconds).
150    #[serde(skip_serializing_if = "Option::is_none")]
151    #[serde(rename = "after")]
152    pub after_ms: Option<i64>,
153    /// Pagination: fetch records before this timestamp (milliseconds).
154    #[serde(skip_serializing_if = "Option::is_none")]
155    #[serde(rename = "before")]
156    pub before_ms: Option<i64>,
157    /// Maximum number of records to return (default 100, max 300 for regular candles, max 100 for history).
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub limit: Option<u32>,
160}
161
162/// Builder for GetCandlesticksParams with validation.
163#[derive(Debug, Default)]
164pub struct GetCandlesticksParamsBuilder {
165    inst_id: Option<String>,
166    bar: Option<String>,
167    after_ms: Option<i64>,
168    before_ms: Option<i64>,
169    limit: Option<u32>,
170}
171
172impl GetCandlesticksParamsBuilder {
173    /// Sets the instrument ID.
174    pub fn inst_id(&mut self, inst_id: impl Into<String>) -> &mut Self {
175        self.inst_id = Some(inst_id.into());
176        self
177    }
178
179    /// Sets the bar interval.
180    pub fn bar(&mut self, bar: impl Into<String>) -> &mut Self {
181        self.bar = Some(bar.into());
182        self
183    }
184
185    /// Sets the after timestamp (milliseconds).
186    pub fn after_ms(&mut self, after_ms: i64) -> &mut Self {
187        self.after_ms = Some(after_ms);
188        self
189    }
190
191    /// Sets the before timestamp (milliseconds).
192    pub fn before_ms(&mut self, before_ms: i64) -> &mut Self {
193        self.before_ms = Some(before_ms);
194        self
195    }
196
197    /// Sets the limit.
198    pub fn limit(&mut self, limit: u32) -> &mut Self {
199        self.limit = Some(limit);
200        self
201    }
202
203    /// Builds the parameters with embedded invariant validation.
204    pub fn build(&mut self) -> Result<GetCandlesticksParams, BuildError> {
205        // Extract values from builder
206        let inst_id = self.inst_id.clone().ok_or(BuildError::MissingInstId)?;
207        let bar = self.bar.clone().ok_or(BuildError::MissingBar)?;
208        let after_ms = self.after_ms;
209        let before_ms = self.before_ms;
210        let limit = self.limit;
211
212        // ───────── Both cursors validation
213        // OKX API doesn't support both 'after' and 'before' parameters together
214        if after_ms.is_some() && before_ms.is_some() {
215            return Err(BuildError::BothCursors);
216        }
217
218        // ───────── Cursor chronological validation
219        // When both after_ms and before_ms are provided as time bounds:
220        // - after_ms represents the start time (older bound)
221        // - before_ms represents the end time (newer bound)
222        // Therefore: after_ms < before_ms for valid time ranges
223        if let (Some(after), Some(before)) = (after_ms, before_ms)
224            && after >= before
225        {
226            return Err(BuildError::InvalidTimeRange {
227                after_ms: after,
228                before_ms: before,
229            });
230        }
231
232        // ───────── Cursor unit (≤ 13 digits ⇒ milliseconds)
233        if let Some(nanos) = after_ms
234            && nanos.abs() > 9_999_999_999_999
235        {
236            return Err(BuildError::CursorIsNanoseconds);
237        }
238
239        if let Some(nanos) = before_ms
240            && nanos.abs() > 9_999_999_999_999
241        {
242            return Err(BuildError::CursorIsNanoseconds);
243        }
244
245        // ───────── Limit validation
246        // Note: Regular endpoint supports up to 300, history endpoint up to 100
247        // This validation is conservative for safety across both endpoints
248        if let Some(limit) = limit
249            && limit > 300
250        {
251            return Err(BuildError::LimitTooHigh);
252        }
253
254        Ok(GetCandlesticksParams {
255            inst_id,
256            bar,
257            after_ms,
258            before_ms,
259            limit,
260        })
261    }
262}
263
264/// Parameters for the GET /api/v5/public/mark-price.
265#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
266#[builder(default)]
267#[builder(setter(into, strip_option))]
268#[serde(rename_all = "camelCase")]
269pub struct GetMarkPriceParams {
270    /// Instrument type: MARGIN, SWAP, FUTURES, OPTION.
271    pub inst_type: OKXInstrumentType,
272    /// Underlying, required for SWAP/FUTURES/OPTION
273    /// Single underlying or multiple underlyings (no more than 3) separated with comma.
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub uly: Option<String>,
276    /// Instrument family, required for SWAP/FUTURES/OPTION
277    /// Single instrument family or multiple families (no more than 5) separated with comma.
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub inst_family: Option<String>,
280    /// Specific instrument ID.
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub inst_id: Option<String>,
283}
284
285/// Parameters for the GET /api/v5/market/index-tickers.
286#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
287#[builder(default)]
288#[builder(setter(into, strip_option))]
289#[serde(rename_all = "camelCase")]
290pub struct GetIndexTickerParams {
291    /// Specific instrument ID.
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub inst_id: Option<String>,
294    /// Quote currency.
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub quote_ccy: Option<String>,
297}
298
299/// Parameters for the GET /api/v5/trade/order-history endpoint.
300#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
301#[builder(default)]
302#[builder(setter(into, strip_option))]
303#[serde(rename_all = "camelCase")]
304pub struct GetOrderHistoryParams {
305    /// Instrument type: SPOT, MARGIN, SWAP, FUTURES, OPTION.
306    pub inst_type: OKXInstrumentType,
307    /// Underlying, for FUTURES, SWAP, OPTION (optional).
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub uly: Option<String>,
310    /// Instrument family, for FUTURES, SWAP, OPTION (optional).
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub inst_family: Option<String>,
313    /// Instrument ID, e.g. "BTC-USD-SWAP" (optional).
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub inst_id: Option<String>,
316    /// Order type: limit, market, post_only, fok, ioc (optional).
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub ord_type: Option<String>,
319    /// Order state: live, filled, canceled (optional).
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub state: Option<String>,
322    /// Pagination parameter: fetch records after this order ID or timestamp (optional).
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub after: Option<String>,
325    /// Pagination parameter: fetch records before this order ID or timestamp (optional).
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub before: Option<String>,
328    /// Maximum number of records to return (default 100, max 100) (optional).
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub limit: Option<u32>,
331}
332
333/// Parameters for the GET /api/v5/trade/orders-pending endpoint.
334#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
335#[builder(default)]
336#[builder(setter(into, strip_option))]
337#[serde(rename_all = "camelCase")]
338pub struct GetOrderListParams {
339    /// Instrument type: SPOT, MARGIN, SWAP, FUTURES, OPTION.
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub inst_type: Option<OKXInstrumentType>,
342    /// Instrument ID, e.g. "BTC-USDT" (optional).
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub inst_id: Option<String>,
345    /// Instrument family, e.g. "BTC-USD" (optional).
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub inst_family: Option<String>,
348    /// State to filter for (optional).
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub state: Option<OKXOrderStatus>,
351    /// Pagination - fetch records **after** this order ID (optional).
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub after: Option<String>,
354    /// Pagination - fetch records **before** this order ID (optional).
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub before: Option<String>,
357    /// Number of results per request (default 100, max 100).
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub limit: Option<u32>,
360}
361
362/// Parameters for the GET /api/v5/trade/fills endpoint (transaction details).
363#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
364#[builder(default)]
365#[builder(setter(into, strip_option))]
366#[serde(rename_all = "camelCase")]
367pub struct GetTransactionDetailsParams {
368    /// Instrument type: SPOT, MARGIN, SWAP, FUTURES, OPTION (optional).
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub inst_type: Option<OKXInstrumentType>,
371    /// Instrument ID, e.g. "BTC-USDT" (optional).
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub inst_id: Option<String>,
374    /// Order ID (optional).
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub ord_id: Option<String>,
377    /// Pagination of data to return records earlier than the requested ID (optional).
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub after: Option<String>,
380    /// Pagination of data to return records newer than the requested ID (optional).
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub before: Option<String>,
383    /// Number of results per request (optional, default 100, max 100).
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub limit: Option<u32>,
386}
387
388/// Parameters for the GET /api/v5/public/positions endpoint.
389#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
390#[builder(default)]
391#[builder(setter(into, strip_option))]
392#[serde(rename_all = "camelCase")]
393pub struct GetPositionsParams {
394    /// Instrument type: MARGIN, SWAP, FUTURES, OPTION.
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub inst_type: Option<OKXInstrumentType>,
397    /// Specific instrument ID.
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub inst_id: Option<String>,
400    /// Single position ID or multiple position IDs (no more than 20) separated with comma.
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub pos_id: Option<String>,
403}
404
405/// Parameters for the GET /api/v5/account/positions-history endpoint.
406#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
407#[builder(default)]
408#[builder(setter(into, strip_option))]
409#[serde(rename_all = "camelCase")]
410pub struct GetPositionsHistoryParams {
411    /// Instrument type: MARGIN, SWAP, FUTURES, OPTION.
412    pub inst_type: OKXInstrumentType,
413    /// Instrument ID, e.g. "BTC-USD-SWAP" (optional).
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub inst_id: Option<String>,
416    /// One or more position IDs, separated by commas (optional).
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub pos_id: Option<String>,
419    /// Pagination parameter - requests records **after** this ID or timestamp (optional).
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub after: Option<String>,
422    /// Pagination parameter - requests records **before** this ID or timestamp (optional).
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub before: Option<String>,
425    /// Number of results per request (default 100, max 100).
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub limit: Option<u32>,
428}
429
430/// Parameters for the GET /api/v5/trade/orders-pending endpoint.
431#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
432#[builder(default)]
433#[builder(setter(into, strip_option))]
434#[serde(rename_all = "camelCase")]
435pub struct GetPendingOrdersParams {
436    /// Instrument type: SPOT, MARGIN, SWAP, FUTURES, OPTION.
437    pub inst_type: OKXInstrumentType,
438    /// Instrument ID, e.g. "BTC-USDT".
439    pub inst_id: String,
440    /// Position side (optional).
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub pos_side: Option<OKXPositionSide>,
443}
444
445/// Parameters for the GET /api/v5/trade/order endpoint (fetch order details).
446#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder)]
447#[builder(default)]
448#[builder(setter(into, strip_option))]
449#[serde(rename_all = "camelCase")]
450pub struct GetOrderParams {
451    /// Instrument type: SPOT, MARGIN, SWAP, FUTURES, OPTION.
452    pub inst_type: OKXInstrumentType,
453    /// Instrument ID, e.g. "BTC-USDT".
454    pub inst_id: String,
455    /// Exchange-assigned order ID (optional if client order ID is provided).
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub ord_id: Option<String>,
458    /// User-assigned client order ID (optional if order ID is provided).
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub cl_ord_id: Option<String>,
461    /// Position side (optional).
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub pos_side: Option<OKXPositionSide>,
464}