Skip to main content

egui_charts/services/
alert_feed.rs

1//! Historical and streaming inputs for headless alert evaluation.
2
3use crate::data::FeedError;
4#[cfg(feature = "ui")]
5use crate::model::AlertSource;
6use crate::model::{Bar, Timeframe};
7use chrono::{DateTime, Utc};
8
9/// Loaded live bars a host can lend to alerts with the same feed view.
10/// Supply only current, unpaused data, never replay bars or incomplete history.
11pub struct AlertData<'a> {
12    /// The symbol, interval, calendar, session and conversion of these bars.
13    pub request: crate::data::HistoryRequest,
14    /// Sorted bars with unique opening times.
15    pub bars: &'a [Bar],
16    /// Whether the feed has no older history.
17    pub history_complete: bool,
18    /// Current drawings in the same bar coordinates, including edits not yet stored.
19    #[cfg(feature = "ui")]
20    pub drawings: &'a [crate::drawings::Drawing],
21}
22
23/// Historical and streaming inputs for an alert without a rendered chart. Uses the same
24/// validation, trade aggregation and history/live merge rules as a chart. Dropping it stops
25/// its subscription and discards late replies. Available with `alert-engine`.
26pub struct AlertFeed {
27    feed: std::sync::Arc<dyn crate::data::Datafeed>,
28    inbox: std::sync::Arc<crate::data::inbox::Inbox>,
29    wake: std::sync::Arc<std::sync::atomic::AtomicBool>,
30    request: crate::data::HistoryRequest,
31    metric: Option<String>,
32    instrument: Option<crate::model::Instrument>,
33    calendar: Option<crate::model::AlertCalendar>,
34    subscription: Option<crate::data::SubscriptionId>,
35    bars: Vec<Bar>,
36    origins: std::collections::BTreeMap<DateTime<Utc>, crate::data::merge::Origin>,
37    pending: bool,
38    ready: bool,
39    retry: Option<DateTime<Utc>>,
40    stopped: bool,
41    #[cfg(feature = "ui")]
42    drawings: Option<(
43        std::sync::Arc<dyn super::Storage>,
44        super::document::StoredDocument,
45    )>,
46    #[cfg(feature = "ui")]
47    stored: Option<crate::drawings::persistence::DrawingStorage>,
48    inputs: Vec<crate::data::OrderFlowInput>,
49    end: bool,
50    #[cfg(feature = "studies")]
51    live: bool,
52    start: Option<DateTime<Utc>>,
53}
54
55impl AlertFeed {
56    /// A latest-history request for 1,000 bars. Set its public calendar, session, conversion
57    /// and count fields before passing it to [`new`](Self::new) to match the source chart.
58    pub fn history_request(
59        symbol: impl Into<String>,
60        timeframe: Timeframe,
61    ) -> crate::data::HistoryRequest {
62        crate::data::HistoryRequest {
63            symbol: symbol.into(),
64            timeframe,
65            clock: crate::model::BarClock::new(timeframe),
66            before: None,
67            since: None,
68            count: 1000,
69            extended_hours: false,
70            adjusted: false,
71            conversion: None,
72        }
73    }
74
75    /// Requests the symbol's metadata, then its history and updates. `metric` selects a study's
76    /// metric series; `None` selects price bars. Calls return promptly as `Datafeed` requires.
77    pub fn new(
78        feed: std::sync::Arc<dyn crate::data::Datafeed>,
79        mut request: crate::data::HistoryRequest,
80        metric: Option<String>,
81    ) -> Self {
82        if metric.is_some() {
83            request.clock = crate::model::BarClock::exact();
84        }
85        let inbox = std::sync::Arc::<crate::data::inbox::Inbox>::default();
86        if let Some(conversion) = &request.conversion
87            && metric.is_none()
88        {
89            feed.resolve_converted(&request.symbol, conversion, inbox.reply(1));
90        } else {
91            feed.resolve(&request.symbol, inbox.reply(1));
92        }
93        Self {
94            feed,
95            inbox,
96            wake: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)),
97            request,
98            metric,
99            instrument: None,
100            calendar: None,
101            subscription: None,
102            bars: Vec::new(),
103            origins: Default::default(),
104            pending: true,
105            ready: false,
106            retry: None,
107            stopped: false,
108            #[cfg(feature = "ui")]
109            drawings: None,
110            #[cfg(feature = "ui")]
111            stored: None,
112            inputs: Vec::new(),
113            end: false,
114            #[cfg(feature = "studies")]
115            live: false,
116            start: None,
117        }
118    }
119
120    #[cfg(feature = "studies")]
121    pub(super) fn set_calendar(&mut self, calendar: Option<crate::model::AlertCalendar>) {
122        self.calendar = calendar;
123    }
124
125    /// Follows this symbol's drawings in host storage, without writing to it.
126    #[cfg(feature = "ui")]
127    pub fn set_storage(&mut self, storage: std::sync::Arc<dyn super::Storage>) {
128        if self
129            .drawings
130            .as_ref()
131            .is_some_and(|(current, _)| std::sync::Arc::ptr_eq(current, &storage))
132        {
133            return;
134        }
135        let mut document = super::document::StoredDocument::open(
136            storage.as_ref(),
137            super::StoreKind::Drawings,
138            &self.request.symbol,
139        );
140        document.follow_changes(storage.as_ref());
141        self.stored = None;
142        self.drawings = Some((storage, document));
143    }
144
145    /// Bars available once history reaches the requested count or its end. Live updates stay
146    /// buffered until then so evaluation never mistakes an incomplete stream for history.
147    pub fn bars(&self) -> &[Bar] {
148        if self.ready { &self.bars } else { &[] }
149    }
150
151    /// Extend coverage to `start`, even when it needs more than the initial bar count.
152    pub fn require_start(&mut self, start: DateTime<Utc>) {
153        self.start = Some(self.start.map_or(start, |held| held.min(start)));
154        if !self.end && self.bars.first().is_none_or(|bar| bar.time > start) {
155            self.ready = false;
156        }
157    }
158
159    #[cfg(feature = "studies")]
160    pub(super) fn set_inputs(&mut self, inputs: Vec<crate::data::OrderFlowInput>) {
161        self.inputs = inputs;
162        if let Some(id) = self.subscription {
163            self.inbox.study_inputs(id, self.inputs.clone());
164        }
165    }
166
167    #[cfg(feature = "studies")]
168    pub(super) fn matches(&self, data: &AlertData<'_>) -> bool {
169        self.metric.is_none()
170            && self.instrument.is_some()
171            && data.request.symbol == self.request.symbol
172            && data.request.timeframe == self.request.timeframe
173            && data.request.clock == self.request.clock
174            && data.request.extended_hours == self.request.extended_hours
175            && data.request.adjusted == self.request.adjusted
176            && data.request.conversion == self.request.conversion
177    }
178
179    #[cfg(feature = "studies")]
180    pub(super) fn follow(&mut self, live: &[AlertData<'_>]) {
181        let found = live.iter().find(|data| {
182            self.matches(data)
183                && self.inputs.is_empty()
184                && data.bars.first().is_some_and(|first| {
185                    self.start
186                        .or_else(|| self.bars.first().map(|bar| bar.time))
187                        .map_or(
188                            data.history_complete || data.bars.len() >= self.request.count,
189                            |start| first.time <= start,
190                        )
191                })
192                && self
193                    .bars
194                    .last()
195                    .is_none_or(|last| data.bars.last().is_some_and(|bar| bar.time >= last.time))
196        });
197        if let Some(data) = found {
198            if !self.live {
199                self.unsubscribe();
200                self.inbox = std::sync::Arc::default();
201                self.pending = false;
202                self.origins.clear();
203            }
204            self.live = true;
205            self.ready = true;
206            self.bars.clear();
207            self.bars.extend_from_slice(data.bars);
208        } else if self.live {
209            self.live = false;
210            self.ready = false;
211            self.end = false;
212            self.stopped = false;
213            self.retry = None;
214            self.request.before = None;
215            self.bars.clear();
216            self.wake.store(true, std::sync::atomic::Ordering::Relaxed);
217        }
218    }
219
220    #[cfg(feature = "studies")]
221    pub(super) fn ready(&self) -> bool {
222        self.ready
223    }
224
225    /// Calendar shared by history, streaming aggregation and study calculation.
226    pub fn clock(&self) -> &crate::model::BarClock {
227        &self.request.clock
228    }
229
230    /// Metadata resolved by the host.
231    pub fn instrument(&self) -> Option<&crate::model::Instrument> {
232        self.instrument.as_ref()
233    }
234
235    /// A stored drawing's level on the evaluated bar, using the chart's interpolation and
236    /// price-scale geometry. `None` while drawings are loading or for an unavailable level.
237    #[cfg(feature = "ui")]
238    pub fn drawing_value(
239        &self,
240        source: &AlertSource,
241        at: usize,
242        scale: crate::scales::PriceScaleMode,
243    ) -> Option<f64> {
244        let bars = self.bars();
245        bars.get(at)?;
246        self.stored
247            .as_ref()?
248            .drawings()
249            .iter()
250            .enumerate()
251            .find_map(|(id, stored)| {
252                let drawing = crate::drawings::persistence::stored_to_drawing_with(
253                    stored,
254                    |point| {
255                        point
256                            .time()
257                            .and_then(|time| {
258                                crate::model::index_at_with_timeframe(
259                                    bars,
260                                    time,
261                                    Some(self.request.timeframe),
262                                    self.instrument
263                                        .as_ref()
264                                        .and_then(|instrument| instrument.time_zone.as_ref()),
265                                )
266                            })
267                            .unwrap_or(f64::NAN) as f32
268                    },
269                    id,
270                );
271                let level = drawing.alert_level(source)?;
272                drawing
273                    .alert_levels_with_bars(at as f64, scale, bars)
274                    .into_iter()
275                    .find(|(index, ..)| *index == level)
276                    .map(|(.., value)| value)
277                    .filter(|value| value.is_finite())
278            })
279    }
280
281    /// Applies replies and coalesced updates and wakes `ctx` when more arrive. Recoverable
282    /// failures retry after the host's delay, or five seconds. Errors are returned for display.
283    pub fn poll(
284        &mut self,
285        #[cfg(feature = "ui")] ctx: &egui::Context,
286        now: DateTime<Utc>,
287    ) -> Vec<FeedError> {
288        use crate::data::merge::{Origin, merge};
289        use crate::data::{
290            HistoryPage, MetricPage, MetricRequest, MetricSubscription, Subscription,
291        };
292        #[cfg(feature = "ui")]
293        {
294            let repaint = ctx.clone();
295            let viewport = ctx.viewport_id();
296            let wake = std::sync::Arc::clone(&self.wake);
297            self.inbox.set_waker(move || {
298                if wake.load(std::sync::atomic::Ordering::Relaxed) {
299                    repaint.request_repaint_of(viewport);
300                }
301            });
302        }
303        let mut errors = Vec::new();
304        let drained = self.inbox.drain();
305        for (key, answer) in drained.answers {
306            self.pending = false;
307            let result = if key == 1 {
308                let result = *answer
309                    .downcast::<Result<crate::model::Instrument, FeedError>>()
310                    .expect("alert instrument");
311                result.map(|mut instrument| {
312                    if let Some(calendar) = &self.calendar
313                        && calendar.symbol == self.request.symbol
314                    {
315                        instrument.time_zone = calendar.timezone.clone();
316                        instrument.session = calendar.session.clone();
317                    }
318                    if let Some(session) = &instrument.session
319                        && self.metric.is_none()
320                    {
321                        self.request.clock = self.request.clock.clone().with_session(
322                            instrument
323                                .time_zone
324                                .clone()
325                                .unwrap_or_else(crate::model::TimezoneId::utc),
326                            session.clone(),
327                            self.request.extended_hours,
328                        );
329                    }
330                    self.instrument = Some(instrument);
331                })
332            } else {
333                let result = if self.metric.is_some() {
334                    (*answer
335                        .downcast::<Result<MetricPage, FeedError>>()
336                        .expect("alert metric history"))
337                    .map(|page| {
338                        (
339                            page.points.into_iter().map(Bar::from).collect::<Vec<_>>(),
340                            page.end_of_history,
341                            None,
342                        )
343                    })
344                } else {
345                    (*answer
346                        .downcast::<Result<HistoryPage, FeedError>>()
347                        .expect("alert history"))
348                    .map(|page| (page.bars, page.end_of_history, page.continue_before))
349                };
350                result.and_then(|(bars, end, continuation)| {
351                    let before = bars
352                        .iter()
353                        .map(|bar| self.request.clock.bar_open(bar.time))
354                        .min()
355                        .or(continuation);
356                    if !end
357                        && before
358                            .zip(self.request.before)
359                            .is_some_and(|(next, previous)| next >= previous)
360                    {
361                        return Err(FeedError::InvalidData(
362                            "Alert history did not advance".into(),
363                        ));
364                    }
365                    let merged = merge(
366                        &mut self.bars,
367                        &mut self.origins,
368                        &self.request.clock,
369                        bars.into_iter().map(|bar| (bar, Origin::History)).collect(),
370                    );
371                    self.end = end || before.is_none();
372                    self.ready = self.end
373                        || self.start.map_or_else(
374                            || self.bars.len() >= self.request.count,
375                            |start| self.bars.first().is_some_and(|bar| bar.time <= start),
376                        );
377                    self.request.before = before;
378                    if merged.rejected > 0 {
379                        errors.push(FeedError::InvalidData("Invalid alert history bars".into()));
380                    }
381                    Ok(())
382                })
383            };
384            if let Err(error) = result {
385                self.failed(&error, now);
386                errors.push(error);
387            }
388        }
389        for (id, updates) in drained.updates {
390            if updates.resnapshot {
391                self.feed.request_depth_snapshot(id);
392            }
393            if updates.reset {
394                self.bars.clear();
395                self.origins.clear();
396                self.ready = false;
397                self.request.before = None;
398                self.end = false;
399            }
400            merge(
401                &mut self.bars,
402                &mut self.origins,
403                &self.request.clock,
404                updates.bars.into_values().collect(),
405            );
406            if let Some(error) = updates.error {
407                // Discard pending replies and replace a failed subscription before evaluating again.
408                self.unsubscribe();
409                self.inbox = std::sync::Arc::default();
410                self.wake.store(true, std::sync::atomic::Ordering::Relaxed);
411                self.pending = false;
412                self.ready = false;
413                for input in &self.inputs {
414                    input.clear();
415                }
416                self.bars.clear();
417                self.origins.clear();
418                self.request.before = None;
419                self.end = false;
420                self.failed(&error, now);
421                errors.push(error);
422            }
423        }
424        if !self.pending && !self.stopped && !self.ready && self.retry.is_none_or(|at| at <= now) {
425            self.retry = None;
426            self.pending = true;
427            if self.instrument.is_none() {
428                if let Some(conversion) = &self.request.conversion
429                    && self.metric.is_none()
430                {
431                    self.feed.resolve_converted(
432                        &self.request.symbol,
433                        conversion,
434                        self.inbox.reply(1),
435                    );
436                } else {
437                    self.feed.resolve(&self.request.symbol, self.inbox.reply(1));
438                }
439            } else {
440                if self.subscription.is_none() {
441                    let clock = if self.metric.is_some() {
442                        crate::model::BarClock::exact()
443                    } else {
444                        self.request.clock.clone()
445                    };
446                    let sink = self.inbox.subscribe(
447                        self.request.symbol.clone(),
448                        clock,
449                        100_000,
450                        self.instrument
451                            .as_ref()
452                            .and_then(|instrument| instrument.tick_size),
453                    );
454                    let id = sink.id;
455                    for input in &self.inputs {
456                        input.set_tick_size(
457                            self.instrument
458                                .as_ref()
459                                .and_then(|instrument| instrument.tick_size),
460                        );
461                    }
462                    self.inbox.study_inputs(id, self.inputs.clone());
463                    self.subscription = Some(id);
464                    if let Some(metric) = &self.metric {
465                        self.feed.subscribe_metric(MetricSubscription {
466                            id,
467                            symbol: self.request.symbol.clone(),
468                            metric: metric.clone(),
469                            sink,
470                        });
471                    } else {
472                        self.feed.subscribe(Subscription {
473                            id,
474                            symbol: self.request.symbol.clone(),
475                            timeframe: self.request.timeframe,
476                            clock: self.request.clock.clone(),
477                            extended_hours: self.request.extended_hours,
478                            conversion: self.request.conversion.clone(),
479                            sink,
480                        });
481                    }
482                }
483                if let Some(metric) = &self.metric {
484                    self.feed.metric_history(
485                        MetricRequest {
486                            symbol: self.request.symbol.clone(),
487                            metric: metric.clone(),
488                            before: self.request.before,
489                            count: self.request.count,
490                        },
491                        self.inbox.reply(2),
492                    );
493                } else {
494                    self.feed.history(self.request.clone(), self.inbox.reply(2));
495                }
496            }
497        }
498        #[cfg(feature = "ui")]
499        if let Some((storage, document)) = &mut self.drawings {
500            for received in document.poll(storage.as_ref(), ctx) {
501                match received {
502                    super::document::Received::Loaded(json) => {
503                        let loaded = json
504                            .as_deref()
505                            .map(crate::drawings::persistence::DrawingStorage::from_json)
506                            .transpose();
507                        match loaded {
508                            Ok(Some((stored, skipped)))
509                                if stored.symbol == self.request.symbol && skipped.is_empty() =>
510                            {
511                                self.stored = Some(stored)
512                            }
513                            Ok(None) => {
514                                self.stored =
515                                    Some(crate::drawings::persistence::DrawingStorage::new(
516                                        self.request.symbol.clone(),
517                                    ))
518                            }
519                            _ => {
520                                self.stored = None;
521                                errors.push(FeedError::InvalidData(
522                                    "Could not read alert drawings".into(),
523                                ));
524                            }
525                        }
526                    }
527                    super::document::Received::Failed(error) => {
528                        self.stored = None;
529                        errors.push(FeedError::Other(error))
530                    }
531                    _ => {}
532                }
533            }
534        }
535        errors
536    }
537
538    fn failed(&mut self, error: &FeedError, now: DateTime<Utc>) {
539        if error.is_recoverable() {
540            let delay = match error {
541                FeedError::RateLimited {
542                    retry_after: Some(delay),
543                } => chrono::TimeDelta::from_std(*delay).unwrap_or(chrono::TimeDelta::seconds(5)),
544                _ => chrono::TimeDelta::seconds(5),
545            };
546            self.retry = now.checked_add_signed(delay);
547        } else {
548            self.stopped = true;
549            self.unsubscribe();
550        }
551    }
552
553    fn unsubscribe(&mut self) {
554        // Unsubscribe may drop a pending reply while the caller holds the context's lock.
555        self.wake.store(false, std::sync::atomic::Ordering::Relaxed);
556        if let Some(id) = self.subscription.take() {
557            self.inbox.unsubscribe(id);
558            self.feed.unsubscribe(id);
559        }
560    }
561}
562
563impl Drop for AlertFeed {
564    fn drop(&mut self) {
565        self.unsubscribe();
566    }
567}