1use crate::error::{Error, Result};
4use crate::indicators::sma::Sma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct AccelerationBandsOutput {
12 pub upper: f64,
14 pub middle: f64,
16 pub lower: f64,
18}
19
20#[derive(Debug, Clone)]
56pub struct AccelerationBands {
57 upper_sma: Sma,
58 middle_sma: Sma,
59 lower_sma: Sma,
60 factor: f64,
61 period: usize,
62}
63
64impl AccelerationBands {
65 pub fn new(period: usize, factor: f64) -> Result<Self> {
72 if !factor.is_finite() || factor <= 0.0 {
73 return Err(Error::NonPositiveMultiplier);
74 }
75 Ok(Self {
76 upper_sma: Sma::new(period)?,
77 middle_sma: Sma::new(period)?,
78 lower_sma: Sma::new(period)?,
79 factor,
80 period,
81 })
82 }
83
84 pub fn classic() -> Self {
86 Self::new(20, 4.0).expect("classic Acceleration Bands parameters are valid")
87 }
88
89 pub const fn parameters(&self) -> (usize, f64) {
91 (self.period, self.factor)
92 }
93}
94
95impl Indicator for AccelerationBands {
96 type Input = Candle;
97 type Output = AccelerationBandsOutput;
98
99 #[inline]
100 fn update(&mut self, candle: Candle) -> Option<AccelerationBandsOutput> {
101 let sum_hl = candle.high + candle.low;
106 let ratio = if sum_hl == 0.0 {
107 0.0
108 } else {
109 (candle.high - candle.low) / sum_hl
110 };
111 let raw_up = candle.high * self.factor.mul_add(ratio, 1.0);
112 let raw_lo = candle.low * (-self.factor).mul_add(ratio, 1.0);
113
114 let upper = self.upper_sma.update(raw_up);
116 let middle = self.middle_sma.update(candle.close);
117 let lower = self.lower_sma.update(raw_lo);
118 let (upper, middle, lower) = (upper?, middle?, lower?);
119 Some(AccelerationBandsOutput {
120 upper,
121 middle,
122 lower,
123 })
124 }
125
126 fn reset(&mut self) {
127 self.upper_sma.reset();
128 self.middle_sma.reset();
129 self.lower_sma.reset();
130 }
131
132 #[inline]
133 fn warmup_period(&self) -> usize {
134 self.period
135 }
136
137 #[inline]
138 fn is_ready(&self) -> bool {
139 self.middle_sma.is_ready()
140 }
141
142 #[inline]
143 fn name(&self) -> &'static str {
144 "AccelerationBands"
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::traits::BatchExt;
152 use approx::assert_relative_eq;
153
154 fn c(h: f64, l: f64, cl: f64) -> Candle {
155 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
156 }
157
158 #[test]
159 fn rejects_zero_period() {
160 assert!(matches!(
161 AccelerationBands::new(0, 0.001),
162 Err(Error::PeriodZero)
163 ));
164 }
165
166 #[test]
167 fn rejects_non_positive_factor() {
168 assert!(matches!(
169 AccelerationBands::new(20, 0.0),
170 Err(Error::NonPositiveMultiplier)
171 ));
172 assert!(matches!(
173 AccelerationBands::new(20, -1.0),
174 Err(Error::NonPositiveMultiplier)
175 ));
176 assert!(matches!(
177 AccelerationBands::new(20, f64::NAN),
178 Err(Error::NonPositiveMultiplier)
179 ));
180 }
181
182 #[test]
183 fn accessors_and_metadata() {
184 let ab = AccelerationBands::classic();
185 let (p, f) = ab.parameters();
186 assert_eq!(p, 20);
187 assert_relative_eq!(f, 4.0, epsilon = 1e-12);
188 assert_eq!(ab.warmup_period(), 20);
189 assert_eq!(ab.name(), "AccelerationBands");
190 }
191
192 #[test]
193 fn flat_market_collapses_to_constant() {
194 let candles: Vec<Candle> = (0..30).map(|_| c(10.0, 10.0, 10.0)).collect();
197 let mut ab = AccelerationBands::new(5, 0.5).unwrap();
198 let last = ab.batch(&candles).into_iter().flatten().last().unwrap();
199 assert_relative_eq!(last.middle, 10.0, epsilon = 1e-9);
200 assert_relative_eq!(last.upper, 10.0, epsilon = 1e-9);
201 assert_relative_eq!(last.lower, 10.0, epsilon = 1e-9);
202 }
203
204 #[test]
205 fn warmup_returns_none() {
206 let mut ab = AccelerationBands::new(5, 0.001).unwrap();
207 for i in 0..4 {
208 let base = 100.0 + f64::from(i);
209 assert!(ab.update(c(base + 1.0, base - 1.0, base)).is_none());
210 }
211 assert!(ab.update(c(105.0, 103.0, 104.0)).is_some());
212 }
213
214 #[test]
215 fn upper_above_middle_above_lower() {
216 let candles: Vec<Candle> = (0..50)
217 .map(|i| {
218 let m = 100.0 + (f64::from(i) * 0.2).sin() * 5.0;
219 c(m + 1.0, m - 1.0, m)
220 })
221 .collect();
222 let mut ab = AccelerationBands::new(20, 0.5).unwrap();
223 for o in ab.batch(&candles).into_iter().flatten() {
224 assert!(o.upper >= o.middle, "{} < {}", o.upper, o.middle);
225 assert!(o.middle >= o.lower, "{} < {}", o.middle, o.lower);
226 }
227 }
228
229 #[test]
230 fn batch_equals_streaming() {
231 let candles: Vec<Candle> = (0..40)
232 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
233 .collect();
234 let mut a = AccelerationBands::new(10, 0.5).unwrap();
235 let mut b = AccelerationBands::new(10, 0.5).unwrap();
236 assert_eq!(
237 a.batch(&candles),
238 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
239 );
240 }
241
242 #[test]
243 fn reset_clears_state() {
244 let candles: Vec<Candle> = (0..10)
245 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
246 .collect();
247 let mut ab = AccelerationBands::new(5, 0.5).unwrap();
248 ab.batch(&candles);
249 assert!(ab.is_ready());
250 ab.reset();
251 assert!(!ab.is_ready());
252 assert_eq!(ab.update(candles[0]), None);
253 }
254
255 #[test]
256 fn zero_price_candle_collapses_ratio_to_zero() {
257 let zero = Candle::new(0.0, 0.0, 0.0, 0.0, 1.0, 0).unwrap();
262 let mut ab = AccelerationBands::new(1, 0.5).unwrap();
263 let v = ab.update(zero).unwrap();
264 assert_relative_eq!(v.upper, 0.0, epsilon = 1e-12);
265 assert_relative_eq!(v.middle, 0.0, epsilon = 1e-12);
266 assert_relative_eq!(v.lower, 0.0, epsilon = 1e-12);
267 }
268
269 #[test]
276 fn reference_value_single_bar() {
277 let mut ab = AccelerationBands::new(1, 0.5).unwrap();
278 let v = ab.update(c(12.0, 8.0, 10.0)).unwrap();
279 assert_relative_eq!(v.upper, 13.2, epsilon = 1e-12);
280 assert_relative_eq!(v.middle, 10.0, epsilon = 1e-12);
281 assert_relative_eq!(v.lower, 7.2, epsilon = 1e-12);
282 }
283
284 #[test]
285 fn rejects_infinite_factor_and_oversized_period() {
286 assert!(matches!(
287 AccelerationBands::new(20, f64::INFINITY),
288 Err(Error::NonPositiveMultiplier)
289 ));
290 let too_long = crate::error::MAX_PERIOD + 1;
291 assert!(matches!(
292 AccelerationBands::new(too_long, 4.0),
293 Err(Error::InvalidPeriod { .. })
294 ));
295 }
296
297 fn wavy_candles(len: i32) -> Vec<Candle> {
298 (0..len)
299 .map(|i| {
300 let mid = 100.0 + (f64::from(i) * 0.4).sin() * 6.0;
301 let half = 0.5 + (f64::from(i) * 0.9).cos().abs() * 2.0;
302 c(mid + half, mid - half, mid + 0.3)
303 })
304 .collect()
305 }
306
307 #[test]
308 fn first_value_lands_exactly_at_warmup_index() {
309 let candles = wavy_candles(30);
310 let mut ab = AccelerationBands::new(7, 4.0).unwrap();
311 let warmup = ab.warmup_period();
312 let out = ab.batch(&candles);
313 assert!(out.iter().take(warmup - 1).all(Option::is_none));
314 assert!(out.iter().skip(warmup - 1).all(Option::is_some));
315 }
316
317 #[test]
318 fn reset_replays_identically_to_fresh_instance() {
319 let candles = wavy_candles(40);
320 let mut used = AccelerationBands::classic();
321 let first = used.batch(&candles);
322 used.reset();
323 let replay = used.batch(&candles);
324 let fresh = AccelerationBands::classic().batch(&candles);
325 assert_eq!(replay, fresh);
326 assert_eq!(first, fresh);
327 }
328
329 #[test]
330 fn batch_equals_streaming_bit_identical() {
331 let candles = wavy_candles(60);
332 let batch = AccelerationBands::new(9, 4.0).unwrap().batch(&candles);
333 let mut streamer = AccelerationBands::new(9, 4.0).unwrap();
334 let identical = candles.iter().zip(&batch).all(|(candle, b)| {
335 let s = streamer.update(*candle);
336 s.map(|o| (o.upper.to_bits(), o.middle.to_bits(), o.lower.to_bits()))
337 == b.map(|o| (o.upper.to_bits(), o.middle.to_bits(), o.lower.to_bits()))
338 });
339 assert!(identical);
340 }
341
342 #[test]
350 fn reference_value_two_bars_headley_factor() {
351 let mut ab = AccelerationBands::new(2, 4.0).unwrap();
352 assert_eq!(ab.update(c(12.0, 8.0, 10.0)), None);
353 let v = ab.update(c(22.0, 18.0, 20.0)).unwrap();
354 assert_relative_eq!(v.upper, 26.2, epsilon = 1e-12);
355 assert_relative_eq!(v.middle, 15.0, epsilon = 1e-12);
356 assert_relative_eq!(v.lower, 6.2, epsilon = 1e-12);
357 }
358}