Skip to main content

rust_okx/api/account/requests/
trading.rs

1use serde::Serialize;
2
3use crate::model::{
4    InstType, PositionSide, RequestValidationError, TradeMode, ValidateRequest, at_least_one,
5    non_empty, one_of, optional_non_empty, optional_one_of, optional_positive_decimal_string,
6    positive_decimal_string, reject_when_present, require_when,
7};
8
9/// Request body for setting leverage.
10#[derive(Debug, Clone, Serialize)]
11pub struct SetLeverageRequest {
12    lever: String,
13    #[serde(rename = "mgnMode")]
14    mgn_mode: TradeMode,
15    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
16    inst_id: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    ccy: Option<String>,
19    #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
20    pos_side: Option<PositionSide>,
21}
22
23impl SetLeverageRequest {
24    /// Create a leverage-setting request.
25    pub fn new(lever: impl Into<String>, mgn_mode: TradeMode) -> Self {
26        Self {
27            lever: lever.into(),
28            mgn_mode,
29            inst_id: None,
30            ccy: None,
31            pos_side: None,
32        }
33    }
34
35    /// Set the instrument ID.
36    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
37        self.inst_id = Some(inst_id.into());
38        self
39    }
40
41    /// Set the currency.
42    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
43        self.ccy = Some(ccy.into());
44        self
45    }
46
47    /// Set the position side.
48    pub fn position_side(mut self, pos_side: PositionSide) -> Self {
49        self.pos_side = Some(pos_side);
50        self
51    }
52}
53
54/// Query parameters for retrieving leverage.
55#[derive(Debug, Clone, Serialize)]
56pub struct LeverageRequest {
57    #[serde(rename = "mgnMode")]
58    mgn_mode: TradeMode,
59    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
60    inst_id: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    ccy: Option<String>,
63}
64
65impl LeverageRequest {
66    /// Create a leverage-info query.
67    pub fn new(mgn_mode: TradeMode) -> Self {
68        Self {
69            mgn_mode,
70            inst_id: None,
71            ccy: None,
72        }
73    }
74
75    /// Set the instrument ID.
76    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
77        self.inst_id = Some(inst_id.into());
78        self
79    }
80
81    /// Set the currency.
82    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
83        self.ccy = Some(ccy.into());
84        self
85    }
86}
87
88/// Query parameters for maximum order size.
89#[derive(Debug, Clone, Serialize)]
90pub struct MaxOrderSizeRequest {
91    #[serde(rename = "instId")]
92    inst_id: String,
93    #[serde(rename = "tdMode")]
94    td_mode: TradeMode,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    ccy: Option<String>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    px: Option<String>,
99    #[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
100    trade_quote_ccy: Option<String>,
101}
102
103impl MaxOrderSizeRequest {
104    /// Create a maximum-order-size query.
105    pub fn new(inst_id: impl Into<String>, td_mode: TradeMode) -> Self {
106        Self {
107            inst_id: inst_id.into(),
108            td_mode,
109            ccy: None,
110            px: None,
111            trade_quote_ccy: None,
112        }
113    }
114
115    /// Set the currency.
116    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
117        self.ccy = Some(ccy.into());
118        self
119    }
120
121    /// Set the price.
122    pub fn price(mut self, px: impl Into<String>) -> Self {
123        self.px = Some(px.into());
124        self
125    }
126
127    /// Set the trade quote currency.
128    pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
129        self.trade_quote_ccy = Some(trade_quote_ccy.into());
130        self
131    }
132}
133
134/// Query parameters for maximum available size.
135#[derive(Debug, Clone, Serialize)]
136pub struct MaxAvailableSizeRequest {
137    #[serde(rename = "instId")]
138    inst_id: String,
139    #[serde(rename = "tdMode")]
140    td_mode: TradeMode,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    ccy: Option<String>,
143    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
144    reduce_only: Option<bool>,
145    #[serde(rename = "unSpotOffset", skip_serializing_if = "Option::is_none")]
146    un_spot_offset: Option<bool>,
147    #[serde(rename = "quickMgnType", skip_serializing_if = "Option::is_none")]
148    quick_mgn_type: Option<String>,
149    #[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
150    trade_quote_ccy: Option<String>,
151}
152
153impl MaxAvailableSizeRequest {
154    /// Create a maximum-available-size query.
155    pub fn new(inst_id: impl Into<String>, td_mode: TradeMode) -> Self {
156        Self {
157            inst_id: inst_id.into(),
158            td_mode,
159            ccy: None,
160            reduce_only: None,
161            un_spot_offset: None,
162            quick_mgn_type: None,
163            trade_quote_ccy: None,
164        }
165    }
166
167    /// Set the currency.
168    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
169        self.ccy = Some(ccy.into());
170        self
171    }
172
173    /// Set the reduce-only filter.
174    pub fn reduce_only(mut self, reduce_only: bool) -> Self {
175        self.reduce_only = Some(reduce_only);
176        self
177    }
178
179    /// Set the spot offset flag.
180    pub fn un_spot_offset(mut self, un_spot_offset: bool) -> Self {
181        self.un_spot_offset = Some(un_spot_offset);
182        self
183    }
184
185    /// Set the quick margin type.
186    pub fn quick_margin_type(mut self, quick_mgn_type: impl Into<String>) -> Self {
187        self.quick_mgn_type = Some(quick_mgn_type.into());
188        self
189    }
190
191    /// Set the trade quote currency.
192    pub fn trade_quote_currency(mut self, trade_quote_ccy: impl Into<String>) -> Self {
193        self.trade_quote_ccy = Some(trade_quote_ccy.into());
194        self
195    }
196}
197
198/// Request body for adjusting position margin.
199#[derive(Debug, Clone, Serialize)]
200pub struct AdjustMarginRequest {
201    #[serde(rename = "instId")]
202    inst_id: String,
203    #[serde(rename = "posSide")]
204    pos_side: PositionSide,
205    #[serde(rename = "type")]
206    action: String,
207    amt: String,
208    #[serde(rename = "loanTrans", skip_serializing_if = "Option::is_none")]
209    loan_trans: Option<bool>,
210}
211
212impl AdjustMarginRequest {
213    /// Create a margin-adjustment request.
214    pub fn new(
215        inst_id: impl Into<String>,
216        pos_side: PositionSide,
217        action: impl Into<String>,
218        amt: impl Into<String>,
219    ) -> Self {
220        Self {
221            inst_id: inst_id.into(),
222            pos_side,
223            action: action.into(),
224            amt: amt.into(),
225            loan_trans: None,
226        }
227    }
228
229    /// Set whether to allow loan transfer.
230    pub fn loan_transfer(mut self, loan_trans: bool) -> Self {
231        self.loan_trans = Some(loan_trans);
232        self
233    }
234}
235
236/// Query parameters for trade fee rates.
237#[derive(Debug, Clone, Serialize)]
238pub struct FeeRatesRequest {
239    #[serde(rename = "instType")]
240    inst_type: InstType,
241    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
242    inst_id: Option<String>,
243    #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
244    underlying: Option<String>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    category: Option<String>,
247    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
248    inst_family: Option<String>,
249}
250
251impl FeeRatesRequest {
252    /// Create a fee-rates query.
253    pub fn new(inst_type: InstType) -> Self {
254        Self {
255            inst_type,
256            inst_id: None,
257            underlying: None,
258            category: None,
259            inst_family: None,
260        }
261    }
262
263    /// Set the instrument ID.
264    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
265        self.inst_id = Some(inst_id.into());
266        self
267    }
268
269    /// Set the underlying.
270    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
271        self.underlying = Some(underlying.into());
272        self
273    }
274
275    /// Set the fee category.
276    pub fn category(mut self, category: impl Into<String>) -> Self {
277        self.category = Some(category.into());
278        self
279    }
280
281    /// Set the instrument family.
282    pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
283        self.inst_family = Some(inst_family.into());
284        self
285    }
286}
287
288/// Query parameters for account instruments.
289#[derive(Debug, Clone, Serialize)]
290pub struct AccountInstrumentsRequest {
291    #[serde(rename = "instType")]
292    inst_type: InstType,
293    #[serde(rename = "seriesId", skip_serializing_if = "Option::is_none")]
294    series_id: Option<String>,
295    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
296    inst_family: Option<String>,
297    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
298    inst_id: Option<String>,
299}
300
301impl AccountInstrumentsRequest {
302    /// Create an empty account-instruments query.
303    pub fn new(inst_type: InstType) -> Self {
304        Self {
305            inst_type,
306            series_id: None,
307            inst_family: None,
308            inst_id: None,
309        }
310    }
311
312    /// Set the series_id filter.
313    pub fn series_id(mut self, series_id: impl Into<String>) -> Self {
314        self.series_id = Some(series_id.into());
315        self
316    }
317
318    /// Set the instrument family filter.
319    pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
320        self.inst_family = Some(inst_family.into());
321        self
322    }
323
324    /// Set the instrument ID filter.
325    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
326        self.inst_id = Some(inst_id.into());
327        self
328    }
329}
330
331impl ValidateRequest for AccountInstrumentsRequest {
332    fn validate(&self) -> Result<(), RequestValidationError> {
333        // Setting Option<String> disallow empty string.
334        optional_non_empty("seriesId", self.series_id.as_deref())?;
335        optional_non_empty("instFamily", self.inst_family.as_deref())?;
336        optional_non_empty("instId", self.inst_id.as_deref())?;
337
338        match &self.inst_type {
339            InstType::Events => {
340                // Series ID, e.g. BTC-ABOVE-DAILY. Required when instType is EVENTS
341                require_when("seriesId", self.series_id.as_deref(), "instType is EVENTS")?;
342                reject_when_present(
343                    "instFamily",
344                    self.inst_family.as_ref(),
345                    "instType is EVENTS",
346                )?;
347            }
348
349            // Only applicable to FUTURES/SWAP/OPTION. If instType is OPTION, instFamily is required.
350            InstType::Option => {
351                require_when(
352                    "instFamily",
353                    self.inst_family.as_deref(),
354                    "instType is OPTION",
355                )?;
356            }
357
358            InstType::Futures | InstType::Swap => {}
359
360            InstType::Spot | InstType::Margin => {
361                // InstFamily is not applicable to SPOT/MARGIN。
362                reject_when_present(
363                    "instFamily",
364                    self.inst_family.as_ref(),
365                    "instType is SPOT or MARGIN",
366                )?;
367            }
368
369            InstType::Unknown(_) => {
370                return Err(RequestValidationError::InvalidFormat {
371                    field: "instType",
372                    expected: "SPOT, MARGIN, SWAP, FUTURES, OPTION, or EVENTS",
373                });
374            }
375        }
376
377        Ok(())
378    }
379}
380
381fn validate_trade_mode(
382    field: &'static str,
383    mode: &TradeMode,
384    allow_cash: bool,
385    allow_spot_isolated: bool,
386) -> Result<(), RequestValidationError> {
387    match mode {
388        TradeMode::Cross | TradeMode::Isolated => Ok(()),
389        TradeMode::Cash if allow_cash => Ok(()),
390        TradeMode::SpotIsolated if allow_spot_isolated => Ok(()),
391        _ => Err(RequestValidationError::InvalidFormat {
392            field,
393            expected: if allow_spot_isolated {
394                "cash, cross, isolated, or spot_isolated"
395            } else if allow_cash {
396                "cash, cross, or isolated"
397            } else {
398                "cross or isolated"
399            },
400        }),
401    }
402}
403
404fn validate_position_side(value: Option<&PositionSide>) -> Result<(), RequestValidationError> {
405    if matches!(value, Some(PositionSide::Unknown(_))) {
406        return Err(RequestValidationError::InvalidFormat {
407            field: "posSide",
408            expected: "long, short, or net",
409        });
410    }
411    Ok(())
412}
413
414impl ValidateRequest for SetLeverageRequest {
415    fn validate(&self) -> Result<(), RequestValidationError> {
416        positive_decimal_string("lever", &self.lever)?;
417        validate_trade_mode("mgnMode", &self.mgn_mode, false, false)?;
418        optional_non_empty("instId", self.inst_id.as_deref())?;
419        optional_non_empty("ccy", self.ccy.as_deref())?;
420        validate_position_side(self.pos_side.as_ref())?;
421        at_least_one("instId, ccy", &[self.inst_id.is_some(), self.ccy.is_some()])?;
422        if matches!(self.mgn_mode, TradeMode::Isolated) {
423            require_when("instId", self.inst_id.as_deref(), "mgnMode is isolated")?;
424            reject_when_present("ccy", self.ccy.as_ref(), "mgnMode is isolated")?;
425        }
426        Ok(())
427    }
428}
429
430impl ValidateRequest for LeverageRequest {
431    fn validate(&self) -> Result<(), RequestValidationError> {
432        validate_trade_mode("mgnMode", &self.mgn_mode, false, false)?;
433        optional_non_empty("instId", self.inst_id.as_deref())?;
434        optional_non_empty("ccy", self.ccy.as_deref())?;
435        at_least_one("instId, ccy", &[self.inst_id.is_some(), self.ccy.is_some()])?;
436        if matches!(self.mgn_mode, TradeMode::Isolated) {
437            require_when("instId", self.inst_id.as_deref(), "mgnMode is isolated")?;
438            reject_when_present("ccy", self.ccy.as_ref(), "mgnMode is isolated")?;
439        }
440        Ok(())
441    }
442}
443
444impl ValidateRequest for MaxOrderSizeRequest {
445    fn validate(&self) -> Result<(), RequestValidationError> {
446        non_empty("instId", &self.inst_id)?;
447        validate_trade_mode("tdMode", &self.td_mode, true, true)?;
448        optional_non_empty("ccy", self.ccy.as_deref())?;
449        optional_positive_decimal_string("px", self.px.as_deref())?;
450        optional_non_empty("tradeQuoteCcy", self.trade_quote_ccy.as_deref())?;
451        Ok(())
452    }
453}
454
455impl ValidateRequest for MaxAvailableSizeRequest {
456    fn validate(&self) -> Result<(), RequestValidationError> {
457        non_empty("instId", &self.inst_id)?;
458        validate_trade_mode("tdMode", &self.td_mode, true, true)?;
459        optional_non_empty("ccy", self.ccy.as_deref())?;
460        optional_one_of(
461            "quickMgnType",
462            self.quick_mgn_type.as_deref(),
463            &["manual", "auto_borrow", "auto_repay"],
464            "manual, auto_borrow, or auto_repay",
465        )?;
466        optional_non_empty("tradeQuoteCcy", self.trade_quote_ccy.as_deref())?;
467        Ok(())
468    }
469}
470
471impl ValidateRequest for AdjustMarginRequest {
472    fn validate(&self) -> Result<(), RequestValidationError> {
473        non_empty("instId", &self.inst_id)?;
474        validate_position_side(Some(&self.pos_side))?;
475        one_of("type", &self.action, &["add", "reduce"], "add or reduce")?;
476        positive_decimal_string("amt", &self.amt)
477    }
478}
479
480impl ValidateRequest for FeeRatesRequest {
481    fn validate(&self) -> Result<(), RequestValidationError> {
482        if matches!(self.inst_type, InstType::Events | InstType::Unknown(_)) {
483            return Err(RequestValidationError::InvalidFormat {
484                field: "instType",
485                expected: "SPOT, MARGIN, SWAP, FUTURES, or OPTION",
486            });
487        }
488        optional_non_empty("instId", self.inst_id.as_deref())?;
489        optional_non_empty("uly", self.underlying.as_deref())?;
490        optional_one_of("category", self.category.as_deref(), &["1", "2"], "1 or 2")?;
491        optional_non_empty("instFamily", self.inst_family.as_deref())?;
492        if matches!(self.inst_type, InstType::Option) {
493            at_least_one(
494                "instId, uly, instFamily",
495                &[
496                    self.inst_id.is_some(),
497                    self.underlying.is_some(),
498                    self.inst_family.is_some(),
499                ],
500            )?;
501        }
502        Ok(())
503    }
504}