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 fn update(&mut self, candle: Candle) -> Option<AccelerationBandsOutput> {
98 let sum_hl = candle.high + candle.low;
103 let ratio = if sum_hl == 0.0 {
104 0.0
105 } else {
106 (candle.high - candle.low) / sum_hl
107 };
108 let raw_up = candle.high * self.factor.mul_add(ratio, 1.0);
109 let raw_lo = candle.low * (-self.factor).mul_add(ratio, 1.0);
110
111 let upper = self.upper_sma.update(raw_up);
113 let middle = self.middle_sma.update(candle.close);
114 let lower = self.lower_sma.update(raw_lo);
115 let (upper, middle, lower) = (upper?, middle?, lower?);
116 Some(AccelerationBandsOutput {
117 upper,
118 middle,
119 lower,
120 })
121 }
122
123 fn reset(&mut self) {
124 self.upper_sma.reset();
125 self.middle_sma.reset();
126 self.lower_sma.reset();
127 }
128
129 fn warmup_period(&self) -> usize {
130 self.period
131 }
132
133 fn is_ready(&self) -> bool {
134 self.middle_sma.is_ready()
135 }
136
137 fn name(&self) -> &'static str {
138 "AccelerationBands"
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::traits::BatchExt;
146 use approx::assert_relative_eq;
147
148 fn c(h: f64, l: f64, cl: f64) -> Candle {
149 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
150 }
151
152 #[test]
153 fn rejects_zero_period() {
154 assert!(matches!(
155 AccelerationBands::new(0, 0.001),
156 Err(Error::PeriodZero)
157 ));
158 }
159
160 #[test]
161 fn rejects_non_positive_factor() {
162 assert!(matches!(
163 AccelerationBands::new(20, 0.0),
164 Err(Error::NonPositiveMultiplier)
165 ));
166 assert!(matches!(
167 AccelerationBands::new(20, -1.0),
168 Err(Error::NonPositiveMultiplier)
169 ));
170 assert!(matches!(
171 AccelerationBands::new(20, f64::NAN),
172 Err(Error::NonPositiveMultiplier)
173 ));
174 }
175
176 #[test]
177 fn accessors_and_metadata() {
178 let ab = AccelerationBands::classic();
179 let (p, f) = ab.parameters();
180 assert_eq!(p, 20);
181 assert_relative_eq!(f, 0.001, epsilon = 1e-12);
182 assert_eq!(ab.warmup_period(), 20);
183 assert_eq!(ab.name(), "AccelerationBands");
184 }
185
186 #[test]
187 fn flat_market_collapses_to_constant() {
188 let candles: Vec<Candle> = (0..30).map(|_| c(10.0, 10.0, 10.0)).collect();
191 let mut ab = AccelerationBands::new(5, 0.5).unwrap();
192 let last = ab.batch(&candles).into_iter().flatten().last().unwrap();
193 assert_relative_eq!(last.middle, 10.0, epsilon = 1e-9);
194 assert_relative_eq!(last.upper, 10.0, epsilon = 1e-9);
195 assert_relative_eq!(last.lower, 10.0, epsilon = 1e-9);
196 }
197
198 #[test]
199 fn warmup_returns_none() {
200 let mut ab = AccelerationBands::new(5, 0.001).unwrap();
201 for i in 0..4 {
202 let base = 100.0 + f64::from(i);
203 assert!(ab.update(c(base + 1.0, base - 1.0, base)).is_none());
204 }
205 assert!(ab.update(c(105.0, 103.0, 104.0)).is_some());
206 }
207
208 #[test]
209 fn upper_above_middle_above_lower() {
210 let candles: Vec<Candle> = (0..50)
211 .map(|i| {
212 let m = 100.0 + (f64::from(i) * 0.2).sin() * 5.0;
213 c(m + 1.0, m - 1.0, m)
214 })
215 .collect();
216 let mut ab = AccelerationBands::new(20, 0.5).unwrap();
217 for o in ab.batch(&candles).into_iter().flatten() {
218 assert!(o.upper >= o.middle, "{} < {}", o.upper, o.middle);
219 assert!(o.middle >= o.lower, "{} < {}", o.middle, o.lower);
220 }
221 }
222
223 #[test]
224 fn batch_equals_streaming() {
225 let candles: Vec<Candle> = (0..40)
226 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
227 .collect();
228 let mut a = AccelerationBands::new(10, 0.5).unwrap();
229 let mut b = AccelerationBands::new(10, 0.5).unwrap();
230 assert_eq!(
231 a.batch(&candles),
232 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
233 );
234 }
235
236 #[test]
237 fn reset_clears_state() {
238 let candles: Vec<Candle> = (0..10)
239 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
240 .collect();
241 let mut ab = AccelerationBands::new(5, 0.5).unwrap();
242 ab.batch(&candles);
243 assert!(ab.is_ready());
244 ab.reset();
245 assert!(!ab.is_ready());
246 assert_eq!(ab.update(candles[0]), None);
247 }
248
249 #[test]
250 fn zero_price_candle_collapses_ratio_to_zero() {
251 let zero = Candle::new(0.0, 0.0, 0.0, 0.0, 1.0, 0).unwrap();
256 let mut ab = AccelerationBands::new(1, 0.5).unwrap();
257 let v = ab.update(zero).unwrap();
258 assert_relative_eq!(v.upper, 0.0, epsilon = 1e-12);
259 assert_relative_eq!(v.middle, 0.0, epsilon = 1e-12);
260 assert_relative_eq!(v.lower, 0.0, epsilon = 1e-12);
261 }
262
263 #[test]
270 fn reference_value_single_bar() {
271 let mut ab = AccelerationBands::new(1, 0.5).unwrap();
272 let v = ab.update(c(12.0, 8.0, 10.0)).unwrap();
273 assert_relative_eq!(v.upper, 13.2, epsilon = 1e-12);
274 assert_relative_eq!(v.middle, 10.0, epsilon = 1e-12);
275 assert_relative_eq!(v.lower, 7.2, epsilon = 1e-12);
276 }
277}