Skip to main content

wickra_core/indicators/
adl.rs

1//! Accumulation/Distribution Line.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Accumulation/Distribution Line — Marc Chaikin's cumulative volume-flow
7/// indicator.
8///
9/// Each bar contributes a *money-flow volume*: the bar's volume weighted by
10/// where the close fell within the bar's range.
11///
12/// ```text
13/// MFM_t = ((close − low) − (high − close)) / (high − low)   (the money-flow multiplier, −1..+1)
14/// MFV_t = MFM_t · volume_t
15/// ADL_t = ADL_{t−1} + MFV_t
16/// ```
17///
18/// A close near the high makes the multiplier near `+1` (accumulation), near
19/// the low near `−1` (distribution). The running total is unbounded and drifts
20/// with cumulative volume — what matters is its slope and its divergence from
21/// price. A bar with `high == low` contributes `0`.
22///
23/// # Example
24///
25/// ```
26/// use wickra_core::{Candle, Indicator, Adl};
27///
28/// let mut indicator = Adl::new();
29/// let mut last = None;
30/// for i in 0..80 {
31///     let base = 100.0 + f64::from(i);
32///     let candle =
33///         Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
34///     last = indicator.update(candle);
35/// }
36/// assert!(last.is_some());
37/// ```
38#[derive(Debug, Clone, Default)]
39pub struct Adl {
40    total: f64,
41    has_emitted: bool,
42}
43
44impl Adl {
45    /// Construct a new Accumulation/Distribution Line starting at zero.
46    pub const fn new() -> Self {
47        Self {
48            total: 0.0,
49            has_emitted: false,
50        }
51    }
52
53    /// Current cumulative value if at least one candle has been ingested.
54    pub const fn value(&self) -> Option<f64> {
55        if self.has_emitted {
56            Some(self.total)
57        } else {
58            None
59        }
60    }
61}
62
63impl Indicator for Adl {
64    type Input = Candle;
65    type Output = f64;
66
67    #[inline]
68    fn update(&mut self, candle: Candle) -> Option<f64> {
69        let range = candle.high - candle.low;
70        let mfv = if range == 0.0 {
71            // A zero-range bar carries no positional information.
72            0.0
73        } else {
74            let mfm = ((candle.close - candle.low) - (candle.high - candle.close)) / range;
75            mfm * candle.volume
76        };
77        self.total += mfv;
78        self.has_emitted = true;
79        Some(self.total)
80    }
81
82    fn reset(&mut self) {
83        self.total = 0.0;
84        self.has_emitted = false;
85    }
86
87    #[inline]
88    fn warmup_period(&self) -> usize {
89        1
90    }
91
92    #[inline]
93    fn is_ready(&self) -> bool {
94        self.has_emitted
95    }
96
97    #[inline]
98    fn name(&self) -> &'static str {
99        "ADL"
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::traits::BatchExt;
107    use approx::assert_relative_eq;
108
109    fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
110        Candle::new(open, high, low, close, volume, ts).unwrap()
111    }
112
113    #[test]
114    fn reference_values() {
115        // bar 1: close at high -> MFM = +1 -> MFV = +100; ADL = 100.
116        // bar 2: h=12 l=8 c=9  -> MFM = ((9-8)-(12-9))/4 = -0.5 -> MFV = -100;
117        //        ADL = 100 - 100 = 0.
118        let mut adl = Adl::new();
119        let out = adl.batch(&[
120            candle(8.0, 10.0, 8.0, 10.0, 100.0, 0),
121            candle(10.0, 12.0, 8.0, 9.0, 200.0, 1),
122        ]);
123        assert_relative_eq!(out[0].unwrap(), 100.0, epsilon = 1e-12);
124        assert_relative_eq!(out[1].unwrap(), 0.0, epsilon = 1e-12);
125    }
126
127    #[test]
128    fn emits_from_first_candle() {
129        let mut adl = Adl::new();
130        assert_eq!(adl.warmup_period(), 1);
131        assert!(adl.update(candle(8.0, 10.0, 8.0, 9.0, 50.0, 0)).is_some());
132    }
133
134    /// Cover the Indicator-impl `name` body (94-96). The other accessors
135    /// are exercised by existing tests; `name` was never queried.
136    #[test]
137    fn accessors_and_metadata() {
138        let adl = Adl::new();
139        assert_eq!(adl.name(), "ADL");
140    }
141
142    #[test]
143    fn close_at_high_accumulates_full_volume() {
144        // Every bar closes at its high: MFM = +1, so ADL grows by `volume`.
145        let mut adl = Adl::new();
146        let mut expected = 0.0;
147        for i in 0..10 {
148            let c = candle(8.0, 10.0, 8.0, 10.0, 25.0, i);
149            expected += 25.0;
150            assert_relative_eq!(adl.update(c).unwrap(), expected, epsilon = 1e-9);
151        }
152    }
153
154    #[test]
155    fn zero_range_bar_contributes_nothing() {
156        let mut adl = Adl::new();
157        adl.update(candle(8.0, 10.0, 8.0, 10.0, 100.0, 0));
158        let before = adl.value().unwrap();
159        // A flat candle (high == low) adds zero.
160        let after = adl.update(candle(9.0, 9.0, 9.0, 9.0, 999.0, 1)).unwrap();
161        assert_relative_eq!(after, before, epsilon = 1e-12);
162    }
163
164    #[test]
165    fn reset_clears_state() {
166        let mut adl = Adl::new();
167        adl.batch(&[
168            candle(8.0, 10.0, 8.0, 9.0, 100.0, 0),
169            candle(9.0, 11.0, 9.0, 10.0, 100.0, 1),
170        ]);
171        assert!(adl.is_ready());
172        adl.reset();
173        assert!(!adl.is_ready());
174        assert_eq!(adl.value(), None);
175    }
176
177    #[test]
178    fn batch_equals_streaming() {
179        let candles: Vec<Candle> = (0..60)
180            .map(|i| {
181                let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
182                candle(
183                    mid,
184                    mid + 2.0,
185                    mid - 2.0,
186                    mid + 0.5,
187                    10.0 + (i % 5) as f64,
188                    i,
189                )
190            })
191            .collect();
192        let batch = Adl::new().batch(&candles);
193        let mut b = Adl::new();
194        let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
195        assert_eq!(batch, streamed);
196    }
197}