Skip to main content

oanda_rs/endpoints/
pricing.rs

1//! Pricing endpoints: current prices and the pricing stream.
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Error;
7use crate::models::{AccountId, ClientPrice, DateTime, HomeConversions, InstrumentName};
8use crate::streaming::{
9    PricingKind, PricingStream, StreamConfig, StreamKind, stream_config_setters,
10};
11
12impl Client {
13    /// Get pricing information for a list of instruments within an
14    /// account.
15    ///
16    /// `GET /v3/accounts/{accountID}/pricing`
17    ///
18    /// # Examples
19    ///
20    /// ```no_run
21    /// # async fn run() -> Result<(), oanda_rs::Error> {
22    /// # let client = oanda_rs::Client::new(oanda_rs::Environment::Practice, "token");
23    /// let pricing = client
24    ///     .prices("101-004-1234567-001", ["EUR_USD", "USD_JPY"])
25    ///     .send()
26    ///     .await?;
27    /// for price in pricing.prices {
28    ///     println!("{:?}: {:?}", price.instrument, price.closeout_bid);
29    /// }
30    /// # Ok(())
31    /// # }
32    /// ```
33    pub fn prices<I>(&self, account_id: impl Into<AccountId>, instruments: I) -> PricesRequest
34    where
35        I: IntoIterator,
36        I::Item: Into<InstrumentName>,
37    {
38        PricesRequest {
39            client: self.clone(),
40            account_id: account_id.into(),
41            instruments: instruments.into_iter().map(Into::into).collect(),
42            params: Vec::new(),
43        }
44    }
45
46    /// Open a stream of price updates (plus a heartbeat every 5 seconds)
47    /// for the given instruments.
48    ///
49    /// The returned [`PricingStream`] manages its own connection: it
50    /// detects stale connections via the heartbeat, reconnects with capped
51    /// exponential backoff, and requests a fresh price snapshot on every
52    /// reconnect. See [`crate::streaming`] for details and tuning.
53    ///
54    /// Connects to the **streaming host**
55    /// (`stream-fxpractice`/`stream-fxtrade`). OANDA allows at most 20
56    /// active streams per IP; prices are throttled to at most 4 updates
57    /// per second per instrument.
58    ///
59    /// `GET /v3/accounts/{accountID}/pricing/stream`
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    /// use futures_util::StreamExt;
67    /// use oanda_rs::models::PriceStreamItem;
68    ///
69    /// let mut stream = client
70    ///     .pricing_stream("101-004-1234567-001", ["EUR_USD"])
71    ///     .send()
72    ///     .await?;
73    /// while let Some(item) = stream.next().await {
74    ///     match item? {
75    ///         PriceStreamItem::Price(price) => println!("{:?}", price.closeout_bid),
76    ///         PriceStreamItem::Heartbeat(_) => {}
77    ///         _ => {}
78    ///     }
79    /// }
80    /// # Ok(())
81    /// # }
82    /// ```
83    pub fn pricing_stream<I>(
84        &self,
85        account_id: impl Into<AccountId>,
86        instruments: I,
87    ) -> PricingStreamRequest
88    where
89        I: IntoIterator,
90        I::Item: Into<InstrumentName>,
91    {
92        PricingStreamRequest {
93            client: self.clone(),
94            account_id: account_id.into(),
95            instruments: instruments.into_iter().map(Into::into).collect(),
96            snapshot: None,
97            config: StreamConfig::default(),
98        }
99    }
100}
101
102/// Builder for [`Client::prices`].
103#[derive(Debug)]
104pub struct PricesRequest {
105    client: Client,
106    account_id: AccountId,
107    instruments: Vec<InstrumentName>,
108    params: Vec<(&'static str, String)>,
109}
110
111impl PricesRequest {
112    /// Only return prices and home conversions updated after this time
113    /// (the response's `time` field can be fed back here when polling).
114    pub fn since(mut self, since: impl Into<DateTime>) -> Self {
115        self.params.push(("since", since.into().0));
116        self
117    }
118
119    /// Also include home currency conversion factors in the response.
120    pub fn include_home_conversions(mut self, include: bool) -> Self {
121        self.params
122            .push(("includeHomeConversions", include.to_string()));
123        self
124    }
125
126    /// Performs the request.
127    pub async fn send(self) -> Result<PricesResponse, Error> {
128        let request = self
129            .client
130            .get(&["accounts", self.account_id.as_str(), "pricing"])
131            .query(&[(
132                "instruments",
133                super::accounts::join_names(&self.instruments),
134            )])
135            .query(&self.params);
136        self.client.execute(request).await
137    }
138}
139
140/// Response of [`Client::prices`].
141#[derive(Debug, Clone, Serialize, Deserialize)]
142#[non_exhaustive]
143pub struct PricesResponse {
144    /// The list of price objects requested.
145    #[serde(rename = "prices", default, skip_serializing_if = "Vec::is_empty")]
146    pub prices: Vec<ClientPrice>,
147    /// The home currency conversion factors requested (only present when
148    /// `includeHomeConversions` was set).
149    #[serde(
150        rename = "homeConversions",
151        default,
152        skip_serializing_if = "Vec::is_empty"
153    )]
154    pub home_conversions: Vec<HomeConversions>,
155    /// The most recent time any of the prices/conversion factors were
156    /// updated; usable as the `since` parameter of the next poll.
157    #[serde(rename = "time", skip_serializing_if = "Option::is_none")]
158    pub time: Option<DateTime>,
159}
160
161/// Builder for [`Client::pricing_stream`].
162#[derive(Debug)]
163pub struct PricingStreamRequest {
164    client: Client,
165    account_id: AccountId,
166    instruments: Vec<InstrumentName>,
167    snapshot: Option<bool>,
168    config: StreamConfig,
169}
170
171impl PricingStreamRequest {
172    /// Whether to send a pricing snapshot of all requested instruments
173    /// when the stream (first) connects (OANDA defaults to `true`).
174    /// Reconnects always request a snapshot regardless of this setting.
175    pub fn snapshot(mut self, snapshot: bool) -> Self {
176        self.snapshot = Some(snapshot);
177        self
178    }
179
180    stream_config_setters!();
181
182    /// Connects and returns the managed stream. Fails fast when the
183    /// initial connection is rejected (e.g. bad token or account).
184    pub async fn send(self) -> Result<PricingStream, Error> {
185        let mut kind = PricingKind {
186            client: self.client,
187            account_id: self.account_id,
188            instruments: super::accounts::join_names(&self.instruments),
189            snapshot: self.snapshot,
190        };
191        let initial = kind.connect(false).await?;
192        Ok(PricingStream::new(kind, self.config, initial))
193    }
194}