Skip to main content

egui_charts/services/
alerts.rs

1//! Price alerts the host keeps, evaluates and reports.
2
3use chrono::{DateTime, Utc};
4
5use super::Events;
6use crate::data::{MaybeSend, Reply};
7use crate::model::{Alert, AlertId};
8
9/// The host's alerts. The host stores and evaluates them, on its own prices or on a server; the
10/// library shows what the host pushes and sends every change the user makes through
11/// [`save`](Alerts::save) and [`remove`](Alerts::remove). With the `alert-engine` feature,
12/// `services::AlertEngine` is a ready-made implementation that evaluates
13/// the alerts on the bars the host feeds it; without it the library evaluates nothing.
14///
15/// The host is the source of truth. The terminal shows only the alerts and triggers that
16/// [`watch`](Alerts::watch) pushed: an alert the user creates appears once the host pushes it, and
17/// the terminal never marks an alert triggered on its own. When the host pushes
18/// [`AlertEvent::Triggered`], the terminal shows a popup and an entry in the alert log; the status
19/// it shows is the one the host last pushed in an [`AlertEvent::Upsert`]. Sound and delivery
20/// outside the application (email, push notifications) are the host's: it can play a sound or send
21/// a message when it pushes the trigger.
22///
23/// Evaluation is the host's, so these rules are the contract between the library that edits an
24/// alert and the service that runs it. A channel is inclusive at both boundaries. The state
25/// conditions [`InsideChannel`](crate::model::AlertCondition::InsideChannel) and
26/// [`OutsideChannel`](crate::model::AlertCondition::OutsideChannel) hold on every evaluation
27/// while the value is in that state, so pair them with a frequency that limits repeats, whereas
28/// entering and exiting hold only on the evaluation that crosses a boundary.
29/// [`AlertFrequency::OncePerMinute`](crate::model::AlertFrequency::OncePerMinute) allows one
30/// trigger per UTC clock minute, from `hh:mm:00` up to but excluding the next minute, whatever the
31/// chart's interval. An alert's [`schedule`](crate::model::Alert::schedule) allows triggers only when
32/// [`NotificationSchedule::allows`](crate::model::NotificationSchedule::allows) accepts the
33/// trigger time. Outside it, keep observing values for crossings without consuming an occurrence
34/// or disarming an alert that fires once.
35///
36/// A [`StrategyFills`](crate::model::AlertCondition::StrategyFills) alert receives each execution
37/// of a host-named run through `AlertEngine::strategy_fills`, with `strategies` and `alert-engine`.
38/// Its event carries the fill time, execution price and a message such as
39/// "Buy 2 at 187.50, position 4". The host supplies bars and retains the run; delivery outside the
40/// application remains the host's responsibility.
41///
42/// These methods are called on the UI thread, while the terminal shows a frame or runs a command,
43/// and must return at once. Do the work elsewhere and answer through the [`Reply`] or push through
44/// the [`Events`] you were given, from any thread, runtime or browser task. A `Reply` dropped
45/// unanswered reports [`FeedError::Unanswered`](crate::data::FeedError::Unanswered).
46///
47/// ```
48/// use std::sync::Mutex;
49///
50/// use egui_charts::data::Reply;
51/// use egui_charts::model::{Alert, AlertId};
52/// use egui_charts::services::{AlertEvent, Alerts, Events};
53///
54/// /// Alerts held in memory, lost when the application exits. Answers inline. A real host also
55/// /// evaluates the alerts on its prices and pushes `AlertEvent::Triggered` when one fires.
56/// #[derive(Default)]
57/// struct InMemory(Mutex<State>);
58///
59/// #[derive(Default)]
60/// struct State {
61///     alerts: Vec<Alert>,
62///     events: Option<Events<AlertEvent>>,
63///     next_id: u64,
64/// }
65///
66/// impl Alerts for InMemory {
67///     fn watch(&self, events: Events<AlertEvent>) {
68///         let mut state = self.0.lock().unwrap();
69///         for alert in &state.alerts {
70///             events.push(AlertEvent::Upsert(alert.clone()));
71///         }
72///         state.events = Some(events);
73///     }
74///
75///     fn save(&self, mut alert: Alert, reply: Reply<()>) {
76///         let mut state = self.0.lock().unwrap();
77///         if alert.id.is_none() {
78///             state.next_id += 1;
79///             alert.id = Some(AlertId(state.next_id.to_string()));
80///         }
81///         state.alerts.retain(|kept| kept.id != alert.id);
82///         state.alerts.push(alert.clone());
83///         if let Some(events) = &state.events {
84///             events.push(AlertEvent::Upsert(alert));
85///         }
86///         reply.send(Ok(()));
87///     }
88///
89///     fn remove(&self, id: AlertId, reply: Reply<()>) {
90///         let mut state = self.0.lock().unwrap();
91///         state.alerts.retain(|kept| kept.id.as_ref() != Some(&id));
92///         if let Some(events) = &state.events {
93///             events.push(AlertEvent::Remove(id));
94///         }
95///         reply.send(Ok(()));
96///     }
97/// }
98///
99/// let alerts: std::sync::Arc<dyn Alerts> = std::sync::Arc::new(InMemory::default());
100/// # let _ = alerts;
101/// ```
102pub trait Alerts: MaybeSend + 'static {
103    /// The local evaluator, when this service uses one. The assembled terminal feeds it from
104    /// its datafeed, including subjects that no live chart shows. Server-backed services keep
105    /// the default so evaluation remains on the server.
106    #[cfg(feature = "alert-engine")]
107    fn engine(&self) -> Option<&super::AlertEngine> {
108        None
109    }
110
111    /// Pushes the host's alerts, then every change and trigger. Called when the terminal starts and
112    /// again after a lagged stream, each time with a new stream that starts from no alerts: push
113    /// the current list first, then keep pushing until `events` is inactive. On the first call,
114    /// also push the triggers that fired while the application was closed; do not push them again
115    /// on a later call.
116    ///
117    /// The stream takes at most 10,000 pushes between two frames, the current list and the missed
118    /// triggers included: send at most that many at once (for example only the most recent
119    /// triggers), or the stream lags on every call and no alert shows.
120    fn watch(&self, events: Events<AlertEvent>);
121
122    /// Creates (`alert.id` is `None`) or replaces an alert. Keep `alert.inputs` with the alert:
123    /// its independent study parameters and source view must survive copies and restoration.
124    /// Answer `Ok` once the change is kept;
125    /// the alert appears when `watch` pushes it, so push the saved alert, with its id, as
126    /// [`AlertEvent::Upsert`].
127    fn save(&self, alert: Alert, reply: Reply<()>);
128
129    /// Deletes the alert `id`. Answer `Ok` once it is deleted and push [`AlertEvent::Remove`].
130    fn remove(&self, id: AlertId, reply: Reply<()>);
131}
132
133/// A change to the host's alerts, pushed through the stream [`Alerts::watch`] received.
134#[derive(Clone, Debug, PartialEq)]
135#[non_exhaustive]
136pub enum AlertEvent {
137    /// Adds the alert, or replaces the one with its id. An alert without an id is ignored.
138    Upsert(Alert),
139    /// Removes the alert with this id, if any.
140    Remove(AlertId),
141    /// Removes every alert, so a host that reconnects can push its full list again.
142    Clear,
143    /// The alert fired at `time`, at `price` (finite) when the host knows it. The terminal shows a
144    /// popup and logs it; push an `Upsert` as well when the alert's status changes.
145    Triggered {
146        /// Identifier of the alert that fired.
147        id: AlertId,
148        /// UTC instant of the trigger.
149        time: DateTime<Utc>,
150        /// Price observed by the host when the alert fired, when available.
151        price: Option<f64>,
152    },
153    /// A trigger with its actual subject, including a member of a watchlist alert.
154    /// The panel retains this symbol even after the alert or watchlist changes.
155    TriggeredForSymbol {
156        /// Identifier of the alert that fired.
157        id: AlertId,
158        /// Symbol whose live source satisfied the conditions.
159        symbol: String,
160        /// UTC instant of the trigger.
161        time: DateTime<Utc>,
162        /// Price observed by the host when available.
163        price: Option<f64>,
164        /// The alert's message with its placeholders filled in, as [`Alert::expand_message`]
165        /// does, empty if the alert has none. The terminal shows it in the popup and the log.
166        message: String,
167    },
168}