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 fn update(&mut self, value: f64) -> Option<f64> {
79 match self {
80 Self::Sma(m) => m.update(value),
81 Self::Ema(m) => m.update(value),
82 Self::Wma(m) => m.update(value),
83 Self::Dema(m) => m.update(value),
84 Self::Tema(m) => m.update(value),
85 Self::Trima(m) => m.update(value),
86 }
87 }
88
89 fn reset(&mut self) {
90 match self {
91 Self::Sma(m) => m.reset(),
92 Self::Ema(m) => m.reset(),
93 Self::Wma(m) => m.reset(),
94 Self::Dema(m) => m.reset(),
95 Self::Tema(m) => m.reset(),
96 Self::Trima(m) => m.reset(),
97 }
98 }
99
100 fn warmup_period(&self) -> usize {
101 match self {
102 Self::Sma(m) => m.warmup_period(),
103 Self::Ema(m) => m.warmup_period(),
104 Self::Wma(m) => m.warmup_period(),
105 Self::Dema(m) => m.warmup_period(),
106 Self::Tema(m) => m.warmup_period(),
107 Self::Trima(m) => m.warmup_period(),
108 }
109 }
110}
111
112#[derive(Debug, Clone)]
135pub struct MacdExt {
136 fast: Ma,
137 slow: Ma,
138 signal: Ma,
139 has_emitted: bool,
140}
141
142impl MacdExt {
143 pub fn new(
150 fast: usize,
151 fast_type: MaType,
152 slow: usize,
153 slow_type: MaType,
154 signal: usize,
155 signal_type: MaType,
156 ) -> Result<Self> {
157 if fast == 0 || slow == 0 || signal == 0 {
158 return Err(Error::PeriodZero);
159 }
160 if fast >= slow {
161 return Err(Error::InvalidPeriod {
162 message: "fast period must be < slow period",
163 });
164 }
165 Ok(Self {
166 fast: Ma::new(fast_type, fast)?,
167 slow: Ma::new(slow_type, slow)?,
168 signal: Ma::new(signal_type, signal)?,
169 has_emitted: false,
170 })
171 }
172}
173
174impl Indicator for MacdExt {
175 type Input = f64;
176 type Output = MacdOutput;
177
178 fn update(&mut self, value: f64) -> Option<MacdOutput> {
179 let fast_v = self.fast.update(value);
180 let slow_v = self.slow.update(value);
181 let (Some(fast_v), Some(slow_v)) = (fast_v, slow_v) else {
182 return None;
183 };
184 let macd = fast_v - slow_v;
185 let signal = self.signal.update(macd)?;
186 self.has_emitted = true;
187 Some(MacdOutput {
188 macd,
189 signal,
190 histogram: macd - signal,
191 })
192 }
193
194 fn reset(&mut self) {
195 self.fast.reset();
196 self.slow.reset();
197 self.signal.reset();
198 self.has_emitted = false;
199 }
200
201 fn warmup_period(&self) -> usize {
202 self.slow.warmup_period() + self.signal.warmup_period()
203 }
204
205 fn is_ready(&self) -> bool {
206 self.has_emitted
207 }
208
209 fn name(&self) -> &'static str {
210 "MACDEXT"
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::traits::BatchExt;
218
219 const TYPES: [MaType; 6] = [
220 MaType::Sma,
221 MaType::Ema,
222 MaType::Wma,
223 MaType::Dema,
224 MaType::Tema,
225 MaType::Trima,
226 ];
227
228 #[test]
229 fn from_code_maps_all_supported_types() {
230 assert_eq!(MaType::from_code(0).unwrap(), MaType::Sma);
231 assert_eq!(MaType::from_code(1).unwrap(), MaType::Ema);
232 assert_eq!(MaType::from_code(2).unwrap(), MaType::Wma);
233 assert_eq!(MaType::from_code(3).unwrap(), MaType::Dema);
234 assert_eq!(MaType::from_code(4).unwrap(), MaType::Tema);
235 assert_eq!(MaType::from_code(5).unwrap(), MaType::Trima);
236 assert!(MaType::from_code(6).is_err());
237 }
238
239 #[test]
240 fn rejects_invalid_periods() {
241 assert!(matches!(
242 MacdExt::new(0, MaType::Ema, 26, MaType::Ema, 9, MaType::Ema),
243 Err(Error::PeriodZero)
244 ));
245 assert!(matches!(
246 MacdExt::new(26, MaType::Ema, 12, MaType::Ema, 9, MaType::Ema),
247 Err(Error::InvalidPeriod { .. })
248 ));
249 }
250
251 #[test]
252 fn accessors_and_metadata() {
253 let m = MacdExt::new(12, MaType::Ema, 26, MaType::Sma, 9, MaType::Sma).unwrap();
254 assert_eq!(m.name(), "MACDEXT");
255 assert!(!m.is_ready());
256 assert!(m.warmup_period() >= 26);
257 }
258
259 #[test]
260 fn every_ma_type_produces_a_consistent_histogram() {
261 let prices: Vec<f64> = (0..120)
262 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 6.0)
263 .collect();
264 for &t in &TYPES {
265 let mut m = MacdExt::new(5, t, 10, t, 4, t).unwrap();
266 let out: Vec<Option<MacdOutput>> = m.batch(&prices);
267 assert!(out.iter().any(Option::is_some), "{t:?} never emitted");
268 for o in out.into_iter().flatten() {
269 assert!((o.histogram - (o.macd - o.signal)).abs() < 1e-9);
270 }
271 assert!(m.warmup_period() >= 10);
273 assert!(m.is_ready());
274 m.reset();
275 assert!(!m.is_ready());
276 }
277 }
278
279 #[test]
280 fn mixed_ma_types_per_line() {
281 let prices: Vec<f64> = (0..120).map(|i| 100.0 + f64::from(i)).collect();
282 let mut m = MacdExt::new(12, MaType::Wma, 26, MaType::Dema, 9, MaType::Trima).unwrap();
283 let last = m.batch(&prices).into_iter().flatten().last();
284 assert!(last.is_some());
285 }
286}