Skip to main content

oanda_client/
position.rs

1//! OANDA position endpoint contracts.
2
3use crate::client::RequestParts;
4use crate::ids::{AccountID, InstrumentName, TransactionID};
5use crate::models::{
6    ClientExtensions, MarketOrderRejectTransaction, MarketOrderTransaction, OrderCancelTransaction,
7    OrderFillTransaction, Position,
8};
9use crate::validation::Validate;
10use crate::{Client, Error, Result};
11use reqwest::Method;
12use serde::{Deserialize, Serialize};
13
14/// Successful response for `list_positions`.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct ListPositionsResponse {
18    /// The list of Account Positions.
19    #[serde(rename = "positions", default, skip_serializing_if = "Option::is_none")]
20    pub positions: Option<Vec<Position>>,
21    /// The ID of the most recent Transaction created for the Account
22    #[serde(
23        rename = "lastTransactionID",
24        default,
25        skip_serializing_if = "Option::is_none"
26    )]
27    pub last_transaction_id: Option<TransactionID>,
28}
29
30impl Client {
31    /// List positions.
32    ///
33    /// # Errors
34    ///
35    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
36    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
37    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
38    pub async fn list_positions(
39        &self,
40        account_id: &AccountID,
41    ) -> std::result::Result<
42        crate::ApiResponse<ListPositionsResponse>,
43        crate::OperationError<crate::GenericRejection>,
44    > {
45        let account_id_encoded = crate::client::segment(account_id.as_str());
46        let path = format!("/v3/accounts/{account_id_encoded}/positions");
47        self.execute(RequestParts::new(Method::GET, &path)).await
48    }
49}
50
51/// Successful response for `list_open_positions`.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct ListOpenPositionsResponse {
55    /// The list of open Positions in the Account.
56    #[serde(rename = "positions", default, skip_serializing_if = "Option::is_none")]
57    pub positions: Option<Vec<Position>>,
58    /// The ID of the most recent Transaction created for the Account
59    #[serde(
60        rename = "lastTransactionID",
61        default,
62        skip_serializing_if = "Option::is_none"
63    )]
64    pub last_transaction_id: Option<TransactionID>,
65}
66
67impl Client {
68    /// List open positions.
69    ///
70    /// # Errors
71    ///
72    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
73    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
74    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
75    pub async fn list_open_positions(
76        &self,
77        account_id: &AccountID,
78    ) -> std::result::Result<
79        crate::ApiResponse<ListOpenPositionsResponse>,
80        crate::OperationError<crate::GenericRejection>,
81    > {
82        let account_id_encoded = crate::client::segment(account_id.as_str());
83        let path = format!("/v3/accounts/{account_id_encoded}/openPositions");
84        self.execute(RequestParts::new(Method::GET, &path)).await
85    }
86}
87
88/// Successful response for `position_details`.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct PositionDetailsResponse {
92    /// The requested Position.
93    #[serde(rename = "position", default, skip_serializing_if = "Option::is_none")]
94    pub position: Option<Position>,
95    /// The ID of the most recent Transaction created for the Account
96    #[serde(
97        rename = "lastTransactionID",
98        default,
99        skip_serializing_if = "Option::is_none"
100    )]
101    pub last_transaction_id: Option<TransactionID>,
102}
103
104impl Client {
105    /// Position details.
106    ///
107    /// # Errors
108    ///
109    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
110    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
111    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
112    pub async fn position_details(
113        &self,
114        account_id: &AccountID,
115        instrument: &InstrumentName,
116    ) -> std::result::Result<
117        crate::ApiResponse<PositionDetailsResponse>,
118        crate::OperationError<crate::GenericRejection>,
119    > {
120        let account_id_encoded = crate::client::segment(account_id.as_str());
121        let instrument_encoded = crate::client::segment(instrument.as_str());
122        let path = format!("/v3/accounts/{account_id_encoded}/positions/{instrument_encoded}");
123        self.execute(RequestParts::new(Method::GET, &path)).await
124    }
125}
126
127/// Request body for `close_position`.
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct ClosePositionBody {
131    /// Indication of how much of the long Position to closeout. Either the string “ALL”, the string “NONE”, or
132    /// a `DecimalNumber` representing how many units of the long position to close using a `PositionCloseout`
133    /// `MarketOrder`. The units specified must always be positive. Omitting this field closes all units
134    /// on this side (the provider default is `ALL`); specify `NONE` to leave it open.
135    #[serde(rename = "longUnits", default, skip_serializing_if = "Option::is_none")]
136    pub long_units: Option<crate::PositionCloseoutUnits>,
137    /// The client extensions to add to the `MarketOrder` used to close the long position.
138    #[serde(
139        rename = "longClientExtensions",
140        default,
141        skip_serializing_if = "Option::is_none"
142    )]
143    pub long_client_extensions: Option<ClientExtensions>,
144    /// Indication of how much of the short Position to closeout. Either the string “ALL”, the string “NONE”, or
145    /// a `DecimalNumber` representing how many units of the short position to close using a `PositionCloseout`
146    /// `MarketOrder`. The units specified must always be positive. Omitting this field closes all units
147    /// on this side (the provider default is `ALL`); specify `NONE` to leave it open.
148    #[serde(
149        rename = "shortUnits",
150        default,
151        skip_serializing_if = "Option::is_none"
152    )]
153    pub short_units: Option<crate::PositionCloseoutUnits>,
154    /// The client extensions to add to the `MarketOrder` used to close the short position.
155    #[serde(
156        rename = "shortClientExtensions",
157        default,
158        skip_serializing_if = "Option::is_none"
159    )]
160    pub short_client_extensions: Option<ClientExtensions>,
161}
162
163impl Validate for ClosePositionBody {
164    fn validate(&self) -> Result<()> {
165        if self.long_units == Some(crate::PositionCloseoutUnits::None)
166            && self.short_units == Some(crate::PositionCloseoutUnits::None)
167        {
168            return Err(Error::InvalidInput(
169                "position close requests no action".into(),
170            ));
171        }
172        for units in [self.long_units, self.short_units].into_iter().flatten() {
173            units.validate()?;
174        }
175        Ok(())
176    }
177}
178
179/// Successful response for `close_position`.
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct ClosePositionResponse {
183    /// The `MarketOrderTransaction` created to close the long Position.
184    #[serde(
185        rename = "longOrderCreateTransaction",
186        default,
187        skip_serializing_if = "Option::is_none"
188    )]
189    pub long_order_create_transaction: Option<MarketOrderTransaction>,
190    /// `OrderFill` Transaction that closes the long Position
191    #[serde(
192        rename = "longOrderFillTransaction",
193        default,
194        skip_serializing_if = "Option::is_none"
195    )]
196    pub long_order_fill_transaction: Option<OrderFillTransaction>,
197    /// `OrderCancel` Transaction that cancels the `MarketOrder` created to close the long Position
198    #[serde(
199        rename = "longOrderCancelTransaction",
200        default,
201        skip_serializing_if = "Option::is_none"
202    )]
203    pub long_order_cancel_transaction: Option<OrderCancelTransaction>,
204    /// The `MarketOrderTransaction` created to close the short Position.
205    #[serde(
206        rename = "shortOrderCreateTransaction",
207        default,
208        skip_serializing_if = "Option::is_none"
209    )]
210    pub short_order_create_transaction: Option<MarketOrderTransaction>,
211    /// `OrderFill` Transaction that closes the short Position
212    #[serde(
213        rename = "shortOrderFillTransaction",
214        default,
215        skip_serializing_if = "Option::is_none"
216    )]
217    pub short_order_fill_transaction: Option<OrderFillTransaction>,
218    /// `OrderCancel` Transaction that cancels the `MarketOrder` created to close the short Position
219    #[serde(
220        rename = "shortOrderCancelTransaction",
221        default,
222        skip_serializing_if = "Option::is_none"
223    )]
224    pub short_order_cancel_transaction: Option<OrderCancelTransaction>,
225    /// The IDs of all Transactions that were created while satisfying the request.
226    #[serde(
227        rename = "relatedTransactionIDs",
228        default,
229        skip_serializing_if = "Option::is_none"
230    )]
231    pub related_transaction_ids: Option<Vec<TransactionID>>,
232    /// The ID of the most recent Transaction created for the Account
233    #[serde(
234        rename = "lastTransactionID",
235        default,
236        skip_serializing_if = "Option::is_none"
237    )]
238    pub last_transaction_id: Option<TransactionID>,
239}
240
241/// Documented rejection fields for `close_position`.
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub struct ClosePositionRejection {
245    /// The Transaction created that rejects the creation of a `MarketOrder` to close the long Position.
246    #[serde(
247        rename = "longOrderRejectTransaction",
248        default,
249        skip_serializing_if = "Option::is_none"
250    )]
251    pub long_order_reject_transaction: Option<MarketOrderRejectTransaction>,
252    /// The Transaction created that rejects the creation of a `MarketOrder` to close the short Position.
253    #[serde(
254        rename = "shortOrderRejectTransaction",
255        default,
256        skip_serializing_if = "Option::is_none"
257    )]
258    pub short_order_reject_transaction: Option<MarketOrderRejectTransaction>,
259    /// The IDs of all Transactions that were created while satisfying the request.
260    #[serde(
261        rename = "relatedTransactionIDs",
262        default,
263        skip_serializing_if = "Option::is_none"
264    )]
265    pub related_transaction_ids: Option<Vec<TransactionID>>,
266    /// The ID of the most recent Transaction created for the Account
267    #[serde(
268        rename = "lastTransactionID",
269        default,
270        skip_serializing_if = "Option::is_none"
271    )]
272    pub last_transaction_id: Option<TransactionID>,
273    /// The code of the error that has occurred. This field may not be returned for some errors.
274    #[serde(rename = "errorCode", default, skip_serializing_if = "Option::is_none")]
275    pub error_code: Option<String>,
276    /// The human-readable description of the error that has occurred.
277    #[serde(rename = "errorMessage")]
278    pub error_message: String,
279}
280
281impl Client {
282    /// Close position.
283    ///
284    /// # Errors
285    ///
286    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
287    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
288    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
289    /// An uncertain sent mutation fences this account until caller-acknowledged reconciliation.
290    pub async fn close_position(
291        &self,
292        account_id: &AccountID,
293        instrument: &InstrumentName,
294        body: &ClosePositionBody,
295    ) -> std::result::Result<
296        crate::ApiResponse<ClosePositionResponse>,
297        crate::OperationError<ClosePositionRejection>,
298    > {
299        let account_id_encoded = crate::client::segment(account_id.as_str());
300        let instrument_encoded = crate::client::segment(instrument.as_str());
301        let path =
302            format!("/v3/accounts/{account_id_encoded}/positions/{instrument_encoded}/close");
303        self.execute(
304            RequestParts::new(Method::PUT, &path)
305                .body(body)?
306                .mutation(crate::client::MutationAdmission::Exit(account_id)),
307        )
308        .await
309    }
310}