1use super::{Indicator, IndicatorLineStyle, IndicatorValues, InputValue};
4use crate::model::LineStyle;
5use crate::model::{Bar, Instrument, Timeframe};
6use egui::Color32;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
10#[non_exhaustive]
11pub enum PlotType {
12 #[default]
14 Line,
15 Histogram,
17 Circles,
19 StepLine,
21 Columns,
23 Crosses,
25 Area,
27 TriangleUp,
29 TriangleDown,
31 BarColor,
33 BackgroundColor,
35}
36
37impl PlotType {
38 pub const ALL: [Self; 11] = [
40 Self::Line,
41 Self::StepLine,
42 Self::Histogram,
43 Self::Columns,
44 Self::Area,
45 Self::Circles,
46 Self::Crosses,
47 Self::TriangleUp,
48 Self::TriangleDown,
49 Self::BarColor,
50 Self::BackgroundColor,
51 ];
52
53 pub fn name(self) -> &'static str {
55 match self {
56 Self::Line => "Line",
57 Self::StepLine => "Step line",
58 Self::Histogram => "Histogram",
59 Self::Columns => "Columns",
60 Self::Area => "Area",
61 Self::Circles => "Circles",
62 Self::Crosses => "Crosses",
63 Self::TriangleUp => "Triangle up",
64 Self::TriangleDown => "Triangle down",
65 Self::BarColor => "Bar colour",
66 Self::BackgroundColor => "Background colour",
67 }
68 }
69
70 pub fn incompatibility(self, overlay: bool, price_scale: bool) -> Option<&'static str> {
74 match self {
75 Self::BarColor if !overlay => {
76 Some("Bar colour repaints the price bars, so the study must be on the price chart.")
77 }
78 Self::Histogram | Self::Columns | Self::Area if overlay && price_scale => Some(
79 "This type draws from zero, which is far from the prices on the price scale. \
80 Give the study its own scale first.",
81 ),
82 _ => None,
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
89pub struct PlotStyle {
90 pub color: Color32,
92 pub width: f32,
94 pub pattern: LineStyle,
96 pub plot_type: PlotType,
98 pub visible: bool,
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
104#[non_exhaustive]
105pub struct LevelStyle {
106 pub value: Option<f64>,
109 pub color: Color32,
111 pub pattern: LineStyle,
113 pub visible: bool,
115}
116
117pub(super) fn declared_levels<S: Indicator + ?Sized>(
119 study: &S,
120) -> (Vec<LevelStyle>, bool, Color32) {
121 let pane = study.pane();
122 let levels = pane
123 .levels
124 .iter()
125 .map(|&(value, color, _)| LevelStyle {
126 value: None,
127 color,
128 pattern: study.level_style(value),
129 visible: true,
130 })
131 .collect();
132 (levels, pane.fill_between_levels, study.level_fill_color())
133}
134
135fn match_declared_levels(study: &dyn Indicator, settings: &mut StudySettings) {
137 let (levels, fill, color) = declared_levels(study);
138 if settings.levels.len() != levels.len() {
139 settings.levels = levels;
140 settings.fill_between_levels = fill;
141 settings.level_fill_color = color;
142 }
143}
144
145#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
149pub enum StudyIntervalRange {
150 #[default]
152 All,
153 AndAbove(Timeframe),
155 AndBelow(Timeframe),
157 Only(Timeframe),
159}
160
161impl StudyIntervalRange {
162 pub fn contains(self, interval: Timeframe) -> bool {
164 let current = match self {
165 Self::All => return true,
166 Self::Only(current) => return interval == current,
167 Self::AndAbove(current) | Self::AndBelow(current) => current,
168 };
169 let order = match (interval, current) {
170 (Timeframe::Ticks(a), Timeframe::Ticks(b))
171 | (Timeframe::Volume(a), Timeframe::Volume(b))
172 | (Timeframe::Range(a), Timeframe::Range(b))
173 | (Timeframe::Delta(a), Timeframe::Delta(b)) => Some(a.cmp(&b)),
174 (a, b) if !a.is_trade_based() && !b.is_trade_based() => {
175 Some(a.duration_ms().cmp(&b.duration_ms()))
176 }
177 _ => None,
178 };
179 order.is_some_and(|order| match self {
180 Self::AndAbove(_) => !order.is_lt(),
181 Self::AndBelow(_) => !order.is_gt(),
182 _ => false,
183 })
184 }
185}
186
187#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
189pub struct StudySettings {
190 pub plots: Vec<PlotStyle>,
192 pub precision: Option<usize>,
194 pub hidden_intervals: Vec<Timeframe>,
196 #[serde(default)]
199 pub interval_range: StudyIntervalRange,
200 #[serde(default)]
204 pub levels: Vec<LevelStyle>,
205 #[serde(default)]
207 pub fill_between_levels: bool,
208 #[serde(default)]
210 pub level_fill_color: Color32,
211 #[serde(default)]
217 pub untagged_plots: Vec<usize>,
218}
219
220#[derive(Clone)]
223pub struct StudySettingsSnapshot {
224 inputs: Vec<(String, InputValue)>,
225 settings: StudySettings,
226 timeframe: Option<Timeframe>,
227 colors: Vec<Color32>,
228 visible: bool,
229 overlay: Option<bool>,
230 scale: Option<super::StudyScale>,
231 source: Option<super::StudySource>,
232 inherited_overlay: bool,
233 inherited_scale: super::StudyScale,
234 inherited_source: Option<super::StudySource>,
235 calculation_symbol: String,
236 calculation_timeframe: Option<Timeframe>,
237 wait_for_close: bool,
238 offset: i32,
239}
240
241impl PartialEq for StudySettingsSnapshot {
242 fn eq(&self, other: &Self) -> bool {
243 self.inputs == other.inputs
244 && self.settings == other.settings
245 && self.colors == other.colors
246 && self.visible == other.visible
247 && self.overlay.unwrap_or(self.inherited_overlay)
248 == other.overlay.unwrap_or(other.inherited_overlay)
249 && self.scale.unwrap_or(self.inherited_scale)
250 == other.scale.unwrap_or(other.inherited_scale)
251 && self.source.or(self.inherited_source) == other.source.or(other.inherited_source)
252 && self.calculation_symbol == other.calculation_symbol
253 && self.calculation_timeframe == other.calculation_timeframe
254 && self.wait_for_close == other.wait_for_close
255 && self.offset == other.offset
256 }
257}
258
259impl StudySettingsSnapshot {
260 pub fn capture(study: &(impl Indicator + ?Sized), visible: bool) -> Self {
263 Self {
264 inputs: study
265 .inputs()
266 .into_iter()
267 .map(|input| (input.id, input.value))
268 .collect(),
269 settings: study.settings(),
270 timeframe: None,
271 colors: study.colors(),
272 visible,
273 overlay: Some(study.is_overlay()),
274 scale: Some(study.scale()),
275 source: None,
276 inherited_overlay: study.is_overlay(),
277 inherited_scale: study.scale(),
278 inherited_source: study.source(),
279 calculation_symbol: String::new(),
280 calculation_timeframe: None,
281 wait_for_close: true,
282 offset: 0,
283 }
284 }
285
286 pub(super) fn restore(
287 &self,
288 study: &mut (impl Indicator + ?Sized),
289 ) -> Result<(), crate::studies::StudyError> {
290 let current = study.inputs();
291 let inputs: Vec<_> = self
292 .inputs
293 .iter()
294 .filter(|(id, value)| {
295 !current
296 .iter()
297 .any(|input| input.id == *id && input.value == *value)
298 })
299 .cloned()
300 .collect();
301 if !inputs.is_empty() || study.settings() != self.settings {
302 study.apply_settings(&inputs, self.settings.clone())?;
303 }
304 study.set_colors(self.colors.clone());
305 study.set_visible(self.visible);
306 if let Some(overlay) = self.overlay {
307 study.set_overlay(overlay);
308 }
309 if let Some(scale) = self.scale {
310 study.set_scale(scale);
311 }
312 Ok(())
313 }
314}
315
316#[derive(Clone)]
323pub struct ConfiguredStudy {
324 study: Box<dyn Indicator>,
325 settings: StudySettings,
326 timeframe: Option<Timeframe>,
327 instrument_precision: Option<u8>,
328 formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
329 overlay: Option<bool>,
330 scale: Option<super::StudyScale>,
331 scale_mapping: Option<crate::scales::PriceMap>,
332 source: Option<super::StudySource>,
333 calculation_symbol: String,
334 calculation_timeframe: Option<Timeframe>,
335 wait_for_close: bool,
336 offset: i32,
337 clock: crate::model::BarClock,
338 aligned: Option<IndicatorValues>,
339 aligned_bars: Vec<Option<usize>>,
340 spans: Vec<std::ops::Range<usize>>,
343 aligned_markers: Vec<super::IndicatorMarker>,
345 aligned_zones: Vec<super::StudyZone>,
346}
347
348impl ConfiguredStudy {
349 pub fn new(study: Box<dyn Indicator>) -> Self {
351 let mut settings = study.settings();
352 if study.is_volume()
354 && let Some(plot) = settings.plots.get_mut(2)
355 {
356 plot.visible = true;
357 }
358 match_declared_levels(study.as_ref(), &mut settings);
359 Self {
360 study,
361 settings,
362 timeframe: None,
363 instrument_precision: None,
364 formatter: None,
365 overlay: None,
366 scale: None,
367 scale_mapping: None,
368 source: None,
369 calculation_symbol: String::new(),
370 calculation_timeframe: None,
371 wait_for_close: true,
372 offset: 0,
373 clock: crate::model::BarClock::new(Timeframe::Min1),
374 aligned: None,
375 aligned_bars: Vec::new(),
376 spans: Vec::new(),
377 aligned_markers: Vec::new(),
378 aligned_zones: Vec::new(),
379 }
380 }
381 fn runs_on_other_bars(&self) -> bool {
384 self.study.source().is_some()
385 || (self.study.data_requests().is_empty() && !self.study.needs_order_flow())
386 }
387 fn update_price(&mut self, data: &[Bar], from: usize) {
388 self.aligned = None;
389 match self.source() {
390 Some(super::StudySource::Price(source)) if self.source() != self.study.source() => {
391 let values: Vec<_> = data
392 .iter()
393 .map(|bar| source.compute(bar.open, bar.high, bar.low, bar.close))
394 .collect();
395 self.study.update_source(data, from, &values);
396 }
397 Some(super::StudySource::Study { .. }) => self.study.update_source(data, from, &[]),
398 _ => self.study.update(data, from),
399 }
400 }
401}
402
403impl Indicator for ConfiguredStudy {
404 fn projections(&self) -> &[super::StudyProjection] {
405 self.study.projections()
406 }
407
408 fn table(&self) -> Option<&super::StudyTable> {
409 self.study.table()
410 }
411
412 fn has_settings(&self) -> bool {
413 true
414 }
415
416 fn name(&self) -> &str {
417 self.study.name()
418 }
419 fn desc(&self) -> &str {
420 self.study.desc()
421 }
422 fn source(&self) -> Option<super::StudySource> {
423 self.source.or_else(|| self.study.source())
424 }
425 fn inputs(&self) -> Vec<super::StudyInput> {
426 let mut inputs = self.study.inputs();
427 if let Some(default) = self.study.source() {
428 let initial = inputs
429 .iter()
430 .find(|input| input.id == "source")
431 .and_then(|input| match input.default {
432 InputValue::Choice(index) => crate::model::PriceSource::all()
433 .get(index)
434 .copied()
435 .map(super::StudySource::Price),
436 InputValue::Source(value) => Some(value),
437 _ => None,
438 })
439 .unwrap_or(default);
440 inputs.retain(|input| input.id != "source");
441 inputs.push(
442 super::StudyInput::new(
443 "source",
444 "Source",
445 InputValue::Source(self.source.unwrap_or(default)),
446 super::InputKind::Source,
447 )
448 .with_default(InputValue::Source(initial)),
449 );
450 }
451 use super::{InputKind, StudyInput};
452 if self.study.source().is_some() {
453 inputs.extend([
454 StudyInput::new(
455 "plot_offset",
456 "Offset",
457 InputValue::Integer(self.offset),
458 InputKind::Integer(i32::MIN, i32::MAX),
459 )
460 .with_default(InputValue::Integer(0)),
461 StudyInput::new(
462 "calculation_symbol",
463 "Symbol",
464 InputValue::Symbol(self.calculation_symbol.clone()),
465 InputKind::Symbol,
466 )
467 .with_default(InputValue::Symbol(String::new())),
468 ]);
469 }
470 if self.runs_on_other_bars() {
471 inputs.extend([
472 StudyInput::new(
473 "calculation_timeframe",
474 "Interval",
475 InputValue::Timeframe(self.calculation_timeframe),
476 InputKind::Timeframe,
477 )
478 .with_default(InputValue::Timeframe(None)),
479 StudyInput::new(
480 "wait_for_close",
481 "Wait for interval close",
482 InputValue::Toggle(self.wait_for_close),
483 InputKind::Toggle,
484 )
485 .with_default(InputValue::Toggle(true)),
486 ]);
487 }
488 inputs
489 }
490 fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
491 if id == "plot_offset" && self.study.source().is_some() {
492 let InputValue::Integer(offset) = value else {
493 return Err(crate::studies::StudyError::invalid_input(id));
494 };
495 self.offset = offset;
496 self.aligned = None;
497 self.study.calculate(&[]);
498 return Ok(());
499 }
500 if match id {
501 "calculation_symbol" => self.study.source().is_some(),
502 "calculation_timeframe" | "wait_for_close" => self.runs_on_other_bars(),
503 _ => false,
504 } {
505 let schema = self
506 .inputs()
507 .into_iter()
508 .find(|input| input.id == id)
509 .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
510 if !schema.accepts(&value) {
511 return Err(crate::studies::StudyError::invalid_input(id));
512 }
513 if matches!(self.source(), Some(super::StudySource::Study { .. }))
514 && !matches!(&value, InputValue::Symbol(s) if s.is_empty())
515 && value != InputValue::Timeframe(None)
516 && id != "wait_for_close"
517 {
518 return Err(super::StudyError::InvalidInput {
519 id: id.into(),
520 reason: "must follow the chart while the source is another study's output"
521 .into(),
522 });
523 }
524 match value {
525 InputValue::Symbol(value) => self.calculation_symbol = value,
526 InputValue::Timeframe(value) => self.calculation_timeframe = value,
527 InputValue::Toggle(value) => self.wait_for_close = value,
528 _ => unreachable!(),
529 }
530 self.aligned = None;
531 self.study.calculate(&[]);
532 return Ok(());
533 }
534 if id == "source" && self.study.source().is_some() {
535 if matches!(value, InputValue::Choice(_)) {
536 self.study.set_input(id, value)?;
537 self.source = None;
538 self.aligned = None;
539 return Ok(());
540 }
541 let InputValue::Source(source) = value else {
542 return Err(crate::studies::StudyError::invalid_input(id));
543 };
544 if matches!(source, super::StudySource::Study { .. })
545 && (!self.calculation_symbol.is_empty() || self.calculation_timeframe.is_some())
546 {
547 return Err(super::StudyError::InvalidInput {
548 id: id.into(),
549 reason: "cannot be another study's output while the study runs on another symbol or interval".into(),
550 });
551 }
552 self.source = Some(source);
553 self.aligned = None;
554 self.study.calculate(&[]);
555 Ok(())
556 } else {
557 let format = self.study.value_format();
558 self.study.set_input(id, value)?;
559 if self.study.value_format() != format {
560 self.formatter = None;
561 }
562 self.aligned = None;
563 Ok(())
564 }
565 }
566 fn data_requests(&self) -> Vec<super::StudyDataRequest> {
567 if !self.calculation_symbol.is_empty() || self.calculation_timeframe.is_some() {
568 vec![super::StudyDataRequest {
569 metric: None,
570 symbol: self.calculation_symbol.clone(),
571 timeframe: self.calculation_timeframe,
572 }]
573 } else {
574 self.study.data_requests()
575 }
576 }
577 fn data_start(
578 &self,
579 request: &super::StudyDataRequest,
580 ) -> Option<chrono::DateTime<chrono::Utc>> {
581 if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
582 self.study.data_start(request)
583 } else {
584 None
585 }
586 }
587 fn update_data(&mut self, data: &[Bar], from: usize, sources: &[super::StudyData<'_>]) {
588 if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
589 if self.study.data_requests().is_empty() {
590 self.update(data, from);
591 } else {
592 self.study.update_data(data, from, sources);
593 }
594 return;
595 }
596 let request = &self.data_requests()[0];
597 let Some(source) = sources.iter().find(|source| &source.request == request) else {
598 self.aligned = Some(IndicatorValues::missing(data.len()));
599 self.aligned_bars.clear();
600 self.spans.clear();
601 self.aligned_markers.clear();
602 self.aligned_zones.clear();
603 return;
604 };
605 self.instrument_precision = source
606 .instrument
607 .and_then(crate::scales::instrument_price_precision);
608 self.study.set_instrument(source.instrument);
609 self.study.set_timeframe(source.timeframe);
610 self.study.set_bar_clock(&source.clock);
611 self.update_price(source.bars, 0);
612 let mut aligned = IndicatorValues::new();
613 self.aligned_bars.clear();
614 for (at, bar) in data.iter().enumerate() {
615 let forming_higher = at + 1 == data.len()
616 && self.wait_for_close
617 && self
618 .clock
619 .timeframe()
620 .is_some_and(|tf| source.timeframe.duration_ms() > tf.duration_ms());
621 let close = if forming_higher {
622 bar.time
623 } else {
624 self.clock
625 .next_open(self.clock.bar_open(bar.time))
626 .unwrap_or(bar.time)
627 };
628 let output = source.bar_at(bar.time).and_then(|_| {
629 source.output_at(close, !self.wait_for_close && at + 1 == data.len())
630 });
631 self.aligned_bars.push(output);
632 let values: Vec<_> = (0..self.study.line_count())
633 .map(|line| {
634 output
635 .and_then(|at| self.study.values().value(line, at))
636 .unwrap_or(f64::NAN)
637 })
638 .collect();
639 aligned.push(super::IndicatorValue::Multiple(values));
640 }
641 self.aligned = Some(aligned);
642 self.spans = spans_of(data, &self.clock, source);
643 let shown = self.aligned_bars.iter().flatten().max().copied();
645 self.aligned_markers = self
646 .study
647 .markers()
648 .iter()
649 .filter(|marker| shown >= Some(marker.bar))
650 .filter_map(|marker| {
651 let bars = chart_bars(&self.spans, &(marker.bar..marker.bar.saturating_add(1)))?;
652 Some(super::IndicatorMarker {
653 bar: bars.end - 1,
654 ..*marker
655 })
656 })
657 .collect();
658 self.aligned_zones = self
659 .study
660 .zones()
661 .iter()
662 .filter_map(|zone| {
663 Some(super::StudyZone {
664 bars: chart_bars(&self.spans, &zone.bars)?,
665 ..zone.clone()
666 })
667 })
668 .collect();
669 }
670 fn order_flow_inputs(&self) -> &[crate::data::OrderFlowInput] {
671 self.study.order_flow_inputs()
672 }
673 fn needs_order_flow(&self) -> bool {
674 self.study.needs_order_flow()
675 }
676
677 fn calculate(&mut self, data: &[Bar]) {
678 self.update(data, 0);
679 }
680 fn update(&mut self, data: &[Bar], from: usize) {
681 if !self.data_requests().is_empty() {
682 self.update_data(data, from, &[]);
683 } else {
684 self.update_price(data, from);
685 }
686 }
687 fn update_source(&mut self, data: &[Bar], from: usize, source: &[f64]) {
688 self.study.update_source(data, from, source);
689 }
690 fn is_incremental(&self) -> bool {
691 self.data_requests().is_empty()
692 && self.source() == self.study.source()
693 && self.study.is_incremental()
694 }
695 fn set_instrument(&mut self, instrument: Option<&Instrument>) -> bool {
696 if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
697 self.instrument_precision =
698 instrument.and_then(crate::scales::instrument_price_precision);
699 self.study.set_instrument(instrument)
700 } else {
701 false
702 }
703 }
704 fn set_time_scale_events(&mut self, events: &[crate::model::TimeScaleEvent]) -> bool {
705 self.study
707 .set_time_scale_events(if self.calculation_symbol.is_empty() {
708 events
709 } else {
710 &[]
711 })
712 }
713 fn set_timeframe(&mut self, timeframe: Timeframe) {
714 self.timeframe = Some(timeframe);
715 self.clock = crate::model::BarClock::new(timeframe);
716 if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
717 self.study.set_timeframe(timeframe);
718 }
719 }
720 fn set_visible_range(&mut self, range: std::ops::Range<usize>) -> bool {
721 self.study.set_visible_range(range)
722 }
723 fn set_bar_clock(&mut self, clock: &crate::model::BarClock) -> bool {
724 self.clock = clock.clone();
725 if self.calculation_symbol.is_empty() && self.calculation_timeframe.is_none() {
726 self.study.set_bar_clock(clock)
727 } else {
728 false
729 }
730 }
731 #[cfg(feature = "drawings")]
732 fn drawings(&self) -> &[crate::drawings::Drawing] {
733 self.study.drawings()
734 }
735 fn candles(&self) -> &[super::StudyCandle] {
736 self.study.candles()
737 }
738 fn tpo(&self) -> Option<&crate::model::TPOConfig> {
739 self.study.tpo()
740 }
741 fn values(&self) -> &IndicatorValues {
742 self.aligned.as_ref().unwrap_or_else(|| self.study.values())
743 }
744 fn colors(&self) -> Vec<Color32> {
745 self.settings.plots.iter().map(|plot| plot.color).collect()
746 }
747 fn set_colors(&mut self, colors: Vec<Color32>) {
748 self.study.set_colors(colors);
749 for (plot, color) in self.settings.plots.iter_mut().zip(self.study.colors()) {
750 plot.color = color;
751 }
752 }
753 fn set_color(&mut self, line: usize, color: Color32) {
754 if let Some(plot) = self.settings.plots.get_mut(line) {
755 self.study.set_color(line, color);
756 plot.color = self.study.plot_style(line).color;
757 }
758 }
759 fn line_style(&self, line: usize) -> IndicatorLineStyle {
760 let mut style = self.study.line_style(line);
761 if let Some(plot) = self.settings.plots.get(line) {
762 style.width = plot.width;
763 }
764 style
765 }
766 fn plot_style(&self, line: usize) -> PlotStyle {
767 let mut plot = self.settings.plots.get(line).copied().unwrap_or_else(|| {
768 let mut plot = self.study.plot_style(line);
769 let output = self.settings.plots.get(self.study.plot_output(line));
770 plot.visible &= output.is_none_or(|output| output.visible);
771 plot
772 });
773 if self.study.is_volume() {
774 plot.visible &= self.study.plot_style(line).visible;
775 }
776 plot
777 }
778 fn plot_tagged(&self, line: usize) -> bool {
779 if line < self.settings.plots.len() {
780 !self.settings.untagged_plots.contains(&line)
781 } else {
782 self.study.plot_tagged(line)
783 }
784 }
785 fn plot_offset(&self, line: usize) -> isize {
786 self.study
787 .plot_offset(line)
788 .saturating_add(self.offset as isize)
789 }
790 fn plot_value(&self, line: usize, bar: usize) -> Option<f64> {
791 if let Some(values) = &self.aligned {
792 values.value(
793 line,
794 bar.checked_add_signed(self.plot_offset(line).checked_neg()?)?,
795 )
796 } else {
797 self.study.plot_value(
798 line,
799 bar.checked_add_signed((self.offset as isize).checked_neg()?)?,
800 )
801 }
802 }
803 fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
804 let bar = if self.aligned.is_some() {
805 self.aligned_bars.get(bar).copied().flatten()?
806 } else {
807 bar
808 };
809 self.study.plot_color(line, bar)
810 }
811 fn plot_text(&self, line: usize, bar: usize) -> Option<String> {
812 let bar = if self.aligned.is_some() {
813 self.aligned_bars.get(bar).copied().flatten()?
814 } else {
815 bar
816 };
817 self.study.plot_text(line, bar)
818 }
819 fn fills(&self) -> Vec<super::StudyFill> {
820 self.study.fills()
821 }
822 fn snapshot_settings(&self) -> StudySettingsSnapshot {
823 let mut snapshot =
824 StudySettingsSnapshot::capture(self.study.as_ref(), self.study.is_visible());
825 snapshot.settings = self.settings.clone();
826 snapshot.timeframe = self.timeframe;
827 snapshot.overlay = self.overlay;
828 snapshot.scale = self.scale;
829 snapshot.source = self.source;
830 snapshot.calculation_symbol = self.calculation_symbol.clone();
831 snapshot.calculation_timeframe = self.calculation_timeframe;
832 snapshot.wait_for_close = self.wait_for_close;
833 snapshot.offset = self.offset;
834 snapshot
835 }
836 fn restore_settings(
837 &mut self,
838 snapshot: &StudySettingsSnapshot,
839 ) -> Result<(), crate::studies::StudyError> {
840 let mut inputs: Vec<_> = snapshot.inputs.iter().collect();
841 while !inputs.is_empty() {
842 let remaining = inputs.len();
843 let mut error = None;
844 inputs.retain(|(id, value)| {
846 if self
847 .study
848 .inputs()
849 .iter()
850 .any(|input| input.id == *id && input.value == *value)
851 {
852 return false;
853 }
854 if let Err(message) = self.study.set_input(id, value.clone()) {
855 error = Some(message);
856 true
857 } else {
858 false
859 }
860 });
861 if inputs.len() == remaining {
862 return Err(error.expect("a retained input failed to apply"));
863 }
864 }
865 self.study.set_colors(snapshot.colors.clone());
866 self.study.set_visible(snapshot.visible);
867 self.study.set_label_precision(snapshot.settings.precision);
868 self.settings = snapshot.settings.clone();
869 self.timeframe = snapshot.timeframe;
870 self.overlay = snapshot.overlay;
871 self.scale = snapshot.scale;
872 self.scale_mapping = None;
873 self.source = snapshot.source;
874 self.calculation_symbol = snapshot.calculation_symbol.clone();
875 self.calculation_timeframe = snapshot.calculation_timeframe;
876 self.wait_for_close = snapshot.wait_for_close;
877 self.offset = snapshot.offset;
878 self.aligned = None;
879 self.aligned_bars.clear();
880 Ok(())
881 }
882 fn settings(&self) -> StudySettings {
883 self.settings.clone()
884 }
885 fn apply_settings(
886 &mut self,
887 inputs: &[(String, InputValue)],
888 mut settings: StudySettings,
889 ) -> Result<(), crate::studies::StudyError> {
890 if let Some(precision) = settings.precision.filter(|precision| *precision > 12) {
891 return Err(super::StudyError::InvalidPrecision(precision));
892 }
893 if let Some((plot, style)) = settings
894 .plots
895 .iter()
896 .enumerate()
897 .find(|(_, plot)| !plot.width.is_finite() || !(0.5..=10.0).contains(&plot.width))
898 {
899 return Err(super::StudyError::InvalidPlotWidth {
900 plot,
901 width: style.width,
902 });
903 }
904 if let Some((level, value)) =
905 settings
906 .levels
907 .iter()
908 .enumerate()
909 .find_map(|(level, style)| {
910 style
911 .value
912 .filter(|value| !value.is_finite())
913 .map(|value| (level, value))
914 })
915 {
916 return Err(super::StudyError::InvalidLevelValue { level, value });
917 }
918 let mut edited = self.clone();
919 let mut inputs: Vec<_> = inputs.iter().collect();
920 if inputs.iter().any(|(id, value)| {
921 id == "source" && matches!(value, InputValue::Source(super::StudySource::Study { .. }))
922 }) {
923 inputs.sort_by_key(|(id, _)| {
924 !matches!(id.as_str(), "calculation_symbol" | "calculation_timeframe")
925 });
926 }
927 for (id, value) in inputs {
928 edited.set_input(id, value.clone())?;
929 }
930 if settings.plots.len() == self.line_count() && edited.line_count() != self.line_count() {
933 let defaults = edited.study.settings();
934 let kept = settings.plots.len().min(defaults.plots.len());
935 settings.plots.truncate(kept);
936 settings.plots.extend_from_slice(&defaults.plots[kept..]);
937 settings.untagged_plots.retain(|&line| line < kept);
938 settings
939 .untagged_plots
940 .extend(defaults.untagged_plots.iter().filter(|&&line| line >= kept));
941 }
942 if settings.plots.len() != edited.line_count() {
943 return Err(super::StudyError::PlotCount {
944 expected: edited.line_count(),
945 found: settings.plots.len(),
946 });
947 }
948 settings
949 .untagged_plots
950 .retain(|&line| line < settings.plots.len());
951 settings.untagged_plots.sort_unstable();
952 settings.untagged_plots.dedup();
953 match_declared_levels(edited.study.as_ref(), &mut settings);
954 for (line, plot) in settings.plots.iter().enumerate() {
956 if plot.color != edited.study.plot_style(line).color {
957 edited.study.set_color(line, plot.color);
958 }
959 }
960 if self.study.value_format() != edited.study.value_format() {
961 self.formatter = None;
962 }
963 self.study = edited.study;
964 self.study.set_label_precision(settings.precision);
965 self.source = edited.source;
966 self.calculation_symbol = edited.calculation_symbol;
967 self.calculation_timeframe = edited.calculation_timeframe;
968 self.wait_for_close = edited.wait_for_close;
969 self.offset = edited.offset;
970 self.aligned = None;
971 self.settings = settings;
972 Ok(())
973 }
974 fn scale_mapping(&self) -> Option<crate::scales::PriceMap> {
975 self.scale_mapping
976 }
977 fn set_scale_mapping(&mut self, mapping: Option<crate::scales::PriceMap>) {
978 self.scale_mapping = mapping;
979 }
980 fn set_value_formatter(
981 &mut self,
982 formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
983 ) -> bool {
984 self.formatter = formatter;
985 true
986 }
987 fn format_value(&self, value: f64) -> String {
988 self.formatter
989 .as_ref()
990 .filter(|_| value.is_finite())
991 .map_or_else(
992 || {
993 if self.precision() == self.study.precision() {
994 self.study.format_value(value)
995 } else {
996 self.value_format().format(value, self.precision())
997 }
998 },
999 |formatter| formatter.format(value),
1000 )
1001 }
1002 fn format_plot_value(&self, line: usize, value: f64) -> String {
1003 if self.formatter.is_some() || self.precision() != self.study.precision() {
1004 self.format_value(value)
1005 } else {
1006 self.study.format_plot_value(line, value)
1007 }
1008 }
1009 fn legend_visible(&self, line: usize) -> bool {
1010 self.plot_style(line).visible
1011 || (self.study.legend_visible(line) && !self.study.plot_style(line).visible)
1012 }
1013 fn value_format(&self) -> crate::scales::StudyFormat {
1014 self.study.value_format()
1015 }
1016 fn scale(&self) -> super::StudyScale {
1017 self.scale.unwrap_or_else(|| self.study.scale())
1018 }
1019 fn precision(&self) -> Option<usize> {
1020 self.settings
1021 .precision
1022 .or_else(|| self.study.precision())
1023 .or_else(|| {
1024 (self.value_format() == crate::scales::StudyFormat::Price)
1025 .then_some(self.instrument_precision.map(usize::from))
1026 .flatten()
1027 })
1028 }
1029 fn line_count(&self) -> usize {
1030 self.study.line_count()
1031 }
1032 fn line_names(&self) -> Vec<String> {
1033 self.study.line_names()
1034 }
1035 fn projection_fill_color(&self, projection: &super::StudyProjection) -> Color32 {
1036 self.study.projection_fill_color(projection)
1037 }
1038 fn plot_label_colors(&self, line: usize) -> Option<(Color32, Color32)> {
1039 self.study
1040 .plot_label_colors(line)
1041 .map(|(_, text)| (self.plot_style(line).color, text))
1042 }
1043 fn marker_style(&self) -> Option<PlotStyle> {
1044 self.study.marker_style()
1045 }
1046 fn markers(&self) -> &[super::IndicatorMarker] {
1047 if self.aligned.is_some() {
1048 &self.aligned_markers
1049 } else {
1050 self.study.markers()
1051 }
1052 }
1053 fn volume_profiles(&self, visible: std::ops::Range<usize>) -> Vec<crate::data::StudyProfile> {
1054 if self.aligned.is_none() {
1055 return self.study.volume_profiles(visible);
1056 }
1057 self.study
1059 .volume_profiles(source_bars(&self.spans, &visible))
1060 .into_iter()
1061 .filter_map(|mut plot| {
1062 plot.bars = chart_bars(&self.spans, &plot.bars)?;
1063 Some(plot)
1064 })
1065 .collect()
1066 }
1067 fn zones(&self) -> &[super::StudyZone] {
1068 if self.aligned.is_some() {
1069 &self.aligned_zones
1070 } else {
1071 self.study.zones()
1072 }
1073 }
1074 fn zones_cover_whole_bars(&self) -> bool {
1075 self.study.zones_cover_whole_bars()
1076 }
1077 fn labels(&self) -> &[super::StudyLabel] {
1078 if self.aligned.is_some() {
1080 &[]
1081 } else {
1082 self.study.labels()
1083 }
1084 }
1085 fn labels_on_price(&self) -> bool {
1086 self.study.labels_on_price()
1087 }
1088 fn set_label_precision(&mut self, precision: Option<usize>) {
1089 self.study.set_label_precision(precision);
1090 }
1091 fn provisional_after(&self, line: usize) -> Option<usize> {
1092 self.aligned
1093 .is_none()
1094 .then(|| self.study.provisional_after(line))
1095 .flatten()
1096 }
1097 fn extend_right(&self) -> bool {
1098 self.study.extend_right()
1099 }
1100 fn plot_label(&self) -> Option<(usize, String)> {
1101 self.study.plot_label()
1102 }
1103 fn is_volume(&self) -> bool {
1104 self.study.is_volume()
1105 }
1106 fn level_style(&self, value: f64) -> LineStyle {
1107 self.study
1109 .pane()
1110 .levels
1111 .iter()
1112 .zip(&self.settings.levels)
1113 .find(|((declared, ..), style)| {
1114 style.visible && style.value.unwrap_or(*declared) == value
1115 })
1116 .map_or_else(|| self.study.level_style(value), |(_, style)| style.pattern)
1117 }
1118 fn level_fill_color(&self) -> Color32 {
1119 self.settings.level_fill_color
1120 }
1121 fn pane(&self) -> super::StudyPane {
1122 let mut pane = self.study.pane();
1123 for ((value, color, _), style) in pane.levels.iter_mut().zip(&self.settings.levels) {
1124 *value = style.value.unwrap_or(*value);
1125 *color = if style.visible {
1127 style.color
1128 } else {
1129 Color32::TRANSPARENT
1130 };
1131 }
1132 pane.fill_between_levels = self.settings.fill_between_levels;
1133 pane
1134 }
1135 fn is_overlay(&self) -> bool {
1136 self.overlay.unwrap_or_else(|| self.study.is_overlay())
1137 }
1138 fn set_overlay(&mut self, overlay: bool) -> bool {
1139 self.overlay = Some(overlay);
1140 true
1141 }
1142 fn set_scale(&mut self, scale: super::StudyScale) -> bool {
1143 self.scale_mapping = None;
1144 self.scale = Some(scale);
1145 true
1146 }
1147 fn is_visible(&self) -> bool {
1148 self.study.is_visible()
1149 && self.timeframe.is_none_or(|tf| {
1150 !self.settings.hidden_intervals.contains(&tf)
1151 && self.settings.interval_range.contains(tf)
1152 })
1153 }
1154 fn set_visible(&mut self, visible: bool) {
1155 self.study.set_visible(visible);
1156 }
1157 fn clone_box(&self) -> Box<dyn Indicator> {
1158 Box::new(self.clone())
1159 }
1160}
1161
1162fn spans_of(
1166 data: &[Bar],
1167 clock: &crate::model::BarClock,
1168 source: &super::StudyData<'_>,
1169) -> Vec<std::ops::Range<usize>> {
1170 let close = |by: &crate::model::BarClock, bars: &[Bar], at: usize| {
1171 by.next_open(by.bar_open(bars[at].time))
1172 .or_else(|| bars.get(at + 1).map(|bar| bar.time))
1173 .unwrap_or(chrono::DateTime::<chrono::Utc>::MAX_UTC)
1174 };
1175 let (mut first, mut end) = (0, 0);
1176 data.iter()
1177 .enumerate()
1178 .map(|(at, bar)| {
1179 let (open, until) = (clock.bar_open(bar.time), close(clock, data, at));
1180 while first < source.bars.len() && close(&source.clock, source.bars, first) <= open {
1181 first += 1;
1182 }
1183 end = end.max(first);
1184 while end < source.bars.len() && source.clock.bar_open(source.bars[end].time) < until {
1185 end += 1;
1186 }
1187 first..end
1188 })
1189 .collect()
1190}
1191
1192fn chart_bars(
1195 spans: &[std::ops::Range<usize>],
1196 bars: &std::ops::Range<usize>,
1197) -> Option<std::ops::Range<usize>> {
1198 let start = spans.partition_point(|span| span.end <= bars.start);
1199 let end = spans.partition_point(|span| span.start < bars.end);
1200 (bars.start < bars.end && start < end).then_some(start..end)
1201}
1202
1203fn source_bars(
1206 spans: &[std::ops::Range<usize>],
1207 bars: &std::ops::Range<usize>,
1208) -> std::ops::Range<usize> {
1209 let end = bars.end.min(spans.len());
1210 if bars.start >= end {
1211 return 0..0;
1212 }
1213 spans[bars.start].start..spans[end - 1].end
1214}
1215
1216#[cfg(all(test, feature = "studies"))]
1217mod tests {
1218 use super::*;
1219 use crate::studies::BollingerBands;
1220
1221 #[test]
1222 fn value_tags_default_by_plot_count_and_persist() {
1223 use crate::studies::{
1224 CustomIndicator, IndicatorValue, MARibbon, StudyTemplate, VolumeWeightedAvgPrice,
1225 };
1226 for (lines, untagged) in [(4, vec![]), (5, vec![0, 1, 2, 3, 4])] {
1227 let study = ConfiguredStudy::new(Box::new(
1228 CustomIndicator::new("Lines", |bars| vec![IndicatorValue::None; bars.len()])
1229 .with_line_count(lines),
1230 ));
1231 assert_eq!(study.settings().untagged_plots, untagged, "{lines} plots");
1232 }
1233
1234 let mut ribbon = ConfiguredStudy::new(Box::new(MARibbon::new()));
1235 assert_eq!(
1236 ribbon.settings().untagged_plots,
1237 (0..12).collect::<Vec<_>>()
1238 );
1239 let mut settings = ribbon.settings();
1240 settings.untagged_plots = (1..11).collect();
1241 ribbon.apply_settings(&[], settings).unwrap();
1242 let template = StudyTemplate::from_study(&ribbon);
1243 let json = serde_json::to_string(&template).unwrap();
1244 let restored = serde_json::from_str::<StudyTemplate>(&json)
1245 .unwrap()
1246 .create()
1247 .unwrap();
1248 assert_eq!(
1249 restored.settings().untagged_plots,
1250 (1..11).collect::<Vec<_>>()
1251 );
1252 assert!(restored.plot_tagged(0) && restored.plot_tagged(11) && !restored.plot_tagged(5));
1253 let mut old = serde_json::to_value(&template).unwrap();
1255 old["settings"]
1256 .as_object_mut()
1257 .unwrap()
1258 .remove("untagged_plots");
1259 let restored = serde_json::from_value::<StudyTemplate>(old)
1260 .unwrap()
1261 .create()
1262 .unwrap();
1263 assert!((0..12).all(|line| restored.plot_tagged(line)));
1264
1265 let mut vwap = ConfiguredStudy::new(Box::new(VolumeWeightedAvgPrice::new()));
1267 let settings = vwap.settings();
1268 vwap.apply_settings(&[("band_count".into(), InputValue::Length(2))], settings)
1269 .unwrap();
1270 assert_eq!(vwap.settings().untagged_plots, [1, 2, 3, 4]);
1271 let mut settings = vwap.settings();
1272 settings.untagged_plots = vec![3, 1, 3, 7];
1273 vwap.apply_settings(&[], settings).unwrap();
1274 assert_eq!(vwap.settings().untagged_plots, [1, 3]);
1275 }
1276
1277 #[test]
1278 fn automatic_precision_tracks_metadata_and_preserves_study_overrides() {
1279 let mut study = ConfiguredStudy::new(Box::new(super::super::MACD::default()));
1280 let instrument = Instrument::new("FX").with_tick_size(0.00001);
1281 study.set_instrument(Some(&instrument));
1282 assert_eq!(study.precision(), Some(5));
1283 assert_eq!(study.settings().precision, None);
1284 let mut settings = study.settings();
1285 settings.precision = Some(3);
1286 study.apply_settings(&[], settings).unwrap();
1287 study.set_instrument(Some(&Instrument::new("INT").with_tick_size(1.0)));
1288 assert_eq!(study.precision(), Some(3));
1289 let mut settings = study.settings();
1290 settings.precision = None;
1291 study.apply_settings(&[], settings).unwrap();
1292 assert_eq!(study.precision(), Some(0));
1293 study.set_instrument(None);
1294 assert_eq!(study.precision(), None);
1295 for (bounded, precision) in [
1296 (
1297 Box::new(super::super::RSI::default()) as Box<dyn Indicator>,
1298 Some(2),
1299 ),
1300 (Box::new(super::super::Stochastic::default()), Some(2)),
1301 (Box::new(super::super::OnBalanceVolume::default()), None),
1302 (
1303 Box::new(
1304 super::super::CustomIndicator::new("Small", |_| Vec::new())
1305 .with_value_format(crate::scales::StudyFormat::Fixed(4)),
1306 ),
1307 Some(4),
1308 ),
1309 ] {
1310 let mut bounded = ConfiguredStudy::new(bounded);
1311 bounded.set_instrument(Some(&instrument));
1312 let mut settings = bounded.settings();
1313 settings.precision = None;
1314 bounded.apply_settings(&[], settings).unwrap();
1315 assert_eq!(bounded.precision(), precision);
1316 }
1317 }
1318
1319 #[test]
1322 fn histogram_defaults_yield_to_a_saved_plot_type() {
1323 use super::super::{NetVolume, PercentageVolumeOscillator, PriceOscillator};
1324 let (line, columns) = (PlotType::Line, PlotType::Columns);
1325 let studies: [(Box<dyn Indicator>, &[PlotType]); 3] = [
1326 (Box::new(NetVolume::default()), &[line]),
1327 (Box::new(PriceOscillator::default()), &[line, line, columns]),
1328 (
1329 Box::new(PercentageVolumeOscillator::default()),
1330 &[line, line, columns],
1331 ),
1332 ];
1333 for (study, expected) in studies {
1334 let name = study.name().to_owned();
1335 let mut study = ConfiguredStudy::new(study);
1336 let types = |study: &ConfiguredStudy| -> Vec<_> {
1337 study.settings().plots.iter().map(|p| p.plot_type).collect()
1338 };
1339 assert_eq!(types(&study), expected, "{name}");
1340 let mut settings = study.settings();
1341 settings.plots[0].plot_type = PlotType::Area;
1342 study.apply_settings(&[], settings).unwrap();
1343 assert_eq!(study.plot_style(0).plot_type, PlotType::Area, "{name}");
1344 }
1345 }
1346
1347 #[test]
1351 fn ohlc_studies_offer_a_calculation_interval() {
1352 use super::super::{ADX, ATR};
1353 let ids = |study: &ConfiguredStudy| -> Vec<String> {
1354 study.inputs().into_iter().map(|input| input.id).collect()
1355 };
1356 for inner in [
1357 Box::new(ATR::new(3)) as Box<dyn Indicator>,
1358 Box::new(ADX::new(3)),
1359 ] {
1360 let mut study = ConfiguredStudy::new(inner);
1361 let own = study.study.inputs().len();
1362 let ids = ids(&study);
1363 assert_eq!(
1364 &ids[own..],
1365 ["calculation_timeframe", "wait_for_close"],
1366 "{}",
1367 study.name()
1368 );
1369 assert!(study.data_requests().is_empty());
1370 study
1371 .set_input(
1372 "calculation_timeframe",
1373 InputValue::Timeframe(Some(Timeframe::Hour1)),
1374 )
1375 .unwrap();
1376 assert_eq!(study.data_requests().len(), 1);
1377 assert!(
1378 study
1379 .set_input("calculation_symbol", InputValue::Symbol("X".into()))
1380 .is_err()
1381 );
1382 }
1383 let breadth = ConfiguredStudy::new(Box::new(super::super::AdvanceDeclineRatio::default()));
1384 assert!(!ids(&breadth).contains(&"calculation_timeframe".to_owned()));
1385 }
1386
1387 #[test]
1391 fn profiles_zones_and_markers_of_another_interval_lie_on_their_chart_bars() {
1392 use super::super::{
1393 CrossDirection, IndicatorMarker, StudyData, StudyZone, VolProfileSession,
1394 VolProfileVisible,
1395 };
1396 #[derive(Clone, Default)]
1398 struct Marks {
1399 values: IndicatorValues,
1400 markers: Vec<IndicatorMarker>,
1401 zones: Vec<StudyZone>,
1402 }
1403 impl Indicator for Marks {
1404 fn name(&self) -> &str {
1405 "Marks"
1406 }
1407 fn calculate(&mut self, data: &[Bar]) {
1408 self.values = IndicatorValues::missing(data.len());
1409 let last = data.len().saturating_sub(1);
1410 let marker = |bar| IndicatorMarker {
1411 bar,
1412 value: 100.0,
1413 direction: CrossDirection::Up,
1414 };
1415 let zone = StudyZone {
1416 bars: 1..last + 1,
1417 top: 101.0,
1418 bottom: 100.0,
1419 plot: 0,
1420 };
1421 (self.markers, self.zones) = if last > 1 {
1422 (vec![marker(1), marker(last)], vec![zone])
1423 } else {
1424 (Vec::new(), Vec::new())
1425 };
1426 }
1427 fn values(&self) -> &IndicatorValues {
1428 &self.values
1429 }
1430 fn colors(&self) -> Vec<Color32> {
1431 vec![Color32::WHITE]
1432 }
1433 fn set_colors(&mut self, _: Vec<Color32>) {}
1434 fn set_visible(&mut self, _: bool) {}
1435 fn markers(&self) -> &[IndicatorMarker] {
1436 &self.markers
1437 }
1438 fn zones(&self) -> &[StudyZone] {
1439 &self.zones
1440 }
1441 fn clone_box(&self) -> Box<dyn Indicator> {
1442 Box::new(self.clone())
1443 }
1444 }
1445 let start: chrono::DateTime<chrono::Utc> = "2026-03-06T23:50:00Z".parse().unwrap();
1448 let bars = |count: i32, interval: Timeframe| -> Vec<Bar> {
1449 (0..count)
1450 .map(|i| {
1451 let low = 100.0 + f64::from(i);
1452 let time = start + interval.duration() * i;
1453 Bar::new(time, low, low + 1.0, low, low + 1.0, 1.0)
1454 })
1455 .collect()
1456 };
1457 let profiles = |study: &ConfiguredStudy, visible| -> Vec<_> {
1458 study
1459 .volume_profiles(visible)
1460 .into_iter()
1461 .map(|plot| {
1462 let rows = &plot.profile;
1463 (plot.bars, rows.low(), rows.high(), rows.volume().to_vec())
1464 })
1465 .collect()
1466 };
1467 for (name, [chart, other], sessions, (view, visible), markers, zones) in [
1468 (
1469 "finer",
1470 [(4, Timeframe::Min5), (20, Timeframe::Min1)],
1471 [
1472 (0..2, 100.0, 110.0, vec![10.0]),
1473 (2..4, 110.0, 120.0, vec![10.0]),
1474 ],
1475 (1..3, (1..3, 105.0, 115.0, vec![10.0])),
1476 vec![0, 3],
1477 0..4,
1478 ),
1479 (
1480 "coarser",
1481 [(20, Timeframe::Min1), (4, Timeframe::Min5)],
1482 [
1483 (0..10, 100.0, 102.0, vec![2.0]),
1484 (10..20, 102.0, 104.0, vec![2.0]),
1485 ],
1486 (3..12, (0..15, 100.0, 103.0, vec![3.0])),
1487 vec![9],
1488 5..20,
1489 ),
1490 ] {
1491 let (interval, source_interval) = (chart.1, other.1);
1492 let (chart, source) = (bars(chart.0, interval), bars(other.0, source_interval));
1493 let calculated = |inner: Box<dyn Indicator>| {
1494 let mut study = ConfiguredStudy::new(inner);
1495 study.set_timeframe(interval);
1496 let other = InputValue::Timeframe(Some(source_interval));
1497 study.set_input("calculation_timeframe", other).unwrap();
1498 let data = StudyData {
1499 request: study.data_requests().remove(0),
1500 bars: &source,
1501 clock: crate::model::BarClock::new(source_interval),
1502 instrument: None,
1503 timeframe: source_interval,
1504 };
1505 study.update_data(&chart, 0, &[data]);
1506 study
1507 };
1508 let mut studies = [
1509 calculated(Box::new(VolProfileSession::new(1))),
1510 calculated(Box::new(VolProfileVisible::new(1))),
1511 calculated(Box::<Marks>::default()),
1512 ];
1513 assert_eq!(profiles(&studies[0], 0..chart.len()), sessions, "{name}");
1514 assert_eq!(profiles(&studies[1], view), [visible], "{name}");
1515 let marked: Vec<_> = studies[2]
1516 .markers()
1517 .iter()
1518 .map(|marker| marker.bar)
1519 .collect();
1520 assert_eq!(marked, markers, "{name}");
1521 let zone = StudyZone {
1522 bars: zones,
1523 top: 101.0,
1524 bottom: 100.0,
1525 plot: 0,
1526 };
1527 assert_eq!(studies[2].zones(), [zone], "{name}");
1528 for study in &mut studies {
1529 study.update_data(&chart, 0, &[]);
1530 }
1531 for study in &studies {
1532 assert!(profiles(study, 0..chart.len()).is_empty(), "{name}");
1533 assert!(
1534 study.markers().is_empty() && study.zones().is_empty(),
1535 "{name}"
1536 );
1537 }
1538 }
1539 }
1540
1541 #[test]
1542 fn edits_are_atomic_and_interval_visibility_preserves_the_global_toggle() {
1543 let mut study = ConfiguredStudy::new(Box::new(BollingerBands::new(3, 2.0)));
1544 let mut settings = study.settings();
1545 settings.plots[0].visible = false;
1546 settings.plots[1].width = 3.0;
1547 settings.precision = Some(4);
1548 settings.hidden_intervals.push(Timeframe::Min1);
1549 assert!(
1550 study
1551 .apply_settings(
1552 &[
1553 ("period".into(), InputValue::Length(2)),
1554 ("std_dev".into(), InputValue::Number(f64::NAN))
1555 ],
1556 settings.clone()
1557 )
1558 .is_err()
1559 );
1560 assert_eq!(study.inputs()[0].value, InputValue::Length(3));
1561 assert!(study.plot_style(0).visible);
1562 study
1563 .apply_settings(&[("period".into(), InputValue::Length(2))], settings)
1564 .unwrap();
1565 assert_eq!(study.inputs()[0].value, InputValue::Length(2));
1566 assert!(!study.plot_style(0).visible);
1567 assert_eq!(study.line_style(1).width, 3.0);
1568 assert_eq!(study.precision(), Some(4));
1569 study.set_timeframe(Timeframe::Min1);
1570 assert!(!study.is_visible());
1571 study.set_timeframe(Timeframe::Hour1);
1572 assert!(study.is_visible());
1573 study.set_visible(false);
1574 study.set_timeframe(Timeframe::Day1);
1575 assert!(!study.is_visible());
1576 for (range, interval, visible) in [
1577 (
1578 StudyIntervalRange::AndAbove(Timeframe::Min5),
1579 Timeframe::Custom(std::num::NonZeroU64::new(120).unwrap()),
1580 false,
1581 ),
1582 (
1583 StudyIntervalRange::AndAbove(Timeframe::Min5),
1584 Timeframe::Custom(std::num::NonZeroU64::new(420).unwrap()),
1585 true,
1586 ),
1587 (
1588 StudyIntervalRange::AndBelow(Timeframe::Ticks(
1589 std::num::NonZeroU64::new(100).unwrap(),
1590 )),
1591 Timeframe::Ticks(std::num::NonZeroU64::new(50).unwrap()),
1592 true,
1593 ),
1594 (
1595 StudyIntervalRange::AndBelow(Timeframe::Ticks(
1596 std::num::NonZeroU64::new(100).unwrap(),
1597 )),
1598 Timeframe::Volume(std::num::NonZeroU64::new(50).unwrap()),
1599 false,
1600 ),
1601 ] {
1602 let mut settings = study.settings();
1603 settings.interval_range = range;
1604 study.apply_settings(&[], settings).unwrap();
1605 study.set_visible(true);
1606 study.set_timeframe(interval);
1607 assert_eq!(study.is_visible(), visible);
1608 study.set_visible(false);
1609 assert!(!study.is_visible());
1610 }
1611 let saved = serde_json::to_string(&study.settings()).unwrap();
1612 let restored: StudySettings = serde_json::from_str(&saved).unwrap();
1613 assert!(!restored.plots[0].visible);
1614 assert_eq!(restored.hidden_intervals, [Timeframe::Min1]);
1615 }
1616
1617 #[test]
1618 fn rsi_levels_round_trip_and_older_settings_take_the_declared_levels() {
1619 use crate::studies::{AdxBreakout, RSI, palette};
1620 let values = |study: &ConfiguredStudy| -> Vec<f64> {
1621 study.pane().levels.iter().map(|level| level.0).collect()
1622 };
1623 let mut study = ConfiguredStudy::new(Box::new(RSI::default()));
1624 let declared = study.settings();
1625 assert_eq!(
1626 declared
1627 .levels
1628 .iter()
1629 .map(|l| (l.value, l.color, l.pattern, l.visible))
1630 .collect::<Vec<_>>(),
1631 [(None, palette::MUTED, LineStyle::Dashed, true); 2]
1632 );
1633 assert!(declared.fill_between_levels);
1634 assert_eq!(
1635 declared.level_fill_color,
1636 Color32::from_rgba_unmultiplied(126, 87, 194, 26)
1637 );
1638 let before = study.snapshot_settings();
1639 let mut edited = declared.clone();
1640 edited.levels[0].value = Some(80.0);
1641 edited.levels[1].visible = false;
1642 study.apply_settings(&[], edited.clone()).unwrap();
1643 assert_eq!(values(&study), [80.0, 30.0]);
1644 let saved = serde_json::to_value(&edited).unwrap();
1646 let mut loaded = ConfiguredStudy::new(Box::new(RSI::default()));
1647 loaded
1648 .apply_settings(&[], serde_json::from_value(saved.clone()).unwrap())
1649 .unwrap();
1650 assert_eq!(loaded.settings(), edited);
1651 let mut older = saved;
1652 for key in ["levels", "fill_between_levels", "level_fill_color"] {
1653 older.as_object_mut().unwrap().remove(key);
1654 }
1655 loaded
1656 .apply_settings(&[], serde_json::from_value(older).unwrap())
1657 .unwrap();
1658 assert_eq!(loaded.settings(), declared);
1659 study.restore_settings(&before).unwrap();
1660 assert_eq!(values(&study), [70.0, 30.0]);
1661 let mut invalid = declared.clone();
1662 invalid.levels[0].value = Some(f64::INFINITY);
1663 assert!(matches!(
1664 study.apply_settings(&[], invalid),
1665 Err(super::super::StudyError::InvalidLevelValue { level: 0, value }) if value == f64::INFINITY
1666 ));
1667 assert_eq!(study.settings(), declared);
1668 let mut adx = ConfiguredStudy::new(Box::new(AdxBreakout::default()));
1669 let settings = adx.settings();
1670 adx.apply_settings(&[("threshold".into(), InputValue::Length(25))], settings)
1671 .unwrap();
1672 assert_eq!(values(&adx), [25.0]);
1673 }
1674}