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}