Skip to main content

ccxt_exchanges/okx/rest/
trading.rs

1//! Trading operations for OKX REST API.
2
3use super::super::{Okx, parser};
4use crate::okx::signed_request::HttpMethod;
5use ccxt_core::{
6    Error, ParseError, Result,
7    types::{Amount, Order, OrderRequest, OrderSide, OrderType, Price, TimeInForce},
8};
9use tracing::warn;
10
11impl Okx {
12    /// Create a new order using the builder pattern.
13    ///
14    /// This is the preferred method for creating orders. It accepts an [`OrderRequest`]
15    /// built using the builder pattern, which provides compile-time validation of
16    /// required fields and a more ergonomic API.
17    ///
18    /// # Arguments
19    ///
20    /// * `request` - Order request built via [`OrderRequest::builder()`]
21    ///
22    /// # Returns
23    ///
24    /// Returns the created [`Order`] structure with order details.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error if authentication fails, market is not found, or the API request fails.
29    ///
30    /// _Requirements: 2.2, 2.6_
31    pub async fn create_order_v2(&self, request: OrderRequest) -> Result<Order> {
32        let market = self.base().market(&request.symbol).await?;
33
34        let path = Self::build_api_path("/trade/order");
35
36        let mut map = serde_json::Map::new();
37        map.insert(
38            "instId".to_string(),
39            serde_json::Value::String(market.id.clone()),
40        );
41        map.insert(
42            "tdMode".to_string(),
43            serde_json::Value::String(self.options().account_mode.clone()),
44        );
45        map.insert(
46            "side".to_string(),
47            serde_json::Value::String(match request.side {
48                OrderSide::Buy => "buy".to_string(),
49                OrderSide::Sell => "sell".to_string(),
50            }),
51        );
52        map.insert(
53            "ordType".to_string(),
54            serde_json::Value::String(match request.order_type {
55                OrderType::LimitMaker => "post_only".to_string(),
56                OrderType::StopLoss
57                | OrderType::StopMarket
58                | OrderType::TakeProfit
59                | OrderType::TrailingStop => "market".to_string(),
60                _ => "limit".to_string(),
61            }),
62        );
63        map.insert(
64            "sz".to_string(),
65            serde_json::Value::String(request.amount.to_string()),
66        );
67
68        if let Some(p) = request.price {
69            if request.order_type == OrderType::Limit || request.order_type == OrderType::LimitMaker
70            {
71                map.insert("px".to_string(), serde_json::Value::String(p.to_string()));
72            }
73        }
74
75        if request.time_in_force == Some(TimeInForce::PO) || request.post_only == Some(true) {
76            // Already handled via ordType = "post_only" above
77        }
78
79        if let Some(client_id) = request.client_order_id {
80            map.insert("clOrdId".to_string(), serde_json::Value::String(client_id));
81        }
82
83        if let Some(reduce_only) = request.reduce_only {
84            map.insert(
85                "reduceOnly".to_string(),
86                serde_json::Value::Bool(reduce_only),
87            );
88        }
89
90        if let Some(trigger) = request.trigger_price.or(request.stop_price) {
91            map.insert(
92                "triggerPx".to_string(),
93                serde_json::Value::String(trigger.to_string()),
94            );
95        }
96
97        if let Some(pos_side) = request.position_side {
98            map.insert("posSide".to_string(), serde_json::Value::String(pos_side));
99        }
100
101        let body = serde_json::Value::Object(map);
102
103        let response = self
104            .signed_request(&path)
105            .method(HttpMethod::Post)
106            .body(body)
107            .execute()
108            .await?;
109
110        let data = response
111            .get("data")
112            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
113
114        let orders = data.as_array().ok_or_else(|| {
115            Error::from(ParseError::invalid_format(
116                "data",
117                "Expected array of orders",
118            ))
119        })?;
120
121        if orders.is_empty() {
122            return Err(Error::exchange("-1", "No order data returned"));
123        }
124
125        parser::parse_order(&orders[0], Some(&market))
126    }
127
128    /// Create a new order (deprecated).
129    ///
130    /// # Deprecated
131    ///
132    /// This method is deprecated. Use [`create_order_v2`](Self::create_order_v2) with
133    /// [`OrderRequest::builder()`] instead for a more ergonomic API.
134    ///
135    /// # Arguments
136    ///
137    /// * `symbol` - Trading pair symbol.
138    /// * `order_type` - Order type (Market, Limit).
139    /// * `side` - Order side (Buy or Sell).
140    /// * `amount` - Order quantity.
141    /// * `price` - Optional price (required for limit orders).
142    ///
143    /// # Returns
144    ///
145    /// Returns the created [`Order`] structure with order details.
146    #[deprecated(
147        since = "0.2.0",
148        note = "Use create_order_v2 with OrderRequest::builder() instead"
149    )]
150    pub async fn create_order(
151        &self,
152        symbol: &str,
153        order_type: OrderType,
154        side: OrderSide,
155        amount: Amount,
156        price: Option<Price>,
157    ) -> Result<Order> {
158        let market = self.base().market(symbol).await?;
159
160        let path = Self::build_api_path("/trade/order");
161
162        let mut map = serde_json::Map::new();
163        map.insert(
164            "instId".to_string(),
165            serde_json::Value::String(market.id.clone()),
166        );
167        map.insert(
168            "tdMode".to_string(),
169            serde_json::Value::String(self.options().account_mode.clone()),
170        );
171        map.insert(
172            "side".to_string(),
173            serde_json::Value::String(match side {
174                OrderSide::Buy => "buy".to_string(),
175                OrderSide::Sell => "sell".to_string(),
176            }),
177        );
178        map.insert(
179            "ordType".to_string(),
180            serde_json::Value::String(match order_type {
181                OrderType::Market => "market".to_string(),
182                OrderType::LimitMaker => "post_only".to_string(),
183                _ => "limit".to_string(),
184            }),
185        );
186        map.insert(
187            "sz".to_string(),
188            serde_json::Value::String(amount.to_string()),
189        );
190
191        if let Some(p) = price {
192            if order_type == OrderType::Limit || order_type == OrderType::LimitMaker {
193                map.insert("px".to_string(), serde_json::Value::String(p.to_string()));
194            }
195        }
196        let body = serde_json::Value::Object(map);
197
198        let response = self
199            .signed_request(&path)
200            .method(HttpMethod::Post)
201            .body(body)
202            .execute()
203            .await?;
204
205        let data = response
206            .get("data")
207            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
208
209        let orders = data.as_array().ok_or_else(|| {
210            Error::from(ParseError::invalid_format(
211                "data",
212                "Expected array of orders",
213            ))
214        })?;
215
216        if orders.is_empty() {
217            return Err(Error::exchange("-1", "No order data returned"));
218        }
219
220        parser::parse_order(&orders[0], Some(&market))
221    }
222
223    /// Cancel an existing order.
224    ///
225    /// # Arguments
226    ///
227    /// * `id` - Order ID to cancel.
228    /// * `symbol` - Trading pair symbol.
229    ///
230    /// # Returns
231    ///
232    /// Returns the canceled [`Order`] structure.
233    pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
234        let market = self.base().market(symbol).await?;
235
236        let path = Self::build_api_path("/trade/cancel-order");
237
238        let mut map = serde_json::Map::new();
239        map.insert(
240            "instId".to_string(),
241            serde_json::Value::String(market.id.clone()),
242        );
243        map.insert(
244            "ordId".to_string(),
245            serde_json::Value::String(id.to_string()),
246        );
247        let body = serde_json::Value::Object(map);
248
249        let response = self
250            .signed_request(&path)
251            .method(HttpMethod::Post)
252            .body(body)
253            .execute()
254            .await?;
255
256        let data = response
257            .get("data")
258            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
259
260        let orders = data.as_array().ok_or_else(|| {
261            Error::from(ParseError::invalid_format(
262                "data",
263                "Expected array of orders",
264            ))
265        })?;
266
267        if orders.is_empty() {
268            return Err(Error::exchange("-1", "No order data returned"));
269        }
270
271        parser::parse_order(&orders[0], Some(&market))
272    }
273
274    /// Fetch a single order by ID.
275    ///
276    /// # Arguments
277    ///
278    /// * `id` - Order ID to fetch.
279    /// * `symbol` - Trading pair symbol.
280    ///
281    /// # Returns
282    ///
283    /// Returns the [`Order`] structure with current status.
284    pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
285        let market = self.base().market(symbol).await?;
286
287        let path = Self::build_api_path("/trade/order");
288
289        let response = self
290            .signed_request(&path)
291            .param("instId", &market.id)
292            .param("ordId", id)
293            .execute()
294            .await?;
295
296        let data = response
297            .get("data")
298            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
299
300        let orders = data.as_array().ok_or_else(|| {
301            Error::from(ParseError::invalid_format(
302                "data",
303                "Expected array of orders",
304            ))
305        })?;
306
307        if orders.is_empty() {
308            return Err(Error::exchange("51400", "Order not found"));
309        }
310
311        parser::parse_order(&orders[0], Some(&market))
312    }
313
314    /// Fetch open orders.
315    ///
316    /// # Arguments
317    ///
318    /// * `symbol` - Optional trading pair symbol. If None, fetches all open orders.
319    /// * `since` - Optional start timestamp in milliseconds.
320    /// * `limit` - Optional limit on number of orders (maximum: 100).
321    ///
322    /// # Returns
323    ///
324    /// Returns a vector of open [`Order`] structures.
325    pub async fn fetch_open_orders(
326        &self,
327        symbol: Option<&str>,
328        since: Option<i64>,
329        limit: Option<u32>,
330    ) -> Result<Vec<Order>> {
331        let path = Self::build_api_path("/trade/orders-pending");
332
333        let actual_limit = limit.map_or(100, |l| l.min(100));
334
335        let market = if let Some(sym) = symbol {
336            let m = self.base().market(sym).await?;
337            Some(m)
338        } else {
339            None
340        };
341
342        let mut builder = self
343            .signed_request(&path)
344            .param("instType", self.get_inst_type())
345            .param("limit", actual_limit);
346
347        if let Some(ref m) = market {
348            builder = builder.param("instId", &m.id);
349        }
350
351        if let Some(start_time) = since {
352            builder = builder.param("begin", start_time);
353        }
354
355        let response = builder.execute().await?;
356
357        let data = response
358            .get("data")
359            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
360
361        let orders_array = data.as_array().ok_or_else(|| {
362            Error::from(ParseError::invalid_format(
363                "data",
364                "Expected array of orders",
365            ))
366        })?;
367
368        let mut orders = Vec::new();
369        for order_data in orders_array {
370            match parser::parse_order(order_data, market.as_deref()) {
371                Ok(order) => orders.push(order),
372                Err(e) => {
373                    warn!(error = %e, "Failed to parse open order");
374                }
375            }
376        }
377
378        Ok(orders)
379    }
380
381    /// Fetch closed orders.
382    ///
383    /// # Arguments
384    ///
385    /// * `symbol` - Optional trading pair symbol. If None, fetches all closed orders.
386    /// * `since` - Optional start timestamp in milliseconds.
387    /// * `limit` - Optional limit on number of orders (maximum: 100).
388    ///
389    /// # Returns
390    ///
391    /// Returns a vector of closed [`Order`] structures.
392    pub async fn fetch_closed_orders(
393        &self,
394        symbol: Option<&str>,
395        since: Option<i64>,
396        limit: Option<u32>,
397    ) -> Result<Vec<Order>> {
398        let path = Self::build_api_path("/trade/orders-history");
399
400        let actual_limit = limit.map_or(100, |l| l.min(100));
401
402        let market = if let Some(sym) = symbol {
403            let m = self.base().market(sym).await?;
404            Some(m)
405        } else {
406            None
407        };
408
409        let mut builder = self
410            .signed_request(&path)
411            .param("instType", self.get_inst_type())
412            .param("limit", actual_limit);
413
414        if let Some(ref m) = market {
415            builder = builder.param("instId", &m.id);
416        }
417
418        if let Some(start_time) = since {
419            builder = builder.param("begin", start_time);
420        }
421
422        let response = builder.execute().await?;
423
424        let data = response
425            .get("data")
426            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
427
428        let orders_array = data.as_array().ok_or_else(|| {
429            Error::from(ParseError::invalid_format(
430                "data",
431                "Expected array of orders",
432            ))
433        })?;
434
435        let mut orders = Vec::new();
436        for order_data in orders_array {
437            match parser::parse_order(order_data, market.as_deref()) {
438                Ok(order) => orders.push(order),
439                Err(e) => {
440                    warn!(error = %e, "Failed to parse closed order");
441                }
442            }
443        }
444
445        Ok(orders)
446    }
447}