1use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct ProjectionBandsOutput {
13 pub upper: f64,
15 pub middle: f64,
17 pub lower: f64,
19}
20
21#[derive(Debug, Clone)]
64pub struct ProjectionBands {
65 period: usize,
66 highs: VecDeque<f64>,
67 lows: VecDeque<f64>,
68 sum_x: f64,
69 sum_xx: f64,
70}
71
72impl ProjectionBands {
73 pub fn new(period: usize) -> Result<Self> {
79 if period < 2 {
80 return Err(Error::InvalidPeriod {
81 message: "projection bands need period >= 2",
82 });
83 }
84 if period > crate::error::MAX_PERIOD {
85 return Err(Error::InvalidPeriod {
86 message: crate::error::PERIOD_ABOVE_MAX,
87 });
88 }
89 let n = period as f64;
90 Ok(Self {
91 period,
92 highs: VecDeque::with_capacity(period),
93 lows: VecDeque::with_capacity(period),
94 sum_x: n * (n - 1.0) / 2.0,
95 sum_xx: (n - 1.0) * n * (2.0 * n - 1.0) / 6.0,
96 })
97 }
98
99 pub const fn period(&self) -> usize {
101 self.period
102 }
103
104 fn slope(&self, values: &VecDeque<f64>) -> f64 {
111 let n = self.period as f64;
112 let mean_y = values.iter().sum::<f64>() / n;
113 let mut sum_y = 0.0;
114 let mut sum_xy = 0.0;
115 for (i, &y) in values.iter().enumerate() {
116 let d = y - mean_y;
117 sum_y += d;
118 sum_xy += (i as f64) * d;
119 }
120 let denom = n * self.sum_xx - self.sum_x * self.sum_x;
121 (n * sum_xy - self.sum_x * sum_y) / denom
122 }
123}
124
125impl Indicator for ProjectionBands {
126 type Input = Candle;
127 type Output = ProjectionBandsOutput;
128
129 #[inline]
130 fn update(&mut self, candle: Candle) -> Option<ProjectionBandsOutput> {
131 if self.highs.len() == self.period {
132 self.highs.pop_front();
133 self.lows.pop_front();
134 }
135 self.highs.push_back(candle.high);
136 self.lows.push_back(candle.low);
137 if self.highs.len() < self.period {
138 return None;
139 }
140
141 let slope_h = self.slope(&self.highs);
142 let slope_l = self.slope(&self.lows);
143 let last = (self.period - 1) as f64;
144
145 let mut upper = f64::NEG_INFINITY;
146 let mut lower = f64::INFINITY;
147 for (i, (&high, &low)) in self.highs.iter().zip(self.lows.iter()).enumerate() {
148 let forward = last - (i as f64);
149 let projected_high = high + slope_h * forward;
150 let projected_low = low + slope_l * forward;
151 if projected_high > upper {
152 upper = projected_high;
153 }
154 if projected_low < lower {
155 lower = projected_low;
156 }
157 }
158
159 Some(ProjectionBandsOutput {
160 upper,
161 middle: f64::midpoint(upper, lower),
162 lower,
163 })
164 }
165
166 fn reset(&mut self) {
167 self.highs.clear();
168 self.lows.clear();
169 }
170
171 #[inline]
172 fn warmup_period(&self) -> usize {
173 self.period
174 }
175
176 #[inline]
177 fn is_ready(&self) -> bool {
178 self.highs.len() == self.period
179 }
180
181 #[inline]
182 fn name(&self) -> &'static str {
183 "ProjectionBands"
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use approx::assert_relative_eq;
191
192 fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
193 Candle::new(low, high, low, close, 10.0, ts).unwrap()
194 }
195
196 #[test]
197 fn rejects_period_below_two() {
198 assert!(matches!(
199 ProjectionBands::new(0),
200 Err(Error::InvalidPeriod { .. })
201 ));
202 assert!(matches!(
203 ProjectionBands::new(1),
204 Err(Error::InvalidPeriod { .. })
205 ));
206 assert!(ProjectionBands::new(2).is_ok());
207 }
208
209 #[test]
210 fn accessors_and_metadata() {
211 let pb = ProjectionBands::new(14).unwrap();
212 assert_eq!(pb.period(), 14);
213 assert_eq!(pb.warmup_period(), 14);
214 assert_eq!(pb.name(), "ProjectionBands");
215 assert!(!pb.is_ready());
216 }
217
218 #[test]
219 fn warms_up_then_emits() {
220 let mut pb = ProjectionBands::new(3).unwrap();
221 assert!(pb.update(candle(10.0, 8.0, 9.0, 0)).is_none());
222 assert!(pb.update(candle(12.0, 9.0, 11.0, 1)).is_none());
223 assert!(pb.update(candle(11.0, 10.0, 11.0, 2)).is_some());
224 assert!(pb.is_ready());
225 }
226
227 #[test]
228 fn known_projection() {
229 let mut pb = ProjectionBands::new(3).unwrap();
232 pb.update(candle(10.0, 8.0, 9.0, 0));
233 pb.update(candle(12.0, 9.0, 11.0, 1));
234 let out = pb.update(candle(11.0, 10.0, 11.0, 2)).unwrap();
235 assert_relative_eq!(out.upper, 12.5, epsilon = 1e-9);
236 assert_relative_eq!(out.lower, 10.0, epsilon = 1e-9);
237 assert_relative_eq!(out.middle, 11.25, epsilon = 1e-9);
238 }
239
240 #[test]
241 fn perfect_trend_pins_bands_to_current_extremes() {
242 let mut pb = ProjectionBands::new(5).unwrap();
246 let mut last = None;
247 for i in 0..10 {
248 let high = 100.0 + f64::from(i);
249 let low = 95.0 + f64::from(i);
250 last = pb.update(candle(high, low, high, i64::from(i)));
251 }
252 let out = last.unwrap();
253 assert_relative_eq!(out.upper, 109.0, epsilon = 1e-9);
254 assert_relative_eq!(out.lower, 104.0, epsilon = 1e-9);
255 assert_relative_eq!(out.middle, 106.5, epsilon = 1e-9);
256 }
257
258 #[test]
259 fn reset_clears_state() {
260 let mut pb = ProjectionBands::new(3).unwrap();
261 pb.update(candle(10.0, 8.0, 9.0, 0));
262 pb.update(candle(12.0, 9.0, 11.0, 1));
263 pb.update(candle(11.0, 10.0, 11.0, 2));
264 assert!(pb.is_ready());
265 pb.reset();
266 assert!(!pb.is_ready());
267 assert!(pb.update(candle(10.0, 8.0, 9.0, 3)).is_none());
268 }
269}