Skip to main content

rust_okx/api/trade/requests/
algo.rs

1use std::borrow::Cow;
2
3use serde::Serialize;
4
5/// One attached take-profit/stop-loss definition for an algo order.
6#[derive(Debug, Clone, Default, Serialize)]
7pub struct AttachedAlgoOrderRequest {
8    #[serde(rename = "attachAlgoClOrdId", skip_serializing_if = "Option::is_none")]
9    attach_algo_cl_ord_id: Option<String>,
10    #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
11    tp_trigger_px: Option<String>,
12    #[serde(rename = "tpTriggerRatio", skip_serializing_if = "Option::is_none")]
13    tp_trigger_ratio: Option<String>,
14    #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
15    tp_trigger_px_type: Option<String>,
16    #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
17    tp_ord_px: Option<String>,
18    #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
19    sl_trigger_px: Option<String>,
20    #[serde(rename = "slTriggerRatio", skip_serializing_if = "Option::is_none")]
21    sl_trigger_ratio: Option<String>,
22    #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
23    sl_trigger_px_type: Option<String>,
24    #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
25    sl_ord_px: Option<String>,
26}
27
28impl AttachedAlgoOrderRequest {
29    /// Create an empty attached TP/SL definition.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Set the client-supplied attached algo ID.
35    pub fn client_algo_order_id(mut self, value: impl Into<String>) -> Self {
36        self.attach_algo_cl_ord_id = Some(value.into());
37        self
38    }
39
40    /// Configure take profit using a trigger price.
41    pub fn take_profit(
42        mut self,
43        trigger_px: impl Into<String>,
44        order_px: impl Into<String>,
45    ) -> Self {
46        self.tp_trigger_px = Some(trigger_px.into());
47        self.tp_ord_px = Some(order_px.into());
48        self
49    }
50
51    /// Configure take profit using a trigger ratio.
52    pub fn take_profit_ratio(
53        mut self,
54        trigger_ratio: impl Into<String>,
55        order_px: impl Into<String>,
56    ) -> Self {
57        self.tp_trigger_ratio = Some(trigger_ratio.into());
58        self.tp_ord_px = Some(order_px.into());
59        self
60    }
61
62    /// Set the take-profit trigger price source.
63    pub fn take_profit_price_type(mut self, value: impl Into<String>) -> Self {
64        self.tp_trigger_px_type = Some(value.into());
65        self
66    }
67
68    /// Configure stop loss using a trigger price.
69    pub fn stop_loss(mut self, trigger_px: impl Into<String>, order_px: impl Into<String>) -> Self {
70        self.sl_trigger_px = Some(trigger_px.into());
71        self.sl_ord_px = Some(order_px.into());
72        self
73    }
74
75    /// Configure stop loss using a trigger ratio.
76    pub fn stop_loss_ratio(
77        mut self,
78        trigger_ratio: impl Into<String>,
79        order_px: impl Into<String>,
80    ) -> Self {
81        self.sl_trigger_ratio = Some(trigger_ratio.into());
82        self.sl_ord_px = Some(order_px.into());
83        self
84    }
85
86    /// Set the stop-loss trigger price source.
87    pub fn stop_loss_price_type(mut self, value: impl Into<String>) -> Self {
88        self.sl_trigger_px_type = Some(value.into());
89        self
90    }
91}
92
93/// One trigger definition used by a `smart_iceberg` request.
94#[derive(Debug, Clone, Serialize)]
95pub struct SmartIcebergTriggerRequest {
96    #[serde(rename = "triggerAction")]
97    trigger_action: String,
98    #[serde(rename = "triggerStrategy")]
99    trigger_strategy: String,
100    #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
101    trigger_px: Option<String>,
102    #[serde(rename = "triggerCond", skip_serializing_if = "Option::is_none")]
103    trigger_cond: Option<String>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    timeframe: Option<String>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    thold: Option<String>,
108    #[serde(rename = "timePeriod", skip_serializing_if = "Option::is_none")]
109    time_period: Option<String>,
110}
111
112impl SmartIcebergTriggerRequest {
113    /// Create a start trigger with strategy `instant`, `price`, or `rsi`.
114    pub fn new(trigger_strategy: impl Into<String>) -> Self {
115        Self {
116            trigger_action: "start".to_owned(),
117            trigger_strategy: trigger_strategy.into(),
118            trigger_px: None,
119            trigger_cond: None,
120            timeframe: None,
121            thold: None,
122            time_period: None,
123        }
124    }
125
126    /// Set a price trigger and optional crossing condition.
127    pub fn price(mut self, trigger_px: impl Into<String>) -> Self {
128        self.trigger_px = Some(trigger_px.into());
129        self
130    }
131
132    /// Set the trigger condition.
133    pub fn condition(mut self, value: impl Into<String>) -> Self {
134        self.trigger_cond = Some(value.into());
135        self
136    }
137
138    /// Set the RSI candle timeframe.
139    pub fn timeframe(mut self, value: impl Into<String>) -> Self {
140        self.timeframe = Some(value.into());
141        self
142    }
143
144    /// Set the RSI threshold from 1 through 100.
145    pub fn threshold(mut self, value: impl Into<String>) -> Self {
146        self.thold = Some(value.into());
147        self
148    }
149
150    /// Set the RSI period. OKX currently fixes this value at `14`.
151    pub fn time_period(mut self, value: impl Into<String>) -> Self {
152        self.time_period = Some(value.into());
153        self
154    }
155}
156
157/// Request body for `POST /api/v5/trade/order-algo`.
158#[derive(Debug, Clone, Serialize)]
159pub struct AlgoOrderRequest {
160    #[serde(rename = "instId")]
161    inst_id: String,
162    #[serde(rename = "tdMode")]
163    td_mode: String,
164    side: String,
165    #[serde(rename = "ordType")]
166    ord_type: String,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    sz: Option<String>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    ccy: Option<String>,
171    #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
172    pos_side: Option<String>,
173    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
174    reduce_only: Option<bool>,
175    #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
176    algo_cl_ord_id: Option<String>,
177    #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
178    tgt_ccy: Option<String>,
179    #[serde(rename = "closeFraction", skip_serializing_if = "Option::is_none")]
180    close_fraction: Option<String>,
181    #[serde(rename = "tradeQuoteCcy", skip_serializing_if = "Option::is_none")]
182    trade_quote_ccy: Option<String>,
183    #[serde(rename = "triggerPx", skip_serializing_if = "Option::is_none")]
184    trigger_px: Option<String>,
185    #[serde(rename = "orderPx", skip_serializing_if = "Option::is_none")]
186    order_px: Option<String>,
187    #[serde(rename = "advanceOrdType", skip_serializing_if = "Option::is_none")]
188    advance_ord_type: Option<String>,
189    #[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
190    trigger_px_type: Option<String>,
191    #[serde(rename = "tpTriggerPx", skip_serializing_if = "Option::is_none")]
192    tp_trigger_px: Option<String>,
193    #[serde(rename = "tpTriggerPxType", skip_serializing_if = "Option::is_none")]
194    tp_trigger_px_type: Option<String>,
195    #[serde(rename = "tpOrdPx", skip_serializing_if = "Option::is_none")]
196    tp_ord_px: Option<String>,
197    #[serde(rename = "tpOrdKind", skip_serializing_if = "Option::is_none")]
198    tp_ord_kind: Option<String>,
199    #[serde(rename = "slTriggerPx", skip_serializing_if = "Option::is_none")]
200    sl_trigger_px: Option<String>,
201    #[serde(rename = "slTriggerPxType", skip_serializing_if = "Option::is_none")]
202    sl_trigger_px_type: Option<String>,
203    #[serde(rename = "slOrdPx", skip_serializing_if = "Option::is_none")]
204    sl_ord_px: Option<String>,
205    #[serde(rename = "cxlOnClosePos", skip_serializing_if = "Option::is_none")]
206    cxl_on_close_pos: Option<bool>,
207    #[serde(rename = "attachAlgoOrds", skip_serializing_if = "Option::is_none")]
208    attach_algo_ords: Option<Vec<AttachedAlgoOrderRequest>>,
209    #[serde(rename = "callbackRatio", skip_serializing_if = "Option::is_none")]
210    callback_ratio: Option<String>,
211    #[serde(rename = "callbackSpread", skip_serializing_if = "Option::is_none")]
212    callback_spread: Option<String>,
213    #[serde(rename = "activePx", skip_serializing_if = "Option::is_none")]
214    active_px: Option<String>,
215    #[serde(rename = "chaseType", skip_serializing_if = "Option::is_none")]
216    chase_type: Option<String>,
217    #[serde(rename = "chaseVal", skip_serializing_if = "Option::is_none")]
218    chase_val: Option<String>,
219    #[serde(rename = "maxChaseType", skip_serializing_if = "Option::is_none")]
220    max_chase_type: Option<String>,
221    #[serde(rename = "maxChaseVal", skip_serializing_if = "Option::is_none")]
222    max_chase_val: Option<String>,
223    #[serde(rename = "pxVar", skip_serializing_if = "Option::is_none")]
224    px_var: Option<String>,
225    #[serde(rename = "pxSpread", skip_serializing_if = "Option::is_none")]
226    px_spread: Option<String>,
227    #[serde(rename = "szLimit", skip_serializing_if = "Option::is_none")]
228    sz_limit: Option<String>,
229    #[serde(rename = "pxLimit", skip_serializing_if = "Option::is_none")]
230    px_limit: Option<String>,
231    #[serde(rename = "timeInterval", skip_serializing_if = "Option::is_none")]
232    time_interval: Option<String>,
233    #[serde(rename = "lmtOrderNumber", skip_serializing_if = "Option::is_none")]
234    lmt_order_number: Option<String>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    aggressiveness: Option<String>,
237    #[serde(rename = "triggerParams", skip_serializing_if = "Option::is_none")]
238    trigger_params: Option<Vec<SmartIcebergTriggerRequest>>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    tag: Option<String>,
241}
242
243impl AlgoOrderRequest {
244    /// Create an algo-order request with a quantity.
245    pub fn new(
246        inst_id: impl Into<String>,
247        td_mode: impl Into<String>,
248        side: impl Into<String>,
249        ord_type: impl Into<String>,
250        sz: impl Into<String>,
251    ) -> Self {
252        Self {
253            inst_id: inst_id.into(),
254            td_mode: td_mode.into(),
255            side: side.into(),
256            ord_type: ord_type.into(),
257            sz: Some(sz.into()),
258            ccy: None,
259            pos_side: None,
260            reduce_only: None,
261            algo_cl_ord_id: None,
262            tgt_ccy: None,
263            close_fraction: None,
264            trade_quote_ccy: None,
265            trigger_px: None,
266            order_px: None,
267            advance_ord_type: None,
268            trigger_px_type: None,
269            tp_trigger_px: None,
270            tp_trigger_px_type: None,
271            tp_ord_px: None,
272            tp_ord_kind: None,
273            sl_trigger_px: None,
274            sl_trigger_px_type: None,
275            sl_ord_px: None,
276            cxl_on_close_pos: None,
277            attach_algo_ords: None,
278            callback_ratio: None,
279            callback_spread: None,
280            active_px: None,
281            chase_type: None,
282            chase_val: None,
283            max_chase_type: None,
284            max_chase_val: None,
285            px_var: None,
286            px_spread: None,
287            sz_limit: None,
288            px_limit: None,
289            time_interval: None,
290            lmt_order_number: None,
291            aggressiveness: None,
292            trigger_params: None,
293            tag: None,
294        }
295    }
296
297    /// Replace `sz` with the only currently supported full-close fraction, `1`.
298    pub fn full_close(mut self) -> Self {
299        self.sz = None;
300        self.close_fraction = Some("1".to_owned());
301        self
302    }
303
304    /// Set the margin currency.
305    pub fn currency(mut self, value: impl Into<String>) -> Self {
306        self.ccy = Some(value.into());
307        self
308    }
309
310    /// Set the position side (`long`, `short`, or `net`).
311    pub fn position_side(mut self, value: impl Into<String>) -> Self {
312        self.pos_side = Some(value.into());
313        self
314    }
315
316    /// Set the reduce-only flag.
317    pub fn reduce_only(mut self, value: bool) -> Self {
318        self.reduce_only = Some(value);
319        self
320    }
321
322    /// Set the client-supplied algo ID.
323    pub fn client_algo_order_id(mut self, value: impl Into<String>) -> Self {
324        self.algo_cl_ord_id = Some(value.into());
325        self
326    }
327
328    /// Set the Spot quantity unit (`base_ccy` or `quote_ccy`).
329    pub fn target_currency(mut self, value: impl Into<String>) -> Self {
330        self.tgt_ccy = Some(value.into());
331        self
332    }
333
334    /// Set the quote currency used for Spot trading.
335    pub fn trade_quote_currency(mut self, value: impl Into<String>) -> Self {
336        self.trade_quote_ccy = Some(value.into());
337        self
338    }
339
340    /// Configure a trigger order's trigger and execution prices.
341    pub fn trigger(mut self, px: impl Into<String>, order_px: impl Into<String>) -> Self {
342        self.trigger_px = Some(px.into());
343        self.order_px = Some(order_px.into());
344        self
345    }
346
347    /// Set trigger execution type (`fok` or `ioc`).
348    pub fn advance_order_type(mut self, value: impl Into<String>) -> Self {
349        self.advance_ord_type = Some(value.into());
350        self
351    }
352
353    /// Set trigger price type (`last`, `index`, or `mark`).
354    pub fn trigger_price_type(mut self, value: impl Into<String>) -> Self {
355        self.trigger_px_type = Some(value.into());
356        self
357    }
358
359    /// Configure take-profit trigger and order prices.
360    pub fn take_profit(
361        mut self,
362        trigger_px: impl Into<String>,
363        order_px: impl Into<String>,
364    ) -> Self {
365        self.tp_trigger_px = Some(trigger_px.into());
366        self.tp_ord_px = Some(order_px.into());
367        self
368    }
369
370    /// Configure a limit take-profit order that does not require a trigger price.
371    pub fn limit_take_profit(mut self, order_px: impl Into<String>) -> Self {
372        self.tp_ord_kind = Some("limit".to_owned());
373        self.tp_ord_px = Some(order_px.into());
374        self
375    }
376
377    /// Set the take-profit trigger source.
378    pub fn take_profit_price_type(mut self, value: impl Into<String>) -> Self {
379        self.tp_trigger_px_type = Some(value.into());
380        self
381    }
382
383    /// Configure stop-loss trigger and order prices.
384    pub fn stop_loss(mut self, trigger_px: impl Into<String>, order_px: impl Into<String>) -> Self {
385        self.sl_trigger_px = Some(trigger_px.into());
386        self.sl_ord_px = Some(order_px.into());
387        self
388    }
389
390    /// Set the stop-loss trigger source.
391    pub fn stop_loss_price_type(mut self, value: impl Into<String>) -> Self {
392        self.sl_trigger_px_type = Some(value.into());
393        self
394    }
395
396    /// Associate TP/SL cancellation with a fully closed position.
397    pub fn cancel_on_close_position(mut self, value: bool) -> Self {
398        self.cxl_on_close_pos = Some(value);
399        self
400    }
401
402    /// Attach TP/SL definitions to the triggered order.
403    pub fn attached_algo_orders(mut self, values: Vec<AttachedAlgoOrderRequest>) -> Self {
404        self.attach_algo_ords = Some(values);
405        self
406    }
407
408    /// Set a trailing-order callback ratio.
409    pub fn callback_ratio(mut self, value: impl Into<String>) -> Self {
410        self.callback_ratio = Some(value.into());
411        self
412    }
413
414    /// Set a trailing-order callback spread.
415    pub fn callback_spread(mut self, value: impl Into<String>) -> Self {
416        self.callback_spread = Some(value.into());
417        self
418    }
419
420    /// Set a trailing-order activation price.
421    pub fn active_price(mut self, value: impl Into<String>) -> Self {
422        self.active_px = Some(value.into());
423        self
424    }
425
426    /// Configure chase type and value.
427    pub fn chase(mut self, chase_type: impl Into<String>, chase_val: impl Into<String>) -> Self {
428        self.chase_type = Some(chase_type.into());
429        self.chase_val = Some(chase_val.into());
430        self
431    }
432
433    /// Configure the optional maximum chase type and value.
434    pub fn maximum_chase(
435        mut self,
436        chase_type: impl Into<String>,
437        chase_val: impl Into<String>,
438    ) -> Self {
439        self.max_chase_type = Some(chase_type.into());
440        self.max_chase_val = Some(chase_val.into());
441        self
442    }
443
444    /// Configure TWAP strategy fields using a percentage variance.
445    pub fn twap_by_variance(
446        mut self,
447        px_var: impl Into<String>,
448        sz_limit: impl Into<String>,
449        px_limit: impl Into<String>,
450        time_interval: impl Into<String>,
451    ) -> Self {
452        self.px_var = Some(px_var.into());
453        self.px_spread = None;
454        self.sz_limit = Some(sz_limit.into());
455        self.px_limit = Some(px_limit.into());
456        self.time_interval = Some(time_interval.into());
457        self
458    }
459
460    /// Configure TWAP strategy fields using an absolute price spread.
461    pub fn twap_by_spread(
462        mut self,
463        px_spread: impl Into<String>,
464        sz_limit: impl Into<String>,
465        px_limit: impl Into<String>,
466        time_interval: impl Into<String>,
467    ) -> Self {
468        self.px_var = None;
469        self.px_spread = Some(px_spread.into());
470        self.sz_limit = Some(sz_limit.into());
471        self.px_limit = Some(px_limit.into());
472        self.time_interval = Some(time_interval.into());
473        self
474    }
475
476    /// Configure required Smart Iceberg execution fields.
477    pub fn smart_iceberg(
478        mut self,
479        sz_limit: impl Into<String>,
480        lmt_order_number: impl Into<String>,
481        aggressiveness: impl Into<String>,
482    ) -> Self {
483        self.sz_limit = Some(sz_limit.into());
484        self.lmt_order_number = Some(lmt_order_number.into());
485        self.aggressiveness = Some(aggressiveness.into());
486        self
487    }
488
489    /// Set an optional Smart Iceberg price limit.
490    pub fn price_limit(mut self, value: impl Into<String>) -> Self {
491        self.px_limit = Some(value.into());
492        self
493    }
494
495    /// Set Smart Iceberg trigger parameters.
496    pub fn smart_iceberg_triggers(mut self, values: Vec<SmartIcebergTriggerRequest>) -> Self {
497        self.trigger_params = Some(values);
498        self
499    }
500
501    /// Set an order tag of at most 16 ASCII alphanumeric characters.
502    pub fn tag(mut self, value: impl Into<String>) -> Self {
503        self.tag = Some(value.into());
504        self
505    }
506}
507
508/// Request body for one entry in `POST /api/v5/trade/cancel-algos`.
509#[derive(Debug, Clone, Serialize)]
510pub struct CancelAlgoOrderRequest<'a> {
511    #[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
512    algo_id: Option<Cow<'a, str>>,
513    #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
514    algo_cl_ord_id: Option<Cow<'a, str>>,
515    #[serde(rename = "instId")]
516    inst_id: Cow<'a, str>,
517}
518
519impl<'a> CancelAlgoOrderRequest<'a> {
520    /// Create a cancellation using an OKX algo ID.
521    pub fn new(algo_id: impl Into<Cow<'a, str>>, inst_id: impl Into<Cow<'a, str>>) -> Self {
522        Self {
523            algo_id: Some(algo_id.into()),
524            algo_cl_ord_id: None,
525            inst_id: inst_id.into(),
526        }
527    }
528
529    /// Create a cancellation using a client-supplied algo ID.
530    pub fn by_client_algo_order_id(
531        algo_cl_ord_id: impl Into<Cow<'a, str>>,
532        inst_id: impl Into<Cow<'a, str>>,
533    ) -> Self {
534        Self {
535            algo_id: None,
536            algo_cl_ord_id: Some(algo_cl_ord_id.into()),
537            inst_id: inst_id.into(),
538        }
539    }
540}
541
542/// Request body for `POST /api/v5/trade/amend-algos`.
543#[derive(Debug, Clone, Serialize)]
544pub struct AmendAlgoOrderRequest<'a> {
545    #[serde(rename = "instId")]
546    inst_id: Cow<'a, str>,
547    #[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
548    algo_id: Option<Cow<'a, str>>,
549    #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
550    algo_cl_ord_id: Option<Cow<'a, str>>,
551    #[serde(rename = "reqId", skip_serializing_if = "Option::is_none")]
552    req_id: Option<Cow<'a, str>>,
553    #[serde(rename = "newSz", skip_serializing_if = "Option::is_none")]
554    new_sz: Option<Cow<'a, str>>,
555    #[serde(rename = "newTpTriggerPx", skip_serializing_if = "Option::is_none")]
556    new_tp_trigger_px: Option<Cow<'a, str>>,
557    #[serde(rename = "newTpOrdPx", skip_serializing_if = "Option::is_none")]
558    new_tp_ord_px: Option<Cow<'a, str>>,
559    #[serde(rename = "newTpTriggerPxType", skip_serializing_if = "Option::is_none")]
560    new_tp_trigger_px_type: Option<Cow<'a, str>>,
561    #[serde(rename = "newSlTriggerPx", skip_serializing_if = "Option::is_none")]
562    new_sl_trigger_px: Option<Cow<'a, str>>,
563    #[serde(rename = "newSlOrdPx", skip_serializing_if = "Option::is_none")]
564    new_sl_ord_px: Option<Cow<'a, str>>,
565    #[serde(rename = "newSlTriggerPxType", skip_serializing_if = "Option::is_none")]
566    new_sl_trigger_px_type: Option<Cow<'a, str>>,
567    #[serde(rename = "cxlOnFail", skip_serializing_if = "Option::is_none")]
568    cancel_on_fail: Option<bool>,
569}
570
571impl<'a> AmendAlgoOrderRequest<'a> {
572    /// Create an amendment for an instrument; add an algo identifier next.
573    pub fn new(inst_id: impl Into<Cow<'a, str>>) -> Self {
574        Self {
575            inst_id: inst_id.into(),
576            algo_id: None,
577            algo_cl_ord_id: None,
578            req_id: None,
579            new_sz: None,
580            new_tp_trigger_px: None,
581            new_tp_ord_px: None,
582            new_tp_trigger_px_type: None,
583            new_sl_trigger_px: None,
584            new_sl_ord_px: None,
585            new_sl_trigger_px_type: None,
586            cancel_on_fail: None,
587        }
588    }
589
590    /// Identify the order by OKX algo ID.
591    pub fn algo_id(mut self, value: impl Into<Cow<'a, str>>) -> Self {
592        self.algo_id = Some(value.into());
593        self
594    }
595
596    /// Identify the order by client-supplied algo ID.
597    pub fn client_algo_order_id(mut self, value: impl Into<Cow<'a, str>>) -> Self {
598        self.algo_cl_ord_id = Some(value.into());
599        self
600    }
601
602    /// Set a client request ID of up to 32 ASCII alphanumeric characters.
603    pub fn request_id(mut self, value: impl Into<Cow<'a, str>>) -> Self {
604        self.req_id = Some(value.into());
605        self
606    }
607
608    /// Amend the order size.
609    pub fn new_size(mut self, value: impl Into<Cow<'a, str>>) -> Self {
610        self.new_sz = Some(value.into());
611        self
612    }
613
614    /// Amend take-profit prices.
615    pub fn take_profit(
616        mut self,
617        trigger_px: impl Into<Cow<'a, str>>,
618        order_px: impl Into<Cow<'a, str>>,
619    ) -> Self {
620        self.new_tp_trigger_px = Some(trigger_px.into());
621        self.new_tp_ord_px = Some(order_px.into());
622        self
623    }
624
625    /// Set the amended take-profit trigger price source.
626    pub fn take_profit_price_type(mut self, value: impl Into<Cow<'a, str>>) -> Self {
627        self.new_tp_trigger_px_type = Some(value.into());
628        self
629    }
630
631    /// Delete the take-profit definition by sending the documented `0` sentinel.
632    pub fn delete_take_profit(mut self) -> Self {
633        self.new_tp_trigger_px = Some(Cow::Borrowed("0"));
634        self.new_tp_ord_px = None;
635        self.new_tp_trigger_px_type = None;
636        self
637    }
638
639    /// Amend stop-loss prices.
640    pub fn stop_loss(
641        mut self,
642        trigger_px: impl Into<Cow<'a, str>>,
643        order_px: impl Into<Cow<'a, str>>,
644    ) -> Self {
645        self.new_sl_trigger_px = Some(trigger_px.into());
646        self.new_sl_ord_px = Some(order_px.into());
647        self
648    }
649
650    /// Set the amended stop-loss trigger price source.
651    pub fn stop_loss_price_type(mut self, value: impl Into<Cow<'a, str>>) -> Self {
652        self.new_sl_trigger_px_type = Some(value.into());
653        self
654    }
655
656    /// Delete the stop-loss definition by sending the documented `0` sentinel.
657    pub fn delete_stop_loss(mut self) -> Self {
658        self.new_sl_trigger_px = Some(Cow::Borrowed("0"));
659        self.new_sl_ord_px = None;
660        self.new_sl_trigger_px_type = None;
661        self
662    }
663
664    /// Cancel the original order automatically when amendment fails.
665    pub fn cancel_on_fail(mut self, value: bool) -> Self {
666        self.cancel_on_fail = Some(value);
667        self
668    }
669}
670
671/// Query parameters shared by pending and historical algo orders.
672#[derive(Debug, Clone, Serialize)]
673pub struct AlgoOrderListRequest<'a> {
674    #[serde(rename = "ordType")]
675    ord_type: Cow<'a, str>,
676    #[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
677    algo_id: Option<Cow<'a, str>>,
678    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
679    inst_type: Option<Cow<'a, str>>,
680    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
681    inst_id: Option<Cow<'a, str>>,
682    #[serde(skip_serializing_if = "Option::is_none")]
683    after: Option<Cow<'a, str>>,
684    #[serde(skip_serializing_if = "Option::is_none")]
685    before: Option<Cow<'a, str>>,
686    #[serde(skip_serializing_if = "Option::is_none")]
687    limit: Option<u32>,
688}
689
690impl<'a> AlgoOrderListRequest<'a> {
691    /// Create a query for one documented algo order type.
692    pub fn new(ord_type: impl Into<Cow<'a, str>>) -> Self {
693        Self {
694            ord_type: ord_type.into(),
695            algo_id: None,
696            inst_type: None,
697            inst_id: None,
698            after: None,
699            before: None,
700            limit: None,
701        }
702    }
703
704    /// Filter by algo ID.
705    pub fn algo_id(mut self, value: impl Into<Cow<'a, str>>) -> Self {
706        self.algo_id = Some(value.into());
707        self
708    }
709
710    /// Filter by instrument type.
711    pub fn inst_type(mut self, value: impl Into<Cow<'a, str>>) -> Self {
712        self.inst_type = Some(value.into());
713        self
714    }
715
716    /// Filter by instrument ID.
717    pub fn inst_id(mut self, value: impl Into<Cow<'a, str>>) -> Self {
718        self.inst_id = Some(value.into());
719        self
720    }
721
722    /// Return records before this algo-ID cursor.
723    pub fn after(mut self, value: impl Into<Cow<'a, str>>) -> Self {
724        self.after = Some(value.into());
725        self
726    }
727
728    /// Return records after this algo-ID cursor.
729    pub fn before(mut self, value: impl Into<Cow<'a, str>>) -> Self {
730        self.before = Some(value.into());
731        self
732    }
733
734    /// Set the number of rows, from 1 through 100.
735    pub fn limit(mut self, value: u32) -> Self {
736        self.limit = Some(value);
737        self
738    }
739}
740
741/// Query parameters for historical algo orders.
742#[derive(Debug, Clone, Serialize)]
743pub struct AlgoOrderHistoryRequest<'a> {
744    #[serde(flatten)]
745    common: AlgoOrderListRequest<'a>,
746    #[serde(skip_serializing_if = "Option::is_none")]
747    state: Option<Cow<'a, str>>,
748}
749
750impl<'a> AlgoOrderHistoryRequest<'a> {
751    /// Create a history query for one documented algo order type.
752    pub fn new(ord_type: impl Into<Cow<'a, str>>) -> Self {
753        Self {
754            common: AlgoOrderListRequest::new(ord_type),
755            state: None,
756        }
757    }
758
759    /// Filter by historical state.
760    pub fn state(mut self, value: impl Into<Cow<'a, str>>) -> Self {
761        self.state = Some(value.into());
762        self
763    }
764
765    /// Filter by OKX algo order ID.
766    pub fn algo_id(mut self, value: impl Into<Cow<'a, str>>) -> Self {
767        self.common = self.common.algo_id(value);
768        self
769    }
770
771    /// Filter by instrument type.
772    pub fn inst_type(mut self, value: impl Into<Cow<'a, str>>) -> Self {
773        self.common = self.common.inst_type(value);
774        self
775    }
776
777    /// Filter by instrument ID.
778    pub fn inst_id(mut self, value: impl Into<Cow<'a, str>>) -> Self {
779        self.common = self.common.inst_id(value);
780        self
781    }
782
783    /// Return records before this cursor.
784    pub fn after(mut self, value: impl Into<Cow<'a, str>>) -> Self {
785        self.common = self.common.after(value);
786        self
787    }
788
789    /// Return records after this cursor.
790    pub fn before(mut self, value: impl Into<Cow<'a, str>>) -> Self {
791        self.common = self.common.before(value);
792        self
793    }
794
795    /// Set the number of rows, from 1 through 100.
796    pub fn limit(mut self, value: u32) -> Self {
797        self.common = self.common.limit(value);
798        self
799    }
800}
801
802/// Query parameters for `GET /api/v5/trade/order-algo`.
803#[derive(Debug, Clone, Default, Serialize)]
804pub struct AlgoOrderDetailsRequest<'a> {
805    #[serde(rename = "algoId", skip_serializing_if = "Option::is_none")]
806    algo_id: Option<Cow<'a, str>>,
807    #[serde(rename = "algoClOrdId", skip_serializing_if = "Option::is_none")]
808    algo_cl_ord_id: Option<Cow<'a, str>>,
809}
810
811impl<'a> AlgoOrderDetailsRequest<'a> {
812    /// Query by OKX algo ID.
813    pub fn by_algo_id(value: impl Into<Cow<'a, str>>) -> Self {
814        Self {
815            algo_id: Some(value.into()),
816            algo_cl_ord_id: None,
817        }
818    }
819
820    /// Query by client-supplied algo ID.
821    pub fn by_client_algo_order_id(value: impl Into<Cow<'a, str>>) -> Self {
822        Self {
823            algo_id: None,
824            algo_cl_ord_id: Some(value.into()),
825        }
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    #[test]
834    fn twap_official_example_validates_and_serializes() {
835        let request = AlgoOrderRequest::new("BTC-USDT-SWAP", "cross", "buy", "twap", "10")
836            .position_side("net")
837            .twap_by_spread("10", "10", "100", "10");
838
839        assert_eq!(
840            serde_json::to_value(&request).unwrap(),
841            serde_json::json!({
842                "instId": "BTC-USDT-SWAP",
843                "tdMode": "cross",
844                "side": "buy",
845                "ordType": "twap",
846                "sz": "10",
847                "posSide": "net",
848                "pxSpread": "10",
849                "szLimit": "10",
850                "pxLimit": "100",
851                "timeInterval": "10"
852            })
853        );
854    }
855
856    #[test]
857    fn smart_iceberg_official_example_validates_and_serializes() {
858        let trigger = SmartIcebergTriggerRequest::new("price")
859            .price("90000")
860            .condition("cross_down");
861        let request = AlgoOrderRequest::new("BTC-USDT", "cash", "buy", "smart_iceberg", "100")
862            .smart_iceberg("10", "5", "conservative")
863            .price_limit("95000")
864            .smart_iceberg_triggers(vec![trigger]);
865
866        assert_eq!(
867            serde_json::to_value(&request).unwrap(),
868            serde_json::json!({
869                "instId": "BTC-USDT",
870                "tdMode": "cash",
871                "side": "buy",
872                "ordType": "smart_iceberg",
873                "sz": "100",
874                "szLimit": "10",
875                "pxLimit": "95000",
876                "lmtOrderNumber": "5",
877                "aggressiveness": "conservative",
878                "triggerParams": [{
879                    "triggerAction": "start",
880                    "triggerStrategy": "price",
881                    "triggerPx": "90000",
882                    "triggerCond": "cross_down"
883                }]
884            })
885        );
886    }
887
888    #[test]
889    fn amend_allows_both_identifiers_and_documented_delete_sentinel() {
890        let request = AmendAlgoOrderRequest::new("BTC-USDT")
891            .algo_id("1")
892            .client_algo_order_id("client1")
893            .delete_take_profit();
894        assert_eq!(
895            serde_json::to_value(&request).unwrap(),
896            serde_json::json!({
897                "instId": "BTC-USDT",
898                "algoId": "1",
899                "algoClOrdId": "client1",
900                "newTpTriggerPx": "0"
901            })
902        );
903    }
904}