wickra_core/indicators/
derivative_oscillator.rs1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::indicators::rsi::Rsi;
6use crate::indicators::sma::Sma;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
45pub struct DerivativeOscillator {
46 rsi: Rsi,
47 ema1: Ema,
48 ema2: Ema,
49 signal: Sma,
50 warmup: usize,
51}
52
53impl DerivativeOscillator {
54 pub fn new(
61 rsi_period: usize,
62 smooth1: usize,
63 smooth2: usize,
64 signal_period: usize,
65 ) -> Result<Self> {
66 if rsi_period == 0 || smooth1 == 0 || smooth2 == 0 || signal_period == 0 {
67 return Err(Error::PeriodZero);
68 }
69 Ok(Self {
70 rsi: Rsi::new(rsi_period)?,
71 ema1: Ema::new(smooth1)?,
72 ema2: Ema::new(smooth2)?,
73 signal: Sma::new(signal_period)?,
74 warmup: rsi_period + smooth1 + smooth2 + signal_period - 2,
76 })
77 }
78
79 pub const fn warmup(&self) -> usize {
81 self.warmup
82 }
83}
84
85impl Indicator for DerivativeOscillator {
86 type Input = f64;
87 type Output = f64;
88
89 #[inline]
90 fn update(&mut self, input: f64) -> Option<f64> {
91 let rsi = self.rsi.update(input)?;
92 let s1 = self.ema1.update(rsi)?;
93 let s2 = self.ema2.update(s1)?;
94 let signal = self.signal.update(s2)?;
95 Some(s2 - signal)
96 }
97
98 fn reset(&mut self) {
99 self.rsi.reset();
100 self.ema1.reset();
101 self.ema2.reset();
102 self.signal.reset();
103 }
104
105 #[inline]
106 fn warmup_period(&self) -> usize {
107 self.warmup
108 }
109
110 #[inline]
111 fn is_ready(&self) -> bool {
112 self.signal.is_ready()
113 }
114
115 #[inline]
116 fn name(&self) -> &'static str {
117 "DerivativeOscillator"
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::traits::BatchExt;
125 use approx::assert_relative_eq;
126
127 #[test]
128 fn rejects_zero_periods() {
129 assert!(matches!(
130 DerivativeOscillator::new(0, 5, 3, 9),
131 Err(Error::PeriodZero)
132 ));
133 assert!(matches!(
134 DerivativeOscillator::new(14, 0, 3, 9),
135 Err(Error::PeriodZero)
136 ));
137 assert!(matches!(
138 DerivativeOscillator::new(14, 5, 0, 9),
139 Err(Error::PeriodZero)
140 ));
141 assert!(matches!(
142 DerivativeOscillator::new(14, 5, 3, 0),
143 Err(Error::PeriodZero)
144 ));
145 }
146
147 #[test]
150 fn accessors_and_metadata() {
151 let d = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
152 assert_eq!(d.warmup(), 29);
154 assert_eq!(d.warmup_period(), 29);
155 assert_eq!(d.name(), "DerivativeOscillator");
156 }
157
158 #[test]
159 fn first_emission_matches_warmup_period() {
160 let prices: Vec<f64> = (0..60)
161 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0)
162 .collect();
163 let mut d = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
164 let out = d.batch(&prices);
165 let warmup = d.warmup_period();
166 for (i, v) in out.iter().enumerate().take(warmup - 1) {
167 assert!(v.is_none(), "index {i} must be None during warmup");
168 }
169 assert!(
170 out[warmup - 1].is_some(),
171 "first value must land at warmup_period - 1"
172 );
173 }
174
175 #[test]
176 fn matches_manual_chain() {
177 let prices: Vec<f64> = (0..80)
179 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 8.0)
180 .collect();
181 let mut d = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
182 let mut rsi = Rsi::new(14).unwrap();
183 let mut e1 = Ema::new(5).unwrap();
184 let mut e2 = Ema::new(3).unwrap();
185 let mut sig = Sma::new(9).unwrap();
186 for (i, &p) in prices.iter().enumerate() {
187 let got = d.update(p);
188 let want = rsi
189 .update(p)
190 .and_then(|r| e1.update(r))
191 .and_then(|x| e2.update(x))
192 .and_then(|s2| sig.update(s2).map(|s| s2 - s));
193 assert_eq!(got.is_some(), want.is_some(), "readiness mismatch at {i}");
194 if let (Some(a), Some(b)) = (got, want) {
195 assert_relative_eq!(a, b, epsilon = 1e-9);
196 }
197 }
198 }
199
200 #[test]
201 fn reset_clears_state() {
202 let mut d = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
203 d.batch(&(0..60).map(|i| 100.0 + f64::from(i)).collect::<Vec<_>>());
204 assert!(d.is_ready());
205 d.reset();
206 assert!(!d.is_ready());
207 assert_eq!(d.update(1.0), None);
208 }
209
210 #[test]
211 fn batch_equals_streaming() {
212 let prices: Vec<f64> = (0..80)
213 .map(|i| 50.0 + (f64::from(i) * 0.5).sin() * 10.0)
214 .collect();
215 let mut a = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
216 let mut b = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
217 assert_eq!(
218 a.batch(&prices),
219 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
220 );
221 }
222}