Skip to main content

oanda_rs/endpoints/
instruments.rs

1//! Instrument endpoints: candlestick data and order/position books.
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Error;
7use crate::models::{
8    AccountId, CandleSpecification, CandlestickGranularity, DateTime, DecimalNumber,
9    InstrumentCandles, InstrumentName, OrderBook, PositionBook, PricingComponent, WeeklyAlignment,
10};
11
12impl Client {
13    /// Fetch candlestick data for an instrument.
14    ///
15    /// `GET /v3/instruments/{instrument}/candles`
16    ///
17    /// # Examples
18    ///
19    /// ```no_run
20    /// # async fn run() -> Result<(), oanda_rs::Error> {
21    /// # let client = oanda_rs::Client::new(oanda_rs::Environment::Practice, "token");
22    /// use oanda_rs::models::CandlestickGranularity;
23    ///
24    /// let candles = client
25    ///     .candles("EUR_USD")
26    ///     .granularity(CandlestickGranularity::H1)
27    ///     .count(500)
28    ///     .send()
29    ///     .await?;
30    /// println!("got {} candles", candles.candles.len());
31    /// # Ok(())
32    /// # }
33    /// ```
34    pub fn candles(&self, instrument: impl Into<InstrumentName>) -> CandlesRequest {
35        CandlesRequest::new(self.clone(), None, instrument.into())
36    }
37
38    /// Fetch candlestick data for an instrument, scoped to an account: the
39    /// data matches what the account would see, and volume-weighted average
40    /// prices can be requested via
41    /// [`units`](CandlesRequest::units).
42    ///
43    /// `GET /v3/accounts/{accountID}/instruments/{instrument}/candles`
44    pub fn account_candles(
45        &self,
46        account_id: impl Into<AccountId>,
47        instrument: impl Into<InstrumentName>,
48    ) -> CandlesRequest {
49        CandlesRequest::new(self.clone(), Some(account_id.into()), instrument.into())
50    }
51
52    /// Get the most recently completed candles within an account for the
53    /// specified combinations of instrument, granularity, and price
54    /// component.
55    ///
56    /// `GET /v3/accounts/{accountID}/candles/latest`
57    ///
58    /// # Examples
59    ///
60    /// ```no_run
61    /// # async fn run() -> Result<(), oanda_rs::Error> {
62    /// # let client = oanda_rs::Client::new(oanda_rs::Environment::Practice, "token");
63    /// use oanda_rs::models::{CandleSpecification, CandlestickGranularity, PricingComponent};
64    ///
65    /// let latest = client
66    ///     .latest_candles(
67    ///         "101-004-1234567-001",
68    ///         [CandleSpecification::new("EUR_USD", CandlestickGranularity::S10)
69    ///             .price(PricingComponent::BID.with_mid())],
70    ///     )
71    ///     .send()
72    ///     .await?;
73    /// # Ok(())
74    /// # }
75    /// ```
76    pub fn latest_candles(
77        &self,
78        account_id: impl Into<AccountId>,
79        specifications: impl IntoIterator<Item = CandleSpecification>,
80    ) -> LatestCandlesRequest {
81        LatestCandlesRequest {
82            client: self.clone(),
83            account_id: account_id.into(),
84            specifications: specifications.into_iter().collect(),
85            params: Vec::new(),
86        }
87    }
88
89    /// Fetch an order book for an instrument.
90    ///
91    /// `GET /v3/instruments/{instrument}/orderBook`
92    pub fn instrument_order_book(&self, instrument: impl Into<InstrumentName>) -> OrderBookRequest {
93        OrderBookRequest {
94            client: self.clone(),
95            instrument: instrument.into(),
96            time: None,
97        }
98    }
99
100    /// Fetch a position book for an instrument.
101    ///
102    /// `GET /v3/instruments/{instrument}/positionBook`
103    pub fn instrument_position_book(
104        &self,
105        instrument: impl Into<InstrumentName>,
106    ) -> PositionBookRequest {
107        PositionBookRequest {
108            client: self.clone(),
109            instrument: instrument.into(),
110            time: None,
111        }
112    }
113}
114
115/// Builder for [`Client::candles`] and [`Client::account_candles`].
116#[derive(Debug)]
117pub struct CandlesRequest {
118    client: Client,
119    account_id: Option<AccountId>,
120    instrument: InstrumentName,
121    params: Vec<(&'static str, String)>,
122}
123
124impl CandlesRequest {
125    fn new(client: Client, account_id: Option<AccountId>, instrument: InstrumentName) -> Self {
126        CandlesRequest {
127            client,
128            account_id,
129            instrument,
130            params: Vec::new(),
131        }
132    }
133
134    /// The price component(s) to get candlestick data for (default mid).
135    pub fn price(mut self, price: PricingComponent) -> Self {
136        self.params.push(("price", price.to_string()));
137        self
138    }
139
140    /// The granularity of the candlesticks to fetch (default `S5`).
141    pub fn granularity(mut self, granularity: CandlestickGranularity) -> Self {
142        self.params.push(("granularity", granularity.to_string()));
143        self
144    }
145
146    /// The number of candlesticks to return (default 500, maximum 5000).
147    /// May not be specified when both `from` and `to` are provided.
148    pub fn count(mut self, count: u32) -> Self {
149        self.params.push(("count", count.to_string()));
150        self
151    }
152
153    /// The start of the time range to fetch candlesticks for.
154    pub fn from(mut self, from: impl Into<DateTime>) -> Self {
155        self.params.push(("from", from.into().0));
156        self
157    }
158
159    /// The end of the time range to fetch candlesticks for.
160    pub fn to(mut self, to: impl Into<DateTime>) -> Self {
161        self.params.push(("to", to.into().0));
162        self
163    }
164
165    /// Whether the candlestick is "smoothed" (uses the previous candle's
166    /// close as its open; default `false`).
167    pub fn smooth(mut self, smooth: bool) -> Self {
168        self.params.push(("smooth", smooth.to_string()));
169        self
170    }
171
172    /// Whether the candlestick covered by the `from` time should be
173    /// included (default `true`).
174    pub fn include_first(mut self, include_first: bool) -> Self {
175        self.params
176            .push(("includeFirst", include_first.to_string()));
177        self
178    }
179
180    /// The hour of the day (in `alignment_timezone`) used for granularities
181    /// with daily alignment (default 17).
182    pub fn daily_alignment(mut self, hour: u8) -> Self {
183        self.params.push(("dailyAlignment", hour.to_string()));
184        self
185    }
186
187    /// The timezone used for `daily_alignment` (default
188    /// `America/New_York`).
189    pub fn alignment_timezone(mut self, timezone: impl Into<String>) -> Self {
190        self.params.push(("alignmentTimezone", timezone.into()));
191        self
192    }
193
194    /// The day of the week used for granularities with weekly alignment
195    /// (default `Friday`).
196    pub fn weekly_alignment(mut self, alignment: WeeklyAlignment) -> Self {
197        self.params.push(("weeklyAlignment", alignment.to_string()));
198        self
199    }
200
201    /// The number of units used to calculate the volume-weighted average
202    /// bid/ask prices. Only supported on the account-scoped
203    /// [`Client::account_candles`] endpoint.
204    pub fn units(mut self, units: impl Into<DecimalNumber>) -> Self {
205        self.params.push(("units", units.into().to_string()));
206        self
207    }
208
209    /// Performs the request.
210    pub async fn send(self) -> Result<InstrumentCandles, Error> {
211        let request = match &self.account_id {
212            Some(account_id) => self.client.get(&[
213                "accounts",
214                account_id.as_str(),
215                "instruments",
216                self.instrument.as_str(),
217                "candles",
218            ]),
219            None => self
220                .client
221                .get(&["instruments", self.instrument.as_str(), "candles"]),
222        };
223        self.client.execute(request.query(&self.params)).await
224    }
225}
226
227/// Builder for [`Client::latest_candles`].
228#[derive(Debug)]
229pub struct LatestCandlesRequest {
230    client: Client,
231    account_id: AccountId,
232    specifications: Vec<CandleSpecification>,
233    params: Vec<(&'static str, String)>,
234}
235
236impl LatestCandlesRequest {
237    /// The number of units used to calculate the volume-weighted average
238    /// bid/ask prices.
239    pub fn units(mut self, units: impl Into<DecimalNumber>) -> Self {
240        self.params.push(("units", units.into().to_string()));
241        self
242    }
243
244    /// Whether the candlestick is "smoothed" (default `false`).
245    pub fn smooth(mut self, smooth: bool) -> Self {
246        self.params.push(("smooth", smooth.to_string()));
247        self
248    }
249
250    /// The hour of the day (in `alignment_timezone`) used for granularities
251    /// with daily alignment (default 17).
252    pub fn daily_alignment(mut self, hour: u8) -> Self {
253        self.params.push(("dailyAlignment", hour.to_string()));
254        self
255    }
256
257    /// The timezone used for `daily_alignment` (default
258    /// `America/New_York`).
259    pub fn alignment_timezone(mut self, timezone: impl Into<String>) -> Self {
260        self.params.push(("alignmentTimezone", timezone.into()));
261        self
262    }
263
264    /// The day of the week used for granularities with weekly alignment
265    /// (default `Friday`).
266    pub fn weekly_alignment(mut self, alignment: WeeklyAlignment) -> Self {
267        self.params.push(("weeklyAlignment", alignment.to_string()));
268        self
269    }
270
271    /// Performs the request.
272    pub async fn send(self) -> Result<LatestCandlesResponse, Error> {
273        let specs = self
274            .specifications
275            .iter()
276            .map(ToString::to_string)
277            .collect::<Vec<_>>()
278            .join(",");
279        let request = self
280            .client
281            .get(&["accounts", self.account_id.as_str(), "candles", "latest"])
282            .query(&[("candleSpecifications", specs)])
283            .query(&self.params);
284        self.client.execute(request).await
285    }
286}
287
288/// Response of [`Client::latest_candles`].
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[non_exhaustive]
291pub struct LatestCandlesResponse {
292    /// The latest candle sets for each candle specification.
293    #[serde(
294        rename = "latestCandles",
295        default,
296        skip_serializing_if = "Vec::is_empty"
297    )]
298    pub latest_candles: Vec<InstrumentCandles>,
299}
300
301/// Builder for [`Client::instrument_order_book`].
302#[derive(Debug)]
303pub struct OrderBookRequest {
304    client: Client,
305    instrument: InstrumentName,
306    time: Option<DateTime>,
307}
308
309impl OrderBookRequest {
310    /// The time of the snapshot to fetch. If not specified, then the most
311    /// recent snapshot is fetched.
312    pub fn time(mut self, time: impl Into<DateTime>) -> Self {
313        self.time = Some(time.into());
314        self
315    }
316
317    /// Performs the request.
318    pub async fn send(self) -> Result<OrderBookResponse, Error> {
319        let mut request = self
320            .client
321            .get(&["instruments", self.instrument.as_str(), "orderBook"]);
322        if let Some(time) = &self.time {
323            request = request.query(&[("time", time.as_str())]);
324        }
325        let (mut response, headers): (OrderBookResponse, _) =
326            self.client.execute_with_headers(request).await?;
327        response.link = crate::transport::header_str(&headers, "Link").map(str::to_owned);
328        Ok(response)
329    }
330}
331
332/// Response of [`Client::instrument_order_book`].
333#[derive(Debug, Clone, Serialize, Deserialize)]
334#[non_exhaustive]
335pub struct OrderBookResponse {
336    /// The instrument's order book.
337    #[serde(rename = "orderBook")]
338    pub order_book: OrderBook,
339    /// The `Link` response header, containing links to the next/previous
340    /// order book snapshots (when a `time` was requested).
341    #[serde(skip)]
342    pub link: Option<String>,
343}
344
345/// Builder for [`Client::instrument_position_book`].
346#[derive(Debug)]
347pub struct PositionBookRequest {
348    client: Client,
349    instrument: InstrumentName,
350    time: Option<DateTime>,
351}
352
353impl PositionBookRequest {
354    /// The time of the snapshot to fetch. If not specified, then the most
355    /// recent snapshot is fetched.
356    pub fn time(mut self, time: impl Into<DateTime>) -> Self {
357        self.time = Some(time.into());
358        self
359    }
360
361    /// Performs the request.
362    pub async fn send(self) -> Result<PositionBookResponse, Error> {
363        let mut request =
364            self.client
365                .get(&["instruments", self.instrument.as_str(), "positionBook"]);
366        if let Some(time) = &self.time {
367            request = request.query(&[("time", time.as_str())]);
368        }
369        let (mut response, headers): (PositionBookResponse, _) =
370            self.client.execute_with_headers(request).await?;
371        response.link = crate::transport::header_str(&headers, "Link").map(str::to_owned);
372        Ok(response)
373    }
374}
375
376/// Response of [`Client::instrument_position_book`].
377#[derive(Debug, Clone, Serialize, Deserialize)]
378#[non_exhaustive]
379pub struct PositionBookResponse {
380    /// The instrument's position book.
381    #[serde(rename = "positionBook")]
382    pub position_book: PositionBook,
383    /// The `Link` response header, containing links to the next/previous
384    /// position book snapshots (when a `time` was requested).
385    #[serde(skip)]
386    pub link: Option<String>,
387}