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 fn update(&mut self, candle: Candle) -> Option<f64> {
86 let Some(prev) = self.prev_close else {
87 self.prev_close = Some(candle.close);
89 return None;
90 };
91 let delta = if candle.close > prev {
92 candle.close - prev.min(candle.low)
94 } else if candle.close < prev {
95 candle.close - prev.max(candle.high)
97 } else {
98 0.0
99 };
100 self.line += delta;
101 self.prev_close = Some(candle.close);
102 let signal = self.signal.update(self.line)?;
103 let osc = self.line - signal;
104 self.last = Some(osc);
105 Some(osc)
106 }
107
108 fn reset(&mut self) {
109 self.prev_close = None;
110 self.line = 0.0;
111 self.signal.reset();
112 self.last = None;
113 }
114
115 fn warmup_period(&self) -> usize {
116 1 + SIGNAL_PERIOD
119 }
120
121 fn is_ready(&self) -> bool {
122 self.last.is_some()
123 }
124
125 fn name(&self) -> &'static str {
126 "ADOSC"
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::indicators::wad::Wad;
134 use crate::traits::BatchExt;
135 use approx::assert_relative_eq;
136
137 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
138 Candle::new(open, high, low, close, 100.0, ts).unwrap()
139 }
140
141 #[test]
142 fn accessors_and_metadata() {
143 let ad = AdOscillator::new();
144 assert_eq!(ad.name(), "ADOSC");
145 assert_eq!(ad.warmup_period(), 14);
146 assert!(!ad.is_ready());
147 assert_eq!(ad.value(), None);
148 assert_eq!(AdOscillator::default().warmup_period(), 14);
150 }
151
152 #[test]
153 fn seed_bar_returns_none() {
154 let mut ad = AdOscillator::new();
155 assert_eq!(ad.update(c(100.0, 101.0, 99.0, 100.0, 0)), None);
156 }
157
158 #[test]
159 fn equals_wad_line_minus_its_sma() {
160 let candles: Vec<Candle> = (0..80_i64)
163 .map(|i| {
164 let base = 100.0 + (i as f64 * 0.3).sin() * 6.0;
165 c(
166 base,
167 base + 2.0,
168 base - 2.0,
169 base + (i as f64 * 0.5).cos(),
170 i,
171 )
172 })
173 .collect();
174 let osc = AdOscillator::new().batch(&candles);
175 let line = Wad::new().batch(&candles);
177 let mut sma = Sma::new(SIGNAL_PERIOD).unwrap();
178 let expected: Vec<Option<f64>> = line
179 .iter()
180 .map(|v| v.and_then(|l| sma.update(l).map(|s| l - s)))
181 .collect();
182 assert_eq!(osc, expected);
183 }
184
185 #[test]
186 fn flat_market_oscillates_at_zero() {
187 let mut ad = AdOscillator::new();
190 let candles: Vec<Candle> = (0..40).map(|i| c(50.0, 50.0, 50.0, 50.0, i)).collect();
191 let out = ad.batch(&candles);
192 for v in out.iter().skip(ad.warmup_period() - 1).flatten() {
193 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
194 }
195 }
196
197 #[test]
198 fn warmup_emits_at_warmup_period() {
199 let mut ad = AdOscillator::new();
200 let candles: Vec<Candle> = (0..20)
201 .map(|i| {
202 let close = 100.0 + f64::from(i);
203 c(close, close + 2.0, close - 2.0, close, i64::from(i))
204 })
205 .collect();
206 let out = ad.batch(&candles);
207 assert_eq!(ad.warmup_period(), 14);
208 for v in out.iter().take(13) {
209 assert!(v.is_none());
210 }
211 assert!(out[13].is_some());
212 }
213
214 #[test]
215 fn reset_clears_state() {
216 let mut ad = AdOscillator::new();
217 let candles: Vec<Candle> = (0..30)
218 .map(|i| {
219 let close = 100.0 + f64::from(i);
220 c(close, close + 2.0, close - 2.0, close, i64::from(i))
221 })
222 .collect();
223 ad.batch(&candles);
224 assert!(ad.is_ready());
225 ad.reset();
226 assert!(!ad.is_ready());
227 assert_eq!(ad.value(), None);
228 }
229
230 #[test]
231 fn batch_equals_streaming() {
232 let candles: Vec<Candle> = (0..100_i64)
233 .map(|i| {
234 let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
235 c(base, base + 1.5, base - 1.5, base + 0.4, i)
236 })
237 .collect();
238 let batch = AdOscillator::new().batch(&candles);
239 let mut s = AdOscillator::new();
240 let streamed: Vec<_> = candles.iter().map(|x| s.update(*x)).collect();
241 assert_eq!(batch, streamed);
242 }
243}