Skip to main content

egui_charts/studies/builtin/
williams_fractals.rs

1//! Williams Fractals.
2//!
3//! Bill Williams ("Trading Chaos", 1995) marks a swing high where a bar's high stands above
4//! the highs around it, and a swing low where a bar's low stands below the lows around it. The
5//! conventional fractal uses five bars: two on each side.
6//!
7//! # Rule
8//!
9//! ```text
10//! Up fractal at bar p:   high[p] > high[p + 1 ..= p + n]
11//!                        and, for some k in 0..=4,
12//!                        high[p] >= high[p - k ..= p - 1] and high[p] > high[p - k - n ..= p - k - 1]
13//! Down fractal at bar p: the same with lows, below instead of above
14//! ```
15//!
16//! The bars after the fractal must be strictly lower. Before it, up to four bars just before
17//! the fractal bar may equal its high, as long as the `n` bars before them are strictly lower,
18//! so a flat top of up to five bars is marked once, on its last bar.
19//!
20//! An up fractal is the high of its bar, drawn as an upward triangle; a down fractal is the
21//! low, drawn as a downward triangle. A fractal is known `n` bars after its bar, so the last
22//! `n` bars have no value yet.
23//!
24//! # Example
25//!
26//! ```rust
27//! use chrono::{TimeZone, Utc};
28//! use egui_charts::model::Bar;
29//! use egui_charts::studies::{Indicator, WilliamsFractals};
30//!
31//! let bars: Vec<Bar> = [1.0, 2.0, 5.0, 3.0, 2.0]
32//!     .into_iter()
33//!     .enumerate()
34//!     .map(|(i, p)| Bar::new(Utc.timestamp_opt(i as i64 * 60, 0).unwrap(), p, p, p - 1.0, p, 1.0))
35//!     .collect();
36//! let mut fractals = WilliamsFractals::default();
37//! fractals.calculate(&bars);
38//! assert_eq!(fractals.values().value(0, 2), Some(5.0));
39//! assert_eq!(fractals.values().value(1, 2), None);
40//! ```
41
42use super::over_finite_bars;
43use crate::model::Bar;
44use crate::studies::stepper::{rolling_max, update_window, usable_behind};
45use crate::studies::{Indicator, IndicatorValues};
46use egui::Color32;
47
48/// Williams Fractals: up fractals at swing highs and down fractals at swing lows.
49#[derive(Clone)]
50pub struct WilliamsFractals {
51    periods: usize,
52    values: IndicatorValues,
53    up_color: Color32,
54    down_color: Color32,
55    visible: bool,
56}
57
58impl WilliamsFractals {
59    /// Fractals with `periods` bars on each side, raised to at least 2 (default 2).
60    pub fn new(periods: usize) -> Self {
61        Self {
62            periods: periods.max(2),
63            values: IndicatorValues::new(),
64            up_color: crate::studies::palette::DEEP_TEAL,
65            down_color: crate::studies::palette::BRIGHT_RED,
66            visible: true,
67        }
68    }
69
70    /// The values over `data`, every bar of which has a finite high and low.
71    fn compute(&self, data: &[Bar]) -> IndicatorValues {
72        let mut values = IndicatorValues::missing(data.len());
73        if !data.is_empty() {
74            // Both columns exist, so a bar with one fractal still has a value per line.
75            values.set_value(1, 0, f64::NAN);
76        }
77        for (line, sign) in [(0, 1.0), (1, -1.0)] {
78            // Lows negated, so a down fractal is an up fractal of the negated lows.
79            let x: Vec<f64> = data
80                .iter()
81                .map(|b| sign * if line == 0 { b.high } else { b.low })
82                .collect();
83            // Highest of the `periods` values ending at each bar.
84            let highest = rolling_max(x.iter().copied(), self.periods);
85            for p in 0..x.len() {
86                let after = p.checked_add(self.periods).and_then(|end| highest.get(end));
87                if !after.is_some_and(|&after| after < x[p]) {
88                    continue;
89                }
90                let before = (0..=4).any(|k: usize| {
91                    p.checked_sub(k + self.periods).is_some()
92                        && x[p - k..p].iter().all(|&v| v <= x[p])
93                        && highest[p - k - 1] < x[p]
94                });
95                if before {
96                    values.set_value(line, p, sign * x[p]);
97                }
98            }
99        }
100        values
101    }
102}
103
104/// Construct with the conventional default parameters.
105impl Default for WilliamsFractals {
106    fn default() -> Self {
107        Self::new(2)
108    }
109}
110
111impl Indicator for WilliamsFractals {
112    crate::studies::inputs::study_inputs!(self;
113        periods: length("Periods", 2, usize::MAX)
114    ; {});
115
116    fn name(&self) -> &str {
117        "Williams Fractals"
118    }
119
120    fn desc(&self) -> &str {
121        "Williams Fractals - Swing highs and lows confirmed by the bars on each side"
122    }
123
124    fn calculate(&mut self, data: &[Bar]) {
125        self.update(data, 0);
126    }
127
128    fn update(&mut self, data: &[Bar], from: usize) {
129        // Confirmation of a right-hand window can change earlier pivot bars.
130        let from = usable_behind(self.periods, |b| b.high.is_finite() && b.low.is_finite())(
131            data,
132            from.min(self.values.len()).min(data.len()),
133        );
134        let mut values = std::mem::take(&mut self.values);
135        update_window(
136            &mut values,
137            data,
138            from,
139            usable_behind(self.periods.saturating_add(4), |b| {
140                b.high.is_finite() && b.low.is_finite()
141            }),
142            |data| {
143                over_finite_bars(
144                    data,
145                    |b| b.high.is_finite() && b.low.is_finite(),
146                    |data| self.compute(data),
147                )
148            },
149        );
150        self.values = values;
151    }
152
153    fn is_incremental(&self) -> bool {
154        true
155    }
156
157    fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
158        crate::studies::PlotStyle {
159            color: if line == 0 {
160                self.up_color
161            } else {
162                self.down_color
163            },
164            width: 2.0,
165            pattern: crate::model::LineStyle::Solid,
166            plot_type: if line == 0 {
167                crate::studies::PlotType::TriangleUp
168            } else {
169                crate::studies::PlotType::TriangleDown
170            },
171            visible: true,
172        }
173    }
174
175    fn values(&self) -> &IndicatorValues {
176        &self.values
177    }
178
179    fn colors(&self) -> Vec<Color32> {
180        vec![self.up_color, self.down_color]
181    }
182
183    fn set_colors(&mut self, colors: Vec<Color32>) {
184        if let Some(&color) = colors.first() {
185            self.up_color = color;
186        }
187        if let Some(&color) = colors.get(1) {
188            self.down_color = color;
189        }
190    }
191
192    fn is_visible(&self) -> bool {
193        self.visible
194    }
195
196    fn set_visible(&mut self, visible: bool) {
197        self.visible = visible;
198    }
199
200    fn clone_box(&self) -> Box<dyn Indicator> {
201        Box::new(self.clone())
202    }
203
204    fn line_names(&self) -> Vec<String> {
205        vec!["Up Fractal".to_string(), "Down Fractal".to_string()]
206    }
207}