Skip to main content

wickra_core/indicators/
chaikin_oscillator.rs

1//! Chaikin Oscillator.
2
3use crate::error::{Error, Result};
4use crate::indicators::adl::Adl;
5use crate::indicators::ema::Ema;
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9/// Chaikin Oscillator — the MACD of the Accumulation/Distribution Line.
10///
11/// ```text
12/// ChaikinOsc_t = EMA(ADL, fast)_t − EMA(ADL, slow)_t
13/// ```
14///
15/// It turns the unbounded, ever-drifting [`Adl`](crate::Adl) into a
16/// zero-centred momentum oscillator: positive when short-term accumulation
17/// outpaces the longer trend, negative when distribution leads. Because the
18/// ADL emits from the very first candle, the slow EMA gates the first output —
19/// the warmup period is exactly `slow`. Chaikin's classic configuration is
20/// `fast = 3`, `slow = 10`.
21///
22/// # Example
23///
24/// ```
25/// use wickra_core::{Candle, Indicator, ChaikinOscillator};
26///
27/// let mut indicator = ChaikinOscillator::classic();
28/// let mut last = None;
29/// for i in 0..80 {
30///     let base = 100.0 + f64::from(i);
31///     let candle =
32///         Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
33///     last = indicator.update(candle);
34/// }
35/// assert!(last.is_some());
36/// ```
37#[derive(Debug, Clone)]
38pub struct ChaikinOscillator {
39    adl: Adl,
40    fast: Ema,
41    slow: Ema,
42    fast_period: usize,
43    slow_period: usize,
44}
45
46impl ChaikinOscillator {
47    /// Construct a Chaikin Oscillator with explicit fast / slow EMA periods.
48    ///
49    /// # Errors
50    /// Returns [`Error::PeriodZero`] if either period is zero, or
51    /// [`Error::InvalidPeriod`] if `fast >= slow`.
52    pub fn new(fast: usize, slow: usize) -> Result<Self> {
53        if fast == 0 || slow == 0 {
54            return Err(Error::PeriodZero);
55        }
56        if fast >= slow {
57            return Err(Error::InvalidPeriod {
58                message: "Chaikin Oscillator needs fast < slow",
59            });
60        }
61        Ok(Self {
62            adl: Adl::new(),
63            fast: Ema::new(fast)?,
64            slow: Ema::new(slow)?,
65            fast_period: fast,
66            slow_period: slow,
67        })
68    }
69
70    /// Chaikin's classic configuration: `EMA(ADL, 3) − EMA(ADL, 10)`.
71    pub fn classic() -> Self {
72        Self::new(3, 10).expect("classic Chaikin Oscillator params are valid")
73    }
74
75    /// Configured `(fast, slow)` periods.
76    pub const fn periods(&self) -> (usize, usize) {
77        (self.fast_period, self.slow_period)
78    }
79}
80
81impl ChaikinOscillator {
82    /// Exact batch over high/low/close/volume columns: one output per bar
83    /// (`NaN` during warmup), bit for bit what replaying `update` over the same
84    /// candles gives (the oscillator reads no open or timestamp). The caller
85    /// guarantees valid OHLCV bars, as the bindings validate them once up
86    /// front.
87    ///
88    /// # Panics
89    ///
90    /// Panics if the five slices differ in length.
91    pub fn batch_hlcv_into(
92        &mut self,
93        high: &[f64],
94        low: &[f64],
95        close: &[f64],
96        volume: &[f64],
97        out: &mut [f64],
98    ) {
99        let n = high.len();
100        assert!(
101            low.len() == n && close.len() == n && volume.len() == n && out.len() == n,
102            "high, low, close, volume and the output must be equal length"
103        );
104        let seeded = self.warm_up(high, low, close, volume, out);
105        if seeded == n {
106            return;
107        }
108        let (adl, fast, slow) = wickra_simd::dispatch(ChaikinTail {
109            high: &high[seeded..],
110            low: &low[seeded..],
111            close: &close[seeded..],
112            volume: &volume[seeded..],
113            out: &mut out[seeded..],
114            state: self.steady_state(),
115            fast: (self.fast.alpha(), self.fast.one_minus_alpha()),
116            slow: (self.slow.alpha(), self.slow.one_minus_alpha()),
117        });
118        self.adl.resume_at(adl);
119        self.fast.seed_to(fast);
120        self.slow.seed_to(slow);
121    }
122
123    /// Replay `update` until both EMAs are seeded (or the bars run out),
124    /// writing those outputs; returns how many bars it consumed.
125    fn warm_up(
126        &mut self,
127        high: &[f64],
128        low: &[f64],
129        close: &[f64],
130        volume: &[f64],
131        out: &mut [f64],
132    ) -> usize {
133        let mut idx = 0;
134        while idx < out.len() && !self.is_ready() {
135            let candle =
136                Candle::new_unchecked(close[idx], high[idx], low[idx], close[idx], volume[idx], 0);
137            out[idx] = self.update(candle).unwrap_or(f64::NAN);
138            idx += 1;
139        }
140        idx
141    }
142
143    /// The accumulation line and both EMAs once both EMAs are seeded.
144    fn steady_state(&self) -> (f64, f64, f64) {
145        (
146            self.adl.value().expect("the ADL emits from its first bar"),
147            self.fast.value().expect("the fast EMA is seeded"),
148            self.slow.value().expect("the slow EMA is seeded"),
149        )
150    }
151
152    /// [`batch_hlcv_into`](Self::batch_hlcv_into) over full OHLCV columns that
153    /// have not been validated: returns `true` with `out` filled if every bar
154    /// is one [`Candle::new`] accepts, or `false` -- the indicator untouched and
155    /// `out` as it was -- otherwise.
156    ///
157    /// # Panics
158    ///
159    /// Panics if the six slices differ in length.
160    pub fn batch_ohlcv_into(
161        &mut self,
162        open: &[f64],
163        high: &[f64],
164        low: &[f64],
165        close: &[f64],
166        volume: &[f64],
167        out: &mut [f64],
168    ) -> bool {
169        assert_ohlcv(open, high, low, close, volume, out);
170        if !Candle::all_valid(open, high, low, close, volume) {
171            return false;
172        }
173        self.batch_hlcv_into(high, low, close, volume, out);
174        true
175    }
176
177    /// [`batch_hlcv_fast_into`](Self::batch_hlcv_fast_into) over full OHLCV
178    /// columns that have not been validated, like
179    /// [`batch_ohlcv_into`](Self::batch_ohlcv_into): `true` with `out` filled
180    /// -- bit for bit what `batch_hlcv_fast_into` gives -- or `false`, the
181    /// indicator untouched, if any bar is invalid. Whether the bars are valid
182    /// and whether they are inside the kernel's range is one pass over the
183    /// columns, not two.
184    ///
185    /// # Panics
186    ///
187    /// Panics if the six slices differ in length.
188    pub fn batch_ohlcv_fast_into(
189        &mut self,
190        open: &[f64],
191        high: &[f64],
192        low: &[f64],
193        close: &[f64],
194        volume: &[f64],
195        out: &mut [f64],
196    ) -> bool {
197        let n = assert_ohlcv(open, high, low, close, volume, out);
198        if !self.fast_path_open(n)
199            || !Candle::all_valid_within(open, high, low, close, volume, crate::fast::MAX_ABS)
200        {
201            return self.batch_ohlcv_into(open, high, low, close, volume, out);
202        }
203        self.fast_batch(high, low, close, volume, out);
204        true
205    }
206
207    /// Whether a fast batch of `n` bars may take the kernel: a fresh indicator
208    /// and a series reaching the first output.
209    fn fast_path_open(&self, n: usize) -> bool {
210        self.adl.value().is_none()
211            && self.fast.is_fresh()
212            && self.slow.is_fresh()
213            && n >= self.slow_period
214    }
215
216    /// The fast batch of a fresh indicator over bars it may take: the exact
217    /// warmup up to the first output, then the kernel from that state, leaving
218    /// the state where the kernel ends.
219    fn fast_batch(
220        &mut self,
221        high: &[f64],
222        low: &[f64],
223        close: &[f64],
224        volume: &[f64],
225        out: &mut [f64],
226    ) {
227        let first = self.slow_period;
228        self.warm_up(high, low, close, volume, &mut out[..first]);
229        let (adl, fast, slow) = wickra_simd::dispatch(crate::fast::ChaikinFast {
230            high: &high[first..],
231            low: &low[first..],
232            close: &close[first..],
233            volume: &volume[first..],
234            state: self.steady_state(),
235            alphas: (self.fast.alpha(), self.slow.alpha()),
236            out: &mut out[first..],
237            _borrow: std::marker::PhantomData,
238        });
239        self.adl.resume_at(adl);
240        self.fast.seed_to(fast);
241        self.slow.seed_to(slow);
242    }
243
244    /// Opt-in fast variant of [`batch_hlcv_into`](Self::batch_hlcv_into): after
245    /// the exact warmup, blocks of money-flow volumes, the accumulation line as
246    /// a running-sum scan and both EMAs as SIMD linear-recurrence scans. Every
247    /// value agrees with the exact batch to within a few units in the last
248    /// place relative to the accumulation line; the first value, warmup `NaN`s
249    /// and length are identical, and the result is the same on every platform.
250    /// Only a fresh indicator over finite values within `1e100`, at least
251    /// `slow` bars long, takes the kernel; anything else is the exact batch.
252    /// Afterwards the oscillator continues streaming from the kernel's last
253    /// values.
254    ///
255    /// # Panics
256    ///
257    /// Panics if the five slices differ in length.
258    pub fn batch_hlcv_fast_into(
259        &mut self,
260        high: &[f64],
261        low: &[f64],
262        close: &[f64],
263        volume: &[f64],
264        out: &mut [f64],
265    ) {
266        let n = high.len();
267        assert!(
268            low.len() == n && close.len() == n && volume.len() == n && out.len() == n,
269            "high, low, close, volume and the output must be equal length"
270        );
271        if !self.fast_path_open(n)
272            || ![high, low, close, volume]
273                .iter()
274                .all(|col| crate::fast::in_range(col))
275        {
276            self.batch_hlcv_into(high, low, close, volume, out);
277            return;
278        }
279        self.fast_batch(high, low, close, volume, out);
280    }
281}
282
283/// The common length of OHLCV columns and their output.
284fn assert_ohlcv(
285    open: &[f64],
286    high: &[f64],
287    low: &[f64],
288    close: &[f64],
289    volume: &[f64],
290    out: &[f64],
291) -> usize {
292    let n = open.len();
293    assert!(
294        high.len() == n
295            && low.len() == n
296            && close.len() == n
297            && volume.len() == n
298            && out.len() == n,
299        "open, high, low, close, volume and the output must be equal length"
300    );
301    n
302}
303
304/// The exact oscillator past its warmup as a [`wickra_simd::Kernel`]: the
305/// accumulation line and both EMA recurrences fused into one pass, with the
306/// arithmetic of `update` in the same order -- so the same bits -- and each
307/// `mul_add` a hardware FMA where the CPU has one. Returns the last ADL, fast
308/// and slow EMA.
309struct ChaikinTail<'a> {
310    high: &'a [f64],
311    low: &'a [f64],
312    close: &'a [f64],
313    volume: &'a [f64],
314    out: &'a mut [f64],
315    state: (f64, f64, f64),
316    /// `(alpha, 1 - alpha)` of the fast EMA.
317    fast: (f64, f64),
318    /// `(alpha, 1 - alpha)` of the slow EMA.
319    slow: (f64, f64),
320}
321
322// Inlining into the dispatching function is what compiles the body with its
323// features; see `wickra_simd::Kernel`.
324#[allow(clippy::inline_always)]
325impl wickra_simd::Kernel for ChaikinTail<'_> {
326    type Output = (f64, f64, f64);
327
328    #[inline(always)]
329    fn run<S: wickra_simd::Simd>(self, _simd: S) -> (f64, f64, f64) {
330        let (mut adl, mut fast, mut slow) = self.state;
331        let ((fast_alpha, fast_oma), (slow_alpha, slow_oma)) = (self.fast, self.slow);
332        let bars = self
333            .high
334            .iter()
335            .zip(self.low)
336            .zip(self.close)
337            .zip(self.volume);
338        for (slot, (((&high, &low), &close), &volume)) in self.out.iter_mut().zip(bars) {
339            adl += crate::fast::money_flow(high, low, close, volume);
340            // `Ema::update` ignores a non-finite input, so an accumulation line
341            // that overflowed leaves both EMAs where they were and emits nothing.
342            *slot = if adl.is_finite() {
343                fast = fast_alpha.mul_add(adl, fast_oma * fast);
344                slow = slow_alpha.mul_add(adl, slow_oma * slow);
345                fast - slow
346            } else {
347                f64::NAN
348            };
349        }
350        (adl, fast, slow)
351    }
352}
353
354impl Indicator for ChaikinOscillator {
355    type Input = Candle;
356    type Output = f64;
357
358    #[inline]
359    fn update(&mut self, candle: Candle) -> Option<f64> {
360        // The ADL emits a value from the very first candle, so both EMAs are
361        // fed on every bar and warm up in parallel.
362        let adl = self.adl.update(candle)?;
363        let fast = self.fast.update(adl);
364        let slow = self.slow.update(adl);
365        Some(fast? - slow?)
366    }
367
368    fn reset(&mut self) {
369        self.adl.reset();
370        self.fast.reset();
371        self.slow.reset();
372    }
373
374    #[inline]
375    fn warmup_period(&self) -> usize {
376        // ADL is ready at candle 1; the slow EMA gates the first emission.
377        self.slow_period
378    }
379
380    #[inline]
381    fn is_ready(&self) -> bool {
382        self.fast.is_ready() && self.slow.is_ready()
383    }
384
385    #[inline]
386    fn name(&self) -> &'static str {
387        "ChaikinOscillator"
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::traits::BatchExt;
395    use approx::assert_relative_eq;
396
397    fn cdl(base: f64, volume: f64, ts: i64) -> Candle {
398        Candle::new(base, base + 1.0, base - 1.0, base, volume, ts).unwrap()
399    }
400
401    fn flat(price: f64, ts: i64) -> Candle {
402        Candle::new(price, price, price, price, 100.0, ts).unwrap()
403    }
404
405    #[test]
406    fn matches_independent_adl_and_emas() {
407        // The oscillator must equal feeding a standalone ADL into two
408        // standalone EMAs and differencing them once both are ready.
409        let candles: Vec<Candle> = (0..80)
410            .map(|i| {
411                let mid = 100.0 + (i as f64 * 0.2).sin() * 6.0;
412                Candle::new(
413                    mid,
414                    mid + 1.5,
415                    mid - 1.5,
416                    mid + 0.3,
417                    10.0 + (i % 6) as f64,
418                    i,
419                )
420                .unwrap()
421            })
422            .collect();
423        let mut osc = ChaikinOscillator::classic();
424        let mut adl = Adl::new();
425        let mut fast = Ema::new(3).unwrap();
426        let mut slow = Ema::new(10).unwrap();
427        for (i, candle) in candles.iter().enumerate() {
428            let got = osc.update(*candle);
429            let a = adl.update(*candle).expect("ADL emits from candle 1");
430            let f = fast.update(a);
431            let s = slow.update(a);
432            match (f, s) {
433                (Some(fv), Some(sv)) => {
434                    assert_relative_eq!(
435                        got.expect("oscillator ready once slow EMA is"),
436                        fv - sv,
437                        epsilon = 1e-9
438                    );
439                }
440                _ => assert!(got.is_none(), "must be None until slow EMA ready (i={i})"),
441            }
442        }
443    }
444
445    #[test]
446    fn flat_market_yields_zero() {
447        // A flat candle has zero money-flow volume, so the ADL never moves and
448        // both EMAs of a constant-zero series stay at zero.
449        let candles: Vec<Candle> = (0..60).map(|i| flat(10.0, i)).collect();
450        let mut osc = ChaikinOscillator::classic();
451        for v in osc.batch(&candles).into_iter().flatten() {
452            assert_relative_eq!(v, 0.0, epsilon = 1e-9);
453        }
454    }
455
456    #[test]
457    fn first_emission_matches_warmup_period() {
458        let candles: Vec<Candle> = (0..40).map(|i| cdl(100.0 + i as f64, 50.0, i)).collect();
459        let mut osc = ChaikinOscillator::classic();
460        let out = osc.batch(&candles);
461        assert_eq!(osc.warmup_period(), 10);
462        for (i, v) in out.iter().enumerate().take(9) {
463            assert!(v.is_none(), "index {i} must be None during warmup");
464        }
465        assert!(out[9].is_some(), "first value lands at warmup_period - 1");
466    }
467
468    #[test]
469    fn rejects_invalid_params() {
470        assert!(ChaikinOscillator::new(0, 10).is_err());
471        assert!(ChaikinOscillator::new(3, 0).is_err());
472        assert!(ChaikinOscillator::new(10, 3).is_err());
473        assert!(ChaikinOscillator::new(5, 5).is_err());
474    }
475
476    /// Cover the const accessor `periods` (76-78) and the Indicator-impl
477    /// `name` body (109-111). `warmup_period` is exercised elsewhere.
478    #[test]
479    fn accessors_and_metadata() {
480        let osc = ChaikinOscillator::classic();
481        assert_eq!(osc.periods(), (3, 10));
482        assert_eq!(osc.name(), "ChaikinOscillator");
483    }
484
485    #[test]
486    fn reset_clears_state() {
487        let candles: Vec<Candle> = (0..40).map(|i| cdl(100.0 + i as f64, 50.0, i)).collect();
488        let mut osc = ChaikinOscillator::classic();
489        osc.batch(&candles);
490        assert!(osc.is_ready());
491        osc.reset();
492        assert!(!osc.is_ready());
493        assert_eq!(osc.update(candles[0]), None);
494    }
495
496    #[test]
497    fn batch_equals_streaming() {
498        let candles: Vec<Candle> = (0..80)
499            .map(|i| {
500                let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
501                Candle::new(
502                    mid,
503                    mid + 2.0,
504                    mid - 2.0,
505                    mid + 0.5,
506                    10.0 + (i % 5) as f64,
507                    i,
508                )
509                .unwrap()
510            })
511            .collect();
512        let mut a = ChaikinOscillator::classic();
513        let mut b = ChaikinOscillator::classic();
514        assert_eq!(
515            a.batch(&candles),
516            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
517        );
518    }
519
520    /// OHLCV columns of `n` valid bars that are not all alike.
521    fn columns(n: usize) -> [Vec<f64>; 5] {
522        let mid: Vec<f64> = (0..n)
523            .map(|i| 100.0 + (i as f64 * 0.37).sin() * 9.0)
524            .collect();
525        let open = mid.iter().map(|m| m - 0.25).collect();
526        let high = mid
527            .iter()
528            .enumerate()
529            .map(|(i, m)| m + 1.0 + (i % 3) as f64)
530            .collect();
531        let low = mid
532            .iter()
533            .enumerate()
534            .map(|(i, m)| m - 1.0 - (i % 4) as f64 * 0.5)
535            .collect();
536        let close = mid
537            .iter()
538            .enumerate()
539            .map(|(i, m)| m + ((i % 7) as f64 - 3.0) * 0.2)
540            .collect();
541        let volume = (0..n).map(|i| 1000.0 + (i % 11) as f64 * 37.0).collect();
542        [open, high, low, close, volume]
543    }
544
545    fn bits(values: &[f64]) -> Vec<u64> {
546        values.iter().map(|v| v.to_bits()).collect()
547    }
548
549    /// The `update` replay over the bars the columns describe, as the column
550    /// batches see them (open is the close, timestamps are zero).
551    fn replay(
552        osc: &mut ChaikinOscillator,
553        [_, high, low, close, volume]: &[Vec<f64>; 5],
554    ) -> Vec<f64> {
555        (0..high.len())
556            .map(|i| {
557                let candle =
558                    Candle::new_unchecked(close[i], high[i], low[i], close[i], volume[i], 0);
559                osc.update(candle).unwrap_or(f64::NAN)
560            })
561            .collect()
562    }
563
564    #[test]
565    fn column_batch_is_the_update_replay_bit_for_bit() {
566        let cols = columns(3000);
567        let [_, high, low, close, volume] = &cols;
568        let mut out = vec![0.0; 3000];
569        let mut osc = ChaikinOscillator::classic();
570        osc.batch_hlcv_into(high, low, close, volume, &mut out);
571        let mut reference = ChaikinOscillator::classic();
572        assert_eq!(bits(&out), bits(&replay(&mut reference, &cols)));
573        // The state it leaves continues exactly like the replay's.
574        let next = Candle::new(100.0, 103.0, 98.0, 101.0, 500.0, 0).unwrap();
575        assert_eq!(
576            osc.update(next).map(f64::to_bits),
577            reference.update(next).map(f64::to_bits)
578        );
579    }
580
581    #[test]
582    fn column_batch_resumes_mid_warmup_and_mid_series() {
583        let cols = columns(400);
584        let [_, high, low, close, volume] = &cols;
585        let mut whole = vec![0.0; 400];
586        ChaikinOscillator::classic().batch_hlcv_into(high, low, close, volume, &mut whole);
587        // Split inside the warmup and again past it: the pieces join up.
588        let mut split = vec![0.0; 400];
589        let mut osc = ChaikinOscillator::classic();
590        for (start, end) in [(0, 4), (4, 4), (4, 150), (150, 400)] {
591            osc.batch_hlcv_into(
592                &high[start..end],
593                &low[start..end],
594                &close[start..end],
595                &volume[start..end],
596                &mut split[start..end],
597            );
598        }
599        assert_eq!(bits(&split), bits(&whole));
600    }
601
602    #[test]
603    fn column_batch_skips_an_overflowed_accumulation_line() {
604        // A full-range bar with a huge volume adds ±volume to the ADL; two of
605        // them overflow it to infinity, which the EMAs refuse, as in `update`.
606        let n = 30;
607        let high = vec![2.0; n];
608        let low = vec![0.0; n];
609        let close = vec![2.0; n];
610        let mut volume = vec![1.0; n];
611        volume[20] = f64::MAX;
612        volume[21] = f64::MAX;
613        let cols = [
614            close.clone(),
615            high.clone(),
616            low.clone(),
617            close.clone(),
618            volume.clone(),
619        ];
620        let mut out = vec![0.0; n];
621        ChaikinOscillator::classic().batch_hlcv_into(&high, &low, &close, &volume, &mut out);
622        assert!(out[21].is_nan() && out[29].is_nan());
623        assert_eq!(
624            bits(&out),
625            bits(&replay(&mut ChaikinOscillator::classic(), &cols))
626        );
627    }
628
629    #[test]
630    fn checked_ohlcv_batches_equal_the_column_batches() {
631        // Long enough for many kernel blocks, with a ragged last one.
632        let n = 12_345;
633        let [open, high, low, close, volume] = columns(n);
634        let mut column = vec![0.0; n];
635        let mut checked = vec![0.0; n];
636        ChaikinOscillator::classic().batch_hlcv_into(&high, &low, &close, &volume, &mut column);
637        assert!(ChaikinOscillator::classic().batch_ohlcv_into(
638            &open,
639            &high,
640            &low,
641            &close,
642            &volume,
643            &mut checked
644        ));
645        assert_eq!(bits(&checked), bits(&column));
646
647        let mut a = ChaikinOscillator::classic();
648        let mut b = ChaikinOscillator::classic();
649        a.batch_hlcv_fast_into(&high, &low, &close, &volume, &mut column);
650        assert!(b.batch_ohlcv_fast_into(&open, &high, &low, &close, &volume, &mut checked));
651        assert_eq!(bits(&checked), bits(&column));
652        let next = Candle::new(100.0, 103.0, 98.0, 101.0, 500.0, 0).unwrap();
653        assert_eq!(
654            a.update(next).map(f64::to_bits),
655            b.update(next).map(f64::to_bits)
656        );
657    }
658
659    #[test]
660    fn checked_ohlcv_batches_reject_an_invalid_bar_untouched() {
661        let n = 9_000;
662        let [open, mut high, low, close, volume] = columns(n);
663        // Valid until late in the series, where a high falls below its low.
664        high[6_010] = low[6_010] - 1.0;
665        let mut out = vec![0.0; n];
666        for fast in [false, true] {
667            let mut osc = ChaikinOscillator::classic();
668            let ok = if fast {
669                osc.batch_ohlcv_fast_into(&open, &high, &low, &close, &volume, &mut out)
670            } else {
671                osc.batch_ohlcv_into(&open, &high, &low, &close, &volume, &mut out)
672            };
673            assert!(!ok);
674            assert!(osc.adl.value().is_none() && osc.fast.is_fresh() && osc.slow.is_fresh());
675            assert!(out.iter().all(|&v| v == 0.0));
676        }
677    }
678
679    #[test]
680    fn checked_fast_batch_outside_the_kernel_range_is_the_exact_batch() {
681        let n = 4_600;
682        let [open, high, low, close, mut volume] = columns(n);
683        let mut exact = vec![0.0; n];
684        let mut fast = vec![0.0; n];
685        // A valid but huge volume in the warmup, then one past it: both send
686        // the whole series to the exact batch.
687        for at in [5, 4_120] {
688            volume[at] = 1e200;
689            ChaikinOscillator::classic().batch_hlcv_into(&high, &low, &close, &volume, &mut exact);
690            assert!(ChaikinOscillator::classic()
691                .batch_ohlcv_fast_into(&open, &high, &low, &close, &volume, &mut fast));
692            assert_eq!(bits(&fast), bits(&exact));
693            volume[at] = 1000.0;
694        }
695        // A series too short for an output, or an indicator past its first
696        // bar, is the checked exact batch as well.
697        let mut osc = ChaikinOscillator::classic();
698        assert!(osc.batch_ohlcv_fast_into(
699            &open[..5],
700            &high[..5],
701            &low[..5],
702            &close[..5],
703            &volume[..5],
704            &mut fast[..5]
705        ));
706        assert!(osc.batch_ohlcv_fast_into(&open, &high, &low, &close, &volume, &mut fast));
707        let mut reference = ChaikinOscillator::classic();
708        reference.batch_hlcv_into(
709            &high[..5],
710            &low[..5],
711            &close[..5],
712            &volume[..5],
713            &mut exact[..5],
714        );
715        reference.batch_hlcv_into(&high, &low, &close, &volume, &mut exact);
716        assert_eq!(bits(&fast), bits(&exact));
717    }
718}