wickra_core/indicators/
ad_oscillator.rs1use crate::indicators::sma::Sma;
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7const SIGNAL_PERIOD: usize = 13;
9
10#[derive(Debug, Clone)]
50pub struct AdOscillator {
51 prev_close: Option<f64>,
52 line: f64,
53 signal: Sma,
54 last: Option<f64>,
55}
56
57impl Default for AdOscillator {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl AdOscillator {
64 #[must_use]
66 pub fn new() -> Self {
67 Self {
68 prev_close: None,
69 line: 0.0,
70 signal: Sma::new(SIGNAL_PERIOD).expect("SIGNAL_PERIOD is non-zero"),
71 last: None,
72 }
73 }
74
75 pub const fn value(&self) -> Option<f64> {
77 self.last
78 }
79}
80
81impl Indicator for AdOscillator {
82 type Input = Candle;
83 type Output = f64;
84
85 #[inline]
86 fn update(&mut self, candle: Candle) -> Option<f64> {
87 let Some(prev) = self.prev_close else {
88 self.prev_close = Some(candle.close);
90 return None;
91 };
92 let delta = if candle.close > prev {
93 candle.close - prev.min(candle.low)
95 } else if candle.close < prev {
96 candle.close - prev.max(candle.high)
98 } else {
99 0.0
100 };
101 self.line += delta;
102 self.prev_close = Some(candle.close);
103 let signal = self.signal.update(self.line)?;
104 let osc = self.line - signal;
105 self.last = Some(osc);
106 Some(osc)
107 }
108
109 fn reset(&mut self) {
110 self.prev_close = None;
111 self.line = 0.0;
112 self.signal.reset();
113 self.last = None;
114 }
115
116 #[inline]
117 fn warmup_period(&self) -> usize {
118 1 + SIGNAL_PERIOD
121 }
122
123 #[inline]
124 fn is_ready(&self) -> bool {
125 self.last.is_some()
126 }
127
128 #[inline]
129 fn name(&self) -> &'static str {
130 "ADOSC"
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::indicators::wad::Wad;
138 use crate::traits::BatchExt;
139 use approx::assert_relative_eq;
140
141 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
142 Candle::new(open, high, low, close, 100.0, ts).unwrap()
143 }
144
145 #[test]
146 fn accessors_and_metadata() {
147 let ad = AdOscillator::new();
148 assert_eq!(ad.name(), "ADOSC");
149 assert_eq!(ad.warmup_period(), 14);
150 assert!(!ad.is_ready());
151 assert_eq!(ad.value(), None);
152 assert_eq!(AdOscillator::default().warmup_period(), 14);
154 }
155
156 #[test]
157 fn seed_bar_returns_none() {
158 let mut ad = AdOscillator::new();
159 assert_eq!(ad.update(c(100.0, 101.0, 99.0, 100.0, 0)), None);
160 }
161
162 #[test]
163 fn equals_wad_line_minus_its_sma() {
164 let candles: Vec<Candle> = (0..80_i64)
167 .map(|i| {
168 let base = 100.0 + (i as f64 * 0.3).sin() * 6.0;
169 c(
170 base,
171 base + 2.0,
172 base - 2.0,
173 base + (i as f64 * 0.5).cos(),
174 i,
175 )
176 })
177 .collect();
178 let osc = AdOscillator::new().batch(&candles);
179 let line = Wad::new().batch(&candles);
181 let mut sma = Sma::new(SIGNAL_PERIOD).unwrap();
182 let expected: Vec<Option<f64>> = line
183 .iter()
184 .map(|v| v.and_then(|l| sma.update(l).map(|s| l - s)))
185 .collect();
186 assert_eq!(osc, expected);
187 }
188
189 #[test]
190 fn flat_market_oscillates_at_zero() {
191 let mut ad = AdOscillator::new();
194 let candles: Vec<Candle> = (0..40).map(|i| c(50.0, 50.0, 50.0, 50.0, i)).collect();
195 let out = ad.batch(&candles);
196 for v in out.iter().skip(ad.warmup_period() - 1).flatten() {
197 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
198 }
199 }
200
201 #[test]
202 fn warmup_emits_at_warmup_period() {
203 let mut ad = AdOscillator::new();
204 let candles: Vec<Candle> = (0..20)
205 .map(|i| {
206 let close = 100.0 + f64::from(i);
207 c(close, close + 2.0, close - 2.0, close, i64::from(i))
208 })
209 .collect();
210 let out = ad.batch(&candles);
211 assert_eq!(ad.warmup_period(), 14);
212 for v in out.iter().take(13) {
213 assert!(v.is_none());
214 }
215 assert!(out[13].is_some());
216 }
217
218 #[test]
219 fn reset_clears_state() {
220 let mut ad = AdOscillator::new();
221 let candles: Vec<Candle> = (0..30)
222 .map(|i| {
223 let close = 100.0 + f64::from(i);
224 c(close, close + 2.0, close - 2.0, close, i64::from(i))
225 })
226 .collect();
227 ad.batch(&candles);
228 assert!(ad.is_ready());
229 ad.reset();
230 assert!(!ad.is_ready());
231 assert_eq!(ad.value(), None);
232 }
233
234 #[test]
235 fn batch_equals_streaming() {
236 let candles: Vec<Candle> = (0..100_i64)
237 .map(|i| {
238 let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
239 c(base, base + 1.5, base - 1.5, base + 0.4, i)
240 })
241 .collect();
242 let batch = AdOscillator::new().batch(&candles);
243 let mut s = AdOscillator::new();
244 let streamed: Vec<_> = candles.iter().map(|x| s.update(*x)).collect();
245 assert_eq!(batch, streamed);
246 }
247}