wickra_core/indicators/
even_better_sinewave.rs1#![allow(clippy::doc_markdown)]
3
4use std::f64::consts::PI;
5
6use crate::error::{Error, Result};
7use crate::indicators::super_smoother::SuperSmoother;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
49pub struct EvenBetterSinewave {
50 hp_period: usize,
51 ssf_length: usize,
52 alpha1: f64,
53 smoother: SuperSmoother,
54 prev_price: Option<f64>,
55 hp: f64,
56 filt1: Option<f64>,
57 filt2: Option<f64>,
58 filt3: Option<f64>,
59 last: Option<f64>,
60}
61
62impl EvenBetterSinewave {
63 pub fn new(hp_period: usize, ssf_length: usize) -> Result<Self> {
70 if hp_period == 0 || ssf_length == 0 {
71 return Err(Error::PeriodZero);
72 }
73 let w = 2.0 * PI / hp_period as f64;
74 let alpha1 = (1.0 - w.sin()) / w.cos();
75 Ok(Self {
76 hp_period,
77 ssf_length,
78 alpha1,
79 smoother: SuperSmoother::new(ssf_length)?,
80 prev_price: None,
81 hp: 0.0,
82 filt1: None,
83 filt2: None,
84 filt3: None,
85 last: None,
86 })
87 }
88
89 pub const fn params(&self) -> (usize, usize) {
91 (self.hp_period, self.ssf_length)
92 }
93
94 pub const fn value(&self) -> Option<f64> {
96 self.last
97 }
98}
99
100impl Indicator for EvenBetterSinewave {
101 type Input = f64;
102 type Output = f64;
103
104 #[inline]
105 fn update(&mut self, price: f64) -> Option<f64> {
106 if !price.is_finite() {
107 return None;
108 }
109 let gain = f64::midpoint(1.0, self.alpha1);
111 let hp = match self.prev_price {
112 Some(prev) => gain * (price - prev) + self.alpha1 * self.hp,
113 None => 0.0,
114 };
115 self.prev_price = Some(price);
116 self.hp = hp;
117 let filt = self.smoother.update(hp)?;
118 self.filt3 = self.filt2;
120 self.filt2 = self.filt1;
121 self.filt1 = Some(filt);
122 let (Some(f1), Some(f2), Some(f3)) = (self.filt1, self.filt2, self.filt3) else {
123 return None;
124 };
125 let wave = (f1 + f2 + f3) / 3.0;
126 let pwr = (f1 * f1 + f2 * f2 + f3 * f3) / 3.0;
127 let ebsw = if pwr > 0.0 {
128 (wave / pwr.sqrt()).clamp(-1.0, 1.0)
129 } else {
130 0.0
131 };
132 self.last = Some(ebsw);
133 Some(ebsw)
134 }
135
136 fn reset(&mut self) {
137 self.smoother.reset();
138 self.prev_price = None;
139 self.hp = 0.0;
140 self.filt1 = None;
141 self.filt2 = None;
142 self.filt3 = None;
143 self.last = None;
144 }
145
146 #[inline]
147 fn warmup_period(&self) -> usize {
148 3
149 }
150
151 #[inline]
152 fn is_ready(&self) -> bool {
153 self.last.is_some()
154 }
155
156 #[inline]
157 fn name(&self) -> &'static str {
158 "EvenBetterSinewave"
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::traits::BatchExt;
166
167 #[test]
168 fn rejects_zero_params() {
169 assert!(matches!(
170 EvenBetterSinewave::new(0, 10),
171 Err(Error::PeriodZero)
172 ));
173 assert!(matches!(
174 EvenBetterSinewave::new(40, 0),
175 Err(Error::PeriodZero)
176 ));
177 }
178
179 #[test]
180 fn accessors_and_metadata() {
181 let e = EvenBetterSinewave::new(40, 10).unwrap();
182 assert_eq!(e.params(), (40, 10));
183 assert_eq!(e.warmup_period(), 3);
184 assert_eq!(e.name(), "EvenBetterSinewave");
185 assert!(!e.is_ready());
186 assert_eq!(e.value(), None);
187 }
188
189 #[test]
190 fn first_emission_at_warmup_period() {
191 let mut e = EvenBetterSinewave::new(40, 10).unwrap();
192 let xs: Vec<f64> = (0..12)
193 .map(|i| 100.0 + (f64::from(i) * 0.5).sin() * 3.0)
194 .collect();
195 let out = e.batch(&xs);
196 for v in out.iter().take(2) {
197 assert!(v.is_none());
198 }
199 assert!(out[2].is_some());
200 }
201
202 #[test]
203 fn output_in_range() {
204 let mut e = EvenBetterSinewave::new(40, 10).unwrap();
205 let xs: Vec<f64> = (0..400)
206 .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 30.0).sin() * 5.0)
207 .collect();
208 for v in e.batch(&xs).into_iter().flatten() {
209 assert!((-1.0..=1.0).contains(&v), "EBSW out of range: {v}");
210 }
211 }
212
213 #[test]
214 fn cyclic_input_swings_both_signs() {
215 let mut e = EvenBetterSinewave::new(30, 8).unwrap();
216 let xs: Vec<f64> = (0..400)
217 .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 30.0).sin() * 5.0)
218 .collect();
219 let out: Vec<f64> = e.batch(&xs).into_iter().flatten().skip(100).collect();
220 assert!(out.iter().any(|&v| v > 0.5));
221 assert!(out.iter().any(|&v| v < -0.5));
222 }
223
224 #[test]
225 fn ignores_non_finite() {
226 let mut e = EvenBetterSinewave::new(40, 10).unwrap();
227 e.batch(
228 &(0..40)
229 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
230 .collect::<Vec<_>>(),
231 );
232 let before = e.value();
233 assert_eq!(e.update(f64::NAN), None);
234 assert_eq!(e.value(), before);
236 }
237
238 #[test]
239 fn reset_clears_state() {
240 let mut e = EvenBetterSinewave::new(40, 10).unwrap();
241 e.batch(
242 &(0..40)
243 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
244 .collect::<Vec<_>>(),
245 );
246 assert!(e.is_ready());
247 e.reset();
248 assert!(!e.is_ready());
249 assert_eq!(e.value(), None);
250 }
251
252 #[test]
253 fn batch_equals_streaming() {
254 let xs: Vec<f64> = (0..120)
255 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
256 .collect();
257 let batch = EvenBetterSinewave::new(40, 10).unwrap().batch(&xs);
258 let mut b = EvenBetterSinewave::new(40, 10).unwrap();
259 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
260 assert_eq!(batch, streamed);
261 }
262
263 #[test]
264 fn flat_input_yields_zero_power() {
265 let flat = [100.0_f64; 200];
268 let last = EvenBetterSinewave::new(40, 10)
269 .unwrap()
270 .batch(&flat)
271 .into_iter()
272 .flatten()
273 .last()
274 .unwrap();
275 assert_eq!(last, 0.0);
276 }
277}