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)]
54pub struct AccelerationBands {
55 upper_sma: Sma,
56 middle_sma: Sma,
57 lower_sma: Sma,
58 factor: f64,
59 period: usize,
60}
61
62impl AccelerationBands {
63 pub fn new(period: usize, factor: f64) -> Result<Self> {
70 if !factor.is_finite() || factor <= 0.0 {
71 return Err(Error::NonPositiveMultiplier);
72 }
73 Ok(Self {
74 upper_sma: Sma::new(period)?,
75 middle_sma: Sma::new(period)?,
76 lower_sma: Sma::new(period)?,
77 factor,
78 period,
79 })
80 }
81
82 pub fn classic() -> Self {
84 Self::new(20, 0.001).expect("classic Acceleration Bands parameters are valid")
85 }
86
87 pub const fn parameters(&self) -> (usize, f64) {
89 (self.period, self.factor)
90 }
91}
92
93impl Indicator for AccelerationBands {
94 type Input = Candle;
95 type Output = AccelerationBandsOutput;
96
97 #[inline]
98 fn update(&mut self, candle: Candle) -> Option<AccelerationBandsOutput> {
99 let sum_hl = candle.high + candle.low;
104 let ratio = if sum_hl == 0.0 {
105 0.0
106 } else {
107 (candle.high - candle.low) / sum_hl
108 };
109 let raw_up = candle.high * self.factor.mul_add(ratio, 1.0);
110 let raw_lo = candle.low * (-self.factor).mul_add(ratio, 1.0);
111
112 let upper = self.upper_sma.update(raw_up);
114 let middle = self.middle_sma.update(candle.close);
115 let lower = self.lower_sma.update(raw_lo);
116 let (upper, middle, lower) = (upper?, middle?, lower?);
117 Some(AccelerationBandsOutput {
118 upper,
119 middle,
120 lower,
121 })
122 }
123
124 fn reset(&mut self) {
125 self.upper_sma.reset();
126 self.middle_sma.reset();
127 self.lower_sma.reset();
128 }
129
130 #[inline]
131 fn warmup_period(&self) -> usize {
132 self.period
133 }
134
135 #[inline]
136 fn is_ready(&self) -> bool {
137 self.middle_sma.is_ready()
138 }
139
140 #[inline]
141 fn name(&self) -> &'static str {
142 "AccelerationBands"
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::traits::BatchExt;
150 use approx::assert_relative_eq;
151
152 fn c(h: f64, l: f64, cl: f64) -> Candle {
153 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
154 }
155
156 #[test]
157 fn rejects_zero_period() {
158 assert!(matches!(
159 AccelerationBands::new(0, 0.001),
160 Err(Error::PeriodZero)
161 ));
162 }
163
164 #[test]
165 fn rejects_non_positive_factor() {
166 assert!(matches!(
167 AccelerationBands::new(20, 0.0),
168 Err(Error::NonPositiveMultiplier)
169 ));
170 assert!(matches!(
171 AccelerationBands::new(20, -1.0),
172 Err(Error::NonPositiveMultiplier)
173 ));
174 assert!(matches!(
175 AccelerationBands::new(20, f64::NAN),
176 Err(Error::NonPositiveMultiplier)
177 ));
178 }
179
180 #[test]
181 fn accessors_and_metadata() {
182 let ab = AccelerationBands::classic();
183 let (p, f) = ab.parameters();
184 assert_eq!(p, 20);
185 assert_relative_eq!(f, 0.001, epsilon = 1e-12);
186 assert_eq!(ab.warmup_period(), 20);
187 assert_eq!(ab.name(), "AccelerationBands");
188 }
189
190 #[test]
191 fn flat_market_collapses_to_constant() {
192 let candles: Vec<Candle> = (0..30).map(|_| c(10.0, 10.0, 10.0)).collect();
195 let mut ab = AccelerationBands::new(5, 0.5).unwrap();
196 let last = ab.batch(&candles).into_iter().flatten().last().unwrap();
197 assert_relative_eq!(last.middle, 10.0, epsilon = 1e-9);
198 assert_relative_eq!(last.upper, 10.0, epsilon = 1e-9);
199 assert_relative_eq!(last.lower, 10.0, epsilon = 1e-9);
200 }
201
202 #[test]
203 fn warmup_returns_none() {
204 let mut ab = AccelerationBands::new(5, 0.001).unwrap();
205 for i in 0..4 {
206 let base = 100.0 + f64::from(i);
207 assert!(ab.update(c(base + 1.0, base - 1.0, base)).is_none());
208 }
209 assert!(ab.update(c(105.0, 103.0, 104.0)).is_some());
210 }
211
212 #[test]
213 fn upper_above_middle_above_lower() {
214 let candles: Vec<Candle> = (0..50)
215 .map(|i| {
216 let m = 100.0 + (f64::from(i) * 0.2).sin() * 5.0;
217 c(m + 1.0, m - 1.0, m)
218 })
219 .collect();
220 let mut ab = AccelerationBands::new(20, 0.5).unwrap();
221 for o in ab.batch(&candles).into_iter().flatten() {
222 assert!(o.upper >= o.middle, "{} < {}", o.upper, o.middle);
223 assert!(o.middle >= o.lower, "{} < {}", o.middle, o.lower);
224 }
225 }
226
227 #[test]
228 fn batch_equals_streaming() {
229 let candles: Vec<Candle> = (0..40)
230 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
231 .collect();
232 let mut a = AccelerationBands::new(10, 0.5).unwrap();
233 let mut b = AccelerationBands::new(10, 0.5).unwrap();
234 assert_eq!(
235 a.batch(&candles),
236 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
237 );
238 }
239
240 #[test]
241 fn reset_clears_state() {
242 let candles: Vec<Candle> = (0..10)
243 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
244 .collect();
245 let mut ab = AccelerationBands::new(5, 0.5).unwrap();
246 ab.batch(&candles);
247 assert!(ab.is_ready());
248 ab.reset();
249 assert!(!ab.is_ready());
250 assert_eq!(ab.update(candles[0]), None);
251 }
252
253 #[test]
254 fn zero_price_candle_collapses_ratio_to_zero() {
255 let zero = Candle::new(0.0, 0.0, 0.0, 0.0, 1.0, 0).unwrap();
260 let mut ab = AccelerationBands::new(1, 0.5).unwrap();
261 let v = ab.update(zero).unwrap();
262 assert_relative_eq!(v.upper, 0.0, epsilon = 1e-12);
263 assert_relative_eq!(v.middle, 0.0, epsilon = 1e-12);
264 assert_relative_eq!(v.lower, 0.0, epsilon = 1e-12);
265 }
266
267 #[test]
274 fn reference_value_single_bar() {
275 let mut ab = AccelerationBands::new(1, 0.5).unwrap();
276 let v = ab.update(c(12.0, 8.0, 10.0)).unwrap();
277 assert_relative_eq!(v.upper, 13.2, epsilon = 1e-12);
278 assert_relative_eq!(v.middle, 10.0, epsilon = 1e-12);
279 assert_relative_eq!(v.lower, 7.2, epsilon = 1e-12);
280 }
281}