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 fn update(&mut self, candle: Candle) -> Option<VolumeWeightedMacdOutput> {
110 let fast = self.fast.update(candle);
111 let slow = self.slow.update(candle);
112 if let (Some(f), Some(s)) = (fast, slow) {
113 let macd = f - s;
114 let signal = self.signal_ema.update(macd)?;
115 let out = VolumeWeightedMacdOutput {
116 macd,
117 signal,
118 histogram: macd - signal,
119 };
120 self.last = Some(out);
121 return Some(out);
122 }
123 None
124 }
125
126 fn reset(&mut self) {
127 self.fast.reset();
128 self.slow.reset();
129 self.signal_ema.reset();
130 self.last = None;
131 }
132
133 fn warmup_period(&self) -> usize {
134 self.slow_period + self.signal_period - 1
135 }
136
137 fn is_ready(&self) -> bool {
138 self.last.is_some()
139 }
140
141 fn name(&self) -> &'static str {
142 "VolumeWeightedMacd"
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::traits::BatchExt;
150 use approx::assert_relative_eq;
151
152 fn candle(close: f64, volume: f64) -> Candle {
153 Candle::new_unchecked(close, close, close, close, volume, 0)
154 }
155
156 #[test]
157 fn rejects_invalid_periods() {
158 assert!(matches!(
159 VolumeWeightedMacd::new(0, 26, 9),
160 Err(Error::PeriodZero)
161 ));
162 assert!(matches!(
163 VolumeWeightedMacd::new(26, 12, 9),
164 Err(Error::InvalidPeriod { .. })
165 ));
166 assert!(matches!(
167 VolumeWeightedMacd::new(12, 12, 9),
168 Err(Error::InvalidPeriod { .. })
169 ));
170 }
171
172 #[test]
173 fn accessors_and_metadata() {
174 let m = VolumeWeightedMacd::new(12, 26, 9).unwrap();
175 assert_eq!(m.periods(), (12, 26, 9));
176 assert_eq!(m.warmup_period(), 34);
177 assert_eq!(m.name(), "VolumeWeightedMacd");
178 assert!(!m.is_ready());
179 assert_eq!(m.value(), None);
180 }
181
182 #[test]
183 fn first_emission_at_warmup_period() {
184 let mut m = VolumeWeightedMacd::new(2, 4, 3).unwrap();
185 let candles: Vec<Candle> = (0..20)
186 .map(|i| candle(100.0 + f64::from(i), 1_000.0))
187 .collect();
188 let out = m.batch(&candles);
189 let warmup = m.warmup_period(); assert_eq!(warmup, 6);
191 for v in out.iter().take(warmup - 1) {
192 assert!(v.is_none());
193 }
194 assert!(out[warmup - 1].is_some());
195 }
196
197 #[test]
198 fn uptrend_has_positive_macd() {
199 let mut m = VolumeWeightedMacd::new(3, 6, 3).unwrap();
201 let candles: Vec<Candle> = (0..60)
202 .map(|i| candle(100.0 + f64::from(i), 1_000.0))
203 .collect();
204 let last = m.batch(&candles).into_iter().flatten().last().unwrap();
205 assert!(
206 last.macd > 0.0,
207 "uptrend should give positive macd, got {}",
208 last.macd
209 );
210 }
211
212 #[test]
213 fn histogram_is_macd_minus_signal() {
214 let mut m = VolumeWeightedMacd::new(3, 6, 3).unwrap();
215 let candles: Vec<Candle> = (0..60)
216 .map(|i| {
217 candle(
218 100.0 + (f64::from(i) * 0.3).sin() * 5.0,
219 1_000.0 + f64::from(i),
220 )
221 })
222 .collect();
223 for o in m.batch(&candles).into_iter().flatten() {
224 assert_relative_eq!(o.histogram, o.macd - o.signal, epsilon = 1e-9);
225 }
226 }
227
228 #[test]
229 fn equal_volume_matches_plain_macd() {
230 let mut m = VolumeWeightedMacd::new(3, 6, 3).unwrap();
233 let candles: Vec<Candle> = (0..60)
234 .map(|i| candle(100.0 + (f64::from(i) * 0.2).sin() * 4.0, 2_000.0))
235 .collect();
236 for o in m.batch(&candles).into_iter().flatten() {
237 assert!(o.macd.is_finite() && o.signal.is_finite());
238 }
239 }
240
241 #[test]
242 fn reset_clears_state() {
243 let mut m = VolumeWeightedMacd::new(3, 6, 3).unwrap();
244 let candles: Vec<Candle> = (0..40)
245 .map(|i| candle(100.0 + f64::from(i), 1_000.0))
246 .collect();
247 m.batch(&candles);
248 assert!(m.is_ready());
249 m.reset();
250 assert!(!m.is_ready());
251 assert_eq!(m.value(), None);
252 assert_eq!(m.update(candle(100.0, 1_000.0)), None);
253 }
254
255 #[test]
256 fn batch_equals_streaming() {
257 let candles: Vec<Candle> = (0..120)
258 .map(|i| {
259 candle(
260 100.0 + (f64::from(i) * 0.25).sin() * 9.0,
261 1_000.0 + f64::from(i),
262 )
263 })
264 .collect();
265 let batch = VolumeWeightedMacd::new(12, 26, 9).unwrap().batch(&candles);
266 let mut b = VolumeWeightedMacd::new(12, 26, 9).unwrap();
267 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
268 assert_eq!(batch, streamed);
269 }
270}