wickra_core/indicators/
linreg_channel.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct LinRegChannelOutput {
11 pub upper: f64,
14 pub middle: f64,
16 pub lower: f64,
19}
20
21#[derive(Debug, Clone)]
53pub struct LinRegChannel {
54 period: usize,
55 multiplier: f64,
56 window: VecDeque<f64>,
57 sum_x: f64,
58 sum_xx: f64,
59}
60
61impl LinRegChannel {
62 pub fn new(period: usize, multiplier: f64) -> Result<Self> {
67 if period < 2 {
68 return Err(Error::InvalidPeriod {
69 message: "linear regression channel needs period >= 2",
70 });
71 }
72 if period > crate::error::MAX_PERIOD {
73 return Err(Error::InvalidPeriod {
74 message: crate::error::PERIOD_ABOVE_MAX,
75 });
76 }
77 if !multiplier.is_finite() || multiplier <= 0.0 {
78 return Err(Error::NonPositiveMultiplier);
79 }
80 let n = period as f64;
81 Ok(Self {
82 period,
83 multiplier,
84 window: VecDeque::with_capacity(period),
85 sum_x: n * (n - 1.0) / 2.0,
86 sum_xx: (n - 1.0) * n * (2.0 * n - 1.0) / 6.0,
87 })
88 }
89
90 pub const fn period(&self) -> usize {
92 self.period
93 }
94
95 pub const fn multiplier(&self) -> f64 {
97 self.multiplier
98 }
99}
100
101impl Indicator for LinRegChannel {
102 type Input = f64;
103 type Output = LinRegChannelOutput;
104
105 fn update(&mut self, value: f64) -> Option<LinRegChannelOutput> {
106 if !value.is_finite() {
107 return None;
108 }
109 if self.window.len() == self.period {
110 self.window.pop_front();
111 }
112 self.window.push_back(value);
113 if self.window.len() < self.period {
114 return None;
115 }
116 let n = self.period as f64;
122 let mean = self.window.iter().sum::<f64>() / n;
127 let mut sum_y = 0.0;
128 let mut sum_xy = 0.0;
129 for (i, &y) in self.window.iter().enumerate() {
130 let x = i as f64;
131 let d = y - mean;
132 sum_y += d;
133 sum_xy += x * d;
134 }
135 let denom = n * self.sum_xx - self.sum_x * self.sum_x;
136 let slope = (n * sum_xy - self.sum_x * sum_y) / denom;
137 let intercept = (sum_y - slope * self.sum_x) / n;
138
139 let mut sum_sq = 0.0;
141 for (i, &y) in self.window.iter().enumerate() {
142 let fitted = intercept + slope * (i as f64);
143 let r = (y - mean) - fitted;
144 sum_sq += r * r;
145 }
146 let sigma = (sum_sq / n).sqrt();
147 let middle = mean + intercept + slope * (n - 1.0);
149 Some(LinRegChannelOutput {
150 upper: middle + self.multiplier * sigma,
151 middle,
152 lower: middle - self.multiplier * sigma,
153 })
154 }
155
156 fn reset(&mut self) {
157 self.window.clear();
158 }
159
160 #[inline]
161 fn warmup_period(&self) -> usize {
162 self.period
163 }
164
165 #[inline]
166 fn is_ready(&self) -> bool {
167 self.window.len() == self.period
168 }
169
170 #[inline]
171 fn name(&self) -> &'static str {
172 "LinRegChannel"
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::traits::BatchExt;
180 use approx::assert_relative_eq;
181
182 #[test]
183 fn rejects_period_below_two() {
184 assert!(LinRegChannel::new(0, 2.0).is_err());
185 assert!(LinRegChannel::new(1, 2.0).is_err());
186 assert!(LinRegChannel::new(2, 2.0).is_ok());
187 }
188
189 #[test]
190 fn rejects_non_positive_multiplier() {
191 assert!(matches!(
192 LinRegChannel::new(20, 0.0),
193 Err(Error::NonPositiveMultiplier)
194 ));
195 assert!(matches!(
196 LinRegChannel::new(20, -1.0),
197 Err(Error::NonPositiveMultiplier)
198 ));
199 assert!(matches!(
200 LinRegChannel::new(20, f64::NAN),
201 Err(Error::NonPositiveMultiplier)
202 ));
203 }
204
205 #[test]
206 fn accessors_and_metadata() {
207 let lc = LinRegChannel::new(20, 2.0).unwrap();
208 assert_eq!(lc.period(), 20);
209 assert_relative_eq!(lc.multiplier(), 2.0, epsilon = 1e-12);
210 assert_eq!(lc.warmup_period(), 20);
211 assert_eq!(lc.name(), "LinRegChannel");
212 }
213
214 #[test]
215 fn perfect_line_collapses_channel() {
216 let prices: Vec<f64> = (0..40).map(|i| 2.0 * f64::from(i) + 5.0).collect();
218 let mut lc = LinRegChannel::new(10, 2.0).unwrap();
219 for o in lc.batch(&prices).into_iter().flatten() {
220 assert_relative_eq!(o.upper, o.middle, epsilon = 1e-9);
221 assert_relative_eq!(o.middle, o.lower, epsilon = 1e-9);
222 }
223 }
224
225 #[test]
226 fn constant_series_collapses_channel() {
227 let mut lc = LinRegChannel::new(8, 2.0).unwrap();
228 let out = lc.batch(&[42.0; 20]);
229 let v = out.iter().rev().flatten().next().unwrap();
230 assert_relative_eq!(v.middle, 42.0, epsilon = 1e-9);
231 assert_relative_eq!(v.upper, 42.0, epsilon = 1e-9);
232 assert_relative_eq!(v.lower, 42.0, epsilon = 1e-9);
233 }
234
235 #[test]
236 fn upper_above_middle_above_lower() {
237 let prices: Vec<f64> = (0..80)
238 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 10.0)
239 .collect();
240 let mut lc = LinRegChannel::new(20, 2.0).unwrap();
241 for o in lc.batch(&prices).into_iter().flatten() {
242 assert!(o.upper >= o.middle);
243 assert!(o.middle >= o.lower);
244 }
245 }
246
247 #[test]
248 fn batch_equals_streaming() {
249 let prices: Vec<f64> = (0..60)
250 .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 10.0)
251 .collect();
252 let mut a = LinRegChannel::new(14, 2.0).unwrap();
253 let mut b = LinRegChannel::new(14, 2.0).unwrap();
254 assert_eq!(
255 a.batch(&prices),
256 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
257 );
258 }
259
260 #[test]
261 fn reset_clears_state() {
262 let mut lc = LinRegChannel::new(5, 2.0).unwrap();
263 lc.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
264 assert!(lc.is_ready());
265 lc.reset();
266 assert!(!lc.is_ready());
267 assert_eq!(lc.update(1.0), None);
268 }
269
270 #[test]
275 fn reference_values() {
276 let mut lc = LinRegChannel::new(3, 2.0).unwrap();
277 let out = lc.batch(&[1.0, 2.0, 9.0]);
278 let v = out[2].unwrap();
279 let s2 = f64::sqrt(2.0);
280 assert_relative_eq!(v.middle, 8.0, epsilon = 1e-9);
281 assert_relative_eq!(v.upper, 8.0 + 2.0 * s2, epsilon = 1e-9);
282 assert_relative_eq!(v.lower, 8.0 - 2.0 * s2, epsilon = 1e-9);
283 }
284}