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    #[inline]
68    fn update(&mut self, candle: Candle) -> Option<f64> {
69        let Some(prev_close) = self.prev_close else {
70            self.prev_close = Some(candle.close);
71            return None;
72        };
73        let ad = if candle.close > prev_close {
74            candle.close - candle.low.min(prev_close)
75        } else if candle.close < prev_close {
76            candle.close - candle.high.max(prev_close)
77        } else {
78            0.0
79        };
80        self.line += ad;
81        self.prev_close = Some(candle.close);
82        self.last = Some(self.line);
83        Some(self.line)
84    }
85
86    fn reset(&mut self) {
87        self.prev_close = None;
88        self.line = 0.0;
89        self.last = None;
90    }
91
92    #[inline]
93    fn warmup_period(&self) -> usize {
94        // The first bar only seeds the reference close; the first value lands on
95        // the second bar.
96        2
97    }
98
99    #[inline]
100    fn is_ready(&self) -> bool {
101        self.last.is_some()
102    }
103
104    #[inline]
105    fn name(&self) -> &'static str {
106        "Wad"
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::traits::BatchExt;
114    use approx::assert_relative_eq;
115
116    fn candle(high: f64, low: f64, close: f64) -> Candle {
117        Candle::new_unchecked(low, high, low, close, 1_000.0, 0)
118    }
119
120    #[test]
121    fn accessors_and_metadata() {
122        let wad = Wad::new();
123        assert_eq!(wad.warmup_period(), 2);
124        assert_eq!(wad.name(), "Wad");
125        assert!(!wad.is_ready());
126        assert_eq!(wad.value(), None);
127    }
128
129    #[test]
130    fn first_bar_seeds_without_output() {
131        let mut wad = Wad::new();
132        assert_eq!(wad.update(candle(101.0, 99.0, 100.0)), None);
133        assert!(wad.update(candle(102.0, 100.0, 101.0)).is_some());
134    }
135
136    #[test]
137    fn up_close_accumulates() {
138        // close rises from 100 -> 101; true low = min(low, prev_close) = min(100,100)=100;
139        // AD = 101 - 100 = 1.
140        let mut wad = Wad::new();
141        wad.update(candle(101.0, 99.0, 100.0));
142        let v = wad.update(candle(102.0, 100.0, 101.0)).unwrap();
143        assert_relative_eq!(v, 1.0, epsilon = 1e-9);
144    }
145
146    #[test]
147    fn down_close_distributes() {
148        // close falls 100 -> 99; true high = max(high, prev_close) = max(101,100)=101;
149        // AD = 99 - 101 = -2.
150        let mut wad = Wad::new();
151        wad.update(candle(102.0, 100.0, 100.0));
152        let v = wad.update(candle(101.0, 98.0, 99.0)).unwrap();
153        assert_relative_eq!(v, -2.0, epsilon = 1e-9);
154    }
155
156    #[test]
157    fn unchanged_close_adds_nothing() {
158        let mut wad = Wad::new();
159        wad.update(candle(101.0, 99.0, 100.0));
160        let v = wad.update(candle(105.0, 95.0, 100.0)).unwrap();
161        assert_relative_eq!(v, 0.0, epsilon = 1e-12);
162    }
163
164    #[test]
165    fn pure_uptrend_is_monotone() {
166        let mut wad = Wad::new();
167        let candles: Vec<Candle> = (0..30)
168            .map(|i| {
169                let base = 100.0 + f64::from(i);
170                candle(base + 1.0, base - 1.0, base)
171            })
172            .collect();
173        let mut prev = f64::NEG_INFINITY;
174        for v in wad.batch(&candles).into_iter().flatten() {
175            assert!(v >= prev, "WAD must rise in an uptrend");
176            prev = v;
177        }
178    }
179
180    #[test]
181    fn reset_clears_state() {
182        let mut wad = Wad::new();
183        let candles: Vec<Candle> = (0..10)
184            .map(|i| {
185                let base = 100.0 + f64::from(i);
186                candle(base + 1.0, base - 1.0, base)
187            })
188            .collect();
189        wad.batch(&candles);
190        assert!(wad.is_ready());
191        wad.reset();
192        assert!(!wad.is_ready());
193        assert_eq!(wad.value(), None);
194        assert_eq!(wad.update(candle(101.0, 99.0, 100.0)), None);
195    }
196
197    #[test]
198    fn batch_equals_streaming() {
199        let candles: Vec<Candle> = (0..80)
200            .map(|i| {
201                let base = 100.0 + (f64::from(i) * 0.3).sin() * 8.0;
202                candle(base + 2.0, base - 2.0, base + 0.5)
203            })
204            .collect();
205        let batch = Wad::new().batch(&candles);
206        let mut b = Wad::new();
207        let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
208        assert_eq!(batch, streamed);
209    }
210}