wickra_core/indicators/
td_dwave.rs1#![allow(clippy::doc_markdown)]
2
3use std::collections::VecDeque;
6
7use crate::error::{Error, Result};
8use crate::ohlcv::Candle;
9use crate::traits::Indicator;
10
11#[derive(Debug, Clone)]
49pub struct TdDWave {
50 strength: usize,
51 window: VecDeque<Candle>,
52 last_is_high: Option<bool>,
53 last_extreme: f64,
54 wave: usize,
55 last_value: Option<f64>,
56}
57
58impl TdDWave {
59 pub fn new(strength: usize) -> Result<Self> {
65 if strength == 0 {
66 return Err(Error::PeriodZero);
67 }
68 if strength > crate::error::MAX_PERIOD {
69 return Err(Error::InvalidPeriod {
70 message: crate::error::PERIOD_ABOVE_MAX,
71 });
72 }
73 Ok(Self {
74 strength,
75 window: VecDeque::with_capacity(2 * strength + 1),
76 last_is_high: None,
77 last_extreme: 0.0,
78 wave: 0,
79 last_value: None,
80 })
81 }
82
83 pub const fn strength(&self) -> usize {
85 self.strength
86 }
87
88 pub const fn value(&self) -> Option<f64> {
90 self.last_value
91 }
92
93 fn advance(&mut self, is_high: bool, price: f64) {
94 match self.last_is_high {
95 Some(prev) if prev == is_high => {
96 let extends = if is_high {
98 price > self.last_extreme
99 } else {
100 price < self.last_extreme
101 };
102 if extends {
103 self.last_extreme = price;
104 }
105 }
106 _ => {
107 self.wave = self.wave % 8 + 1;
109 self.last_is_high = Some(is_high);
110 self.last_extreme = price;
111 self.last_value = Some(self.wave as f64);
112 }
113 }
114 }
115}
116
117impl Indicator for TdDWave {
118 type Input = Candle;
119 type Output = f64;
120
121 #[inline]
122 fn update(&mut self, candle: Candle) -> Option<f64> {
123 let span = 2 * self.strength + 1;
124 if self.window.len() == span {
125 self.window.pop_front();
126 }
127 self.window.push_back(candle);
128 if self.window.len() == span {
129 let center = self.window[self.strength];
130 let is_high = self
131 .window
132 .iter()
133 .enumerate()
134 .all(|(i, c)| i == self.strength || c.high < center.high);
135 let is_low = self
136 .window
137 .iter()
138 .enumerate()
139 .all(|(i, c)| i == self.strength || c.low > center.low);
140 if is_high && !is_low {
141 self.advance(true, center.high);
142 } else if is_low && !is_high {
143 self.advance(false, center.low);
144 }
145 }
146 self.last_value
147 }
148
149 fn reset(&mut self) {
150 self.window.clear();
151 self.last_is_high = None;
152 self.last_extreme = 0.0;
153 self.wave = 0;
154 self.last_value = None;
155 }
156
157 #[inline]
158 fn warmup_period(&self) -> usize {
159 2 * self.strength + 1
160 }
161
162 #[inline]
163 fn is_ready(&self) -> bool {
164 self.last_value.is_some()
165 }
166
167 #[inline]
168 fn name(&self) -> &'static str {
169 "TDDWave"
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::traits::BatchExt;
177
178 fn c(high: f64, low: f64) -> Candle {
179 Candle::new_unchecked(
180 f64::midpoint(high, low),
181 high,
182 low,
183 f64::midpoint(high, low),
184 1_000.0,
185 0,
186 )
187 }
188
189 fn zigzag() -> Vec<Candle> {
190 (0..200)
191 .map(|i| {
192 let base = 100.0 + (f64::from(i) * 0.5).sin() * 10.0;
193 c(base + 1.0, base - 1.0)
194 })
195 .collect()
196 }
197
198 #[test]
199 fn rejects_zero_strength() {
200 assert!(matches!(TdDWave::new(0), Err(Error::PeriodZero)));
201 }
202
203 #[test]
204 fn accessors_and_metadata() {
205 let td = TdDWave::new(2).unwrap();
206 assert_eq!(td.strength(), 2);
207 assert_eq!(td.warmup_period(), 5);
208 assert_eq!(td.name(), "TDDWave");
209 assert!(!td.is_ready());
210 assert_eq!(td.value(), None);
211 }
212
213 #[test]
214 fn counts_waves_on_swings() {
215 let mut td = TdDWave::new(2).unwrap();
216 let out = td.batch(&zigzag());
217 assert!(out.iter().any(Option::is_some));
218 assert!(td.is_ready());
219 }
220
221 #[test]
222 fn same_direction_pivots_extend_one_leg() {
223 let mut td = TdDWave::new(1).unwrap();
228 let bars = [
229 (10.0, 100.0),
230 (20.0, 99.0),
231 (12.0, 98.0),
232 (30.0, 97.0),
233 (15.0, 96.0),
234 (25.0, 95.0),
235 (14.0, 94.0),
236 (14.0, 93.0),
237 ];
238 let vals: Vec<f64> = bars
239 .iter()
240 .filter_map(|&(high, low)| td.update(c(high, low)))
241 .collect();
242 assert!(!vals.is_empty());
243 assert!(vals.iter().all(|&v| v == 1.0));
244 }
245
246 #[test]
247 fn same_direction_low_pivots_extend_one_leg() {
248 let mut td = TdDWave::new(1).unwrap();
252 let bars = [
253 (100.0, 10.0),
254 (101.0, 5.0),
255 (102.0, 8.0),
256 (103.0, 2.0),
257 (104.0, 6.0),
258 (105.0, 4.0),
259 (106.0, 7.0),
260 (107.0, 7.0),
261 ];
262 let vals: Vec<f64> = bars
263 .iter()
264 .filter_map(|&(high, low)| td.update(c(high, low)))
265 .collect();
266 assert!(!vals.is_empty());
267 assert!(vals.iter().all(|&v| v == 1.0));
268 }
269
270 #[test]
271 fn wave_stays_in_one_to_eight() {
272 let mut td = TdDWave::new(2).unwrap();
273 for v in td.batch(&zigzag()).into_iter().flatten() {
274 assert!((1.0..=8.0).contains(&v), "wave out of range: {v}");
275 }
276 }
277
278 #[test]
279 fn flat_input_never_counts() {
280 let mut td = TdDWave::new(2).unwrap();
282 let candles: Vec<Candle> = (0..40).map(|_| c(100.0, 100.0)).collect();
283 assert!(td.batch(&candles).iter().all(Option::is_none));
284 }
285
286 #[test]
287 fn reset_clears_state() {
288 let mut td = TdDWave::new(2).unwrap();
289 td.batch(&zigzag());
290 assert!(td.is_ready());
291 td.reset();
292 assert!(!td.is_ready());
293 assert_eq!(td.value(), None);
294 }
295
296 #[test]
297 fn batch_equals_streaming() {
298 let candles = zigzag();
299 let batch = TdDWave::new(2).unwrap().batch(&candles);
300 let mut b = TdDWave::new(2).unwrap();
301 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
302 assert_eq!(batch, streamed);
303 }
304}