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}