wickra_core/indicators/
chaikin_oscillator.rs1use 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 Indicator for ChaikinOscillator {
82 type Input = Candle;
83 type Output = f64;
84
85 #[inline]
86 fn update(&mut self, candle: Candle) -> Option<f64> {
87 let adl = self.adl.update(candle)?;
90 let fast = self.fast.update(adl);
91 let slow = self.slow.update(adl);
92 Some(fast? - slow?)
93 }
94
95 fn reset(&mut self) {
96 self.adl.reset();
97 self.fast.reset();
98 self.slow.reset();
99 }
100
101 #[inline]
102 fn warmup_period(&self) -> usize {
103 self.slow_period
105 }
106
107 #[inline]
108 fn is_ready(&self) -> bool {
109 self.fast.is_ready() && self.slow.is_ready()
110 }
111
112 #[inline]
113 fn name(&self) -> &'static str {
114 "ChaikinOscillator"
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::traits::BatchExt;
122 use approx::assert_relative_eq;
123
124 fn cdl(base: f64, volume: f64, ts: i64) -> Candle {
125 Candle::new(base, base + 1.0, base - 1.0, base, volume, ts).unwrap()
126 }
127
128 fn flat(price: f64, ts: i64) -> Candle {
129 Candle::new(price, price, price, price, 100.0, ts).unwrap()
130 }
131
132 #[test]
133 fn matches_independent_adl_and_emas() {
134 let candles: Vec<Candle> = (0..80)
137 .map(|i| {
138 let mid = 100.0 + (i as f64 * 0.2).sin() * 6.0;
139 Candle::new(
140 mid,
141 mid + 1.5,
142 mid - 1.5,
143 mid + 0.3,
144 10.0 + (i % 6) as f64,
145 i,
146 )
147 .unwrap()
148 })
149 .collect();
150 let mut osc = ChaikinOscillator::classic();
151 let mut adl = Adl::new();
152 let mut fast = Ema::new(3).unwrap();
153 let mut slow = Ema::new(10).unwrap();
154 for (i, candle) in candles.iter().enumerate() {
155 let got = osc.update(*candle);
156 let a = adl.update(*candle).expect("ADL emits from candle 1");
157 let f = fast.update(a);
158 let s = slow.update(a);
159 match (f, s) {
160 (Some(fv), Some(sv)) => {
161 assert_relative_eq!(
162 got.expect("oscillator ready once slow EMA is"),
163 fv - sv,
164 epsilon = 1e-9
165 );
166 }
167 _ => assert!(got.is_none(), "must be None until slow EMA ready (i={i})"),
168 }
169 }
170 }
171
172 #[test]
173 fn flat_market_yields_zero() {
174 let candles: Vec<Candle> = (0..60).map(|i| flat(10.0, i)).collect();
177 let mut osc = ChaikinOscillator::classic();
178 for v in osc.batch(&candles).into_iter().flatten() {
179 assert_relative_eq!(v, 0.0, epsilon = 1e-9);
180 }
181 }
182
183 #[test]
184 fn first_emission_matches_warmup_period() {
185 let candles: Vec<Candle> = (0..40).map(|i| cdl(100.0 + i as f64, 50.0, i)).collect();
186 let mut osc = ChaikinOscillator::classic();
187 let out = osc.batch(&candles);
188 assert_eq!(osc.warmup_period(), 10);
189 for (i, v) in out.iter().enumerate().take(9) {
190 assert!(v.is_none(), "index {i} must be None during warmup");
191 }
192 assert!(out[9].is_some(), "first value lands at warmup_period - 1");
193 }
194
195 #[test]
196 fn rejects_invalid_params() {
197 assert!(ChaikinOscillator::new(0, 10).is_err());
198 assert!(ChaikinOscillator::new(3, 0).is_err());
199 assert!(ChaikinOscillator::new(10, 3).is_err());
200 assert!(ChaikinOscillator::new(5, 5).is_err());
201 }
202
203 #[test]
206 fn accessors_and_metadata() {
207 let osc = ChaikinOscillator::classic();
208 assert_eq!(osc.periods(), (3, 10));
209 assert_eq!(osc.name(), "ChaikinOscillator");
210 }
211
212 #[test]
213 fn reset_clears_state() {
214 let candles: Vec<Candle> = (0..40).map(|i| cdl(100.0 + i as f64, 50.0, i)).collect();
215 let mut osc = ChaikinOscillator::classic();
216 osc.batch(&candles);
217 assert!(osc.is_ready());
218 osc.reset();
219 assert!(!osc.is_ready());
220 assert_eq!(osc.update(candles[0]), None);
221 }
222
223 #[test]
224 fn batch_equals_streaming() {
225 let candles: Vec<Candle> = (0..80)
226 .map(|i| {
227 let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
228 Candle::new(
229 mid,
230 mid + 2.0,
231 mid - 2.0,
232 mid + 0.5,
233 10.0 + (i % 5) as f64,
234 i,
235 )
236 .unwrap()
237 })
238 .collect();
239 let mut a = ChaikinOscillator::classic();
240 let mut b = ChaikinOscillator::classic();
241 assert_eq!(
242 a.batch(&candles),
243 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
244 );
245 }
246}