mantis_ta/indicators/volume/
accum_dist.rs1use crate::indicators::Indicator;
2use crate::types::Candle;
3
4#[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}