Skip to main content

mantis_ta/indicators/volume/
accum_dist.rs

1use crate::indicators::Indicator;
2use crate::types::Candle;
3
4/// Accumulation/Distribution Line.
5///
6/// A cumulative volume-flow indicator. Each bar's volume is weighted by a
7/// Close Location Value (money flow multiplier):
8///
9/// ```text
10/// MFM = ((Close - Low) - (High - Close)) / (High - Low)
11/// AD  = AD_prev + MFM * Volume
12/// ```
13///
14/// `MFM` ranges over `[-1, 1]` and measures where the close settled within
15/// the bar's high/low range (`+1` = close at the high, `-1` = close at the
16/// low). When `High == Low` the multiplier is defined as `0` (no
17/// accumulation or distribution on a zero-range bar), matching TA-Lib's
18/// `AD` function.
19///
20/// # Examples
21/// ```rust
22/// use mantis_ta::indicators::{AccumDist, Indicator};
23/// use mantis_ta::types::Candle;
24///
25/// let candles: Vec<Candle> = [
26///     (12.0, 8.0, 10.0, 100.0),
27///     (13.0, 9.0, 12.0, 200.0),
28///     (11.0, 7.0, 8.0, 150.0),
29/// ]
30/// .iter()
31/// .enumerate()
32/// .map(|(i, (h, l, c, v))| Candle {
33///     timestamp: i as i64,
34///     open: *c,
35///     high: *h,
36///     low: *l,
37///     close: *c,
38///     volume: *v,
39/// })
40/// .collect();
41///
42/// let out = AccumDist::new().calculate(&candles);
43/// assert_eq!(out[0], Some(0.0)); // ((10-8)-(12-10))/4 * 100 = 0
44/// assert_eq!(out[1], Some(100.0)); // 0 + ((12-9)-(13-12))/4 * 200 = 100
45/// assert_eq!(out[2], Some(25.0)); // 100 + ((8-7)-(11-8))/4 * 150 = 25
46/// ```
47#[derive(Debug, Clone)]
48pub struct AccumDist {
49    current: f64,
50}
51
52impl AccumDist {
53    pub fn new() -> Self {
54        Self { current: 0.0 }
55    }
56
57    #[inline]
58    fn update(&mut self, high: f64, low: f64, close: f64, volume: f64) -> f64 {
59        let range = high - low;
60        let money_flow_multiplier = if range == 0.0 {
61            0.0
62        } else {
63            ((close - low) - (high - close)) / range
64        };
65        self.current += money_flow_multiplier * volume;
66        self.current
67    }
68}
69
70impl Default for AccumDist {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl Indicator for AccumDist {
77    type Output = f64;
78
79    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
80        Some(self.update(candle.high, candle.low, candle.close, candle.volume))
81    }
82
83    fn reset(&mut self) {
84        self.current = 0.0;
85    }
86
87    fn warmup_period(&self) -> usize {
88        0
89    }
90
91    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
92        Box::new(self.clone())
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    fn candle(h: f64, l: f64, c: f64, v: f64) -> Candle {
101        Candle {
102            timestamp: 0,
103            open: c,
104            high: h,
105            low: l,
106            close: c,
107            volume: v,
108        }
109    }
110
111    #[test]
112    fn accum_dist_warmup_period_is_zero() {
113        assert_eq!(AccumDist::new().warmup_period(), 0);
114    }
115
116    #[test]
117    fn accum_dist_matches_manual_calculation() {
118        let mut ad = AccumDist::new();
119        let candles = vec![
120            candle(12.0, 8.0, 10.0, 100.0),
121            candle(13.0, 9.0, 12.0, 200.0),
122            candle(11.0, 7.0, 8.0, 150.0),
123        ];
124        let outputs: Vec<_> = candles.iter().map(|c| ad.next(c)).collect();
125
126        assert!((outputs[0].unwrap() - 0.0).abs() < 1e-9);
127        assert!((outputs[1].unwrap() - 100.0).abs() < 1e-9);
128        assert!((outputs[2].unwrap() - 25.0).abs() < 1e-9);
129    }
130
131    #[test]
132    fn accum_dist_zero_range_bar_does_not_panic_or_change_total() {
133        let mut ad = AccumDist::new();
134        ad.next(&candle(100.0, 100.0, 100.0, 10_000.0));
135        assert_eq!(ad.next(&candle(100.0, 100.0, 100.0, 10_000.0)), Some(0.0));
136    }
137
138    #[test]
139    fn accum_dist_close_at_high_adds_full_volume() {
140        let mut ad = AccumDist::new();
141        assert_eq!(ad.next(&candle(110.0, 90.0, 110.0, 500.0)), Some(500.0));
142    }
143
144    #[test]
145    fn accum_dist_close_at_low_subtracts_full_volume() {
146        let mut ad = AccumDist::new();
147        assert_eq!(ad.next(&candle(110.0, 90.0, 90.0, 500.0)), Some(-500.0));
148    }
149
150    #[test]
151    fn accum_dist_streaming_matches_batch() {
152        let candles: Vec<Candle> = (0..15)
153            .map(|i| {
154                candle(
155                    101.0 + i as f64,
156                    99.0 - i as f64 * 0.5,
157                    100.0 + i as f64 * 0.3,
158                    1_000.0 + i as f64 * 10.0,
159                )
160            })
161            .collect();
162
163        let batch = AccumDist::new().calculate(&candles);
164
165        let mut streamed_ad = AccumDist::new();
166        let streamed: Vec<_> = candles.iter().map(|c| streamed_ad.next(c)).collect();
167
168        assert_eq!(streamed, batch);
169    }
170
171    #[test]
172    fn accum_dist_reset_clears_state() {
173        let mut ad = AccumDist::new();
174        let candles: Vec<Candle> = (0..6)
175            .map(|i| candle(101.0 + i as f64, 99.0, 100.0 + i as f64 * 0.5, 1_000.0))
176            .collect();
177        for c in &candles {
178            ad.next(c);
179        }
180        ad.reset();
181
182        let mut fresh = AccumDist::new();
183        for c in &candles {
184            assert_eq!(ad.next(c), fresh.next(c));
185        }
186    }
187}