wickra_core/indicators/
wave_pm.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::ema::Ema;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
47pub struct WavePm {
48 length: usize,
49 smoothing: usize,
50 closes: VecDeque<f64>,
51 energy_ema: Ema,
52 smooth_ema: Ema,
53}
54
55impl WavePm {
56 pub fn new(length: usize, smoothing: usize) -> Result<Self> {
63 if length == 0 {
64 return Err(Error::PeriodZero);
65 }
66 if length > crate::error::MAX_PERIOD {
67 return Err(Error::InvalidPeriod {
68 message: crate::error::PERIOD_ABOVE_MAX,
69 });
70 }
71 Ok(Self {
72 length,
73 smoothing,
74 closes: VecDeque::with_capacity(length + 1),
75 energy_ema: Ema::new(length)?,
76 smooth_ema: Ema::new(smoothing)?,
77 })
78 }
79
80 pub const fn periods(&self) -> (usize, usize) {
82 (self.length, self.smoothing)
83 }
84}
85
86impl Indicator for WavePm {
87 type Input = f64;
88 type Output = f64;
89
90 #[inline]
91 fn update(&mut self, close: f64) -> Option<f64> {
92 if !close.is_finite() {
93 return None;
96 }
97 self.closes.push_back(close);
98 if self.closes.len() > self.length + 1 {
99 self.closes.pop_front();
100 }
101 if self.closes.len() <= self.length {
102 return None;
103 }
104
105 let oldest = *self.closes.front().unwrap_or(&close);
106 let momentum = close - oldest;
107 let energy = self.energy_ema.update(momentum * momentum)?;
108 let raw = if energy <= 0.0 {
109 0.0
110 } else {
111 1.0 - (-(momentum * momentum) / (2.0 * energy)).exp()
112 };
113 self.smooth_ema.update(raw).map(|v| v * 100.0)
114 }
115
116 fn reset(&mut self) {
117 self.closes.clear();
118 self.energy_ema.reset();
119 self.smooth_ema.reset();
120 }
121
122 #[inline]
123 fn warmup_period(&self) -> usize {
124 2 * self.length + self.smoothing - 1
125 }
126
127 #[inline]
128 fn is_ready(&self) -> bool {
129 self.smooth_ema.is_ready()
130 }
131
132 #[inline]
133 fn name(&self) -> &'static str {
134 "WavePm"
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::traits::BatchExt;
142 use approx::assert_relative_eq;
143
144 #[test]
145 fn rejects_zero_period() {
146 assert!(matches!(WavePm::new(0, 3), Err(Error::PeriodZero)));
147 assert!(matches!(WavePm::new(10, 0), Err(Error::PeriodZero)));
148 }
149
150 #[test]
151 fn accessors_and_metadata() {
152 let w = WavePm::new(10, 3).unwrap();
153 assert_eq!(w.periods(), (10, 3));
154 assert_eq!(w.warmup_period(), 22);
156 assert_eq!(w.name(), "WavePm");
157 assert!(!w.is_ready());
158 }
159
160 #[test]
161 fn warmup_emits_at_expected_bar() {
162 let mut w = WavePm::new(3, 2).unwrap();
163 let inputs: Vec<f64> = (0..12).map(f64::from).collect();
165 let out = w.batch(&inputs);
166 assert!(out[5].is_none());
167 assert!(out[6].is_some());
168 }
169
170 #[test]
171 fn flat_market_reads_zero() {
172 let mut w = WavePm::new(4, 2).unwrap();
173 let inputs = [50.0; 20];
174 let last = w.batch(&inputs).last().unwrap().unwrap();
175 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
176 }
177
178 #[test]
179 fn steady_trend_reads_baseline() {
180 let mut w = WavePm::new(10, 3).unwrap();
183 let inputs: Vec<f64> = (0..60).map(|i| f64::from(i) * 5.0).collect();
184 let last = w.batch(&inputs).last().unwrap().unwrap();
185 let baseline = 100.0 * (1.0 - (-0.5_f64).exp());
186 assert_relative_eq!(last, baseline, epsilon = 1e-9);
187 }
188
189 #[test]
190 fn acceleration_reads_above_baseline() {
191 let mut w = WavePm::new(10, 3).unwrap();
194 let inputs: Vec<f64> = (0..60).map(|i| f64::from(i * i) * 0.1).collect();
195 let last = w.batch(&inputs).last().unwrap().unwrap();
196 let baseline = 100.0 * (1.0 - (-0.5_f64).exp());
197 assert!(
198 last > baseline,
199 "accelerating wpm {last} should exceed {baseline}"
200 );
201 assert!(last <= 100.0, "wpm {last} must stay <= 100");
202 }
203
204 #[test]
205 fn reset_clears_state() {
206 let mut w = WavePm::new(10, 3).unwrap();
207 let inputs: Vec<f64> = (0..60).map(|i| f64::from(i) * 5.0).collect();
208 w.batch(&inputs);
209 assert!(w.is_ready());
210 w.reset();
211 assert!(!w.is_ready());
212 }
213
214 #[test]
215 fn batch_equals_streaming() {
216 let inputs: Vec<f64> = (0..80)
217 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
218 .collect();
219 let mut a = WavePm::new(10, 3).unwrap();
220 let mut b = WavePm::new(10, 3).unwrap();
221 assert_eq!(
222 a.batch(&inputs),
223 inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
224 );
225 }
226}