Skip to main content

egui_charts/studies/builtin/
chart_patterns.rs

1//! Automatic chart patterns found in confirmed swing pivots.
2//!
3//! [`ChartPatterns`] detects continuation formations (flags, pennants, rectangles and
4//! triangles) or reversal formations (cup and handle, double and triple tops and bottoms,
5//! head and shoulders) in a bar history, and returns each one's anchors, boundary segments,
6//! direction and confirmation time through [`ChartPatterns::formations`]. Developing formations are
7//! told apart from confirmed ones, and every confirmed formation is marked on the bar that
8//! confirms it.
9
10use 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
21/// A flag's end width stays within this fraction of its start width, and a pennant, triangle
22/// or other converging pair of lines ends at most this fraction narrower.
23const SLACK: f64 = 0.25;
24/// Pivots kept: enough for the longest formation, eight touches on each of two boundaries.
25const MAX_PIVOTS: usize = 16;
26/// Developing formations kept; the oldest is dropped first.
27const MAX_PENDING: usize = 8;
28/// Confirmed formations kept; the oldest is dropped first.
29const MAX_CONFIRMED: usize = 64;
30
31/// The formation a detector looks for.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum FormationKind {
34    /// A rise, then a falling channel; confirms above it.
35    BullFlag,
36    /// A fall, then a rising channel; confirms below it.
37    BearFlag,
38    /// A rise, then converging lines; confirms above them.
39    BullPennant,
40    /// A fall, then converging lines; confirms below them.
41    BearPennant,
42    /// Flat highs over flat lows; confirms in the direction of the break.
43    Rectangle,
44    /// Flat highs over rising lows; confirms above.
45    AscendingTriangle,
46    /// Falling highs over flat lows; confirms below.
47    DescendingTriangle,
48    /// Falling highs over rising lows; confirms in the direction of the break.
49    SymmetricalTriangle,
50    /// A rounded fall and recovery to the same level, a small pullback, then a break above the rim.
51    CupAndHandle,
52    /// The mirror image: a rounded rise and return, a small bounce, then a break below the rim.
53    InvertedCupAndHandle,
54    /// Two tops at the same level; confirms below the valley between them.
55    DoubleTop,
56    /// Two bottoms at the same level; confirms above the peak between them.
57    DoubleBottom,
58    /// Three tops at the same level; confirms below the lower valley.
59    TripleTop,
60    /// Three bottoms at the same level; confirms above the higher peak.
61    TripleBottom,
62    /// A head above two shoulders; confirms below the neckline through the valleys.
63    HeadAndShoulders,
64    /// A head below two shoulders; confirms above the neckline through the peaks.
65    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    /// The direction the formation resolves in, `None` for a two-sided one.
112    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/// Which formations a [`ChartPatterns`] study looks for.
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub enum PatternFamily {
136    /// Flags, pennants, rectangles and triangles.
137    Continuation,
138    /// Cup and handle, double and triple tops and bottoms, head and shoulders.
139    Reversal,
140}
141
142impl PatternFamily {
143    /// Every family.
144    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/// The side a confirmed formation points to.
180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181pub enum PatternDirection {
182    /// A break above the formation.
183    Bullish,
184    /// A break below the formation.
185    Bearish,
186}
187
188/// Whether a formation has broken out.
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190pub enum FormationStatus {
191    /// All its pivots are known but no close has broken its boundary yet.
192    Developing,
193    /// A close broke its boundary, on bar [`Formation::confirmed_at`].
194    Confirmed,
195}
196
197/// A detected formation, with bar indices into the calculated bars.
198#[derive(Clone, Debug, PartialEq)]
199pub struct Formation {
200    /// The formation's shape.
201    pub kind: FormationKind,
202    /// Developing or confirmed.
203    pub status: FormationStatus,
204    /// The confirmed direction; for a developing formation the direction its kind resolves in,
205    /// `None` for a rectangle or symmetrical triangle.
206    pub direction: Option<PatternDirection>,
207    /// The pivots that form it, as bar index and price, oldest first.
208    pub anchors: Vec<(usize, f64)>,
209    /// Boundary segments as bar index and price at each end. A confirmed formation's end at
210    /// its confirmation bar, a developing one's at the last bar.
211    pub boundaries: Vec<[(usize, f64); 2]>,
212    /// The bar whose close confirmed the formation.
213    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/// A straight line in position and price.
229#[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/// A formation whose pivots are known, waiting for a close beyond a boundary.
257#[derive(Clone)]
258struct Candidate {
259    kind: FormationKind,
260    anchors: Vec<Swing>,
261    /// Index of the anchor the drawn boundaries start at.
262    from: usize,
263    upper: Line,
264    lower: Line,
265    on_up: Outcome,
266    on_down: Outcome,
267    /// Whether the upper and lower line are reported as boundaries.
268    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
316/// `a` and `b` differ by at most `tolerance` percent of their mean.
317fn 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
325/// A flag or pennant: a pole from the first pivot to the second, then lines through the
326/// second and fourth pivots and through the third and fifth.
327fn 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
379/// A rectangle or triangle through the last `2 * touches` pivots.
380fn 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
443/// A horizontal line.
444fn level(price: f64) -> Line {
445    Line {
446        seq: 0.0,
447        price,
448        slope: 0.0,
449    }
450}
451
452/// A double or triple top or bottom through the last three or five pivots.
453fn 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    // A valley this far below (above) a top (bottom).
474    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
534/// A head and shoulders (or its inverse) through the last five pivots.
535fn 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
587/// A cup and handle (or its inverse) through the last four pivots: the rims, the cup's
588/// extreme between them and the handle's.
589fn 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/// The detectors' state after a bar: the bars of the latest pivot test, the pivots,
645/// the formations waiting for a break and the confirmed ones.
646#[derive(Clone, Default)]
647struct State {
648    /// Bars seen, finite or not.
649    index: usize,
650    /// Index of the last finite bar.
651    last: usize,
652    swings: Swings,
653    pending: Vec<Candidate>,
654    confirmed: Vec<Formation>,
655    /// Per kind, the last pivot of the latest formation found.
656    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        // A pennant's last four pivots are also a symmetrical triangle; only the pennant is kept.
733        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/// Automatic chart patterns: the formations of a [`PatternFamily`], each with a toggle,
768/// detected from swing pivots and marked on the bar that confirms them. Read the formations,
769/// with their anchors, boundaries, direction and confirmation bar, from
770/// [`formations`](Self::formations).
771///
772/// # Rules
773///
774/// A swing pivot is a bar whose high (low) is greater (lower) than those of the `strength`
775/// bars before it and at least (at most) those of the `strength` bars after it. It is known
776/// `strength` bars later, and consecutive pivots of one kind keep the more extreme. A
777/// formation is built from the last pivots, so it exists only once its last pivot is known.
778///
779/// A line through two pivots is projected forward. A formation is confirmed on the first bar
780/// from the one that completes it onward whose close lies beyond its breakout boundary, and
781/// is dropped if the close first crosses the opposite guard or the formation is older than
782/// `max_length` bars. A bar with a non-finite price is skipped as if absent, and lengths
783/// count the other bars. Every rule reads bars up to the confirmation bar only, so results
784/// on a prefix of the history equal those of the full history up to that bar.
785///
786/// | Formation | Pivots | Rule |
787/// |---|---|---|
788/// | Bull flag | low, high, low, high, low | pole from the first to the second rises at least `pole` percent; the highs and the lows after the pole fall along nearly parallel lines (the end width within a quarter of the start width); both lows stay above the pole's middle |
789/// | Bear flag | mirror | the pole falls and the flag rises |
790/// | Bull pennant | as the bull flag | the highs fall, the lows rise and the end width is at most three quarters of the start width |
791/// | Bear pennant | as the bear flag | the same converging lines |
792/// | Rectangle | `2 * touches` alternating pivots | each boundary through its first and last pivot is flat, all its pivots within `tolerance` percent of it, and the height at least twice the tolerance |
793/// | Ascending triangle | as the rectangle | flat upper boundary, rising lower boundary |
794/// | Descending triangle | as the rectangle | falling upper boundary, flat lower boundary |
795/// | Symmetrical triangle | as the rectangle | falling upper and rising lower boundary |
796///
797/// | Cup and handle | high, low, high, low | the rims differ by at most `tolerance` percent of their mean; the cup is at least `depth` percent deep below the mean rim, lasts between `min_length` and `max_length` bars and has its low in the middle half of that time; the handle pulls back from the right rim by at most `handle` percent of the cup depth over at most half the cup's duration; confirms bullish above the higher rim, fails below the cup's low |
798/// | Inverted cup and handle | mirror | confirms bearish below the lower rim |
799/// | Double top | high, low, high | the tops differ by at most `tolerance` percent; at least `min_length` bars apart; the valley at least `depth` percent below them; the neckline is the valley's low; confirms bearish below it, fails above the higher top by more than `tolerance` percent |
800/// | Double bottom | mirror | the neckline is the valley's high; confirms bullish above it |
801/// | Triple top | high, low, high, low, high | as the double top for all three tops and both valleys; the neckline is the lower valley |
802/// | Triple bottom | mirror | the neckline is the higher valley |
803/// | Head and shoulders | high, low, high, low, high | the shoulders differ by at most `tolerance` percent, the head is at least `depth` percent above each, each valley lies below its shoulder, the shoulders are between `min_length` and `max_length` bars apart; the neckline runs through the valleys and may slope; confirms bearish below it, fails above the head |
804/// | Inverse head and shoulders | mirror | confirms bullish above the neckline |
805///
806/// A boundary is flat when it moves at most `tolerance` percent between the first and last
807/// pivot. A flag or pennant spans at least `min_length` bars from its pole's top and at most
808/// `max_length` bars from the pole's start; the others span between `min_length` and
809/// `max_length` bars from their first pivot. A bull flag, bull pennant and ascending triangle
810/// confirm bullish above their upper boundary; a bear flag, bear pennant and descending
811/// triangle confirm bearish below their lower boundary. A
812/// rectangle and a symmetrical triangle confirm in the direction of the break. A pennant's
813/// last four pivots also form a symmetrical triangle, which is then not reported, and a triple
814/// top or bottom follows the double top or bottom of its first two tops. Each pivot
815/// belongs to at most one formation of a kind. At most 16 pivots, 8 developing and 64
816/// confirmed formations are kept.
817///
818/// # Outputs
819///
820/// Line 0 marks a bullish confirmation at the low of its bar, line 1 a bearish one at the
821/// high, each labelled with the abbreviations of the formations it confirms. Boundaries are
822/// also generated as drawings, dashed while developing; a reversal formation draws only its
823/// neckline or breakout level.
824#[derive(Clone)]
825pub struct ChartPatterns {
826    family: PatternFamily,
827    config: Config,
828    enabled: [bool; 8],
829    /// One bit per kind of the family confirmed bullish and bearish on each bar.
830    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    /// Detect every formation of `family` with its conventional parameters.
841    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    /// Detect only the formations whose label, such as `"Bull Flag"`, `keep` accepts.
878    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    /// Set the bars on each side of a pivot (at least 1), the shortest and longest formation in
886    /// bars, the price tolerance in percent and the pivots per boundary (2 to 8). Bounds are
887    /// clamped, NaN to the bottom.
888    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    /// The smallest rise or fall of a flag or pennant pole, in percent; NaN counts as zero.
904    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    /// The smallest depth of a cup, of a valley between tops and of a head above its shoulders,
910    /// in percent, and the largest pullback of a cup's handle, in percent of the cup's depth,
911    /// each clamped into 0 to 100, NaN to 0.
912    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    /// The confirmed formations, oldest first, then the developing ones, from the last
919    /// calculation. Developing formations are re-evaluated on every bar.
920    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    /// Flat bars along straight legs of 5 bars between `anchors`, so every anchor is a pivot at
1173    /// a multiple of 5 (the first bar excepted) and is known 3 bars later.
1174    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    /// A label, anchors, the formation looked for and its expected direction and confirmation bar.
1196    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    /// Each fixture confirms its formation on the hand-computed bar, or, as a near miss that
1219    /// breaks one rule, finds nothing: the flag's lows widen the channel, the pennant's lines
1220    /// stay too far apart, the rectangle's lows differ by more than the tolerance, and the
1221    /// triangles lack the flat or the converging side. Mirrors give the bearish kinds.
1222    #[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        // The small-pole flag is found once its 4 percent pole is enough.
1258        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        // A formation's toggle is an input: with the bull flag off, none is found.
1269        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        // A swing strength at the input's upper bound finds no pivot instead of overflowing.
1276        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        // A symmetrical triangle whose last low is followed by a bounce and a break down
1282        // confirms bearish, on the first close under its lower line.
1283        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    /// The bull flag: pole 100 to 130, falling highs 130 and 127 and lows 122 and 119 on lines
1299    /// of slope -0.3 (so 6.5 apart throughout), the close passing the upper line
1300    /// first on bar 27 but the last pivot only becoming known on bar 28, where it confirms.
1301    /// A flag still inside its lines is developing, drawn dashed, with a boundary end on the
1302    /// last bar.
1303    #[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        // Cut after the pivot is known and before any break: the same flag is developing.
1330        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    /// Each reversal fixture confirms on the hand-computed bar, often later than the first
1348    /// close beyond the neckline because the last pivot is known only three bars on, or, as a
1349    /// near miss breaking one rule, does not: unequal tops, a shallow valley, a third top
1350    /// that is too low, a head too close to its shoulders, shoulders of unequal height, and a
1351    /// handle that gives back more than half the cup. Mirrors give the other direction.
1352    #[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        // The double top without a break is developing, with only its neckline as boundary,
1394        // ending on the last bar.
1395        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        // A head and shoulders with a sloping neckline through (10, 100) and (20, 101) breaks
1407        // it where the line is, 101.7 on bar 27, but is known only on bar 28.
1408        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        // With a minimum separation longer than the tops have, the same double top is not one.
1422        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    /// Replaying any prefix of the bars finds the confirmed formations the full history
1430    /// confirmed by then and no others, and updating bar by bar equals a fresh calculation.
1431    #[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}