Skip to main content

egui_charts/studies/builtin/
hma.rs

1//! Hull Moving Avg (HMA) Indicator
2//!
3//! HMA is a fast, smooth moving avg developed by Alan Hull that
4//! significantly reduces lag while maintaining smoothness.
5//!
6//! # Formula
7//! HMA = WMA(2 * WMA(price, n/2) - WMA(price, n), sqrt(n))
8//!
9//! # Example
10//! ```
11//! # 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();
12//! use egui_charts::studies::{HMA, Indicator};
13//!
14//! let mut hma = HMA::new(9);
15//! hma.calculate(&bars);
16//! ```
17
18use crate::model::Bar;
19use crate::studies::helpers;
20use crate::studies::palette;
21use crate::studies::stepper::update_window;
22use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
23use egui::Color32;
24
25/// Hull Moving Avg indicator
26#[derive(Clone)]
27pub struct HMA {
28    period: usize,
29    values: IndicatorValues,
30    color: Color32,
31    visible: bool,
32}
33
34impl HMA {
35    /// Create a new HMA indicator
36    ///
37    /// # Arguments
38    /// * `period` - Number of periods for calculation (typically 9, 14, or 21)
39    pub fn new(period: usize) -> Self {
40        Self {
41            period: period.max(2),
42            values: IndicatorValues::new(),
43            color: palette::LIGHT_BLUE,
44            visible: true,
45        }
46    }
47
48    /// Set the line color
49    pub fn with_color(mut self, color: Color32) -> Self {
50        self.color = color;
51        self
52    }
53}
54
55/// Construct with the conventional default parameters.
56impl Default for HMA {
57    fn default() -> Self {
58        Self::new(9)
59    }
60}
61
62impl HMA {
63    fn compute(&self, data: &[Bar]) -> IndicatorValues {
64        let mut values = IndicatorValues::new();
65
66        if data.is_empty() {
67            return values;
68        }
69
70        let half_period = (self.period / 2).max(1);
71        let sqrt_period = (self.period as f64).sqrt() as usize;
72        let sqrt_period = sqrt_period.max(1);
73
74        // Extract close prices
75        let closes: Vec<f64> = data.iter().map(|b| b.close).collect();
76
77        // Step 1: Calculate WMA(close, period/2)
78        let wma_half = helpers::wma(&closes, half_period);
79
80        // Step 2: Calculate WMA(close, period)
81        let wma_full = helpers::wma(&closes, self.period);
82
83        // Step 3: Calculate 2 * WMA(half) - WMA(full)
84        let mut raw_hma = vec![f64::NAN; data.len()];
85        for i in 0..data.len() {
86            if !wma_half[i].is_nan() && !wma_full[i].is_nan() {
87                raw_hma[i] = 2.0 * wma_half[i] - wma_full[i];
88            }
89        }
90
91        // Step 4: Calculate WMA(raw_hma, sqrt(period))
92        let hma = helpers::wma(&raw_hma, sqrt_period);
93
94        // Build output
95        let min_period = self.period.saturating_add(sqrt_period) - 1;
96        for i in 0..data.len() {
97            if i < min_period - 1 || hma[i].is_nan() {
98                values.push(IndicatorValue::None);
99            } else {
100                values.push(IndicatorValue::Single(hma[i]));
101            }
102        }
103        values
104    }
105}
106
107impl Indicator for HMA {
108    fn source(&self) -> Option<crate::studies::StudySource> {
109        Some(crate::studies::StudySource::Price(
110            crate::model::PriceSource::Close,
111        ))
112    }
113
114    crate::studies::inputs::study_inputs!(self;
115        period: length("Period", 2, usize::MAX)
116    ; {});
117
118    fn name(&self) -> &str {
119        "HMA"
120    }
121
122    fn desc(&self) -> &str {
123        "Hull Moving Avg - Fast and smooth moving avg"
124    }
125
126    fn calculate(&mut self, data: &[Bar]) {
127        self.update(data, 0);
128    }
129
130    fn update(&mut self, data: &[Bar], from: usize) {
131        let mut values = std::mem::take(&mut self.values);
132        update_window(
133            &mut values,
134            data,
135            from,
136            |_data, from| {
137                from.saturating_sub(
138                    self.period
139                        .saturating_add(((self.period as f64).sqrt() as usize).max(1) - 1)
140                        - 1,
141                )
142            },
143            |data| self.compute(data),
144        );
145        self.values = values;
146    }
147
148    fn is_incremental(&self) -> bool {
149        true
150    }
151
152    fn values(&self) -> &IndicatorValues {
153        &self.values
154    }
155
156    fn colors(&self) -> Vec<Color32> {
157        vec![self.color]
158    }
159
160    fn set_colors(&mut self, colors: Vec<Color32>) {
161        if !colors.is_empty() {
162            self.color = colors[0];
163        }
164    }
165
166    fn is_overlay(&self) -> bool {
167        true
168    }
169
170    fn line_count(&self) -> usize {
171        1
172    }
173
174    fn is_visible(&self) -> bool {
175        self.visible
176    }
177
178    fn set_visible(&mut self, visible: bool) {
179        self.visible = visible;
180    }
181
182    fn clone_box(&self) -> Box<dyn Indicator> {
183        Box::new(self.clone())
184    }
185
186    fn line_names(&self) -> Vec<String> {
187        vec![format!("HMA({})", self.period)]
188    }
189}