1use super::clamp_param;
2use crate::model::Bar;
3use crate::scales::StudyFormat;
4use crate::studies::palette;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues, LabelPlacement, StudyLabel};
6use egui::Color32;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
10pub enum ZigZagMode {
11 #[default]
13 HighLow,
14 Close,
16 Typical,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
22#[non_exhaustive]
23pub enum ZigZagChange {
24 Price,
26 #[default]
28 Percent,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq)]
33pub struct ZigZagPivot {
34 pub bar_index: usize,
36 pub price: f64,
38 pub is_high: bool,
40}
41
42#[derive(Clone)]
71pub struct ZigZag {
72 deviation_percent: f64,
74 depth: usize,
76 mode: ZigZagMode,
78 values: IndicatorValues,
79 pivots: Vec<ZigZagPivot>,
80 source_len: usize,
81 finite: Vec<usize>,
82 prices: Vec<(usize, f64, f64)>,
83 completed: Vec<((usize, f64), bool)>,
84 detector: Detector,
85 before_last: Checkpoint,
86 color: Color32,
87 visible: bool,
88 price_labels: bool,
89 change_labels: bool,
90 change_mode: ZigZagChange,
91 volume_labels: bool,
92 precision: Option<usize>,
94 label_precision: Option<usize>,
95 labels: Vec<StudyLabel>,
96}
97
98#[derive(Clone, Copy, Default)]
99struct Detector {
100 extremes: Option<((usize, f64), (usize, f64))>,
101 trend: Option<(bool, (usize, f64))>,
102}
103
104#[derive(Clone, Default)]
105struct Checkpoint {
106 detector: Detector,
107 finite: usize,
108 prices: usize,
109 completed: usize,
110}
111
112impl ZigZag {
113 pub fn new(deviation_percent: f64, depth: usize) -> Self {
115 Self {
116 deviation_percent: clamp_param(deviation_percent, 0.0, f64::MAX),
117 depth: depth.max(1),
118 mode: ZigZagMode::HighLow,
119 values: IndicatorValues::new(),
120 pivots: Vec::new(),
121 source_len: 0,
122 finite: Vec::new(),
123 prices: Vec::new(),
124 completed: Vec::new(),
125 detector: Detector::default(),
126 before_last: Checkpoint::default(),
127 color: palette::BLUE,
128 visible: true,
129 price_labels: false,
130 change_labels: false,
131 change_mode: ZigZagChange::Percent,
132 volume_labels: false,
133 precision: None,
134 label_precision: None,
135 labels: Vec::new(),
136 }
137 }
138
139 pub fn pivots(&self) -> &[ZigZagPivot] {
142 &self.pivots
143 }
144
145 pub fn with_mode(mut self, mode: ZigZagMode) -> Self {
147 self.mode = mode;
148 self
149 }
150
151 pub fn with_color(mut self, color: Color32) -> Self {
153 self.color = color;
154 self
155 }
156
157 pub fn with_labels(mut self, price: bool, change: Option<ZigZagChange>, volume: bool) -> Self {
161 self.price_labels = price;
162 self.change_labels = change.is_some();
163 self.change_mode = change.unwrap_or(self.change_mode);
164 self.volume_labels = volume;
165 self
166 }
167
168 fn refresh_labels(&mut self, data: &[Bar], fixed: usize) {
171 if !(self.price_labels || self.change_labels || self.volume_labels) {
172 self.labels.clear();
173 return;
174 }
175 let kept = fixed.checked_sub(1).map_or(0, |last| {
176 let bar = self.pivots[last].bar_index;
177 self.labels.partition_point(|label| label.bar <= bar)
178 });
179 self.labels.truncate(kept);
180 let precision = self.label_precision.or(self.precision);
181 for (k, pivot) in self.pivots.iter().enumerate().skip(fixed) {
182 let mut lines = Vec::new();
183 if self.price_labels {
184 lines.push(StudyFormat::Price.format(pivot.price, precision));
185 }
186 if let Some(previous) = k.checked_sub(1).map(|k| self.pivots[k]) {
187 let change = pivot.price - previous.price;
188 let sign = if change > 0.0 { "+" } else { "" };
189 if self.change_labels {
190 lines.push(match self.change_mode {
191 ZigZagChange::Price => {
192 format!("{sign}{}", StudyFormat::Price.format(change, precision))
193 }
194 ZigZagChange::Percent => {
195 let percent = change / previous.price * 100.0;
196 format!("{sign}{}", StudyFormat::Percent.format(percent, Some(2)))
197 }
198 });
199 }
200 if self.volume_labels {
201 let first = self.finite.binary_search(&previous.bar_index).unwrap() + 1;
202 let last = self.finite.binary_search(&pivot.bar_index).unwrap();
203 let volume: f64 = self.finite[first..=last]
204 .iter()
205 .map(|&bar| data[bar].volume)
206 .filter(|volume| volume.is_finite())
207 .sum();
208 lines.push(StudyFormat::Volume.format(volume, None));
209 }
210 }
211 if !lines.is_empty() {
212 self.labels.push(StudyLabel {
213 bar: pivot.bar_index,
214 price: pivot.price,
215 text: lines.join("\n"),
216 placement: if pivot.is_high {
217 LabelPlacement::Above
218 } else {
219 LabelPlacement::Below
220 },
221 plot: 0,
222 });
223 }
224 }
225 }
226
227 fn get_price(&self, bar: &Bar, is_high: bool) -> f64 {
229 match self.mode {
230 ZigZagMode::HighLow => {
231 if is_high {
232 bar.high
233 } else {
234 bar.low
235 }
236 }
237 ZigZagMode::Close => bar.close,
238 ZigZagMode::Typical => (bar.high + bar.low + bar.close) / 3.0,
239 }
240 }
241
242 fn tail_from(&self, data: &[Bar], from: usize) -> usize {
243 if self.source_len > 0 && from >= self.source_len - 1 && data.len() >= self.source_len {
244 from.min(self.source_len)
245 } else {
246 0
247 }
248 }
249
250 pub(super) fn update_swings(
255 &mut self,
256 data: &[Bar],
257 from: usize,
258 threshold: Option<&dyn Fn(usize) -> f64>,
259 ) -> usize {
260 let start = self.tail_from(data, from);
261 if start == 0 {
262 self.finite.clear();
263 self.prices.clear();
264 self.completed.clear();
265 self.detector = Detector::default();
266 self.before_last = Checkpoint::default();
267 } else if start < self.source_len {
268 self.finite.truncate(self.before_last.finite);
269 self.prices.truncate(self.before_last.prices);
270 self.completed.truncate(self.before_last.completed);
271 self.detector = self.before_last.detector;
272 }
273 let fixed = self.completed.len();
274 self.pivots.truncate(fixed);
275 let deviation = self.deviation_percent;
276 let fixed_threshold = move |_: usize| deviation;
277 let threshold = threshold.unwrap_or(&fixed_threshold);
278 for (source, bar) in data.iter().enumerate().skip(start) {
279 if source + 1 == data.len() {
280 self.before_last = Checkpoint {
281 detector: self.detector,
282 finite: self.finite.len(),
283 prices: self.prices.len(),
284 completed: self.completed.len(),
285 };
286 }
287 let (high, low) = (self.get_price(bar, true), self.get_price(bar, false));
288 if !high.is_finite() || !low.is_finite() {
289 continue;
290 }
291 let sequence = self.finite.len();
292 self.finite.push(source);
293 if high <= 0.0 || low <= 0.0 {
294 continue;
295 }
296 self.prices.push((sequence, high, low));
297 self.process(self.prices.len() - 1, threshold);
298 }
299 self.pivots.extend(
300 self.completed[fixed..]
301 .iter()
302 .map(|&((point, price), is_high)| ZigZagPivot {
303 bar_index: self.finite[self.prices[point].0],
304 price,
305 is_high,
306 }),
307 );
308 if let Some((up, (point, price))) = self.detector.trend {
309 self.pivots.push(ZigZagPivot {
310 bar_index: self.finite[self.prices[point].0],
311 price,
312 is_high: up,
313 });
314 }
315 self.source_len = data.len();
316 fixed
317 }
318
319 fn process(&mut self, mut p: usize, threshold: &dyn Fn(usize) -> f64) {
320 let bars = &self.prices;
321 let Some((mut top, mut bottom)) = self.detector.extremes else {
322 self.detector.extremes = Some(((p, bars[p].1), (p, bars[p].2)));
323 return;
324 };
325 let depth = self.depth;
326 let finite = &self.finite;
327 let reaches = |p: usize, change: f64| {
328 let threshold = threshold(finite[bars[p].0]);
329 threshold > 0.0 && change >= threshold
330 };
331 let rises = |from: (usize, f64), p: usize| {
332 bars[p].0 - bars[from.0].0 >= depth && reaches(p, (bars[p].1 - from.1) / from.1 * 100.0)
333 };
334 let falls = |from: (usize, f64), p: usize| {
335 bars[p].0 - bars[from.0].0 >= depth && reaches(p, (from.1 - bars[p].2) / from.1 * 100.0)
336 };
337 while p < bars.len() {
338 let (_, high, low) = bars[p];
339 let mut reversal = None;
341 match &mut self.detector.trend {
342 None => {
343 if high > top.1 {
344 top = (p, high);
345 }
346 if low < bottom.1 {
347 bottom = (p, low);
348 }
349 let (rise, fall) = (rises(bottom, p), falls(top, p));
351 if rise && !(fall && top.0 < bottom.0) {
352 reversal = Some((bottom, true));
353 } else if fall {
354 reversal = Some((top, false));
355 }
356 }
357 Some((up, extreme)) => {
358 let (extends, reverses) = if *up {
359 (high > extreme.1, falls(*extreme, p))
360 } else {
361 (low < extreme.1, rises(*extreme, p))
362 };
363 if extends {
364 *extreme = (p, if *up { high } else { low });
365 } else if reverses {
366 reversal = Some((*extreme, !*up));
367 }
368 }
369 }
370 if let Some((pivot, up)) = reversal {
371 self.completed.push((pivot, !up));
372 let extreme = (pivot.0 + 1..=p)
375 .map(|q| (q, if up { bars[q].1 } else { bars[q].2 }))
376 .reduce(|best, c| {
377 if (c.1 > best.1) == up && c.1 != best.1 {
378 c
379 } else {
380 best
381 }
382 })
383 .expect("the pivot comes before this bar");
384 self.detector.trend = Some((up, extreme));
385 p = extreme.0;
386 }
387 p += 1;
388 }
389 self.detector.extremes = Some((top, bottom));
390 }
391}
392
393impl Default for ZigZag {
395 fn default() -> Self {
396 Self::new(5.0, 10)
397 }
398}
399
400impl Indicator for ZigZag {
401 fn line_style(&self, _line: usize) -> crate::studies::IndicatorLineStyle {
402 crate::studies::IndicatorLineStyle {
403 width: 2.0,
404 ..Default::default()
405 }
406 }
407
408 crate::studies::inputs::study_inputs!(self;
409 deviation_percent: number("Deviation percent", 0.0, f64::MAX),
410 depth: length("Depth", 1, usize::MAX),
411 mode: choice("Mode", [ZigZagMode::HighLow, ZigZagMode::Close, ZigZagMode::Typical]),
412 price_labels: toggle("Show price"),
413 change_labels: toggle("Show change"),
414 change_mode: choice("Change as", [ZigZagChange::Price, ZigZagChange::Percent]),
415 volume_labels: toggle("Show leg volume")
416 ; {});
417
418 fn name(&self) -> &str {
419 "ZigZag"
420 }
421
422 fn desc(&self) -> &str {
423 "ZigZag - Filters out minor price movements to show significant swings"
424 }
425
426 fn calculate(&mut self, data: &[Bar]) {
427 self.update(data, 0);
428 }
429
430 fn update(&mut self, data: &[Bar], from: usize) {
431 let start = self.tail_from(data, from);
432 if start == 0 {
433 self.values = IndicatorValues::missing(data.len());
434 } else {
435 let fixed = if start < self.source_len {
436 self.before_last.completed
437 } else {
438 self.completed.len()
439 };
440 if let (Some(first), Some(last)) =
441 (self.pivots.get(fixed.saturating_sub(1)), self.pivots.last())
442 && first.bar_index < last.bar_index
443 {
444 let first = self.finite.partition_point(|&bar| bar < first.bar_index);
445 let end = self.finite.partition_point(|&bar| bar <= last.bar_index);
446 for &bar in &self.finite[first..end] {
447 self.values.set(bar, IndicatorValue::None);
448 }
449 }
450 self.values.resize(data.len(), IndicatorValue::None);
451 }
452 let fixed = self.update_swings(data, from, None);
453 for leg in self.pivots[fixed.saturating_sub(1)..].windows(2) {
454 let first = self.finite.binary_search(&leg[0].bar_index).unwrap();
455 let end = self.finite.binary_search(&leg[1].bar_index).unwrap();
456 for position in first..=end {
457 let t = (position - first) as f64 / (end - first) as f64;
458 self.values.set(
459 self.finite[position],
460 IndicatorValue::Single(leg[0].price + (leg[1].price - leg[0].price) * t),
461 );
462 }
463 }
464 self.refresh_labels(data, fixed);
465 }
466
467 fn is_incremental(&self) -> bool {
468 true
469 }
470
471 fn set_instrument(&mut self, instrument: Option<&crate::model::Instrument>) -> bool {
472 let precision = instrument
473 .and_then(crate::scales::instrument_price_precision)
474 .map(usize::from);
475 let changed = precision != self.precision;
476 self.precision = precision;
477 changed
478 }
479
480 fn labels(&self) -> &[StudyLabel] {
481 &self.labels
482 }
483
484 fn set_label_precision(&mut self, precision: Option<usize>) {
485 self.label_precision = precision;
486 }
487
488 fn provisional_after(&self, line: usize) -> Option<usize> {
490 let before_last = self.pivots.len().checked_sub(2)?;
491 (line == 0).then(|| self.pivots[before_last].bar_index)
492 }
493
494 fn values(&self) -> &IndicatorValues {
495 &self.values
496 }
497
498 fn colors(&self) -> Vec<Color32> {
499 vec![self.color]
500 }
501
502 fn set_colors(&mut self, colors: Vec<Color32>) {
503 if !colors.is_empty() {
504 self.color = colors[0];
505 }
506 }
507
508 fn is_overlay(&self) -> bool {
509 true }
511
512 fn is_visible(&self) -> bool {
513 self.visible
514 }
515
516 fn set_visible(&mut self, visible: bool) {
517 self.visible = visible;
518 }
519
520 fn clone_box(&self) -> Box<dyn Indicator> {
521 Box::new(self.clone())
522 }
523
524 fn line_names(&self) -> Vec<String> {
525 vec![format!(
526 "ZigZag({:.1}%, {})",
527 self.deviation_percent, self.depth
528 )]
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::studies::test_support::make_range_bar as make_bar;
536
537 #[test]
541 fn pivots_are_labelled_and_the_last_leg_is_provisional() {
542 let data: Vec<Bar> = [
543 (101.0, 100.0, 100.0),
544 (103.0, 102.0, 1200.0),
545 (106.0, 105.0, 300.0),
546 (110.0, 109.0, 500.0),
547 (104.0, 103.0, 750.0),
548 (105.0, 104.0, 40.0),
549 ]
550 .into_iter()
551 .map(|(high, low, volume)| Bar {
552 volume,
553 ..make_bar(high, low, low)
554 })
555 .collect();
556 let study = || ZigZag::new(5.0, 1);
557 for (price, change, volume, want) in [
558 (
559 true,
560 Some(ZigZagChange::Price),
561 true,
562 &[
563 (0, "100.00"),
564 (3, "110.00\n+10.00\n2.00K"),
565 (4, "103.00\n-7.00\n750"),
566 ][..],
567 ),
568 (
569 true,
570 Some(ZigZagChange::Percent),
571 true,
572 &[
573 (0, "100.00"),
574 (3, "110.00\n+10.00%\n2.00K"),
575 (4, "103.00\n-6.36%\n750"),
576 ][..],
577 ),
578 (
579 false,
580 Some(ZigZagChange::Percent),
581 false,
582 &[(3, "+10.00%"), (4, "-6.36%")][..],
583 ),
584 (false, None, false, &[][..]),
585 ] {
586 let mut zigzag = study().with_labels(price, change, volume);
587 zigzag.calculate(&data);
588 let got: Vec<_> = zigzag
589 .labels()
590 .iter()
591 .map(|label| (label.bar, label.text.as_str()))
592 .collect();
593 assert_eq!(got, want);
594 for label in zigzag.labels() {
595 let placement = if label.bar == 3 {
596 LabelPlacement::Above
597 } else {
598 LabelPlacement::Below
599 };
600 assert_eq!(label.placement, placement);
601 }
602 }
603
604 let mut zigzag = study();
605 zigzag.calculate(&data[..4]);
606 assert_eq!(zigzag.provisional_after(0), Some(0));
607 zigzag.update(&data[..5], 4);
608 assert_eq!(zigzag.provisional_after(0), Some(3));
609 zigzag.update(&data, 5);
610 assert_eq!(zigzag.provisional_after(0), Some(3));
611 assert_eq!(zigzag.provisional_after(1), None);
612 }
613
614 #[test]
617 fn zigzag_draws_lines_between_pivots() {
618 type Case<'a> = (&'a str, ZigZag, &'a [(f64, f64)], &'a [Option<f64>]);
620 let nan = f64::NAN;
621 let sine: Vec<(f64, f64)> = (0..20)
622 .map(|i| match i {
623 0 => (nan, nan),
624 _ => {
625 let c = 100.0 + 5.0 * (0.7 * f64::from(i)).sin();
626 (c + 1.0, c - 1.0)
627 }
628 })
629 .collect();
630 let cases: &[Case] = &[
631 (
632 "close mode",
634 ZigZag::new(5.0, 1).with_mode(ZigZagMode::Close),
635 &[(120.0, 100.0), (130.0, 110.0), (120.0, 100.0)],
636 &[Some(100.0), Some(110.0), Some(100.0)],
637 ),
638 (
639 "swings",
642 ZigZag::new(5.0, 1),
643 &[
644 (99.0, 98.5),
645 (100.0, 98.0),
646 (99.0, 96.0),
647 (97.0, 94.0),
648 (96.0, 93.0),
649 (nan, nan),
650 (97.0, 94.0),
651 (99.0, 95.0),
652 (101.0, 97.0),
653 (100.0, 96.0),
654 (98.0, 95.0),
655 (97.0, 95.5),
656 (96.0, 95.2),
657 ],
658 &[
659 None,
660 Some(100.0),
661 Some(97.666_666_666_666_67),
662 Some(95.333_333_333_333_33),
663 Some(93.0),
664 None,
665 Some(93.0 + 8.0 / 3.0),
666 Some(93.0 + 16.0 / 3.0),
667 Some(101.0),
668 Some(98.0),
669 Some(95.0),
670 None,
671 None,
672 ],
673 ),
674 (
675 "flat",
676 ZigZag::new(5.0, 1),
677 &[(100.0, 100.0); 6],
678 &[None; 6],
679 ),
680 (
681 "both reversals at once",
685 ZigZag::new(5.0, 2),
686 &[(110.0, 109.0), (95.0, 90.0), (108.0, 105.0), (106.0, 104.0)],
687 &[Some(110.0), Some(90.0), Some(108.0), None],
688 ),
689 (
690 "swing extreme before the confirming bar",
693 ZigZag::new(5.0, 3),
694 &[
695 (101.0, 100.0),
696 (105.0, 104.0),
697 (108.0, 107.0),
698 (113.0, 112.0),
699 (110.0, 104.0),
700 (103.0, 100.0),
701 (107.0, 106.0),
702 (110.0, 109.0),
703 (114.0, 113.0),
704 (118.0, 117.0),
705 ],
706 &[
707 Some(100.0),
708 Some(313.0 / 3.0),
709 Some(326.0 / 3.0),
710 Some(113.0),
711 Some(106.5),
712 Some(100.0),
713 Some(104.5),
714 Some(109.0),
715 Some(113.5),
716 Some(118.0),
717 ],
718 ),
719 (
720 "deviation 0",
722 ZigZag::new(0.0, 1),
723 &[
724 (101.0, 100.0),
725 (103.0, 102.0),
726 (106.0, 105.0),
727 (110.0, 109.0),
728 ],
729 &[None; 4],
730 ),
731 (
732 "rises from the first bar",
734 ZigZag::new(5.0, 1),
735 &[
736 (101.0, 100.0),
737 (103.0, 102.0),
738 (106.0, 105.0),
739 (110.0, 109.0),
740 (104.0, 103.0),
741 (105.0, 104.0),
742 ],
743 &[
744 Some(100.0),
745 Some(103.333_333_333_333_33),
746 Some(106.666_666_666_666_67),
747 Some(110.0),
748 Some(103.0),
749 None,
750 ],
751 ),
752 (
753 "outside bar",
755 ZigZag::new(5.0, 1),
756 &[
757 (101.0, 100.0),
758 (110.0, 108.0),
759 (112.0, 104.0),
760 (111.0, 109.0),
761 (118.0, 116.0),
762 ],
763 &[
764 Some(100.0),
765 Some(104.5),
766 Some(109.0),
767 Some(113.5),
768 Some(118.0),
769 ],
770 ),
771 (
772 "high before the low",
774 ZigZag::new(5.0, 1),
775 &[(100.0, 99.0), (96.0, 95.1), (99.9, 99.0)],
776 &[None, Some(95.1), Some(99.9)],
777 ),
778 (
779 "touches zero",
781 ZigZag::new(5.0, 1),
782 &[(5.0, 4.0), (3.0, 2.0), (1.0, 0.0), (1.02, 0.5)],
783 &[Some(5.0), Some(3.5), Some(2.0), Some(0.5)],
784 ),
785 (
786 "NaN bar inside a leg",
789 ZigZag::new(5.0, 2),
790 &[(100.0, 99.0), (110.0, 109.0), (nan, nan), (104.0, 103.0)],
791 &[Some(99.0), Some(110.0), None, None],
792 ),
793 (
794 "negative prices",
795 ZigZag::new(5.0, 1),
796 &[
797 (-1.0, -2.0),
798 (-3.0, -4.0),
799 (-5.0, -6.0),
800 (-1.0, -2.0),
801 (3.0, 2.0),
802 (-4.0, -5.0),
803 ],
804 &[None; 6],
805 ),
806 (
807 "NaN first bar",
809 ZigZag::new(5.0, 1),
810 &sine,
811 &[
812 None,
813 None,
814 Some(105.9272486499423),
815 Some(103.5593463073295),
816 Some(101.19144396471671),
817 Some(98.82354162210393),
818 Some(96.45563927949114),
819 Some(94.08773693687834),
820 Some(97.05101299500501),
821 Some(100.01428905313168),
822 Some(102.97756511125833),
823 Some(105.940841169385),
824 Some(103.57349520635668),
825 Some(101.20614924332837),
826 Some(98.83880328030004),
827 Some(96.47145731727173),
828 Some(94.10411135424341),
829 Some(97.51869050648995),
830 Some(100.93326965873648),
831 Some(104.34784881098301),
832 ],
833 ),
834 ];
835
836 for (name, zz, bars, expected) in cases {
837 let data: Vec<Bar> = bars.iter().map(|&(h, l)| make_bar(h, l, l)).collect();
838 let mut zz = zz.clone();
839 zz.calculate(&data);
840 let extremes: &[(usize, bool)] = match *name {
841 "close mode" => &[(0, false), (1, true), (2, false)],
842 "swings" => &[(1, true), (4, false), (8, true), (10, false)],
843 "both reversals at once" => &[(0, true), (1, false), (2, true)],
844 "swing extreme before the confirming bar" => {
845 &[(0, false), (3, true), (5, false), (9, true)]
846 }
847 "rises from the first bar" => &[(0, false), (3, true), (4, false)],
848 "outside bar" => &[(0, false), (4, true)],
849 "high before the low" => &[(1, false), (2, true)],
850 "touches zero" => &[(0, true), (3, false)],
851 "NaN bar inside a leg" => &[(0, false), (1, true)],
852 "NaN first bar" => &[(2, true), (7, false), (11, true), (16, false), (19, true)],
853 "flat" | "deviation 0" | "negative prices" => &[],
854 _ => unreachable!("case needs its pivot expectations"),
855 };
856 assert_eq!(zz.pivots().len(), extremes.len(), "{name}");
857 for (pivot, &(bar, high)) in zz.pivots().iter().zip(extremes) {
858 assert_eq!((pivot.bar_index, pivot.is_high), (bar, high), "{name}");
859 assert!(
860 (pivot.price - expected[bar].unwrap()).abs() < 1e-9,
861 "{name}"
862 );
863 }
864 let got: Vec<Option<f64>> = zz
865 .values()
866 .to_rows()
867 .iter()
868 .map(|v| match v {
869 IndicatorValue::Single(x) => Some(*x),
870 _ => None,
871 })
872 .collect();
873 assert_eq!(got.len(), expected.len(), "{name}");
874 for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
875 match (g, e) {
876 (Some(g), Some(e)) => {
877 assert!((g - e).abs() < 1e-9, "{name} bar {i}: {g} != {e}")
878 }
879 (None, None) => {}
880 _ => panic!("{name} bar {i}: got {g:?}, expected {e:?}\n got {got:?}"),
881 }
882 }
883 zz.set_input("deviation_percent", crate::studies::InputValue::Number(0.0))
884 .unwrap();
885 zz.calculate(&data);
886 for _ in 0..2 {
887 zz.update(&data, data.len().saturating_sub(1));
888 assert!(zz.pivots().is_empty(), "{name}: disabled swings");
889 assert!((0..data.len()).all(|bar| zz.values().value(0, bar).is_none()));
890 }
891 }
892 }
893}