1use super::clamp_param;
11use super::swings::{Swing, Swings};
12use crate::model::Bar;
13use crate::studies::palette;
14use crate::studies::stepper::Stepper;
15use crate::studies::{
16 Indicator, IndicatorValue, IndicatorValues, InputKind, InputValue, StudyInput,
17};
18use egui::Color32;
19use std::collections::VecDeque;
20
21const SLACK: f64 = 0.25;
24const MAX_PIVOTS: usize = 16;
26const MAX_PENDING: usize = 8;
28const MAX_CONFIRMED: usize = 64;
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum FormationKind {
34 BullFlag,
36 BearFlag,
38 BullPennant,
40 BearPennant,
42 Rectangle,
44 AscendingTriangle,
46 DescendingTriangle,
48 SymmetricalTriangle,
50 CupAndHandle,
52 InvertedCupAndHandle,
54 DoubleTop,
56 DoubleBottom,
58 TripleTop,
60 TripleBottom,
62 HeadAndShoulders,
64 InverseHeadAndShoulders,
66}
67
68impl FormationKind {
69 fn label(self) -> &'static str {
70 match self {
71 Self::BullFlag => "Bull Flag",
72 Self::BearFlag => "Bear Flag",
73 Self::BullPennant => "Bull Pennant",
74 Self::BearPennant => "Bear Pennant",
75 Self::Rectangle => "Rectangle",
76 Self::AscendingTriangle => "Ascending Triangle",
77 Self::DescendingTriangle => "Descending Triangle",
78 Self::SymmetricalTriangle => "Symmetrical Triangle",
79 Self::CupAndHandle => "Cup and Handle",
80 Self::InvertedCupAndHandle => "Inverted Cup and Handle",
81 Self::DoubleTop => "Double Top",
82 Self::DoubleBottom => "Double Bottom",
83 Self::TripleTop => "Triple Top",
84 Self::TripleBottom => "Triple Bottom",
85 Self::HeadAndShoulders => "Head and Shoulders",
86 Self::InverseHeadAndShoulders => "Inverse Head and Shoulders",
87 }
88 }
89
90 fn abbreviation(self) -> &'static str {
91 match self {
92 Self::BullFlag => "BF",
93 Self::BearFlag => "SF",
94 Self::BullPennant => "BP",
95 Self::BearPennant => "SP",
96 Self::Rectangle => "REC",
97 Self::AscendingTriangle => "ATR",
98 Self::DescendingTriangle => "DTR",
99 Self::SymmetricalTriangle => "STR",
100 Self::CupAndHandle => "CH",
101 Self::InvertedCupAndHandle => "ICH",
102 Self::DoubleTop => "DT",
103 Self::DoubleBottom => "DB",
104 Self::TripleTop => "TT",
105 Self::TripleBottom => "TB",
106 Self::HeadAndShoulders => "HS",
107 Self::InverseHeadAndShoulders => "IHS",
108 }
109 }
110
111 fn expected(self) -> Option<PatternDirection> {
113 match self {
114 Self::BullFlag
115 | Self::BullPennant
116 | Self::AscendingTriangle
117 | Self::CupAndHandle
118 | Self::DoubleBottom
119 | Self::TripleBottom
120 | Self::InverseHeadAndShoulders => Some(PatternDirection::Bullish),
121 Self::BearFlag
122 | Self::BearPennant
123 | Self::DescendingTriangle
124 | Self::InvertedCupAndHandle
125 | Self::DoubleTop
126 | Self::TripleTop
127 | Self::HeadAndShoulders => Some(PatternDirection::Bearish),
128 Self::Rectangle | Self::SymmetricalTriangle => None,
129 }
130 }
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub enum PatternFamily {
136 Continuation,
138 Reversal,
140}
141
142impl PatternFamily {
143 pub(crate) const ALL: [Self; 2] = [Self::Continuation, Self::Reversal];
145
146 fn kinds(self) -> &'static [FormationKind] {
147 match self {
148 Self::Continuation => &[
149 FormationKind::BullFlag,
150 FormationKind::BearFlag,
151 FormationKind::BullPennant,
152 FormationKind::BearPennant,
153 FormationKind::Rectangle,
154 FormationKind::AscendingTriangle,
155 FormationKind::DescendingTriangle,
156 FormationKind::SymmetricalTriangle,
157 ],
158 Self::Reversal => &[
159 FormationKind::CupAndHandle,
160 FormationKind::InvertedCupAndHandle,
161 FormationKind::DoubleTop,
162 FormationKind::DoubleBottom,
163 FormationKind::TripleTop,
164 FormationKind::TripleBottom,
165 FormationKind::HeadAndShoulders,
166 FormationKind::InverseHeadAndShoulders,
167 ],
168 }
169 }
170
171 fn name(self) -> &'static str {
172 match self {
173 Self::Continuation => "Continuation Chart Patterns",
174 Self::Reversal => "Reversal Chart Patterns",
175 }
176 }
177}
178
179#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181pub enum PatternDirection {
182 Bullish,
184 Bearish,
186}
187
188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190pub enum FormationStatus {
191 Developing,
193 Confirmed,
195}
196
197#[derive(Clone, Debug, PartialEq)]
199pub struct Formation {
200 pub kind: FormationKind,
202 pub status: FormationStatus,
204 pub direction: Option<PatternDirection>,
207 pub anchors: Vec<(usize, f64)>,
209 pub boundaries: Vec<[(usize, f64); 2]>,
212 pub confirmed_at: Option<usize>,
214}
215
216#[derive(Clone, Copy, Debug)]
217struct Config {
218 strength: usize,
219 min_length: usize,
220 max_length: usize,
221 tolerance: f64,
222 touches: usize,
223 pole: f64,
224 depth: f64,
225 handle: f64,
226}
227
228#[derive(Clone, Copy)]
230struct Line {
231 seq: f64,
232 price: f64,
233 slope: f64,
234}
235
236impl Line {
237 fn through(a: &Swing, b: &Swing) -> Self {
238 Self {
239 seq: a.position as f64,
240 price: a.price,
241 slope: (b.price - a.price) / (b.position as f64 - a.position as f64),
242 }
243 }
244
245 fn at(&self, seq: usize) -> f64 {
246 self.price + self.slope * (seq as f64 - self.seq)
247 }
248}
249
250#[derive(Clone, Copy)]
251enum Outcome {
252 Confirm(PatternDirection),
253 Fail,
254}
255
256#[derive(Clone)]
258struct Candidate {
259 kind: FormationKind,
260 anchors: Vec<Swing>,
261 from: usize,
263 upper: Line,
264 lower: Line,
265 on_up: Outcome,
266 on_down: Outcome,
267 shown: [bool; 2],
269}
270
271impl Candidate {
272 fn resolve(&self, close: f64, seq: usize, max_length: usize) -> Option<Outcome> {
273 if seq - self.anchors[0].position > max_length {
274 Some(Outcome::Fail)
275 } else if close > self.upper.at(seq) {
276 Some(self.on_up)
277 } else if close < self.lower.at(seq) {
278 Some(self.on_down)
279 } else {
280 None
281 }
282 }
283
284 fn formation(
285 &self,
286 direction: Option<PatternDirection>,
287 confirmed: bool,
288 end: (usize, usize),
289 ) -> Formation {
290 let start = self.anchors[self.from];
291 Formation {
292 kind: self.kind,
293 status: if confirmed {
294 FormationStatus::Confirmed
295 } else {
296 FormationStatus::Developing
297 },
298 direction,
299 anchors: self.anchors.iter().map(|p| (p.index, p.price)).collect(),
300 boundaries: [self.upper, self.lower]
301 .iter()
302 .zip(self.shown)
303 .filter(|&(_, shown)| shown)
304 .map(|(line, _)| {
305 [
306 (start.index, line.at(start.position)),
307 (end.1, line.at(end.0)),
308 ]
309 })
310 .collect(),
311 confirmed_at: confirmed.then_some(end.1),
312 }
313 }
314}
315
316fn near(a: f64, b: f64, tolerance: f64) -> bool {
318 (a - b).abs() <= tolerance / 100.0 * (a.abs() + b.abs()) / 2.0
319}
320
321fn last_pivots(pivots: &VecDeque<Swing>, count: usize) -> Option<Vec<Swing>> {
322 (pivots.len() >= count).then(|| pivots.iter().skip(pivots.len() - count).copied().collect())
323}
324
325fn flag(config: &Config, pivots: &VecDeque<Swing>, bull: bool, pennant: bool) -> Option<Candidate> {
328 let w = last_pivots(pivots, 5)?;
329 if w[0].high == bull || (w[1].price > w[0].price) != bull {
330 return None;
331 }
332 let pole = (w[1].price - w[0].price).abs() / w[0].price.abs() * 100.0;
333 let length = w[4].position - w[1].position;
334 if pole < config.pole
335 || length < config.min_length
336 || w[4].position - w[0].position > config.max_length
337 {
338 return None;
339 }
340 let (a, b) = (Line::through(&w[1], &w[3]), Line::through(&w[2], &w[4]));
341 let (upper, lower) = if bull { (a, b) } else { (b, a) };
342 let width = |seq| upper.at(seq) - lower.at(seq);
343 let (start, end) = (width(w[1].position), width(w[4].position));
344 let middle = (w[0].price + w[1].price) / 2.0;
345 if start <= 0.0 || end <= 0.0 || w[2..].iter().step_by(2).any(|p| (p.price > middle) != bull) {
346 return None;
347 }
348 let shaped = if pennant {
349 upper.slope < 0.0 && lower.slope > 0.0 && end <= start * (1.0 - SLACK)
350 } else {
351 let falling = |line: Line| (line.slope < 0.0) == bull && line.slope != 0.0;
352 falling(upper) && falling(lower) && (end / start - 1.0).abs() <= SLACK
353 };
354 shaped.then_some(Candidate {
355 kind: match (bull, pennant) {
356 (true, false) => FormationKind::BullFlag,
357 (false, false) => FormationKind::BearFlag,
358 (true, true) => FormationKind::BullPennant,
359 (false, true) => FormationKind::BearPennant,
360 },
361 anchors: w,
362 from: 1,
363 upper,
364 lower,
365 shown: [true; 2],
366 on_up: if bull {
367 Outcome::Confirm(PatternDirection::Bullish)
368 } else {
369 Outcome::Fail
370 },
371 on_down: if bull {
372 Outcome::Fail
373 } else {
374 Outcome::Confirm(PatternDirection::Bearish)
375 },
376 })
377}
378
379fn channel(config: &Config, pivots: &VecDeque<Swing>) -> Option<Candidate> {
381 let w = last_pivots(pivots, 2 * config.touches)?;
382 let side =
383 |high: bool| -> Vec<Swing> { w.iter().filter(|p| p.high == high).copied().collect() };
384 let (highs, lows) = (side(true), side(false));
385 let upper = Line::through(&highs[0], highs.last()?);
386 let lower = Line::through(&lows[0], lows.last()?);
387 let on_line = |pivots: &[Swing], line: Line| {
388 pivots
389 .iter()
390 .all(|p| near(p.price, line.at(p.position), config.tolerance))
391 };
392 let (first, last) = (w[0].position, w[w.len() - 1].position);
393 if !on_line(&highs, upper)
394 || !on_line(&lows, lower)
395 || !(config.min_length..=config.max_length).contains(&(last - first))
396 {
397 return None;
398 }
399 let (start, end) = (
400 upper.at(first) - lower.at(first),
401 upper.at(last) - lower.at(last),
402 );
403 if start <= 0.0 || end <= 0.0 {
404 return None;
405 }
406 let flat = |line: Line| {
407 (line.at(last) - line.at(first)).abs()
408 <= config.tolerance / 100.0 * line.at(first).abs().max(line.at(last).abs())
409 };
410 let kind = match (flat(upper), flat(lower)) {
411 (true, true) if end >= 2.0 * config.tolerance / 100.0 * lower.at(last).abs() => {
412 FormationKind::Rectangle
413 }
414 (true, false) if lower.slope > 0.0 => FormationKind::AscendingTriangle,
415 (false, true) if upper.slope < 0.0 => FormationKind::DescendingTriangle,
416 (false, false) if upper.slope < 0.0 && lower.slope > 0.0 => {
417 FormationKind::SymmetricalTriangle
418 }
419 _ => return None,
420 };
421 let up = Outcome::Confirm(PatternDirection::Bullish);
422 let down = Outcome::Confirm(PatternDirection::Bearish);
423 Some(Candidate {
424 kind,
425 anchors: w,
426 from: 0,
427 upper,
428 lower,
429 shown: [true; 2],
430 on_up: if kind == FormationKind::DescendingTriangle {
431 Outcome::Fail
432 } else {
433 up
434 },
435 on_down: if kind == FormationKind::AscendingTriangle {
436 Outcome::Fail
437 } else {
438 down
439 },
440 })
441}
442
443fn level(price: f64) -> Line {
445 Line {
446 seq: 0.0,
447 price,
448 slope: 0.0,
449 }
450}
451
452fn multiple(
454 config: &Config,
455 pivots: &VecDeque<Swing>,
456 top: bool,
457 triple: bool,
458) -> Option<Candidate> {
459 let w = last_pivots(pivots, if triple { 5 } else { 3 })?;
460 if w[0].high != top {
461 return None;
462 }
463 let peaks: Vec<f64> = w.iter().step_by(2).map(|p| p.price).collect();
464 let valleys: Vec<f64> = w.iter().skip(1).step_by(2).map(|p| p.price).collect();
465 let (high, low) = (
466 peaks.iter().copied().fold(f64::MIN, f64::max),
467 peaks.iter().copied().fold(f64::MAX, f64::min),
468 );
469 let mean = peaks.iter().sum::<f64>() / peaks.len() as f64;
470 let apart = w
471 .windows(3)
472 .all(|p| p[2].position - p[0].position >= config.min_length);
473 let deep = |valley: f64| (mean - valley) * if top { 1.0 } else { -1.0 } / mean.abs() * 100.0;
475 if !near(high, low, config.tolerance)
476 || !apart
477 || w[w.len() - 1].position - w[0].position > config.max_length
478 || valleys.iter().any(|&v| deep(v) < config.depth)
479 {
480 return None;
481 }
482 let tolerance = config.tolerance / 100.0;
483 let (neckline, kind) = match (top, triple) {
484 (true, false) => (
485 valleys.iter().copied().fold(f64::MAX, f64::min),
486 FormationKind::DoubleTop,
487 ),
488 (true, true) => (
489 valleys.iter().copied().fold(f64::MAX, f64::min),
490 FormationKind::TripleTop,
491 ),
492 (false, false) => (
493 valleys.iter().copied().fold(f64::MIN, f64::max),
494 FormationKind::DoubleBottom,
495 ),
496 (false, true) => (
497 valleys.iter().copied().fold(f64::MIN, f64::max),
498 FormationKind::TripleBottom,
499 ),
500 };
501 let (up, down) = (
502 Outcome::Confirm(PatternDirection::Bullish),
503 Outcome::Confirm(PatternDirection::Bearish),
504 );
505 let (upper, lower, on_up, on_down, shown) = if top {
506 (
507 level(high * (1.0 + tolerance)),
508 level(neckline),
509 Outcome::Fail,
510 down,
511 [false, true],
512 )
513 } else {
514 (
515 level(neckline),
516 level(low * (1.0 - tolerance)),
517 up,
518 Outcome::Fail,
519 [true, false],
520 )
521 };
522 Some(Candidate {
523 kind,
524 anchors: w,
525 from: 0,
526 upper,
527 lower,
528 on_up,
529 on_down,
530 shown,
531 })
532}
533
534fn head_and_shoulders(config: &Config, pivots: &VecDeque<Swing>, top: bool) -> Option<Candidate> {
536 let w = last_pivots(pivots, 5)?;
537 if w[0].high != top {
538 return None;
539 }
540 let side = if top { 1.0 } else { -1.0 };
541 let (left, first, head, second, right) = (w[0], w[1], w[2], w[3], w[4]);
542 let above = |shoulder: &Swing| {
543 side * (head.price - shoulder.price) / shoulder.price.abs() * 100.0 >= config.depth
544 };
545 if !near(left.price, right.price, config.tolerance)
546 || !above(&left)
547 || !above(&right)
548 || side * first.price >= side * left.price
549 || side * second.price >= side * right.price
550 || !(config.min_length..=config.max_length).contains(&(right.position - left.position))
551 {
552 return None;
553 }
554 let neckline = Line::through(&first, &second);
555 let guard = level(head.price);
556 let (kind, upper, lower, on_up, on_down, shown) = if top {
557 (
558 FormationKind::HeadAndShoulders,
559 guard,
560 neckline,
561 Outcome::Fail,
562 Outcome::Confirm(PatternDirection::Bearish),
563 [false, true],
564 )
565 } else {
566 (
567 FormationKind::InverseHeadAndShoulders,
568 neckline,
569 guard,
570 Outcome::Confirm(PatternDirection::Bullish),
571 Outcome::Fail,
572 [true, false],
573 )
574 };
575 Some(Candidate {
576 kind,
577 anchors: w,
578 from: 1,
579 upper,
580 lower,
581 on_up,
582 on_down,
583 shown,
584 })
585}
586
587fn cup(config: &Config, pivots: &VecDeque<Swing>, bull: bool) -> Option<Candidate> {
590 let w = last_pivots(pivots, 4)?;
591 if w[0].high != bull {
592 return None;
593 }
594 let side = if bull { 1.0 } else { -1.0 };
595 let (left, base, right, handle) = (w[0], w[1], w[2], w[3]);
596 let mean = (left.price + right.price) / 2.0;
597 let depth = side * (mean - base.price);
598 let duration = right.position - left.position;
599 let pullback = side * (right.price - handle.price);
600 if !near(left.price, right.price, config.tolerance)
601 || depth <= 0.0
602 || depth / mean.abs() * 100.0 < config.depth
603 || !(config.min_length..=config.max_length).contains(&duration)
604 || handle.position - left.position > config.max_length
605 || 4 * (base.position - left.position) < duration
606 || 4 * (right.position - base.position) < duration
607 || pullback <= 0.0
608 || pullback > config.handle / 100.0 * depth
609 || 2 * (handle.position - right.position) > duration
610 {
611 return None;
612 }
613 let (kind, upper, lower, on_up, on_down, shown) = if bull {
614 (
615 FormationKind::CupAndHandle,
616 level(left.price.max(right.price)),
617 level(base.price),
618 Outcome::Confirm(PatternDirection::Bullish),
619 Outcome::Fail,
620 [true, false],
621 )
622 } else {
623 (
624 FormationKind::InvertedCupAndHandle,
625 level(base.price),
626 level(left.price.min(right.price)),
627 Outcome::Fail,
628 Outcome::Confirm(PatternDirection::Bearish),
629 [false, true],
630 )
631 };
632 Some(Candidate {
633 kind,
634 anchors: w,
635 from: 0,
636 upper,
637 lower,
638 on_up,
639 on_down,
640 shown,
641 })
642}
643
644#[derive(Clone, Default)]
647struct State {
648 index: usize,
650 last: usize,
652 swings: Swings,
653 pending: Vec<Candidate>,
654 confirmed: Vec<Formation>,
655 used: [Option<usize>; 8],
657}
658
659impl State {
660 fn advance(
661 &mut self,
662 config: &Config,
663 family: PatternFamily,
664 enabled: &[bool; 8],
665 bar: &Bar,
666 ) -> [u8; 2] {
667 let index = self.index;
668 self.index += 1;
669 let mut marks = [0; 2];
670 if ![bar.open, bar.high, bar.low, bar.close]
671 .iter()
672 .all(|v| v.is_finite())
673 {
674 self.swings.skip();
675 return marks;
676 }
677 let seq = self.swings.position;
678 self.last = index;
679 if self
680 .swings
681 .push(bar, config.strength, config.strength, MAX_PIVOTS)
682 {
683 self.detect(config, family, enabled);
684 }
685 let mut i = 0;
686 while i < self.pending.len() {
687 match self.pending[i].resolve(bar.close, seq, config.max_length) {
688 None => i += 1,
689 Some(Outcome::Fail) => {
690 self.pending.remove(i);
691 }
692 Some(Outcome::Confirm(direction)) => {
693 let candidate = self.pending.remove(i);
694 self.confirmed
695 .push(candidate.formation(Some(direction), true, (seq, index)));
696 if self.confirmed.len() > MAX_CONFIRMED {
697 self.confirmed.remove(0);
698 }
699 let slot = family
700 .kinds()
701 .iter()
702 .position(|&k| k == candidate.kind)
703 .unwrap();
704 marks[direction as usize] |= 1 << slot;
705 }
706 }
707 }
708 marks
709 }
710
711 fn detect(&mut self, config: &Config, family: PatternFamily, enabled: &[bool; 8]) {
712 let pivots = &self.swings.pivots;
713 let found: Vec<Option<Candidate>> = match family {
714 PatternFamily::Continuation => vec![
715 flag(config, pivots, true, false),
716 flag(config, pivots, false, false),
717 flag(config, pivots, true, true),
718 flag(config, pivots, false, true),
719 channel(config, pivots),
720 ],
721 PatternFamily::Reversal => vec![
722 cup(config, pivots, true),
723 cup(config, pivots, false),
724 multiple(config, pivots, true, false),
725 multiple(config, pivots, false, false),
726 multiple(config, pivots, true, true),
727 multiple(config, pivots, false, true),
728 head_and_shoulders(config, pivots, true),
729 head_and_shoulders(config, pivots, false),
730 ],
731 };
732 let pennant = found.iter().flatten().any(|c| {
734 matches!(
735 c.kind,
736 FormationKind::BullPennant | FormationKind::BearPennant
737 ) && enabled[family.kinds().iter().position(|&k| k == c.kind).unwrap()]
738 });
739 for candidate in found.into_iter().flatten() {
740 if pennant
741 && candidate.kind == FormationKind::SymmetricalTriangle
742 && candidate.anchors.len() == 4
743 {
744 continue;
745 }
746 let slot = family
747 .kinds()
748 .iter()
749 .position(|&k| k == candidate.kind)
750 .unwrap();
751 let (first, last) = (
752 candidate.anchors[0].position,
753 candidate.anchors.last().unwrap().position,
754 );
755 if !enabled[slot] || self.used[slot].is_some_and(|used| first <= used) {
756 continue;
757 }
758 self.used[slot] = Some(last);
759 self.pending.push(candidate);
760 if self.pending.len() > MAX_PENDING {
761 self.pending.remove(0);
762 }
763 }
764 }
765}
766
767#[derive(Clone)]
825pub struct ChartPatterns {
826 family: PatternFamily,
827 config: Config,
828 enabled: [bool; 8],
829 found: Vec<[u8; 2]>,
831 values: IndicatorValues,
832 stepper: Stepper<State>,
833 #[cfg(feature = "drawings")]
834 drawings: Vec<crate::drawings::Drawing>,
835 colors: [Color32; 3],
836 visible: bool,
837}
838
839impl ChartPatterns {
840 pub fn new(family: PatternFamily) -> Self {
842 Self {
843 family,
844 config: match family {
845 PatternFamily::Continuation => Config {
846 strength: 3,
847 min_length: 8,
848 max_length: 120,
849 tolerance: 1.0,
850 touches: 2,
851 pole: 5.0,
852 depth: 3.0,
853 handle: 50.0,
854 },
855 PatternFamily::Reversal => Config {
856 strength: 3,
857 min_length: 10,
858 max_length: 200,
859 tolerance: 3.0,
860 touches: 2,
861 pole: 5.0,
862 depth: 3.0,
863 handle: 50.0,
864 },
865 },
866 enabled: [true; 8],
867 found: Vec::new(),
868 values: IndicatorValues::new(),
869 stepper: Stepper::default(),
870 #[cfg(feature = "drawings")]
871 drawings: Vec::new(),
872 colors: [palette::BLUE, palette::RED, palette::GRAY],
873 visible: true,
874 }
875 }
876
877 pub fn with_patterns(mut self, keep: impl Fn(&str) -> bool) -> Self {
879 for (on, kind) in self.enabled.iter_mut().zip(self.family.kinds()) {
880 *on = keep(kind.label());
881 }
882 self
883 }
884
885 pub fn with_geometry(
889 mut self,
890 strength: usize,
891 length: std::ops::RangeInclusive<usize>,
892 tolerance: f64,
893 touches: usize,
894 ) -> Self {
895 self.config.strength = strength.max(1);
896 self.config.min_length = *length.start();
897 self.config.max_length = (*length.end()).max(*length.start());
898 self.config.tolerance = clamp_param(tolerance, 0.0, 100.0);
899 self.config.touches = touches.clamp(2, MAX_PIVOTS / 2);
900 self
901 }
902
903 pub fn with_pole(mut self, percent: f64) -> Self {
905 self.config.pole = clamp_param(percent, 0.0, f64::MAX);
906 self
907 }
908
909 pub fn with_depth(mut self, depth: f64, handle: f64) -> Self {
913 self.config.depth = clamp_param(depth, 0.0, 100.0);
914 self.config.handle = clamp_param(handle, 0.0, 100.0);
915 self
916 }
917
918 pub fn formations(&self) -> Vec<Formation> {
921 let state = self.stepper.state();
922 let end = (state.swings.position.saturating_sub(1), state.last);
923 let mut formations = state.confirmed.clone();
924 formations.extend(
925 state
926 .pending
927 .iter()
928 .map(|c| c.formation(c.kind.expected(), false, end)),
929 );
930 formations
931 }
932}
933
934impl Indicator for ChartPatterns {
935 fn inputs(&self) -> Vec<StudyInput> {
936 let defaults = Self::new(self.family);
937 let length = |id: &str, label: &str, value, default| {
938 StudyInput::new(
939 id,
940 label,
941 InputValue::Length(value),
942 InputKind::Length(1, usize::MAX),
943 )
944 .with_default(InputValue::Length(default))
945 };
946 let number = |id: &str, label: &str, value, default| {
947 StudyInput::new(
948 id,
949 label,
950 InputValue::Number(value),
951 InputKind::Number(0.0, 100.0),
952 )
953 .with_default(InputValue::Number(default))
954 };
955 let (c, d) = (&self.config, &defaults.config);
956 let mut inputs = vec![
957 length("strength", "Swing strength", c.strength, d.strength),
958 length("min_length", "Minimum length", c.min_length, d.min_length),
959 length("max_length", "Maximum length", c.max_length, d.max_length),
960 number("tolerance", "Tolerance %", c.tolerance, d.tolerance),
961 ];
962 inputs.extend(match self.family {
963 PatternFamily::Continuation => vec![
964 StudyInput::new(
965 "touches",
966 "Touches per boundary",
967 InputValue::Length(c.touches),
968 InputKind::Length(2, MAX_PIVOTS / 2),
969 )
970 .with_default(InputValue::Length(d.touches)),
971 StudyInput::new(
972 "pole",
973 "Pole %",
974 InputValue::Number(c.pole),
975 InputKind::Number(0.0, f64::MAX),
976 )
977 .with_default(InputValue::Number(d.pole)),
978 ],
979 PatternFamily::Reversal => vec![
980 number("depth", "Depth %", c.depth, d.depth),
981 number("handle", "Handle pullback %", c.handle, d.handle),
982 ],
983 });
984 inputs.extend(self.family.kinds().iter().enumerate().map(|(k, kind)| {
985 StudyInput::new(
986 format!("patterns[{k}]"),
987 kind.label(),
988 InputValue::Toggle(self.enabled[k]),
989 InputKind::Toggle,
990 )
991 .with_default(InputValue::Toggle(true))
992 }));
993 inputs
994 }
995
996 fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
997 let schema = self
998 .inputs()
999 .into_iter()
1000 .find(|input| input.id == id)
1001 .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
1002 if !schema.accepts(&value) {
1003 return Err(crate::studies::StudyError::invalid_input(id));
1004 }
1005 match (id, value) {
1006 ("strength", InputValue::Length(v)) => self.config.strength = v,
1007 ("min_length", InputValue::Length(v)) => self.config.min_length = v,
1008 ("max_length", InputValue::Length(v)) => self.config.max_length = v,
1009 ("touches", InputValue::Length(v)) => self.config.touches = v,
1010 ("tolerance", InputValue::Number(v)) => self.config.tolerance = v,
1011 ("pole", InputValue::Number(v)) => self.config.pole = v,
1012 ("depth", InputValue::Number(v)) => self.config.depth = v,
1013 ("handle", InputValue::Number(v)) => self.config.handle = v,
1014 (_, InputValue::Toggle(on)) => {
1015 let slot = id
1016 .strip_prefix("patterns[")
1017 .and_then(|rest| rest.strip_suffix(']'))
1018 .and_then(|k| k.parse::<usize>().ok())
1019 .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
1020 self.enabled[slot] = on;
1021 }
1022 _ => return Err(crate::studies::StudyError::UnknownInput(id.into())),
1023 }
1024 self.calculate(&[]);
1025 Ok(())
1026 }
1027
1028 fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
1029 crate::studies::PlotStyle {
1030 color: self.colors[line.min(1)],
1031 width: 1.0,
1032 pattern: crate::model::LineStyle::Solid,
1033 plot_type: if line == 0 {
1034 crate::studies::PlotType::TriangleUp
1035 } else {
1036 crate::studies::PlotType::TriangleDown
1037 },
1038 visible: true,
1039 }
1040 }
1041
1042 fn plot_text(&self, line: usize, bar: usize) -> Option<String> {
1043 let bits = self.found.get(bar)?.get(line).copied()?;
1044 let text: Vec<&str> = self
1045 .family
1046 .kinds()
1047 .iter()
1048 .enumerate()
1049 .filter(|(k, _)| bits >> k & 1 == 1)
1050 .map(|(_, kind)| kind.abbreviation())
1051 .collect();
1052 (!text.is_empty()).then(|| text.join("\n"))
1053 }
1054
1055 fn name(&self) -> &str {
1056 self.family.name()
1057 }
1058
1059 fn desc(&self) -> &str {
1060 "Chart patterns found in swing pivots, confirmed by a close beyond their boundary"
1061 }
1062
1063 fn calculate(&mut self, data: &[Bar]) {
1064 self.update(data, 0);
1065 }
1066
1067 fn update(&mut self, data: &[Bar], from: usize) {
1068 let len = self.values.len();
1069 let start = if len > 0 && from >= len - 1 && data.len() >= len {
1070 from.min(len)
1071 } else {
1072 0
1073 };
1074 self.found.truncate(start);
1075 let (config, family, enabled) = (self.config, self.family, self.enabled);
1076 self.stepper
1077 .run(data, from, &mut self.values, |state, bar| {
1078 let marks = state.advance(&config, family, &enabled, bar);
1079 self.found.push(marks);
1080 if marks == [0, 0] {
1081 return IndicatorValue::None;
1082 }
1083 IndicatorValue::Multiple(vec![
1084 if marks[0] != 0 { bar.low } else { f64::NAN },
1085 if marks[1] != 0 { bar.high } else { f64::NAN },
1086 ])
1087 });
1088 #[cfg(feature = "drawings")]
1089 {
1090 self.drawings = self
1091 .formations()
1092 .iter()
1093 .flat_map(|formation| {
1094 let confirmed = formation.status == FormationStatus::Confirmed;
1095 let color = match formation.direction {
1096 Some(PatternDirection::Bullish) if confirmed => self.colors[0],
1097 Some(PatternDirection::Bearish) if confirmed => self.colors[1],
1098 _ => self.colors[2],
1099 };
1100 formation.boundaries.iter().map(move |segment| {
1101 let mut drawing = crate::drawings::Drawing::new(
1102 0,
1103 crate::drawings::DrawingToolType::TrendLine,
1104 );
1105 drawing.completed = true;
1106 drawing.color = color.to_srgba_unmultiplied();
1107 if !confirmed {
1108 drawing.line_style = crate::model::LineStyle::Dashed;
1109 }
1110 drawing.chart_points = segment
1111 .iter()
1112 .map(|&(bar, price)| {
1113 crate::drawings::ChartPoint::new(bar as f32, price)
1114 })
1115 .collect();
1116 drawing
1117 })
1118 })
1119 .collect();
1120 }
1121 }
1122
1123 fn is_incremental(&self) -> bool {
1124 true
1125 }
1126
1127 fn values(&self) -> &IndicatorValues {
1128 &self.values
1129 }
1130
1131 #[cfg(feature = "drawings")]
1132 fn drawings(&self) -> &[crate::drawings::Drawing] {
1133 &self.drawings
1134 }
1135
1136 fn colors(&self) -> Vec<Color32> {
1137 self.colors.to_vec()
1138 }
1139
1140 fn set_colors(&mut self, colors: Vec<Color32>) {
1141 for (slot, color) in self.colors.iter_mut().zip(colors) {
1142 *slot = color;
1143 }
1144 }
1145
1146 fn is_overlay(&self) -> bool {
1147 true
1148 }
1149
1150 fn line_names(&self) -> Vec<String> {
1151 vec!["Bullish".into(), "Bearish".into()]
1152 }
1153
1154 fn is_visible(&self) -> bool {
1155 self.visible
1156 }
1157
1158 fn set_visible(&mut self, visible: bool) {
1159 self.visible = visible;
1160 }
1161
1162 fn clone_box(&self) -> Box<dyn Indicator> {
1163 Box::new(self.clone())
1164 }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170 use chrono::{TimeZone, Utc};
1171
1172 fn path(anchors: &[f64]) -> Vec<Bar> {
1175 let mut closes = vec![anchors[0]];
1176 for pair in anchors.windows(2) {
1177 closes.extend((1..=5).map(|i| pair[0] + (pair[1] - pair[0]) * f64::from(i) / 5.0));
1178 }
1179 closes
1180 .iter()
1181 .enumerate()
1182 .map(|(i, &c)| {
1183 Bar::new(
1184 Utc.timestamp_opt(i as i64 * 60, 0).unwrap(),
1185 c,
1186 c,
1187 c,
1188 c,
1189 1.0,
1190 )
1191 })
1192 .collect()
1193 }
1194
1195 type Case = (
1197 &'static str,
1198 Vec<f64>,
1199 FormationKind,
1200 Option<(PatternDirection, usize)>,
1201 );
1202
1203 fn mirror(anchors: &[f64]) -> Vec<f64> {
1204 anchors.iter().map(|a| 260.0 - a).collect()
1205 }
1206
1207 fn found(study: &mut ChartPatterns, bars: &[Bar]) -> Vec<Formation> {
1208 study.calculate(bars);
1209 study.formations()
1210 }
1211
1212 const BULL_FLAG: [f64; 7] = [105.0, 100.0, 130.0, 122.0, 127.0, 119.0, 135.0];
1213 const BULL_PENNANT: [f64; 7] = [105.0, 100.0, 130.0, 118.0, 125.0, 121.0, 135.0];
1214 const RECTANGLE: [f64; 6] = [105.0, 110.0, 100.0, 110.5, 100.5, 115.0];
1215 const ASCENDING: [f64; 6] = [105.0, 110.0, 100.0, 110.5, 103.0, 116.0];
1216 const SYMMETRICAL: [f64; 6] = [106.0, 112.0, 100.0, 109.0, 103.0, 114.0];
1217
1218 #[test]
1223 fn continuation_fixtures_confirm_where_the_rules_say() {
1224 use FormationKind::*;
1225 use PatternDirection::*;
1226 let flat = |a: &[f64]| a.to_vec();
1227 #[rustfmt::skip]
1228 let cases: [Case; 14] = [
1229 ("bull flag", flat(&BULL_FLAG), BullFlag, Some((Bullish, 28))),
1230 ("bear flag", mirror(&BULL_FLAG), BearFlag, Some((Bearish, 28))),
1231 ("flag with widening lows", vec![105.0, 100.0, 130.0, 122.0, 127.0, 116.0, 135.0], BullFlag, None),
1232 ("flag with a small pole", vec![105.0, 100.0, 104.0, 102.8, 103.5, 102.3, 108.0], BullFlag, None),
1233 ("bull pennant", flat(&BULL_PENNANT), BullPennant, Some((Bullish, 28))),
1234 ("bear pennant", mirror(&BULL_PENNANT), BearPennant, Some((Bearish, 28))),
1235 ("pennant too wide", vec![105.0, 100.0, 130.0, 118.0, 129.0, 118.9, 135.0], BullPennant, None),
1236 ("rectangle up", flat(&RECTANGLE), Rectangle, Some((Bullish, 24))),
1237 ("rectangle down", mirror(&RECTANGLE), Rectangle, Some((Bearish, 24))),
1238 ("rectangle with unequal lows", vec![105.0, 110.0, 100.0, 110.5, 98.5, 115.0], Rectangle, None),
1239 ("ascending triangle", flat(&ASCENDING), AscendingTriangle, Some((Bullish, 24))),
1240 ("descending triangle", mirror(&ASCENDING), DescendingTriangle, Some((Bearish, 24))),
1241 ("rising channel", vec![105.0, 110.0, 100.0, 113.0, 103.0, 116.0], AscendingTriangle, None),
1242 ("symmetrical triangle", flat(&SYMMETRICAL), SymmetricalTriangle, Some((Bullish, 23))),
1243 ];
1244 for (label, anchors, kind, expected) in cases {
1245 let formations = found(
1246 &mut ChartPatterns::new(PatternFamily::Continuation),
1247 &path(&anchors),
1248 );
1249 let got: Vec<_> = formations
1250 .iter()
1251 .filter(|f| f.kind == kind && f.status == FormationStatus::Confirmed)
1252 .map(|f| (f.direction.unwrap(), f.confirmed_at.unwrap()))
1253 .collect();
1254 assert_eq!(got, expected.into_iter().collect::<Vec<_>>(), "{label}");
1255 }
1256
1257 let small = [105.0, 100.0, 104.0, 102.8, 103.5, 102.3, 108.0];
1259 let formations = found(
1260 &mut ChartPatterns::new(PatternFamily::Continuation).with_pole(3.0),
1261 &path(&small),
1262 );
1263 assert_eq!(
1264 formations.iter().map(|f| f.kind).collect::<Vec<_>>(),
1265 [BullFlag]
1266 );
1267
1268 let mut off = ChartPatterns::new(PatternFamily::Continuation);
1270 off.set_input("patterns[0]", InputValue::Toggle(false))
1271 .unwrap();
1272 let formations = found(&mut off, &path(&BULL_FLAG));
1273 assert!(formations.iter().all(|f| f.kind != BullFlag));
1274
1275 let mut wide = ChartPatterns::new(PatternFamily::Continuation);
1277 wide.set_input("strength", InputValue::Length(usize::MAX))
1278 .unwrap();
1279 assert!(found(&mut wide, &path(&BULL_FLAG)).is_empty());
1280
1281 let down = [106.0, 112.0, 100.0, 109.0, 103.0, 105.5, 95.0];
1284 let formations = found(
1285 &mut ChartPatterns::new(PatternFamily::Continuation),
1286 &path(&down),
1287 );
1288 assert_eq!(
1289 formations
1290 .iter()
1291 .filter(|f| f.kind == SymmetricalTriangle)
1292 .map(|f| (f.direction, f.confirmed_at))
1293 .collect::<Vec<_>>(),
1294 [(Some(Bearish), Some(26))]
1295 );
1296 }
1297
1298 #[test]
1304 fn a_confirmed_flag_reports_its_anchors_boundaries_and_confirmation_bar() {
1305 let mut study = ChartPatterns::new(PatternFamily::Continuation);
1306 let bars = path(&BULL_FLAG);
1307 let formations = found(&mut study, &bars);
1308 assert_eq!(
1309 formations,
1310 [Formation {
1311 kind: FormationKind::BullFlag,
1312 status: FormationStatus::Confirmed,
1313 direction: Some(PatternDirection::Bullish),
1314 anchors: vec![
1315 (5, 100.0),
1316 (10, 130.0),
1317 (15, 122.0),
1318 (20, 127.0),
1319 (25, 119.0)
1320 ],
1321 boundaries: vec![[(10, 130.0), (28, 124.6)], [(10, 123.5), (28, 118.1)]],
1322 confirmed_at: Some(28),
1323 }]
1324 );
1325 assert_eq!(study.plot_text(0, 28).as_deref(), Some("BF"));
1326 assert_eq!(study.values().value(0, 28), Some(bars[28].low));
1327 assert_eq!(study.plot_text(0, 27), None);
1328
1329 let inside = found(&mut study, &bars[..27]);
1331 assert_eq!(inside, []);
1332 let mut held = BULL_FLAG;
1333 held[6] = 122.0;
1334 let developing = found(&mut study, &path(&held)[..31]);
1335 assert_eq!(developing.len(), 1);
1336 assert_eq!(developing[0].status, FormationStatus::Developing);
1337 assert_eq!(developing[0].direction, Some(PatternDirection::Bullish));
1338 assert_eq!(developing[0].confirmed_at, None);
1339 assert_eq!(developing[0].boundaries[0][1].0, 30);
1340 }
1341
1342 const DOUBLE_TOP: [f64; 5] = [100.0, 120.0, 105.0, 119.0, 60.0];
1343 const TRIPLE_TOP: [f64; 7] = [100.0, 120.0, 105.0, 119.5, 105.5, 119.0, 60.0];
1344 const HEAD_AND_SHOULDERS: [f64; 7] = [100.0, 110.0, 100.0, 120.0, 101.0, 111.0, 80.0];
1345 const CUP: [f64; 6] = [110.0, 120.0, 100.0, 119.0, 114.0, 130.0];
1346
1347 #[test]
1353 fn reversal_fixtures_confirm_where_the_rules_say() {
1354 use FormationKind::*;
1355 use PatternDirection::*;
1356 let flat = |a: &[f64]| a.to_vec();
1357 #[rustfmt::skip]
1358 let cases: [Case; 20] = [
1359 ("double top", flat(&DOUBLE_TOP), DoubleTop, Some((Bearish, 18))),
1360 ("double bottom", mirror(&DOUBLE_TOP), DoubleBottom, Some((Bullish, 18))),
1361 ("double top with unequal tops", vec![100.0, 120.0, 105.0, 115.0, 60.0], DoubleTop, None),
1362 ("double top with a shallow valley", vec![100.0, 120.0, 118.0, 119.5, 60.0], DoubleTop, None),
1363 ("triple top", flat(&TRIPLE_TOP), TripleTop, Some((Bearish, 28))),
1364 ("triple bottom", mirror(&TRIPLE_TOP), TripleBottom, Some((Bullish, 28))),
1365 ("triple top with a low third top", vec![100.0, 120.0, 105.0, 119.5, 105.5, 112.0, 60.0], TripleTop, None),
1366 ("head and shoulders", flat(&HEAD_AND_SHOULDERS), HeadAndShoulders, Some((Bearish, 28))),
1367 ("inverse head and shoulders", mirror(&HEAD_AND_SHOULDERS), InverseHeadAndShoulders, Some((Bullish, 28))),
1368 ("head too low", vec![100.0, 110.0, 100.0, 112.0, 101.0, 111.0, 80.0], HeadAndShoulders, None),
1369 ("unequal shoulders", vec![100.0, 110.0, 100.0, 125.0, 101.0, 116.0, 80.0], HeadAndShoulders, None),
1370 ("cup and handle", flat(&CUP), CupAndHandle, Some((Bullish, 23))),
1371 ("inverted cup and handle", mirror(&CUP), InvertedCupAndHandle, Some((Bearish, 23))),
1372 ("handle too deep", vec![110.0, 120.0, 100.0, 119.0, 105.0, 130.0], CupAndHandle, None),
1373 ("unequal rims", vec![110.0, 120.0, 100.0, 112.0, 108.0, 130.0], CupAndHandle, None),
1374 ("cup without a break", vec![110.0, 120.0, 100.0, 119.0, 114.0, 116.0], CupAndHandle, None),
1375 ("double top that recovers", vec![100.0, 120.0, 105.0, 119.0, 125.0], DoubleTop, None),
1376 ("head and shoulders that recovers", vec![100.0, 110.0, 100.0, 120.0, 101.0, 111.0, 125.0], HeadAndShoulders, None),
1377 ("double top with no break", vec![100.0, 120.0, 105.0, 119.0, 112.0], DoubleTop, None),
1378 ("cup with a break back down", vec![110.0, 120.0, 100.0, 119.0, 114.0, 95.0], CupAndHandle, None),
1379 ];
1380 for (label, anchors, kind, expected) in cases {
1381 let formations = found(
1382 &mut ChartPatterns::new(PatternFamily::Reversal),
1383 &path(&anchors),
1384 );
1385 let got: Vec<_> = formations
1386 .iter()
1387 .filter(|f| f.kind == kind && f.status == FormationStatus::Confirmed)
1388 .map(|f| (f.direction.unwrap(), f.confirmed_at.unwrap()))
1389 .collect();
1390 assert_eq!(got, expected.into_iter().collect::<Vec<_>>(), "{label}");
1391 }
1392
1393 let formations = found(
1396 &mut ChartPatterns::new(PatternFamily::Reversal),
1397 &path(&[100.0, 120.0, 105.0, 119.0, 112.0]),
1398 );
1399 let double: Vec<_> = formations.iter().filter(|f| f.kind == DoubleTop).collect();
1400 assert_eq!(double.len(), 1);
1401 assert_eq!(double[0].status, FormationStatus::Developing);
1402 assert_eq!(double[0].direction, Some(Bearish));
1403 assert_eq!(double[0].anchors, [(5, 120.0), (10, 105.0), (15, 119.0)]);
1404 assert_eq!(double[0].boundaries, [[(5, 105.0), (20, 105.0)]]);
1405
1406 let formations = found(
1409 &mut ChartPatterns::new(PatternFamily::Reversal),
1410 &path(&HEAD_AND_SHOULDERS),
1411 );
1412 let shoulders = formations
1413 .iter()
1414 .find(|f| f.kind == HeadAndShoulders)
1415 .unwrap();
1416 assert_eq!(shoulders.boundaries.len(), 1);
1417 let [(from, start), (to, end)] = shoulders.boundaries[0];
1418 assert_eq!((from, to), (10, 28));
1419 assert!((start - 100.0).abs() < 1e-9 && (end - 101.8).abs() < 1e-9);
1420
1421 let apart = found(
1423 &mut ChartPatterns::new(PatternFamily::Reversal).with_geometry(3, 12..=200, 3.0, 2),
1424 &path(&DOUBLE_TOP),
1425 );
1426 assert!(apart.iter().all(|f| f.kind != DoubleTop));
1427 }
1428
1429 #[test]
1432 fn prefixes_and_incremental_updates_agree_with_the_full_history() {
1433 use PatternFamily::{Continuation, Reversal};
1434 for (family, anchors) in [
1435 (Continuation, BULL_FLAG.to_vec()),
1436 (Continuation, RECTANGLE.to_vec()),
1437 (Continuation, mirror(&ASCENDING)),
1438 (Reversal, TRIPLE_TOP.to_vec()),
1439 (Reversal, HEAD_AND_SHOULDERS.to_vec()),
1440 (Reversal, mirror(&CUP)),
1441 ] {
1442 let bars = path(&anchors);
1443 let full = found(&mut ChartPatterns::new(family), &bars);
1444 assert!(!full.is_empty());
1445 let mut live = ChartPatterns::new(family);
1446 for k in 1..=bars.len() {
1447 let confirmed = |formations: Vec<Formation>| -> Vec<Formation> {
1448 formations
1449 .into_iter()
1450 .filter(|f| f.status == FormationStatus::Confirmed)
1451 .collect()
1452 };
1453 let replay = confirmed(found(&mut ChartPatterns::new(family), &bars[..k]));
1454 let expected: Vec<_> = confirmed(full.clone())
1455 .into_iter()
1456 .filter(|f| f.confirmed_at.unwrap() < k)
1457 .collect();
1458 assert_eq!(replay, expected, "prefix {k}");
1459 live.update(&bars[..k], k - 1);
1460 let fresh = found(&mut ChartPatterns::new(family), &bars[..k]);
1461 assert_eq!(live.formations(), fresh, "update at {k}");
1462 }
1463 }
1464 }
1465}