Skip to main content

oanda_rs/models/
pricing.rs

1//! Pricing models: client prices, conversion factors, and stream heartbeats.
2
3use serde::{Deserialize, Serialize};
4
5use super::macros::string_enum;
6use super::{Currency, DateTime, DecimalNumber, InstrumentName, PriceValue, UnitsAvailable};
7
8/// The Price representation
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[non_exhaustive]
11pub struct Price {
12    /// The Price's Instrument.
13    #[serde(rename = "instrument", skip_serializing_if = "Option::is_none")]
14    pub instrument: Option<InstrumentName>,
15
16    /// Flag indicating if the Price is tradeable or not
17    #[serde(rename = "tradeable", skip_serializing_if = "Option::is_none")]
18    pub tradeable: Option<bool>,
19
20    /// The date/time when the Price was created.
21    #[serde(rename = "timestamp", skip_serializing_if = "Option::is_none")]
22    pub timestamp: Option<DateTime>,
23
24    /// The base bid price as calculated by pricing.
25    #[serde(rename = "baseBid", skip_serializing_if = "Option::is_none")]
26    pub base_bid: Option<PriceValue>,
27
28    /// The base ask price as calculated by pricing.
29    #[serde(rename = "baseAsk", skip_serializing_if = "Option::is_none")]
30    pub base_ask: Option<PriceValue>,
31
32    /// The list of prices and liquidity available on the Instrument's bid side.
33    /// It is possible for this list to be empty if there is no bid liquidity
34    /// currently available for the Instrument in the Account.
35    #[serde(rename = "bids", default, skip_serializing_if = "Vec::is_empty")]
36    pub bids: Vec<PriceBucket>,
37
38    /// The list of prices and liquidity available on the Instrument's ask side.
39    /// It is possible for this list to be empty if there is no ask liquidity
40    /// currently available for the Instrument in the Account.
41    #[serde(rename = "asks", default, skip_serializing_if = "Vec::is_empty")]
42    pub asks: Vec<PriceBucket>,
43
44    /// The closeout bid price. This price is used when a bid is required to
45    /// closeout a Position (margin closeout or manual) yet there is no bid
46    /// liquidity. The closeout bid is never used to open a new position.
47    #[serde(rename = "closeoutBid", skip_serializing_if = "Option::is_none")]
48    pub closeout_bid: Option<PriceValue>,
49
50    /// The closeout ask price. This price is used when an ask is required to
51    /// closeout a Position (margin closeout or manual) yet there is no ask
52    /// liquidity. The closeout ask is never used to open a new position.
53    #[serde(rename = "closeoutAsk", skip_serializing_if = "Option::is_none")]
54    pub closeout_ask: Option<PriceValue>,
55}
56
57/// The specification of an Account-specific Price.
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[non_exhaustive]
60pub struct ClientPrice {
61    /// The type discriminator, always `PRICE`.
62    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
63    pub r#type: Option<String>,
64
65    /// The Price's Instrument.
66    #[serde(rename = "instrument", skip_serializing_if = "Option::is_none")]
67    pub instrument: Option<InstrumentName>,
68
69    /// The date/time when the Price was created
70    #[serde(rename = "time", skip_serializing_if = "Option::is_none")]
71    pub time: Option<DateTime>,
72
73    /// The `status` field.
74    #[serde(rename = "status", skip_serializing_if = "Option::is_none")]
75    pub status: Option<PriceStatus>,
76
77    /// Flag indicating if the Price is tradeable or not
78    #[serde(rename = "tradeable", skip_serializing_if = "Option::is_none")]
79    pub tradeable: Option<bool>,
80
81    /// The list of prices and liquidity available on the Instrument's bid side.
82    /// It is possible for this list to be empty if there is no bid liquidity
83    /// currently available for the Instrument in the Account.
84    #[serde(rename = "bids", default, skip_serializing_if = "Vec::is_empty")]
85    pub bids: Vec<PriceBucket>,
86
87    /// The list of prices and liquidity available on the Instrument's ask side.
88    /// It is possible for this list to be empty if there is no ask liquidity
89    /// currently available for the Instrument in the Account.
90    #[serde(rename = "asks", default, skip_serializing_if = "Vec::is_empty")]
91    pub asks: Vec<PriceBucket>,
92
93    /// The closeout bid Price. This Price is used when a bid is required to
94    /// closeout a Position (margin closeout or manual) yet there is no bid
95    /// liquidity. The closeout bid is never used to open a new position.
96    #[serde(rename = "closeoutBid", skip_serializing_if = "Option::is_none")]
97    pub closeout_bid: Option<PriceValue>,
98
99    /// The closeout ask Price. This Price is used when a ask is required to
100    /// closeout a Position (margin closeout or manual) yet there is no ask
101    /// liquidity. The closeout ask is never used to open a new position.
102    #[serde(rename = "closeoutAsk", skip_serializing_if = "Option::is_none")]
103    pub closeout_ask: Option<PriceValue>,
104
105    /// The `quoteHomeConversionFactors` field.
106    #[serde(
107        rename = "quoteHomeConversionFactors",
108        skip_serializing_if = "Option::is_none"
109    )]
110    pub quote_home_conversion_factors: Option<QuoteHomeConversionFactors>,
111
112    /// The `unitsAvailable` field.
113    #[serde(rename = "unitsAvailable", skip_serializing_if = "Option::is_none")]
114    pub units_available: Option<UnitsAvailable>,
115
116    /// The date/time when the Price was created. This field is returned by the
117    /// live v20 API but is not present in OANDA's official documentation.
118    #[serde(rename = "timestamp", skip_serializing_if = "Option::is_none")]
119    pub timestamp: Option<DateTime>,
120}
121
122/// A Price Bucket represents a price available for an amount of liquidity
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124#[non_exhaustive]
125pub struct PriceBucket {
126    /// The Price offered by the PriceBucket
127    #[serde(rename = "price", skip_serializing_if = "Option::is_none")]
128    pub price: Option<PriceValue>,
129
130    /// The amount of liquidity offered by the PriceBucket. The REST API encodes
131    /// this as a string, the streaming API as a JSON number; OANDA documents it
132    /// as "integer or decimal if available".
133    #[serde(rename = "liquidity", skip_serializing_if = "Option::is_none")]
134    pub liquidity: Option<DecimalNumber>,
135}
136
137string_enum! {
138    /// The status of the Price.
139    pub enum PriceStatus {
140        Tradeable => "tradeable",
141        NonTradeable => "non-tradeable",
142        Invalid => "invalid",
143    }
144}
145
146/// A PricingHeartbeat object is injected into the Pricing stream to ensure that
147/// the HTTP connection remains active.
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149#[non_exhaustive]
150pub struct PricingHeartbeat {
151    /// The type discriminator, always `HEARTBEAT`.
152    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
153    pub r#type: Option<String>,
154
155    /// The date/time when the Heartbeat was created.
156    #[serde(rename = "time", skip_serializing_if = "Option::is_none")]
157    pub time: Option<DateTime>,
158}
159
160/// HomeConversions represents the factors to use to convert quantities of a
161/// given currency into the Account's home currency. The conversion factor
162/// depends on the scenario the conversion is required for.
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[non_exhaustive]
165pub struct HomeConversions {
166    /// The currency to be converted into the home currency.
167    #[serde(rename = "currency", skip_serializing_if = "Option::is_none")]
168    pub currency: Option<Currency>,
169
170    /// The factor used to convert any gains for an Account in the specified
171    /// currency into the Account's home currency. This would include positive
172    /// realized P/L and positive financing amounts. Conversion is performed by
173    /// multiplying the positive P/L by the conversion factor.
174    #[serde(rename = "accountGain", skip_serializing_if = "Option::is_none")]
175    pub account_gain: Option<DecimalNumber>,
176
177    /// The string representation of a decimal number.
178    #[serde(rename = "accountLoss", skip_serializing_if = "Option::is_none")]
179    pub account_loss: Option<DecimalNumber>,
180
181    /// The factor used to convert a Position or Trade Value in the specified
182    /// currency into the Account's home currency. Conversion is performed by
183    /// multiplying the Position or Trade Value by the conversion factor.
184    #[serde(rename = "positionValue", skip_serializing_if = "Option::is_none")]
185    pub position_value: Option<DecimalNumber>,
186}
187
188/// A HomeConversionFactors message contains information used to convert
189/// amounts, from an Instrument's base or quote currency, to the home currency
190/// of an Account.
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[non_exhaustive]
193pub struct HomeConversionFactors {
194    /// The `gainQuoteHome` field.
195    #[serde(rename = "gainQuoteHome", skip_serializing_if = "Option::is_none")]
196    pub gain_quote_home: Option<ConversionFactor>,
197
198    /// The `lossQuoteHome` field.
199    #[serde(rename = "lossQuoteHome", skip_serializing_if = "Option::is_none")]
200    pub loss_quote_home: Option<ConversionFactor>,
201
202    /// The `gainBaseHome` field.
203    #[serde(rename = "gainBaseHome", skip_serializing_if = "Option::is_none")]
204    pub gain_base_home: Option<ConversionFactor>,
205
206    /// The `lossBaseHome` field.
207    #[serde(rename = "lossBaseHome", skip_serializing_if = "Option::is_none")]
208    pub loss_base_home: Option<ConversionFactor>,
209}
210
211/// QuoteHomeConversionFactors represents the factors that can be used used to
212/// convert quantities of a Price's Instrument's quote currency into the
213/// Account's home currency.
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215#[non_exhaustive]
216pub struct QuoteHomeConversionFactors {
217    /// The factor used to convert a positive amount of the Price's Instrument's
218    /// quote currency into a positive amount of the Account's home currency.
219    /// Conversion is performed by multiplying the quote units by the conversion
220    /// factor.
221    #[serde(rename = "positiveUnits", skip_serializing_if = "Option::is_none")]
222    pub positive_units: Option<DecimalNumber>,
223
224    /// The factor used to convert a negative amount of the Price's Instrument's
225    /// quote currency into a negative amount of the Account's home currency.
226    /// Conversion is performed by multiplying the quote units by the conversion
227    /// factor.
228    #[serde(rename = "negativeUnits", skip_serializing_if = "Option::is_none")]
229    pub negative_units: Option<DecimalNumber>,
230}
231
232/// A ConversionFactor contains information used to convert an amount, from an
233/// Instrument's base or quote currency, to the home currency of an Account.
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235#[non_exhaustive]
236pub struct ConversionFactor {
237    /// The factor by which to multiply the amount in the given currency to
238    /// obtain the amount in the home currency of the Account.
239    #[serde(rename = "factor", skip_serializing_if = "Option::is_none")]
240    pub factor: Option<DecimalNumber>,
241}
242
243/// A single line of the pricing stream: either a price update or a
244/// heartbeat (sent every 5 seconds to keep the connection alive).
245#[derive(Debug, Clone, PartialEq)]
246#[non_exhaustive]
247#[allow(clippy::large_enum_variant)] // variants mirror the wire format; boxing would hurt ergonomics
248pub enum PriceStreamItem {
249    /// A price update (`"type": "PRICE"`).
250    Price(ClientPrice),
251    /// A keep-alive heartbeat (`"type": "HEARTBEAT"`).
252    Heartbeat(PricingHeartbeat),
253}
254
255impl serde::Serialize for PriceStreamItem {
256    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
257        match self {
258            PriceStreamItem::Price(price) => price.serialize(serializer),
259            PriceStreamItem::Heartbeat(heartbeat) => heartbeat.serialize(serializer),
260        }
261    }
262}
263
264impl<'de> serde::Deserialize<'de> for PriceStreamItem {
265    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
266        let value = serde_json::Value::deserialize(deserializer)?;
267        let is_heartbeat =
268            value.get("type").and_then(serde_json::Value::as_str) == Some("HEARTBEAT");
269        if is_heartbeat {
270            serde_json::from_value(value)
271                .map(PriceStreamItem::Heartbeat)
272                .map_err(serde::de::Error::custom)
273        } else {
274            serde_json::from_value(value)
275                .map(PriceStreamItem::Price)
276                .map_err(serde::de::Error::custom)
277        }
278    }
279}