1use crate::error::{Error, Result};
4use crate::indicators::adl::Adl;
5use crate::indicators::ema::Ema;
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[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 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 pub fn classic() -> Self {
72 Self::new(3, 10).expect("classic Chaikin Oscillator params are valid")
73 }
74
75 pub const fn periods(&self) -> (usize, usize) {
77 (self.fast_period, self.slow_period)
78 }
79}
80
81impl ChaikinOscillator {
82 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 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 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 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 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 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 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 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
283fn 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
304struct 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 fast: (f64, f64),
318 slow: (f64, f64),
320}
321
322#[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 *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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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}