1use crate::data::FeedError;
4#[cfg(feature = "ui")]
5use crate::model::AlertSource;
6use crate::model::{Bar, Timeframe};
7use chrono::{DateTime, Utc};
8
9pub struct AlertData<'a> {
12 pub request: crate::data::HistoryRequest,
14 pub bars: &'a [Bar],
16 pub history_complete: bool,
18 #[cfg(feature = "ui")]
20 pub drawings: &'a [crate::drawings::Drawing],
21}
22
23pub 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 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 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 #[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 pub fn bars(&self) -> &[Bar] {
148 if self.ready { &self.bars } else { &[] }
149 }
150
151 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 pub fn clock(&self) -> &crate::model::BarClock {
227 &self.request.clock
228 }
229
230 pub fn instrument(&self) -> Option<&crate::model::Instrument> {
232 self.instrument.as_ref()
233 }
234
235 #[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 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 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 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}