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