Skip to main content

egui_charts/services/
alert_evaluator.rs

1//! Saved alert definitions evaluated without constructing a chart.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use chrono::{DateTime, Utc};
7
8use super::{AlertData, AlertEngine, AlertFeed};
9use crate::data::{Datafeed, FeedError, HistoryRequest};
10use crate::model::{
11    Alert, AlertId, AlertInputs, AlertSource, Bar, BarClock, Instrument, Timeframe,
12};
13use crate::studies::{
14    ConfiguredStudy, Indicator, IndicatorFactory, StudyData, StudyDataRequest, StudySource,
15};
16
17/// History, subscriptions and independent study state for the alert service. Both interactive
18/// hosts and headless hosts poll this evaluator with the same saved definitions. Chart navigation
19/// and replay never replace its saved study definitions. Dropping it releases all subscriptions.
20pub struct AlertEvaluator {
21    feed: Arc<dyn Datafeed>,
22    runs: HashMap<(AlertId, String), Evaluation>,
23    feeds: HashMap<Subject, AlertFeed>,
24    #[cfg(feature = "ui")]
25    storage: Option<Arc<dyn super::Storage>>,
26}
27
28#[derive(Clone, PartialEq, Eq, Hash)]
29struct Subject {
30    symbol: String,
31    timeframe: Timeframe,
32    metric: Option<String>,
33    timezone: Option<crate::model::TimezoneId>,
34    extended_hours: bool,
35    conversion: Option<String>,
36    count: usize,
37    start: Option<DateTime<Utc>>,
38    /// Event readers belong to one alert, including when its OHLC inputs match another alert.
39    owner: Option<(AlertId, String)>,
40    calendar: Option<crate::model::AlertCalendar>,
41}
42
43struct Evaluation {
44    alert: Alert,
45    inputs: AlertInputs,
46    studies: Vec<(usize, Box<dyn Indicator>)>,
47    error: Option<String>,
48    initialized: bool,
49    start: Option<DateTime<Utc>>,
50}
51
52impl Evaluation {
53    fn new(alert: &Alert, factory: &IndicatorFactory) -> Self {
54        let inputs = alert.inputs.as_deref().cloned().unwrap_or_default();
55        let mut required: HashSet<_> = alert
56            .sources()
57            .filter_map(|source| match source {
58                AlertSource::Study { id, .. } => id.parse::<usize>().ok(),
59                _ => None,
60            })
61            .collect();
62        loop {
63            let before = required.len();
64            for study in &inputs.studies {
65                if !required.contains(&study.id) {
66                    continue;
67                }
68                for (_, input) in &study.inputs {
69                    if let crate::model::InputValue::Source(StudySource::Study { id, .. }) = input {
70                        required.insert(*id);
71                    }
72                }
73            }
74            if required.len() == before {
75                break;
76            }
77        }
78        let mut result = Self {
79            alert: alert.clone(),
80            start: inputs.start,
81            inputs,
82            studies: Vec::new(),
83            error: None,
84            initialized: false,
85        };
86        for id in required {
87            let restored = (|| {
88                let saved = result
89                    .inputs
90                    .studies
91                    .iter()
92                    .find(|study| study.id == id)
93                    .ok_or_else(|| format!("Alert has no saved inputs for study {id}"))?;
94                let study = factory
95                    .create(&saved.name)
96                    .ok_or_else(|| format!("Unknown alert study: {}", saved.name))?;
97                let mut study = ConfiguredStudy::new(study);
98                for (id, value) in &saved.inputs {
99                    study
100                        .set_input(id, value.clone())
101                        .map_err(|error| error.to_string())?;
102                }
103                study.set_timeframe(alert.timeframe.unwrap_or_default());
104                Ok::<_, String>((id, Box::new(study) as Box<dyn Indicator>))
105            })();
106            match restored {
107                Ok(study) => result.studies.push(study),
108                Err(error) => result.error = Some(error),
109            }
110        }
111        result
112    }
113
114    fn subject(&self, symbol: &str, request: Option<&StudyDataRequest>) -> Subject {
115        Subject {
116            symbol: request
117                .filter(|request| !request.symbol.is_empty())
118                .map_or(symbol, |request| &request.symbol)
119                .into(),
120            timeframe: request
121                .and_then(|request| request.timeframe)
122                .or(self.alert.timeframe)
123                .unwrap_or_default(),
124            metric: request.and_then(|request| request.metric.clone()),
125            timezone: self.inputs.timezone.clone(),
126            extended_hours: self.inputs.extended_hours,
127            conversion: self.inputs.conversion.clone(),
128            count: self.inputs.count.max(1),
129            start: self.inputs.start,
130            calendar: self.inputs.calendar.clone(),
131            owner: self
132                .studies
133                .iter()
134                .any(|(_, study)| !study.order_flow_inputs().is_empty())
135                .then(|| (self.alert.id.clone().unwrap(), symbol.to_owned())),
136        }
137    }
138
139    fn event_inputs(&self, request: Option<&StudyDataRequest>) -> Vec<crate::data::OrderFlowInput> {
140        self.studies
141            .iter()
142            .filter(|(_, study)| {
143                let requests = study.data_requests();
144                request.map_or(requests.is_empty(), |request| requests.contains(request))
145            })
146            .flat_map(|(_, study)| study.order_flow_inputs().iter().cloned())
147            .collect()
148    }
149
150    fn calculate(
151        &mut self,
152        bars: &[Bar],
153        clock: &BarClock,
154        instrument: Option<&Instrument>,
155        data: &[StudyData<'_>],
156    ) {
157        let mut pending: Vec<_> = self.studies.iter().map(|(id, _)| *id).collect();
158        while !pending.is_empty() {
159            let Some(at) = pending.iter().position(|id| {
160                let study = &self.studies.iter().find(|(key, _)| key == id).unwrap().1;
161                match study.source() {
162                    Some(StudySource::Study { id, .. }) => !pending.contains(&id),
163                    _ => true,
164                }
165            }) else {
166                break;
167            };
168            let id = pending.remove(at);
169            let source = self
170                .studies
171                .iter()
172                .find(|(key, _)| *key == id)
173                .unwrap()
174                .1
175                .source();
176            let values = match source {
177                Some(StudySource::Study { id, plot }) => self
178                    .studies
179                    .iter()
180                    .find(|(key, _)| *key == id)
181                    .and_then(|(_, study)| study.values().columns().get(plot))
182                    .cloned(),
183                _ => None,
184            };
185            let study = &mut self
186                .studies
187                .iter_mut()
188                .find(|(key, _)| *key == id)
189                .unwrap()
190                .1;
191            study.set_timeframe(self.alert.timeframe.unwrap_or_default());
192            study.set_bar_clock(clock);
193            study.set_instrument(instrument);
194            study.set_visible_range(self.inputs.visible.map_or(0..bars.len(), |(start, end)| {
195                bars.partition_point(|bar| bar.time < start)
196                    ..bars.partition_point(|bar| bar.time <= end)
197            }));
198            if matches!(source, Some(StudySource::Study { .. })) {
199                study.update_source(bars, 0, values.as_deref().unwrap_or(&[]));
200            } else {
201                study.update_data(bars, 0, data);
202            }
203        }
204    }
205
206    fn requests(&self, first: DateTime<Utc>) -> Vec<(StudyDataRequest, DateTime<Utc>)> {
207        self.studies
208            .iter()
209            .flat_map(|(_, study)| {
210                study.data_requests().into_iter().map(|request| {
211                    let start = study
212                        .data_start(&request)
213                        .map_or(first, |start| start.max(first));
214                    (request, start)
215                })
216            })
217            .collect()
218    }
219}
220
221impl Subject {
222    fn open(&self, feed: Arc<dyn Datafeed>) -> AlertFeed {
223        let mut reader = AlertFeed::new(feed, self.request(), self.metric.clone());
224        reader.set_calendar(self.calendar.clone());
225        reader
226    }
227
228    fn request(&self) -> HistoryRequest {
229        let mut request = AlertFeed::history_request(&self.symbol, self.timeframe);
230        request.clock = match &self.timezone {
231            Some(zone) => request.clock.with_timezone(zone.clone()),
232            None => request.clock.with_local_timezone(),
233        };
234        request.extended_hours = self.extended_hours;
235        request.conversion = self.conversion.clone();
236        request.count = self.count.max(1000);
237        request
238    }
239}
240
241impl AlertEvaluator {
242    /// Connect to the host's live feed. Study constructors come from the factory passed to `poll`.
243    pub fn new(feed: Arc<dyn Datafeed>) -> Self {
244        Self {
245            feed,
246            runs: HashMap::new(),
247            feeds: HashMap::new(),
248            #[cfg(feature = "ui")]
249            storage: None,
250        }
251    }
252
253    /// Follow drawing changes in host storage using the same reader as background price alerts.
254    #[cfg(feature = "ui")]
255    pub fn set_storage(&mut self, storage: Arc<dyn super::Storage>) {
256        self.storage = Some(storage);
257    }
258
259    /// Poll live inputs and evaluate ready alerts. With `ui`, `ctx` wakes the host on new data
260    /// and follows stored drawings. Headless hosts pass an empty `live` slice.
261    /// `live` lends compatible loaded chart data; event studies retain independent streams.
262    /// Errors leave affected sources unavailable and are returned for the host to report.
263    pub fn poll(
264        &mut self,
265        engine: &AlertEngine,
266        factory: &IndicatorFactory,
267        live: &[AlertData<'_>],
268        #[cfg(feature = "ui")] ctx: &egui::Context,
269        now: DateTime<Utc>,
270    ) -> Vec<FeedError> {
271        engine.tick(now);
272        let mut armed = HashSet::new();
273        let mut errors = Vec::new();
274        for alert in engine.alerts() {
275            let Some(id) = &alert.id else {
276                continue;
277            };
278            for symbol in engine.subjects(id) {
279                let key = (id.clone(), symbol.clone());
280                armed.insert(key.clone());
281                if self.runs.get(&key).is_none_or(|run| run.alert != alert) {
282                    // Replacing an event definition must also replace its subscription.
283                    self.feeds
284                        .retain(|subject, _| subject.owner.as_ref() != Some(&key));
285                    self.runs
286                        .insert(key.clone(), Evaluation::new(&alert, factory));
287                }
288                let run = self.runs.get_mut(&key).unwrap();
289                if let Some(error) = run.error.take() {
290                    errors.push(FeedError::InvalidData(error));
291                    run.studies.clear();
292                }
293            }
294        }
295        self.runs.retain(|key, _| armed.contains(key));
296        for ((_, symbol), run) in &self.runs {
297            let subject = run.subject(symbol, None);
298            let reader = self.feeds.entry(subject.clone()).or_insert_with(|| {
299                let mut reader = subject.open(self.feed.clone());
300                if let Some(start) = subject.start {
301                    reader.require_start(start);
302                }
303                reader
304            });
305            if let Some(start) = run.start {
306                reader.require_start(start);
307            }
308            reader.set_inputs(run.event_inputs(None));
309            #[cfg(feature = "ui")]
310            if run
311                .alert
312                .sources()
313                .any(|source| matches!(source, AlertSource::Drawing(_)))
314                && let Some(storage) = &self.storage
315            {
316                reader.set_storage(storage.clone());
317            }
318        }
319        for (subject, reader) in &mut self.feeds {
320            reader.follow(if subject.owner.is_none() { live } else { &[] });
321            errors.extend(reader.poll(
322                #[cfg(feature = "ui")]
323                ctx,
324                now,
325            ));
326        }
327        for ((id, symbol), run) in &mut self.runs {
328            let subject = run.subject(symbol, None);
329            let reader = &self.feeds[&subject];
330            let all = reader.bars();
331            let bars =
332                &all[all.partition_point(|bar| run.start.is_some_and(|start| bar.time < start))..];
333            let Some(first) = bars.first() else {
334                continue;
335            };
336            run.start.get_or_insert(first.time);
337            if !run.initialized {
338                // Establish the main span for data_start; these outputs are never evaluated.
339                run.calculate(bars, reader.clock(), reader.instrument(), &[]);
340                run.initialized = true;
341            }
342            let requests = run.requests(first.time);
343            for (request, start) in &requests {
344                let subject = run.subject(symbol, Some(request));
345                let reader = self
346                    .feeds
347                    .entry(subject.clone())
348                    .or_insert_with(|| subject.open(self.feed.clone()));
349                reader.require_start(*start);
350                let mut inputs = run.event_inputs(Some(request));
351                if subject == run.subject(symbol, None) {
352                    inputs.extend(run.event_inputs(None));
353                }
354                reader.set_inputs(inputs);
355            }
356            if requests
357                .iter()
358                .any(|(request, _)| !self.feeds[&run.subject(symbol, Some(request))].ready())
359            {
360                continue;
361            }
362            let reader = &self.feeds[&subject];
363            let all = reader.bars();
364            let bars =
365                &all[all.partition_point(|bar| run.start.is_some_and(|start| bar.time < start))..];
366            let data: Vec<_> = requests
367                .iter()
368                .map(|(request, _)| {
369                    let reader = &self.feeds[&run.subject(symbol, Some(request))];
370                    StudyData {
371                        request: request.clone(),
372                        bars: reader.bars(),
373                        clock: reader.clock().clone(),
374                        instrument: reader.instrument(),
375                        timeframe: run.subject(symbol, Some(request)).timeframe,
376                    }
377                })
378                .collect();
379            run.calculate(bars, reader.clock(), reader.instrument(), &data);
380            // data_start may change after calculation, for example when a profile's span moves.
381            let reaches = run.requests(bars[0].time);
382            for (request, start) in &reaches {
383                let subject = run.subject(symbol, Some(request));
384                self.feeds
385                    .entry(subject.clone())
386                    .or_insert_with(|| subject.open(self.feed.clone()))
387                    .require_start(*start);
388            }
389            if reaches
390                .iter()
391                .any(|(request, _)| !self.feeds[&run.subject(symbol, Some(request))].ready())
392            {
393                continue;
394            }
395            let reader = &self.feeds[&subject];
396            let all = reader.bars();
397            let from = all.partition_point(|bar| run.start.is_some_and(|start| bar.time < start));
398            let bars = &all[from..];
399            engine.update_alert(
400                id,
401                symbol,
402                subject.timeframe,
403                bars,
404                now,
405                |source, at| match source {
406                    AlertSource::Study { id, output } => run
407                        .studies
408                        .iter()
409                        .find(|(key, _)| key.to_string() == *id)
410                        .and_then(|(_, study)| study.plot_value(*output, at)),
411                    #[cfg(feature = "ui")]
412                    AlertSource::Drawing(_) => {
413                        let scale = if run.inputs.logarithmic {
414                            crate::scales::PriceScaleMode::Logarithmic
415                        } else {
416                            crate::scales::PriceScaleMode::Normal
417                        };
418                        match live.iter().find(|data| reader.matches(data)) {
419                            Some(data) => data.drawings.iter().find_map(|drawing| {
420                                let level = drawing.alert_level(source)?;
421                                let index = crate::model::index_at_with_timeframe(
422                                    data.bars,
423                                    bars[at].time,
424                                    Some(subject.timeframe),
425                                    reader
426                                        .instrument()
427                                        .and_then(|instrument| instrument.time_zone.as_ref()),
428                                )?;
429                                drawing
430                                    .alert_levels_with_bars(index, scale, data.bars)
431                                    .into_iter()
432                                    .find(|(index, ..)| *index == level)
433                                    .map(|(.., value)| value)
434                            }),
435                            None => reader.drawing_value(source, from + at, scale),
436                        }
437                    }
438                    _ => None,
439                },
440            );
441        }
442        // Once alerts and removed watchlist members release their readers in the same frame.
443        self.runs
444            .retain(|(id, symbol), _| engine.subjects(id).contains(symbol));
445        let active: HashSet<_> = self
446            .runs
447            .iter()
448            .flat_map(|((_, symbol), run)| {
449                std::iter::once(run.subject(symbol, None)).chain(
450                    run.studies
451                        .iter()
452                        .flat_map(|(_, study)| study.data_requests())
453                        .map(|request| run.subject(symbol, Some(&request))),
454                )
455            })
456            .collect();
457        self.feeds.retain(|subject, _| active.contains(subject));
458        errors
459    }
460}