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 fn update(&mut self, price: f64) -> Option<f64> {
105 if !price.is_finite() {
106 return self.last;
107 }
108 let hp = match self.prev_price {
109 Some(prev) => 0.5 * (1.0 + self.alpha1) * (price - prev) + self.alpha1 * self.hp,
110 None => 0.0,
111 };
112 self.prev_price = Some(price);
113 self.hp = hp;
114 let filt = self.smoother.update(hp)?;
115 self.filt3 = self.filt2;
117 self.filt2 = self.filt1;
118 self.filt1 = Some(filt);
119 let (Some(f1), Some(f2), Some(f3)) = (self.filt1, self.filt2, self.filt3) else {
120 return None;
121 };
122 let wave = (f1 + f2 + f3) / 3.0;
123 let pwr = (f1 * f1 + f2 * f2 + f3 * f3) / 3.0;
124 let ebsw = if pwr > 0.0 {
125 (wave / pwr.sqrt()).clamp(-1.0, 1.0)
126 } else {
127 0.0
128 };
129 self.last = Some(ebsw);
130 Some(ebsw)
131 }
132
133 fn reset(&mut self) {
134 self.smoother.reset();
135 self.prev_price = None;
136 self.hp = 0.0;
137 self.filt1 = None;
138 self.filt2 = None;
139 self.filt3 = None;
140 self.last = None;
141 }
142
143 fn warmup_period(&self) -> usize {
144 3
145 }
146
147 fn is_ready(&self) -> bool {
148 self.last.is_some()
149 }
150
151 fn name(&self) -> &'static str {
152 "EvenBetterSinewave"
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::traits::BatchExt;
160
161 #[test]
162 fn rejects_zero_params() {
163 assert!(matches!(
164 EvenBetterSinewave::new(0, 10),
165 Err(Error::PeriodZero)
166 ));
167 assert!(matches!(
168 EvenBetterSinewave::new(40, 0),
169 Err(Error::PeriodZero)
170 ));
171 }
172
173 #[test]
174 fn accessors_and_metadata() {
175 let e = EvenBetterSinewave::new(40, 10).unwrap();
176 assert_eq!(e.params(), (40, 10));
177 assert_eq!(e.warmup_period(), 3);
178 assert_eq!(e.name(), "EvenBetterSinewave");
179 assert!(!e.is_ready());
180 assert_eq!(e.value(), None);
181 }
182
183 #[test]
184 fn first_emission_at_warmup_period() {
185 let mut e = EvenBetterSinewave::new(40, 10).unwrap();
186 let xs: Vec<f64> = (0..12)
187 .map(|i| 100.0 + (f64::from(i) * 0.5).sin() * 3.0)
188 .collect();
189 let out = e.batch(&xs);
190 for v in out.iter().take(2) {
191 assert!(v.is_none());
192 }
193 assert!(out[2].is_some());
194 }
195
196 #[test]
197 fn output_in_range() {
198 let mut e = EvenBetterSinewave::new(40, 10).unwrap();
199 let xs: Vec<f64> = (0..400)
200 .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 30.0).sin() * 5.0)
201 .collect();
202 for v in e.batch(&xs).into_iter().flatten() {
203 assert!((-1.0..=1.0).contains(&v), "EBSW out of range: {v}");
204 }
205 }
206
207 #[test]
208 fn cyclic_input_swings_both_signs() {
209 let mut e = EvenBetterSinewave::new(30, 8).unwrap();
210 let xs: Vec<f64> = (0..400)
211 .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 30.0).sin() * 5.0)
212 .collect();
213 let out: Vec<f64> = e.batch(&xs).into_iter().flatten().skip(100).collect();
214 assert!(out.iter().any(|&v| v > 0.5));
215 assert!(out.iter().any(|&v| v < -0.5));
216 }
217
218 #[test]
219 fn ignores_non_finite() {
220 let mut e = EvenBetterSinewave::new(40, 10).unwrap();
221 e.batch(
222 &(0..40)
223 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
224 .collect::<Vec<_>>(),
225 );
226 let before = e.value();
227 assert_eq!(e.update(f64::NAN), before);
228 }
229
230 #[test]
231 fn reset_clears_state() {
232 let mut e = EvenBetterSinewave::new(40, 10).unwrap();
233 e.batch(
234 &(0..40)
235 .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
236 .collect::<Vec<_>>(),
237 );
238 assert!(e.is_ready());
239 e.reset();
240 assert!(!e.is_ready());
241 assert_eq!(e.value(), None);
242 }
243
244 #[test]
245 fn batch_equals_streaming() {
246 let xs: Vec<f64> = (0..120)
247 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
248 .collect();
249 let batch = EvenBetterSinewave::new(40, 10).unwrap().batch(&xs);
250 let mut b = EvenBetterSinewave::new(40, 10).unwrap();
251 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
252 assert_eq!(batch, streamed);
253 }
254
255 #[test]
256 fn flat_input_yields_zero_power() {
257 let flat = [100.0_f64; 200];
260 let last = EvenBetterSinewave::new(40, 10)
261 .unwrap()
262 .batch(&flat)
263 .into_iter()
264 .flatten()
265 .last()
266 .unwrap();
267 assert_eq!(last, 0.0);
268 }
269}