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 let n = period as f64;
85 Ok(Self {
86 period,
87 highs: VecDeque::with_capacity(period),
88 lows: VecDeque::with_capacity(period),
89 sum_x: n * (n - 1.0) / 2.0,
90 sum_xx: (n - 1.0) * n * (2.0 * n - 1.0) / 6.0,
91 })
92 }
93
94 pub const fn period(&self) -> usize {
96 self.period
97 }
98
99 fn slope(&self, values: &VecDeque<f64>) -> f64 {
101 let n = self.period as f64;
102 let mut sum_y = 0.0;
103 let mut sum_xy = 0.0;
104 for (i, &y) in values.iter().enumerate() {
105 sum_y += y;
106 sum_xy += (i as f64) * y;
107 }
108 let denom = n * self.sum_xx - self.sum_x * self.sum_x;
109 (n * sum_xy - self.sum_x * sum_y) / denom
110 }
111}
112
113impl Indicator for ProjectionBands {
114 type Input = Candle;
115 type Output = ProjectionBandsOutput;
116
117 fn update(&mut self, candle: Candle) -> Option<ProjectionBandsOutput> {
118 if self.highs.len() == self.period {
119 self.highs.pop_front();
120 self.lows.pop_front();
121 }
122 self.highs.push_back(candle.high);
123 self.lows.push_back(candle.low);
124 if self.highs.len() < self.period {
125 return None;
126 }
127
128 let slope_h = self.slope(&self.highs);
129 let slope_l = self.slope(&self.lows);
130 let last = (self.period - 1) as f64;
131
132 let mut upper = f64::NEG_INFINITY;
133 let mut lower = f64::INFINITY;
134 for (i, (&high, &low)) in self.highs.iter().zip(self.lows.iter()).enumerate() {
135 let forward = last - (i as f64);
136 let projected_high = high + slope_h * forward;
137 let projected_low = low + slope_l * forward;
138 if projected_high > upper {
139 upper = projected_high;
140 }
141 if projected_low < lower {
142 lower = projected_low;
143 }
144 }
145
146 Some(ProjectionBandsOutput {
147 upper,
148 middle: f64::midpoint(upper, lower),
149 lower,
150 })
151 }
152
153 fn reset(&mut self) {
154 self.highs.clear();
155 self.lows.clear();
156 }
157
158 fn warmup_period(&self) -> usize {
159 self.period
160 }
161
162 fn is_ready(&self) -> bool {
163 self.highs.len() == self.period
164 }
165
166 fn name(&self) -> &'static str {
167 "ProjectionBands"
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use approx::assert_relative_eq;
175
176 fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
177 Candle::new(low, high, low, close, 10.0, ts).unwrap()
178 }
179
180 #[test]
181 fn rejects_period_below_two() {
182 assert!(matches!(
183 ProjectionBands::new(0),
184 Err(Error::InvalidPeriod { .. })
185 ));
186 assert!(matches!(
187 ProjectionBands::new(1),
188 Err(Error::InvalidPeriod { .. })
189 ));
190 assert!(ProjectionBands::new(2).is_ok());
191 }
192
193 #[test]
194 fn accessors_and_metadata() {
195 let pb = ProjectionBands::new(14).unwrap();
196 assert_eq!(pb.period(), 14);
197 assert_eq!(pb.warmup_period(), 14);
198 assert_eq!(pb.name(), "ProjectionBands");
199 assert!(!pb.is_ready());
200 }
201
202 #[test]
203 fn warms_up_then_emits() {
204 let mut pb = ProjectionBands::new(3).unwrap();
205 assert!(pb.update(candle(10.0, 8.0, 9.0, 0)).is_none());
206 assert!(pb.update(candle(12.0, 9.0, 11.0, 1)).is_none());
207 assert!(pb.update(candle(11.0, 10.0, 11.0, 2)).is_some());
208 assert!(pb.is_ready());
209 }
210
211 #[test]
212 fn known_projection() {
213 let mut pb = ProjectionBands::new(3).unwrap();
216 pb.update(candle(10.0, 8.0, 9.0, 0));
217 pb.update(candle(12.0, 9.0, 11.0, 1));
218 let out = pb.update(candle(11.0, 10.0, 11.0, 2)).unwrap();
219 assert_relative_eq!(out.upper, 12.5, epsilon = 1e-9);
220 assert_relative_eq!(out.lower, 10.0, epsilon = 1e-9);
221 assert_relative_eq!(out.middle, 11.25, epsilon = 1e-9);
222 }
223
224 #[test]
225 fn perfect_trend_pins_bands_to_current_extremes() {
226 let mut pb = ProjectionBands::new(5).unwrap();
230 let mut last = None;
231 for i in 0..10 {
232 let high = 100.0 + f64::from(i);
233 let low = 95.0 + f64::from(i);
234 last = pb.update(candle(high, low, high, i64::from(i)));
235 }
236 let out = last.unwrap();
237 assert_relative_eq!(out.upper, 109.0, epsilon = 1e-9);
238 assert_relative_eq!(out.lower, 104.0, epsilon = 1e-9);
239 assert_relative_eq!(out.middle, 106.5, epsilon = 1e-9);
240 }
241
242 #[test]
243 fn reset_clears_state() {
244 let mut pb = ProjectionBands::new(3).unwrap();
245 pb.update(candle(10.0, 8.0, 9.0, 0));
246 pb.update(candle(12.0, 9.0, 11.0, 1));
247 pb.update(candle(11.0, 10.0, 11.0, 2));
248 assert!(pb.is_ready());
249 pb.reset();
250 assert!(!pb.is_ready());
251 assert!(pb.update(candle(10.0, 8.0, 9.0, 3)).is_none());
252 }
253}