Skip to main content

wickra_core/indicators/
wad.rs

1//! Williams Accumulation/Distribution (WAD) — Larry Williams' cumulative line.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Williams Accumulation/Distribution — a cumulative price-only line that adds
7/// the day's accumulation on up-closes and subtracts the day's distribution on
8/// down-closes.
9///
10/// ```text
11/// if close > prev_close:  AD =  close − min(low,  prev_close)   (true low)
12/// if close < prev_close:  AD =  close − max(high, prev_close)   (true high)
13/// if close = prev_close:  AD =  0
14/// WAD_t = WAD_{t−1} + AD
15/// ```
16///
17/// Larry Williams' A/D line (distinct from Chaikin's volume-based
18/// [`Adl`](crate::Adl)) uses **no volume at all** — it measures accumulation as
19/// how far price closed above the *true low* on up-days and distribution as how
20/// far it closed below the *true high* on down-days, then accumulates the result.
21/// A rising WAD that diverges from a flat or falling price is the classic
22/// accumulation signal; a falling WAD under a rising price warns of distribution.
23///
24/// The line is unbounded and its absolute level is meaningless — only its slope
25/// and divergences against price matter. The first candle has no previous close,
26/// so it seeds the reference and emits nothing; thereafter every bar emits the
27/// running total. Each `update` is O(1).
28///
29/// # Example
30///
31/// ```
32/// use wickra_core::{Candle, Indicator, Wad};
33///
34/// let mut indicator = Wad::new();
35/// let mut last = None;
36/// for i in 0..20 {
37///     let base = 100.0 + f64::from(i);
38///     let c = Candle::new(base, base + 1.0, base - 1.0, base + 0.5, 1_000.0, 0).unwrap();
39///     last = indicator.update(c);
40/// }
41/// assert!(last.is_some());
42/// ```
43#[derive(Debug, Clone, Default)]
44pub struct Wad {
45    prev_close: Option<f64>,
46    line: f64,
47    last: Option<f64>,
48}
49
50impl Wad {
51    /// Construct a new Williams A/D line. The line is parameter-free.
52    #[must_use]
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Current value if available.
58    pub const fn value(&self) -> Option<f64> {
59        self.last
60    }
61}
62
63impl Indicator for Wad {
64    type Input = Candle;
65    type Output = f64;
66
67    fn update(&mut self, candle: Candle) -> Option<f64> {
68        let Some(prev_close) = self.prev_close else {
69            self.prev_close = Some(candle.close);
70            return None;
71        };
72        let ad = if candle.close > prev_close {
73            candle.close - candle.low.min(prev_close)
74        } else if candle.close < prev_close {
75            candle.close - candle.high.max(prev_close)
76        } else {
77            0.0
78        };
79        self.line += ad;
80        self.prev_close = Some(candle.close);
81        self.last = Some(self.line);
82        Some(self.line)
83    }
84
85    fn reset(&mut self) {
86        self.prev_close = None;
87        self.line = 0.0;
88        self.last = None;
89    }
90
91    fn warmup_period(&self) -> usize {
92        // The first bar only seeds the reference close; the first value lands on
93        // the second bar.
94        2
95    }
96
97    fn is_ready(&self) -> bool {
98        self.last.is_some()
99    }
100
101    fn name(&self) -> &'static str {
102        "Wad"
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::traits::BatchExt;
110    use approx::assert_relative_eq;
111
112    fn candle(high: f64, low: f64, close: f64) -> Candle {
113        Candle::new_unchecked(low, high, low, close, 1_000.0, 0)
114    }
115
116    #[test]
117    fn accessors_and_metadata() {
118        let wad = Wad::new();
119        assert_eq!(wad.warmup_period(), 2);
120        assert_eq!(wad.name(), "Wad");
121        assert!(!wad.is_ready());
122        assert_eq!(wad.value(), None);
123    }
124
125    #[test]
126    fn first_bar_seeds_without_output() {
127        let mut wad = Wad::new();
128        assert_eq!(wad.update(candle(101.0, 99.0, 100.0)), None);
129        assert!(wad.update(candle(102.0, 100.0, 101.0)).is_some());
130    }
131
132    #[test]
133    fn up_close_accumulates() {
134        // close rises from 100 -> 101; true low = min(low, prev_close) = min(100,100)=100;
135        // AD = 101 - 100 = 1.
136        let mut wad = Wad::new();
137        wad.update(candle(101.0, 99.0, 100.0));
138        let v = wad.update(candle(102.0, 100.0, 101.0)).unwrap();
139        assert_relative_eq!(v, 1.0, epsilon = 1e-9);
140    }
141
142    #[test]
143    fn down_close_distributes() {
144        // close falls 100 -> 99; true high = max(high, prev_close) = max(101,100)=101;
145        // AD = 99 - 101 = -2.
146        let mut wad = Wad::new();
147        wad.update(candle(102.0, 100.0, 100.0));
148        let v = wad.update(candle(101.0, 98.0, 99.0)).unwrap();
149        assert_relative_eq!(v, -2.0, epsilon = 1e-9);
150    }
151
152    #[test]
153    fn unchanged_close_adds_nothing() {
154        let mut wad = Wad::new();
155        wad.update(candle(101.0, 99.0, 100.0));
156        let v = wad.update(candle(105.0, 95.0, 100.0)).unwrap();
157        assert_relative_eq!(v, 0.0, epsilon = 1e-12);
158    }
159
160    #[test]
161    fn pure_uptrend_is_monotone() {
162        let mut wad = Wad::new();
163        let candles: Vec<Candle> = (0..30)
164            .map(|i| {
165                let base = 100.0 + f64::from(i);
166                candle(base + 1.0, base - 1.0, base)
167            })
168            .collect();
169        let mut prev = f64::NEG_INFINITY;
170        for v in wad.batch(&candles).into_iter().flatten() {
171            assert!(v >= prev, "WAD must rise in an uptrend");
172            prev = v;
173        }
174    }
175
176    #[test]
177    fn reset_clears_state() {
178        let mut wad = Wad::new();
179        let candles: Vec<Candle> = (0..10)
180            .map(|i| {
181                let base = 100.0 + f64::from(i);
182                candle(base + 1.0, base - 1.0, base)
183            })
184            .collect();
185        wad.batch(&candles);
186        assert!(wad.is_ready());
187        wad.reset();
188        assert!(!wad.is_ready());
189        assert_eq!(wad.value(), None);
190        assert_eq!(wad.update(candle(101.0, 99.0, 100.0)), None);
191    }
192
193    #[test]
194    fn batch_equals_streaming() {
195        let candles: Vec<Candle> = (0..80)
196            .map(|i| {
197                let base = 100.0 + (f64::from(i) * 0.3).sin() * 8.0;
198                candle(base + 2.0, base - 2.0, base + 0.5)
199            })
200            .collect();
201        let batch = Wad::new().batch(&candles);
202        let mut b = Wad::new();
203        let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
204        assert_eq!(batch, streamed);
205    }
206}