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}