wickra_core/indicators/
decycler_oscillator.rs1use crate::error::{Error, Result};
4use crate::indicators::decycler::Decycler;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
28pub struct DecyclerOscillator {
29 fast: Decycler,
30 slow: Decycler,
31 last_value: Option<f64>,
32}
33
34impl DecyclerOscillator {
35 pub fn new(fast: usize, slow: usize) -> Result<Self> {
42 if fast == 0 || slow == 0 {
43 return Err(Error::PeriodZero);
44 }
45 if fast >= slow {
46 return Err(Error::InvalidPeriod {
47 message: "fast period must be strictly less than slow period",
48 });
49 }
50 Ok(Self {
51 fast: Decycler::new(fast)?,
52 slow: Decycler::new(slow)?,
53 last_value: None,
54 })
55 }
56
57 pub fn periods(&self) -> (usize, usize) {
59 (self.fast.period(), self.slow.period())
60 }
61
62 pub const fn value(&self) -> Option<f64> {
64 self.last_value
65 }
66}
67
68impl Indicator for DecyclerOscillator {
69 type Input = f64;
70 type Output = f64;
71
72 #[inline]
73 fn update(&mut self, input: f64) -> Option<f64> {
74 if !input.is_finite() {
75 return None;
76 }
77 let f = self.fast.update(input)?;
82 let s = self.slow.update(input)?;
83 let v = f - s;
84 self.last_value = Some(v);
85 Some(v)
86 }
87
88 fn reset(&mut self) {
89 self.fast.reset();
90 self.slow.reset();
91 self.last_value = None;
92 }
93
94 #[inline]
95 fn warmup_period(&self) -> usize {
96 self.fast.warmup_period().max(self.slow.warmup_period())
97 }
98
99 #[inline]
100 fn is_ready(&self) -> bool {
101 self.last_value.is_some()
102 }
103
104 #[inline]
105 fn name(&self) -> &'static str {
106 "DecyclerOscillator"
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::traits::BatchExt;
114 use approx::assert_relative_eq;
115
116 #[test]
117 fn new_rejects_invalid_periods() {
118 assert!(matches!(
119 DecyclerOscillator::new(0, 20),
120 Err(Error::PeriodZero)
121 ));
122 assert!(matches!(
123 DecyclerOscillator::new(10, 0),
124 Err(Error::PeriodZero)
125 ));
126 assert!(matches!(
127 DecyclerOscillator::new(20, 10),
128 Err(Error::InvalidPeriod { .. })
129 ));
130 assert!(matches!(
131 DecyclerOscillator::new(10, 10),
132 Err(Error::InvalidPeriod { .. })
133 ));
134 }
135
136 #[test]
137 fn accessors_and_metadata() {
138 let mut dco = DecyclerOscillator::new(10, 30).unwrap();
139 assert_eq!(dco.periods(), (10, 30));
140 assert_eq!(dco.name(), "DecyclerOscillator");
141 assert!(dco.warmup_period() >= 1);
142 assert!(!dco.is_ready());
143 dco.update(100.0);
144 assert!(dco.is_ready());
145 assert!(dco.value().is_some());
146 }
147
148 #[test]
149 fn constant_series_yields_zero() {
150 let mut dco = DecyclerOscillator::new(10, 30).unwrap();
151 let out = dco.batch(&[42.0_f64; 80]);
152 for x in out.iter().flatten() {
153 assert_relative_eq!(*x, 0.0, epsilon = 1e-9);
154 }
155 }
156
157 #[test]
158 fn batch_equals_streaming() {
159 let prices: Vec<f64> = (0..100)
160 .map(|i| 100.0 + (f64::from(i) * 0.2).cos() * 6.0)
161 .collect();
162 let mut a = DecyclerOscillator::new(10, 30).unwrap();
163 let mut b = DecyclerOscillator::new(10, 30).unwrap();
164 let batch = a.batch(&prices);
165 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
166 assert_eq!(batch, streamed);
167 }
168
169 #[test]
170 fn ignores_non_finite_input() {
171 let mut dco = DecyclerOscillator::new(10, 30).unwrap();
172 dco.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
173 let before = dco.value();
174 assert!(before.is_some());
175 assert_eq!(dco.update(f64::NAN), None);
176 }
177
178 #[test]
179 fn reset_clears_state() {
180 let mut dco = DecyclerOscillator::new(10, 30).unwrap();
181 dco.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
182 assert!(dco.is_ready());
183 dco.reset();
184 assert!(!dco.is_ready());
185 }
186}