egui_charts/studies/builtin/
vw_macd.rs1use crate::model::Bar;
33use crate::studies::palette;
34use crate::studies::stepper::Stepper;
35use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
36use egui::Color32;
37
38#[derive(Clone)]
44pub struct VolumeWeightedMACD {
45 fast_period: usize,
46 slow_period: usize,
47 signal_period: usize,
48 values: IndicatorValues,
49 stepper: Stepper<(std::collections::VecDeque<[f64; 2]>, Option<f64>)>,
50 colors: Vec<Color32>,
51 visible: bool,
52}
53
54impl Default for VolumeWeightedMACD {
55 fn default() -> Self {
56 Self::new(12, 26, 9)
57 }
58}
59
60impl VolumeWeightedMACD {
61 pub fn new(fast_period: usize, slow_period: usize, signal_period: usize) -> Self {
63 Self {
64 fast_period: fast_period.max(1),
65 slow_period: slow_period.max(1),
66 signal_period: signal_period.max(1),
67 values: IndicatorValues::new(),
68 stepper: Stepper::default(),
69 colors: vec![
70 palette::BLUE, palette::ORANGE, palette::GREEN, ],
74 visible: true,
75 }
76 }
77}
78
79impl Indicator for VolumeWeightedMACD {
80 fn source(&self) -> Option<crate::studies::StudySource> {
81 Some(crate::studies::StudySource::Price(
82 crate::model::PriceSource::Close,
83 ))
84 }
85
86 crate::studies::inputs::study_inputs!(self;
87 fast_period: length("Fast period", 1, usize::MAX),
88 slow_period: length("Slow period", 1, usize::MAX),
89 signal_period: length("Signal period", 1, usize::MAX)
90 ; {});
91
92 fn value_format(&self) -> crate::scales::StudyFormat {
93 crate::scales::StudyFormat::Price
94 }
95
96 fn name(&self) -> &str {
97 "VW-MACD"
98 }
99
100 fn desc(&self) -> &str {
101 "Volume Weighted MACD - MACD using volume-weighted prices"
102 }
103
104 fn calculate(&mut self, data: &[Bar]) {
105 self.update(data, 0);
106 }
107
108 fn update(&mut self, data: &[Bar], from: usize) {
109 self.stepper
110 .run(data, from, &mut self.values, |state, bar| {
111 if !bar.close.is_finite() || !bar.volume.is_finite() {
112 return IndicatorValue::None;
113 }
114 state.0.push_back([bar.close, bar.volume]);
115 let length = self.fast_period.max(self.slow_period);
116 if state.0.len() > length {
117 state.0.pop_front();
118 }
119 if state.0.len() < length {
120 return IndicatorValue::None;
121 }
122 let average = |period| {
123 let start = state.0.len() - period;
124 let price_volume: f64 = state.0.iter().skip(start).map(|v| v[0] * v[1]).sum();
125 let volume: f64 = state.0.iter().skip(start).map(|v| v[1]).sum();
126 if volume > 0.0 {
127 price_volume / volume
128 } else {
129 state.0.iter().skip(start).map(|v| v[0]).sum::<f64>() / period as f64
130 }
131 };
132 let (fast, slow) = (average(self.fast_period), average(self.slow_period));
133 if !fast.is_finite() || !slow.is_finite() {
134 return IndicatorValue::None;
135 }
136 let line = fast - slow;
137 let signal = state.1.map_or(line, |previous| {
138 (line - previous) * (2.0 / (self.signal_period as f64 + 1.0)) + previous
139 });
140 state.1 = Some(signal);
141 IndicatorValue::Multiple(vec![line, signal, line - signal])
142 });
143 }
144
145 fn is_incremental(&self) -> bool {
146 true
147 }
148
149 fn values(&self) -> &IndicatorValues {
150 &self.values
151 }
152
153 fn colors(&self) -> Vec<Color32> {
154 self.colors.clone()
155 }
156
157 fn set_colors(&mut self, colors: Vec<Color32>) {
158 if colors.len() >= 3 {
159 self.colors = colors;
160 } else if !colors.is_empty() {
161 self.colors = vec![colors[0], colors[0], colors[0]];
162 }
163 }
164
165 fn is_overlay(&self) -> bool {
166 false }
168
169 fn line_count(&self) -> usize {
170 3
171 }
172
173 fn line_names(&self) -> Vec<String> {
174 vec![
175 format!(
176 "VW-MACD({},{},{})",
177 self.fast_period, self.slow_period, self.signal_period
178 ),
179 "Signal".to_string(),
180 "Histogram".to_string(),
181 ]
182 }
183
184 fn is_visible(&self) -> bool {
185 self.visible
186 }
187
188 fn set_visible(&mut self, visible: bool) {
189 self.visible = visible;
190 }
191
192 fn clone_box(&self) -> Box<dyn Indicator> {
193 Box::new(self.clone())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn bars(closes: &[f64], volumes: &[f64]) -> Vec<Bar> {
202 closes
203 .iter()
204 .zip(volumes)
205 .map(|(&c, &v)| Bar::new(chrono::Utc::now(), c, c, c, c, v))
206 .collect()
207 }
208
209 fn lines(study: &VolumeWeightedMACD, bar: usize) -> Option<Vec<f64>> {
210 match &study.values().to_rows()[bar] {
211 IndicatorValue::Multiple(v) => Some(v.clone()),
212 _ => None,
213 }
214 }
215
216 #[test]
221 fn matches_published_definition() {
222 let closes = [
223 102.0, 107.0, 108.0, 110.0, 114.0, 112.0, 117.0, 119.0, 121.0, 124.0,
224 ];
225 let volumes = [
226 1000.0, 1500.0, 1200.0, 2000.0, 1800.0, 1400.0, 2500.0, 1600.0, 1300.0, 1700.0,
227 ];
228 let expected: [Option<[f64; 3]>; 10] = [
229 None,
230 None,
231 None,
232 None,
233 Some([1.9866666666666646, 1.9866666666666646, 0.0]),
234 Some([1.5306718597857838, 1.7586692632262242, -0.2279974034404404]),
235 Some([2.0043366844076473, 1.8815029738169358, 0.12283371059071158]),
236 Some([
237 1.8037145650048956,
238 1.8426087694109157,
239 -0.038894204406020094,
240 ]),
241 Some([2.0206718346253183, 1.931640302018117, 0.08903153260720131]),
242 Some([2.8483375959079353, 2.389988948963026, 0.4583486469449092]),
243 ];
244 let mut study = VolumeWeightedMACD::new(3, 5, 3);
245 study.calculate(&bars(&closes, &volumes));
246 assert_eq!(study.values().len(), expected.len());
247 for (bar, want) in expected.iter().enumerate() {
248 let got = lines(&study, bar);
249 match want {
250 None => assert!(got.is_none(), "bar {bar}: {got:?}"),
251 Some(want) => {
252 let got = got.unwrap_or_else(|| panic!("bar {bar}: no value"));
253 for (g, w) in got.iter().zip(want) {
254 assert!((g - w).abs() < 1e-12, "bar {bar}: {got:?} != {want:?}");
255 }
256 }
257 }
258 }
259
260 let closes: Vec<f64> = (0..200).map(|i| 100.0 + i as f64).collect();
263 let mut study = VolumeWeightedMACD::default();
264 study.calculate(&bars(&closes, &[1000.0; 200]));
265 assert!(lines(&study, 24).is_none());
266 for bar in 25..200 {
267 let line = lines(&study, bar).map(|v| v[0]);
268 assert!(
269 line.is_some_and(|l| (l - 7.0).abs() < 1e-9),
270 "bar {bar}: {line:?}"
271 );
272 }
273 }
274}