wickra_core/indicators/
dema.rs1use crate::error::Result;
4use crate::indicators::ema::Ema;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
25pub struct Dema {
26 ema1: Ema,
27 ema2: Ema,
28 period: usize,
29}
30
31impl Dema {
32 pub fn new(period: usize) -> Result<Self> {
35 Ok(Self {
36 ema1: Ema::new(period)?,
37 ema2: Ema::new(period)?,
38 period,
39 })
40 }
41
42 pub const fn period(&self) -> usize {
44 self.period
45 }
46}
47
48impl Indicator for Dema {
49 type Input = f64;
50 type Output = f64;
51
52 #[inline]
53 fn update(&mut self, input: f64) -> Option<f64> {
54 let e1 = self.ema1.update(input)?;
55 let e2 = self.ema2.update(e1)?;
56 Some(2.0 * e1 - e2)
57 }
58
59 fn reset(&mut self) {
60 self.ema1.reset();
61 self.ema2.reset();
62 }
63
64 #[inline]
65 fn warmup_period(&self) -> usize {
66 2 * self.period - 1
68 }
69
70 #[inline]
71 fn is_ready(&self) -> bool {
72 self.ema2.is_ready()
73 }
74
75 #[inline]
76 fn name(&self) -> &'static str {
77 "DEMA"
78 }
79
80 fn batch_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
87 assert_eq!(
88 inputs.len(),
89 out.len(),
90 "batch output length must equal input length"
91 );
92 let p = self.period;
93 if !(self.ema1.is_fresh() && self.ema2.is_fresh())
94 || inputs.len() < 2 * p - 1
95 || !crate::fast::in_range(inputs)
96 {
97 self.batch_nan_into(inputs, out);
98 return;
99 }
100 let (e1, e2) = wickra_simd::dispatch(crate::fast::DemaFast {
101 x: inputs,
102 period: p,
103 alpha: self.ema1.alpha(),
104 out,
105 _borrow: std::marker::PhantomData,
106 });
107 self.ema1.seed_to(e1);
108 self.ema2.seed_to(e2);
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::traits::BatchExt;
116 use approx::assert_relative_eq;
117
118 #[test]
119 fn constant_series_yields_constant_dema() {
120 let mut dema = Dema::new(5).unwrap();
121 let out = dema.batch(&[100.0_f64; 60]);
122 let last = out.iter().rev().flatten().next().unwrap();
123 assert_relative_eq!(*last, 100.0, epsilon = 1e-9);
124 }
125
126 #[test]
127 fn linear_uptrend_dema_above_ema_eventually() {
128 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
131 let mut dema = Dema::new(20).unwrap();
132 let mut ema = Ema::new(20).unwrap();
133 let dema_out = dema.batch(&prices);
134 let ema_out = ema.batch(&prices);
135 let d = dema_out.last().unwrap().unwrap();
137 let e = ema_out.last().unwrap().unwrap();
138 assert!(d > e, "DEMA={d} should exceed EMA={e} on uptrend");
139 }
140
141 #[test]
142 fn batch_equals_streaming() {
143 let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
144 let mut a = Dema::new(7).unwrap();
145 let mut b = Dema::new(7).unwrap();
146 assert_eq!(
147 a.batch(&prices),
148 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
149 );
150 }
151
152 #[test]
153 fn reset_clears_state() {
154 let mut dema = Dema::new(5).unwrap();
155 dema.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
156 assert!(dema.is_ready());
157 dema.reset();
158 assert!(!dema.is_ready());
159 }
160
161 #[test]
162 fn rejects_zero_period() {
163 assert!(Dema::new(0).is_err());
164 }
165
166 #[test]
170 fn accessors_and_metadata() {
171 let dema = Dema::new(5).unwrap();
172 assert_eq!(dema.period(), 5);
173 assert_eq!(dema.warmup_period(), 9);
176 assert_eq!(dema.name(), "DEMA");
177 }
178}