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 fn update(&mut self, input: f64) -> Option<f64> {
73 if !input.is_finite() {
74 return self.last_value;
75 }
76 let f = self.fast.update(input)?;
81 let s = self.slow.update(input)?;
82 let v = f - s;
83 self.last_value = Some(v);
84 Some(v)
85 }
86
87 fn reset(&mut self) {
88 self.fast.reset();
89 self.slow.reset();
90 self.last_value = None;
91 }
92
93 fn warmup_period(&self) -> usize {
94 self.fast.warmup_period().max(self.slow.warmup_period())
95 }
96
97 fn is_ready(&self) -> bool {
98 self.last_value.is_some()
99 }
100
101 fn name(&self) -> &'static str {
102 "DecyclerOscillator"
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::traits::BatchExt;
110 use approx::assert_relative_eq;
111
112 #[test]
113 fn new_rejects_invalid_periods() {
114 assert!(matches!(
115 DecyclerOscillator::new(0, 20),
116 Err(Error::PeriodZero)
117 ));
118 assert!(matches!(
119 DecyclerOscillator::new(10, 0),
120 Err(Error::PeriodZero)
121 ));
122 assert!(matches!(
123 DecyclerOscillator::new(20, 10),
124 Err(Error::InvalidPeriod { .. })
125 ));
126 assert!(matches!(
127 DecyclerOscillator::new(10, 10),
128 Err(Error::InvalidPeriod { .. })
129 ));
130 }
131
132 #[test]
133 fn accessors_and_metadata() {
134 let mut dco = DecyclerOscillator::new(10, 30).unwrap();
135 assert_eq!(dco.periods(), (10, 30));
136 assert_eq!(dco.name(), "DecyclerOscillator");
137 assert!(dco.warmup_period() >= 1);
138 assert!(!dco.is_ready());
139 dco.update(100.0);
140 assert!(dco.is_ready());
141 assert!(dco.value().is_some());
142 }
143
144 #[test]
145 fn constant_series_yields_zero() {
146 let mut dco = DecyclerOscillator::new(10, 30).unwrap();
147 let out = dco.batch(&[42.0_f64; 80]);
148 for x in out.iter().flatten() {
149 assert_relative_eq!(*x, 0.0, epsilon = 1e-9);
150 }
151 }
152
153 #[test]
154 fn batch_equals_streaming() {
155 let prices: Vec<f64> = (0..100)
156 .map(|i| 100.0 + (f64::from(i) * 0.2).cos() * 6.0)
157 .collect();
158 let mut a = DecyclerOscillator::new(10, 30).unwrap();
159 let mut b = DecyclerOscillator::new(10, 30).unwrap();
160 let batch = a.batch(&prices);
161 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
162 assert_eq!(batch, streamed);
163 }
164
165 #[test]
166 fn ignores_non_finite_input() {
167 let mut dco = DecyclerOscillator::new(10, 30).unwrap();
168 dco.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
169 let before = dco.value();
170 assert!(before.is_some());
171 assert_eq!(dco.update(f64::NAN), before);
172 }
173
174 #[test]
175 fn reset_clears_state() {
176 let mut dco = DecyclerOscillator::new(10, 30).unwrap();
177 dco.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
178 assert!(dco.is_ready());
179 dco.reset();
180 assert!(!dco.is_ready());
181 }
182}