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    /// Continue from an accumulation `total` computed elsewhere (a batch
54    /// kernel), as if every earlier bar had been fed.
55    pub(crate) fn resume_at(&mut self, total: f64) {
56        self.total = total;
57        self.has_emitted = true;
58    }
59
60    /// Current cumulative value if at least one candle has been ingested.
61    pub const fn value(&self) -> Option<f64> {
62        if self.has_emitted {
63            Some(self.total)
64        } else {
65            None
66        }
67    }
68}
69
70impl Indicator for Adl {
71    type Input = Candle;
72    type Output = f64;
73
74    #[inline]
75    fn update(&mut self, candle: Candle) -> Option<f64> {
76        let range = candle.high - candle.low;
77        let mfv = if range == 0.0 {
78            // A zero-range bar carries no positional information.
79            0.0
80        } else {
81            let mfm = ((candle.close - candle.low) - (candle.high - candle.close)) / range;
82            mfm * candle.volume
83        };
84        self.total += mfv;
85        self.has_emitted = true;
86        Some(self.total)
87    }
88
89    fn reset(&mut self) {
90        self.total = 0.0;
91        self.has_emitted = false;
92    }
93
94    #[inline]
95    fn warmup_period(&self) -> usize {
96        1
97    }
98
99    #[inline]
100    fn is_ready(&self) -> bool {
101        self.has_emitted
102    }
103
104    #[inline]
105    fn name(&self) -> &'static str {
106        "ADL"
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(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
117        Candle::new(open, high, low, close, volume, ts).unwrap()
118    }
119
120    #[test]
121    fn reference_values() {
122        // bar 1: close at high -> MFM = +1 -> MFV = +100; ADL = 100.
123        // bar 2: h=12 l=8 c=9  -> MFM = ((9-8)-(12-9))/4 = -0.5 -> MFV = -100;
124        //        ADL = 100 - 100 = 0.
125        let mut adl = Adl::new();
126        let out = adl.batch(&[
127            candle(8.0, 10.0, 8.0, 10.0, 100.0, 0),
128            candle(10.0, 12.0, 8.0, 9.0, 200.0, 1),
129        ]);
130        assert_relative_eq!(out[0].unwrap(), 100.0, epsilon = 1e-12);
131        assert_relative_eq!(out[1].unwrap(), 0.0, epsilon = 1e-12);
132    }
133
134    #[test]
135    fn emits_from_first_candle() {
136        let mut adl = Adl::new();
137        assert_eq!(adl.warmup_period(), 1);
138        assert!(adl.update(candle(8.0, 10.0, 8.0, 9.0, 50.0, 0)).is_some());
139    }
140
141    /// Cover the Indicator-impl `name` body (94-96). The other accessors
142    /// are exercised by existing tests; `name` was never queried.
143    #[test]
144    fn accessors_and_metadata() {
145        let adl = Adl::new();
146        assert_eq!(adl.name(), "ADL");
147    }
148
149    #[test]
150    fn close_at_high_accumulates_full_volume() {
151        // Every bar closes at its high: MFM = +1, so ADL grows by `volume`.
152        let mut adl = Adl::new();
153        let mut expected = 0.0;
154        for i in 0..10 {
155            let c = candle(8.0, 10.0, 8.0, 10.0, 25.0, i);
156            expected += 25.0;
157            assert_relative_eq!(adl.update(c).unwrap(), expected, epsilon = 1e-9);
158        }
159    }
160
161    #[test]
162    fn zero_range_bar_contributes_nothing() {
163        let mut adl = Adl::new();
164        adl.update(candle(8.0, 10.0, 8.0, 10.0, 100.0, 0));
165        let before = adl.value().unwrap();
166        // A flat candle (high == low) adds zero.
167        let after = adl.update(candle(9.0, 9.0, 9.0, 9.0, 999.0, 1)).unwrap();
168        assert_relative_eq!(after, before, epsilon = 1e-12);
169    }
170
171    #[test]
172    fn reset_clears_state() {
173        let mut adl = Adl::new();
174        adl.batch(&[
175            candle(8.0, 10.0, 8.0, 9.0, 100.0, 0),
176            candle(9.0, 11.0, 9.0, 10.0, 100.0, 1),
177        ]);
178        assert!(adl.is_ready());
179        adl.reset();
180        assert!(!adl.is_ready());
181        assert_eq!(adl.value(), None);
182    }
183
184    #[test]
185    fn batch_equals_streaming() {
186        let candles: Vec<Candle> = (0..60)
187            .map(|i| {
188                let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
189                candle(
190                    mid,
191                    mid + 2.0,
192                    mid - 2.0,
193                    mid + 0.5,
194                    10.0 + (i % 5) as f64,
195                    i,
196                )
197            })
198            .collect();
199        let batch = Adl::new().batch(&candles);
200        let mut b = Adl::new();
201        let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
202        assert_eq!(batch, streamed);
203    }
204}