Skip to main content

oanda_client/
account.rs

1//! OANDA account endpoint contracts.
2
3use crate::client::RequestParts;
4use crate::ids::{AccountID, InstrumentName, TransactionID};
5use crate::models::{
6    Account, AccountChanges, AccountChangesState, AccountProperties, AccountSummary,
7    ClientConfigureRejectTransaction, ClientConfigureTransaction, Instrument,
8};
9use crate::validation::Validate;
10use crate::{Client, Error, Result};
11use reqwest::Method;
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14
15/// Successful response for `list_accounts`.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct ListAccountsResponse {
19    /// The list of Accounts the client is authorized to access and their associated properties.
20    #[serde(rename = "accounts", default, skip_serializing_if = "Option::is_none")]
21    pub accounts: Option<Vec<AccountProperties>>,
22}
23
24impl Client {
25    /// List accounts.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
30    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
31    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
32    pub async fn list_accounts(
33        &self,
34    ) -> std::result::Result<
35        crate::ApiResponse<ListAccountsResponse>,
36        crate::OperationError<crate::GenericRejection>,
37    > {
38        let path = "/v3/accounts".to_owned();
39        self.execute(RequestParts::new(Method::GET, &path)).await
40    }
41}
42
43/// Successful response for `account_details`.
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct AccountDetailsResponse {
47    /// The full details of the requested Account.
48    #[serde(rename = "account", default, skip_serializing_if = "Option::is_none")]
49    pub account: Option<Account>,
50    /// The ID of the most recent Transaction created for the Account.
51    #[serde(
52        rename = "lastTransactionID",
53        default,
54        skip_serializing_if = "Option::is_none"
55    )]
56    pub last_transaction_id: Option<TransactionID>,
57}
58
59impl Client {
60    /// Account details.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
65    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
66    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
67    pub async fn account_details(
68        &self,
69        account_id: &AccountID,
70    ) -> std::result::Result<
71        crate::ApiResponse<AccountDetailsResponse>,
72        crate::OperationError<crate::GenericRejection>,
73    > {
74        let account_id_encoded = crate::client::segment(account_id.as_str());
75        let path = format!("/v3/accounts/{account_id_encoded}");
76        self.execute(RequestParts::new(Method::GET, &path)).await
77    }
78}
79
80/// Successful response for `account_summary`.
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct AccountSummaryResponse {
84    /// The summary of the requested Account.
85    #[serde(rename = "account", default, skip_serializing_if = "Option::is_none")]
86    pub account: Option<AccountSummary>,
87    /// The ID of the most recent Transaction created for the Account.
88    #[serde(
89        rename = "lastTransactionID",
90        default,
91        skip_serializing_if = "Option::is_none"
92    )]
93    pub last_transaction_id: Option<TransactionID>,
94}
95
96impl Client {
97    /// Account summary.
98    ///
99    /// # Errors
100    ///
101    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
102    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
103    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
104    pub async fn account_summary(
105        &self,
106        account_id: &AccountID,
107    ) -> std::result::Result<
108        crate::ApiResponse<AccountSummaryResponse>,
109        crate::OperationError<crate::GenericRejection>,
110    > {
111        let account_id_encoded = crate::client::segment(account_id.as_str());
112        let path = format!("/v3/accounts/{account_id_encoded}/summary");
113        self.execute(RequestParts::new(Method::GET, &path)).await
114    }
115}
116
117/// Query parameters for `account_instruments`.
118///
119/// The default selects all account instruments.
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
121#[serde(rename_all = "camelCase")]
122pub struct AccountInstrumentsQuery {
123    /// List of instruments to query specifically.
124    #[serde(
125        rename = "instruments",
126        default,
127        skip_serializing_if = "Option::is_none"
128    )]
129    pub instruments: Option<Vec<InstrumentName>>,
130}
131
132impl Validate for AccountInstrumentsQuery {
133    fn validate(&self) -> Result<()> {
134        if let Some(instruments) = &self.instruments {
135            crate::validation::nonempty(instruments, "instruments")?;
136        }
137        Ok(())
138    }
139}
140
141/// Successful response for `account_instruments`.
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct AccountInstrumentsResponse {
145    /// The requested list of instruments.
146    #[serde(
147        rename = "instruments",
148        default,
149        skip_serializing_if = "Option::is_none"
150    )]
151    pub instruments: Option<Vec<Instrument>>,
152    /// The ID of the most recent Transaction created for the Account.
153    #[serde(
154        rename = "lastTransactionID",
155        default,
156        skip_serializing_if = "Option::is_none"
157    )]
158    pub last_transaction_id: Option<TransactionID>,
159}
160
161impl Client {
162    /// Account instruments.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
167    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
168    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
169    pub async fn account_instruments(
170        &self,
171        account_id: &AccountID,
172        query: Option<&AccountInstrumentsQuery>,
173    ) -> std::result::Result<
174        crate::ApiResponse<AccountInstrumentsResponse>,
175        crate::OperationError<crate::GenericRejection>,
176    > {
177        let account_id_encoded = crate::client::segment(account_id.as_str());
178        let path = format!("/v3/accounts/{account_id_encoded}/instruments");
179        let request = RequestParts::new(Method::GET, &path);
180        let request = match query {
181            Some(query) => request.query(query)?,
182            None => request,
183        };
184        self.execute(request).await
185    }
186}
187
188/// Request body for `configure_account`.
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct ConfigureAccountBody {
192    /// Client-defined alias (name) for the Account
193    #[serde(rename = "alias", default, skip_serializing_if = "Option::is_none")]
194    pub alias: Option<String>,
195    /// The string representation of a decimal number.
196    #[serde(
197        rename = "marginRate",
198        default,
199        skip_serializing_if = "Option::is_none"
200    )]
201    #[serde(deserialize_with = "crate::decimal_wire::optional_number_or_string")]
202    pub margin_rate: Option<Decimal>,
203}
204
205impl Validate for ConfigureAccountBody {
206    fn validate(&self) -> Result<()> {
207        if self.alias.is_none() && self.margin_rate.is_none() {
208            return Err(Error::InvalidInput(
209                "mutation body must not be empty".into(),
210            ));
211        }
212        if self
213            .margin_rate
214            .is_some_and(|rate| rate <= Decimal::ZERO || rate > Decimal::ONE)
215        {
216            return Err(Error::InvalidInput("marginRate must be in (0,1]".into()));
217        }
218        Ok(())
219    }
220}
221
222/// Successful response for `configure_account`.
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct ConfigureAccountResponse {
226    /// The transaction that configures the Account.
227    #[serde(
228        rename = "clientConfigureTransaction",
229        default,
230        skip_serializing_if = "Option::is_none"
231    )]
232    pub client_configure_transaction: Option<ClientConfigureTransaction>,
233    /// The ID of the last Transaction created for the Account.
234    #[serde(
235        rename = "lastTransactionID",
236        default,
237        skip_serializing_if = "Option::is_none"
238    )]
239    pub last_transaction_id: Option<TransactionID>,
240}
241
242/// Documented rejection fields for `configure_account`.
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct ConfigureAccountRejection {
246    /// The transaction that rejects the configuration of the Account.
247    #[serde(
248        rename = "clientConfigureRejectTransaction",
249        default,
250        skip_serializing_if = "Option::is_none"
251    )]
252    pub client_configure_reject_transaction: Option<ClientConfigureRejectTransaction>,
253    /// The ID of the last Transaction created for the Account.
254    #[serde(
255        rename = "lastTransactionID",
256        default,
257        skip_serializing_if = "Option::is_none"
258    )]
259    pub last_transaction_id: Option<TransactionID>,
260    /// The code of the error that has occurred. This field may not be returned for some errors.
261    #[serde(rename = "errorCode", default, skip_serializing_if = "Option::is_none")]
262    pub error_code: Option<String>,
263    /// The human-readable description of the error that has occurred.
264    #[serde(rename = "errorMessage")]
265    pub error_message: String,
266}
267
268impl Client {
269    /// Configure account.
270    ///
271    /// # Errors
272    ///
273    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
274    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
275    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
276    /// An uncertain sent mutation fences this account until caller-acknowledged reconciliation.
277    pub async fn configure_account(
278        &self,
279        account_id: &AccountID,
280        body: &ConfigureAccountBody,
281    ) -> std::result::Result<
282        crate::ApiResponse<ConfigureAccountResponse>,
283        crate::OperationError<ConfigureAccountRejection>,
284    > {
285        let account_id_encoded = crate::client::segment(account_id.as_str());
286        let path = format!("/v3/accounts/{account_id_encoded}/configuration");
287        self.execute(
288            RequestParts::new(Method::PATCH, &path)
289                .body(body)?
290                .mutation(crate::client::MutationAdmission::Ordinary(account_id)),
291        )
292        .await
293    }
294}
295
296/// Query parameters for `account_changes`.
297///
298/// The default leaves the transaction cursor to the provider.
299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
300#[serde(rename_all = "camelCase")]
301pub struct AccountChangesQuery {
302    /// ID of the Transaction to get Account changes since.
303    #[serde(
304        rename = "sinceTransactionID",
305        default,
306        skip_serializing_if = "Option::is_none"
307    )]
308    pub since_transaction_id: Option<TransactionID>,
309}
310
311impl Validate for AccountChangesQuery {
312    fn validate(&self) -> Result<()> {
313        Ok(())
314    }
315}
316
317/// Successful response for `account_changes`.
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct AccountChangesResponse {
321    /// The changes to the Account’s Orders, Trades and Positions since the specified Transaction ID. Only
322    /// provided if the sinceTransactionID is supplied to the poll request.
323    #[serde(rename = "changes", default, skip_serializing_if = "Option::is_none")]
324    pub changes: Option<AccountChanges>,
325    /// The Account’s current price-dependent state.
326    #[serde(rename = "state", default, skip_serializing_if = "Option::is_none")]
327    pub state: Option<AccountChangesState>,
328    /// The ID of the last Transaction created for the Account. This Transaction ID should be used for future
329    /// poll requests, as the client has already observed all changes up to and including it.
330    #[serde(
331        rename = "lastTransactionID",
332        default,
333        skip_serializing_if = "Option::is_none"
334    )]
335    pub last_transaction_id: Option<TransactionID>,
336}
337
338impl Client {
339    /// Account changes.
340    ///
341    /// # Errors
342    ///
343    /// Returns [`crate::OperationError::Client`] for invalid input, admission, transport, or
344    /// malformed response failures. Provider refusals return [`crate::OperationError::Rejected`]
345    /// or [`crate::OperationError::RejectedUnreadable`] when the rejection body cannot be read.
346    pub async fn account_changes(
347        &self,
348        account_id: &AccountID,
349        query: Option<&AccountChangesQuery>,
350    ) -> std::result::Result<
351        crate::ApiResponse<AccountChangesResponse>,
352        crate::OperationError<crate::GenericRejection>,
353    > {
354        let account_id_encoded = crate::client::segment(account_id.as_str());
355        let path = format!("/v3/accounts/{account_id_encoded}/changes");
356        let request = RequestParts::new(Method::GET, &path);
357        let request = match query {
358            Some(query) => request.query(query)?,
359            None => request,
360        };
361        self.execute(request).await
362    }
363}