1#![allow(clippy::doc_markdown)]
2
3use std::collections::VecDeque;
34
35use crate::error::{Error, Result};
36use crate::ohlcv::Candle;
37use crate::traits::Indicator;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41enum Direction {
42 None,
43 Buy,
44 Sell,
45}
46
47#[derive(Debug, Clone)]
64pub struct TdCountdown {
65 setup_lookback: usize,
66 setup_target: usize,
67 countdown_lookback: usize,
68 countdown_target: usize,
69 candles: VecDeque<Candle>,
70 buy_setup: usize,
71 sell_setup: usize,
72 buy_countdown: usize,
73 sell_countdown: usize,
74 qualifier_close: f64,
77 direction: Direction,
78 ready: bool,
79}
80
81impl TdCountdown {
82 pub fn new(
90 setup_lookback: usize,
91 setup_target: usize,
92 countdown_lookback: usize,
93 countdown_target: usize,
94 ) -> Result<Self> {
95 if setup_lookback == 0
96 || setup_target == 0
97 || countdown_lookback == 0
98 || countdown_target == 0
99 {
100 return Err(Error::PeriodZero);
101 }
102 let cap = setup_lookback.max(countdown_lookback) + 1;
103 Ok(Self {
104 setup_lookback,
105 setup_target,
106 countdown_lookback,
107 countdown_target,
108 candles: VecDeque::with_capacity(cap),
109 buy_setup: 0,
110 sell_setup: 0,
111 buy_countdown: 0,
112 sell_countdown: 0,
113 qualifier_close: f64::NAN,
114 direction: Direction::None,
115 ready: false,
116 })
117 }
118
119 pub fn classic() -> Self {
122 Self::new(4, 9, 2, 13).expect("classic TD Countdown parameters are valid")
123 }
124
125 pub const fn params(&self) -> (usize, usize, usize, usize) {
128 (
129 self.setup_lookback,
130 self.setup_target,
131 self.countdown_lookback,
132 self.countdown_target,
133 )
134 }
135}
136
137impl Indicator for TdCountdown {
138 type Input = Candle;
139 type Output = f64;
140
141 fn update(&mut self, candle: Candle) -> Option<f64> {
142 let need = self.setup_lookback.max(self.countdown_lookback);
143 let cap = need + 1;
144 if self.candles.len() == cap {
145 self.candles.pop_front();
146 }
147 if self.candles.len() < need {
148 self.candles.push_back(candle);
149 return None;
150 }
151
152 let setup_ref_idx = need - self.setup_lookback;
154 let setup_ref_close = self.candles[setup_ref_idx].close;
155 if candle.close < setup_ref_close {
156 self.buy_setup = (self.buy_setup + 1).min(self.setup_target);
157 self.sell_setup = 0;
158 } else if candle.close > setup_ref_close {
159 self.sell_setup = (self.sell_setup + 1).min(self.setup_target);
160 self.buy_setup = 0;
161 } else {
162 self.buy_setup = 0;
163 self.sell_setup = 0;
164 }
165
166 if self.buy_setup == self.setup_target {
167 if self.direction != Direction::Buy {
168 self.buy_countdown = 0;
169 self.sell_countdown = 0;
170 self.qualifier_close = f64::NAN;
171 }
172 self.direction = Direction::Buy;
173 } else if self.sell_setup == self.setup_target {
174 if self.direction != Direction::Sell {
175 self.buy_countdown = 0;
176 self.sell_countdown = 0;
177 self.qualifier_close = f64::NAN;
178 }
179 self.direction = Direction::Sell;
180 }
181
182 let cd_ref = self.candles[need - self.countdown_lookback];
183 match self.direction {
184 Direction::Buy => {
185 if candle.close <= cd_ref.low && self.buy_countdown < self.countdown_target {
186 let next = self.buy_countdown + 1;
189 if next < self.countdown_target
190 || (self.qualifier_close.is_nan() || candle.low <= self.qualifier_close)
191 {
192 self.buy_countdown = next;
193 if next + 5 == self.countdown_target {
194 self.qualifier_close = candle.close;
195 }
196 }
197 }
198 }
199 Direction::Sell => {
200 if candle.close >= cd_ref.high && self.sell_countdown < self.countdown_target {
201 let next = self.sell_countdown + 1;
204 if next < self.countdown_target
205 || (self.qualifier_close.is_nan() || candle.high >= self.qualifier_close)
206 {
207 self.sell_countdown = next;
208 if next + 5 == self.countdown_target {
209 self.qualifier_close = candle.close;
210 }
211 }
212 }
213 }
214 Direction::None => {}
215 }
216
217 self.candles.push_back(candle);
218 self.ready = true;
219
220 let v = match self.direction {
221 Direction::Buy => self.buy_countdown as f64,
222 Direction::Sell => -(self.sell_countdown as f64),
223 Direction::None => 0.0,
224 };
225 Some(v)
226 }
227
228 fn reset(&mut self) {
229 self.candles.clear();
230 self.buy_setup = 0;
231 self.sell_setup = 0;
232 self.buy_countdown = 0;
233 self.sell_countdown = 0;
234 self.qualifier_close = f64::NAN;
235 self.direction = Direction::None;
236 self.ready = false;
237 }
238
239 #[inline]
240 fn warmup_period(&self) -> usize {
241 self.setup_lookback.max(self.countdown_lookback) + 1
242 }
243
244 #[inline]
245 fn is_ready(&self) -> bool {
246 self.ready
247 }
248
249 #[inline]
250 fn name(&self) -> &'static str {
251 "TDCountdown"
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::traits::BatchExt;
259
260 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
261 Candle::new_unchecked(close, high, low, close, 0.0, ts)
262 }
263
264 #[test]
265 fn pure_uptrend_completes_setup_then_runs_sell_countdown_to_minus_13() {
266 let candles: Vec<Candle> = (1..=40)
267 .map(|i| {
268 c(
269 f64::from(i) + 0.5,
270 f64::from(i) - 0.5,
271 f64::from(i),
272 i64::from(i),
273 )
274 })
275 .collect();
276 let mut td = TdCountdown::classic();
277 let out = td.batch(&candles);
278 for v in out.iter().take(4) {
280 assert!(v.is_none());
281 }
282 assert_eq!(out[12].expect("ready"), -1.0);
286 assert_eq!(out[30].expect("ready"), -13.0);
288 }
289
290 #[test]
291 fn pure_downtrend_completes_setup_then_runs_buy_countdown_to_plus_13() {
292 let candles: Vec<Candle> = (1..=40)
293 .rev()
294 .enumerate()
295 .map(|(k, i)| {
296 c(
297 f64::from(i) + 0.5,
298 f64::from(i) - 0.5,
299 f64::from(i),
300 i64::try_from(k).unwrap(),
301 )
302 })
303 .collect();
304 let mut td = TdCountdown::classic();
305 let out = td.batch(&candles);
306 for v in out.iter().take(4) {
307 assert!(v.is_none());
308 }
309 assert_eq!(out[12].expect("ready"), 1.0);
313 assert_eq!(out[30].expect("ready"), 13.0);
315 }
316
317 #[test]
318 fn flat_series_never_arms_countdown() {
319 let candles: Vec<Candle> = (0..30).map(|i| c(10.5, 9.5, 10.0, i64::from(i))).collect();
320 let mut td = TdCountdown::classic();
321 for v in td.batch(&candles).into_iter().flatten() {
322 assert_eq!(v, 0.0);
323 }
324 }
325
326 #[test]
327 fn batch_equals_streaming() {
328 let candles: Vec<Candle> = (0..80)
329 .map(|i| {
330 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
331 c(m + 1.0, m - 1.0, m, i64::from(i))
332 })
333 .collect();
334 let mut a = TdCountdown::classic();
335 let mut b = TdCountdown::classic();
336 assert_eq!(
337 a.batch(&candles),
338 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
339 );
340 }
341
342 #[test]
343 fn rejects_invalid_params() {
344 assert!(matches!(
345 TdCountdown::new(0, 9, 2, 13),
346 Err(Error::PeriodZero)
347 ));
348 assert!(matches!(
349 TdCountdown::new(4, 0, 2, 13),
350 Err(Error::PeriodZero)
351 ));
352 assert!(matches!(
353 TdCountdown::new(4, 9, 0, 13),
354 Err(Error::PeriodZero)
355 ));
356 assert!(matches!(
357 TdCountdown::new(4, 9, 2, 0),
358 Err(Error::PeriodZero)
359 ));
360 }
361
362 #[test]
363 fn reset_clears_state() {
364 let candles: Vec<Candle> = (1..=30)
365 .map(|i| {
366 c(
367 f64::from(i) + 0.5,
368 f64::from(i) - 0.5,
369 f64::from(i),
370 i64::from(i),
371 )
372 })
373 .collect();
374 let mut td = TdCountdown::classic();
375 td.batch(&candles);
376 assert!(td.is_ready());
377 td.reset();
378 assert!(!td.is_ready());
379 assert_eq!(td.update(candles[0]), None);
380 }
381
382 #[test]
383 fn accessors_and_metadata() {
384 let td = TdCountdown::classic();
385 assert_eq!(td.params(), (4, 9, 2, 13));
386 assert_eq!(td.warmup_period(), 5);
387 assert_eq!(td.name(), "TDCountdown");
388 }
389
390 fn from_closes(closes: &[f64]) -> Vec<Candle> {
392 closes
393 .iter()
394 .enumerate()
395 .map(|(k, &m)| c(m + 0.5, m - 0.5, m, i64::try_from(k).unwrap()))
396 .collect()
397 }
398
399 fn buy_deferral_closes() -> Vec<f64> {
407 let mut closes: Vec<f64> = (77..=100).rev().map(f64::from).collect();
408 closes.extend([90.0, 95.0, 95.0, 89.0, 80.0]);
409 closes
410 }
411
412 fn sell_deferral_closes() -> Vec<f64> {
416 buy_deferral_closes().iter().map(|x| 200.0 - x).collect()
417 }
418
419 #[test]
420 fn buy_bar_13_is_deferred_until_low_reaches_bar_8_close() {
421 let mut td = TdCountdown::classic();
422 let out = td.batch(&from_closes(&buy_deferral_closes()));
423 assert_eq!(out[19], Some(8.0));
424 assert_eq!(out[23], Some(12.0));
425 assert!(out[24..28].iter().all(|v| *v == Some(12.0)));
427 assert_eq!(out[28], Some(13.0));
428 }
429
430 #[test]
431 fn sell_bar_13_is_deferred_until_high_reaches_bar_8_close() {
432 let mut td = TdCountdown::classic();
433 let out = td.batch(&from_closes(&sell_deferral_closes()));
434 assert_eq!(out[19], Some(-8.0));
435 assert_eq!(out[23], Some(-12.0));
436 assert!(out[24..28].iter().all(|v| *v == Some(-12.0)));
437 assert_eq!(out[28], Some(-13.0));
438 }
439
440 #[test]
441 fn qualifier_close_is_bar_8_close() {
442 let mut td = TdCountdown::classic();
443 let candles = from_closes(&buy_deferral_closes());
444 for candle in &candles[..19] {
445 td.update(*candle);
446 }
447 assert!(td.qualifier_close.is_nan());
448 td.update(candles[19]);
449 assert_eq!(td.qualifier_close.to_bits(), 81.0_f64.to_bits());
450 }
451
452 #[test]
453 fn short_target_has_no_qualifier() {
454 let closes: Vec<f64> = (70..=100).rev().map(f64::from).collect();
457 let candles = from_closes(&closes);
458 let mut buy = TdCountdown::new(4, 9, 2, 3).unwrap();
459 let out = buy.batch(&candles);
460 assert_eq!(out[12], Some(1.0));
461 assert_eq!(out[14], Some(3.0));
462 assert_eq!(out[30], Some(3.0));
463 assert!(buy.qualifier_close.is_nan());
464
465 let rising: Vec<f64> = closes.iter().map(|x| 200.0 - x).collect();
466 let mut sell = TdCountdown::new(4, 9, 2, 3).unwrap();
467 let out = sell.batch(&from_closes(&rising));
468 assert_eq!(out[14], Some(-3.0));
469 assert_eq!(out[30], Some(-3.0));
470 assert!(sell.qualifier_close.is_nan());
471 }
472
473 #[test]
474 fn opposite_setup_invalidates_and_clears_qualifier() {
475 let mut closes: Vec<f64> = (77..=100).rev().map(f64::from).collect();
480 closes.extend((78..=120).map(f64::from));
481 let candles = from_closes(&closes);
482 let mut td = TdCountdown::classic();
483 let out: Vec<Option<f64>> = candles.iter().map(|x| td.update(*x)).collect();
484 assert_eq!(out[23], Some(12.0));
485 assert_eq!(out[33], Some(12.0));
486 assert_eq!(out[34], Some(-1.0));
489 assert_eq!(out[46], Some(-13.0));
490
491 let mut probe = TdCountdown::classic();
492 for candle in &candles[..34] {
493 probe.update(*candle);
494 }
495 assert_eq!(probe.qualifier_close.to_bits(), 81.0_f64.to_bits());
496 probe.update(candles[34]);
497 assert!(probe.qualifier_close.is_nan());
498
499 let mut back = closes.clone();
501 back.extend((60..=119).rev().map(f64::from));
502 let mut td2 = TdCountdown::classic();
503 let out2 = td2.batch(&from_closes(&back));
504 let last = out2.last().copied().flatten().unwrap();
505 assert_eq!(last.to_bits(), 13.0_f64.to_bits());
506 }
507
508 #[test]
509 fn first_value_lands_at_warmup_minus_one() {
510 let candles = from_closes(&buy_deferral_closes());
511 for (sl, cl) in [(4, 2), (2, 6), (1, 1)] {
512 let mut td = TdCountdown::new(sl, 9, cl, 13).unwrap();
513 let warm = td.warmup_period();
514 let out = td.batch(&candles);
515 assert!(out[..warm - 1].iter().all(Option::is_none));
516 assert!(out[warm - 1].is_some());
517 }
518 }
519
520 #[test]
521 fn reset_reproduces_fresh_run() {
522 let candles = from_closes(&sell_deferral_closes());
523 let mut fresh = TdCountdown::classic();
524 let expected = fresh.batch(&candles);
525 let mut td = TdCountdown::classic();
526 td.batch(&from_closes(&buy_deferral_closes()));
527 td.reset();
528 assert!(td.qualifier_close.is_nan());
529 assert_eq!(td.batch(&candles), expected);
530 }
531
532 #[test]
533 fn batch_nan_into_matches_streaming() {
534 let candles = from_closes(&buy_deferral_closes());
535 let mut a = TdCountdown::classic();
536 let mut out = vec![0.0; candles.len()];
537 a.batch_nan_into(&candles, &mut out);
538 let mut b = TdCountdown::classic();
539 let streamed: Vec<f64> = candles
540 .iter()
541 .map(|x| b.update(*x).unwrap_or(f64::NAN))
542 .collect();
543 assert!(out
544 .iter()
545 .zip(&streamed)
546 .all(|(x, y)| x.to_bits() == y.to_bits()));
547 }
548}