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