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