Skip to main content

oanda_rs/endpoints/
orders.rs

1//! Order endpoints: creating, listing, replacing and cancelling orders.
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Error;
7use crate::models::transaction::{
8    OrderCancelRejectTransaction, OrderCancelTransaction,
9    OrderClientExtensionsModifyRejectTransaction, OrderClientExtensionsModifyTransaction,
10    OrderFillTransaction, Transaction,
11};
12use crate::models::{
13    AccountId, ClientExtensions, InstrumentName, Order, OrderId, OrderRequest, OrderSpecifier,
14    OrderStateFilter, TransactionId,
15};
16
17impl Client {
18    /// Create an order for an account.
19    ///
20    /// `POST /v3/accounts/{accountID}/orders`
21    ///
22    /// # Errors
23    ///
24    /// Rejections (HTTP 400/404) carry an `orderRejectTransaction`; recover
25    /// it with
26    /// [`ApiErrorBody::details::<CreateOrderRejectBody>`](crate::ApiErrorBody::details).
27    ///
28    /// # Examples
29    ///
30    /// ```no_run
31    /// # async fn run() -> Result<(), oanda_rs::Error> {
32    /// # let client = oanda_rs::Client::new(oanda_rs::Environment::Practice, "token");
33    /// use oanda_rs::models::{MarketOrderRequest, StopLossDetails, TakeProfitDetails};
34    ///
35    /// let response = client
36    ///     .create_order(
37    ///         "101-004-1234567-001",
38    ///         MarketOrderRequest::new("EUR_USD", 100)
39    ///             .take_profit_on_fill(TakeProfitDetails::at_price("1.1050".parse().unwrap()))
40    ///             .stop_loss_on_fill(StopLossDetails::at_distance("0.0050".parse().unwrap())),
41    ///     )
42    ///     .await?;
43    /// println!("created: {:?}", response.order_create_transaction.id());
44    /// # Ok(())
45    /// # }
46    /// ```
47    pub async fn create_order(
48        &self,
49        account_id: impl Into<AccountId>,
50        order: impl Into<OrderRequest>,
51    ) -> Result<CreateOrderResponse, Error> {
52        let account_id = account_id.into();
53        let request = self
54            .post(&["accounts", account_id.as_str(), "orders"])
55            .json(&OrderRequestBody {
56                order: order.into(),
57            });
58        let (mut response, headers): (CreateOrderResponse, _) =
59            self.execute_with_headers(request).await?;
60        response.location = crate::transport::header_str(&headers, "Location").map(str::to_owned);
61        Ok(response)
62    }
63
64    /// Get a list of orders for an account.
65    ///
66    /// `GET /v3/accounts/{accountID}/orders`
67    pub fn list_orders(&self, account_id: impl Into<AccountId>) -> ListOrdersRequest {
68        ListOrdersRequest {
69            client: self.clone(),
70            account_id: account_id.into(),
71            params: Vec::new(),
72        }
73    }
74
75    /// List all pending orders in an account.
76    ///
77    /// `GET /v3/accounts/{accountID}/pendingOrders`
78    pub async fn list_pending_orders(
79        &self,
80        account_id: impl Into<AccountId>,
81    ) -> Result<ListOrdersResponse, Error> {
82        let account_id = account_id.into();
83        self.execute(self.get(&["accounts", account_id.as_str(), "pendingOrders"]))
84            .await
85    }
86
87    /// Get details for a single order in an account.
88    ///
89    /// `GET /v3/accounts/{accountID}/orders/{orderSpecifier}`
90    pub async fn order(
91        &self,
92        account_id: impl Into<AccountId>,
93        order: impl Into<OrderSpecifier>,
94    ) -> Result<OrderResponse, Error> {
95        let account_id = account_id.into();
96        let order = order.into();
97        self.execute(self.get(&["accounts", account_id.as_str(), "orders", order.as_str()]))
98            .await
99    }
100
101    /// Replace an order in an account by simultaneously cancelling it and
102    /// creating a replacement.
103    ///
104    /// `PUT /v3/accounts/{accountID}/orders/{orderSpecifier}`
105    pub async fn replace_order(
106        &self,
107        account_id: impl Into<AccountId>,
108        order: impl Into<OrderSpecifier>,
109        replacement: impl Into<OrderRequest>,
110    ) -> Result<ReplaceOrderResponse, Error> {
111        let account_id = account_id.into();
112        let order = order.into();
113        let request = self
114            .put(&["accounts", account_id.as_str(), "orders", order.as_str()])
115            .json(&OrderRequestBody {
116                order: replacement.into(),
117            });
118        let (mut response, headers): (ReplaceOrderResponse, _) =
119            self.execute_with_headers(request).await?;
120        response.location = crate::transport::header_str(&headers, "Location").map(str::to_owned);
121        Ok(response)
122    }
123
124    /// Cancel a pending order in an account.
125    ///
126    /// `PUT /v3/accounts/{accountID}/orders/{orderSpecifier}/cancel`
127    ///
128    /// # Errors
129    ///
130    /// A failed cancellation (HTTP 404) carries an
131    /// `orderCancelRejectTransaction`; recover it with
132    /// [`ApiErrorBody::details::<CancelOrderRejectBody>`](crate::ApiErrorBody::details).
133    pub async fn cancel_order(
134        &self,
135        account_id: impl Into<AccountId>,
136        order: impl Into<OrderSpecifier>,
137    ) -> Result<CancelOrderResponse, Error> {
138        let account_id = account_id.into();
139        let order = order.into();
140        self.execute(self.put(&[
141            "accounts",
142            account_id.as_str(),
143            "orders",
144            order.as_str(),
145            "cancel",
146        ]))
147        .await
148    }
149
150    /// Update the client extensions for an order in an account.
151    ///
152    /// **Do not set, modify, or delete `client_extensions` if the account
153    /// is associated with MT4.**
154    ///
155    /// `PUT /v3/accounts/{accountID}/orders/{orderSpecifier}/clientExtensions`
156    pub fn set_order_client_extensions(
157        &self,
158        account_id: impl Into<AccountId>,
159        order: impl Into<OrderSpecifier>,
160    ) -> SetOrderClientExtensionsRequest {
161        SetOrderClientExtensionsRequest {
162            client: self.clone(),
163            account_id: account_id.into(),
164            order: order.into(),
165            body: SetOrderClientExtensionsBody {
166                client_extensions: None,
167                trade_client_extensions: None,
168            },
169        }
170    }
171}
172
173#[derive(Debug, Serialize)]
174struct OrderRequestBody {
175    order: OrderRequest,
176}
177
178/// Response of [`Client::create_order`] (HTTP 201).
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[non_exhaustive]
181pub struct CreateOrderResponse {
182    /// The transaction that created the order.
183    #[serde(rename = "orderCreateTransaction")]
184    pub order_create_transaction: Transaction,
185    /// The transaction that filled the order (only for market orders, or
186    /// orders that were immediately filled).
187    #[serde(
188        rename = "orderFillTransaction",
189        skip_serializing_if = "Option::is_none"
190    )]
191    pub order_fill_transaction: Option<OrderFillTransaction>,
192    /// The transaction that cancelled the order (only when the order was
193    /// immediately cancelled).
194    #[serde(
195        rename = "orderCancelTransaction",
196        skip_serializing_if = "Option::is_none"
197    )]
198    pub order_cancel_transaction: Option<OrderCancelTransaction>,
199    /// The transaction that reissued the order (only when the order was
200    /// partially filled and reissued).
201    #[serde(
202        rename = "orderReissueTransaction",
203        skip_serializing_if = "Option::is_none"
204    )]
205    pub order_reissue_transaction: Option<Transaction>,
206    /// The transaction that rejected the reissue of the order.
207    #[serde(
208        rename = "orderReissueRejectTransaction",
209        skip_serializing_if = "Option::is_none"
210    )]
211    pub order_reissue_reject_transaction: Option<Transaction>,
212    /// The IDs of all transactions created while satisfying the request.
213    #[serde(
214        rename = "relatedTransactionIDs",
215        default,
216        skip_serializing_if = "Vec::is_empty"
217    )]
218    pub related_transaction_ids: Vec<TransactionId>,
219    /// The ID of the most recent transaction created for the account.
220    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
221    pub last_transaction_id: Option<TransactionId>,
222    /// The `Location` response header: the URL of the created order.
223    #[serde(skip)]
224    pub location: Option<String>,
225}
226
227/// Typed view of the error body when [`Client::create_order`] or
228/// [`Client::replace_order`] is rejected (HTTP 400/404). Recover it via
229/// [`ApiErrorBody::details`](crate::ApiErrorBody::details).
230#[derive(Debug, Clone, Serialize, Deserialize)]
231#[non_exhaustive]
232pub struct CreateOrderRejectBody {
233    /// The transaction that rejected the creation of the order.
234    #[serde(
235        rename = "orderRejectTransaction",
236        skip_serializing_if = "Option::is_none"
237    )]
238    pub order_reject_transaction: Option<Transaction>,
239    /// The IDs of all transactions created while satisfying the request.
240    #[serde(
241        rename = "relatedTransactionIDs",
242        default,
243        skip_serializing_if = "Vec::is_empty"
244    )]
245    pub related_transaction_ids: Vec<TransactionId>,
246    /// The ID of the most recent transaction created for the account.
247    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
248    pub last_transaction_id: Option<TransactionId>,
249}
250
251/// Builder for [`Client::list_orders`].
252#[derive(Debug)]
253pub struct ListOrdersRequest {
254    client: Client,
255    account_id: AccountId,
256    params: Vec<(&'static str, String)>,
257}
258
259impl ListOrdersRequest {
260    /// Restricts the response to the given order IDs.
261    pub fn ids<I>(mut self, ids: I) -> Self
262    where
263        I: IntoIterator,
264        I::Item: Into<OrderId>,
265    {
266        let joined = ids
267            .into_iter()
268            .map(|id| id.into().0)
269            .collect::<Vec<_>>()
270            .join(",");
271        self.params.push(("ids", joined));
272        self
273    }
274
275    /// The state to filter the requested orders by (default `PENDING`).
276    pub fn state(mut self, state: OrderStateFilter) -> Self {
277        self.params.push(("state", state.to_string()));
278        self
279    }
280
281    /// The instrument to filter the requested orders by.
282    pub fn instrument(mut self, instrument: impl Into<InstrumentName>) -> Self {
283        self.params
284            .push(("instrument", instrument.into().as_str().to_owned()));
285        self
286    }
287
288    /// The maximum number of orders to return (default 50, maximum 500).
289    pub fn count(mut self, count: u32) -> Self {
290        self.params.push(("count", count.to_string()));
291        self
292    }
293
294    /// The maximum order ID to return: only orders with IDs at or below
295    /// this are returned.
296    pub fn before_id(mut self, before_id: impl Into<OrderId>) -> Self {
297        self.params.push(("beforeID", before_id.into().0));
298        self
299    }
300
301    /// Performs the request.
302    pub async fn send(self) -> Result<ListOrdersResponse, Error> {
303        let request = self
304            .client
305            .get(&["accounts", self.account_id.as_str(), "orders"])
306            .query(&self.params);
307        self.client.execute(request).await
308    }
309}
310
311/// Response of [`Client::list_orders`] and [`Client::list_pending_orders`].
312#[derive(Debug, Clone, Serialize, Deserialize)]
313#[non_exhaustive]
314pub struct ListOrdersResponse {
315    /// The list of orders satisfying the request.
316    #[serde(rename = "orders", default, skip_serializing_if = "Vec::is_empty")]
317    pub orders: Vec<Order>,
318    /// The ID of the most recent transaction created for the account.
319    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
320    pub last_transaction_id: Option<TransactionId>,
321}
322
323/// Response of [`Client::order`].
324#[derive(Debug, Clone, Serialize, Deserialize)]
325#[non_exhaustive]
326pub struct OrderResponse {
327    /// The details of the requested order.
328    #[serde(rename = "order")]
329    pub order: Order,
330    /// The ID of the most recent transaction created for the account.
331    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
332    pub last_transaction_id: Option<TransactionId>,
333}
334
335/// Response of [`Client::replace_order`] (HTTP 201).
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[non_exhaustive]
338pub struct ReplaceOrderResponse {
339    /// The transaction that cancelled the order to be replaced.
340    #[serde(
341        rename = "orderCancelTransaction",
342        skip_serializing_if = "Option::is_none"
343    )]
344    pub order_cancel_transaction: Option<OrderCancelTransaction>,
345    /// The transaction that created the replacing order.
346    #[serde(
347        rename = "orderCreateTransaction",
348        skip_serializing_if = "Option::is_none"
349    )]
350    pub order_create_transaction: Option<Transaction>,
351    /// The transaction that filled the replacing order (only when it was
352    /// immediately filled).
353    #[serde(
354        rename = "orderFillTransaction",
355        skip_serializing_if = "Option::is_none"
356    )]
357    pub order_fill_transaction: Option<OrderFillTransaction>,
358    /// The transaction that reissued the replacing order.
359    #[serde(
360        rename = "orderReissueTransaction",
361        skip_serializing_if = "Option::is_none"
362    )]
363    pub order_reissue_transaction: Option<Transaction>,
364    /// The transaction that rejected the reissue of the replacing order.
365    #[serde(
366        rename = "orderReissueRejectTransaction",
367        skip_serializing_if = "Option::is_none"
368    )]
369    pub order_reissue_reject_transaction: Option<Transaction>,
370    /// The transaction that cancelled the replacing order (only when it
371    /// was immediately cancelled).
372    #[serde(
373        rename = "replacingOrderCancelTransaction",
374        skip_serializing_if = "Option::is_none"
375    )]
376    pub replacing_order_cancel_transaction: Option<OrderCancelTransaction>,
377    /// The IDs of all transactions created while satisfying the request.
378    #[serde(
379        rename = "relatedTransactionIDs",
380        default,
381        skip_serializing_if = "Vec::is_empty"
382    )]
383    pub related_transaction_ids: Vec<TransactionId>,
384    /// The ID of the most recent transaction created for the account.
385    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
386    pub last_transaction_id: Option<TransactionId>,
387    /// The `Location` response header: the URL of the replacing order.
388    #[serde(skip)]
389    pub location: Option<String>,
390}
391
392/// Response of [`Client::cancel_order`].
393#[derive(Debug, Clone, Serialize, Deserialize)]
394#[non_exhaustive]
395pub struct CancelOrderResponse {
396    /// The transaction that cancelled the order.
397    #[serde(
398        rename = "orderCancelTransaction",
399        skip_serializing_if = "Option::is_none"
400    )]
401    pub order_cancel_transaction: Option<OrderCancelTransaction>,
402    /// The IDs of all transactions created while satisfying the request.
403    #[serde(
404        rename = "relatedTransactionIDs",
405        default,
406        skip_serializing_if = "Vec::is_empty"
407    )]
408    pub related_transaction_ids: Vec<TransactionId>,
409    /// The ID of the most recent transaction created for the account.
410    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
411    pub last_transaction_id: Option<TransactionId>,
412}
413
414/// Typed view of the error body when [`Client::cancel_order`] fails
415/// (HTTP 404). Recover it via
416/// [`ApiErrorBody::details`](crate::ApiErrorBody::details).
417#[derive(Debug, Clone, Serialize, Deserialize)]
418#[non_exhaustive]
419pub struct CancelOrderRejectBody {
420    /// The transaction that rejected the cancellation of the order.
421    #[serde(
422        rename = "orderCancelRejectTransaction",
423        skip_serializing_if = "Option::is_none"
424    )]
425    pub order_cancel_reject_transaction: Option<OrderCancelRejectTransaction>,
426    /// The IDs of all transactions created while satisfying the request.
427    #[serde(
428        rename = "relatedTransactionIDs",
429        default,
430        skip_serializing_if = "Vec::is_empty"
431    )]
432    pub related_transaction_ids: Vec<TransactionId>,
433    /// The ID of the most recent transaction created for the account.
434    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
435    pub last_transaction_id: Option<TransactionId>,
436}
437
438/// Builder for [`Client::set_order_client_extensions`].
439#[derive(Debug)]
440pub struct SetOrderClientExtensionsRequest {
441    client: Client,
442    account_id: AccountId,
443    order: OrderSpecifier,
444    body: SetOrderClientExtensionsBody,
445}
446
447#[derive(Debug, Serialize)]
448struct SetOrderClientExtensionsBody {
449    #[serde(rename = "clientExtensions", skip_serializing_if = "Option::is_none")]
450    client_extensions: Option<ClientExtensions>,
451    #[serde(
452        rename = "tradeClientExtensions",
453        skip_serializing_if = "Option::is_none"
454    )]
455    trade_client_extensions: Option<ClientExtensions>,
456}
457
458impl SetOrderClientExtensionsRequest {
459    /// The client extensions to update for the order.
460    pub fn client_extensions(mut self, extensions: ClientExtensions) -> Self {
461        self.body.client_extensions = Some(extensions);
462        self
463    }
464
465    /// The client extensions to update for the trade created when the
466    /// order fills.
467    pub fn trade_client_extensions(mut self, extensions: ClientExtensions) -> Self {
468        self.body.trade_client_extensions = Some(extensions);
469        self
470    }
471
472    /// Performs the request.
473    pub async fn send(self) -> Result<SetOrderClientExtensionsResponse, Error> {
474        let request = self
475            .client
476            .put(&[
477                "accounts",
478                self.account_id.as_str(),
479                "orders",
480                self.order.as_str(),
481                "clientExtensions",
482            ])
483            .json(&self.body);
484        self.client.execute(request).await
485    }
486}
487
488/// Response of [`Client::set_order_client_extensions`].
489#[derive(Debug, Clone, Serialize, Deserialize)]
490#[non_exhaustive]
491pub struct SetOrderClientExtensionsResponse {
492    /// The transaction that modified the client extensions.
493    #[serde(
494        rename = "orderClientExtensionsModifyTransaction",
495        skip_serializing_if = "Option::is_none"
496    )]
497    pub order_client_extensions_modify_transaction: Option<OrderClientExtensionsModifyTransaction>,
498    /// The IDs of all transactions created while satisfying the request.
499    #[serde(
500        rename = "relatedTransactionIDs",
501        default,
502        skip_serializing_if = "Vec::is_empty"
503    )]
504    pub related_transaction_ids: Vec<TransactionId>,
505    /// The ID of the most recent transaction created for the account.
506    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
507    pub last_transaction_id: Option<TransactionId>,
508}
509
510/// Typed view of the error body when
511/// [`Client::set_order_client_extensions`] is rejected (HTTP 400/404).
512/// Recover it via [`ApiErrorBody::details`](crate::ApiErrorBody::details).
513#[derive(Debug, Clone, Serialize, Deserialize)]
514#[non_exhaustive]
515pub struct SetOrderClientExtensionsRejectBody {
516    /// The transaction that rejected the modification.
517    #[serde(
518        rename = "orderClientExtensionsModifyRejectTransaction",
519        skip_serializing_if = "Option::is_none"
520    )]
521    pub order_client_extensions_modify_reject_transaction:
522        Option<OrderClientExtensionsModifyRejectTransaction>,
523    /// The IDs of all transactions created while satisfying the request.
524    #[serde(
525        rename = "relatedTransactionIDs",
526        default,
527        skip_serializing_if = "Vec::is_empty"
528    )]
529    pub related_transaction_ids: Vec<TransactionId>,
530    /// The ID of the most recent transaction created for the account.
531    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
532    pub last_transaction_id: Option<TransactionId>,
533}