Skip to main content

egui_charts/data/
mod.rs

1//! Historical and streaming market data contracts, aggregation and order flow data.
2//!
3//! Implement [`Datafeed`] to connect a chart to your backend, or use the data
4//! containers and aggregation functions independently of a widget.
5//!
6//! # Connecting a feed
7//!
8//! Attach a feed with `ChartBuilder::with_feed`. Only [`Datafeed::history`] is
9//! required. The chart calls the feed on the UI thread; implementations should
10//! return promptly, answer each request once through [`Reply`] and push live updates
11//! into the [`Sink`] of a [`Subscription`]. Replies and pushes can come from another
12//! thread, runtime or browser task. The optional `http` adapter connects to a host's server;
13//! no market data provider is built in.
14//!
15//! [`Subscription::clock`] and [`HistoryRequest::clock`] carry the same bar boundaries
16//! so history and streaming aggregation agree. Symbol search is a separate
17//! [`SymbolSearch`](crate::services::SymbolSearch) service.
18//!
19//! # Order flow and derived data
20//!
21//! Push executions, quotes and book updates through [`Sink`]. [`Footprint`] holds
22//! executed volume at price; [`DepthHistory`] holds the book over time.
23//! [`ReplayDriver`] loads host recordings and drives subscriptions through one [`ReplayClock`].
24//! [`resample`] aggregates
25//! bars into a longer interval, and [`VolumeProfile`] describes volume by price.
26//!
27//! The optional feed methods provide auxiliary study data and records for host UI,
28//! including news, calendars, hotlists and screener rows. Unsupported requests use
29//! [`FeedError::NotSupported`].
30
31pub use crate::model::Bar;
32pub use crate::model::FeedService;
33pub use crate::model::Timeframe;
34use crate::model::{BarClock, Instrument, SymbolType, Tick};
35use chrono::{DateTime, Utc};
36use std::error::Error;
37use std::fmt;
38use std::time::Duration;
39
40pub(crate) mod depth;
41mod depth_history;
42mod footprint;
43#[cfg(feature = "http")]
44pub mod http;
45#[cfg_attr(
46    not(any(test, feature = "widget")),
47    expect(dead_code, reason = "only the chart widget consumes a feed")
48)]
49pub(crate) mod inbox;
50mod market;
51mod metrics;
52pub use metrics::{MetricPage, MetricPoint, MetricRequest, MetricSeries, MetricSubscription};
53#[cfg_attr(
54    not(any(test, feature = "widget", feature = "alert-engine")),
55    expect(dead_code, reason = "only charts and alerts consume a feed")
56)]
57pub(crate) mod merge;
58mod option_chain;
59mod options_volume;
60mod order_flow;
61mod orders;
62mod recording;
63mod replay;
64mod resample;
65mod study;
66mod targets;
67mod volume_profile;
68pub use depth_history::{DepthColumn, DepthHistory, DepthHistoryConfig};
69#[cfg(any(feature = "studies", feature = "detectors"))]
70pub(crate) use footprint::meets_threshold;
71pub use footprint::{Footprint, FootprintBar, FootprintLevel, NodeSensitivity};
72pub use market::{
73    CalendarEvent, CalendarKind, CalendarQuery, HotlistEntry, HotlistQuery, Importance, NewsQuery,
74    NewsStory, ScreenerColumn, ScreenerColumnSet, ScreenerFilter, ScreenerOp, ScreenerQuery,
75    ScreenerRow, ScreenerTextAnswer, ScreenerUniverse,
76};
77pub use option_chain::{
78    OptionChain, OptionChainQuery, OptionChainSink, OptionExposure, OptionExposureHandle,
79    OptionExposureKind, OptionExposureQuery, OptionExposureRow, OptionExposureSink, OptionKind,
80    OptionQuote,
81};
82#[cfg(feature = "studies")]
83pub(crate) use options_volume::OptionsVolumeSource;
84pub use options_volume::{OptionsVolume, OptionsVolumeQuery, OptionsVolumeSink};
85#[cfg_attr(
86    not(any(test, feature = "widget")),
87    expect(dead_code, reason = "only the chart widget consumes a feed")
88)]
89pub(crate) mod trades;
90
91pub use inbox::{Reply, Sink, aggregate_trades, aggregate_trades_with_tick_size};
92pub use order_flow::{BarBook, OrderFlowBars, OrderFlowInput, OrderFlowKind};
93pub use orders::{
94    OrderEvent, OrderFilter, OrderFilterUnavailable, OrderQueue, OrderSizeTier, OrderSizeTiers,
95    RestingOrder,
96};
97pub use recording::{RecordingReader, RecordingWriter};
98pub use replay::{MarketEvent, ReplayClock, ReplayDriver, ReplayEvent};
99#[cfg(feature = "studies")]
100pub(crate) use resample::resample_periods;
101pub use resample::{resample, resample_with_session};
102pub use study::{StudyData, StudyDataRequest};
103pub use targets::PriceTargets;
104pub use trades::{CombinedTrade, ConsumedOrder, combine_trades};
105pub use volume_profile::{
106    ProfileDisplay, ProfileSource, StudyProfile, VolumeProfile, VolumeProfileConfig,
107};
108
109/// `Send + Sync` on native targets. No bound on wasm32, where feeds hold JavaScript handles.
110#[cfg(not(target_arch = "wasm32"))]
111pub trait MaybeSend: Send + Sync {}
112#[cfg(not(target_arch = "wasm32"))]
113impl<T: Send + Sync + ?Sized> MaybeSend for T {}
114/// `Send + Sync` on native targets. No bound on wasm32, where feeds hold JavaScript handles.
115#[cfg(target_arch = "wasm32")]
116pub trait MaybeSend {}
117#[cfg(target_arch = "wasm32")]
118impl<T: ?Sized> MaybeSend for T {}
119
120/// Source of instrument metadata, history and live updates for a chart, given to it with
121/// `ChartBuilder::with_feed`.
122///
123/// Methods are called on the UI thread and must return promptly. Do the work elsewhere and answer
124/// through the [`Reply`] or [`Sink`] you were given, from any thread, runtime or browser task.
125///
126/// ```
127/// use egui_charts::data::{Datafeed, HistoryPage, HistoryRequest, Reply};
128/// use egui_charts::model::Bar;
129///
130/// /// Bars held in memory, oldest first. Answers inline, without live updates.
131/// struct InMemory(Vec<Bar>);
132///
133/// impl Datafeed for InMemory {
134///     fn history(&self, request: HistoryRequest, reply: Reply<HistoryPage>) {
135///         let before = |bar: &Bar| request.before.is_none_or(|before| bar.time < before);
136///         let end = self.0.partition_point(before);
137///         let since = |bar: &Bar| request.since.is_some_and(|since| bar.time < since);
138///         let first = self.0.partition_point(since).min(end);
139///         let start = end.saturating_sub(request.count).max(first);
140///         reply.send(Ok(HistoryPage::new(self.0[start..end].to_vec(), start == 0)));
141///     }
142/// }
143/// ```
144pub trait Datafeed: MaybeSend + 'static {
145    /// What the feed offers as a whole, before any symbol is resolved: the intervals, exchanges
146    /// and symbol classes its selectors may offer, and the optional services it supplies. The
147    /// chart asks once when it attaches the feed and keeps the answer in
148    /// `Chart::feed_capabilities` (requires `widget`). The default
149    /// answers [`FeedCapabilities::default`], which reports nothing and so restricts nothing.
150    fn capabilities(&self, reply: Reply<FeedCapabilities>) {
151        reply.send(Ok(FeedCapabilities::default()));
152    }
153
154    /// Describes `symbol`. The default reports [`Instrument::new`] of the symbol.
155    /// Fill [`Instrument::profile_resolutions`] to suggest the bars volume profile studies and
156    /// drawings read.
157    fn resolve(&self, symbol: &str, reply: Reply<Instrument>) {
158        reply.send(Ok(Instrument::new(symbol)));
159    }
160
161    /// Describes `symbol` with its prices converted to `conversion`, the currency or unit the
162    /// host chose with `Chart::set_conversion` (requires `widget`). Label the
163    /// instrument with the converted [`currency`](Instrument::currency) and
164    /// [`unit`](Instrument::unit). The library converts nothing: the default answers
165    /// [`FeedError::NotSupported`], which shows no data rather than unconverted data under a
166    /// converted choice.
167    fn resolve_converted(&self, symbol: &str, conversion: &str, reply: Reply<Instrument>) {
168        let _ = (symbol, conversion);
169        reply.send(Err(FeedError::NotSupported));
170    }
171
172    /// Up to `request.count` bars of the range `[request.since, request.before)`, the newest ones
173    /// when there are more. Either end left `None` is unbounded: without `before` the range
174    /// reaches the latest bar.
175    fn history(&self, request: HistoryRequest, reply: Reply<HistoryPage>);
176
177    /// Starts pushing live updates into `subscription.sink` until `unsubscribe`. The default does
178    /// nothing, which gives a history-only chart.
179    fn subscribe(&self, subscription: Subscription) {
180        let _ = subscription;
181    }
182
183    /// Stops what `subscribe` started. Pushes that arrive afterwards are ignored. The default does
184    /// nothing; a producer can then poll [`Sink::is_active`] to know when to stop.
185    ///
186    /// Also called when the chart is dropped, which an application may do while egui holds the
187    /// lock of its `Context`, for example inside `Context::input` or `Ui::input`. A push into a
188    /// [`Sink`] or an answer through a [`Reply`] may wake a chart, and waking waits for that lock
189    /// until the application's closure returns. So:
190    ///
191    /// - Return without waiting for a thread that pushes or answers: do not join it, and do not
192    ///   take a lock it holds while it pushes or answers.
193    /// - Answer or drop here only the replies of the chart that held `id`, and push into no other
194    ///   subscription's `Sink`: another chart would be woken on this thread, which holds the
195    ///   lock. A feed that cannot tell which chart a `Reply` is for keeps its replies and answers
196    ///   them as usual; an answer for a dropped chart is never read.
197    ///
198    /// When the chart held the last reference to the feed, the feed's own `Drop` runs right
199    /// after this call and must not wait for such a thread either.
200    fn unsubscribe(&self, id: SubscriptionId) {
201        let _ = id;
202    }
203
204    /// Up to `request.count` trades of the range `[request.since, request.before)`, the newest
205    /// ones when there are more, so order flow survives a reload and follows the view back in
206    /// time. The default answers [`FeedError::NotSupported`].
207    fn trades(&self, request: TradeRequest, reply: Reply<TradePage>) {
208        let _ = request;
209        reply.send(Err(FeedError::NotSupported));
210    }
211
212    /// Push complete options-volume snapshots for the underlying and inclusive expiry range.
213    /// Each push replaces the previous figures, including an empty snapshot. Sum contracts
214    /// across the requested expiries at each strike. Stop when `sink.is_active()` is false.
215    /// The default supplies none; the library never fetches options data itself.
216    fn options_volume(&self, query: OptionsVolumeQuery, sink: OptionsVolumeSink) {
217        let _ = (query, sink);
218    }
219
220    /// Listed expiry dates for `underlying`, in ascending order with no duplicates.
221    /// The default answers [`FeedError::NotSupported`]. Both options methods are declared by
222    /// [`FeedService::OptionChain`]; the host supplies the data.
223    fn option_expiries(&self, underlying: &str, reply: Reply<Vec<chrono::NaiveDate>>) {
224        let _ = underlying;
225        reply.send(Err(FeedError::NotSupported));
226    }
227
228    /// Push complete snapshots of the listed contracts in `query`'s inclusive expiry range.
229    /// Without [`as_of`](OptionChainQuery::as_of), keep pushing until the sink is inactive.
230    /// With it, push one snapshot for that instant or report [`FeedError::NotSupported`].
231    /// Unknown figures stay absent. See [`OptionQuote`] for units and [`OptionChainSink::push`]
232    /// for validation. The default reports [`FeedError::NotSupported`] to the sink.
233    ///
234    /// ```
235    /// use chrono::{NaiveDate, TimeZone, Utc};
236    /// use egui_charts::data::{
237    ///     Datafeed, HistoryPage, HistoryRequest, OptionChain, OptionChainQuery,
238    ///     OptionChainSink, OptionKind, OptionQuote, Reply,
239    /// };
240    ///
241    /// struct Feed;
242    /// impl Datafeed for Feed {
243    ///     fn history(&self, _: HistoryRequest, reply: Reply<HistoryPage>) {
244    ///         reply.send(Ok(HistoryPage::default()));
245    ///     }
246    ///     fn option_expiries(&self, _: &str, reply: Reply<Vec<NaiveDate>>) {
247    ///         reply.send(Ok(vec![
248    ///             NaiveDate::from_ymd_opt(2026, 12, 18).unwrap(),
249    ///             NaiveDate::from_ymd_opt(2027, 1, 15).unwrap(),
250    ///         ]));
251    ///     }
252    ///     fn option_chain(&self, query: OptionChainQuery, sink: OptionChainSink) {
253    ///         let time = Utc.with_ymd_and_hms(2026, 11, 2, 12, 0, 0).unwrap();
254    ///         let mut row = OptionQuote::new(
255    ///             "SAMPLE-261218-C100", *query.expiries.start(), 100.0, OptionKind::Call, time,
256    ///         );
257    ///         row.bid = Some(3.5);
258    ///         row.open_interest = Some(1200.0);
259    ///         row.gamma = Some(0.04);
260    ///         sink.push(vec![row]).unwrap();
261    ///     }
262    /// }
263    /// let expiry = NaiveDate::from_ymd_opt(2026, 12, 18).unwrap();
264    /// let chain = OptionChain::new(
265    ///     &Feed, OptionChainQuery::new("SAMPLE", expiry..=expiry), || {},
266    /// ).unwrap();
267    /// let rows = chain.rows();
268    /// assert_eq!(rows.len(), 1);
269    /// assert_eq!(rows[0].bid, Some(3.5));
270    /// assert_eq!(rows[0].open_interest, Some(1200.0));
271    /// assert_eq!(rows[0].gamma, Some(0.04));
272    /// assert_eq!(rows[0].ask, None);
273    /// assert_eq!(rows[0].volume, None);
274    /// assert_eq!(chain.error(), None);
275    /// ```
276    fn option_chain(&self, query: OptionChainQuery, sink: OptionChainSink) {
277        let _ = query;
278        sink.report_error(FeedError::NotSupported);
279    }
280
281    /// Push complete exposure snapshots for the underlying, inclusive expiry range and kind.
282    /// Without [`as_of`](OptionExposureQuery::as_of), keep pushing until the sink is inactive;
283    /// with it, answer once for that instant or report [`FeedError::NotSupported`] (the default).
284    /// Declared by [`FeedService::OptionExposure`]. The host supplies the data and model.
285    ///
286    /// [`OptionExposureHandle::snapshot_or_gamma`] prefers the supplied snapshot unchanged over
287    /// computed gamma. See [`OptionExposureSink::push`] for atomic validation and replacement.
288    fn option_exposure(&self, query: OptionExposureQuery, sink: OptionExposureSink) {
289        let _ = query;
290        sink.report_error(FeedError::NotSupported);
291    }
292
293    /// One page of recorded depth on the requested grid. The default answers
294    /// [`FeedError::NotSupported`]; live-only feeds need not implement this method.
295    fn depth_history(&self, request: DepthRequest, reply: Reply<DepthPage>) {
296        let _ = request;
297        reply.send(Err(FeedError::NotSupported));
298    }
299
300    /// Metrics offered for `symbol`, in display order. The default offers none.
301    /// The host publishes the values; the library computes no instrument metrics.
302    fn metrics(&self, symbol: &str, reply: Reply<Vec<MetricSeries>>) {
303        let _ = symbol;
304        reply.send(Ok(Vec::new()));
305    }
306
307    /// Metric history. The default answers [`FeedError::NotSupported`].
308    fn metric_history(&self, request: MetricRequest, reply: Reply<MetricPage>) {
309        let _ = request;
310        reply.send(Err(FeedError::NotSupported));
311    }
312
313    /// Start publishing metric points until [`Self::unsubscribe`]. The default does nothing.
314    fn subscribe_metric(&self, subscription: MetricSubscription) {
315        let _ = subscription;
316    }
317
318    /// Analyst price targets for `symbol`. The default answers [`FeedError::NotSupported`],
319    /// which leaves the chart without targets and shows no error.
320    fn price_targets(&self, symbol: &str, reply: Reply<crate::data::PriceTargets>) {
321        let _ = symbol;
322        reply.send(Err(FeedError::NotSupported));
323    }
324
325    /// Key statistics and the company or contract profile of `symbol`, for the symbol details
326    /// panel. The default answers [`FeedError::NotSupported`], and the panel then shows neither.
327    fn symbol_details(&self, symbol: &str, reply: Reply<SymbolDetails>) {
328        let _ = symbol;
329        reply.send(Err(FeedError::NotSupported));
330    }
331
332    /// Up to `query.limit` stories, newest first, for the news and highlights panels. Answer
333    /// [`FeedError::NotSupported`] (the default) to have the panels say the feed has none.
334    fn news(&self, query: NewsQuery, reply: Reply<Vec<NewsStory>>) {
335        let _ = query;
336        reply.send(Err(FeedError::NotSupported));
337    }
338
339    /// The events of one calendar that pass `query`'s filters, in time order, for the calendar
340    /// panel. Answer [`FeedError::NotSupported`] (the default) to have the panel say the feed has
341    /// none.
342    fn calendar(&self, query: CalendarQuery, reply: Reply<Vec<CalendarEvent>>) {
343        let _ = query;
344        reply.send(Err(FeedError::NotSupported));
345    }
346
347    /// Up to `query.limit` symbols that meet every filter of `query`, sorted as it asks, each
348    /// with the values of its columns, for the screener. The feed does the screening. Answer
349    /// [`FeedError::InvalidData`] for a filter it cannot apply, and [`FeedError::NotSupported`]
350    /// (the default) to have the panel say the feed has none.
351    fn screen(&self, query: ScreenerQuery, reply: Reply<Vec<ScreenerRow>>) {
352        let _ = query;
353        reply.send(Err(FeedError::NotSupported));
354    }
355
356    /// Up to `query.limit` symbols of the hotlist `query.list`, in the list's order, for the
357    /// hotlists panel. Answer [`FeedError::NotSupported`] (the default) to have the panel say the
358    /// feed has none.
359    fn hotlist(&self, query: HotlistQuery, reply: Reply<Vec<HotlistEntry>>) {
360        let _ = query;
361        reply.send(Err(FeedError::NotSupported));
362    }
363
364    /// As [`screen`](Self::screen), for a query whose `text` describes the screen in words. The
365    /// feed interprets the text with the other criteria and answers the rows with an explanation
366    /// of how it understood them. Answer [`FeedError::NotSupported`] (the default) to have the
367    /// panel say the feed takes no descriptions, and [`FeedError::InvalidData`] for one it cannot
368    /// interpret.
369    fn screen_text(&self, query: ScreenerQuery, reply: Reply<ScreenerTextAnswer>) {
370        let _ = query;
371        reply.send(Err(FeedError::NotSupported));
372    }
373
374    /// The order book of subscription `id` was lost to a gap, an unusable update or a book deeper
375    /// than the limit: push a new snapshot with [`Sink::push_depth_snapshot`], and keep pushing
376    /// deltas while it is fetched. The snapshot may be older than the latest delta: the chart
377    /// holds the deltas pushed while the book waits and applies those numbered above the
378    /// snapshot. Called once per loss: after an unusable snapshot the chart does not ask
379    /// again, and the book stays empty until the feed pushes a usable one or the subscription
380    /// restarts. The default does nothing, for feeds without depth.
381    fn request_depth_snapshot(&self, id: SubscriptionId) {
382        let _ = id;
383    }
384}
385
386/// What a feed offers as a whole, answered by [`Datafeed::capabilities`]. Each list distinguishes
387/// `None`, which the feed does not report and so restricts nothing, from `Some` of an empty
388/// list, which it reports as offering none. Use [`declares`](Self::declares) when a service
389/// needs explicit confirmation.
390#[derive(Clone, Debug, Default, PartialEq)]
391#[non_exhaustive]
392pub struct FeedCapabilities {
393    /// Intervals [`Datafeed::history`] answers for at least some symbol. A symbol's own
394    /// [`supported_timeframes`](Instrument::supported_timeframes) narrows them further.
395    pub timeframes: Option<Vec<Timeframe>>,
396    /// Exchanges whose symbols the feed serves.
397    pub exchanges: Option<Vec<String>>,
398    /// Classes of symbol the feed serves.
399    pub symbol_types: Option<Vec<SymbolType>>,
400    /// Optional services the feed supplies for at least some symbol. A symbol's own
401    /// [`services`](Instrument::services) narrows them further. Services absent from a
402    /// reported list are unavailable.
403    pub services: Option<Vec<FeedService>>,
404}
405
406impl FeedCapabilities {
407    /// Reports no capability, which restricts nothing.
408    pub fn new() -> Self {
409        Self::default()
410    }
411
412    /// Sets [`timeframes`](Self::timeframes).
413    pub fn with_timeframes(mut self, timeframes: impl IntoIterator<Item = Timeframe>) -> Self {
414        self.timeframes = Some(timeframes.into_iter().collect());
415        self
416    }
417
418    /// Sets [`exchanges`](Self::exchanges).
419    pub fn with_exchanges(mut self, exchanges: impl IntoIterator<Item = String>) -> Self {
420        self.exchanges = Some(exchanges.into_iter().collect());
421        self
422    }
423
424    /// Sets [`symbol_types`](Self::symbol_types).
425    pub fn with_symbol_types(mut self, types: impl IntoIterator<Item = SymbolType>) -> Self {
426        self.symbol_types = Some(types.into_iter().collect());
427        self
428    }
429
430    /// Sets [`services`](Self::services).
431    pub fn with_services(mut self, services: impl IntoIterator<Item = FeedService>) -> Self {
432        self.services = Some(services.into_iter().collect());
433        self
434    }
435
436    /// Whether to offer the controls of `service`: it is listed, or the feed reports no list.
437    pub fn offers(&self, service: FeedService) -> bool {
438        self.services
439            .as_ref()
440            .is_none_or(|services| services.contains(&service))
441    }
442
443    /// Whether `service` is explicitly reported for `instrument`. Its own list takes precedence;
444    /// without one, the feed's list applies. A reported feed list must also include the service.
445    /// With neither list reported, this returns false, unlike [`offers`](Self::offers).
446    pub fn declares(&self, service: FeedService, instrument: &Instrument) -> bool {
447        self.offers(service)
448            && instrument
449                .services
450                .as_ref()
451                .or(self.services.as_ref())
452                .is_some_and(|services| services.contains(&service))
453    }
454}
455
456/// A request for one page of history.
457#[derive(Clone, Debug, PartialEq)]
458#[non_exhaustive]
459pub struct HistoryRequest {
460    /// Instrument identifier used by the host.
461    pub symbol: String,
462    /// Bar interval requested from the feed.
463    pub timeframe: Timeframe,
464    /// Calendar boundaries selected by the chart, shared by historical and streaming aggregation.
465    pub clock: BarClock,
466    /// Only bars that open strictly before this instant: the end of the range is exclusive.
467    /// `None` is the initial load and asks for the latest bars.
468    pub before: Option<DateTime<Utc>>,
469    /// Only bars that open at or after this instant: the start of the range is inclusive. With
470    /// `before` it bounds the range as `[since, before)`; answer the newest `count` bars of it.
471    /// The chart sets it when it needs bars back to a time it does not hold, such as for
472    /// `Chart::scroll_to_time` (requires `widget`). `None` bounds the range
473    /// by `count` alone.
474    pub since: Option<DateTime<Utc>>,
475    /// Bars wanted. Fewer is fine; the chart asks again as the user scrolls back.
476    pub count: usize,
477    /// Include the instrument's extended trading hours, as well as regular hours.
478    pub extended_hours: bool,
479    /// Request dividend-adjusted bars when the instrument supports them. An unsupported feed
480    /// returns unadjusted bars; [`Instrument::dividend_adjustment`] advertises the choice.
481    pub adjusted: bool,
482    /// The currency or unit the host chose, in which the bars are wanted, as
483    /// [`Datafeed::resolve_converted`] described the symbol. `None` asks for the instrument's own.
484    pub conversion: Option<String>,
485}
486
487/// One page of history. A page without bars ends scroll-back like `end_of_history` does, unless it
488/// names where older bars continue with [`continue_before`](Self::continue_before), so a feed that
489/// pages by time window can answer an empty window such as a weekend.
490///
491/// Trades streamed through [`Sink::push_tick`] extend the page's newest bar even when they arrive
492/// first. That bar must count only trades the stream did not send, or the feed must hold those
493/// trades until the page is answered. Completed page bars replace bars built from ticks.
494#[derive(Clone, Debug, Default, PartialEq)]
495#[non_exhaustive]
496pub struct HistoryPage {
497    /// Any order, each stamped with its open or any instant inside it: a bar stamped with its
498    /// close time lands in the next bar. A bar of a day or longer covers a calendar period of
499    /// [`HistoryRequest::clock`], such as a day, a week from Monday or a calendar
500    /// month, in UTC by default: stamp it inside that period, for example at its local midnight. A
501    /// session open on the evening before can fall in the period of the bar before, and one of
502    /// the two bars is then dropped. The chart sorts, deduplicates and validates them.
503    pub bars: Vec<Bar>,
504    /// Nothing exists before the oldest returned bar (or before `request.before` when `bars` is
505    /// empty).
506    pub end_of_history: bool,
507    /// On a page without bars: the window had none but older bars exist, and the chart asks
508    /// again for the bars before this instant. It must be earlier than `request.before`, or
509    /// the chart stops paging and reports [`FeedError::InvalidData`] instead of repeating the
510    /// request. Ignored on a page with bars, which continues before its oldest bar.
511    pub continue_before: Option<DateTime<Utc>>,
512}
513
514impl HistoryPage {
515    /// A page of `bars`, the last one when `end_of_history`.
516    pub fn new(bars: Vec<Bar>, end_of_history: bool) -> Self {
517        Self {
518            bars,
519            end_of_history,
520            continue_before: None,
521        }
522    }
523
524    /// Sets [`continue_before`](Self::continue_before).
525    pub fn with_continue_before(mut self, before: DateTime<Utc>) -> Self {
526        self.continue_before = Some(before);
527        self
528    }
529}
530
531/// A request for one page of trades.
532#[derive(Clone, Debug, PartialEq)]
533#[non_exhaustive]
534pub struct TradeRequest {
535    /// Instrument identifier used by the host.
536    pub symbol: String,
537    /// Only trades strictly before this instant: the end of the range is exclusive. `None` is the
538    /// initial load and asks for the latest trades.
539    pub before: Option<DateTime<Utc>>,
540    /// Only trades at or after this instant: the start of the range is inclusive; see
541    /// [`HistoryRequest::since`]. `None` bounds the range by `count` alone.
542    pub since: Option<DateTime<Utc>>,
543    /// Trades wanted. Fewer is fine.
544    pub count: usize,
545    /// The currency or unit the host chose, in which the prices are wanted; see
546    /// [`HistoryRequest::conversion`].
547    pub conversion: Option<String>,
548}
549
550/// One page of trades. A page without trades ends scroll-back like `end_of_history` does, unless
551/// it names where older trades continue with [`continue_before`](Self::continue_before).
552#[derive(Clone, Debug, Default, PartialEq)]
553#[non_exhaustive]
554pub struct TradePage {
555    /// Oldest first; trades sharing a time keep their order. They feed order flow only: bars come
556    /// from [`Datafeed::history`]. The chart drops invalid trades and takes as many as its trade
557    /// store has room for, up to the oldest trade it holds: later ones are left to the live stream.
558    /// A trade with an [`id`](Tick::id) the chart already holds is dropped, so history and live
559    /// trades join exactly when both carry ids. Without ids, only trades older than every held
560    /// trade are taken, and live trades at or before the newest trade a page added are dropped:
561    /// trades at the boundary instant may be lost, never counted twice.
562    pub trades: Vec<Tick>,
563    /// Nothing exists before the oldest returned trade.
564    pub end_of_history: bool,
565    /// On a page without trades: older trades exist, and the chart asks again for those before
566    /// this instant, which must be earlier than `request.before`; see
567    /// [`HistoryPage::continue_before`].
568    pub continue_before: Option<DateTime<Utc>>,
569}
570
571impl TradePage {
572    /// A page of `trades`, the last one when `end_of_history`.
573    pub fn new(trades: Vec<Tick>, end_of_history: bool) -> Self {
574        Self {
575            trades,
576            end_of_history,
577            continue_before: None,
578        }
579    }
580
581    /// Sets [`continue_before`](Self::continue_before).
582    pub fn with_continue_before(mut self, before: DateTime<Utc>) -> Self {
583        self.continue_before = Some(before);
584        self
585    }
586}
587
588/// One page of recorded depth for a symbol, using the same price and time grid as live columns.
589#[derive(Clone, Debug, PartialEq)]
590#[non_exhaustive]
591pub struct DepthRequest {
592    /// Instrument requested.
593    pub symbol: String,
594    /// Columns opening strictly before this instant: the end of the range is exclusive. `None`
595    /// is the initial load and asks for the latest columns.
596    pub before: Option<DateTime<Utc>>,
597    /// Columns opening at or after this instant: the start of the range is inclusive; see
598    /// [`HistoryRequest::since`]. `None` bounds the range by `count` alone.
599    pub since: Option<DateTime<Utc>>,
600    /// Maximum columns wanted.
601    pub count: usize,
602    /// Cadence, row grouping and limits of the receiving store.
603    pub config: DepthHistoryConfig,
604    /// Price of one tick, finite and positive.
605    pub tick_size: f64,
606    /// The currency or unit the host chose, in which the prices are wanted; see
607    /// [`HistoryRequest::conversion`].
608    pub conversion: Option<String>,
609}
610
611impl DepthRequest {
612    /// Requests the latest `count` columns using `config` and the instrument's `tick_size`.
613    pub fn new(
614        symbol: impl Into<String>,
615        count: usize,
616        config: DepthHistoryConfig,
617        tick_size: f64,
618    ) -> Self {
619        Self {
620            symbol: symbol.into(),
621            before: None,
622            since: None,
623            count,
624            config,
625            tick_size,
626            conversion: None,
627        }
628    }
629}
630
631/// Recorded depth columns. Empty pages end paging, including when `end_of_history` is false,
632/// unless they name where older columns continue with [`continue_before`](Self::continue_before).
633#[derive(Clone, Debug, Default, PartialEq)]
634#[non_exhaustive]
635pub struct DepthPage {
636    /// Columns on the requested grid; held live columns win on overlap. Gaps remain gaps.
637    pub columns: Vec<DepthColumn>,
638    /// Nothing exists before the oldest returned column.
639    pub end_of_history: bool,
640    /// On a page without columns: older columns exist, and the chart asks again for those
641    /// before this instant, which must be earlier than `request.before`; see
642    /// [`HistoryPage::continue_before`].
643    pub continue_before: Option<DateTime<Utc>>,
644}
645
646impl DepthPage {
647    /// A page of `columns`, the last one when `end_of_history`.
648    pub fn new(columns: Vec<DepthColumn>, end_of_history: bool) -> Self {
649        Self {
650            columns,
651            end_of_history,
652            continue_before: None,
653        }
654    }
655
656    /// Sets [`continue_before`](Self::continue_before).
657    pub fn with_continue_before(mut self, before: DateTime<Utc>) -> Self {
658        self.continue_before = Some(before);
659        self
660    }
661}
662
663/// Facts about one symbol as labelled values, already formatted for display, in the order to
664/// show them. Supply only what is known: the panel shows a section only when it has values.
665#[derive(Clone, Debug, Default, PartialEq)]
666#[non_exhaustive]
667pub struct SymbolDetails {
668    /// Key statistics, such as `("Market capitalization", "2.95T USD")`.
669    pub statistics: Vec<(String, String)>,
670    /// The company or contract profile, such as `("Sector", "Technology")`.
671    pub profile: Vec<(String, String)>,
672    /// A page about the instrument, linked beside its name.
673    pub url: Option<String>,
674    /// What the instrument is, such as `"Technology • Software"`, shown under its name.
675    pub category: Option<String>,
676}
677
678impl SymbolDetails {
679    /// Details made of `statistics` and `profile`.
680    pub fn new(statistics: Vec<(String, String)>, profile: Vec<(String, String)>) -> Self {
681        Self {
682            statistics,
683            profile,
684            url: None,
685            category: None,
686        }
687    }
688
689    /// Sets [`url`](Self::url).
690    pub fn with_url(mut self, url: impl Into<String>) -> Self {
691        self.url = Some(url.into());
692        self
693    }
694
695    /// Sets [`category`](Self::category).
696    pub fn with_category(mut self, category: impl Into<String>) -> Self {
697        self.category = Some(category.into());
698        self
699    }
700}
701
702/// A live subscription: push its updates into `sink` until [`Datafeed::unsubscribe`] gets `id`.
703#[non_exhaustive]
704pub struct Subscription {
705    /// Identifier passed back to the feed when unsubscribing.
706    pub id: SubscriptionId,
707    /// Instrument identifier used by the host.
708    pub symbol: String,
709    /// Bar interval requested from the feed.
710    pub timeframe: Timeframe,
711    /// The same calendar boundaries supplied to history requests.
712    pub clock: BarClock,
713    /// Include the instrument's extended trading hours, as well as regular hours.
714    pub extended_hours: bool,
715    /// The currency or unit the host chose, in which the updates are wanted; see
716    /// [`HistoryRequest::conversion`].
717    pub conversion: Option<String>,
718    /// Destination for updates; stop publishing when it becomes inactive.
719    pub sink: Sink,
720}
721
722/// Identifies a subscription. Unique within the process, so one feed can serve many charts.
723#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
724pub struct SubscriptionId(u64);
725
726/// Connection state of a live subscription.
727#[derive(Clone, Copy, Debug, PartialEq, Eq)]
728#[non_exhaustive]
729pub enum FeedStatus {
730    /// A connection attempt is in progress.
731    Connecting,
732    /// The feed reports live data.
733    Live,
734    /// The connection is closed.
735    Disconnected,
736}
737
738/// Why a feed or another service could not answer.
739#[derive(Clone, Debug, PartialEq)]
740#[non_exhaustive]
741pub enum FeedError {
742    /// The feed or service does not offer this request.
743    NotSupported,
744    /// The service could not find the requested instrument.
745    SymbolNotFound(String),
746    /// The service has limited the request rate.
747    RateLimited {
748        /// Suggested delay before retrying, when supplied by the service.
749        retry_after: Option<Duration>,
750    },
751    /// A transport error with a service-supplied message.
752    Network(String),
753    /// Invalid input or data, described by the service.
754    InvalidData(String),
755    /// The feed or service dropped a [`Reply`] without answering.
756    Unanswered,
757    /// A service error outside the other categories.
758    Other(String),
759}
760
761impl FeedError {
762    /// Whether retrying the same request can succeed without changing its inputs.
763    pub fn is_recoverable(&self) -> bool {
764        matches!(
765            self,
766            Self::Network(_) | Self::RateLimited { .. } | Self::Unanswered
767        )
768    }
769}
770
771impl fmt::Display for FeedError {
772    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773        match self {
774            Self::NotSupported => f.write_str("not supported"),
775            Self::SymbolNotFound(symbol) => write!(f, "symbol not found: {symbol}"),
776            Self::RateLimited {
777                retry_after: Some(after),
778            } => write!(f, "rate limited, retry after {after:?}"),
779            Self::RateLimited { retry_after: None } => f.write_str("rate limited"),
780            Self::Network(message) => write!(f, "network error: {message}"),
781            Self::InvalidData(message) => write!(f, "invalid data: {message}"),
782            Self::Unanswered => f.write_str("no answer"),
783            Self::Other(message) => f.write_str(message),
784        }
785    }
786}
787
788impl Error for FeedError {}
789
790/// What the chart knows about one series' feed, for the application to show or log.
791#[derive(Clone, Debug, Default, PartialEq)]
792#[non_exhaustive]
793pub struct FeedState {
794    /// Last status pushed by the feed. `None` without a live subscription.
795    pub status: Option<FeedStatus>,
796    /// Catalog-change notifications received for this subscription, for host search refreshes.
797    pub symbol_catalog_revision: u64,
798    /// Whether a historical page is currently being requested.
799    pub loading_history: bool,
800    /// Whether the feed has reported that no older history remains.
801    pub history_complete: bool,
802    /// Whether no older trades remain to be loaded: the feed reported the end, answered nothing
803    /// new, does not offer trades or the trade store is full.
804    pub trade_history_complete: bool,
805    /// Whether no older depth columns remain to be loaded.
806    pub depth_history_complete: bool,
807    /// Most recent feed error, when present.
808    pub last_error: Option<FeedError>,
809    /// Bars and ticks dropped by validation: non-finite values, a high or low on the wrong side of
810    /// the open and close, negative volume, a tick or history bar that would overflow its slot's
811    /// volume, a time too far ahead of the feed's clock (see `Sink::set_server_time`), or pending
812    /// bar slots evicted after overflow (also reported in `last_error`).
813    pub rejected_bars: u64,
814    /// Trades older than the last accepted execution or forming bar, dropped.
815    pub late_ticks: u64,
816    /// Trades whose id is among those of the last trades the subscription folded, as many as its
817    /// trade store holds, dropped: a feed replaying its last trades after a reconnect.
818    pub repeated_ticks: u64,
819    /// Order book snapshots and deltas dropped by validation: a non-finite price or size, a
820    /// negative size. Each one clears the book until the feed pushes a new snapshot.
821    pub rejected_depth: u64,
822}
823
824#[cfg(test)]
825mod tests {
826    use super::*;
827
828    #[test]
829    fn declared_services_require_a_report_and_respect_both_lists() {
830        use FeedService::{Depth, OrderDepth, Trades};
831
832        for (feed, symbol, service, expected) in [
833            (None, None, Depth, false),
834            (Some(vec![Depth]), None, Depth, true),
835            (Some(vec![Depth]), Some(vec![]), Depth, false),
836            (None, Some(vec![Depth]), Depth, true),
837            (Some(vec![Trades]), Some(vec![Depth]), Depth, false),
838            (
839                Some(vec![Depth, OrderDepth]),
840                Some(vec![Depth]),
841                Depth,
842                true,
843            ),
844            (
845                Some(vec![Depth, OrderDepth]),
846                Some(vec![Depth]),
847                OrderDepth,
848                false,
849            ),
850            (Some(vec![]), Some(vec![Depth]), Depth, false),
851            (Some(vec![]), None, Depth, false),
852            (None, Some(vec![]), Depth, false),
853            (None, Some(vec![Trades]), Depth, false),
854            (Some(vec![Depth]), Some(vec![Trades]), Depth, false),
855            (Some(vec![Trades]), None, Trades, true),
856        ] {
857            let mut capabilities = FeedCapabilities::new();
858            if let Some(services) = feed {
859                capabilities = capabilities.with_services(services);
860            }
861            let mut instrument = Instrument::new("A");
862            if let Some(services) = symbol {
863                instrument = instrument.with_services(services);
864            }
865            assert_eq!(
866                capabilities.declares(service, &instrument),
867                expected,
868                "feed {:?}, symbol {:?}, service {service:?}",
869                capabilities.services,
870                instrument.services,
871            );
872        }
873
874        assert!(FeedCapabilities::new().offers(Depth));
875        assert!(!FeedCapabilities::new().with_services([]).offers(Depth));
876    }
877}