Skip to main content

egui_charts/model/
instrument.rs

1//! Instrument metadata reported by a feed or a search service.
2
3use super::{Timeframe, TimezoneId};
4use chrono::{NaiveDate, NaiveTime, TimeDelta, Weekday};
5use serde::{Deserialize, Serialize};
6
7/// Symbol type categories for instrument classification.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
9#[non_exhaustive]
10pub enum SymbolType {
11    #[default]
12    /// An equity security.
13    Stock,
14    /// A market index.
15    Index,
16    /// A currency pair.
17    Forex,
18    /// A futures contract.
19    Futures,
20    /// A cryptocurrency instrument.
21    Crypto,
22    /// A contract for difference.
23    Cfd,
24    /// A debt security.
25    Bond,
26    /// An investment fund.
27    Fund,
28    /// An options contract.
29    Option,
30    /// A commodity instrument.
31    Commodity,
32    /// An economic data series.
33    Economic,
34    /// A depositary receipt.
35    Dr,
36    /// A subscription right.
37    Right,
38    /// A warrant.
39    Warrant,
40    /// A host-defined expression over instruments.
41    Expression,
42    /// A spread between instruments.
43    Spread,
44}
45
46impl std::fmt::Display for SymbolType {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Self::Stock => write!(f, "stock"),
50            Self::Index => write!(f, "index"),
51            Self::Forex => write!(f, "forex"),
52            Self::Futures => write!(f, "futures"),
53            Self::Crypto => write!(f, "crypto"),
54            Self::Cfd => write!(f, "cfd"),
55            Self::Bond => write!(f, "bond"),
56            Self::Fund => write!(f, "fund"),
57            Self::Option => write!(f, "option"),
58            Self::Commodity => write!(f, "commodity"),
59            Self::Economic => write!(f, "economic"),
60            Self::Dr => write!(f, "dr"),
61            Self::Right => write!(f, "right"),
62            Self::Warrant => write!(f, "warrant"),
63            Self::Expression => write!(f, "expression"),
64            Self::Spread => write!(f, "spread"),
65        }
66    }
67}
68
69/// An optional service of a [`Datafeed`](crate::data::Datafeed), listed in
70/// [`FeedCapabilities::services`](crate::data::FeedCapabilities::services) and [`Instrument::services`].
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
72#[non_exhaustive]
73pub enum FeedService {
74    /// [`Datafeed::trades`](crate::data::Datafeed::trades).
75    Trades,
76    /// [`Datafeed::depth_history`](crate::data::Datafeed::depth_history).
77    DepthHistory,
78    /// [`Datafeed::options_volume`](crate::data::Datafeed::options_volume).
79    OptionsVolume,
80    /// [`Datafeed::option_expiries`](crate::data::Datafeed::option_expiries) and
81    /// [`Datafeed::option_chain`](crate::data::Datafeed::option_chain).
82    OptionChain,
83    /// [`Datafeed::option_exposure`](crate::data::Datafeed::option_exposure).
84    OptionExposure,
85    /// [`Datafeed::metrics`](crate::data::Datafeed::metrics) and
86    /// [`Datafeed::metric_history`](crate::data::Datafeed::metric_history).
87    Metrics,
88    /// [`Datafeed::price_targets`](crate::data::Datafeed::price_targets).
89    PriceTargets,
90    /// [`Datafeed::symbol_details`](crate::data::Datafeed::symbol_details).
91    SymbolDetails,
92    /// [`Datafeed::news`](crate::data::Datafeed::news).
93    News,
94    /// [`Datafeed::calendar`](crate::data::Datafeed::calendar).
95    Calendar,
96    /// [`Datafeed::screen`](crate::data::Datafeed::screen).
97    Screener,
98    /// [`Datafeed::hotlist`](crate::data::Datafeed::hotlist).
99    Hotlist,
100    /// Live executions through [`Sink::push_tick`](crate::data::Sink::push_tick).
101    LiveTrades,
102    /// Aggressor classification in [`Tick::side`](super::Tick::side) of supplied executions.
103    TradeSides,
104    /// Best bid and ask through [`Sink::push_quote`](crate::data::Sink::push_quote).
105    BestBidAsk,
106    /// Price-level depth through [`Sink::push_depth_snapshot`](crate::data::Sink::push_depth_snapshot)
107    /// and [`Sink::push_depth_delta`](crate::data::Sink::push_depth_delta).
108    Depth,
109    /// Individual resting orders through [`Sink::push_order_snapshot`](crate::data::Sink::push_order_snapshot)
110    /// and [`Sink::push_order_event`](crate::data::Sink::push_order_event).
111    OrderDepth,
112}
113
114/// What a feed or a search service reports about a symbol. Build it with [`Instrument::new`] and the `with_` setters;
115/// fields left out stay unknown.
116#[derive(Clone, Debug, PartialEq)]
117#[non_exhaustive]
118pub struct Instrument {
119    /// The symbol as the feed knows it.
120    pub symbol: String,
121    /// Full name, such as "Apple Inc.".
122    pub description: Option<String>,
123    /// Exchange or venue, such as "NASDAQ".
124    pub exchange: Option<String>,
125    /// URI of the symbol's logo for egui's installed image loaders; supply a square image. The
126    /// legend and symbol choosers draw it in a circle, and the symbol's first letter or digit
127    /// while it is unknown, loading or unreadable. A prefixed symbol such as `VENUE:ABC` shows V.
128    pub logo: Option<String>,
129    /// URI of the exchange's mark, for egui's installed image loaders. Symbol search shows the
130    /// exchange name alone while it is unknown, loading or unreadable.
131    pub exchange_logo: Option<String>,
132    /// Country of the listing, as an ISO 3166-1 alpha-2 code such as "US".
133    pub country: Option<String>,
134    /// Root shared by related instruments, such as "ES" for the contracts of a futures root or
135    /// one security listed on several exchanges. Symbol search lists the first instrument of a
136    /// root and offers the others under it.
137    pub root: Option<String>,
138    /// Instrument type.
139    pub symbol_type: Option<SymbolType>,
140    /// Smallest price increment: positive and finite. Consumers ignore any other value.
141    pub tick_size: Option<f64>,
142    /// Price represented by one pip: positive and finite. Unknown for instruments without pips.
143    pub pip_size: Option<f64>,
144    /// Decimals of price labels.
145    pub price_decimals: Option<u8>,
146    /// Decimals of volume readouts, including abbreviated K/M/B values.
147    pub volume_decimals: Option<u8>,
148    /// Currency of prices, such as "USD".
149    pub currency: Option<String>,
150    /// Host-supplied price unit, such as "USD/barrel"; no conversion is performed.
151    pub unit: Option<String>,
152    /// Exchange time zone.
153    pub time_zone: Option<TimezoneId>,
154    /// Regular trading hours, in exchange time.
155    pub session: Option<Session>,
156    /// OHLCV intervals supplied without aggregation. `None` means unspecified; an empty list
157    /// means no native bar history. Derived intervals remain available through host resampling.
158    pub native_timeframes: Option<Vec<Timeframe>>,
159    /// Intervals [`Datafeed::history`](crate::data::Datafeed::history) answers, native or
160    /// derived. `None` allows every interval. A chart does not ask the feed for any other
161    /// interval. Pass the list to `ui::top_toolbar::TimeframeSelector::set_supported_timeframes`
162    /// so that the interval picker offers only these.
163    pub supported_timeframes: Option<Vec<Timeframe>>,
164    /// Optional services supplied for this symbol, narrowing the feed's
165    /// [`services`](crate::data::FeedCapabilities::services). `None` defers to the feed's list;
166    /// an empty list supplies none. Use [`FeedCapabilities::declares`](crate::data::FeedCapabilities::declares)
167    /// to require explicit confirmation from either list while respecting both.
168    pub services: Option<Vec<FeedService>>,
169    /// How current the feed's data is. The legend marks delayed and end-of-day data.
170    pub data_status: Option<DataStatus>,
171    /// Whether the feed supplies dividend-adjusted history when
172    /// [`HistoryRequest::adjusted`](crate::data::HistoryRequest::adjusted) is set.
173    pub dividend_adjustment: bool,
174    /// Bar intervals the feed suggests for volume profile studies and drawings, by profile
175    /// length. A study whose source follows the chart, or a fixed range or anchored drawing
176    /// without executions, reads the shortest suggested interval shorter than the chart's that
177    /// serves its length. Empty, the default, suggests none; profiles then spread each chart
178    /// bar's volume over its range.
179    pub profile_resolutions: Vec<ProfileResolution>,
180}
181
182impl Instrument {
183    /// `symbol`, with everything else unknown.
184    pub fn new(symbol: impl Into<String>) -> Self {
185        Self {
186            symbol: symbol.into(),
187            description: None,
188            exchange: None,
189            logo: None,
190            exchange_logo: None,
191            country: None,
192            root: None,
193            symbol_type: None,
194            tick_size: None,
195            pip_size: None,
196            price_decimals: None,
197            volume_decimals: None,
198            currency: None,
199            unit: None,
200            time_zone: None,
201            session: None,
202            native_timeframes: None,
203            supported_timeframes: None,
204            services: None,
205            data_status: None,
206            dividend_adjustment: false,
207            profile_resolutions: Vec::new(),
208        }
209    }
210
211    /// Sets [`description`](Self::description).
212    pub fn with_description(mut self, description: impl Into<String>) -> Self {
213        self.description = Some(description.into());
214        self
215    }
216
217    /// Sets the price [`unit`](Self::unit).
218    pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
219        self.unit = Some(unit.into());
220        self
221    }
222
223    /// Sets [`exchange`](Self::exchange).
224    pub fn with_exchange(mut self, exchange: impl Into<String>) -> Self {
225        self.exchange = Some(exchange.into());
226        self
227    }
228
229    /// Sets [`logo`](Self::logo).
230    pub fn with_logo(mut self, uri: impl Into<String>) -> Self {
231        self.logo = Some(uri.into());
232        self
233    }
234
235    /// Sets [`exchange_logo`](Self::exchange_logo).
236    pub fn with_exchange_logo(mut self, uri: impl Into<String>) -> Self {
237        self.exchange_logo = Some(uri.into());
238        self
239    }
240
241    /// Sets [`country`](Self::country).
242    pub fn with_country(mut self, country: impl Into<String>) -> Self {
243        self.country = Some(country.into());
244        self
245    }
246
247    /// Sets [`root`](Self::root).
248    pub fn with_root(mut self, root: impl Into<String>) -> Self {
249        self.root = Some(root.into());
250        self
251    }
252
253    /// Sets [`symbol_type`](Self::symbol_type).
254    pub fn with_symbol_type(mut self, symbol_type: SymbolType) -> Self {
255        self.symbol_type = Some(symbol_type);
256        self
257    }
258
259    /// Sets [`tick_size`](Self::tick_size).
260    pub fn with_tick_size(mut self, tick_size: f64) -> Self {
261        self.tick_size = Some(tick_size);
262        self
263    }
264
265    /// Sets [`pip_size`](Self::pip_size).
266    pub fn with_pip_size(mut self, pip_size: f64) -> Self {
267        self.pip_size = Some(pip_size);
268        self
269    }
270
271    /// Sets [`price_decimals`](Self::price_decimals).
272    pub fn with_price_decimals(mut self, decimals: u8) -> Self {
273        self.price_decimals = Some(decimals);
274        self
275    }
276
277    /// Sets [`volume_decimals`](Self::volume_decimals).
278    pub fn with_volume_decimals(mut self, decimals: u8) -> Self {
279        self.volume_decimals = Some(decimals);
280        self
281    }
282
283    /// Sets [`currency`](Self::currency).
284    pub fn with_currency(mut self, currency: impl Into<String>) -> Self {
285        self.currency = Some(currency.into());
286        self
287    }
288
289    /// Sets [`time_zone`](Self::time_zone).
290    pub fn with_time_zone(mut self, time_zone: TimezoneId) -> Self {
291        self.time_zone = Some(time_zone);
292        self
293    }
294
295    /// Sets [`session`](Self::session).
296    pub fn with_session(mut self, session: Session) -> Self {
297        self.session = Some(session);
298        self
299    }
300
301    /// Sets [`native_timeframes`](Self::native_timeframes), reported by [`Datafeed::resolve`](crate::data::Datafeed::resolve).
302    pub fn with_native_timeframes(
303        mut self,
304        timeframes: impl IntoIterator<Item = Timeframe>,
305    ) -> Self {
306        self.native_timeframes = Some(timeframes.into_iter().collect());
307        self
308    }
309
310    /// Sets [`supported_timeframes`](Self::supported_timeframes).
311    pub fn with_supported_timeframes(
312        mut self,
313        timeframes: impl IntoIterator<Item = Timeframe>,
314    ) -> Self {
315        self.supported_timeframes = Some(timeframes.into_iter().collect());
316        self
317    }
318
319    /// Sets [`services`](Self::services).
320    pub fn with_services(mut self, services: impl IntoIterator<Item = FeedService>) -> Self {
321        self.services = Some(services.into_iter().collect());
322        self
323    }
324
325    /// Sets [`dividend_adjustment`](Self::dividend_adjustment).
326    pub fn with_dividend_adjustment(mut self, adjustment: bool) -> Self {
327        self.dividend_adjustment = adjustment;
328        self
329    }
330
331    /// Sets [`data_status`](Self::data_status).
332    pub fn with_data_status(mut self, status: DataStatus) -> Self {
333        self.data_status = Some(status);
334        self
335    }
336
337    /// Sets [`profile_resolutions`](Self::profile_resolutions).
338    pub fn with_profile_resolutions(
339        mut self,
340        resolutions: impl IntoIterator<Item = ProfileResolution>,
341    ) -> Self {
342        self.profile_resolutions = resolutions.into_iter().collect();
343        self
344    }
345}
346
347/// A bar interval a feed suggests for volume profile studies and drawings, for profiles up to a length.
348/// Report them in [`Instrument::profile_resolutions`].
349#[derive(Clone, Copy, Debug, PartialEq, Eq)]
350#[non_exhaustive]
351pub struct ProfileResolution {
352    /// Interval of the bars whose volume the profile distributes.
353    pub timeframe: Timeframe,
354    /// The longest profile this interval serves, from the open of the profile's first chart
355    /// bar to the close of its last. `None` serves profiles of any length.
356    pub max_span: Option<TimeDelta>,
357}
358
359impl ProfileResolution {
360    /// Suggests `timeframe` for profiles up to `max_span` long, or of any length for `None`.
361    pub fn new(timeframe: Timeframe, max_span: Option<TimeDelta>) -> Self {
362        Self {
363            timeframe,
364            max_span,
365        }
366    }
367
368    /// The shortest time interval shorter than `interval` that serves `span`, from the open of
369    /// the profile's first bar to the close of its last.
370    #[cfg(any(feature = "studies", feature = "drawings"))]
371    pub(crate) fn suggested(
372        resolutions: &[Self],
373        interval: Timeframe,
374        span: TimeDelta,
375    ) -> Option<Timeframe> {
376        resolutions
377            .iter()
378            .filter(|hint| {
379                !hint.timeframe.is_trade_based()
380                    && hint.timeframe.duration_ms() < interval.duration_ms()
381                    && hint.max_span.is_none_or(|max| span <= max)
382            })
383            .min_by_key(|hint| hint.timeframe.duration_ms())
384            .map(|hint| hint.timeframe)
385    }
386}
387
388/// How current a feed's data is.
389#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
390#[non_exhaustive]
391pub enum DataStatus {
392    /// Updates as the market trades.
393    Realtime,
394    /// Updates some time after the market trades, as exchanges require of many free feeds.
395    Delayed,
396    /// Updates once a day, after the close.
397    EndOfDay,
398}
399
400/// Trading hours: the windows in which an instrument trades.
401#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
402#[non_exhaustive]
403pub struct Session {
404    /// Regular windows. With no regular or extended windows, the session is always open, apart
405    /// from its holidays and the dates in [`overrides`](Self::overrides).
406    pub windows: Vec<SessionWindow>,
407    /// Pre-market and post-market windows in the exchange zone.
408    pub extended_windows: Vec<SessionWindow>,
409    /// Trading dates on which every window is closed, including overnight windows.
410    pub holidays: Vec<NaiveDate>,
411    /// Date-specific hours, such as early closes and exceptional openings. An override replaces
412    /// the weekly windows and any holiday of its date.
413    pub overrides: Vec<SessionOverride>,
414}
415
416impl Session {
417    /// Trading hours made of `windows`.
418    pub fn new(windows: impl IntoIterator<Item = SessionWindow>) -> Self {
419        Self {
420            windows: windows.into_iter().collect(),
421            extended_windows: Vec::new(),
422            holidays: Vec::new(),
423            overrides: Vec::new(),
424        }
425    }
426
427    /// Adds the extended windows beside the regular windows.
428    pub fn with_extended_windows(
429        mut self,
430        windows: impl IntoIterator<Item = SessionWindow>,
431    ) -> Self {
432        self.extended_windows = windows.into_iter().collect();
433        self
434    }
435
436    /// Closes every window on these trading dates.
437    pub fn with_holidays(mut self, holidays: impl IntoIterator<Item = NaiveDate>) -> Self {
438        self.holidays = holidays.into_iter().collect();
439        self
440    }
441
442    /// Replaces the hours of the dates in `overrides`. For a date listed twice the first applies.
443    pub fn with_overrides(mut self, overrides: impl IntoIterator<Item = SessionOverride>) -> Self {
444        self.overrides = overrides.into_iter().collect();
445        self
446    }
447
448    /// Drops the pre-market and post-market windows, weekly and date-specific.
449    pub(crate) fn clear_extended_windows(&mut self) {
450        self.extended_windows.clear();
451        for date in &mut self.overrides {
452            date.extended_windows.clear();
453        }
454    }
455}
456
457/// The hours of one trading date, in place of its weekly windows and holiday.
458#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
459#[non_exhaustive]
460pub struct SessionOverride {
461    /// The trading date, named by the day on which the windows close, as in
462    /// [`SessionWindow::days`].
463    pub date: NaiveDate,
464    /// Regular windows of the date; an early close is one window ending early.
465    pub windows: Vec<OverrideWindow>,
466    /// Pre-market and post-market windows of the date.
467    pub extended_windows: Vec<OverrideWindow>,
468}
469
470impl SessionOverride {
471    /// A closure: every window of `date` is closed. Add hours with
472    /// [`with_windows`](Self::with_windows) and
473    /// [`with_extended_windows`](Self::with_extended_windows).
474    pub fn new(date: NaiveDate) -> Self {
475        Self {
476            date,
477            windows: Vec::new(),
478            extended_windows: Vec::new(),
479        }
480    }
481
482    /// Sets the regular windows of the date.
483    pub fn with_windows(mut self, windows: impl IntoIterator<Item = OverrideWindow>) -> Self {
484        self.windows = windows.into_iter().collect();
485        self
486    }
487
488    /// Sets the extended windows of the date.
489    pub fn with_extended_windows(
490        mut self,
491        windows: impl IntoIterator<Item = OverrideWindow>,
492    ) -> Self {
493        self.extended_windows = windows.into_iter().collect();
494        self
495    }
496}
497
498/// One window of a [`SessionOverride`].
499#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
500#[non_exhaustive]
501pub struct OverrideWindow {
502    /// Exchange time.
503    pub open: NaiveTime,
504    /// Exchange time. At or before `open`, the window spans midnight.
505    pub close: NaiveTime,
506}
507
508impl OverrideWindow {
509    /// `open` to `close`.
510    pub fn new(open: NaiveTime, close: NaiveTime) -> Self {
511        Self { open, close }
512    }
513}
514
515/// One trading window, repeated on each of `days`.
516#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
517#[non_exhaustive]
518pub struct SessionWindow {
519    /// Trading days, named by the day on which the window closes.
520    pub days: Vec<Weekday>,
521    /// Exchange time.
522    pub open: NaiveTime,
523    /// Exchange time. At or before `open`, the window spans midnight.
524    pub close: NaiveTime,
525}
526
527impl SessionWindow {
528    /// `open` to `close` on each of `days`.
529    pub fn new(days: impl IntoIterator<Item = Weekday>, open: NaiveTime, close: NaiveTime) -> Self {
530        Self {
531            days: days.into_iter().collect(),
532            open,
533            close,
534        }
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn test_symbol_type_display() {
544        assert_eq!(SymbolType::Stock.to_string(), "stock");
545        assert_eq!(SymbolType::Crypto.to_string(), "crypto");
546        assert_eq!(SymbolType::Futures.to_string(), "futures");
547        assert_eq!(SymbolType::Forex.to_string(), "forex");
548    }
549}