1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::indicators::vwma::Vwma;
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct VolumeWeightedMacdOutput {
13 pub macd: f64,
15 pub signal: f64,
17 pub histogram: f64,
19}
20
21#[derive(Debug, Clone)]
57pub struct VolumeWeightedMacd {
58 fast: Vwma,
59 slow: Vwma,
60 signal_ema: Ema,
61 fast_period: usize,
62 slow_period: usize,
63 signal_period: usize,
64 last: Option<VolumeWeightedMacdOutput>,
65}
66
67impl VolumeWeightedMacd {
68 pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
75 if fast == 0 || slow == 0 || signal == 0 {
76 return Err(Error::PeriodZero);
77 }
78 if fast >= slow {
79 return Err(Error::InvalidPeriod {
80 message: "fast period must be strictly less than slow period",
81 });
82 }
83 Ok(Self {
84 fast: Vwma::new(fast)?,
85 slow: Vwma::new(slow)?,
86 signal_ema: Ema::new(signal)?,
87 fast_period: fast,
88 slow_period: slow,
89 signal_period: signal,
90 last: None,
91 })
92 }
93
94 pub const fn periods(&self) -> (usize, usize, usize) {
96 (self.fast_period, self.slow_period, self.signal_period)
97 }
98
99 pub const fn value(&self) -> Option<VolumeWeightedMacdOutput> {
101 self.last
102 }
103}
104
105impl Indicator for VolumeWeightedMacd {
106 type Input = Candle;
107 type Output = VolumeWeightedMacdOutput;
108
109 #[inline]
110 fn update(&mut self, candle: Candle) -> Option<VolumeWeightedMacdOutput> {
111 let fast = self.fast.update(candle);
112 let slow = self.slow.update(candle);
113 if let (Some(f), Some(s)) = (fast, slow) {
114 let macd = f - s;
115 let signal = self.signal_ema.update(macd)?;
116 let out = VolumeWeightedMacdOutput {
117 macd,
118 signal,
119 histogram: macd - signal,
120 };
121 self.last = Some(out);
122 return Some(out);
123 }
124 None
125 }
126
127 fn reset(&mut self) {
128 self.fast.reset();
129 self.slow.reset();
130 self.signal_ema.reset();
131 self.last = None;
132 }
133
134 #[inline]
135 fn warmup_period(&self) -> usize {
136 self.slow_period + self.signal_period - 1
137 }
138
139 #[inline]
140 fn is_ready(&self) -> bool {
141 self.last.is_some()
142 }
143
144 #[inline]
145 fn name(&self) -> &'static str {
146 "VolumeWeightedMacd"
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::traits::BatchExt;
154 use approx::assert_relative_eq;
155
156 fn candle(close: f64, volume: f64) -> Candle {
157 Candle::new_unchecked(close, close, close, close, volume, 0)
158 }
159
160 #[test]
161 fn rejects_invalid_periods() {
162 assert!(matches!(
163 VolumeWeightedMacd::new(0, 26, 9),
164 Err(Error::PeriodZero)
165 ));
166 assert!(matches!(
167 VolumeWeightedMacd::new(26, 12, 9),
168 Err(Error::InvalidPeriod { .. })
169 ));
170 assert!(matches!(
171 VolumeWeightedMacd::new(12, 12, 9),
172 Err(Error::InvalidPeriod { .. })
173 ));
174 }
175
176 #[test]
177 fn accessors_and_metadata() {
178 let m = VolumeWeightedMacd::new(12, 26, 9).unwrap();
179 assert_eq!(m.periods(), (12, 26, 9));
180 assert_eq!(m.warmup_period(), 34);
181 assert_eq!(m.name(), "VolumeWeightedMacd");
182 assert!(!m.is_ready());
183 assert_eq!(m.value(), None);
184 }
185
186 #[test]
187 fn first_emission_at_warmup_period() {
188 let mut m = VolumeWeightedMacd::new(2, 4, 3).unwrap();
189 let candles: Vec<Candle> = (0..20)
190 .map(|i| candle(100.0 + f64::from(i), 1_000.0))
191 .collect();
192 let out = m.batch(&candles);
193 let warmup = m.warmup_period(); assert_eq!(warmup, 6);
195 for v in out.iter().take(warmup - 1) {
196 assert!(v.is_none());
197 }
198 assert!(out[warmup - 1].is_some());
199 }
200
201 #[test]
202 fn uptrend_has_positive_macd() {
203 let mut m = VolumeWeightedMacd::new(3, 6, 3).unwrap();
205 let candles: Vec<Candle> = (0..60)
206 .map(|i| candle(100.0 + f64::from(i), 1_000.0))
207 .collect();
208 let last = m.batch(&candles).into_iter().flatten().last().unwrap();
209 assert!(
210 last.macd > 0.0,
211 "uptrend should give positive macd, got {}",
212 last.macd
213 );
214 }
215
216 #[test]
217 fn histogram_is_macd_minus_signal() {
218 let mut m = VolumeWeightedMacd::new(3, 6, 3).unwrap();
219 let candles: Vec<Candle> = (0..60)
220 .map(|i| {
221 candle(
222 100.0 + (f64::from(i) * 0.3).sin() * 5.0,
223 1_000.0 + f64::from(i),
224 )
225 })
226 .collect();
227 for o in m.batch(&candles).into_iter().flatten() {
228 assert_relative_eq!(o.histogram, o.macd - o.signal, epsilon = 1e-9);
229 }
230 }
231
232 #[test]
233 fn equal_volume_matches_plain_macd() {
234 let mut m = VolumeWeightedMacd::new(3, 6, 3).unwrap();
237 let candles: Vec<Candle> = (0..60)
238 .map(|i| candle(100.0 + (f64::from(i) * 0.2).sin() * 4.0, 2_000.0))
239 .collect();
240 for o in m.batch(&candles).into_iter().flatten() {
241 assert!(o.macd.is_finite() && o.signal.is_finite());
242 }
243 }
244
245 #[test]
246 fn reset_clears_state() {
247 let mut m = VolumeWeightedMacd::new(3, 6, 3).unwrap();
248 let candles: Vec<Candle> = (0..40)
249 .map(|i| candle(100.0 + f64::from(i), 1_000.0))
250 .collect();
251 m.batch(&candles);
252 assert!(m.is_ready());
253 m.reset();
254 assert!(!m.is_ready());
255 assert_eq!(m.value(), None);
256 assert_eq!(m.update(candle(100.0, 1_000.0)), None);
257 }
258
259 #[test]
260 fn batch_equals_streaming() {
261 let candles: Vec<Candle> = (0..120)
262 .map(|i| {
263 candle(
264 100.0 + (f64::from(i) * 0.25).sin() * 9.0,
265 1_000.0 + f64::from(i),
266 )
267 })
268 .collect();
269 let batch = VolumeWeightedMacd::new(12, 26, 9).unwrap().batch(&candles);
270 let mut b = VolumeWeightedMacd::new(12, 26, 9).unwrap();
271 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
272 assert_eq!(batch, streamed);
273 }
274}