1#![allow(clippy::doc_markdown)]
2
3use std::collections::VecDeque;
32
33use crate::error::{Error, Result};
34use crate::ohlcv::Candle;
35use crate::traits::Indicator;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39enum Direction {
40 None,
41 Buy,
42 Sell,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct TdSequentialOutput {
49 pub setup: f64,
53 pub countdown: f64,
57 pub direction: f64,
60}
61
62#[derive(Debug, Clone)]
79pub struct TdSequential {
80 candles: VecDeque<Candle>,
84 setup_lookback: usize,
85 setup_target: usize,
86 countdown_lookback: usize,
87 countdown_target: usize,
88 buy_setup: usize,
89 sell_setup: usize,
90 buy_countdown: usize,
91 sell_countdown: usize,
92 qualifier_close: f64,
95 countdown_dir: Direction,
96 ready: bool,
97}
98
99impl TdSequential {
100 pub fn new(
108 setup_lookback: usize,
109 setup_target: usize,
110 countdown_lookback: usize,
111 countdown_target: usize,
112 ) -> Result<Self> {
113 if setup_lookback == 0
114 || setup_target == 0
115 || countdown_lookback == 0
116 || countdown_target == 0
117 {
118 return Err(Error::PeriodZero);
119 }
120 let cap = setup_lookback.max(countdown_lookback) + 1;
123 Ok(Self {
124 candles: VecDeque::with_capacity(cap),
125 setup_lookback,
126 setup_target,
127 countdown_lookback,
128 countdown_target,
129 buy_setup: 0,
130 sell_setup: 0,
131 buy_countdown: 0,
132 sell_countdown: 0,
133 qualifier_close: f64::NAN,
134 countdown_dir: Direction::None,
135 ready: false,
136 })
137 }
138
139 pub fn classic() -> Self {
142 Self::new(4, 9, 2, 13).expect("classic TD Sequential parameters are valid")
143 }
144
145 pub const fn params(&self) -> (usize, usize, usize, usize) {
148 (
149 self.setup_lookback,
150 self.setup_target,
151 self.countdown_lookback,
152 self.countdown_target,
153 )
154 }
155}
156
157impl Indicator for TdSequential {
158 type Input = Candle;
159 type Output = TdSequentialOutput;
160
161 fn update(&mut self, candle: Candle) -> Option<TdSequentialOutput> {
162 let cap = self.setup_lookback.max(self.countdown_lookback) + 1;
163 if self.candles.len() == cap {
164 self.candles.pop_front();
165 }
166 let need = self.setup_lookback.max(self.countdown_lookback);
170 if self.candles.len() < need {
171 self.candles.push_back(candle);
172 return None;
173 }
174
175 let setup_ref_idx = need - self.setup_lookback;
180 let setup_ref_close = self.candles[setup_ref_idx].close;
181
182 if candle.close < setup_ref_close {
183 self.buy_setup = (self.buy_setup + 1).min(self.setup_target);
184 self.sell_setup = 0;
185 } else if candle.close > setup_ref_close {
186 self.sell_setup = (self.sell_setup + 1).min(self.setup_target);
187 self.buy_setup = 0;
188 } else {
189 self.buy_setup = 0;
190 self.sell_setup = 0;
191 }
192
193 if self.buy_setup == self.setup_target {
197 if self.countdown_dir != Direction::Buy {
198 self.buy_countdown = 0;
199 self.sell_countdown = 0;
200 self.qualifier_close = f64::NAN;
201 }
202 self.countdown_dir = Direction::Buy;
203 } else if self.sell_setup == self.setup_target {
204 if self.countdown_dir != Direction::Sell {
205 self.buy_countdown = 0;
206 self.sell_countdown = 0;
207 self.qualifier_close = f64::NAN;
208 }
209 self.countdown_dir = Direction::Sell;
210 }
211
212 let cd_ref_idx = need - self.countdown_lookback;
218 let cd_ref = &self.candles[cd_ref_idx];
219 match self.countdown_dir {
220 Direction::Buy => {
221 if candle.close <= cd_ref.low && self.buy_countdown < self.countdown_target {
222 let next = self.buy_countdown + 1;
225 if next < self.countdown_target
226 || (self.qualifier_close.is_nan() || candle.low <= self.qualifier_close)
227 {
228 self.buy_countdown = next;
229 if next + 5 == self.countdown_target {
230 self.qualifier_close = candle.close;
231 }
232 }
233 }
234 }
235 Direction::Sell => {
236 if candle.close >= cd_ref.high && self.sell_countdown < self.countdown_target {
237 let next = self.sell_countdown + 1;
240 if next < self.countdown_target
241 || (self.qualifier_close.is_nan() || candle.high >= self.qualifier_close)
242 {
243 self.sell_countdown = next;
244 if next + 5 == self.countdown_target {
245 self.qualifier_close = candle.close;
246 }
247 }
248 }
249 }
250 Direction::None => {}
251 }
252
253 self.candles.push_back(candle);
254 self.ready = true;
255
256 let setup = if self.buy_setup > 0 {
257 self.buy_setup as f64
258 } else if self.sell_setup > 0 {
259 -(self.sell_setup as f64)
260 } else {
261 0.0
262 };
263 let (countdown, direction) = match self.countdown_dir {
264 Direction::Buy => (self.buy_countdown as f64, 1.0),
265 Direction::Sell => (-(self.sell_countdown as f64), -1.0),
266 Direction::None => (0.0, 0.0),
267 };
268
269 Some(TdSequentialOutput {
270 setup,
271 countdown,
272 direction,
273 })
274 }
275
276 fn reset(&mut self) {
277 self.candles.clear();
278 self.buy_setup = 0;
279 self.sell_setup = 0;
280 self.buy_countdown = 0;
281 self.sell_countdown = 0;
282 self.qualifier_close = f64::NAN;
283 self.countdown_dir = Direction::None;
284 self.ready = false;
285 }
286
287 #[inline]
288 fn warmup_period(&self) -> usize {
289 self.setup_lookback.max(self.countdown_lookback) + 1
290 }
291
292 #[inline]
293 fn is_ready(&self) -> bool {
294 self.ready
295 }
296
297 #[inline]
298 fn name(&self) -> &'static str {
299 "TDSequential"
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::traits::BatchExt;
307
308 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
309 Candle::new_unchecked(close, high, low, close, 0.0, ts)
310 }
311
312 #[test]
313 fn pure_uptrend_completes_sell_setup_then_progresses_countdown() {
314 let candles: Vec<Candle> = (1..=40)
320 .map(|i| {
321 c(
322 f64::from(i) + 0.5,
323 f64::from(i) - 0.5,
324 f64::from(i),
325 i64::from(i),
326 )
327 })
328 .collect();
329 let mut td = TdSequential::classic();
330 let out = td.batch(&candles);
331
332 for v in out.iter().take(4) {
334 assert!(v.is_none());
335 }
336 let at_12 = out[12].expect("setup ready");
339 assert_eq!(at_12.setup, -9.0);
340 assert_eq!(at_12.direction, -1.0); let later = out[30].expect("ready");
345 assert_eq!(later.direction, -1.0);
346 assert_eq!(later.countdown, -13.0);
347 }
348
349 #[test]
350 fn pure_downtrend_completes_buy_setup_then_progresses_countdown() {
351 let candles: Vec<Candle> = (1..=40)
356 .rev()
357 .enumerate()
358 .map(|(k, i)| {
359 c(
360 f64::from(i) + 0.5,
361 f64::from(i) - 0.5,
362 f64::from(i),
363 i64::try_from(k).unwrap(),
364 )
365 })
366 .collect();
367 let mut td = TdSequential::classic();
368 let out = td.batch(&candles);
369
370 for v in out.iter().take(4) {
372 assert!(v.is_none());
373 }
374 let at_12 = out[12].expect("setup ready");
375 assert_eq!(at_12.setup, 9.0);
376 assert_eq!(at_12.direction, 1.0); let later = out[30].expect("ready");
380 assert_eq!(later.direction, 1.0);
381 assert_eq!(later.countdown, 13.0);
382 }
383
384 #[test]
385 fn flat_series_emits_zero_setup_and_no_countdown() {
386 let candles: Vec<Candle> = (0..30).map(|i| c(10.5, 9.5, 10.0, i64::from(i))).collect();
389 let mut td = TdSequential::classic();
390 let out = td.batch(&candles);
391 for v in out.iter().skip(5) {
392 let o = v.expect("ready post-warmup");
393 assert_eq!(o.setup, 0.0);
394 assert_eq!(o.countdown, 0.0);
395 assert_eq!(o.direction, 0.0);
396 }
397 }
398
399 #[test]
400 fn batch_equals_streaming() {
401 let candles: Vec<Candle> = (0..60)
402 .map(|i| {
403 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
404 c(m + 1.0, m - 1.0, m, i64::from(i))
405 })
406 .collect();
407 let mut a = TdSequential::classic();
408 let mut b = TdSequential::classic();
409 assert_eq!(
410 a.batch(&candles),
411 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
412 );
413 }
414
415 #[test]
416 fn rejects_invalid_params() {
417 assert!(matches!(
418 TdSequential::new(0, 9, 2, 13),
419 Err(Error::PeriodZero)
420 ));
421 assert!(matches!(
422 TdSequential::new(4, 0, 2, 13),
423 Err(Error::PeriodZero)
424 ));
425 assert!(matches!(
426 TdSequential::new(4, 9, 0, 13),
427 Err(Error::PeriodZero)
428 ));
429 assert!(matches!(
430 TdSequential::new(4, 9, 2, 0),
431 Err(Error::PeriodZero)
432 ));
433 }
434
435 #[test]
436 fn reset_clears_state() {
437 let candles: Vec<Candle> = (1..=20)
438 .map(|i| {
439 c(
440 f64::from(i) + 0.5,
441 f64::from(i) - 0.5,
442 f64::from(i),
443 i64::from(i),
444 )
445 })
446 .collect();
447 let mut td = TdSequential::classic();
448 td.batch(&candles);
449 assert!(td.is_ready());
450 td.reset();
451 assert!(!td.is_ready());
452 assert_eq!(td.update(candles[0]), None);
453 }
454
455 #[test]
456 fn accessors_and_metadata() {
457 let td = TdSequential::classic();
458 assert_eq!(td.params(), (4, 9, 2, 13));
459 assert_eq!(td.warmup_period(), 5);
460 assert_eq!(td.name(), "TDSequential");
461 }
462
463 fn from_closes(closes: &[f64]) -> Vec<Candle> {
465 closes
466 .iter()
467 .enumerate()
468 .map(|(k, &m)| c(m + 0.5, m - 0.5, m, i64::try_from(k).unwrap()))
469 .collect()
470 }
471
472 fn buy_deferral_closes() -> Vec<f64> {
480 let mut closes: Vec<f64> = (77..=100).rev().map(f64::from).collect();
481 closes.extend([90.0, 95.0, 95.0, 89.0, 80.0]);
482 closes
483 }
484
485 fn sell_deferral_closes() -> Vec<f64> {
489 buy_deferral_closes().iter().map(|x| 200.0 - x).collect()
490 }
491
492 fn countdowns(out: &[Option<TdSequentialOutput>]) -> Vec<Option<f64>> {
493 out.iter().map(|o| o.map(|v| v.countdown)).collect()
494 }
495
496 #[test]
497 fn buy_bar_13_is_deferred_until_low_reaches_bar_8_close() {
498 let mut td = TdSequential::classic();
499 let out = td.batch(&from_closes(&buy_deferral_closes()));
500 let cd = countdowns(&out);
501 assert_eq!(cd[19], Some(8.0));
502 assert_eq!(cd[23], Some(12.0));
503 assert!(cd[24..28].iter().all(|v| *v == Some(12.0)));
504 assert_eq!(cd[28], Some(13.0));
505 let at_27 = out[27].unwrap();
508 assert_eq!(at_27.setup, -4.0);
509 assert_eq!(at_27.direction, 1.0);
510 assert_eq!(out[28].unwrap().setup, 1.0);
512 }
513
514 #[test]
515 fn sell_bar_13_is_deferred_until_high_reaches_bar_8_close() {
516 let mut td = TdSequential::classic();
517 let out = td.batch(&from_closes(&sell_deferral_closes()));
518 let cd = countdowns(&out);
519 assert_eq!(cd[19], Some(-8.0));
520 assert_eq!(cd[23], Some(-12.0));
521 assert!(cd[24..28].iter().all(|v| *v == Some(-12.0)));
522 assert_eq!(cd[28], Some(-13.0));
523 let at_27 = out[27].unwrap();
524 assert_eq!(at_27.setup, 4.0);
525 assert_eq!(at_27.direction, -1.0);
526 }
527
528 #[test]
529 fn qualifier_close_is_bar_8_close() {
530 let mut td = TdSequential::classic();
531 let candles = from_closes(&sell_deferral_closes());
532 for candle in &candles[..19] {
533 td.update(*candle);
534 }
535 assert!(td.qualifier_close.is_nan());
536 td.update(candles[19]);
537 assert_eq!(td.qualifier_close.to_bits(), 119.0_f64.to_bits());
538 }
539
540 #[test]
541 fn short_target_has_no_qualifier() {
542 let closes: Vec<f64> = (70..=100).rev().map(f64::from).collect();
545 let mut buy = TdSequential::new(4, 9, 2, 3).unwrap();
546 let cd = countdowns(&buy.batch(&from_closes(&closes)));
547 assert_eq!(cd[12], Some(1.0));
548 assert_eq!(cd[14], Some(3.0));
549 assert_eq!(cd[30], Some(3.0));
550 assert!(buy.qualifier_close.is_nan());
551
552 let rising: Vec<f64> = closes.iter().map(|x| 200.0 - x).collect();
553 let mut sell = TdSequential::new(4, 9, 2, 3).unwrap();
554 let cd = countdowns(&sell.batch(&from_closes(&rising)));
555 assert_eq!(cd[14], Some(-3.0));
556 assert_eq!(cd[30], Some(-3.0));
557 assert!(sell.qualifier_close.is_nan());
558 }
559
560 #[test]
561 fn opposite_setup_invalidates_and_clears_qualifier() {
562 let mut closes: Vec<f64> = (77..=100).rev().map(f64::from).collect();
567 closes.extend((78..=120).map(f64::from));
568 let candles = from_closes(&closes);
569 let mut td = TdSequential::classic();
570 let out: Vec<Option<TdSequentialOutput>> = candles.iter().map(|x| td.update(*x)).collect();
571 let at_25 = out[25].unwrap();
572 assert_eq!((at_25.setup, at_25.countdown), (0.0, 12.0));
573 assert_eq!(out[33].unwrap().countdown, 12.0);
574 let at_34 = out[34].unwrap();
577 assert_eq!(
578 (at_34.setup, at_34.countdown, at_34.direction),
579 (-9.0, -1.0, -1.0)
580 );
581 assert_eq!(out[46].unwrap().countdown, -13.0);
582
583 let mut probe = TdSequential::classic();
584 for candle in &candles[..34] {
585 probe.update(*candle);
586 }
587 assert_eq!(probe.qualifier_close.to_bits(), 81.0_f64.to_bits());
588 probe.update(candles[34]);
589 assert!(probe.qualifier_close.is_nan());
590
591 let mut back = closes.clone();
593 back.extend((60..=119).rev().map(f64::from));
594 let mut td2 = TdSequential::classic();
595 let last = td2
596 .batch(&from_closes(&back))
597 .last()
598 .copied()
599 .flatten()
600 .unwrap();
601 assert_eq!((last.countdown, last.direction), (13.0, 1.0));
602 }
603
604 #[test]
605 fn first_value_lands_at_warmup_minus_one() {
606 let candles = from_closes(&buy_deferral_closes());
607 for (sl, cl) in [(4, 2), (2, 6), (1, 1)] {
608 let mut td = TdSequential::new(sl, 9, cl, 13).unwrap();
609 let warm = td.warmup_period();
610 let out = td.batch(&candles);
611 assert!(out[..warm - 1].iter().all(Option::is_none));
612 assert!(out[warm - 1].is_some());
613 }
614 }
615
616 #[test]
617 fn reset_reproduces_fresh_run() {
618 let candles = from_closes(&sell_deferral_closes());
619 let mut fresh = TdSequential::classic();
620 let expected = fresh.batch(&candles);
621 let mut td = TdSequential::classic();
622 td.batch(&from_closes(&buy_deferral_closes()));
623 td.reset();
624 assert!(td.qualifier_close.is_nan());
625 assert_eq!(td.batch(&candles), expected);
626 }
627
628 #[test]
629 fn batch_equals_streaming_on_deferral_series() {
630 let candles = from_closes(&buy_deferral_closes());
631 let mut a = TdSequential::classic();
632 let mut b = TdSequential::classic();
633 let streamed: Vec<Option<TdSequentialOutput>> =
634 candles.iter().map(|x| b.update(*x)).collect();
635 assert_eq!(a.batch(&candles), streamed);
636 }
637}