Skip to main content

egui_charts/studies/builtin/
supertrend.rs

1//! SuperTrend Indicator
2//!
3//! SuperTrend is a trend-following indicator that uses ATR to calculate
4//! dynamic support and resistance levels. It's useful for identifying
5//! trend direction and potential entry/exit points.
6//!
7//! # Components
8//! - Up Trend: the SuperTrend line while the trend is bullish (below price)
9//! - Down Trend: the SuperTrend line while the trend is bearish (above price)
10//!
11//! Each bar has a value on exactly one of the two lines; the other is NaN.
12//!
13//! # Interpretation
14//! - SuperTrend below price (green): Bullish trend
15//! - SuperTrend above price (red): Bearish trend
16//! - Price crossing SuperTrend signals trend change
17//!
18//! # Example
19//! ```
20//! # 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();
21//! use egui_charts::studies::{SuperTrend, Indicator};
22//!
23//! let mut st = SuperTrend::new(10, 3.0);
24//! st.calculate(&bars);
25//! ```
26
27use super::clamp_param;
28use crate::model::Bar;
29use crate::studies::palette;
30use crate::studies::stepper::Stepper;
31use crate::studies::{ATR, Indicator, IndicatorValue, IndicatorValues};
32use egui::Color32;
33
34/// SuperTrend indicator
35#[derive(Clone)]
36pub struct SuperTrend {
37    atr_period: usize,
38    multiplier: f64,
39    values: IndicatorValues,
40    stepper: Stepper<State>,
41    bullish_color: Color32,
42    bearish_color: Color32,
43    visible: bool,
44}
45
46#[derive(Clone)]
47struct State {
48    usable: usize,
49    previous: f64,
50    atr: f64,
51    upper: f64,
52    lower: f64,
53    trend: f64,
54}
55
56impl Default for State {
57    fn default() -> Self {
58        Self {
59            usable: 0,
60            previous: 0.0,
61            atr: -0.0,
62            upper: 0.0,
63            lower: 0.0,
64            trend: 0.0,
65        }
66    }
67}
68
69impl SuperTrend {
70    /// Create a new SuperTrend indicator
71    ///
72    /// # Arguments
73    /// * `atr_period` - ATR period (typically 10)
74    /// * `multiplier` - ATR multiplier (typically 3.0), clamped into 0 to `f64::MAX`, NaN to 0
75    pub fn new(atr_period: usize, multiplier: f64) -> Self {
76        Self {
77            atr_period: atr_period.max(1),
78            multiplier: clamp_param(multiplier, 0.0, f64::MAX),
79            values: IndicatorValues::new(),
80            stepper: Stepper::default(),
81            bullish_color: palette::GREEN, // Green
82            bearish_color: palette::RED,   // Red
83            visible: true,
84        }
85    }
86
87    /// Create with default params (10, 3.0)
88    pub fn default_params() -> Self {
89        Self::new(10, 3.0)
90    }
91
92    /// Set colors for bullish and bearish trends
93    pub fn with_colors(mut self, bullish: Color32, bearish: Color32) -> Self {
94        self.bullish_color = bullish;
95        self.bearish_color = bearish;
96        self
97    }
98}
99
100/// Construct with the conventional default parameters.
101impl Default for SuperTrend {
102    fn default() -> Self {
103        Self::new(10, 3.0)
104    }
105}
106
107impl Indicator for SuperTrend {
108    crate::studies::inputs::study_inputs!(self;
109        atr_period: length("ATR period", 1, usize::MAX),
110        multiplier: number("Multiplier", 0.0, f64::MAX)
111    ; {});
112
113    fn name(&self) -> &str {
114        "SuperTrend"
115    }
116
117    fn desc(&self) -> &str {
118        "SuperTrend - Trend following indicator"
119    }
120
121    fn calculate(&mut self, data: &[Bar]) {
122        self.update(data, 0);
123    }
124
125    fn update(&mut self, data: &[Bar], from: usize) {
126        let period = self.atr_period;
127        let threshold = period.saturating_add(1);
128        // The batch warm-up hides even the seed bar until one more usable bar arrives.
129        let from = if self.stepper.state().usable < threshold {
130            0
131        } else {
132            from
133        };
134        let atr_mult = 1.0 / period as f64;
135        let multiplier = self.multiplier;
136        self.stepper
137            .run(data, from, &mut self.values, |state, bar| {
138                if !bar.high.is_finite() || !bar.low.is_finite() || !bar.close.is_finite() {
139                    return IndicatorValue::None;
140                }
141                let tr = if state.usable == 0 {
142                    bar.high - bar.low
143                } else {
144                    ATR::true_range(bar, state.previous)
145                };
146                let previous = state.previous;
147                state.previous = bar.close;
148                state.usable += 1;
149                if state.usable <= period {
150                    state.atr += tr;
151                    if state.usable < period {
152                        return IndicatorValue::None;
153                    }
154                    state.atr /= period as f64;
155                } else {
156                    state.atr += atr_mult * (tr - state.atr);
157                }
158                let hl2 = (bar.high + bar.low) / 2.0;
159                let upper = hl2 + multiplier * state.atr;
160                let lower = hl2 - multiplier * state.atr;
161                let bullish = if state.usable == period {
162                    state.upper = upper;
163                    state.lower = lower;
164                    true
165                } else {
166                    let was_upper = state.trend == state.upper;
167                    if upper < state.upper || previous > state.upper {
168                        state.upper = upper;
169                    }
170                    if lower > state.lower || previous < state.lower {
171                        state.lower = lower;
172                    }
173                    if was_upper {
174                        bar.close > state.upper
175                    } else {
176                        bar.close.partial_cmp(&state.lower) != Some(std::cmp::Ordering::Less)
177                    }
178                };
179                state.trend = if bullish { state.lower } else { state.upper };
180                if bullish {
181                    IndicatorValue::Multiple(vec![state.trend, f64::NAN])
182                } else {
183                    IndicatorValue::Multiple(vec![f64::NAN, state.trend])
184                }
185            });
186        if self.stepper.state().usable < threshold {
187            self.values.fill(IndicatorValue::None);
188        }
189    }
190
191    fn is_incremental(&self) -> bool {
192        true
193    }
194
195    fn values(&self) -> &IndicatorValues {
196        &self.values
197    }
198
199    fn colors(&self) -> Vec<Color32> {
200        vec![self.bullish_color, self.bearish_color]
201    }
202
203    fn set_colors(&mut self, colors: Vec<Color32>) {
204        if !colors.is_empty() {
205            self.bullish_color = colors[0];
206        }
207        if colors.len() > 1 {
208            self.bearish_color = colors[1];
209        }
210    }
211
212    fn is_overlay(&self) -> bool {
213        true // SuperTrend is drawn on main chart
214    }
215
216    fn is_visible(&self) -> bool {
217        self.visible
218    }
219
220    fn set_visible(&mut self, visible: bool) {
221        self.visible = visible;
222    }
223
224    fn clone_box(&self) -> Box<dyn Indicator> {
225        Box::new(self.clone())
226    }
227
228    fn line_names(&self) -> Vec<String> {
229        vec!["Up Trend".to_string(), "Down Trend".to_string()]
230    }
231}