Skip to main content

oanda_rs/models/
instrument.rs

1//! Instrument metadata, candlestick, and order/position book models.
2
3use serde::{Deserialize, Serialize};
4
5use super::macros::string_enum;
6use super::{DateTime, DecimalNumber, GuaranteedStopLossOrderMode, InstrumentName, PriceValue};
7
8/// Full specification of an Instrument.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[non_exhaustive]
11pub struct Instrument {
12    /// The name of the Instrument
13    #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
14    pub name: Option<InstrumentName>,
15
16    /// The `type` field.
17    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
18    pub r#type: Option<InstrumentType>,
19
20    /// The display name of the Instrument
21    #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
22    pub display_name: Option<String>,
23
24    /// The location of the "pip" for this instrument. The decimal position of
25    /// the pip in this Instrument's price can be found at 10 ^ pipLocation
26    /// (e.g. -4 pipLocation results in a decimal pip position of 10 ^ -4 =
27    /// 0.0001).
28    #[serde(rename = "pipLocation", skip_serializing_if = "Option::is_none")]
29    pub pip_location: Option<i64>,
30
31    /// The number of decimal places that should be used to display prices for
32    /// this instrument. (e.g. a displayPrecision of 5 would result in a price
33    /// of "1" being displayed as "1.00000")
34    #[serde(rename = "displayPrecision", skip_serializing_if = "Option::is_none")]
35    pub display_precision: Option<i64>,
36
37    /// The amount of decimal places that may be provided when specifying the
38    /// number of units traded for this instrument.
39    #[serde(
40        rename = "tradeUnitsPrecision",
41        skip_serializing_if = "Option::is_none"
42    )]
43    pub trade_units_precision: Option<i64>,
44
45    /// The smallest number of units allowed to be traded for this instrument.
46    #[serde(rename = "minimumTradeSize", skip_serializing_if = "Option::is_none")]
47    pub minimum_trade_size: Option<DecimalNumber>,
48
49    /// The maximum trailing stop distance allowed for a trailing stop loss
50    /// created for this instrument. Specified in price units.
51    #[serde(
52        rename = "maximumTrailingStopDistance",
53        skip_serializing_if = "Option::is_none"
54    )]
55    pub maximum_trailing_stop_distance: Option<DecimalNumber>,
56
57    /// The minimum trailing stop distance allowed for a trailing stop loss
58    /// created for this instrument. Specified in price units.
59    #[serde(
60        rename = "minimumTrailingStopDistance",
61        skip_serializing_if = "Option::is_none"
62    )]
63    pub minimum_trailing_stop_distance: Option<DecimalNumber>,
64
65    /// The maximum position size allowed for this instrument. Specified in
66    /// units.
67    #[serde(
68        rename = "maximumPositionSize",
69        skip_serializing_if = "Option::is_none"
70    )]
71    pub maximum_position_size: Option<DecimalNumber>,
72
73    /// The maximum units allowed for an Order placed for this instrument.
74    /// Specified in units.
75    #[serde(rename = "maximumOrderUnits", skip_serializing_if = "Option::is_none")]
76    pub maximum_order_units: Option<DecimalNumber>,
77
78    /// The margin rate for this instrument.
79    #[serde(rename = "marginRate", skip_serializing_if = "Option::is_none")]
80    pub margin_rate: Option<DecimalNumber>,
81
82    /// The `commission` field.
83    #[serde(rename = "commission", skip_serializing_if = "Option::is_none")]
84    pub commission: Option<InstrumentCommission>,
85
86    /// The `guaranteedStopLossOrderMode` field.
87    #[serde(
88        rename = "guaranteedStopLossOrderMode",
89        skip_serializing_if = "Option::is_none"
90    )]
91    pub guaranteed_stop_loss_order_mode: Option<GuaranteedStopLossOrderMode>,
92
93    /// The minimum distance allowed between the Trade's fill price and the
94    /// configured price for guaranteed Stop Loss Orders created for this
95    /// instrument. Specified in price units.
96    #[serde(
97        rename = "minimumGuaranteedStopLossDistance",
98        skip_serializing_if = "Option::is_none"
99    )]
100    pub minimum_guaranteed_stop_loss_distance: Option<DecimalNumber>,
101
102    /// The amount that is charged to the account if a guaranteed Stop Loss
103    /// Order is triggered and filled. The value is in price units and is
104    /// charged for each unit of the Trade.
105    #[serde(
106        rename = "guaranteedStopLossOrderExecutionPremium",
107        skip_serializing_if = "Option::is_none"
108    )]
109    pub guaranteed_stop_loss_order_execution_premium: Option<DecimalNumber>,
110
111    /// The `guaranteedStopLossOrderLevelRestriction` field.
112    #[serde(
113        rename = "guaranteedStopLossOrderLevelRestriction",
114        skip_serializing_if = "Option::is_none"
115    )]
116    pub guaranteed_stop_loss_order_level_restriction:
117        Option<GuaranteedStopLossOrderLevelRestriction>,
118
119    /// The `financing` field.
120    #[serde(rename = "financing", skip_serializing_if = "Option::is_none")]
121    pub financing: Option<InstrumentFinancing>,
122
123    /// The tags associated with this instrument.
124    #[serde(rename = "tags", default, skip_serializing_if = "Vec::is_empty")]
125    pub tags: Vec<Tag>,
126}
127
128/// An InstrumentCommission represents an instrument-specific commission
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[non_exhaustive]
131pub struct InstrumentCommission {
132    /// The commission amount (in the Account's home currency) charged per
133    /// unitsTraded of the instrument
134    #[serde(rename = "commission", skip_serializing_if = "Option::is_none")]
135    pub commission: Option<DecimalNumber>,
136
137    /// The number of units traded that the commission amount is based on.
138    #[serde(rename = "unitsTraded", skip_serializing_if = "Option::is_none")]
139    pub units_traded: Option<DecimalNumber>,
140
141    /// The minimum commission amount (in the Account's home currency) that is
142    /// charged when an Order is filled for this instrument.
143    #[serde(rename = "minimumCommission", skip_serializing_if = "Option::is_none")]
144    pub minimum_commission: Option<DecimalNumber>,
145}
146
147/// Financing data for the instrument.
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149#[non_exhaustive]
150pub struct InstrumentFinancing {
151    /// The financing rate to be used for a long position for the instrument.
152    #[serde(rename = "longRate", skip_serializing_if = "Option::is_none")]
153    pub long_rate: Option<DecimalNumber>,
154
155    /// The financing rate to be used for a short position for the instrument.
156    #[serde(rename = "shortRate", skip_serializing_if = "Option::is_none")]
157    pub short_rate: Option<DecimalNumber>,
158
159    /// The days of the week to debit or credit financing charges; the exact
160    /// time of day at which to charge the financing is set in the
161    /// DivisionTradingGroup for the client's account.
162    #[serde(
163        rename = "financingDaysOfWeek",
164        default,
165        skip_serializing_if = "Vec::is_empty"
166    )]
167    pub financing_days_of_week: Vec<FinancingDayOfWeek>,
168}
169
170/// A FinancingDayOfWeek message defines a day of the week when financing
171/// charges are debited or credited.
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173#[non_exhaustive]
174pub struct FinancingDayOfWeek {
175    /// The `dayOfWeek` field.
176    #[serde(rename = "dayOfWeek", skip_serializing_if = "Option::is_none")]
177    pub day_of_week: Option<DayOfWeek>,
178
179    /// The number of days worth of financing to be charged on dayOfWeek.
180    #[serde(rename = "daysCharged", skip_serializing_if = "Option::is_none")]
181    pub days_charged: Option<i64>,
182}
183
184string_enum! {
185    /// The DayOfWeek provides a representation of the day of the week.
186    pub enum DayOfWeek {
187        Sunday => "SUNDAY",
188        Monday => "MONDAY",
189        Tuesday => "TUESDAY",
190        Wednesday => "WEDNESDAY",
191        Thursday => "THURSDAY",
192        Friday => "FRIDAY",
193        Saturday => "SATURDAY",
194    }
195}
196
197string_enum! {
198    /// The type of an Instrument.
199    pub enum InstrumentType {
200        Currency => "CURRENCY",
201        Cfd => "CFD",
202        Metal => "METAL",
203    }
204}
205
206/// A tag associated with an entity.
207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
208#[non_exhaustive]
209pub struct Tag {
210    /// The type of the tag.
211    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
212    pub r#type: Option<String>,
213
214    /// The name of the tag.
215    #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
216    pub name: Option<String>,
217}
218
219/// A GuaranteedStopLossOrderLevelRestriction represents the total position size
220/// that can exist within a given price window for Trades with guaranteed Stop
221/// Loss Orders attached for a specific Instrument.
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223#[non_exhaustive]
224pub struct GuaranteedStopLossOrderLevelRestriction {
225    /// Applies to Trades with a guaranteed Stop Loss Order attached for the
226    /// specified Instrument. This is the total allowed Trade volume that can
227    /// exist within the priceRange based on the trigger prices of the
228    /// guaranteed Stop Loss Orders.
229    #[serde(rename = "volume", skip_serializing_if = "Option::is_none")]
230    pub volume: Option<DecimalNumber>,
231
232    /// The price range the volume applies to. This value is in price units.
233    #[serde(rename = "priceRange", skip_serializing_if = "Option::is_none")]
234    pub price_range: Option<DecimalNumber>,
235}
236
237string_enum! {
238    /// The granularity of a candlestick
239    pub enum CandlestickGranularity {
240        S5 => "S5",
241        S10 => "S10",
242        S15 => "S15",
243        S30 => "S30",
244        M1 => "M1",
245        M2 => "M2",
246        M4 => "M4",
247        M5 => "M5",
248        M10 => "M10",
249        M15 => "M15",
250        M30 => "M30",
251        H1 => "H1",
252        H2 => "H2",
253        H3 => "H3",
254        H4 => "H4",
255        H6 => "H6",
256        H8 => "H8",
257        H12 => "H12",
258        D => "D",
259        W => "W",
260        M => "M",
261    }
262}
263
264string_enum! {
265    /// The day of the week to use for candlestick granularities with weekly
266    /// alignment.
267    pub enum WeeklyAlignment {
268        Monday => "Monday",
269        Tuesday => "Tuesday",
270        Wednesday => "Wednesday",
271        Thursday => "Thursday",
272        Friday => "Friday",
273        Saturday => "Saturday",
274        Sunday => "Sunday",
275    }
276}
277
278/// The price data (open, high, low, close) for the Candlestick representation.
279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
280#[non_exhaustive]
281pub struct CandlestickData {
282    /// The first (open) price in the time-range represented by the candlestick.
283    #[serde(rename = "o", skip_serializing_if = "Option::is_none")]
284    pub o: Option<PriceValue>,
285
286    /// The highest price in the time-range represented by the candlestick.
287    #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
288    pub h: Option<PriceValue>,
289
290    /// The lowest price in the time-range represented by the candlestick.
291    #[serde(rename = "l", skip_serializing_if = "Option::is_none")]
292    pub l: Option<PriceValue>,
293
294    /// The last (closing) price in the time-range represented by the
295    /// candlestick.
296    #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
297    pub c: Option<PriceValue>,
298}
299
300/// The Candlestick representation
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302#[non_exhaustive]
303pub struct Candlestick {
304    /// The start time of the candlestick
305    #[serde(rename = "time", skip_serializing_if = "Option::is_none")]
306    pub time: Option<DateTime>,
307
308    /// The `bid` field.
309    #[serde(rename = "bid", skip_serializing_if = "Option::is_none")]
310    pub bid: Option<CandlestickData>,
311
312    /// The `ask` field.
313    #[serde(rename = "ask", skip_serializing_if = "Option::is_none")]
314    pub ask: Option<CandlestickData>,
315
316    /// The `mid` field.
317    #[serde(rename = "mid", skip_serializing_if = "Option::is_none")]
318    pub mid: Option<CandlestickData>,
319
320    /// The number of prices created during the time-range represented by the
321    /// candlestick.
322    #[serde(rename = "volume", skip_serializing_if = "Option::is_none")]
323    pub volume: Option<i64>,
324
325    /// A flag indicating if the candlestick is complete. A complete candlestick
326    /// is one whose ending time is not in the future.
327    #[serde(rename = "complete", skip_serializing_if = "Option::is_none")]
328    pub complete: Option<bool>,
329}
330
331/// The LatestCandles representation.
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
333#[non_exhaustive]
334pub struct InstrumentCandles {
335    /// The instrument whose Prices are represented by the candlesticks.
336    #[serde(rename = "instrument", skip_serializing_if = "Option::is_none")]
337    pub instrument: Option<InstrumentName>,
338
339    /// The `granularity` field.
340    #[serde(rename = "granularity", skip_serializing_if = "Option::is_none")]
341    pub granularity: Option<CandlestickGranularity>,
342
343    /// The list of candlesticks that satisfy the request.
344    #[serde(rename = "candles", default, skip_serializing_if = "Vec::is_empty")]
345    pub candles: Vec<Candlestick>,
346}
347
348/// The representation of an instrument's order book at a point in time
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350#[non_exhaustive]
351pub struct OrderBook {
352    /// The order book's instrument
353    #[serde(rename = "instrument", skip_serializing_if = "Option::is_none")]
354    pub instrument: Option<InstrumentName>,
355
356    /// The time when the order book snapshot was created.
357    #[serde(rename = "time", skip_serializing_if = "Option::is_none")]
358    pub time: Option<DateTime>,
359
360    /// The price (midpoint) for the order book's instrument at the time of the
361    /// order book snapshot
362    #[serde(rename = "price", skip_serializing_if = "Option::is_none")]
363    pub price: Option<PriceValue>,
364
365    /// The price width for each bucket. Each bucket covers the price range from
366    /// the bucket's price to the bucket's price + bucketWidth.
367    #[serde(rename = "bucketWidth", skip_serializing_if = "Option::is_none")]
368    pub bucket_width: Option<PriceValue>,
369
370    /// The partitioned order book, divided into buckets using a default bucket
371    /// width. These buckets are only provided for price ranges which actually
372    /// contain order or position data.
373    #[serde(rename = "buckets", default, skip_serializing_if = "Vec::is_empty")]
374    pub buckets: Vec<OrderBookBucket>,
375
376    /// The book's creation time as a Unix timestamp (seconds), string-encoded.
377    /// This field is returned by the live v20 API but is not present in OANDA's
378    /// official documentation.
379    #[serde(rename = "unixTime", skip_serializing_if = "Option::is_none")]
380    pub unix_time: Option<DateTime>,
381}
382
383/// The order book data for a partition of the instrument's prices.
384#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
385#[non_exhaustive]
386pub struct OrderBookBucket {
387    /// The lowest price (inclusive) covered by the bucket. The bucket covers
388    /// the price range from the price to price + the order book's bucketWidth.
389    #[serde(rename = "price", skip_serializing_if = "Option::is_none")]
390    pub price: Option<PriceValue>,
391
392    /// The percentage of the total number of orders represented by the long
393    /// orders found in this bucket.
394    #[serde(rename = "longCountPercent", skip_serializing_if = "Option::is_none")]
395    pub long_count_percent: Option<DecimalNumber>,
396
397    /// The percentage of the total number of orders represented by the short
398    /// orders found in this bucket.
399    #[serde(rename = "shortCountPercent", skip_serializing_if = "Option::is_none")]
400    pub short_count_percent: Option<DecimalNumber>,
401}
402
403/// The representation of an instrument's position book at a point in time
404#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
405#[non_exhaustive]
406pub struct PositionBook {
407    /// The position book's instrument
408    #[serde(rename = "instrument", skip_serializing_if = "Option::is_none")]
409    pub instrument: Option<InstrumentName>,
410
411    /// The time when the position book snapshot was created
412    #[serde(rename = "time", skip_serializing_if = "Option::is_none")]
413    pub time: Option<DateTime>,
414
415    /// The price (midpoint) for the position book's instrument at the time of
416    /// the position book snapshot
417    #[serde(rename = "price", skip_serializing_if = "Option::is_none")]
418    pub price: Option<PriceValue>,
419
420    /// The price width for each bucket. Each bucket covers the price range from
421    /// the bucket's price to the bucket's price + bucketWidth.
422    #[serde(rename = "bucketWidth", skip_serializing_if = "Option::is_none")]
423    pub bucket_width: Option<PriceValue>,
424
425    /// The partitioned position book, divided into buckets using a default
426    /// bucket width. These buckets are only provided for price ranges which
427    /// actually contain order or position data.
428    #[serde(rename = "buckets", default, skip_serializing_if = "Vec::is_empty")]
429    pub buckets: Vec<PositionBookBucket>,
430
431    /// The book's creation time as a Unix timestamp (seconds), string-encoded.
432    /// This field is returned by the live v20 API but is not present in OANDA's
433    /// official documentation.
434    #[serde(rename = "unixTime", skip_serializing_if = "Option::is_none")]
435    pub unix_time: Option<DateTime>,
436}
437
438/// The position book data for a partition of the instrument's prices.
439#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
440#[non_exhaustive]
441pub struct PositionBookBucket {
442    /// The lowest price (inclusive) covered by the bucket. The bucket covers
443    /// the price range from the price to price + the position book's
444    /// bucketWidth.
445    #[serde(rename = "price", skip_serializing_if = "Option::is_none")]
446    pub price: Option<PriceValue>,
447
448    /// The percentage of the total number of positions represented by the long
449    /// positions found in this bucket.
450    #[serde(rename = "longCountPercent", skip_serializing_if = "Option::is_none")]
451    pub long_count_percent: Option<DecimalNumber>,
452
453    /// The percentage of the total number of positions represented by the short
454    /// positions found in this bucket.
455    #[serde(rename = "shortCountPercent", skip_serializing_if = "Option::is_none")]
456    pub short_count_percent: Option<DecimalNumber>,
457}
458
459/// The price component(s) a candlestick request applies to: any combination
460/// of bid (`B`), mid (`M`) and ask (`A`).
461///
462/// ```
463/// use oanda_rs::models::PricingComponent;
464///
465/// assert_eq!(PricingComponent::MID.to_string(), "M");
466/// assert_eq!(PricingComponent::MID.with_bid().with_ask().to_string(), "BMA");
467/// ```
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
469pub struct PricingComponent {
470    /// Include bid candles.
471    pub bid: bool,
472    /// Include mid candles.
473    pub mid: bool,
474    /// Include ask candles.
475    pub ask: bool,
476}
477
478impl PricingComponent {
479    /// Bid candles only (`B`).
480    pub const BID: PricingComponent = PricingComponent {
481        bid: true,
482        mid: false,
483        ask: false,
484    };
485    /// Mid candles only (`M`) — OANDA's default.
486    pub const MID: PricingComponent = PricingComponent {
487        bid: false,
488        mid: true,
489        ask: false,
490    };
491    /// Ask candles only (`A`).
492    pub const ASK: PricingComponent = PricingComponent {
493        bid: false,
494        mid: false,
495        ask: true,
496    };
497
498    /// Adds the bid component.
499    pub fn with_bid(mut self) -> Self {
500        self.bid = true;
501        self
502    }
503
504    /// Adds the mid component.
505    pub fn with_mid(mut self) -> Self {
506        self.mid = true;
507        self
508    }
509
510    /// Adds the ask component.
511    pub fn with_ask(mut self) -> Self {
512        self.ask = true;
513        self
514    }
515}
516
517impl std::fmt::Display for PricingComponent {
518    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519        if self.bid {
520            f.write_str("B")?;
521        }
522        if self.mid {
523            f.write_str("M")?;
524        }
525        if self.ask {
526            f.write_str("A")?;
527        }
528        Ok(())
529    }
530}
531
532impl std::str::FromStr for PricingComponent {
533    type Err = String;
534
535    fn from_str(s: &str) -> Result<Self, Self::Err> {
536        let mut component = PricingComponent::default();
537        for c in s.chars() {
538            match c.to_ascii_uppercase() {
539                'B' => component.bid = true,
540                'M' => component.mid = true,
541                'A' => component.ask = true,
542                other => return Err(format!("invalid pricing component character: {other:?}")),
543            }
544        }
545        Ok(component)
546    }
547}
548
549/// An instrument name, a granularity, and a price component to get
550/// candlestick data for, e.g. `EUR_USD:S10:BM`.
551#[derive(Debug, Clone, PartialEq, Eq, Hash)]
552pub struct CandleSpecification {
553    /// The instrument to get candles for.
554    pub instrument: InstrumentName,
555    /// The candlestick granularity.
556    pub granularity: CandlestickGranularity,
557    /// The price component(s); OANDA defaults to mid when omitted.
558    pub price: Option<PricingComponent>,
559}
560
561impl CandleSpecification {
562    /// A specification for the given instrument and granularity (mid price).
563    pub fn new(instrument: impl Into<InstrumentName>, granularity: CandlestickGranularity) -> Self {
564        CandleSpecification {
565            instrument: instrument.into(),
566            granularity,
567            price: None,
568        }
569    }
570
571    /// Selects the price component(s) for this specification.
572    pub fn price(mut self, price: PricingComponent) -> Self {
573        self.price = Some(price);
574        self
575    }
576}
577
578impl std::fmt::Display for CandleSpecification {
579    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580        write!(f, "{}:{}", self.instrument, self.granularity)?;
581        if let Some(price) = &self.price {
582            write!(f, ":{price}")?;
583        }
584        Ok(())
585    }
586}