Skip to main content

egui_charts/studies/builtin/
volatility_stop.rs

1//! Volatility Stop
2//!
3//! A stop-and-reverse level after Welles Wilder's volatility system (New Concepts in Technical
4//! Trading Systems, 1978): the stop trails the extreme source value of the current trend by a
5//! multiple of the average true range, only ever moving in the trend's direction, and the trend
6//! reverses when the source crosses it.
7//!
8//! # Formula
9//! With `band = k * ATR(n)` (Wilder's average true range, length 20 and multiplier 2 by
10//! default) and the source the close:
11//! - `max` and `min` are the highest and lowest source values since the last reversal
12//! - in an uptrend `stop = max(previous stop, max - band)`,
13//!   in a downtrend `stop = min(previous stop, min + band)`
14//! - the trend is up while `source >= stop`; on a reversal `max` and `min` restart at the
15//!   source and the stop restarts at `source - band` (up) or `source + band` (down)
16//!
17//! The first stop is the first source value, in an uptrend. Until the ATR has `n` true ranges
18//! the band is the bar's true range, without the multiplier. The stop is drawn as crosses,
19//! green in an uptrend and red in a downtrend.
20//!
21//! # Example
22//! ```
23//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
24//! use egui_charts::studies::{Indicator, VolatilityStop};
25//!
26//! let mut stop = VolatilityStop::new(20, 2.0);
27//! stop.calculate(&bars);
28//! ```
29
30use super::clamp_param;
31use crate::model::Bar;
32use crate::studies::stepper::Stepper;
33use crate::studies::{ATR, Indicator, IndicatorValue, IndicatorValues};
34use egui::Color32;
35
36/// Volatility Stop: a trailing ATR stop that reverses with the trend.
37#[derive(Clone)]
38pub struct VolatilityStop {
39    length: usize,
40    multiplier: f64,
41    values: IndicatorValues,
42    stepper: Stepper<State>,
43    /// Whether each source bar is in an uptrend, for its colour.
44    uptrend: Vec<bool>,
45    colors: [Color32; 2],
46    visible: bool,
47}
48
49#[derive(Clone, Default)]
50struct State {
51    usable: usize,
52    previous: f64,
53    atr: f64,
54    max: f64,
55    min: f64,
56    stop: f64,
57    downtrend: bool,
58}
59
60impl VolatilityStop {
61    /// Create the stop with the ATR length (typically 20) and the ATR multiplier (typically 2,
62    /// clamped into 0 to `f64::MAX`, NaN to 0).
63    pub fn new(length: usize, multiplier: f64) -> Self {
64        Self {
65            length: length.max(1),
66            multiplier: clamp_param(multiplier, 0.0, f64::MAX),
67            values: IndicatorValues::new(),
68            stepper: Stepper::default(),
69            uptrend: Vec::new(),
70            colors: [
71                crate::studies::palette::DEEP_TEAL,
72                crate::studies::palette::BRIGHT_RED,
73            ],
74            visible: true,
75        }
76    }
77}
78
79/// Construct with the conventional default parameters.
80impl Default for VolatilityStop {
81    fn default() -> Self {
82        Self::new(20, 2.0)
83    }
84}
85
86impl Indicator for VolatilityStop {
87    crate::studies::inputs::study_inputs!(self;
88        length: length("Length", 1, usize::MAX),
89        multiplier: number("Multiplier", 0.0, f64::MAX)
90    ; {});
91
92    fn plot_style(&self, _line: usize) -> crate::studies::PlotStyle {
93        crate::studies::PlotStyle {
94            color: self.colors[0],
95            width: 1.0,
96            pattern: crate::model::LineStyle::Solid,
97            plot_type: crate::studies::PlotType::Crosses,
98            visible: true,
99        }
100    }
101
102    fn plot_color(&self, _line: usize, bar: usize) -> Option<Color32> {
103        self.values.value(0, bar)?;
104        let up = *self.uptrend.get(bar)?;
105        Some(self.colors[usize::from(!up)])
106    }
107
108    fn name(&self) -> &str {
109        "Volatility Stop"
110    }
111
112    fn desc(&self) -> &str {
113        "Volatility Stop - ATR trailing stop and reverse"
114    }
115
116    fn calculate(&mut self, data: &[Bar]) {
117        self.update(data, 0);
118    }
119
120    fn update(&mut self, data: &[Bar], from: usize) {
121        let (length, multiplier) = (self.length, self.multiplier);
122        let mut trends = Vec::new();
123        self.stepper
124            .run(data, from, &mut self.values, |state, bar| {
125                if ![bar.high, bar.low, bar.close].iter().all(|p| p.is_finite()) {
126                    trends.push(false);
127                    return IndicatorValue::None;
128                }
129                let source = bar.close;
130                let first = state.usable == 0;
131                let tr = if first {
132                    bar.high - bar.low
133                } else {
134                    ATR::true_range(bar, state.previous)
135                };
136                state.previous = bar.close;
137                state.usable += 1;
138                if state.usable <= length {
139                    state.atr += tr;
140                    if state.usable == length {
141                        state.atr /= length as f64;
142                    }
143                } else {
144                    state.atr += (tr - state.atr) / length as f64;
145                }
146                let band = if state.usable >= length {
147                    multiplier * state.atr
148                } else {
149                    tr
150                };
151                if first {
152                    (state.max, state.min, state.stop) = (source, source, source);
153                } else {
154                    state.max = state.max.max(source);
155                    state.min = state.min.min(source);
156                    state.stop = if state.downtrend {
157                        state.stop.min(state.min + band)
158                    } else {
159                        state.stop.max(state.max - band)
160                    };
161                }
162                let downtrend = source < state.stop;
163                if downtrend != state.downtrend {
164                    state.downtrend = downtrend;
165                    (state.max, state.min) = (source, source);
166                    state.stop = if downtrend {
167                        source + band
168                    } else {
169                        source - band
170                    };
171                }
172                trends.push(!state.downtrend);
173                IndicatorValue::Single(state.stop)
174            });
175        self.uptrend.truncate(data.len() - trends.len());
176        self.uptrend.extend(trends);
177    }
178
179    fn is_incremental(&self) -> bool {
180        true
181    }
182
183    fn values(&self) -> &IndicatorValues {
184        &self.values
185    }
186
187    fn colors(&self) -> Vec<Color32> {
188        self.colors.to_vec()
189    }
190
191    fn set_colors(&mut self, colors: Vec<Color32>) {
192        for (old, new) in self.colors.iter_mut().zip(colors) {
193            *old = new;
194        }
195    }
196
197    fn is_visible(&self) -> bool {
198        self.visible
199    }
200
201    fn set_visible(&mut self, visible: bool) {
202        self.visible = visible;
203    }
204
205    fn clone_box(&self) -> Box<dyn Indicator> {
206        Box::new(self.clone())
207    }
208
209    fn line_names(&self) -> Vec<String> {
210        vec!["Volatility Stop".into()]
211    }
212}