1use crate::error::{Error, Result};
4use crate::indicators::dema::Dema;
5use crate::indicators::ema::Ema;
6use crate::indicators::macd::MacdOutput;
7use crate::indicators::sma::Sma;
8use crate::indicators::tema::Tema;
9use crate::indicators::trima::Trima;
10use crate::indicators::wma::Wma;
11use crate::traits::Indicator;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum MaType {
20 Sma,
22 Ema,
24 Wma,
26 Dema,
28 Tema,
30 Trima,
32}
33
34impl MaType {
35 pub fn from_code(code: u32) -> Result<Self> {
41 match code {
42 0 => Ok(Self::Sma),
43 1 => Ok(Self::Ema),
44 2 => Ok(Self::Wma),
45 3 => Ok(Self::Dema),
46 4 => Ok(Self::Tema),
47 5 => Ok(Self::Trima),
48 _ => Err(Error::InvalidPeriod {
49 message: "unsupported moving-average type code (expected 0..=5)",
50 }),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57enum Ma {
58 Sma(Sma),
59 Ema(Ema),
60 Wma(Wma),
61 Dema(Dema),
62 Tema(Tema),
63 Trima(Trima),
64}
65
66impl Ma {
67 fn new(kind: MaType, period: usize) -> Result<Self> {
68 Ok(match kind {
69 MaType::Sma => Self::Sma(Sma::new(period)?),
70 MaType::Ema => Self::Ema(Ema::new(period)?),
71 MaType::Wma => Self::Wma(Wma::new(period)?),
72 MaType::Dema => Self::Dema(Dema::new(period)?),
73 MaType::Tema => Self::Tema(Tema::new(period)?),
74 MaType::Trima => Self::Trima(Trima::new(period)?),
75 })
76 }
77
78 #[inline]
79 fn update(&mut self, value: f64) -> Option<f64> {
80 match self {
81 Self::Sma(m) => m.update(value),
82 Self::Ema(m) => m.update(value),
83 Self::Wma(m) => m.update(value),
84 Self::Dema(m) => m.update(value),
85 Self::Tema(m) => m.update(value),
86 Self::Trima(m) => m.update(value),
87 }
88 }
89
90 fn reset(&mut self) {
91 match self {
92 Self::Sma(m) => m.reset(),
93 Self::Ema(m) => m.reset(),
94 Self::Wma(m) => m.reset(),
95 Self::Dema(m) => m.reset(),
96 Self::Tema(m) => m.reset(),
97 Self::Trima(m) => m.reset(),
98 }
99 }
100
101 #[inline]
102 fn warmup_period(&self) -> usize {
103 match self {
104 Self::Sma(m) => m.warmup_period(),
105 Self::Ema(m) => m.warmup_period(),
106 Self::Wma(m) => m.warmup_period(),
107 Self::Dema(m) => m.warmup_period(),
108 Self::Tema(m) => m.warmup_period(),
109 Self::Trima(m) => m.warmup_period(),
110 }
111 }
112}
113
114#[derive(Debug, Clone)]
137pub struct MacdExt {
138 fast: Ma,
139 slow: Ma,
140 signal: Ma,
141 has_emitted: bool,
142}
143
144impl MacdExt {
145 pub fn new(
152 fast: usize,
153 fast_type: MaType,
154 slow: usize,
155 slow_type: MaType,
156 signal: usize,
157 signal_type: MaType,
158 ) -> Result<Self> {
159 if fast == 0 || slow == 0 || signal == 0 {
160 return Err(Error::PeriodZero);
161 }
162 if fast >= slow {
163 return Err(Error::InvalidPeriod {
164 message: "fast period must be < slow period",
165 });
166 }
167 Ok(Self {
168 fast: Ma::new(fast_type, fast)?,
169 slow: Ma::new(slow_type, slow)?,
170 signal: Ma::new(signal_type, signal)?,
171 has_emitted: false,
172 })
173 }
174}
175
176impl Indicator for MacdExt {
177 type Input = f64;
178 type Output = MacdOutput;
179
180 #[inline]
181 fn update(&mut self, value: f64) -> Option<MacdOutput> {
182 let fast_v = self.fast.update(value);
183 let slow_v = self.slow.update(value);
184 let (Some(fast_v), Some(slow_v)) = (fast_v, slow_v) else {
185 return None;
186 };
187 let macd = fast_v - slow_v;
188 let signal = self.signal.update(macd)?;
189 self.has_emitted = true;
190 Some(MacdOutput {
191 macd,
192 signal,
193 histogram: macd - signal,
194 })
195 }
196
197 fn reset(&mut self) {
198 self.fast.reset();
199 self.slow.reset();
200 self.signal.reset();
201 self.has_emitted = false;
202 }
203
204 #[inline]
205 fn warmup_period(&self) -> usize {
206 self.slow.warmup_period() + self.signal.warmup_period() - 1
209 }
210
211 #[inline]
212 fn is_ready(&self) -> bool {
213 self.has_emitted
214 }
215
216 #[inline]
217 fn name(&self) -> &'static str {
218 "MACDEXT"
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::traits::BatchExt;
226
227 const TYPES: [MaType; 6] = [
228 MaType::Sma,
229 MaType::Ema,
230 MaType::Wma,
231 MaType::Dema,
232 MaType::Tema,
233 MaType::Trima,
234 ];
235
236 #[test]
237 fn from_code_maps_all_supported_types() {
238 assert_eq!(MaType::from_code(0).unwrap(), MaType::Sma);
239 assert_eq!(MaType::from_code(1).unwrap(), MaType::Ema);
240 assert_eq!(MaType::from_code(2).unwrap(), MaType::Wma);
241 assert_eq!(MaType::from_code(3).unwrap(), MaType::Dema);
242 assert_eq!(MaType::from_code(4).unwrap(), MaType::Tema);
243 assert_eq!(MaType::from_code(5).unwrap(), MaType::Trima);
244 assert!(MaType::from_code(6).is_err());
245 }
246
247 #[test]
248 fn rejects_invalid_periods() {
249 assert!(matches!(
250 MacdExt::new(0, MaType::Ema, 26, MaType::Ema, 9, MaType::Ema),
251 Err(Error::PeriodZero)
252 ));
253 assert!(matches!(
254 MacdExt::new(26, MaType::Ema, 12, MaType::Ema, 9, MaType::Ema),
255 Err(Error::InvalidPeriod { .. })
256 ));
257 }
258
259 #[test]
260 fn accessors_and_metadata() {
261 let m = MacdExt::new(12, MaType::Ema, 26, MaType::Sma, 9, MaType::Sma).unwrap();
262 assert_eq!(m.name(), "MACDEXT");
263 assert!(!m.is_ready());
264 assert!(m.warmup_period() >= 26);
265 }
266
267 #[test]
268 fn every_ma_type_produces_a_consistent_histogram() {
269 let prices: Vec<f64> = (0..120)
270 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 6.0)
271 .collect();
272 for &t in &TYPES {
273 let mut m = MacdExt::new(5, t, 10, t, 4, t).unwrap();
274 let out: Vec<Option<MacdOutput>> = m.batch(&prices);
275 assert!(out.iter().any(Option::is_some), "{t:?} never emitted");
276 for o in out.into_iter().flatten() {
277 assert!((o.histogram - (o.macd - o.signal)).abs() < 1e-9);
278 }
279 assert!(m.warmup_period() >= 10);
281 assert!(m.is_ready());
282 m.reset();
283 assert!(!m.is_ready());
284 }
285 }
286
287 #[test]
288 fn mixed_ma_types_per_line() {
289 let prices: Vec<f64> = (0..120).map(|i| 100.0 + f64::from(i)).collect();
290 let mut m = MacdExt::new(12, MaType::Wma, 26, MaType::Dema, 9, MaType::Trima).unwrap();
291 let last = m.batch(&prices).into_iter().flatten().last();
292 assert!(last.is_some());
293 }
294}