Skip to main content

egui_charts/studies/builtin/
keltner.rs

1//! Keltner Channels Indicator
2//!
3//! Keltner Channels are volatility-based bands set above and below a moving average.
4//!
5//! # Components
6//! - Middle Line: an exponential average of the source, close by default, seeded with the mean
7//!   of its first `length` values, or a simple average of the source.
8//! - Upper and Lower Bands: the middle line plus and minus the multiplier times the band
9//!   distance, which is Wilder's average true range over the ATR length (the default), the
10//!   bar's own true range, or Wilder's average of the bar's high minus low over the middle
11//!   line's length.
12//!
13//! A bar with a non-finite high, low, close or source price is skipped and changes nothing.
14//!
15//! # Interpretation
16//! - Price above upper band: Strong uptrend or overbought
17//! - Price below lower band: Strong downtrend or oversold
18//! - Channel squeeze (narrow bands): Low volatility, potential breakout
19//!
20//! # Example
21//! ```
22//! # 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();
23//! use egui_charts::studies::{KeltnerChannels, Indicator};
24//!
25//! let mut keltner = KeltnerChannels::new(20, 10, 2.0);
26//! keltner.calculate(&bars);
27//! ```
28
29use super::clamp_param;
30use super::price_oscillator::{Mean, OscillatorAverage};
31use super::smoothing::Smoothing;
32use crate::model::{Bar, PriceSource};
33use crate::studies::palette;
34use crate::studies::stepper::Stepper;
35use crate::studies::{ATR, Indicator, IndicatorValue, IndicatorValues};
36use egui::Color32;
37
38/// How far the bands of [`KeltnerChannels`] lie from the middle line, before the multiplier.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum KeltnerBand {
42    /// Wilder's average of the true range over the ATR length.
43    AverageTrueRange,
44    /// The bar's own true range; high minus low on the first bar.
45    TrueRange,
46    /// Wilder's average of the bar's high minus low over the middle line's length.
47    Range,
48}
49
50/// Keltner Channels indicator
51#[derive(Clone)]
52pub struct KeltnerChannels {
53    ema_period: usize,
54    atr_period: usize,
55    multiplier: f64,
56    source: PriceSource,
57    average: OscillatorAverage,
58    band: KeltnerBand,
59    values: IndicatorValues,
60    stepper: Stepper<State>,
61    upper_color: Color32,
62    middle_color: Color32,
63    lower_color: Color32,
64    fill_color: Color32,
65    visible: bool,
66}
67
68#[derive(Clone, Default)]
69struct State {
70    previous: Option<f64>,
71    middle: Mean,
72    band: Smoothing,
73}
74
75impl KeltnerChannels {
76    /// Create a new Keltner Channels indicator
77    ///
78    /// # Arguments
79    /// * `ema_period` - EMA period for middle line (typically 20)
80    /// * `atr_period` - ATR period (typically 10)
81    /// * `multiplier` - ATR multiplier (typically 2.0), clamped into 0 to `f64::MAX`, NaN to 0
82    pub fn new(ema_period: usize, atr_period: usize, multiplier: f64) -> Self {
83        Self {
84            ema_period: ema_period.max(1),
85            atr_period: atr_period.max(1),
86            multiplier: clamp_param(multiplier, 0.0, f64::MAX),
87            source: PriceSource::Close,
88            average: OscillatorAverage::Exponential,
89            band: KeltnerBand::AverageTrueRange,
90            values: IndicatorValues::new(),
91            stepper: Stepper::default(),
92            upper_color: palette::BLUE,
93            middle_color: palette::BLUE,
94            lower_color: palette::BLUE,
95            fill_color: palette::LIGHT_BLUE.gamma_multiply_u8(13),
96            visible: true,
97        }
98    }
99
100    /// Create with default params (20, 10, 2.0)
101    pub fn default_params() -> Self {
102        Self::new(20, 10, 2.0)
103    }
104
105    /// Set colors for upper, middle, and lower bands
106    pub fn with_colors(mut self, upper: Color32, middle: Color32, lower: Color32) -> Self {
107        self.upper_color = upper;
108        self.middle_color = middle;
109        self.lower_color = lower;
110        self.fill_color = Color32::from_rgba_unmultiplied(upper.r(), upper.g(), upper.b(), 16);
111        self
112    }
113
114    /// Average `source` for the middle line instead of the close. The bands keep using the bars.
115    pub fn with_source(mut self, source: PriceSource) -> Self {
116        self.source = source;
117        self
118    }
119
120    /// Use an exponential (the default) or simple average for the middle line.
121    pub fn with_average(mut self, average: OscillatorAverage) -> Self {
122        self.average = average;
123        self
124    }
125
126    /// Measure the band distance with `band` instead of the average true range.
127    pub fn with_band(mut self, band: KeltnerBand) -> Self {
128        self.band = band;
129        self
130    }
131}
132
133/// Construct with the conventional default parameters.
134impl Default for KeltnerChannels {
135    fn default() -> Self {
136        Self::new(20, 10, 2.0)
137    }
138}
139
140impl Indicator for KeltnerChannels {
141    crate::studies::inputs::study_inputs!(self;
142        ema_period: length("Length", 1, usize::MAX),
143        multiplier: number("Multiplier", 0.0, f64::MAX),
144        source: choice("Source", PriceSource::all()),
145        average: choice("Average", [OscillatorAverage::Exponential, OscillatorAverage::Simple]),
146        band: choice("Band style", [KeltnerBand::AverageTrueRange, KeltnerBand::TrueRange, KeltnerBand::Range]),
147        atr_period: length("ATR length", 1, usize::MAX)
148    ; {});
149
150    fn fills(&self) -> Vec<crate::studies::StudyFill> {
151        vec![crate::studies::StudyFill {
152            first: crate::studies::FillBoundary::Plot(1),
153            second: crate::studies::FillBoundary::Plot(2),
154            color: self.fill_color,
155            inverted_color: None,
156        }]
157    }
158
159    fn name(&self) -> &str {
160        "Keltner"
161    }
162
163    fn desc(&self) -> &str {
164        "Keltner Channels - Volatility-based envelope"
165    }
166
167    fn calculate(&mut self, data: &[Bar]) {
168        self.update(data, 0);
169    }
170
171    fn update(&mut self, data: &[Bar], from: usize) {
172        self.stepper
173            .run(data, from, &mut self.values, |state, bar| {
174                let price = self.source.compute(bar.open, bar.high, bar.low, bar.close);
175                if ![bar.high, bar.low, bar.close, price]
176                    .iter()
177                    .all(|v| v.is_finite())
178                {
179                    return IndicatorValue::None;
180                }
181                let tr = state
182                    .previous
183                    .map_or(bar.high - bar.low, |close| ATR::true_range(bar, close));
184                state.previous = Some(bar.close);
185                let middle = state.middle.next(self.average, price, self.ema_period);
186                let band = match self.band {
187                    KeltnerBand::AverageTrueRange => {
188                        state
189                            .band
190                            .next(tr, self.atr_period, 1.0 / self.atr_period as f64)
191                    }
192                    KeltnerBand::TrueRange => tr,
193                    KeltnerBand::Range => state.band.next(
194                        bar.high - bar.low,
195                        self.ema_period,
196                        1.0 / self.ema_period as f64,
197                    ),
198                };
199                // Only NaN is missing here: infinite bands from a huge multiplier keep the middle line.
200                if middle.is_nan() || band.is_nan() {
201                    return IndicatorValue::None;
202                }
203                let width = self.multiplier * band;
204                IndicatorValue::Multiple(vec![middle, middle + width, middle - width])
205            });
206    }
207
208    fn is_incremental(&self) -> bool {
209        true
210    }
211
212    fn values(&self) -> &IndicatorValues {
213        &self.values
214    }
215
216    fn colors(&self) -> Vec<Color32> {
217        vec![
218            self.middle_color,
219            self.upper_color,
220            self.lower_color,
221            self.fill_color,
222        ]
223    }
224
225    fn set_colors(&mut self, colors: Vec<Color32>) {
226        if !colors.is_empty() {
227            self.middle_color = colors[0];
228        }
229        if colors.len() > 1 {
230            self.upper_color = colors[1];
231        }
232        if colors.len() > 2 {
233            self.lower_color = colors[2];
234        }
235        if let Some(&color) = colors.get(3) {
236            self.fill_color = color;
237        } else if colors.len() > 1 {
238            let color = self.upper_color;
239            self.fill_color = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 16);
240        }
241    }
242
243    fn is_overlay(&self) -> bool {
244        true // Keltner is drawn on main chart
245    }
246
247    fn line_count(&self) -> usize {
248        3 // Middle, Upper, Lower
249    }
250
251    fn is_visible(&self) -> bool {
252        self.visible
253    }
254
255    fn set_visible(&mut self, visible: bool) {
256        self.visible = visible;
257    }
258
259    fn clone_box(&self) -> Box<dyn Indicator> {
260        Box::new(self.clone())
261    }
262
263    fn line_names(&self) -> Vec<String> {
264        let average = match self.average {
265            OscillatorAverage::Exponential => "EMA",
266            OscillatorAverage::Simple => "SMA",
267        };
268        let band = match self.band {
269            KeltnerBand::AverageTrueRange => "ATR",
270            KeltnerBand::TrueRange => "TR",
271            KeltnerBand::Range => "Range",
272        };
273        vec![
274            format!("{average}({})", self.ema_period),
275            format!("Upper({}x {band})", self.multiplier),
276            format!("Lower({}x {band})", self.multiplier),
277        ]
278    }
279}