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