Skip to main content

rust_okx/api/
trade.rs

1//! Authenticated trading endpoints (`/api/v5/trade/*`).
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::OkxClient;
6use crate::error::Error;
7use crate::model::{NumberString, OrderSide, OrderState, OrderType, PositionSide, TradeMode};
8use crate::transport::Transport;
9
10const ORDER: &str = "/api/v5/trade/order";
11const BATCH_ORDERS: &str = "/api/v5/trade/batch-orders";
12const CANCEL_ORDER: &str = "/api/v5/trade/cancel-order";
13const CANCEL_BATCH_ORDERS: &str = "/api/v5/trade/cancel-batch-orders";
14const AMEND_ORDER: &str = "/api/v5/trade/amend-order";
15const AMEND_BATCH_ORDERS: &str = "/api/v5/trade/amend-batch-orders";
16const CLOSE_POSITION: &str = "/api/v5/trade/close-position";
17const ORDERS_PENDING: &str = "/api/v5/trade/orders-pending";
18const ORDERS_HISTORY: &str = "/api/v5/trade/orders-history";
19const ORDERS_HISTORY_ARCHIVE: &str = "/api/v5/trade/orders-history-archive";
20const FILLS: &str = "/api/v5/trade/fills";
21const FILLS_HISTORY: &str = "/api/v5/trade/fills-history";
22
23/// Accessor for the authenticated trading endpoints.
24///
25/// Obtain one via [`OkxClient::trade`](crate::OkxClient::trade). All methods
26/// require credentials.
27pub struct Trade<'a, T> {
28    client: &'a OkxClient<T>,
29}
30
31impl<'a, T: Transport> Trade<'a, T> {
32    pub(crate) fn new(client: &'a OkxClient<T>) -> Self {
33        Self { client }
34    }
35
36    /// Place an order.
37    ///
38    /// `POST /api/v5/trade/order`. Authenticated. Build the request with
39    /// [`PlaceOrderRequest::new`] plus optional setters. The returned vector
40    /// contains one [`PlaceOrderResult`]; inspect its
41    /// [`s_code`](PlaceOrderResult::s_code) to confirm acceptance (`"0"`).
42    ///
43    /// # Errors
44    ///
45    /// Returns [`Error::Configuration`] without credentials, [`Error::Api`] on a
46    /// non-zero top-level OKX code, or transport/decode errors.
47    pub async fn place_order(
48        &self,
49        request: &PlaceOrderRequest,
50    ) -> Result<Vec<PlaceOrderResult>, Error> {
51        self.client.post(ORDER, request, true).await
52    }
53
54    /// Place multiple orders.
55    ///
56    /// `POST /api/v5/trade/batch-orders`. Authenticated.
57    ///
58    /// # Errors
59    ///
60    /// See [`place_order`](Self::place_order).
61    pub async fn place_multiple_orders(
62        &self,
63        requests: &[PlaceOrderRequest],
64    ) -> Result<Vec<PlaceOrderResult>, Error> {
65        self.client.post(BATCH_ORDERS, &requests, true).await
66    }
67
68    /// Cancel an order by its OKX order ID.
69    ///
70    /// `POST /api/v5/trade/cancel-order`. Authenticated.
71    ///
72    /// # Errors
73    ///
74    /// See [`place_order`](Self::place_order).
75    pub async fn cancel_order(
76        &self,
77        inst_id: &str,
78        ord_id: &str,
79    ) -> Result<Vec<CancelOrderResult>, Error> {
80        let body = CancelOrderBody { inst_id, ord_id };
81        self.client.post(CANCEL_ORDER, &body, true).await
82    }
83
84    /// Cancel multiple orders.
85    ///
86    /// `POST /api/v5/trade/cancel-batch-orders`. Authenticated.
87    ///
88    /// # Errors
89    ///
90    /// See [`place_order`](Self::place_order).
91    pub async fn cancel_multiple_orders(
92        &self,
93        requests: &[CancelOrderRequest],
94    ) -> Result<Vec<CancelOrderResult>, Error> {
95        self.client.post(CANCEL_BATCH_ORDERS, &requests, true).await
96    }
97
98    /// Amend an existing order.
99    ///
100    /// `POST /api/v5/trade/amend-order`. Authenticated.
101    ///
102    /// # Errors
103    ///
104    /// See [`place_order`](Self::place_order).
105    pub async fn amend_order(
106        &self,
107        request: &AmendOrderRequest,
108    ) -> Result<Vec<AmendOrderResult>, Error> {
109        self.client.post(AMEND_ORDER, request, true).await
110    }
111
112    /// Amend multiple existing orders.
113    ///
114    /// `POST /api/v5/trade/amend-batch-orders`. Authenticated.
115    ///
116    /// # Errors
117    ///
118    /// See [`place_order`](Self::place_order).
119    pub async fn amend_multiple_orders(
120        &self,
121        requests: &[AmendOrderRequest],
122    ) -> Result<Vec<AmendOrderResult>, Error> {
123        self.client.post(AMEND_BATCH_ORDERS, &requests, true).await
124    }
125
126    /// Close positions for an instrument.
127    ///
128    /// `POST /api/v5/trade/close-position`. Authenticated.
129    ///
130    /// # Errors
131    ///
132    /// See [`place_order`](Self::place_order).
133    pub async fn close_positions(
134        &self,
135        request: &ClosePositionRequest,
136    ) -> Result<Vec<ClosePositionResult>, Error> {
137        self.client.post(CLOSE_POSITION, request, true).await
138    }
139
140    /// Retrieve the details of a single order by its OKX order ID.
141    ///
142    /// `GET /api/v5/trade/order`. Authenticated.
143    ///
144    /// # Errors
145    ///
146    /// See [`place_order`](Self::place_order).
147    pub async fn get_order(&self, inst_id: &str, ord_id: &str) -> Result<Vec<Order>, Error> {
148        let query = GetOrderQuery { inst_id, ord_id };
149        self.client.get(ORDER, &query, true).await
150    }
151
152    /// Retrieve pending orders.
153    ///
154    /// `GET /api/v5/trade/orders-pending`. Authenticated.
155    ///
156    /// # Errors
157    ///
158    /// See [`place_order`](Self::place_order).
159    pub async fn get_order_list(&self, request: &OrderListRequest) -> Result<Vec<Order>, Error> {
160        self.client.get(ORDERS_PENDING, request, true).await
161    }
162
163    /// Retrieve order history for the recent window.
164    ///
165    /// `GET /api/v5/trade/orders-history`. Authenticated.
166    ///
167    /// # Errors
168    ///
169    /// See [`place_order`](Self::place_order).
170    pub async fn get_orders_history(
171        &self,
172        request: &OrderHistoryRequest,
173    ) -> Result<Vec<Order>, Error> {
174        self.client.get(ORDERS_HISTORY, request, true).await
175    }
176
177    /// Retrieve archived order history.
178    ///
179    /// `GET /api/v5/trade/orders-history-archive`. Authenticated.
180    ///
181    /// # Errors
182    ///
183    /// See [`place_order`](Self::place_order).
184    pub async fn get_orders_history_archive(
185        &self,
186        request: &OrderHistoryRequest,
187    ) -> Result<Vec<Order>, Error> {
188        self.client
189            .get(ORDERS_HISTORY_ARCHIVE, request, true)
190            .await
191    }
192
193    /// Retrieve recent fills.
194    ///
195    /// `GET /api/v5/trade/fills`. Authenticated.
196    ///
197    /// # Errors
198    ///
199    /// See [`place_order`](Self::place_order).
200    pub async fn get_fills(&self, request: &FillsRequest) -> Result<Vec<Fill>, Error> {
201        self.client.get(FILLS, request, true).await
202    }
203
204    /// Retrieve historical fills.
205    ///
206    /// `GET /api/v5/trade/fills-history`. Authenticated.
207    ///
208    /// # Errors
209    ///
210    /// See [`place_order`](Self::place_order).
211    pub async fn get_fills_history(&self, request: &FillsRequest) -> Result<Vec<Fill>, Error> {
212        self.client.get(FILLS_HISTORY, request, true).await
213    }
214}
215
216/// A request to place an order.
217///
218/// Construct with [`PlaceOrderRequest::new`] (required fields) and chain setters
219/// for optional fields. Optional fields are omitted from the request body when
220/// unset.
221#[derive(Debug, Clone, Serialize)]
222pub struct PlaceOrderRequest {
223    #[serde(rename = "instId")]
224    inst_id: String,
225    #[serde(rename = "tdMode")]
226    td_mode: TradeMode,
227    side: OrderSide,
228    #[serde(rename = "ordType")]
229    ord_type: OrderType,
230    sz: String,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    ccy: Option<String>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    tag: Option<String>,
235    #[serde(rename = "px", skip_serializing_if = "Option::is_none")]
236    px: Option<String>,
237    #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
238    pos_side: Option<PositionSide>,
239    #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
240    cl_ord_id: Option<String>,
241    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
242    reduce_only: Option<bool>,
243    #[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
244    tgt_ccy: Option<String>,
245}
246
247impl PlaceOrderRequest {
248    /// Create a new order request with the required fields.
249    pub fn new(
250        inst_id: impl Into<String>,
251        td_mode: TradeMode,
252        side: OrderSide,
253        ord_type: OrderType,
254        sz: impl Into<String>,
255    ) -> Self {
256        Self {
257            inst_id: inst_id.into(),
258            td_mode,
259            side,
260            ord_type,
261            sz: sz.into(),
262            ccy: None,
263            tag: None,
264            px: None,
265            pos_side: None,
266            cl_ord_id: None,
267            reduce_only: None,
268            tgt_ccy: None,
269        }
270    }
271
272    /// Set the order price (required for `limit`-style orders).
273    pub fn price(mut self, px: impl Into<String>) -> Self {
274        self.px = Some(px.into());
275        self
276    }
277
278    /// Set the position side (`long`/`short`/`net`).
279    pub fn position_side(mut self, pos_side: PositionSide) -> Self {
280        self.pos_side = Some(pos_side);
281        self
282    }
283
284    /// Set a client-supplied order ID.
285    pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
286        self.cl_ord_id = Some(cl_ord_id.into());
287        self
288    }
289
290    /// Mark the order as reduce-only.
291    pub fn reduce_only(mut self, reduce_only: bool) -> Self {
292        self.reduce_only = Some(reduce_only);
293        self
294    }
295
296    /// Set the quantity unit for spot market orders (`base_ccy`/`quote_ccy`).
297    pub fn target_ccy(mut self, tgt_ccy: impl Into<String>) -> Self {
298        self.tgt_ccy = Some(tgt_ccy.into());
299        self
300    }
301
302    /// Set the margin currency.
303    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
304        self.ccy = Some(ccy.into());
305        self
306    }
307
308    /// Set an order tag.
309    pub fn tag(mut self, tag: impl Into<String>) -> Self {
310        self.tag = Some(tag.into());
311        self
312    }
313}
314
315#[derive(Serialize)]
316struct CancelOrderBody<'a> {
317    #[serde(rename = "instId")]
318    inst_id: &'a str,
319    #[serde(rename = "ordId")]
320    ord_id: &'a str,
321}
322
323#[derive(Serialize)]
324struct GetOrderQuery<'a> {
325    #[serde(rename = "instId")]
326    inst_id: &'a str,
327    #[serde(rename = "ordId")]
328    ord_id: &'a str,
329}
330
331/// A request to cancel an order.
332#[derive(Debug, Clone, Serialize)]
333pub struct CancelOrderRequest {
334    #[serde(rename = "instId")]
335    inst_id: String,
336    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
337    ord_id: Option<String>,
338    #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
339    cl_ord_id: Option<String>,
340}
341
342impl CancelOrderRequest {
343    /// Cancel by OKX order ID.
344    pub fn by_order_id(inst_id: impl Into<String>, ord_id: impl Into<String>) -> Self {
345        Self {
346            inst_id: inst_id.into(),
347            ord_id: Some(ord_id.into()),
348            cl_ord_id: None,
349        }
350    }
351
352    /// Cancel by client order ID.
353    pub fn by_client_order_id(inst_id: impl Into<String>, cl_ord_id: impl Into<String>) -> Self {
354        Self {
355            inst_id: inst_id.into(),
356            ord_id: None,
357            cl_ord_id: Some(cl_ord_id.into()),
358        }
359    }
360}
361
362/// A request to amend an order.
363#[derive(Debug, Clone, Serialize)]
364pub struct AmendOrderRequest {
365    #[serde(rename = "instId")]
366    inst_id: String,
367    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
368    ord_id: Option<String>,
369    #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
370    cl_ord_id: Option<String>,
371    #[serde(rename = "reqId", skip_serializing_if = "Option::is_none")]
372    req_id: Option<String>,
373    #[serde(rename = "cxlOnFail", skip_serializing_if = "Option::is_none")]
374    cxl_on_fail: Option<bool>,
375    #[serde(rename = "newSz", skip_serializing_if = "Option::is_none")]
376    new_sz: Option<String>,
377    #[serde(rename = "newPx", skip_serializing_if = "Option::is_none")]
378    new_px: Option<String>,
379}
380
381impl AmendOrderRequest {
382    /// Create an amend-order request for an instrument.
383    pub fn new(inst_id: impl Into<String>) -> Self {
384        Self {
385            inst_id: inst_id.into(),
386            ord_id: None,
387            cl_ord_id: None,
388            req_id: None,
389            cxl_on_fail: None,
390            new_sz: None,
391            new_px: None,
392        }
393    }
394
395    /// Set the OKX order ID.
396    pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
397        self.ord_id = Some(ord_id.into());
398        self
399    }
400
401    /// Set the client order ID.
402    pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
403        self.cl_ord_id = Some(cl_ord_id.into());
404        self
405    }
406
407    /// Set a request ID.
408    pub fn request_id(mut self, req_id: impl Into<String>) -> Self {
409        self.req_id = Some(req_id.into());
410        self
411    }
412
413    /// Set whether OKX should cancel the order if amendment fails.
414    pub fn cancel_on_fail(mut self, cxl_on_fail: bool) -> Self {
415        self.cxl_on_fail = Some(cxl_on_fail);
416        self
417    }
418
419    /// Set the new order size.
420    pub fn new_size(mut self, new_sz: impl Into<String>) -> Self {
421        self.new_sz = Some(new_sz.into());
422        self
423    }
424
425    /// Set the new order price.
426    pub fn new_price(mut self, new_px: impl Into<String>) -> Self {
427        self.new_px = Some(new_px.into());
428        self
429    }
430}
431
432/// A request to close positions.
433#[derive(Debug, Clone, Serialize)]
434pub struct ClosePositionRequest {
435    #[serde(rename = "instId")]
436    inst_id: String,
437    #[serde(rename = "mgnMode")]
438    mgn_mode: TradeMode,
439    #[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
440    pos_side: Option<PositionSide>,
441    #[serde(skip_serializing_if = "Option::is_none")]
442    ccy: Option<String>,
443    #[serde(rename = "autoCxl", skip_serializing_if = "Option::is_none")]
444    auto_cancel: Option<bool>,
445    #[serde(rename = "clOrdId", skip_serializing_if = "Option::is_none")]
446    cl_ord_id: Option<String>,
447    #[serde(skip_serializing_if = "Option::is_none")]
448    tag: Option<String>,
449}
450
451impl ClosePositionRequest {
452    /// Create a close-position request.
453    pub fn new(inst_id: impl Into<String>, mgn_mode: TradeMode) -> Self {
454        Self {
455            inst_id: inst_id.into(),
456            mgn_mode,
457            pos_side: None,
458            ccy: None,
459            auto_cancel: None,
460            cl_ord_id: None,
461            tag: None,
462        }
463    }
464
465    /// Set the position side.
466    pub fn position_side(mut self, pos_side: PositionSide) -> Self {
467        self.pos_side = Some(pos_side);
468        self
469    }
470
471    /// Set the margin currency.
472    pub fn currency(mut self, ccy: impl Into<String>) -> Self {
473        self.ccy = Some(ccy.into());
474        self
475    }
476
477    /// Set whether pending orders should be canceled automatically.
478    pub fn auto_cancel(mut self, auto_cancel: bool) -> Self {
479        self.auto_cancel = Some(auto_cancel);
480        self
481    }
482
483    /// Set the client order ID.
484    pub fn client_order_id(mut self, cl_ord_id: impl Into<String>) -> Self {
485        self.cl_ord_id = Some(cl_ord_id.into());
486        self
487    }
488
489    /// Set an order tag.
490    pub fn tag(mut self, tag: impl Into<String>) -> Self {
491        self.tag = Some(tag.into());
492        self
493    }
494}
495
496/// Query parameters for pending order lists.
497#[derive(Debug, Clone, Default, Serialize)]
498pub struct OrderListRequest {
499    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
500    inst_type: Option<crate::model::InstType>,
501    #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
502    underlying: Option<String>,
503    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
504    inst_id: Option<String>,
505    #[serde(rename = "ordType", skip_serializing_if = "Option::is_none")]
506    ord_type: Option<OrderType>,
507    #[serde(skip_serializing_if = "Option::is_none")]
508    state: Option<OrderState>,
509    #[serde(skip_serializing_if = "Option::is_none")]
510    after: Option<String>,
511    #[serde(skip_serializing_if = "Option::is_none")]
512    before: Option<String>,
513    #[serde(skip_serializing_if = "Option::is_none")]
514    limit: Option<u32>,
515    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
516    inst_family: Option<String>,
517}
518
519impl OrderListRequest {
520    /// Create an empty order-list query.
521    pub fn new() -> Self {
522        Self::default()
523    }
524
525    /// Set the instrument type filter.
526    pub fn inst_type(mut self, inst_type: crate::model::InstType) -> Self {
527        self.inst_type = Some(inst_type);
528        self
529    }
530
531    /// Set the underlying filter.
532    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
533        self.underlying = Some(underlying.into());
534        self
535    }
536
537    /// Set the instrument ID filter.
538    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
539        self.inst_id = Some(inst_id.into());
540        self
541    }
542
543    /// Set the order type filter.
544    pub fn order_type(mut self, ord_type: OrderType) -> Self {
545        self.ord_type = Some(ord_type);
546        self
547    }
548
549    /// Set the order state filter.
550    pub fn state(mut self, state: OrderState) -> Self {
551        self.state = Some(state);
552        self
553    }
554
555    /// Return records after this pagination cursor.
556    pub fn after(mut self, after: impl Into<String>) -> Self {
557        self.after = Some(after.into());
558        self
559    }
560
561    /// Return records before this pagination cursor.
562    pub fn before(mut self, before: impl Into<String>) -> Self {
563        self.before = Some(before.into());
564        self
565    }
566
567    /// Set the maximum number of rows to return.
568    pub fn limit(mut self, limit: u32) -> Self {
569        self.limit = Some(limit);
570        self
571    }
572
573    /// Set the instrument family filter.
574    pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
575        self.inst_family = Some(inst_family.into());
576        self
577    }
578}
579
580/// Query parameters for order history.
581#[derive(Debug, Clone, Serialize)]
582pub struct OrderHistoryRequest {
583    #[serde(flatten)]
584    base: OrderListRequest,
585    #[serde(skip_serializing_if = "Option::is_none")]
586    begin: Option<String>,
587    #[serde(skip_serializing_if = "Option::is_none")]
588    end: Option<String>,
589}
590
591impl OrderHistoryRequest {
592    /// Create an order-history query with the required instrument type.
593    pub fn new(inst_type: crate::model::InstType) -> Self {
594        Self {
595            base: OrderListRequest::new().inst_type(inst_type),
596            begin: None,
597            end: None,
598        }
599    }
600
601    /// Set the common order-list filters.
602    pub fn filters(mut self, base: OrderListRequest) -> Self {
603        self.base = base;
604        self
605    }
606
607    /// Set the begin timestamp.
608    pub fn begin(mut self, begin: impl Into<String>) -> Self {
609        self.begin = Some(begin.into());
610        self
611    }
612
613    /// Set the end timestamp.
614    pub fn end(mut self, end: impl Into<String>) -> Self {
615        self.end = Some(end.into());
616        self
617    }
618}
619
620/// Query parameters for fills.
621#[derive(Debug, Clone, Default, Serialize)]
622pub struct FillsRequest {
623    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
624    inst_type: Option<crate::model::InstType>,
625    #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
626    underlying: Option<String>,
627    #[serde(rename = "instId", skip_serializing_if = "Option::is_none")]
628    inst_id: Option<String>,
629    #[serde(rename = "ordId", skip_serializing_if = "Option::is_none")]
630    ord_id: Option<String>,
631    #[serde(skip_serializing_if = "Option::is_none")]
632    after: Option<String>,
633    #[serde(skip_serializing_if = "Option::is_none")]
634    before: Option<String>,
635    #[serde(skip_serializing_if = "Option::is_none")]
636    begin: Option<String>,
637    #[serde(skip_serializing_if = "Option::is_none")]
638    end: Option<String>,
639    #[serde(skip_serializing_if = "Option::is_none")]
640    limit: Option<u32>,
641    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
642    inst_family: Option<String>,
643}
644
645impl FillsRequest {
646    /// Create an empty fills query.
647    pub fn new() -> Self {
648        Self::default()
649    }
650
651    /// Set the instrument type filter.
652    pub fn inst_type(mut self, inst_type: crate::model::InstType) -> Self {
653        self.inst_type = Some(inst_type);
654        self
655    }
656
657    /// Set the underlying filter.
658    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
659        self.underlying = Some(underlying.into());
660        self
661    }
662
663    /// Set the instrument ID filter.
664    pub fn inst_id(mut self, inst_id: impl Into<String>) -> Self {
665        self.inst_id = Some(inst_id.into());
666        self
667    }
668
669    /// Set the order ID filter.
670    pub fn order_id(mut self, ord_id: impl Into<String>) -> Self {
671        self.ord_id = Some(ord_id.into());
672        self
673    }
674
675    /// Return records after this pagination cursor.
676    pub fn after(mut self, after: impl Into<String>) -> Self {
677        self.after = Some(after.into());
678        self
679    }
680
681    /// Return records before this pagination cursor.
682    pub fn before(mut self, before: impl Into<String>) -> Self {
683        self.before = Some(before.into());
684        self
685    }
686
687    /// Set the begin timestamp.
688    pub fn begin(mut self, begin: impl Into<String>) -> Self {
689        self.begin = Some(begin.into());
690        self
691    }
692
693    /// Set the end timestamp.
694    pub fn end(mut self, end: impl Into<String>) -> Self {
695        self.end = Some(end.into());
696        self
697    }
698
699    /// Set the maximum number of rows to return.
700    pub fn limit(mut self, limit: u32) -> Self {
701        self.limit = Some(limit);
702        self
703    }
704
705    /// Set the instrument family filter.
706    pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
707        self.inst_family = Some(inst_family.into());
708        self
709    }
710}
711
712/// The result of placing an order.
713#[derive(Debug, Clone, Deserialize)]
714#[serde(rename_all = "camelCase")]
715#[non_exhaustive]
716pub struct PlaceOrderResult {
717    /// OKX order ID.
718    #[serde(default)]
719    pub ord_id: String,
720    /// Client-supplied order ID, if any.
721    #[serde(default)]
722    pub cl_ord_id: String,
723    /// Order tag.
724    #[serde(default)]
725    pub tag: String,
726    /// Per-order status code (`"0"` on success).
727    pub s_code: String,
728    /// Per-order status message.
729    #[serde(default)]
730    pub s_msg: String,
731    /// Timestamp (Unix milliseconds).
732    #[serde(default)]
733    pub ts: NumberString,
734}
735
736/// The result of cancelling an order.
737#[derive(Debug, Clone, Deserialize)]
738#[serde(rename_all = "camelCase")]
739#[non_exhaustive]
740pub struct CancelOrderResult {
741    /// OKX order ID.
742    #[serde(default)]
743    pub ord_id: String,
744    /// Client-supplied order ID, if any.
745    #[serde(default)]
746    pub cl_ord_id: String,
747    /// Per-order status code (`"0"` on success).
748    pub s_code: String,
749    /// Per-order status message.
750    #[serde(default)]
751    pub s_msg: String,
752}
753
754/// The result of amending an order.
755#[derive(Debug, Clone, Deserialize)]
756#[serde(rename_all = "camelCase")]
757#[non_exhaustive]
758pub struct AmendOrderResult {
759    /// OKX order ID.
760    #[serde(default)]
761    pub ord_id: String,
762    /// Client-supplied order ID, if any.
763    #[serde(default)]
764    pub cl_ord_id: String,
765    /// Request ID, if supplied.
766    #[serde(default)]
767    pub req_id: String,
768    /// Per-order status code (`"0"` on success).
769    pub s_code: String,
770    /// Per-order status message.
771    #[serde(default)]
772    pub s_msg: String,
773}
774
775/// The result of closing a position.
776#[derive(Debug, Clone, Deserialize)]
777#[serde(rename_all = "camelCase")]
778#[non_exhaustive]
779pub struct ClosePositionResult {
780    /// Instrument ID.
781    #[serde(default)]
782    pub inst_id: String,
783    /// Position side.
784    #[serde(default)]
785    pub pos_side: String,
786    /// Client order ID, if supplied.
787    #[serde(default)]
788    pub cl_ord_id: String,
789    /// Tag, if supplied.
790    #[serde(default)]
791    pub tag: String,
792}
793
794/// Details of an existing order.
795#[derive(Debug, Clone, Deserialize)]
796#[serde(rename_all = "camelCase")]
797#[non_exhaustive]
798pub struct Order {
799    /// Instrument ID.
800    pub inst_id: String,
801    /// OKX order ID.
802    pub ord_id: String,
803    /// Client-supplied order ID, if any.
804    #[serde(default)]
805    pub cl_ord_id: String,
806    /// Order price.
807    #[serde(default)]
808    pub px: NumberString,
809    /// Order size.
810    #[serde(default)]
811    pub sz: NumberString,
812    /// Order type.
813    pub ord_type: OrderType,
814    /// Order side.
815    pub side: OrderSide,
816    /// Position side.
817    pub pos_side: PositionSide,
818    /// Trade mode.
819    pub td_mode: TradeMode,
820    /// Accumulated filled size.
821    #[serde(default)]
822    pub acc_fill_sz: NumberString,
823    /// Average fill price.
824    #[serde(default)]
825    pub avg_px: NumberString,
826    /// Order state.
827    pub state: OrderState,
828    /// Creation time (Unix milliseconds).
829    #[serde(default)]
830    pub c_time: NumberString,
831}
832
833/// A trade fill.
834#[derive(Debug, Clone, Deserialize)]
835#[serde(rename_all = "camelCase")]
836#[non_exhaustive]
837pub struct Fill {
838    /// Instrument type.
839    #[serde(default)]
840    pub inst_type: String,
841    /// Instrument ID.
842    pub inst_id: String,
843    /// Trade ID.
844    #[serde(default)]
845    pub trade_id: String,
846    /// OKX order ID.
847    #[serde(default)]
848    pub ord_id: String,
849    /// Fill price.
850    #[serde(default)]
851    pub fill_px: NumberString,
852    /// Fill size.
853    #[serde(default)]
854    pub fill_sz: NumberString,
855    /// Fill side.
856    pub side: OrderSide,
857    /// Order type.
858    pub ord_type: OrderType,
859    /// Fee currency.
860    #[serde(default)]
861    pub fee_ccy: String,
862    /// Fee amount.
863    #[serde(default)]
864    pub fee: NumberString,
865    /// Fill timestamp (Unix milliseconds).
866    #[serde(default)]
867    pub ts: NumberString,
868}
869
870#[cfg(test)]
871mod tests {
872    use crate::model::{OrderSide, OrderType, TradeMode};
873    use crate::test_util::MockTransport;
874    use crate::{Credentials, OkxClient};
875
876    use super::PlaceOrderRequest;
877
878    fn signed_client(mock: MockTransport) -> OkxClient<MockTransport> {
879        OkxClient::with_transport(mock)
880            .credentials(Credentials::new("key", "secret", "pass"))
881            .build()
882    }
883
884    #[tokio::test]
885    async fn place_order_posts_signed_json_body() {
886        let body = r#"{"code":"0","msg":"","data":[
887            {"ordId":"312","clOrdId":"b1","tag":"","sCode":"0","sMsg":"","ts":"1597026383085"}]}"#;
888        let mock = MockTransport::new(body);
889        let client = signed_client(mock.clone());
890
891        let request = PlaceOrderRequest::new(
892            "BTC-USDT",
893            TradeMode::Cash,
894            OrderSide::Buy,
895            OrderType::Limit,
896            "0.01",
897        )
898        .price("42000")
899        .client_order_id("b1");
900
901        let result = client.trade().place_order(&request).await.unwrap();
902        assert_eq!(result[0].ord_id, "312");
903        assert_eq!(result[0].s_code, "0");
904
905        let req = mock.captured();
906        assert_eq!(req.method, http::Method::POST);
907        assert!(req.uri.ends_with("/api/v5/trade/order"));
908        assert!(req.is_signed());
909
910        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
911        assert_eq!(sent["instId"], "BTC-USDT");
912        assert_eq!(sent["tdMode"], "cash");
913        assert_eq!(sent["side"], "buy");
914        assert_eq!(sent["ordType"], "limit");
915        assert_eq!(sent["sz"], "0.01");
916        assert_eq!(sent["px"], "42000");
917        assert_eq!(sent["clOrdId"], "b1");
918        // Unset optional fields are omitted.
919        assert!(sent.get("reduceOnly").is_none());
920    }
921
922    #[tokio::test]
923    async fn place_multiple_orders_posts_array_body() {
924        let body = r#"{"code":"0","msg":"","data":[
925            {"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""},
926            {"ordId":"313","clOrdId":"b2","sCode":"0","sMsg":""}]}"#;
927        let mock = MockTransport::new(body);
928        let client = signed_client(mock.clone());
929        let requests = vec![
930            PlaceOrderRequest::new(
931                "BTC-USDT",
932                TradeMode::Cash,
933                OrderSide::Buy,
934                OrderType::Limit,
935                "0.01",
936            )
937            .price("42000")
938            .client_order_id("b1"),
939            PlaceOrderRequest::new(
940                "BTC-USDT",
941                TradeMode::Cash,
942                OrderSide::Sell,
943                OrderType::Limit,
944                "0.02",
945            )
946            .price("43000")
947            .client_order_id("b2"),
948        ];
949
950        let result = client
951            .trade()
952            .place_multiple_orders(&requests)
953            .await
954            .unwrap();
955        assert_eq!(result[1].ord_id, "313");
956
957        let req = mock.captured();
958        assert_eq!(req.method, http::Method::POST);
959        assert!(req.uri.ends_with("/api/v5/trade/batch-orders"));
960        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
961        assert_eq!(sent[0]["clOrdId"], "b1");
962        assert_eq!(sent[1]["side"], "sell");
963        assert!(req.is_signed());
964    }
965
966    #[tokio::test]
967    async fn cancel_order_posts_ids() {
968        let body = r#"{"code":"0","msg":"","data":[
969            {"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""}]}"#;
970        let mock = MockTransport::new(body);
971        let client = signed_client(mock.clone());
972
973        let result = client.trade().cancel_order("BTC-USDT", "312").await.unwrap();
974        assert_eq!(result[0].ord_id, "312");
975
976        let req = mock.captured();
977        assert_eq!(req.method, http::Method::POST);
978        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
979        assert_eq!(sent["instId"], "BTC-USDT");
980        assert_eq!(sent["ordId"], "312");
981    }
982
983    #[tokio::test]
984    async fn cancel_multiple_orders_posts_array_body() {
985        let body = r#"{"code":"0","msg":"","data":[
986            {"ordId":"312","clOrdId":"b1","sCode":"0","sMsg":""}]}"#;
987        let mock = MockTransport::new(body);
988        let client = signed_client(mock.clone());
989        let requests = vec![super::CancelOrderRequest::by_order_id("BTC-USDT", "312")];
990
991        let result = client
992            .trade()
993            .cancel_multiple_orders(&requests)
994            .await
995            .unwrap();
996        assert_eq!(result[0].s_code, "0");
997
998        let req = mock.captured();
999        assert!(req.uri.ends_with("/api/v5/trade/cancel-batch-orders"));
1000        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1001        assert_eq!(sent[0]["instId"], "BTC-USDT");
1002        assert_eq!(sent[0]["ordId"], "312");
1003        assert!(sent[0].get("clOrdId").is_none());
1004        assert!(req.is_signed());
1005    }
1006
1007    #[tokio::test]
1008    async fn amend_order_posts_builder_body() {
1009        let body = r#"{"code":"0","msg":"","data":[
1010            {"ordId":"312","clOrdId":"b1","reqId":"r1","sCode":"0","sMsg":""}]}"#;
1011        let mock = MockTransport::new(body);
1012        let client = signed_client(mock.clone());
1013        let request = super::AmendOrderRequest::new("BTC-USDT")
1014            .order_id("312")
1015            .request_id("r1")
1016            .new_price("42100");
1017
1018        let result = client.trade().amend_order(&request).await.unwrap();
1019        assert_eq!(result[0].req_id, "r1");
1020
1021        let req = mock.captured();
1022        assert!(req.uri.ends_with("/api/v5/trade/amend-order"));
1023        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1024        assert_eq!(sent["instId"], "BTC-USDT");
1025        assert_eq!(sent["ordId"], "312");
1026        assert_eq!(sent["newPx"], "42100");
1027        assert!(sent.get("newSz").is_none());
1028        assert!(req.is_signed());
1029    }
1030
1031    #[tokio::test]
1032    async fn amend_multiple_orders_posts_array_body() {
1033        let body = r#"{"code":"0","msg":"","data":[
1034            {"ordId":"312","clOrdId":"b1","reqId":"r1","sCode":"0","sMsg":""}]}"#;
1035        let mock = MockTransport::new(body);
1036        let client = signed_client(mock.clone());
1037        let requests = vec![super::AmendOrderRequest::new("BTC-USDT")
1038            .client_order_id("b1")
1039            .new_size("0.03")];
1040
1041        let result = client
1042            .trade()
1043            .amend_multiple_orders(&requests)
1044            .await
1045            .unwrap();
1046        assert_eq!(result[0].s_code, "0");
1047
1048        let req = mock.captured();
1049        assert!(req.uri.ends_with("/api/v5/trade/amend-batch-orders"));
1050        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1051        assert_eq!(sent[0]["clOrdId"], "b1");
1052        assert_eq!(sent[0]["newSz"], "0.03");
1053        assert!(req.is_signed());
1054    }
1055
1056    #[tokio::test]
1057    async fn close_positions_posts_builder_body() {
1058        let body = r#"{"code":"0","msg":"","data":[
1059            {"instId":"BTC-USDT-SWAP","posSide":"long","clOrdId":"close1","tag":"t"}]}"#;
1060        let mock = MockTransport::new(body);
1061        let client = signed_client(mock.clone());
1062        let request = super::ClosePositionRequest::new("BTC-USDT-SWAP", TradeMode::Cross)
1063            .position_side(crate::model::PositionSide::Long)
1064            .auto_cancel(true)
1065            .client_order_id("close1");
1066
1067        let result = client.trade().close_positions(&request).await.unwrap();
1068        assert_eq!(result[0].cl_ord_id, "close1");
1069
1070        let req = mock.captured();
1071        assert!(req.uri.ends_with("/api/v5/trade/close-position"));
1072        let sent: serde_json::Value = serde_json::from_str(req.body_str()).unwrap();
1073        assert_eq!(sent["instId"], "BTC-USDT-SWAP");
1074        assert_eq!(sent["mgnMode"], "cross");
1075        assert_eq!(sent["posSide"], "long");
1076        assert_eq!(sent["autoCxl"], true);
1077        assert!(req.is_signed());
1078    }
1079
1080    #[tokio::test]
1081    async fn get_order_queries_and_parses() {
1082        let body = r#"{"code":"0","msg":"","data":[
1083            {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1084             "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1085             "accFillSz":"0","avgPx":"","state":"live","cTime":"1597026383085"}]}"#;
1086        let mock = MockTransport::new(body);
1087        let client = signed_client(mock.clone());
1088
1089        let orders = client.trade().get_order("BTC-USDT", "312").await.unwrap();
1090        assert_eq!(orders[0].ord_id, "312");
1091        assert_eq!(orders[0].state, crate::model::OrderState::Live);
1092        assert_eq!(orders[0].side, OrderSide::Buy);
1093
1094        let req = mock.captured();
1095        assert_eq!(req.method, http::Method::GET);
1096        assert_eq!(req.query(), Some("instId=BTC-USDT&ordId=312"));
1097        assert!(req.is_signed());
1098    }
1099
1100    #[tokio::test]
1101    async fn get_order_list_uses_builder_query() {
1102        let body = r#"{"code":"0","msg":"","data":[
1103            {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1104             "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1105             "accFillSz":"0","avgPx":"","state":"live","cTime":"1597026383085"}]}"#;
1106        let mock = MockTransport::new(body);
1107        let client = signed_client(mock.clone());
1108        let request = super::OrderListRequest::new()
1109            .inst_type(crate::model::InstType::Spot)
1110            .inst_id("BTC-USDT")
1111            .limit(1);
1112
1113        let orders = client.trade().get_order_list(&request).await.unwrap();
1114        assert_eq!(orders[0].ord_id, "312");
1115
1116        let req = mock.captured();
1117        assert_eq!(req.query(), Some("instType=SPOT&instId=BTC-USDT&limit=1"));
1118        assert!(req.is_signed());
1119    }
1120
1121    #[tokio::test]
1122    async fn get_orders_history_uses_builder_query() {
1123        let body = r#"{"code":"0","msg":"","data":[
1124            {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1125             "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1126             "accFillSz":"0","avgPx":"","state":"filled","cTime":"1597026383085"}]}"#;
1127        let mock = MockTransport::new(body);
1128        let client = signed_client(mock.clone());
1129        let request = super::OrderHistoryRequest::new(crate::model::InstType::Spot)
1130            .begin("100")
1131            .end("200");
1132
1133        let orders = client.trade().get_orders_history(&request).await.unwrap();
1134        assert_eq!(orders[0].state, crate::model::OrderState::Filled);
1135
1136        let req = mock.captured();
1137        assert_eq!(req.query(), Some("instType=SPOT&begin=100&end=200"));
1138        assert!(req.is_signed());
1139    }
1140
1141    #[tokio::test]
1142    async fn get_orders_history_archive_uses_builder_query() {
1143        let body = r#"{"code":"0","msg":"","data":[
1144            {"instId":"BTC-USDT","ordId":"312","clOrdId":"b1","px":"42000","sz":"0.01",
1145             "ordType":"limit","side":"buy","posSide":"net","tdMode":"cash",
1146             "accFillSz":"0","avgPx":"","state":"canceled","cTime":"1597026383085"}]}"#;
1147        let mock = MockTransport::new(body);
1148        let client = signed_client(mock.clone());
1149        let request = super::OrderHistoryRequest::new(crate::model::InstType::Spot)
1150            .filters(super::OrderListRequest::new().inst_type(crate::model::InstType::Spot).limit(1));
1151
1152        let orders = client
1153            .trade()
1154            .get_orders_history_archive(&request)
1155            .await
1156            .unwrap();
1157        assert_eq!(orders[0].state, crate::model::OrderState::Canceled);
1158
1159        let req = mock.captured();
1160        assert_eq!(req.query(), Some("instType=SPOT&limit=1"));
1161        assert!(req.is_signed());
1162    }
1163
1164    #[tokio::test]
1165    async fn get_fills_uses_builder_query() {
1166        let body = r#"{"code":"0","msg":"","data":[
1167            {"instType":"SPOT","instId":"BTC-USDT","tradeId":"t1","ordId":"312",
1168             "fillPx":"42000","fillSz":"0.01","side":"buy","ordType":"limit",
1169             "feeCcy":"USDT","fee":"-1","ts":"1597026383085"}]}"#;
1170        let mock = MockTransport::new(body);
1171        let client = signed_client(mock.clone());
1172        let request = super::FillsRequest::new()
1173            .inst_type(crate::model::InstType::Spot)
1174            .order_id("312");
1175
1176        let fills = client.trade().get_fills(&request).await.unwrap();
1177        assert_eq!(fills[0].trade_id, "t1");
1178
1179        let req = mock.captured();
1180        assert_eq!(req.query(), Some("instType=SPOT&ordId=312"));
1181        assert!(req.is_signed());
1182    }
1183
1184    #[tokio::test]
1185    async fn get_fills_history_uses_builder_query() {
1186        let body = r#"{"code":"0","msg":"","data":[
1187            {"instType":"SPOT","instId":"BTC-USDT","tradeId":"t1","ordId":"312",
1188             "fillPx":"42000","fillSz":"0.01","side":"buy","ordType":"limit",
1189             "feeCcy":"USDT","fee":"-1","ts":"1597026383085"}]}"#;
1190        let mock = MockTransport::new(body);
1191        let client = signed_client(mock.clone());
1192        let request = super::FillsRequest::new()
1193            .inst_type(crate::model::InstType::Spot)
1194            .begin("100")
1195            .end("200")
1196            .limit(1);
1197
1198        let fills = client.trade().get_fills_history(&request).await.unwrap();
1199        assert_eq!(fills[0].fee.as_str(), "-1");
1200
1201        let req = mock.captured();
1202        assert_eq!(req.query(), Some("instType=SPOT&begin=100&end=200&limit=1"));
1203        assert!(req.is_signed());
1204    }
1205}