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 fn update(&mut self, candle: Candle) -> Option<WaveTrendOutput> {
118 let ap = (candle.high + candle.low + candle.close) / 3.0;
119
120 let esa = self.esa.update(ap)?;
123
124 let d = self.dev_ema.update((ap - esa).abs())?;
126
127 let flat_tol = esa.abs().max(1.0) * 16.0 * f64::EPSILON;
134 let ci = if d <= flat_tol {
135 0.0
136 } else {
137 (ap - esa) / (0.015 * d)
138 };
139
140 let wt1 = self.tci.update(ci)?;
142
143 let wt2 = self.signal.update(wt1)?;
145
146 let out = WaveTrendOutput { wt1, wt2 };
147 self.last = Some(out);
148 Some(out)
149 }
150
151 fn reset(&mut self) {
152 self.esa.reset();
153 self.dev_ema.reset();
154 self.tci.reset();
155 self.signal.reset();
156 self.last = None;
157 }
158
159 fn warmup_period(&self) -> usize {
160 2 * self.channel_period + self.average_period + self.signal_period - 3
182 }
183
184 fn is_ready(&self) -> bool {
185 self.last.is_some()
186 }
187
188 fn name(&self) -> &'static str {
189 "WaveTrend"
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::traits::BatchExt;
197
198 fn candle(h: f64, l: f64, c: f64, ts: i64) -> Candle {
199 Candle::new(c, h, l, c, 1.0, ts).unwrap()
200 }
201
202 #[test]
203 fn rejects_zero_period() {
204 assert!(matches!(WaveTrend::new(0, 21, 4), Err(Error::PeriodZero)));
205 assert!(matches!(WaveTrend::new(10, 0, 4), Err(Error::PeriodZero)));
206 assert!(matches!(WaveTrend::new(10, 21, 0), Err(Error::PeriodZero)));
207 }
208
209 #[test]
210 fn accessors_and_metadata() {
211 let mut w = WaveTrend::classic().unwrap();
212 assert_eq!(w.periods(), (10, 21, 4));
213 assert_eq!(w.name(), "WaveTrend");
214 assert_eq!(w.warmup_period(), 42);
216 assert!(w.value().is_none());
217 let candles: Vec<Candle> = (0..80_i64)
218 .map(|i| {
219 let p = 100.0 + ((i as f64) * 0.3).sin() * 5.0;
220 candle(p + 1.0, p - 1.0, p, i)
221 })
222 .collect();
223 for c in &candles {
224 w.update(*c);
225 }
226 assert!(w.value().is_some());
227 }
228
229 #[test]
230 fn first_emission_at_warmup_period() {
231 let candles: Vec<Candle> = (0..60_i64)
232 .map(|i| {
233 let p = 100.0 + ((i as f64) * 0.25).sin() * 6.0;
234 candle(p + 1.0, p - 1.0, p, i)
235 })
236 .collect();
237 let mut w = WaveTrend::new(5, 8, 3).unwrap();
238 let warmup = 2 * 5 + 8 + 3 - 3; assert_eq!(w.warmup_period(), warmup);
240 let out = w.batch(&candles);
241 for v in out.iter().take(warmup - 1) {
242 assert!(v.is_none());
243 }
244 assert!(out[warmup - 1].is_some());
245 }
246
247 #[test]
248 fn constant_series_yields_zero_lines() {
249 let candles: Vec<Candle> = (0..80_i64).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
252 let mut w = WaveTrend::new(5, 8, 3).unwrap();
253 let last = w.batch(&candles).into_iter().flatten().last().unwrap();
254 assert_eq!(last.wt1, 0.0);
255 assert_eq!(last.wt2, 0.0);
256 }
257
258 #[test]
259 fn pure_uptrend_is_positive() {
260 let candles: Vec<Candle> = (0..120_i64)
261 .map(|i| {
262 let base = 100.0 + (i as f64) * 0.5;
263 candle(base + 1.0, base - 0.5, base + 0.5, i)
264 })
265 .collect();
266 let mut w = WaveTrend::classic().unwrap();
267 let last = w.batch(&candles).into_iter().flatten().last().unwrap();
268 assert!(
269 last.wt1 > 0.0,
270 "uptrend wt1 should be positive, got {}",
271 last.wt1
272 );
273 assert!(
274 last.wt2 > 0.0,
275 "uptrend wt2 should be positive, got {}",
276 last.wt2
277 );
278 }
279
280 #[test]
281 fn pure_downtrend_is_negative() {
282 let candles: Vec<Candle> = (0..120_i64)
283 .map(|i| {
284 let base = 200.0 - (i as f64) * 0.5;
285 candle(base + 1.0, base - 0.5, base - 0.5, i)
286 })
287 .collect();
288 let mut w = WaveTrend::classic().unwrap();
289 let last = w.batch(&candles).into_iter().flatten().last().unwrap();
290 assert!(last.wt1 < 0.0);
291 assert!(last.wt2 < 0.0);
292 }
293
294 #[test]
295 fn outputs_remain_finite() {
296 let candles: Vec<Candle> = (0..200_i64)
297 .map(|i| {
298 let p = 100.0 + ((i as f64) * 0.3).sin() * 8.0;
299 candle(p + 2.0, p - 2.0, p, i)
300 })
301 .collect();
302 let mut w = WaveTrend::classic().unwrap();
303 for v in w.batch(&candles).into_iter().flatten() {
304 assert!(v.wt1.is_finite() && v.wt2.is_finite());
305 }
306 }
307
308 #[test]
309 fn batch_equals_streaming() {
310 let candles: Vec<Candle> = (0..120_i64)
311 .map(|i| {
312 let p = 100.0 + ((i as f64) * 0.27).sin() * 6.0;
313 candle(p + 1.5, p - 1.5, p, i)
314 })
315 .collect();
316 let mut a = WaveTrend::classic().unwrap();
317 let mut b = WaveTrend::classic().unwrap();
318 assert_eq!(
319 a.batch(&candles),
320 candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
321 );
322 }
323
324 #[test]
325 fn reset_clears_state() {
326 let candles: Vec<Candle> = (0..80_i64).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
327 let mut w = WaveTrend::classic().unwrap();
328 w.batch(&candles);
329 assert!(w.is_ready());
330 w.reset();
331 assert!(!w.is_ready());
332 assert_eq!(w.update(candles[0]), None);
333 }
334}