1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::indicators::sma::Sma;
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct WaveTrendOutput {
13 pub wt1: f64,
15 pub wt2: f64,
17}
18
19#[derive(Debug, Clone)]
60pub struct WaveTrend {
61 channel_period: usize,
62 average_period: usize,
63 signal_period: usize,
64 esa: Ema,
65 dev_ema: Ema,
66 tci: Ema,
67 signal: Sma,
68 last: Option<WaveTrendOutput>,
69}
70
71impl WaveTrend {
72 pub fn new(channel_period: usize, average_period: usize, signal_period: usize) -> Result<Self> {
78 if channel_period == 0 || average_period == 0 || signal_period == 0 {
79 return Err(Error::PeriodZero);
80 }
81 Ok(Self {
82 channel_period,
83 average_period,
84 signal_period,
85 esa: Ema::new(channel_period)?,
86 dev_ema: Ema::new(channel_period)?,
87 tci: Ema::new(average_period)?,
88 signal: Sma::new(signal_period)?,
89 last: None,
90 })
91 }
92
93 pub fn classic() -> Result<Self> {
99 Self::new(10, 21, 4)
100 }
101
102 pub const fn periods(&self) -> (usize, usize, usize) {
104 (self.channel_period, self.average_period, self.signal_period)
105 }
106
107 pub const fn value(&self) -> Option<WaveTrendOutput> {
109 self.last
110 }
111}
112
113impl Indicator for WaveTrend {
114 type Input = Candle;
115 type Output = WaveTrendOutput;
116
117 #[inline]
118 fn update(&mut self, candle: Candle) -> Option<WaveTrendOutput> {
119 let ap = (candle.high + candle.low + candle.close) / 3.0;
120
121 let esa = self.esa.update(ap)?;
124
125 let d = self.dev_ema.update((ap - esa).abs())?;
127
128 let flat_tol = esa.abs().max(1.0) * 16.0 * f64::EPSILON;
135 let ci = if d <= flat_tol {
136 0.0
137 } else {
138 (ap - esa) / (0.015 * d)
139 };
140
141 let wt1 = self.tci.update(ci)?;
143
144 let wt2 = self.signal.update(wt1)?;
146
147 let out = WaveTrendOutput { wt1, wt2 };
148 self.last = Some(out);
149 Some(out)
150 }
151
152 fn reset(&mut self) {
153 self.esa.reset();
154 self.dev_ema.reset();
155 self.tci.reset();
156 self.signal.reset();
157 self.last = None;
158 }
159
160 #[inline]
161 fn warmup_period(&self) -> usize {
162 2 * self.channel_period + self.average_period + self.signal_period - 3
184 }
185
186 #[inline]
187 fn is_ready(&self) -> bool {
188 self.last.is_some()
189 }
190
191 #[inline]
192 fn name(&self) -> &'static str {
193 "WaveTrend"
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::traits::BatchExt;
201
202 fn candle(h: f64, l: f64, c: f64, ts: i64) -> Candle {
203 Candle::new(c, h, l, c, 1.0, ts).unwrap()
204 }
205
206 #[test]
207 fn rejects_zero_period() {
208 assert!(matches!(WaveTrend::new(0, 21, 4), Err(Error::PeriodZero)));
209 assert!(matches!(WaveTrend::new(10, 0, 4), Err(Error::PeriodZero)));
210 assert!(matches!(WaveTrend::new(10, 21, 0), Err(Error::PeriodZero)));
211 }
212
213 #[test]
214 fn accessors_and_metadata() {
215 let mut w = WaveTrend::classic().unwrap();
216 assert_eq!(w.periods(), (10, 21, 4));
217 assert_eq!(w.name(), "WaveTrend");
218 assert_eq!(w.warmup_period(), 42);
220 assert!(w.value().is_none());
221 let candles: Vec<Candle> = (0..80_i64)
222 .map(|i| {
223 let p = 100.0 + ((i as f64) * 0.3).sin() * 5.0;
224 candle(p + 1.0, p - 1.0, p, i)
225 })
226 .collect();
227 for c in &candles {
228 w.update(*c);
229 }
230 assert!(w.value().is_some());
231 }
232
233 #[test]
234 fn first_emission_at_warmup_period() {
235 let candles: Vec<Candle> = (0..60_i64)
236 .map(|i| {
237 let p = 100.0 + ((i as f64) * 0.25).sin() * 6.0;
238 candle(p + 1.0, p - 1.0, p, i)
239 })
240 .collect();
241 let mut w = WaveTrend::new(5, 8, 3).unwrap();
242 let warmup = 2 * 5 + 8 + 3 - 3; assert_eq!(w.warmup_period(), warmup);
244 let out = w.batch(&candles);
245 for v in out.iter().take(warmup - 1) {
246 assert!(v.is_none());
247 }
248 assert!(out[warmup - 1].is_some());
249 }
250
251 #[test]
252 fn constant_series_yields_zero_lines() {
253 let candles: Vec<Candle> = (0..80_i64).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
256 let mut w = WaveTrend::new(5, 8, 3).unwrap();
257 let last = w.batch(&candles).into_iter().flatten().last().unwrap();
258 assert_eq!(last.wt1, 0.0);
259 assert_eq!(last.wt2, 0.0);
260 }
261
262 #[test]
263 fn pure_uptrend_is_positive() {
264 let candles: Vec<Candle> = (0..120_i64)
265 .map(|i| {
266 let base = 100.0 + (i as f64) * 0.5;
267 candle(base + 1.0, base - 0.5, base + 0.5, i)
268 })
269 .collect();
270 let mut w = WaveTrend::classic().unwrap();
271 let last = w.batch(&candles).into_iter().flatten().last().unwrap();
272 assert!(
273 last.wt1 > 0.0,
274 "uptrend wt1 should be positive, got {}",
275 last.wt1
276 );
277 assert!(
278 last.wt2 > 0.0,
279 "uptrend wt2 should be positive, got {}",
280 last.wt2
281 );
282 }
283
284 #[test]
285 fn pure_downtrend_is_negative() {
286 let candles: Vec<Candle> = (0..120_i64)
287 .map(|i| {
288 let base = 200.0 - (i as f64) * 0.5;
289 candle(base + 1.0, base - 0.5, base - 0.5, i)
290 })
291 .collect();
292 let mut w = WaveTrend::classic().unwrap();
293 let last = w.batch(&candles).into_iter().flatten().last().unwrap();
294 assert!(last.wt1 < 0.0);
295 assert!(last.wt2 < 0.0);
296 }
297
298 #[test]
299 fn outputs_remain_finite() {
300 let candles: Vec<Candle> = (0..200_i64)
301 .map(|i| {
302 let p = 100.0 + ((i as f64) * 0.3).sin() * 8.0;
303 candle(p + 2.0, p - 2.0, p, i)
304 })
305 .collect();
306 let mut w = WaveTrend::classic().unwrap();
307 for v in w.batch(&candles).into_iter().flatten() {
308 assert!(v.wt1.is_finite() && v.wt2.is_finite());
309 }
310 }
311
312 #[test]
313 fn batch_equals_streaming() {
314 let candles: Vec<Candle> = (0..120_i64)
315 .map(|i| {
316 let p = 100.0 + ((i as f64) * 0.27).sin() * 6.0;
317 candle(p + 1.5, p - 1.5, p, i)
318 })
319 .collect();
320 let mut a = WaveTrend::classic().unwrap();
321 let mut b = WaveTrend::classic().unwrap();
322 assert_eq!(
323 a.batch(&candles),
324 candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
325 );
326 }
327
328 #[test]
329 fn reset_clears_state() {
330 let candles: Vec<Candle> = (0..80_i64).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
331 let mut w = WaveTrend::classic().unwrap();
332 w.batch(&candles);
333 assert!(w.is_ready());
334 w.reset();
335 assert!(!w.is_ready());
336 assert_eq!(w.update(candles[0]), None);
337 }
338}