1use super::{Chart, ChartElementId, SeriesId};
4use crate::model::Bar;
5use crate::studies::{Indicator, InputValue, RefusedStudy, StudyError, StudySource};
6use web_time::Instant;
7
8impl Chart {
9 pub fn set_study_limit(&mut self, limit: Option<usize>) {
14 self.study_limit = limit;
15 }
16
17 pub fn study_limit(&self) -> Option<usize> {
19 self.study_limit
20 }
21
22 pub(crate) fn study_limit_reached(&self) -> bool {
24 self.study_limit
25 .is_some_and(|limit| self.study_ids.len() >= limit)
26 }
27
28 pub fn try_add_indicator(
32 &mut self,
33 indicator: Box<dyn Indicator>,
34 ) -> Result<SeriesId, StudyError> {
35 if self.study_limit_reached() {
36 return Err(StudyError::StudyLimitReached(RefusedStudy::new(indicator)));
37 }
38 Ok(self.add_indicator(indicator))
39 }
40
41 pub fn add_indicator(&mut self, mut indicator: Box<dyn Indicator>) -> SeriesId {
46 if !indicator.has_settings() {
47 indicator = Box::new(crate::studies::ConfiguredStudy::new(indicator));
48 }
49 if let Some(timeframe) = self.timeframe() {
50 indicator.set_timeframe(timeframe);
51 }
52 if indicator.value_format() == crate::scales::StudyFormat::Volume {
53 indicator.set_value_formatter(self.volume_formatter.clone());
54 }
55 indicator.set_instrument(self.instrument());
56 indicator.set_time_scale_events(held_events(
57 &self.time_scale_events,
58 self.frozen_source_events,
59 ));
60 indicator.set_bar_clock(&self.bar_clock());
61 if let Some(crate::studies::StudySource::Study { id, plot }) = indicator.source() {
62 let source = self
63 .study_ids
64 .iter()
65 .position(|candidate| candidate.0 == id)
66 .and_then(|at| self.studies.indicators()[at].values().columns().get(plot));
67 indicator.update_source(
68 &self.state.data().bars,
69 0,
70 source.map_or(&[], Vec::as_slice),
71 );
72 } else {
73 let sources = self.study_data.data(
74 indicator.data_requests(),
75 &self.symbol,
76 self.timeframe,
77 self.replay_time(),
78 );
79 indicator.update_data(&self.state.data().bars, 0, &sources);
80 }
81 let id = SeriesId(self.next_series);
82 self.next_series += 1;
83 self.record_study(id);
84 let overlay = indicator.is_overlay();
85 self.studies.add(indicator);
86 self.study_ids.push(id);
87 self.study_autoscale.push(true);
88 self.study_inputs.push(String::new());
89 self.pending_from.push(None);
90 self.next_refresh.push(f64::NEG_INFINITY);
91 if !overlay {
92 self.add_study_pane(id);
93 }
94 id
95 }
96
97 pub fn set_indicator_inputs(&mut self, id: SeriesId, inputs: impl Into<String>) -> bool {
99 let Some(index) = self.study_ids.iter().position(|candidate| *candidate == id) else {
100 return false;
101 };
102 self.record_study(id);
103 self.study_inputs[index] = inputs.into();
104 true
105 }
106
107 pub fn indicator_inputs(&self, id: SeriesId) -> Option<&str> {
109 self.study_ids
110 .iter()
111 .position(|candidate| *candidate == id)
112 .map(|index| self.study_inputs[index].as_str())
113 }
114
115 pub(super) fn take_study(&mut self, id: SeriesId) -> Option<Box<dyn Indicator>> {
116 let at = self.study_ids.iter().position(|study| *study == id)?;
117 self.record_study(id);
118 #[cfg(feature = "studies")]
119 if self.volume_study == Some(id) {
120 self.volume_study = None;
121 self.config.show_volume = false;
122 self.volume_setting = false;
123 }
124 let indicator = self.studies.remove_indicator(at);
125 self.study_ids.remove(at);
126 self.study_autoscale.remove(at);
127 self.study_inputs.remove(at);
128 self.pending_from.remove(at);
129 self.next_refresh.remove(at);
130 if self.selection.selected_id() == Some(ChartElementId::Indicator(id)) {
131 self.selection.deselect();
132 }
133 indicator
134 }
135
136 pub fn set_indicator_visible(&mut self, id: SeriesId, visible: bool) -> bool {
139 let Some(at) = self.study_ids.iter().position(|study| *study == id) else {
140 return false;
141 };
142 self.record_study(id);
143 self.studies.indicators_mut()[at].set_visible(visible);
144 true
145 }
146
147 pub fn set_indicator_autoscale(&mut self, id: SeriesId, autoscale: bool) -> bool {
151 let Some(at) = self.study_ids.iter().position(|study| *study == id) else {
152 return false;
153 };
154 self.record_study(id);
155 self.study_autoscale[at] = autoscale;
156 true
157 }
158
159 pub fn set_indicator_scale(&mut self, id: SeriesId, scale: crate::studies::StudyScale) -> bool {
162 let Some(at) = self.study_ids.iter().position(|study| *study == id) else {
163 return false;
164 };
165 self.record_study(id);
166 if let crate::studies::StudyScale::Shared(key) = scale {
167 self.ensure_series_scale(key);
168 }
169 self.studies.indicators_mut()[at].set_scale(scale)
170 }
171
172 pub fn indicators(&self) -> impl Iterator<Item = (SeriesId, &dyn Indicator)> {
180 let studies = self.studies.indicators().iter().map(|study| study.as_ref());
181 self.study_ids.iter().copied().zip(studies)
182 }
183
184 pub fn indicator_mut(&mut self, id: SeriesId) -> Option<&mut (dyn Indicator + 'static)> {
187 let at = self.study_ids.iter().position(|study| *study == id)?;
188 self.record_study(id);
189 self.pending_from[at] = Some(0);
190 self.next_refresh[at] = f64::NEG_INFINITY;
191 Some(self.studies.indicators_mut()[at].as_mut())
192 }
193
194 pub(super) fn refresh_studies(&mut self, now: f64) -> Option<f64> {
203 let instrument = self.instrument().cloned();
204 let clock = self.bar_clock();
205 let bars = &self.state.data().bars;
206 let events = held_events(&self.time_scale_events, self.frozen_source_events);
207 for (at, study) in self.studies.indicators_mut().iter_mut().enumerate() {
208 let instrument_changed = study.set_instrument(instrument.as_ref());
209 let clock_changed = study.set_bar_clock(&clock);
210 let events_changed = study.set_time_scale_events(events);
211 if instrument_changed || clock_changed || events_changed {
212 self.pending_from[at] = Some(0);
213 self.element_studies.dirty = true;
214 }
215 }
216 let requests = self
217 .studies
218 .indicators()
219 .iter()
220 .flat_map(|study| study.data_requests())
221 .collect();
222 let sources =
223 self.study_data
224 .data(requests, &self.symbol, self.timeframe, self.replay_time());
225 refresh_values(
226 self.studies.indicators_mut(),
227 &self.study_ids,
228 bars,
229 &mut self.pending_from,
230 &mut self.next_refresh,
231 now,
232 &sources,
233 )
234 }
235
236 pub(super) fn refresh_element_studies(&mut self, now: f64) -> Option<f64> {
237 let cutoff = self.replay_time();
238 let cache = &mut self.element_studies;
239 let requests = cache
240 .studies
241 .indicators()
242 .iter()
243 .flat_map(|study| study.data_requests())
244 .collect();
245 let sources = self
246 .study_data
247 .data(requests, &self.symbol, self.timeframe, cutoff);
248 refresh_values(
249 cache.studies.indicators_mut(),
250 &self.study_ids,
251 &cache.bars,
252 &mut cache.pending_from,
253 &mut cache.next_refresh,
254 now,
255 &sources,
256 )
257 }
258
259 pub(super) fn rendered_studies(&self) -> &crate::studies::IndicatorRegistry {
260 if self.chart_type.is_time_independent() {
261 &self.element_studies.studies
262 } else {
263 &self.studies
264 }
265 }
266
267 pub(super) fn recalculate_studies(&mut self) {
268 let requests = self
269 .studies
270 .indicators()
271 .iter()
272 .flat_map(|study| study.data_requests())
273 .collect();
274 let sources =
275 self.study_data
276 .data(requests, &self.symbol, self.timeframe, self.replay_time());
277 calculate(
278 self.studies.indicators_mut(),
279 &self.study_ids,
280 &self.state.data().bars,
281 &sources,
282 );
283 }
284
285 pub fn set_indicator_source(
288 &mut self,
289 id: SeriesId,
290 source: StudySource,
291 ) -> Result<(), StudyError> {
292 let at = self
293 .study_ids
294 .iter()
295 .position(|candidate| *candidate == id)
296 .ok_or(StudyError::UnknownStudyId(id.0))?;
297 if let StudySource::Study { id: parent, plot } = source {
298 let parent_at = self
299 .study_ids
300 .iter()
301 .position(|candidate| candidate.0 == parent)
302 .ok_or(StudyError::UnknownSourceStudy(parent))?;
303 if plot >= self.studies.indicators()[parent_at].line_count() {
304 return Err(StudyError::UnknownSourcePlot { id: parent, plot });
305 }
306 let mut next = Some(parent);
307 for _ in 0..=self.study_ids.len() {
308 let Some(parent) = next else {
309 break;
310 };
311 if parent == id.0 {
312 return Err(StudyError::CyclicSource);
313 }
314 let index = self
315 .study_ids
316 .iter()
317 .position(|candidate| candidate.0 == parent)
318 .ok_or(StudyError::UnknownSourceStudy(parent))?;
319 next = match self.studies.indicators()[index].source() {
320 Some(StudySource::Study { id, .. }) => Some(id),
321 _ => None,
322 };
323 }
324 if next.is_some() {
325 return Err(StudyError::CyclicSource);
326 }
327 }
328 let mut edited = self.studies.indicators()[at].clone_box();
329 edited.set_input("source", InputValue::Source(source))?;
330 self.record_study(id);
331 self.studies.indicators_mut()[at] = edited;
332 self.pending_from[at] = Some(0);
333 self.next_refresh[at] = f64::NEG_INFINITY;
334 self.element_studies.dirty = true;
335 Ok(())
336 }
337}
338
339fn held_events(
342 events: &[crate::model::TimeScaleEvent],
343 frozen: usize,
344) -> &[crate::model::TimeScaleEvent] {
345 &events[..events.len().saturating_sub(frozen)]
346}
347
348pub(super) fn update(
350 studies: &mut [Box<dyn Indicator>],
351 ids: &[SeriesId],
352 at: usize,
353 bars: &[Bar],
354 from: usize,
355 sources: &[crate::studies::StudyData<'_>],
356) {
357 if let Some(StudySource::Study { id, plot }) = studies[at].source() {
358 let values = ids
359 .iter()
360 .position(|candidate| candidate.0 == id)
361 .and_then(|source| studies[source].values().columns().get(plot))
362 .cloned()
363 .unwrap_or_default();
364 studies[at].update_source(bars, from, &values);
365 } else {
366 studies[at].update_data(bars, from, sources);
367 }
368}
369
370pub(super) fn calculate(
371 studies: &mut [Box<dyn Indicator>],
372 ids: &[SeriesId],
373 bars: &[Bar],
374 sources: &[crate::studies::StudyData<'_>],
375) {
376 let order = order(studies, ids);
377 for at in 0..studies.len() {
378 if !order.contains(&at) {
379 studies[at].update_source(bars, 0, &[]);
380 }
381 }
382 for at in order {
383 update(studies, ids, at, bars, 0, sources);
384 }
385}
386
387pub(super) fn order(studies: &[Box<dyn Indicator>], ids: &[SeriesId]) -> Vec<usize> {
389 let mut ordered = Vec::with_capacity(studies.len());
390 loop {
391 let before = ordered.len();
392 for (at, study) in studies.iter().enumerate() {
393 if ordered.contains(&at) {
394 continue;
395 }
396 let ready = match study.source() {
397 Some(StudySource::Study { id, plot }) => ids
398 .iter()
399 .position(|candidate| candidate.0 == id)
400 .is_some_and(|parent| {
401 ordered.contains(&parent) && plot < studies[parent].line_count()
402 }),
403 _ => true,
404 };
405 if ready {
406 ordered.push(at);
407 }
408 }
409 if ordered.len() == before {
410 break;
411 }
412 }
413 ordered
414}
415
416pub(super) fn refresh_values(
417 studies: &mut [Box<dyn Indicator>],
418 ids: &[SeriesId],
419 bars: &[Bar],
420 pending_from: &mut [Option<usize>],
421 next_refresh: &mut [f64],
422 now: f64,
423 sources: &[crate::studies::StudyData<'_>],
424) -> Option<f64> {
425 let order = super::studies::order(studies, ids);
426 for at in 0..studies.len() {
427 if !order.contains(&at) {
428 studies[at].update_source(bars, 0, &[]);
429 pending_from[at] = None;
430 }
431 }
432 let mut recomputed = false;
433 for &at in &order {
434 let parent = match studies[at].source() {
435 Some(crate::studies::StudySource::Study { id, .. }) => {
436 ids.iter().position(|candidate| candidate.0 == id)
437 }
438 _ => None,
439 };
440 if parent.is_some_and(|parent| pending_from[parent] == Some(0)) {
443 continue;
444 }
445 let Some(from) = pending_from[at] else {
446 continue;
447 };
448 let len = studies[at].values().len();
449 let incremental =
450 studies[at].is_incremental() && len > 0 && from + 1 >= len && bars.len() >= len;
451 if !incremental {
452 if recomputed {
453 continue;
454 }
455 let due = order
456 .iter()
457 .copied()
458 .filter(|&at| {
459 let parent_ready = match studies[at].source() {
460 Some(crate::studies::StudySource::Study { id, .. }) => ids
461 .iter()
462 .position(|candidate| candidate.0 == id)
463 .is_some_and(|parent| pending_from[parent] != Some(0)),
464 _ => true,
465 };
466 parent_ready
467 && pending_from[at].is_some_and(|from| {
468 let len = studies[at].values().len();
469 !(studies[at].is_incremental()
470 && len > 0
471 && from + 1 >= len
472 && bars.len() >= len)
473 && (from == 0 || now >= next_refresh[at])
474 })
475 })
476 .min_by(|&a, &b| next_refresh[a].total_cmp(&next_refresh[b]));
477 if due != Some(at) {
478 continue;
479 }
480 }
481 let started = Instant::now();
482 let mut previous: Vec<(usize, Vec<f64>)> = Vec::new();
484 for study in studies.iter() {
485 if let Some(StudySource::Study { id, plot }) = study.source()
486 && id == ids[at].0
487 && !previous.iter().any(|(line, _)| *line == plot)
488 {
489 previous.push((plot, studies[at].values().line(plot).to_vec()));
490 }
491 }
492 super::studies::update(studies, ids, at, bars, from, sources);
493 pending_from[at] = None;
494 if !incremental {
495 recomputed = true;
496 let cost = started.elapsed().as_secs_f64();
497 next_refresh[at] = now + cost + REFRESH_INTERVAL.max(4.0 * cost);
498 }
499 for (plot, previous) in previous {
500 let values = studies[at].values().line(plot);
501 let from = previous
502 .iter()
503 .zip(values)
504 .position(|(old, new)| old.to_bits() != new.to_bits())
505 .or_else(|| {
506 (previous.len() != values.len()).then_some(previous.len().min(values.len()))
507 })
508 .map_or(from, |changed| from.min(changed));
509 for (child, study) in studies.iter().enumerate() {
510 if matches!(study.source(), Some(StudySource::Study { id, plot: source_plot }) if id == ids[at].0 && source_plot == plot)
511 {
512 pending_from[child] =
513 Some(pending_from[child].map_or(from, |old| old.min(from)));
514 }
515 }
516 }
517 }
518 let mut wait = None::<f64>;
519 for (at, study) in studies.iter_mut().enumerate() {
520 let Some(from) = pending_from[at] else {
521 continue;
522 };
523 let left = if from == 0 {
524 if !study.values().is_empty() {
525 study.update(&[], 0);
526 }
527 0.0
528 } else {
529 (next_refresh[at] - now).max(0.0)
530 };
531 wait = Some(wait.map_or(left, |wait| wait.min(left)));
532 }
533 wait
534}
535
536const REFRESH_INTERVAL: f64 = 0.25;
539
540#[cfg(all(test, feature = "studies"))]
541mod tests {
542 use crate::model::{Bar, BarData, PriceSource};
543 use crate::studies::Indicator;
544 use crate::studies::{InputValue, RSI, SMA, StudyError, StudySource};
545 use crate::widget::test_frames::frame;
546 use crate::widget::{Chart, SeriesId, test_frames::shapes};
547 use chrono::{DateTime, Duration, TimeDelta, Utc};
548 use std::sync::Arc;
549 use std::sync::atomic::{AtomicUsize, Ordering};
550 use std::time::Instant;
551
552 #[test]
554 fn study_sources_follow_repainting_output_during_appends() {
555 let bars: Vec<_> = (0..800)
556 .map(|i| {
557 let close = 100.0 + 10.0 * (i as f64 / 12.0).sin();
558 Bar::new(
559 chrono::DateTime::<Utc>::UNIX_EPOCH + Duration::minutes(i),
560 close,
561 close + 1.0,
562 close - 1.0,
563 close,
564 100.0,
565 )
566 })
567 .collect();
568 let ctx = egui::Context::default();
569 let mut chart = Chart::new(BarData::from_bars(bars[..500].to_vec()));
570 let parent = chart.add_indicator(Box::new(crate::studies::ZigZag::default()));
571 let child = chart.add_indicator(Box::new(SMA::new(1)));
572 chart
573 .set_indicator_source(
574 child,
575 StudySource::Study {
576 id: parent.0,
577 plot: 0,
578 },
579 )
580 .unwrap();
581 for frame in 0..6 {
582 shapes(&ctx, &mut chart, frame as f64, Vec::new());
583 }
584 for (at, bar) in bars[500..].iter().enumerate() {
585 chart.update(SeriesId::MAIN, bar.clone());
586 shapes(&ctx, &mut chart, 6.0 + at as f64, Vec::new());
587 }
588 for frame in 306..312 {
589 shapes(&ctx, &mut chart, frame as f64, Vec::new());
590 }
591 let output = |id| {
592 chart
593 .indicators()
594 .find(|(candidate, _)| *candidate == id)
595 .unwrap()
596 .1
597 .values()
598 };
599 assert_eq!(output(parent).len(), bars.len());
600 assert_eq!(output(child).len(), bars.len());
601 for at in 0..bars.len() {
602 assert_eq!(
603 output(child).value(0, at).map(f64::to_bits),
604 output(parent).value(0, at).map(f64::to_bits),
605 "bar {at}: SMA(1) must equal its source"
606 );
607 }
608 }
609
610 #[test]
612 fn study_dependencies_refresh_during_continuous_ticks() {
613 for (reverse, count, incremental_source) in [
614 (false, 2, true),
615 (true, 2, true),
616 (false, 3, true),
617 (true, 3, false),
618 ] {
619 let ctx = egui::Context::default();
620 let bars: Vec<_> = (0..1_000)
621 .map(|i| {
622 Bar::new(
623 chrono::DateTime::<Utc>::UNIX_EPOCH + Duration::minutes(i),
624 10.0,
625 10.0,
626 10.0,
627 10.0,
628 100.0,
629 )
630 })
631 .collect();
632 let mut chart = Chart::new(BarData::from_bars(bars.clone()));
633 let mut ids: Vec<_> = (0..count)
634 .map(|_| chart.add_indicator(Box::new(SMA::new(1))))
635 .collect();
636 if reverse {
637 ids.reverse();
638 }
639 if !incremental_source {
640 chart
641 .set_indicator_source(ids[0], StudySource::Price(PriceSource::High))
642 .unwrap();
643 }
644 for pair in ids.windows(2) {
645 chart
646 .set_indicator_source(
647 pair[1],
648 StudySource::Study {
649 id: pair[0].0,
650 plot: 0,
651 },
652 )
653 .unwrap();
654 }
655 for time in 0..6 {
656 shapes(&ctx, &mut chart, time as f64, Vec::new());
657 }
658 chart.next_refresh.fill(f64::NEG_INFINITY);
659 for tick in 1..=60 {
660 let close = 10.0 + tick as f64 * 0.01;
661 let mut bar = bars.last().unwrap().clone();
662 bar.close = close;
663 bar.high = close;
664 chart.update(SeriesId::MAIN, bar);
665 let refresh = chart.next_refresh.clone();
666 let pass = ctx.cumulative_pass_nr();
667 let now = 6.0 + tick as f64;
668 shapes(&ctx, &mut chart, now, Vec::new());
669 assert!(
670 chart
671 .next_refresh
672 .iter()
673 .zip(&refresh)
674 .filter(|(a, b)| **a != **b)
675 .count()
676 <= (ctx.cumulative_pass_nr() - pass) as usize,
677 "at most one full recompute per pass"
678 );
679 for (next, before) in chart.next_refresh.iter_mut().zip(refresh) {
681 if *next != before {
682 *next = now + super::REFRESH_INTERVAL;
683 }
684 }
685 for (id, study) in chart.indicators().filter(|(id, _)| ids.contains(id)) {
686 let value = study.values().value(0, 999).unwrap();
687 let full = count - usize::from(incremental_source);
690 let lag = full * (full - 1);
691 assert!(
692 value <= close + 1e-12 && value >= close - lag as f64 * 0.01 - 1e-12,
693 "{id:?}, tick {tick}: {value}, expected {close} within {lag} ticks"
694 );
695 }
696 }
697 }
698 }
699
700 #[test]
701 fn price_sources_and_study_dependencies_follow_edits_and_live_bars() {
702 let start = chrono::DateTime::<Utc>::UNIX_EPOCH;
703 let bars: Vec<_> = (0..3)
704 .map(|i| {
705 Bar::new(
706 start + Duration::minutes(i),
707 1.0 + 2.0 * i as f64,
708 5.0 + 4.0 * i as f64,
709 1.0 + 2.0 * i as f64,
710 3.0 + 2.0 * i as f64,
711 1.0,
712 )
713 })
714 .collect();
715 let ctx = egui::Context::default();
716 let settle = |chart: &mut Chart| {
717 for frame in 0..6 {
718 shapes(&ctx, chart, frame as f64, Vec::new());
719 }
720 };
721 for (source, expected, shifted) in [
722 (PriceSource::Open, 4.0, 2.0),
723 (PriceSource::High, 11.0, 7.0),
724 (PriceSource::Low, 4.0, 2.0),
725 (PriceSource::Close, 6.0, 4.0),
726 (PriceSource::HL2, 7.5, 4.5),
727 (PriceSource::HLC3, 7.0, 13.0 / 3.0),
728 (PriceSource::OHLC4, 6.25, 3.75),
729 ] {
730 let mut chart = Chart::new(BarData::from_bars(bars.clone()));
731 let id = chart.add_indicator(Box::new(SMA::new(2)));
732 let value = InputValue::Source(StudySource::Price(source));
733 chart
734 .indicator_mut(id)
735 .unwrap()
736 .set_input("source", value)
737 .unwrap();
738 settle(&mut chart);
739 assert_eq!(
740 chart.indicators().next().unwrap().1.values().value(0, 2),
741 Some(expected),
742 "{source:?}"
743 );
744 chart
745 .indicator_mut(id)
746 .unwrap()
747 .set_input("plot_offset", InputValue::Integer(1))
748 .unwrap();
749 settle(&mut chart);
750 let study = chart.indicators().next().unwrap().1;
751 assert!(
752 (study.plot_value(0, 2).unwrap() - shifted).abs() < 1e-12,
753 "{source:?}"
754 );
755 assert_eq!(study.values().value(0, 2), Some(expected));
756 }
757 let bars: Vec<_> = [1.0, 2.0, 1.0, 2.0]
759 .into_iter()
760 .enumerate()
761 .map(|(i, p)| Bar::new(start + Duration::minutes(i as i64), p, p, p, p, 1.0))
762 .collect();
763 let mut chart = Chart::new(BarData::from_bars(bars.clone()));
764 let mean = chart.add_indicator(Box::new(SMA::new(2)));
765 let rsi = chart.add_indicator(Box::new(RSI::new(1)));
766 let value = InputValue::Source(StudySource::Study { id: rsi.0, plot: 0 });
767 chart
768 .indicator_mut(mean)
769 .unwrap()
770 .set_input("source", value)
771 .unwrap();
772 chart.set_indicator_visible(rsi, false);
773 settle(&mut chart);
774 let output = |chart: &Chart, bar| {
775 chart
776 .indicators()
777 .find(|(id, _)| *id == mean)
778 .unwrap()
779 .1
780 .values()
781 .value(0, bar)
782 };
783 assert_eq!(output(&chart, 1), None);
784 assert!((output(&chart, 3).unwrap() - 50.0).abs() < 1e-8);
785 chart.update(
786 SeriesId::MAIN,
787 Bar::new(bars[3].time, 0.5, 0.5, 0.5, 0.5, 1.0),
788 );
789 settle(&mut chart);
790 assert_eq!(output(&chart, 3), Some(0.0));
791 chart
792 .indicator_mut(rsi)
793 .unwrap()
794 .set_input("period", InputValue::Length(2))
795 .unwrap();
796 settle(&mut chart);
797 assert!((output(&chart, 3).unwrap() - (50.0 + 100.0 / 3.0) / 2.0).abs() < 1e-8);
799 assert!(matches!(
800 chart.set_indicator_source(
801 rsi,
802 StudySource::Study {
803 id: mean.0,
804 plot: 0
805 }
806 ),
807 Err(StudyError::CyclicSource)
808 ));
809 #[cfg(feature = "csv")]
810 {
811 let csv = chart.export_csv().unwrap();
812 assert!(csv.contains(",41.67"), "{csv}");
813 }
814 chart.user_action(|chart| chart.remove_series(rsi));
815 settle(&mut chart);
816 assert_eq!(output(&chart, 3), None);
817 assert!(chart.undo());
818 settle(&mut chart);
819 assert!((output(&chart, 3).unwrap() - 125.0 / 3.0).abs() < 1e-8);
820 }
821 fn bar(seconds: i64, close: f64) -> Bar {
823 let t =
824 "2024-03-15T10:00:00Z".parse::<DateTime<Utc>>().unwrap() + TimeDelta::seconds(seconds);
825 Bar::new(t, close, close, close, close, 1.0)
826 }
827
828 struct Counted(RSI, Arc<AtomicUsize>, std::time::Duration);
830
831 impl Indicator for Counted {
832 fn name(&self) -> &str {
833 self.0.name()
834 }
835 fn calculate(&mut self, data: &[Bar]) {
836 if !data.is_empty() {
837 self.1.fetch_add(1, Ordering::Relaxed);
838 std::thread::sleep(self.2);
839 }
840 self.0.calculate(data);
841 }
842 fn update(&mut self, data: &[Bar], from: usize) {
843 if !data.is_empty() {
844 self.1.fetch_add(1, Ordering::Relaxed);
845 std::thread::sleep(self.2);
846 }
847 self.0.update(data, from);
848 }
849 fn values(&self) -> &crate::studies::IndicatorValues {
850 self.0.values()
851 }
852 fn colors(&self) -> Vec<egui::Color32> {
853 self.0.colors()
854 }
855 fn set_colors(&mut self, colors: Vec<egui::Color32>) {
856 self.0.set_colors(colors);
857 }
858 fn is_overlay(&self) -> bool {
859 self.0.is_overlay()
860 }
861 fn set_visible(&mut self, visible: bool) {
862 self.0.set_visible(visible);
863 }
864 fn clone_box(&self) -> Box<dyn Indicator> {
865 Box::new(Counted(self.0.clone(), Arc::clone(&self.1), self.2))
866 }
867 }
868
869 #[test]
873 fn full_recomputes_run_one_per_frame() {
874 let mut chart = Chart::new(BarData::from_bars(
875 (0..100).map(|i| bar(i * 60, i as f64)).collect(),
876 ));
877 let calls: Vec<_> = (0..3).map(|_| Arc::new(AtomicUsize::new(0))).collect();
878 for counter in &calls {
879 chart.add_indicator(Box::new(Counted(
880 RSI::new(14),
881 counter.clone(),
882 std::time::Duration::ZERO,
883 )));
884 counter.store(0, Ordering::Relaxed);
885 }
886 for (round, close) in [(0, 200.0), (1, 300.0)] {
888 if round == 1 {
889 chart.next_refresh.copy_from_slice(&[3.0, 2.0, 1.0]);
891 }
892 for counter in &calls {
893 counter.store(0, Ordering::Relaxed);
894 }
895 chart.set_data(
896 SeriesId::MAIN,
897 (0..100)
898 .map(|i| bar(i * 60, close + (i * 7 % 13) as f64))
899 .collect(),
900 );
901 for k in 1..=3 {
902 let now = if round == 0 { k as f64 } else { 3.0 } / 60.0;
903 let wait = chart.refresh_studies(now);
904 let counts: Vec<_> = calls
905 .iter()
906 .map(|counter| counter.load(Ordering::Relaxed))
907 .collect();
908 assert_eq!(counts.iter().sum::<usize>(), k, "round {round}, call {k}");
909 for (j, (_, study)) in chart.indicators().enumerate() {
910 assert!(counts[j] <= 1, "round {round}, call {k}, study {j}");
911 assert_eq!(
912 study.values().is_empty(),
913 counts[j] == 0,
914 "round {round}, call {k}, study {j}"
915 );
916 }
917 assert_eq!(wait, if k < 3 { Some(0.0) } else { None });
918 }
919 }
920 let mut fresh = RSI::new(14);
921 fresh.calculate(chart.bars(SeriesId::MAIN));
922 for (_, study) in chart.indicators() {
923 assert_eq!(
924 format!("{:?}", study.values()),
925 format!("{:?}", fresh.values())
926 );
927 }
928 }
929
930 #[test]
933 fn a_slow_full_recompute_does_not_take_every_frame() {
934 let mut chart = Chart::new(BarData::from_bars(
935 (0..100).map(|i| bar(i * 60, i as f64)).collect(),
936 ));
937 let calls = Arc::new(AtomicUsize::new(0));
938 chart.add_indicator(Box::new(Counted(
939 RSI::new(14),
940 calls.clone(),
941 std::time::Duration::from_millis(300),
942 )));
943 calls.store(0, Ordering::Relaxed);
944 let mut time = 0.0;
945 for i in 0..10 {
946 chart.update(SeriesId::MAIN, bar(99 * 60, 50.0 + i as f64));
947 let started = Instant::now();
948 chart.refresh_studies(time);
949 time += 1.0 / 60.0 + started.elapsed().as_secs_f64();
950 }
951 assert_eq!(calls.load(Ordering::Relaxed), 1);
952 }
953
954 #[test]
957 fn set_data_every_frame_recomputes_each_study_in_turn() {
958 for cost in [
959 std::time::Duration::ZERO,
960 std::time::Duration::from_millis(20),
961 ] {
962 let mut bars: Vec<Bar> = (0..100).map(|i| bar(i * 60, i as f64)).collect();
963 let mut chart = Chart::new(BarData::from_bars(bars.clone()));
964 let calls: Vec<_> = (0..3).map(|_| Arc::new(AtomicUsize::new(0))).collect();
965 for counter in &calls {
966 chart.add_indicator(Box::new(Counted(RSI::new(14), counter.clone(), cost)));
967 counter.store(0, Ordering::Relaxed);
968 }
969 for k in 0..120 {
970 bars.push(bar((100 + k) * 60, 50.0 + k as f64));
971 chart.set_data(SeriesId::MAIN, bars.clone());
972 chart.refresh_studies(k as f64 / 60.0);
973 }
974 let calls: Vec<_> = calls.iter().map(|c| c.load(Ordering::Relaxed)).collect();
976 assert!(
977 calls.iter().all(|&count| count > 0),
978 "cost {cost:?}: {calls:?}"
979 );
980 }
981 }
982
983 #[test]
989 fn slow_studies_refresh_four_times_a_second_and_end_current() {
990 let ctx = egui::Context::default();
991 let mut chart = Chart::new(BarData::from_bars(
992 (0..100).map(|i| bar(i * 60, i as f64)).collect(),
993 ));
994 let sma = chart.add_indicator(Box::new(SMA::new(5)));
995 let calls = Arc::new(AtomicUsize::new(0));
996 let rsi = chart.add_indicator(Box::new(Counted(
997 RSI::new(14),
998 Arc::clone(&calls),
999 std::time::Duration::ZERO,
1000 )));
1001 let values = |chart: &Chart, id| {
1002 let (_, study) = chart.indicators().find(|(study, _)| *study == id).unwrap();
1003 format!("{:?}", study.values())
1004 };
1005 let fresh = |chart: &Chart, mut study: Box<dyn Indicator>| {
1006 study.calculate(chart.bars(SeriesId::MAIN));
1007 format!("{:?}", study.values())
1008 };
1009 frame(&ctx, &mut chart, 0.0, Vec::new());
1010 calls.store(0, Ordering::Relaxed);
1011 let mut time = 0.0;
1012 for i in 0..60 {
1013 time += 1.0 / 60.0;
1014 chart.update(
1016 SeriesId::MAIN,
1017 bar((100 + i / 2) * 60, 50.0 + (i * 7 % 13) as f64),
1018 );
1019 frame(&ctx, &mut chart, time, Vec::new());
1020 assert_eq!(
1021 values(&chart, sma),
1022 fresh(&chart, Box::new(SMA::new(5))),
1023 "SMA at {i}"
1024 );
1025 }
1026 assert!(
1027 calls.load(Ordering::Relaxed) <= 4,
1028 "{calls:?} refreshes while streaming"
1029 );
1030 let streamed = calls.load(Ordering::Relaxed);
1031 let stop = time;
1032 while time < stop + 0.25 {
1033 time += 1.0 / 60.0;
1034 frame(&ctx, &mut chart, time, Vec::new());
1035 }
1036 assert_eq!(
1037 calls.load(Ordering::Relaxed),
1038 streamed + 1,
1039 "one refresh after the stream"
1040 );
1041 assert_eq!(
1042 values(&chart, rsi),
1043 fresh(&chart, Box::new(RSI::new(14))),
1044 "RSI"
1045 );
1046 }
1047
1048 #[test]
1051 fn the_study_limit_refuses_additions_beyond_it() {
1052 let mut chart = super::super::test_frames::chart(&[100.0], &[110.0]);
1053 let copied = {
1054 let id = chart.add_indicator(Box::new(SMA::new(9)));
1055 let text = chart.copy_indicator(id).unwrap();
1056 chart.remove_series(id);
1057 text
1058 };
1059 chart.set_study_limit(Some(2));
1060 for period in [3, 5] {
1061 assert!(chart.try_add_indicator(Box::new(SMA::new(period))).is_ok());
1062 }
1063 let undoable = chart.can_undo();
1064 let period = |study: Box<dyn crate::studies::Indicator>| {
1065 study
1066 .inputs()
1067 .into_iter()
1068 .find(|input| input.id == "period")
1069 .map(|input| input.value)
1070 };
1071 let Err(StudyError::StudyLimitReached(refused)) =
1072 chart.try_add_indicator(Box::new(SMA::new(7)))
1073 else {
1074 panic!("the cap refuses a third study");
1075 };
1076 assert_eq!(period(refused.into_inner()), Some(InputValue::Length(7)));
1077 let Err(StudyError::StudyLimitReached(refused)) =
1078 chart.paste_indicator(&copied, &crate::studies::IndicatorFactory::new())
1079 else {
1080 panic!("the cap refuses a pasted study");
1081 };
1082 assert_eq!(period(refused.into_inner()), Some(InputValue::Length(9)));
1083 assert_eq!(chart.indicators().count(), 2);
1084 assert_eq!(chart.can_undo(), undoable);
1085 chart.add_indicator(Box::new(SMA::new(7)));
1086 assert_eq!(chart.indicators().count(), 3);
1087 chart.set_study_limit(None);
1088 assert!(chart.try_add_indicator(Box::new(SMA::new(8))).is_ok());
1089 }
1090 #[test]
1091 fn api_errors_are_std_errors() {
1092 fn needs_error<E: std::error::Error + Send + Sync + 'static>() {}
1093 needs_error::<crate::model::ParseError>();
1094 needs_error::<crate::model::PnfBoxesError>();
1095 needs_error::<crate::widget::ExportError>();
1096 needs_error::<crate::studies::StudyError>();
1097 needs_error::<crate::widget::settings::ChartSettingsError>();
1098 #[cfg(feature = "ui")]
1099 needs_error::<crate::widget::RestoreError>();
1100 #[cfg(feature = "drawings")]
1101 needs_error::<crate::widget::DrawingError>();
1102 }
1103}