Skip to main content

oanda_rs/endpoints/
trades.rs

1//! Trade endpoints: listing, closing, and modifying open trades.
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Error;
7use crate::models::transaction::{
8    MarketOrderRejectTransaction, MarketOrderTransaction, OrderCancelRejectTransaction,
9    OrderCancelTransaction, OrderFillTransaction, StopLossOrderRejectTransaction,
10    StopLossOrderTransaction, TakeProfitOrderRejectTransaction, TakeProfitOrderTransaction,
11    TradeClientExtensionsModifyRejectTransaction, TradeClientExtensionsModifyTransaction,
12    TrailingStopLossOrderRejectTransaction, TrailingStopLossOrderTransaction,
13};
14use crate::models::{
15    AccountId, ClientExtensions, InstrumentName, StopLossDetails, TakeProfitDetails, Trade,
16    TradeId, TradeSpecifier, TradeStateFilter, TrailingStopLossDetails, TransactionId,
17};
18
19impl Client {
20    /// Get a list of trades for an account.
21    ///
22    /// `GET /v3/accounts/{accountID}/trades`
23    pub fn list_trades(&self, account_id: impl Into<AccountId>) -> ListTradesRequest {
24        ListTradesRequest {
25            client: self.clone(),
26            account_id: account_id.into(),
27            params: Vec::new(),
28        }
29    }
30
31    /// Get the list of open trades for an account.
32    ///
33    /// `GET /v3/accounts/{accountID}/openTrades`
34    pub async fn list_open_trades(
35        &self,
36        account_id: impl Into<AccountId>,
37    ) -> Result<ListTradesResponse, Error> {
38        let account_id = account_id.into();
39        self.execute(self.get(&["accounts", account_id.as_str(), "openTrades"]))
40            .await
41    }
42
43    /// Get the details of a specific trade in an account.
44    ///
45    /// `GET /v3/accounts/{accountID}/trades/{tradeSpecifier}`
46    pub async fn trade(
47        &self,
48        account_id: impl Into<AccountId>,
49        trade: impl Into<TradeSpecifier>,
50    ) -> Result<TradeResponse, Error> {
51        let account_id = account_id.into();
52        let trade = trade.into();
53        self.execute(self.get(&["accounts", account_id.as_str(), "trades", trade.as_str()]))
54            .await
55    }
56
57    /// Close (partially or fully) a specific open trade in an account.
58    ///
59    /// `PUT /v3/accounts/{accountID}/trades/{tradeSpecifier}/close`
60    ///
61    /// # Examples
62    ///
63    /// ```no_run
64    /// # async fn run() -> Result<(), oanda_rs::Error> {
65    /// # let client = oanda_rs::Client::new(oanda_rs::Environment::Practice, "token");
66    /// // Close a trade fully (the default) ...
67    /// client.close_trade("101-004-1234567-001", "6543").send().await?;
68    /// // ... or partially:
69    /// client
70    ///     .close_trade("101-004-1234567-001", "6543")
71    ///     .units("50")
72    ///     .send()
73    ///     .await?;
74    /// # Ok(())
75    /// # }
76    /// ```
77    pub fn close_trade(
78        &self,
79        account_id: impl Into<AccountId>,
80        trade: impl Into<TradeSpecifier>,
81    ) -> CloseTradeRequest {
82        CloseTradeRequest {
83            client: self.clone(),
84            account_id: account_id.into(),
85            trade: trade.into(),
86            units: None,
87        }
88    }
89
90    /// Update the client extensions for a trade.
91    ///
92    /// **Do not add, update, or delete the client extensions if your
93    /// account is associated with MT4.**
94    ///
95    /// `PUT /v3/accounts/{accountID}/trades/{tradeSpecifier}/clientExtensions`
96    pub async fn set_trade_client_extensions(
97        &self,
98        account_id: impl Into<AccountId>,
99        trade: impl Into<TradeSpecifier>,
100        extensions: ClientExtensions,
101    ) -> Result<SetTradeClientExtensionsResponse, Error> {
102        let account_id = account_id.into();
103        let trade = trade.into();
104        let request = self
105            .put(&[
106                "accounts",
107                account_id.as_str(),
108                "trades",
109                trade.as_str(),
110                "clientExtensions",
111            ])
112            .json(&SetTradeClientExtensionsBody {
113                client_extensions: extensions,
114            });
115        self.execute(request).await
116    }
117
118    /// Create, replace and cancel the dependent orders (take-profit,
119    /// stop-loss and trailing stop-loss) of a trade.
120    ///
121    /// Setting a detail replaces the existing dependent order (or creates
122    /// one); setting it to "cancel" removes it. Details not set are left
123    /// unmodified.
124    ///
125    /// `PUT /v3/accounts/{accountID}/trades/{tradeSpecifier}/orders`
126    pub fn set_trade_dependent_orders(
127        &self,
128        account_id: impl Into<AccountId>,
129        trade: impl Into<TradeSpecifier>,
130    ) -> SetTradeDependentOrdersRequest {
131        SetTradeDependentOrdersRequest {
132            client: self.clone(),
133            account_id: account_id.into(),
134            trade: trade.into(),
135            body: SetTradeDependentOrdersBody {
136                take_profit: None,
137                stop_loss: None,
138                trailing_stop_loss: None,
139            },
140        }
141    }
142}
143
144/// Builder for [`Client::list_trades`].
145#[derive(Debug)]
146pub struct ListTradesRequest {
147    client: Client,
148    account_id: AccountId,
149    params: Vec<(&'static str, String)>,
150}
151
152impl ListTradesRequest {
153    /// Restricts the response to the given trade IDs.
154    pub fn ids<I>(mut self, ids: I) -> Self
155    where
156        I: IntoIterator,
157        I::Item: Into<TradeId>,
158    {
159        let joined = ids
160            .into_iter()
161            .map(|id| id.into().0)
162            .collect::<Vec<_>>()
163            .join(",");
164        self.params.push(("ids", joined));
165        self
166    }
167
168    /// The state to filter the requested trades by (default `OPEN`).
169    pub fn state(mut self, state: TradeStateFilter) -> Self {
170        self.params.push(("state", state.to_string()));
171        self
172    }
173
174    /// The instrument to filter the requested trades by.
175    pub fn instrument(mut self, instrument: impl Into<InstrumentName>) -> Self {
176        self.params
177            .push(("instrument", instrument.into().as_str().to_owned()));
178        self
179    }
180
181    /// The maximum number of trades to return (default 50, maximum 500).
182    pub fn count(mut self, count: u32) -> Self {
183        self.params.push(("count", count.to_string()));
184        self
185    }
186
187    /// The maximum trade ID to return: only trades with IDs at or below
188    /// this are returned.
189    pub fn before_id(mut self, before_id: impl Into<TradeId>) -> Self {
190        self.params.push(("beforeID", before_id.into().0));
191        self
192    }
193
194    /// Performs the request.
195    pub async fn send(self) -> Result<ListTradesResponse, Error> {
196        let request = self
197            .client
198            .get(&["accounts", self.account_id.as_str(), "trades"])
199            .query(&self.params);
200        self.client.execute(request).await
201    }
202}
203
204/// Response of [`Client::list_trades`] and [`Client::list_open_trades`].
205#[derive(Debug, Clone, Serialize, Deserialize)]
206#[non_exhaustive]
207pub struct ListTradesResponse {
208    /// The list of trades satisfying the request.
209    #[serde(rename = "trades", default, skip_serializing_if = "Vec::is_empty")]
210    pub trades: Vec<Trade>,
211    /// The ID of the most recent transaction created for the account.
212    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
213    pub last_transaction_id: Option<TransactionId>,
214}
215
216/// Response of [`Client::trade`].
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[non_exhaustive]
219pub struct TradeResponse {
220    /// The details of the requested trade.
221    #[serde(rename = "trade")]
222    pub trade: Trade,
223    /// The ID of the most recent transaction created for the account.
224    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
225    pub last_transaction_id: Option<TransactionId>,
226}
227
228/// Builder for [`Client::close_trade`].
229#[derive(Debug)]
230pub struct CloseTradeRequest {
231    client: Client,
232    account_id: AccountId,
233    trade: TradeSpecifier,
234    units: Option<String>,
235}
236
237#[derive(Debug, Serialize)]
238struct CloseTradeBody {
239    #[serde(skip_serializing_if = "Option::is_none")]
240    units: Option<String>,
241}
242
243impl CloseTradeRequest {
244    /// How much of the trade to close: either `"ALL"` (the default) or a
245    /// number of units as a decimal string (e.g. `"50"`).
246    pub fn units(mut self, units: impl Into<String>) -> Self {
247        self.units = Some(units.into());
248        self
249    }
250
251    /// Performs the request.
252    pub async fn send(self) -> Result<CloseTradeResponse, Error> {
253        let request = self
254            .client
255            .put(&[
256                "accounts",
257                self.account_id.as_str(),
258                "trades",
259                self.trade.as_str(),
260                "close",
261            ])
262            .json(&CloseTradeBody { units: self.units });
263        self.client.execute(request).await
264    }
265}
266
267/// Response of [`Client::close_trade`].
268#[derive(Debug, Clone, Serialize, Deserialize)]
269#[non_exhaustive]
270pub struct CloseTradeResponse {
271    /// The market order created to close the trade.
272    #[serde(
273        rename = "orderCreateTransaction",
274        skip_serializing_if = "Option::is_none"
275    )]
276    pub order_create_transaction: Option<MarketOrderTransaction>,
277    /// The fill of the close-trade market order.
278    #[serde(
279        rename = "orderFillTransaction",
280        skip_serializing_if = "Option::is_none"
281    )]
282    pub order_fill_transaction: Option<OrderFillTransaction>,
283    /// The cancellation of the close-trade market order (when it could not
284    /// be filled).
285    #[serde(
286        rename = "orderCancelTransaction",
287        skip_serializing_if = "Option::is_none"
288    )]
289    pub order_cancel_transaction: Option<OrderCancelTransaction>,
290    /// The IDs of all transactions created while satisfying the request.
291    #[serde(
292        rename = "relatedTransactionIDs",
293        default,
294        skip_serializing_if = "Vec::is_empty"
295    )]
296    pub related_transaction_ids: Vec<TransactionId>,
297    /// The ID of the most recent transaction created for the account.
298    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
299    pub last_transaction_id: Option<TransactionId>,
300}
301
302/// Typed view of the error body when [`Client::close_trade`] is rejected
303/// (HTTP 400/404). Recover it via
304/// [`ApiErrorBody::details`](crate::ApiErrorBody::details).
305#[derive(Debug, Clone, Serialize, Deserialize)]
306#[non_exhaustive]
307pub struct CloseTradeRejectBody {
308    /// The market order reject transaction.
309    #[serde(
310        rename = "orderRejectTransaction",
311        skip_serializing_if = "Option::is_none"
312    )]
313    pub order_reject_transaction: Option<MarketOrderRejectTransaction>,
314    /// The IDs of all transactions created while satisfying the request.
315    #[serde(
316        rename = "relatedTransactionIDs",
317        default,
318        skip_serializing_if = "Vec::is_empty"
319    )]
320    pub related_transaction_ids: Vec<TransactionId>,
321    /// The ID of the most recent transaction created for the account.
322    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
323    pub last_transaction_id: Option<TransactionId>,
324}
325
326#[derive(Debug, Serialize)]
327struct SetTradeClientExtensionsBody {
328    #[serde(rename = "clientExtensions")]
329    client_extensions: ClientExtensions,
330}
331
332/// Response of [`Client::set_trade_client_extensions`].
333#[derive(Debug, Clone, Serialize, Deserialize)]
334#[non_exhaustive]
335pub struct SetTradeClientExtensionsResponse {
336    /// The transaction that updated the trade's client extensions.
337    #[serde(
338        rename = "tradeClientExtensionsModifyTransaction",
339        skip_serializing_if = "Option::is_none"
340    )]
341    pub trade_client_extensions_modify_transaction: Option<TradeClientExtensionsModifyTransaction>,
342    /// The IDs of all transactions created while satisfying the request.
343    #[serde(
344        rename = "relatedTransactionIDs",
345        default,
346        skip_serializing_if = "Vec::is_empty"
347    )]
348    pub related_transaction_ids: Vec<TransactionId>,
349    /// The ID of the most recent transaction created for the account.
350    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
351    pub last_transaction_id: Option<TransactionId>,
352}
353
354/// Typed view of the error body when
355/// [`Client::set_trade_client_extensions`] is rejected (HTTP 400/404).
356/// Recover it via [`ApiErrorBody::details`](crate::ApiErrorBody::details).
357#[derive(Debug, Clone, Serialize, Deserialize)]
358#[non_exhaustive]
359pub struct SetTradeClientExtensionsRejectBody {
360    /// The transaction that rejected the modification.
361    #[serde(
362        rename = "tradeClientExtensionsModifyRejectTransaction",
363        skip_serializing_if = "Option::is_none"
364    )]
365    pub trade_client_extensions_modify_reject_transaction:
366        Option<TradeClientExtensionsModifyRejectTransaction>,
367    /// The IDs of all transactions created while satisfying the request.
368    #[serde(
369        rename = "relatedTransactionIDs",
370        default,
371        skip_serializing_if = "Vec::is_empty"
372    )]
373    pub related_transaction_ids: Vec<TransactionId>,
374    /// The ID of the most recent transaction created for the account.
375    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
376    pub last_transaction_id: Option<TransactionId>,
377}
378
379/// Builder for [`Client::set_trade_dependent_orders`].
380#[derive(Debug)]
381pub struct SetTradeDependentOrdersRequest {
382    client: Client,
383    account_id: AccountId,
384    trade: TradeSpecifier,
385    body: SetTradeDependentOrdersBody,
386}
387
388#[derive(Debug, Serialize)]
389struct SetTradeDependentOrdersBody {
390    #[serde(rename = "takeProfit", skip_serializing_if = "Option::is_none")]
391    take_profit: Option<Option<TakeProfitDetails>>,
392    #[serde(rename = "stopLoss", skip_serializing_if = "Option::is_none")]
393    stop_loss: Option<Option<StopLossDetails>>,
394    #[serde(rename = "trailingStopLoss", skip_serializing_if = "Option::is_none")]
395    trailing_stop_loss: Option<Option<TrailingStopLossDetails>>,
396}
397
398impl SetTradeDependentOrdersRequest {
399    /// Creates or replaces the trade's take-profit order.
400    pub fn take_profit(mut self, details: TakeProfitDetails) -> Self {
401        self.body.take_profit = Some(Some(details));
402        self
403    }
404
405    /// Cancels the trade's take-profit order (sends `null`).
406    pub fn cancel_take_profit(mut self) -> Self {
407        self.body.take_profit = Some(None);
408        self
409    }
410
411    /// Creates or replaces the trade's stop-loss order.
412    pub fn stop_loss(mut self, details: StopLossDetails) -> Self {
413        self.body.stop_loss = Some(Some(details));
414        self
415    }
416
417    /// Cancels the trade's stop-loss order (sends `null`).
418    pub fn cancel_stop_loss(mut self) -> Self {
419        self.body.stop_loss = Some(None);
420        self
421    }
422
423    /// Creates or replaces the trade's trailing stop-loss order.
424    pub fn trailing_stop_loss(mut self, details: TrailingStopLossDetails) -> Self {
425        self.body.trailing_stop_loss = Some(Some(details));
426        self
427    }
428
429    /// Cancels the trade's trailing stop-loss order (sends `null`).
430    pub fn cancel_trailing_stop_loss(mut self) -> Self {
431        self.body.trailing_stop_loss = Some(None);
432        self
433    }
434
435    /// Performs the request.
436    pub async fn send(self) -> Result<SetTradeDependentOrdersResponse, Error> {
437        let request = self
438            .client
439            .put(&[
440                "accounts",
441                self.account_id.as_str(),
442                "trades",
443                self.trade.as_str(),
444                "orders",
445            ])
446            .json(&self.body);
447        self.client.execute(request).await
448    }
449}
450
451/// Response of [`Client::set_trade_dependent_orders`].
452#[derive(Debug, Clone, Serialize, Deserialize)]
453#[non_exhaustive]
454pub struct SetTradeDependentOrdersResponse {
455    /// The transaction cancelling the trade's previous take-profit order.
456    #[serde(
457        rename = "takeProfitOrderCancelTransaction",
458        skip_serializing_if = "Option::is_none"
459    )]
460    pub take_profit_order_cancel_transaction: Option<OrderCancelTransaction>,
461    /// The transaction creating the trade's new take-profit order.
462    #[serde(
463        rename = "takeProfitOrderTransaction",
464        skip_serializing_if = "Option::is_none"
465    )]
466    pub take_profit_order_transaction: Option<TakeProfitOrderTransaction>,
467    /// The fill of the new take-profit order (only when it was immediately
468    /// filled).
469    #[serde(
470        rename = "takeProfitOrderFillTransaction",
471        skip_serializing_if = "Option::is_none"
472    )]
473    pub take_profit_order_fill_transaction: Option<OrderFillTransaction>,
474    /// The cancellation of the new take-profit order (only when it was
475    /// immediately cancelled).
476    #[serde(
477        rename = "takeProfitOrderCreatedCancelTransaction",
478        skip_serializing_if = "Option::is_none"
479    )]
480    pub take_profit_order_created_cancel_transaction: Option<OrderCancelTransaction>,
481    /// The transaction cancelling the trade's previous stop-loss order.
482    #[serde(
483        rename = "stopLossOrderCancelTransaction",
484        skip_serializing_if = "Option::is_none"
485    )]
486    pub stop_loss_order_cancel_transaction: Option<OrderCancelTransaction>,
487    /// The transaction creating the trade's new stop-loss order.
488    #[serde(
489        rename = "stopLossOrderTransaction",
490        skip_serializing_if = "Option::is_none"
491    )]
492    pub stop_loss_order_transaction: Option<StopLossOrderTransaction>,
493    /// The fill of the new stop-loss order (only when it was immediately
494    /// filled).
495    #[serde(
496        rename = "stopLossOrderFillTransaction",
497        skip_serializing_if = "Option::is_none"
498    )]
499    pub stop_loss_order_fill_transaction: Option<OrderFillTransaction>,
500    /// The cancellation of the new stop-loss order (only when it was
501    /// immediately cancelled).
502    #[serde(
503        rename = "stopLossOrderCreatedCancelTransaction",
504        skip_serializing_if = "Option::is_none"
505    )]
506    pub stop_loss_order_created_cancel_transaction: Option<OrderCancelTransaction>,
507    /// The transaction cancelling the trade's previous trailing stop-loss
508    /// order.
509    #[serde(
510        rename = "trailingStopLossOrderCancelTransaction",
511        skip_serializing_if = "Option::is_none"
512    )]
513    pub trailing_stop_loss_order_cancel_transaction: Option<OrderCancelTransaction>,
514    /// The transaction creating the trade's new trailing stop-loss order.
515    #[serde(
516        rename = "trailingStopLossOrderTransaction",
517        skip_serializing_if = "Option::is_none"
518    )]
519    pub trailing_stop_loss_order_transaction: Option<TrailingStopLossOrderTransaction>,
520    /// The IDs of all transactions created while satisfying the request.
521    #[serde(
522        rename = "relatedTransactionIDs",
523        default,
524        skip_serializing_if = "Vec::is_empty"
525    )]
526    pub related_transaction_ids: Vec<TransactionId>,
527    /// The ID of the most recent transaction created for the account.
528    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
529    pub last_transaction_id: Option<TransactionId>,
530}
531
532/// Typed view of the error body when
533/// [`Client::set_trade_dependent_orders`] is rejected (HTTP 400). Recover
534/// it via [`ApiErrorBody::details`](crate::ApiErrorBody::details).
535#[derive(Debug, Clone, Serialize, Deserialize)]
536#[non_exhaustive]
537pub struct SetTradeDependentOrdersRejectBody {
538    /// The rejection of the cancellation of the trade's take-profit order.
539    #[serde(
540        rename = "takeProfitOrderCancelRejectTransaction",
541        skip_serializing_if = "Option::is_none"
542    )]
543    pub take_profit_order_cancel_reject_transaction: Option<OrderCancelRejectTransaction>,
544    /// The rejection of the creation of a new take-profit order.
545    #[serde(
546        rename = "takeProfitOrderRejectTransaction",
547        skip_serializing_if = "Option::is_none"
548    )]
549    pub take_profit_order_reject_transaction: Option<TakeProfitOrderRejectTransaction>,
550    /// The rejection of the cancellation of the trade's stop-loss order.
551    #[serde(
552        rename = "stopLossOrderCancelRejectTransaction",
553        skip_serializing_if = "Option::is_none"
554    )]
555    pub stop_loss_order_cancel_reject_transaction: Option<OrderCancelRejectTransaction>,
556    /// The rejection of the creation of a new stop-loss order.
557    #[serde(
558        rename = "stopLossOrderRejectTransaction",
559        skip_serializing_if = "Option::is_none"
560    )]
561    pub stop_loss_order_reject_transaction: Option<StopLossOrderRejectTransaction>,
562    /// The rejection of the cancellation of the trade's trailing stop-loss
563    /// order.
564    #[serde(
565        rename = "trailingStopLossOrderCancelRejectTransaction",
566        skip_serializing_if = "Option::is_none"
567    )]
568    pub trailing_stop_loss_order_cancel_reject_transaction: Option<OrderCancelRejectTransaction>,
569    /// The rejection of the creation of a new trailing stop-loss order.
570    #[serde(
571        rename = "trailingStopLossOrderRejectTransaction",
572        skip_serializing_if = "Option::is_none"
573    )]
574    pub trailing_stop_loss_order_reject_transaction: Option<TrailingStopLossOrderRejectTransaction>,
575    /// The ID of the most recent transaction created for the account.
576    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
577    pub last_transaction_id: Option<TransactionId>,
578}