Skip to main content

indicators/volume/
adl.rs

1//! Accumulation/Distribution Line (ADL).
2//!
3//! Python source: `indicators/trend/accumulation_distribution_line.py :: class ADLineIndicator`
4//!
5//! # Python algorithm (to port)
6//! ```python
7//! # Money Flow Multiplier (MFM):
8//! mfm = ((close - low) - (high - close)) / (high - low)
9//! mfm[high == low] = 0          # avoid division by zero
10//!
11//! # Money Flow Volume:
12//! mfv = mfm * volume
13//!
14//! # ADL = cumulative sum of MFV
15//! adl = mfv.cumsum()
16//! ```
17//!
18//! Output column: `"ADL"`.
19
20use std::collections::HashMap;
21
22use crate::error::IndicatorError;
23use crate::indicator::{Indicator, IndicatorOutput};
24use crate::types::Candle;
25
26// ── Indicator struct ──────────────────────────────────────────────────────────
27
28/// Accumulation/Distribution Line.  No configurable parameters.
29#[derive(Debug, Clone, Default)]
30pub struct Adl;
31
32impl Adl {
33    pub fn new() -> Self {
34        Self
35    }
36}
37
38impl Indicator for Adl {
39    fn name(&self) -> &'static str {
40        "ADL"
41    }
42    fn required_len(&self) -> usize {
43        1
44    }
45    fn required_columns(&self) -> &[&'static str] {
46        &["high", "low", "close", "volume"]
47    }
48
49    /// Ports `mfv.cumsum()` where `mfv = mfm * volume` and
50    /// `mfm = ((close - low) - (high - close)) / (high - low)`.
51    ///
52    /// When `high == low` the multiplier is clamped to `0` (matching the
53    /// Python `mfm[high == low] = 0` mask), so no divide-by-zero can occur.
54    fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
55        self.check_len(candles)?;
56
57        let mut adl = 0.0f64;
58        let values: Vec<f64> = candles
59            .iter()
60            .map(|c| {
61                let range = c.high - c.low;
62                let mfm = if range == 0.0 {
63                    0.0
64                } else {
65                    ((c.close - c.low) - (c.high - c.close)) / range
66                };
67                adl += mfm * c.volume;
68                adl
69            })
70            .collect();
71
72        Ok(IndicatorOutput::from_pairs([("ADL".to_string(), values)]))
73    }
74}
75
76// ── Registry factory ──────────────────────────────────────────────────────────
77
78pub fn factory<S: ::std::hash::BuildHasher>(
79    _params: &HashMap<String, String, S>,
80) -> Result<Box<dyn Indicator>, IndicatorError> {
81    Ok(Box::new(Adl::new()))
82}
83
84// ── Tests ─────────────────────────────────────────────────────────────────────
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    fn candle(h: f64, l: f64, c: f64, v: f64) -> Candle {
91        Candle {
92            time: 0,
93            open: c,
94            high: h,
95            low: l,
96            close: c,
97            volume: v,
98        }
99    }
100
101    #[test]
102    fn adl_zero_range_no_panic() {
103        // high == low should produce mfm=0, not a divide-by-zero
104        let bars = vec![candle(5.0, 5.0, 5.0, 1000.0)];
105        let out = Adl::new().calculate(&bars).unwrap();
106        let vals = out.get("ADL").unwrap();
107        assert_eq!(vals[0], 0.0);
108    }
109
110    #[test]
111    fn adl_full_positive_bar() {
112        // close==high → mfm=1, mfv=volume, adl=volume
113        let bars = vec![candle(10.0, 8.0, 10.0, 500.0)];
114        let out = Adl::new().calculate(&bars).unwrap();
115        let vals = out.get("ADL").unwrap();
116        // mfm = ((10-8)-(10-10))/(10-8) = 2/2 = 1; mfv = 500
117        assert!((vals[0] - 500.0).abs() < 1e-9, "got {}", vals[0]);
118    }
119
120    #[test]
121    fn adl_is_cumulative() {
122        // Two identical bars: ADL[1] = 2 * ADL[0]
123        let bars = vec![candle(10.0, 8.0, 9.0, 100.0); 2];
124        let out = Adl::new().calculate(&bars).unwrap();
125        let vals = out.get("ADL").unwrap();
126        assert!((vals[1] - 2.0 * vals[0]).abs() < 1e-9);
127    }
128
129    #[test]
130    fn factory_creates_adl() {
131        let ind = factory(&HashMap::new()).unwrap();
132        assert_eq!(ind.name(), "ADL");
133    }
134}