Skip to main content

egui_charts/studies/builtin/
elliott_wave.rs

1//! Elliott impulse waves.
2//!
3//! Finds five-wave impulses in confirmed swing pivots, with the wave number of each anchor, and
4//! lists the developing and the confirmed constructions.
5//!
6//! # Rule
7//!
8//! Swing pivots are confirmed `right` bars after their bar and alternate between highs and lows
9//! (see [`PivotPointsHighLow`](super::PivotPointsHighLow)). Six pivots `P0` to `P5` starting at a
10//! low form a bullish impulse, starting at a high a bearish one, which mirrors it. For the bullish
11//! impulse, with `Wn` the length of wave `n` between `P(n-1)` and `Pn`:
12//!
13//! ```text
14//! Wave 2 stays above the start of wave 1:   P2 > P0
15//! Wave 3 passes the end of wave 1:          P3 > P1
16//! Wave 4 does not overlap wave 1:           P4 > P1
17//! Wave 3 is not the shortest of 1, 3 and 5: not (W3 < W1 and W3 < W5)
18//! ```
19//!
20//! A truncated fifth wave is allowed. Whenever the swings change, the latest pivots are checked for
21//! the longest construction that satisfies the rules up to the wave reached. From wave 3 on it is
22//! developing, with its wave number at the latest anchor; it is invalidated when the close breaks
23//! the end of wave 1 (`P1`), where wave 4 would overlap wave 1, or when a later pivot breaks a rule.
24//! A construction that reaches `P5` is confirmed on the bar that confirms `P5` and consumes its
25//! pivots, except `P5`, so the next one needs new pivots. Confirmed constructions never change.
26//!
27//! The outputs are the price of the latest developing anchor, the price of the last anchor of a
28//! confirmed bullish and of a confirmed bearish impulse, and the signed wave number, positive for a
29//! bullish construction, which is hidden. Each is on the bar that confirms the pivot and drawn
30//! displaced to the pivot's own bar. The table lists the developing construction and the last
31//! `history` confirmed ones with their direction, confirmation time and the price of every anchor
32//! numbered from 0. Bars with a non-finite high, low or close are skipped. The defaults are 5 bars on
33//! each side of a pivot and 5 confirmed impulses.
34
35use super::swings::{Swing, Swings};
36use crate::model::Bar;
37use crate::studies::palette;
38use crate::studies::stepper::Stepper;
39use crate::studies::{Indicator, IndicatorValue, IndicatorValues, StudyCell, StudyTable};
40use chrono::{DateTime, Utc};
41use egui::Color32;
42use std::collections::VecDeque;
43
44/// Elliott Wave: five-wave impulses in confirmed swing pivots.
45#[derive(Clone)]
46pub struct ElliottWave {
47    left: usize,
48    right: usize,
49    history: usize,
50    values: IndicatorValues,
51    stepper: Stepper<State>,
52    table: StudyTable,
53    colors: [Color32; 3],
54    visible: bool,
55}
56
57/// Anchors `P0` to `Pn` of a construction.
58#[derive(Clone)]
59struct Construction {
60    anchors: Vec<Swing>,
61    confirmed: Option<DateTime<Utc>>,
62}
63
64impl Construction {
65    fn bullish(&self) -> bool {
66        !self.anchors[0].high
67    }
68
69    /// The wave number reached.
70    fn wave(&self) -> usize {
71        self.anchors.len() - 1
72    }
73}
74
75#[derive(Clone, Default)]
76struct State {
77    swings: Swings,
78    developing: Option<Construction>,
79    confirmed: VecDeque<Construction>,
80}
81
82/// The longest construction of at least three waves that ends at the newest pivot.
83fn construct(pivots: &VecDeque<Swing>) -> Option<Construction> {
84    (3..=5).rev().find_map(|waves| {
85        let anchors: Vec<_> = pivots
86            .range(pivots.len().checked_sub(waves + 1)?..)
87            .copied()
88            .collect();
89        // Prices as seen by a bullish impulse, negated for a bearish one.
90        let sign = if anchors[0].high { -1.0 } else { 1.0 };
91        let p = |n: usize| sign * anchors[n].price;
92        let wave = |n: usize| p(n) - p(n - 1);
93        let valid = p(2) > p(0)
94            && p(3) > p(1)
95            && (waves < 4 || p(4) > p(1))
96            && (waves < 5 || wave(3) >= wave(1) || wave(3) >= wave(5));
97        valid.then_some(Construction {
98            anchors,
99            confirmed: None,
100        })
101    })
102}
103
104impl ElliottWave {
105    /// Pivots with `left` and `right` bars on each side, each raised to at least 1, and the last
106    /// `history` confirmed impulses listed, from 1 to 100.
107    pub fn new(left: usize, right: usize, history: usize) -> Self {
108        Self {
109            left: left.max(1),
110            right: right.max(1),
111            history: history.clamp(1, 100),
112            values: IndicatorValues::new(),
113            stepper: Stepper::default(),
114            table: StudyTable {
115                title: "Elliott Wave".into(),
116                columns: ["Impulse", "Confirmed", "Anchors"]
117                    .map(String::from)
118                    .to_vec(),
119                rows: Vec::new(),
120            },
121            colors: [palette::ACCENT, palette::GREEN, palette::RED],
122            visible: true,
123        }
124    }
125}
126
127/// Construct with the conventional default parameters.
128impl Default for ElliottWave {
129    fn default() -> Self {
130        Self::new(5, 5, 5)
131    }
132}
133
134impl Indicator for ElliottWave {
135    crate::studies::inputs::study_inputs!(self;
136        left: length("Pivot bars left", 1, usize::MAX),
137        right: length("Pivot bars right", 1, usize::MAX),
138        history: length("Confirmed impulses listed", 1, 100)
139    ; {});
140
141    fn name(&self) -> &str {
142        "Elliott Wave"
143    }
144
145    fn desc(&self) -> &str {
146        "Elliott Wave - Five-wave impulses in confirmed swing pivots, developing and confirmed"
147    }
148
149    fn calculate(&mut self, data: &[Bar]) {
150        self.update(data, 0);
151    }
152
153    fn update(&mut self, data: &[Bar], from: usize) {
154        let mut stepper = std::mem::take(&mut self.stepper);
155        let mut values = std::mem::take(&mut self.values);
156        stepper.run(data, from, &mut values, |state, bar| {
157            if ![bar.high, bar.low, bar.close].iter().all(|v| v.is_finite()) {
158                state.swings.skip();
159                return IndicatorValue::None;
160            }
161            let mut row = [f64::NAN; 4];
162            if state.swings.push(bar, self.left, self.right, 6) {
163                state.developing = construct(&state.swings.pivots);
164                if let Some(mut construction) = state.developing.take() {
165                    let sign = if construction.bullish() { 1.0 } else { -1.0 };
166                    let last = construction.anchors[construction.wave()].price;
167                    row[3] = sign * construction.wave() as f64;
168                    if construction.wave() == 5 {
169                        row[if construction.bullish() { 1 } else { 2 }] = last;
170                        construction.confirmed = Some(bar.time);
171                        state.confirmed.push_front(construction);
172                        state.confirmed.truncate(self.history);
173                        // The last pivot starts the next construction.
174                        while state.swings.pivots.len() > 1 {
175                            state.swings.pivots.pop_front();
176                        }
177                    } else {
178                        row[0] = last;
179                        state.developing = Some(construction);
180                    }
181                }
182            } else if let Some(construction) = &state.developing {
183                // Wave 4 would overlap wave 1.
184                let wave_1_end = construction.anchors[1].price;
185                if (construction.bullish() && bar.close < wave_1_end)
186                    || (!construction.bullish() && bar.close > wave_1_end)
187                {
188                    state.developing = None;
189                }
190            }
191            IndicatorValue::Multiple(row.to_vec())
192        });
193        let state = stepper.state();
194        let time = |time: DateTime<Utc>| time.format("%Y-%m-%d %H:%M").to_string();
195        let describe = |construction: &Construction| {
196            let anchors = construction
197                .anchors
198                .iter()
199                .enumerate()
200                .map(|(n, anchor)| format!("{n}:{:.2}", anchor.price))
201                .collect::<Vec<_>>()
202                .join(" ");
203            let direction = if construction.bullish() {
204                "Bullish"
205            } else {
206                "Bearish"
207            };
208            let confirmed = construction.confirmed.map_or("-".into(), time);
209            vec![
210                StudyCell::Text(format!("{direction}, wave {}", construction.wave())),
211                StudyCell::Text(confirmed),
212                StudyCell::Text(anchors),
213            ]
214        };
215        self.table.rows = state
216            .developing
217            .iter()
218            .chain(&state.confirmed)
219            .map(describe)
220            .collect();
221        if self.table.rows.is_empty() {
222            let text = |text: &str| StudyCell::Text(text.into());
223            self.table
224                .rows
225                .push(vec![text("No impulse"), text("-"), text("-")]);
226        }
227        self.stepper = stepper;
228        self.values = values;
229    }
230
231    fn is_incremental(&self) -> bool {
232        true
233    }
234
235    fn table(&self) -> Option<&StudyTable> {
236        Some(&self.table)
237    }
238
239    /// The pivot's own bar, `right` bars before its confirmation.
240    fn plot_offset(&self, line: usize) -> isize {
241        if line < 3 {
242            -(self.right.min(isize::MAX as usize) as isize)
243        } else {
244            0
245        }
246    }
247
248    fn plot_color(&self, line: usize, bar: usize) -> Option<Color32> {
249        (line == 0).then(|| {
250            let wave = self.values.value(3, bar)?;
251            Some(if wave > 0.0 {
252                self.colors[1]
253            } else {
254                self.colors[2]
255            })
256        })?
257    }
258
259    fn plot_text(&self, line: usize, bar: usize) -> Option<String> {
260        self.values.value(line, bar)?;
261        let wave = self.values.value(3, bar)?;
262        (line < 3).then(|| format!("{}", wave.abs()))
263    }
264
265    fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
266        crate::studies::PlotStyle {
267            color: self.colors[line.min(2)],
268            width: 1.0,
269            pattern: crate::model::LineStyle::Solid,
270            plot_type: match line {
271                0 => crate::studies::PlotType::Circles,
272                1 => crate::studies::PlotType::TriangleUp,
273                _ => crate::studies::PlotType::TriangleDown,
274            },
275            visible: line < 3,
276        }
277    }
278
279    fn values(&self) -> &IndicatorValues {
280        &self.values
281    }
282
283    fn colors(&self) -> Vec<Color32> {
284        self.colors.to_vec()
285    }
286
287    fn set_colors(&mut self, colors: Vec<Color32>) {
288        for (old, new) in self.colors.iter_mut().zip(colors) {
289            *old = new;
290        }
291    }
292
293    fn is_visible(&self) -> bool {
294        self.visible
295    }
296
297    fn set_visible(&mut self, visible: bool) {
298        self.visible = visible;
299    }
300
301    fn clone_box(&self) -> Box<dyn Indicator> {
302        Box::new(self.clone())
303    }
304
305    fn line_names(&self) -> Vec<String> {
306        [
307            "Developing Anchor",
308            "Confirmed Bullish Impulse",
309            "Confirmed Bearish Impulse",
310            "Wave",
311        ]
312        .map(String::from)
313        .to_vec()
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    const IMPULSE: [f64; 13] = [
322        105.0, 100.0, 105.0, 110.0, 107.0, 104.0, 115.0, 125.0, 120.0, 115.0, 122.0, 130.0, 128.0,
323    ];
324
325    fn run(closes: &[f64]) -> (Vec<Bar>, ElliottWave) {
326        let hlc: Vec<_> = closes.iter().map(|&c| (c, c, c)).collect();
327        let bars = super::super::hlc_bars(&hlc);
328        let mut study = ElliottWave::new(1, 1, 5);
329        study.calculate(&bars);
330        (bars, study)
331    }
332
333    fn column(study: &ElliottWave, line: usize, len: usize) -> Vec<Option<f64>> {
334        (0..len).map(|at| study.values().value(line, at)).collect()
335    }
336
337    /// Pivots by hand with one bar on each side: low 100 (bar 1), high 110 (3), low 104 (5), high 125
338    /// (7), low 115 (9) and high 130 (11), confirmed one bar later. Waves 1, 3 and 5 are 10, 21 and 15
339    /// long, wave 2 stays over 100 and wave 4 over 110. Wave 3 is developing on bar 8 and wave 4 on
340    /// bar 10, and the impulse is confirmed on bar 12. Negated prices give the bearish impulse.
341    #[test]
342    fn impulses_are_developing_then_confirmed_without_changes() {
343        let none = None;
344        for sign in [1.0, -1.0] {
345            let closes = IMPULSE.map(|c| sign * c);
346            let (bars, study) = run(&closes);
347            super::super::assert_replay_matches_batch(&study, &bars);
348            let anchor = |price: f64| Some(sign * price);
349            assert_eq!(
350                column(&study, 0, 13),
351                [
352                    none,
353                    none,
354                    none,
355                    none,
356                    none,
357                    none,
358                    none,
359                    none,
360                    anchor(125.0),
361                    none,
362                    anchor(115.0),
363                    none,
364                    none
365                ]
366            );
367            let confirmed = column(&study, if sign > 0.0 { 1 } else { 2 }, 13);
368            assert_eq!(confirmed[..12], [none; 12]);
369            assert_eq!(confirmed[12], anchor(130.0));
370            assert_eq!(
371                column(&study, if sign > 0.0 { 2 } else { 1 }, 13),
372                [none; 13]
373            );
374            assert_eq!(study.plot_text(0, 8).as_deref(), Some("3"));
375            assert_eq!(
376                study
377                    .plot_text(if sign > 0.0 { 1 } else { 2 }, 12)
378                    .as_deref(),
379                Some("5")
380            );
381            assert_eq!(study.plot_offset(0), -1);
382            let rows = &study.table().unwrap().rows;
383            assert_eq!(rows.len(), 1);
384            let StudyCell::Text(anchors) = &rows[0][2] else {
385                panic!()
386            };
387            let expected: Vec<_> = [100.0, 110.0, 104.0, 125.0, 115.0, 130.0]
388                .iter()
389                .enumerate()
390                .map(|(n, p)| format!("{n}:{:.2}", sign * p))
391                .collect();
392            // A bearish impulse starts at a high, so its anchors are the negated prices.
393            assert_eq!(anchors, &expected.join(" "));
394            assert_eq!(rows[0][1], StudyCell::Text("1970-01-13 00:00".into()));
395        }
396    }
397
398    /// Wave 3 shorter than waves 1 and 5 breaks the rule: with P3 = 112, P4 = 111 and P5 = 130
399    /// the waves are 10, 8 and 19, so nothing is confirmed. The pivots from the 104 low are a new
400    /// wave 3 on bar 12. A close under the end of wave 1 ends the developing wave on the bar.
401    #[test]
402    fn invalid_sequences_are_not_confirmed() {
403        let (_, study) = run(&[
404            105.0, 100.0, 105.0, 110.0, 107.0, 104.0, 108.0, 112.0, 111.5, 111.0, 120.0, 130.0,
405            128.0,
406        ]);
407        assert_eq!(column(&study, 1, 13), [None; 13]);
408        assert_eq!(study.values().value(0, 12), Some(130.0));
409        assert_eq!(study.values().value(3, 12), Some(3.0));
410        // The impulse up to bar 8, then a close at 108 under 110.
411        let (_, study) = run(&[
412            105.0, 100.0, 105.0, 110.0, 107.0, 104.0, 115.0, 125.0, 120.0, 108.0,
413        ]);
414        assert_eq!(study.values().value(0, 8), Some(125.0));
415        let rows = &study.table().unwrap().rows;
416        assert_eq!(rows.len(), 1);
417        assert_eq!(rows[0][0], StudyCell::Text("No impulse".into()));
418    }
419}