Skip to main content

wickra_core/indicators/
ad_oscillator.rs

1//! Williams A/D Oscillator (ADOSC).
2
3use crate::indicators::sma::Sma;
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7/// Smoothing window applied to the Williams A/D line to form the oscillator.
8const SIGNAL_PERIOD: usize = 13;
9
10/// Williams **A/D Oscillator** — the volume-free Williams Accumulation/
11/// Distribution line measured against its own moving average, so it oscillates
12/// around zero instead of drifting like the cumulative line.
13///
14/// The underlying line is Larry Williams' volume-less A/D (1972), which uses a
15/// *true* high/low anchored on the prior close; the oscillator subtracts its
16/// 13-bar simple moving average:
17///
18/// ```text
19/// TR_h_t = max(close_{t−1}, high_t)
20/// TR_l_t = min(close_{t−1}, low_t)
21/// WAD_t  = WAD_{t−1} + (close_t − TR_l_t)   if close_t > close_{t−1}
22/// WAD_t  = WAD_{t−1} + (close_t − TR_h_t)   if close_t < close_{t−1}
23/// WAD_t  = WAD_{t−1}                          if close_t == close_{t−1}
24/// ADOSC_t = WAD_t − SMA(WAD, 13)_t
25/// ```
26///
27/// This is distinct from the raw cumulative line, which Wickra ships as
28/// [`Wad`](crate::Wad): `Wad` is the drifting line for divergence analysis,
29/// while this oscillator is its zero-centred, mean-reverting form (positive
30/// when accumulation is running ahead of its recent average, negative when
31/// distribution is). The first bar only seeds the previous close; the first
32/// oscillator value lands once the 13-bar average of the line is full.
33///
34/// # Example
35///
36/// ```
37/// use wickra_core::{Candle, Indicator, AdOscillator};
38///
39/// let mut indicator = AdOscillator::new();
40/// let mut last = None;
41/// for i in 0..80 {
42///     let base = 100.0 + f64::from(i);
43///     let candle =
44///         Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
45///     last = indicator.update(candle);
46/// }
47/// assert!(last.is_some());
48/// ```
49#[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    /// Construct a new Williams A/D Oscillator.
65    #[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    /// Current oscillator value if available.
76    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            // The first bar only establishes the previous close anchor.
88            self.prev_close = Some(candle.close);
89            return None;
90        };
91        let delta = if candle.close > prev {
92            // Accumulation: distance from the true low.
93            candle.close - prev.min(candle.low)
94        } else if candle.close < prev {
95            // Distribution: distance from the true high (negative).
96            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        // One seed bar establishes the prior close; the line then feeds the
117        // 13-bar signal SMA, which is full after `SIGNAL_PERIOD` line values.
118        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        // `Default` matches `new`.
149        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        // The oscillator is exactly the Williams A/D line minus its 13-SMA, so
161        // it must match the standalone `Wad` line passed through an SMA(13).
162        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        // Reconstruct: Wad line, then line − SMA(line, 13).
176        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        // A flat market never accumulates or distributes, so the line is
188        // constant and the oscillator sits at zero once warm.
189        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}