Skip to main content

oanda_rs/endpoints/
positions.rs

1//! Position endpoints: listing and closing positions.
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Error;
7use crate::models::transaction::{
8    MarketOrderRejectTransaction, MarketOrderTransaction, OrderCancelTransaction,
9    OrderFillTransaction,
10};
11use crate::models::{AccountId, ClientExtensions, InstrumentName, Position, TransactionId};
12
13impl Client {
14    /// List all positions for an account. The positions returned are for
15    /// every instrument that has had a position during the lifetime of the
16    /// account.
17    ///
18    /// `GET /v3/accounts/{accountID}/positions`
19    pub async fn list_positions(
20        &self,
21        account_id: impl Into<AccountId>,
22    ) -> Result<ListPositionsResponse, Error> {
23        let account_id = account_id.into();
24        self.execute(self.get(&["accounts", account_id.as_str(), "positions"]))
25            .await
26    }
27
28    /// List all open positions for an account. An open position is a
29    /// position that currently has a trade open.
30    ///
31    /// `GET /v3/accounts/{accountID}/openPositions`
32    pub async fn list_open_positions(
33        &self,
34        account_id: impl Into<AccountId>,
35    ) -> Result<ListPositionsResponse, Error> {
36        let account_id = account_id.into();
37        self.execute(self.get(&["accounts", account_id.as_str(), "openPositions"]))
38            .await
39    }
40
41    /// Get the details of a single instrument's position in an account.
42    ///
43    /// `GET /v3/accounts/{accountID}/positions/{instrument}`
44    pub async fn position(
45        &self,
46        account_id: impl Into<AccountId>,
47        instrument: impl Into<InstrumentName>,
48    ) -> Result<PositionResponse, Error> {
49        let account_id = account_id.into();
50        let instrument = instrument.into();
51        self.execute(self.get(&[
52            "accounts",
53            account_id.as_str(),
54            "positions",
55            instrument.as_str(),
56        ]))
57        .await
58    }
59
60    /// Close a position for a specific instrument.
61    ///
62    /// At least one of [`long_units`](ClosePositionRequest::long_units) /
63    /// [`short_units`](ClosePositionRequest::short_units) should be set;
64    /// OANDA defaults both to `ALL` when omitted.
65    ///
66    /// `PUT /v3/accounts/{accountID}/positions/{instrument}/close`
67    ///
68    /// # Examples
69    ///
70    /// ```no_run
71    /// # async fn run() -> Result<(), oanda_rs::Error> {
72    /// # let client = oanda_rs::Client::new(oanda_rs::Environment::Practice, "token");
73    /// // Close the whole long side of the EUR_USD position:
74    /// client
75    ///     .close_position("101-004-1234567-001", "EUR_USD")
76    ///     .long_units("ALL")
77    ///     .send()
78    ///     .await?;
79    /// # Ok(())
80    /// # }
81    /// ```
82    pub fn close_position(
83        &self,
84        account_id: impl Into<AccountId>,
85        instrument: impl Into<InstrumentName>,
86    ) -> ClosePositionRequest {
87        ClosePositionRequest {
88            client: self.clone(),
89            account_id: account_id.into(),
90            instrument: instrument.into(),
91            body: ClosePositionBody {
92                long_units: None,
93                long_client_extensions: None,
94                short_units: None,
95                short_client_extensions: None,
96            },
97        }
98    }
99}
100
101/// Response of [`Client::list_positions`] and
102/// [`Client::list_open_positions`].
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[non_exhaustive]
105pub struct ListPositionsResponse {
106    /// The list of positions satisfying the request.
107    #[serde(rename = "positions", default, skip_serializing_if = "Vec::is_empty")]
108    pub positions: Vec<Position>,
109    /// The ID of the most recent transaction created for the account.
110    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
111    pub last_transaction_id: Option<TransactionId>,
112}
113
114/// Response of [`Client::position`].
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[non_exhaustive]
117pub struct PositionResponse {
118    /// The requested position.
119    #[serde(rename = "position")]
120    pub position: Position,
121    /// The ID of the most recent transaction created for the account.
122    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
123    pub last_transaction_id: Option<TransactionId>,
124}
125
126/// Builder for [`Client::close_position`].
127#[derive(Debug)]
128pub struct ClosePositionRequest {
129    client: Client,
130    account_id: AccountId,
131    instrument: InstrumentName,
132    body: ClosePositionBody,
133}
134
135#[derive(Debug, Serialize)]
136struct ClosePositionBody {
137    #[serde(rename = "longUnits", skip_serializing_if = "Option::is_none")]
138    long_units: Option<String>,
139    #[serde(
140        rename = "longClientExtensions",
141        skip_serializing_if = "Option::is_none"
142    )]
143    long_client_extensions: Option<ClientExtensions>,
144    #[serde(rename = "shortUnits", skip_serializing_if = "Option::is_none")]
145    short_units: Option<String>,
146    #[serde(
147        rename = "shortClientExtensions",
148        skip_serializing_if = "Option::is_none"
149    )]
150    short_client_extensions: Option<ClientExtensions>,
151}
152
153impl ClosePositionRequest {
154    /// How much of the long side to close: `"ALL"`, `"NONE"`, or a number
155    /// of units as a decimal string.
156    pub fn long_units(mut self, units: impl Into<String>) -> Self {
157        self.body.long_units = Some(units.into());
158        self
159    }
160
161    /// Client extensions to add to the market order used to close the long
162    /// side.
163    pub fn long_client_extensions(mut self, extensions: ClientExtensions) -> Self {
164        self.body.long_client_extensions = Some(extensions);
165        self
166    }
167
168    /// How much of the short side to close: `"ALL"`, `"NONE"`, or a number
169    /// of units as a decimal string.
170    pub fn short_units(mut self, units: impl Into<String>) -> Self {
171        self.body.short_units = Some(units.into());
172        self
173    }
174
175    /// Client extensions to add to the market order used to close the
176    /// short side.
177    pub fn short_client_extensions(mut self, extensions: ClientExtensions) -> Self {
178        self.body.short_client_extensions = Some(extensions);
179        self
180    }
181
182    /// Performs the request.
183    pub async fn send(self) -> Result<ClosePositionResponse, Error> {
184        let request = self
185            .client
186            .put(&[
187                "accounts",
188                self.account_id.as_str(),
189                "positions",
190                self.instrument.as_str(),
191                "close",
192            ])
193            .json(&self.body);
194        self.client.execute(request).await
195    }
196}
197
198/// Response of [`Client::close_position`].
199#[derive(Debug, Clone, Serialize, Deserialize)]
200#[non_exhaustive]
201pub struct ClosePositionResponse {
202    /// The market order created to close the long side.
203    #[serde(
204        rename = "longOrderCreateTransaction",
205        skip_serializing_if = "Option::is_none"
206    )]
207    pub long_order_create_transaction: Option<MarketOrderTransaction>,
208    /// The fill of the long-side close order.
209    #[serde(
210        rename = "longOrderFillTransaction",
211        skip_serializing_if = "Option::is_none"
212    )]
213    pub long_order_fill_transaction: Option<OrderFillTransaction>,
214    /// The cancellation of the long-side close order.
215    #[serde(
216        rename = "longOrderCancelTransaction",
217        skip_serializing_if = "Option::is_none"
218    )]
219    pub long_order_cancel_transaction: Option<OrderCancelTransaction>,
220    /// The market order created to close the short side.
221    #[serde(
222        rename = "shortOrderCreateTransaction",
223        skip_serializing_if = "Option::is_none"
224    )]
225    pub short_order_create_transaction: Option<MarketOrderTransaction>,
226    /// The fill of the short-side close order.
227    #[serde(
228        rename = "shortOrderFillTransaction",
229        skip_serializing_if = "Option::is_none"
230    )]
231    pub short_order_fill_transaction: Option<OrderFillTransaction>,
232    /// The cancellation of the short-side close order.
233    #[serde(
234        rename = "shortOrderCancelTransaction",
235        skip_serializing_if = "Option::is_none"
236    )]
237    pub short_order_cancel_transaction: Option<OrderCancelTransaction>,
238    /// The IDs of all transactions created while satisfying the request.
239    #[serde(
240        rename = "relatedTransactionIDs",
241        default,
242        skip_serializing_if = "Vec::is_empty"
243    )]
244    pub related_transaction_ids: Vec<TransactionId>,
245    /// The ID of the most recent transaction created for the account.
246    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
247    pub last_transaction_id: Option<TransactionId>,
248}
249
250/// Typed view of the error body when [`Client::close_position`] is
251/// rejected (HTTP 400/404). Recover it via
252/// [`ApiErrorBody::details`](crate::ApiErrorBody::details).
253#[derive(Debug, Clone, Serialize, Deserialize)]
254#[non_exhaustive]
255pub struct ClosePositionRejectBody {
256    /// The rejection of the market order created to close the long side.
257    #[serde(
258        rename = "longOrderRejectTransaction",
259        skip_serializing_if = "Option::is_none"
260    )]
261    pub long_order_reject_transaction: Option<MarketOrderRejectTransaction>,
262    /// The rejection of the market order created to close the short side.
263    #[serde(
264        rename = "shortOrderRejectTransaction",
265        skip_serializing_if = "Option::is_none"
266    )]
267    pub short_order_reject_transaction: Option<MarketOrderRejectTransaction>,
268    /// The IDs of all transactions created while satisfying the request.
269    #[serde(
270        rename = "relatedTransactionIDs",
271        default,
272        skip_serializing_if = "Vec::is_empty"
273    )]
274    pub related_transaction_ids: Vec<TransactionId>,
275    /// The ID of the most recent transaction created for the account.
276    #[serde(rename = "lastTransactionID", skip_serializing_if = "Option::is_none")]
277    pub last_transaction_id: Option<TransactionId>,
278}