wickra_core/indicators/
sine_wave.rs1#![allow(clippy::manual_clamp)]
3
4use std::f64::consts::PI;
5
6use crate::indicators::hilbert_dominant_cycle::HilbertDominantCycle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Default)]
34pub struct SineWave {
35 cycle: HilbertDominantCycle,
36 smooth_buf: Vec<f64>,
37 detrender_buf: Vec<f64>,
38 last_phase: f64,
39 last_sine: Option<f64>,
40 last_lead: f64,
41 count: usize,
42}
43
44impl SineWave {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub const fn lead(&self) -> f64 {
52 self.last_lead
53 }
54
55 pub const fn value(&self) -> Option<f64> {
57 self.last_sine
58 }
59
60 fn push_front(buf: &mut Vec<f64>, v: f64, cap: usize) {
61 buf.insert(0, v);
62 if buf.len() > cap {
63 buf.truncate(cap);
64 }
65 }
66}
67
68impl Indicator for SineWave {
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_sine;
75 }
76 self.count += 1;
77 let _ = self.cycle.update(input);
80
81 Self::push_front(&mut self.smooth_buf, input, 7);
82 if self.smooth_buf.len() < 4 {
83 return None;
84 }
85 let smooth = (4.0 * self.smooth_buf[0]
86 + 3.0 * self.smooth_buf[1]
87 + 2.0 * self.smooth_buf[2]
88 + self.smooth_buf[3])
89 / 10.0;
90 if self.smooth_buf.len() < 7 {
91 return None;
92 }
93 let period = self.cycle.value().unwrap_or(15.0).max(6.0).min(50.0);
94 let adj = 0.075 * period + 0.54;
95 let s0 = smooth;
96 let s2 = self.smooth_buf[2];
97 let s4 = self.smooth_buf[4];
98 let s6 = self.smooth_buf[6];
99 let detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj;
100 Self::push_front(&mut self.detrender_buf, detrender, 7);
101 if self.detrender_buf.len() < 7 {
102 return None;
103 }
104 let q1 = (0.0962 * self.detrender_buf[0] + 0.5769 * self.detrender_buf[2]
105 - 0.5769 * self.detrender_buf[4]
106 - 0.0962 * self.detrender_buf[6])
107 * adj;
108 let i1 = self.detrender_buf[3];
109 let phase = if i1.abs() > f64::EPSILON {
110 (q1 / i1).atan()
111 } else {
112 self.last_phase
113 };
114 self.last_phase = phase;
115 let sine = phase.sin();
116 let lead = (phase + PI / 4.0).sin();
117
118 if self.count < 50 {
119 return None;
120 }
121 self.last_sine = Some(sine);
122 self.last_lead = lead;
123 Some(sine)
124 }
125
126 fn reset(&mut self) {
127 self.cycle.reset();
128 self.smooth_buf.clear();
129 self.detrender_buf.clear();
130 self.last_phase = 0.0;
131 self.last_sine = None;
132 self.last_lead = 0.0;
133 self.count = 0;
134 }
135
136 fn warmup_period(&self) -> usize {
137 50
138 }
139
140 fn is_ready(&self) -> bool {
141 self.last_sine.is_some()
142 }
143
144 fn name(&self) -> &'static str {
145 "SineWave"
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::traits::BatchExt;
153
154 #[test]
155 fn accessors_and_metadata() {
156 let mut sw = SineWave::new();
157 assert_eq!(sw.warmup_period(), 50);
158 assert_eq!(sw.name(), "SineWave");
159 assert!(!sw.is_ready());
160 assert!(sw.value().is_none());
161 let prices: Vec<f64> = (0..120)
162 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
163 .collect();
164 sw.batch(&prices);
165 assert!(sw.is_ready());
166 assert!(sw.value().is_some());
167 }
168
169 #[test]
170 fn output_bounded() {
171 let prices: Vec<f64> = (0..200)
172 .map(|i| 100.0 + (f64::from(i) * 0.3).cos() * 5.0)
173 .collect();
174 let mut sw = SineWave::new();
175 for v in sw.batch(&prices).into_iter().flatten() {
176 assert!((-1.0..=1.0).contains(&v), "sine out of bounds: {v}");
177 }
178 assert!(sw.lead() >= -1.0 && sw.lead() <= 1.0);
180 }
181
182 #[test]
183 fn batch_equals_streaming() {
184 let prices: Vec<f64> = (0..200)
185 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
186 .collect();
187 let mut a = SineWave::new();
188 let mut b = SineWave::new();
189 let batch = a.batch(&prices);
190 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
191 assert_eq!(batch, streamed);
192 }
193
194 #[test]
195 fn ignores_non_finite_input() {
196 let mut sw = SineWave::new();
197 let prices: Vec<f64> = (0..120)
198 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
199 .collect();
200 sw.batch(&prices);
201 let before = sw.value();
202 assert!(before.is_some());
203 assert_eq!(sw.update(f64::NAN), before);
204 }
205
206 #[test]
207 fn reset_clears_state() {
208 let mut sw = SineWave::new();
209 let prices: Vec<f64> = (0..120)
210 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
211 .collect();
212 sw.batch(&prices);
213 assert!(sw.is_ready());
214 sw.reset();
215 assert!(!sw.is_ready());
216 assert!(sw.value().is_none());
217 }
218
219 #[test]
220 fn flat_input_uses_phase_fallback() {
221 let mut sw = SineWave::new();
227 let _ = sw.batch(&[0.0_f64; 120]);
228 assert!(sw.value().is_some());
229 }
230}