Skip to main content

wickra_core/indicators/
td_sequential.rs

1#![allow(clippy::doc_markdown)]
2
3//! Tom DeMark TD Sequential (Setup + Countdown).
4//!
5//! TD Sequential is DeMark's flagship two-phase exhaustion pattern:
6//!
7//! 1. **Setup phase** — 9 consecutive bars whose close is less-than (buy
8//!    setup) or greater-than (sell setup) the close 4 bars earlier. The
9//!    setup *completes* on the 9th bar.
10//! 2. **Countdown phase** — after a completed setup, count up to 13 bars
11//!    that satisfy the countdown comparison (buy countdown: `close <= low`
12//!    two bars earlier; sell countdown: `close >= high` two bars earlier).
13//!    Countdown bars do not need to be consecutive. The 13th bar must also
14//!    trade through the close of countdown bar 8 (low at or below it for a
15//!    buy, high at or above it for a sell); otherwise it is deferred.
16//!
17//! A completed countdown (13) signals exhaustion in the direction of the
18//! original setup and is the canonical DeMark reversal signal.
19//!
20//! Output struct `TdSequentialOutput`:
21//!
22//! - `setup`: signed setup count (positive for buy setup, negative for sell
23//!   setup, 0 when no streak is active; capped at ±9).
24//! - `countdown`: signed countdown count (positive for buy countdown, negative
25//!   for sell countdown, 0 when no countdown is active; capped at ±13).
26//! - `direction`: `+1.0` if a buy countdown is currently active, `-1.0` if a
27//!   sell countdown is active, `0.0` otherwise. The countdown direction is
28//!   set when the originating setup completes and stays valid until the
29//!   countdown finishes or is invalidated by an opposite-direction setup.
30
31use std::collections::VecDeque;
32
33use crate::error::{Error, Result};
34use crate::ohlcv::Candle;
35use crate::traits::Indicator;
36
37/// Direction of an active TD Sequential countdown phase.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39enum Direction {
40    None,
41    Buy,
42    Sell,
43}
44
45/// Output of [`TdSequential`]: setup count, countdown count, and active
46/// countdown direction.
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct TdSequentialOutput {
49    /// Signed setup count: +N for an active buy setup of length `N`, −N for
50    /// a sell setup of length `N`, 0 if neither streak is active. Capped at
51    /// ±9 (the canonical setup target).
52    pub setup: f64,
53    /// Signed countdown count: +N for an active buy countdown of length `N`,
54    /// −N for a sell countdown of length `N`, 0 if no countdown is active.
55    /// Capped at ±13.
56    pub countdown: f64,
57    /// Direction of the active countdown: `+1.0` for buy, `−1.0` for sell,
58    /// `0.0` if no countdown is currently active.
59    pub direction: f64,
60}
61
62/// TD Sequential state machine: combined Setup (1-9) + Countdown (1-13).
63/// # Example
64///
65/// ```
66/// use wickra_core::{TdSequential, Candle, Indicator};
67///
68/// let mut indicator = TdSequential::new(4, 9, 2, 13).unwrap();
69/// // `None` during warmup, then `Some(_)` once enough bars are seen.
70/// let mut out = None;
71/// for i in 0..40i64 {
72///     let p = 100.0 + (i as f64 * 0.4).sin() * 5.0;
73///     let candle = Candle::new(p, p + 1.5, p - 1.5, p + 0.3, 1_000.0, i).unwrap();
74///     out = indicator.update(candle);
75/// }
76/// let _ = out;
77/// ```
78#[derive(Debug, Clone)]
79pub struct TdSequential {
80    // Rolling window of recent candles. We need up to 5 closes back (for the
81    // setup rule which compares close[i] vs close[i-4]) and the high/low from
82    // 2 bars ago (for the countdown rule).
83    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    /// Close of countdown bar `countdown_target − 5` (bar 8 of 13), which bar
93    /// 13 must reach; `NaN` until that bar is counted.
94    qualifier_close: f64,
95    countdown_dir: Direction,
96    ready: bool,
97}
98
99impl TdSequential {
100    /// Construct a TD Sequential with explicit lookbacks and targets. The
101    /// canonical DeMark configuration is `setup_lookback = 4`, `setup_target =
102    /// 9`, `countdown_lookback = 2`, `countdown_target = 13`.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`Error::PeriodZero`] if any argument is zero.
107    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        // Need to keep enough candles for both rules: setup uses close[-N];
121        // countdown uses high/low[-M]. Reserve `max(N, M) + 1` slots.
122        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    /// DeMark's classic configuration: setup `lookback = 4, target = 9`,
140    /// countdown `lookback = 2, target = 13`.
141    pub fn classic() -> Self {
142        Self::new(4, 9, 2, 13).expect("classic TD Sequential parameters are valid")
143    }
144
145    /// Configured `(setup_lookback, setup_target, countdown_lookback,
146    /// countdown_target)`.
147    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        // The required minimum history is `max(setup_lookback,
167        // countdown_lookback)` previous bars. Once we have that many, we can
168        // evaluate both rules.
169        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        // --- Setup rule: compare to close[setup_lookback bars ago] ---
176        // After `need` candles are buffered, the candle at offset `need - L`
177        // from the front is the one `L` bars before the new candle (0-based
178        // count: `front()` is `need` bars ago).
179        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        // --- Countdown activation: when a setup completes, arm the countdown
194        // in the same direction; an opposite-direction setup invalidates any
195        // active countdown.
196        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        // --- Countdown rule: compare close to high/low `countdown_lookback`
213        // bars ago. Only the active direction advances. Once a countdown
214        // reaches `countdown_target`, the strict `< countdown_target` guard
215        // keeps it pinned so the caller can detect the "13" signal on this
216        // bar and any subsequent bar until a new setup arms a fresh run.
217        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                    // The final bar must also trade at or below the close of
223                    // countdown bar 8; otherwise it is deferred.
224                    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                    // The final bar must also trade at or above the close of
238                    // countdown bar 8; otherwise it is deferred.
239                    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        // Strictly increasing closes -> sell setup increments every bar past
315        // warmup, reaching -9 by index 12 (warmup is 4 + 1). After that,
316        // every bar continues to make a higher close, so each subsequent bar
317        // also makes a higher close than the high 2 bars ago — the sell
318        // countdown increments on each bar after activation.
319        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        // Warmup: indices 0..3 yield None (need=4 prior closes).
333        for v in out.iter().take(4) {
334            assert!(v.is_none());
335        }
336        // After index 12, setup reaches -9 (completed). From the next bar on,
337        // countdown begins to increment.
338        let at_12 = out[12].expect("setup ready");
339        assert_eq!(at_12.setup, -9.0);
340        assert_eq!(at_12.direction, -1.0); // countdown direction armed
341
342        // Each subsequent bar makes close > high[i-2], so the sell countdown
343        // advances by one per bar; by some later index it caps at -13.
344        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        // Strictly decreasing closes -> buy setup increments every bar past
352        // warmup, reaching 9 by index 12. After activation, every subsequent
353        // bar satisfies close <= low[i-2], so the buy countdown advances by
354        // one per bar and pins at +13.
355        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        // Warmup: indices 0..3 yield None.
371        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); // buy direction armed
377
378        // By idx 30 the buy countdown has saturated at +13.
379        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        // All closes equal -> never completes any setup; countdown never
387        // activates; setup, countdown, direction all stay at 0.
388        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    /// Candles with a +-0.5 range around each close, timestamped by index.
464    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    /// Buy-side deferral series. Closes fall 100 -> 77 (idx 0..=23): the buy
473    /// setup completes at idx 12 (countdown 1), countdown bar 8 is idx 19
474    /// (close 81, the stored qualifier) and idx 23 reaches countdown 12.
475    /// A rally (90, 95, 95) follows, then idx 27 closes at 89 <= low[25] =
476    /// 94.5 (countdown comparison met) but its low 88.5 > 81, so bar 13 is
477    /// deferred. Idx 28 closes at 80 <= low[26] = 94.5 with low 79.5 <= 81,
478    /// which completes the countdown. The rally only builds a sell setup of 4.
479    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    /// Mirror image of [`buy_deferral_closes`] around 100: the sell qualifier
486    /// is close 119 at idx 19; idx 27 (high 111.5 < 119) is deferred and idx
487    /// 28 (high 120.5 >= 119) completes the sell countdown.
488    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        // idx 27: closes 90, 95, 95, 89 vs closes 4 back (80, 79, 78, 77)
506        // form a sell setup of 4 while the buy countdown stays armed.
507        let at_27 = out[27].unwrap();
508        assert_eq!(at_27.setup, -4.0);
509        assert_eq!(at_27.direction, 1.0);
510        // idx 28: 80 < close[24] = 90 starts a new buy setup of 1.
511        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        // countdown_target = 3 <= 5: no bar 8 exists, so the qualifier stays
543        // NaN and the final bar completes unconditionally (idx 12, 13, 14).
544        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        // Buy countdown reaches 12 at idx 23 with the qualifier stored (81);
563        // then closes rise 78, 79, ... Idx 24 (78 < 80) and idx 25 (79 == 79)
564        // do not count, so the sell setup runs idx 26..=34 and completes at
565        // idx 34 (close 88), resetting everything.
566        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        // idx 34: invalidated; close 88 >= high[32] = 86.5 so the sell
575        // countdown starts at 1 on the same bar and reaches 13 at idx 46.
576        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        // And back again: a buy setup after the sell countdown re-arms buy.
592        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}