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 fn update(&mut self, input: f64) -> Option<f64> {
90 let rsi = self.rsi.update(input)?;
91 let s1 = self.ema1.update(rsi)?;
92 let s2 = self.ema2.update(s1)?;
93 let signal = self.signal.update(s2)?;
94 Some(s2 - signal)
95 }
96
97 fn reset(&mut self) {
98 self.rsi.reset();
99 self.ema1.reset();
100 self.ema2.reset();
101 self.signal.reset();
102 }
103
104 fn warmup_period(&self) -> usize {
105 self.warmup
106 }
107
108 fn is_ready(&self) -> bool {
109 self.signal.is_ready()
110 }
111
112 fn name(&self) -> &'static str {
113 "DerivativeOscillator"
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::traits::BatchExt;
121 use approx::assert_relative_eq;
122
123 #[test]
124 fn rejects_zero_periods() {
125 assert!(matches!(
126 DerivativeOscillator::new(0, 5, 3, 9),
127 Err(Error::PeriodZero)
128 ));
129 assert!(matches!(
130 DerivativeOscillator::new(14, 0, 3, 9),
131 Err(Error::PeriodZero)
132 ));
133 assert!(matches!(
134 DerivativeOscillator::new(14, 5, 0, 9),
135 Err(Error::PeriodZero)
136 ));
137 assert!(matches!(
138 DerivativeOscillator::new(14, 5, 3, 0),
139 Err(Error::PeriodZero)
140 ));
141 }
142
143 #[test]
146 fn accessors_and_metadata() {
147 let d = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
148 assert_eq!(d.warmup(), 29);
150 assert_eq!(d.warmup_period(), 29);
151 assert_eq!(d.name(), "DerivativeOscillator");
152 }
153
154 #[test]
155 fn first_emission_matches_warmup_period() {
156 let prices: Vec<f64> = (0..60)
157 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0)
158 .collect();
159 let mut d = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
160 let out = d.batch(&prices);
161 let warmup = d.warmup_period();
162 for (i, v) in out.iter().enumerate().take(warmup - 1) {
163 assert!(v.is_none(), "index {i} must be None during warmup");
164 }
165 assert!(
166 out[warmup - 1].is_some(),
167 "first value must land at warmup_period - 1"
168 );
169 }
170
171 #[test]
172 fn matches_manual_chain() {
173 let prices: Vec<f64> = (0..80)
175 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 8.0)
176 .collect();
177 let mut d = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
178 let mut rsi = Rsi::new(14).unwrap();
179 let mut e1 = Ema::new(5).unwrap();
180 let mut e2 = Ema::new(3).unwrap();
181 let mut sig = Sma::new(9).unwrap();
182 for (i, &p) in prices.iter().enumerate() {
183 let got = d.update(p);
184 let want = rsi
185 .update(p)
186 .and_then(|r| e1.update(r))
187 .and_then(|x| e2.update(x))
188 .and_then(|s2| sig.update(s2).map(|s| s2 - s));
189 assert_eq!(got.is_some(), want.is_some(), "readiness mismatch at {i}");
190 if let (Some(a), Some(b)) = (got, want) {
191 assert_relative_eq!(a, b, epsilon = 1e-9);
192 }
193 }
194 }
195
196 #[test]
197 fn reset_clears_state() {
198 let mut d = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
199 d.batch(&(0..60).map(|i| 100.0 + f64::from(i)).collect::<Vec<_>>());
200 assert!(d.is_ready());
201 d.reset();
202 assert!(!d.is_ready());
203 assert_eq!(d.update(1.0), None);
204 }
205
206 #[test]
207 fn batch_equals_streaming() {
208 let prices: Vec<f64> = (0..80)
209 .map(|i| 50.0 + (f64::from(i) * 0.5).sin() * 10.0)
210 .collect();
211 let mut a = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
212 let mut b = DerivativeOscillator::new(14, 5, 3, 9).unwrap();
213 assert_eq!(
214 a.batch(&prices),
215 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
216 );
217 }
218}