wickra_core/indicators/
pearson_correlation.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::ShiftedPairMoments;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
48pub struct PearsonCorrelation {
49 period: usize,
50 window: VecDeque<(f64, f64)>,
51 moments: ShiftedPairMoments,
52}
53
54impl PearsonCorrelation {
55 pub fn new(period: usize) -> Result<Self> {
61 if period < 2 {
62 return Err(Error::InvalidPeriod {
63 message: "pearson correlation needs period >= 2",
64 });
65 }
66 if period > crate::error::MAX_PERIOD {
67 return Err(Error::InvalidPeriod {
68 message: crate::error::PERIOD_ABOVE_MAX,
69 });
70 }
71 Ok(Self {
72 period,
73 window: VecDeque::with_capacity(period),
74 moments: ShiftedPairMoments::new(),
75 })
76 }
77
78 pub const fn period(&self) -> usize {
80 self.period
81 }
82}
83
84impl Indicator for PearsonCorrelation {
85 type Input = (f64, f64);
86 type Output = f64;
87
88 #[inline]
89 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
90 let (x, y) = input;
91 if !x.is_finite() || !y.is_finite() {
92 return None;
93 }
94 if self.window.len() == self.period {
95 let (ox, oy) = self.window.pop_front().expect("non-empty");
96 self.moments.evict(ox, oy);
97 }
98 self.window.push_back((x, y));
99 self.moments.push(x, y);
100 if self.moments.needs_reseed(self.period) {
101 self.moments.reseed(self.window.iter().copied());
102 }
103 if self.window.len() < self.period {
104 return None;
105 }
106 let var_x = self.moments.var_a(self.period);
107 let var_y = self.moments.var_b(self.period);
108 let cov = self.moments.cov(self.period);
109 let denom = (var_x * var_y).sqrt();
110 if denom == 0.0 {
111 return Some(0.0);
113 }
114 Some((cov / denom).clamp(-1.0, 1.0))
115 }
116
117 fn reset(&mut self) {
118 self.window.clear();
119 self.moments.reset();
120 }
121
122 #[inline]
123 fn warmup_period(&self) -> usize {
124 self.period
125 }
126
127 #[inline]
128 fn is_ready(&self) -> bool {
129 self.window.len() == self.period
130 }
131
132 #[inline]
133 fn name(&self) -> &'static str {
134 "PearsonCorrelation"
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::traits::BatchExt;
142 use approx::assert_relative_eq;
143
144 #[test]
145 fn rejects_period_below_two() {
146 assert!(PearsonCorrelation::new(0).is_err());
147 assert!(PearsonCorrelation::new(1).is_err());
148 assert!(PearsonCorrelation::new(2).is_ok());
149 }
150
151 #[test]
152 fn accessors_and_metadata() {
153 let p = PearsonCorrelation::new(14).unwrap();
154 assert_eq!(p.period(), 14);
155 assert_eq!(p.warmup_period(), 14);
156 assert_eq!(p.name(), "PearsonCorrelation");
157 }
158
159 #[test]
160 fn perfect_positive_is_one() {
161 let pairs: Vec<(f64, f64)> = (0..10)
162 .map(|i| (f64::from(i), 3.0 * f64::from(i) + 1.0))
163 .collect();
164 let last = PearsonCorrelation::new(5)
165 .unwrap()
166 .batch(&pairs)
167 .into_iter()
168 .flatten()
169 .last()
170 .unwrap();
171 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
172 }
173
174 #[test]
175 fn perfect_negative_is_minus_one() {
176 let pairs: Vec<(f64, f64)> = (0..10)
177 .map(|i| (f64::from(i), -2.0 * f64::from(i) + 5.0))
178 .collect();
179 let last = PearsonCorrelation::new(5)
180 .unwrap()
181 .batch(&pairs)
182 .into_iter()
183 .flatten()
184 .last()
185 .unwrap();
186 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
187 }
188
189 #[test]
190 fn constant_channel_yields_zero() {
191 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (f64::from(i), 7.0)).collect();
192 let last = PearsonCorrelation::new(5)
193 .unwrap()
194 .batch(&pairs)
195 .into_iter()
196 .flatten()
197 .last()
198 .unwrap();
199 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
200 }
201
202 #[test]
203 fn output_in_minus_one_to_one_range() {
204 let pairs: Vec<(f64, f64)> = (0..60)
205 .map(|i| {
206 let t = f64::from(i);
207 (100.0 + t.sin() * 5.0, 50.0 + (t * 0.3).cos() * 3.0)
208 })
209 .collect();
210 let mut p = PearsonCorrelation::new(20).unwrap();
211 for v in p.batch(&pairs).into_iter().flatten() {
212 assert!((-1.0..=1.0).contains(&v));
213 }
214 }
215
216 #[test]
217 fn reset_clears_state() {
218 let mut p = PearsonCorrelation::new(5).unwrap();
219 p.batch(&[(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0), (5.0, 10.0)]);
220 assert!(p.is_ready());
221 p.reset();
222 assert!(!p.is_ready());
223 assert_eq!(p.update((1.0, 1.0)), None);
224 }
225
226 #[test]
227 fn batch_equals_streaming() {
228 let pairs: Vec<(f64, f64)> = (0..60)
229 .map(|i| {
230 let t = f64::from(i);
231 (t.sin(), (t * 0.5).cos())
232 })
233 .collect();
234 let batch = PearsonCorrelation::new(14).unwrap().batch(&pairs);
235 let mut b = PearsonCorrelation::new(14).unwrap();
236 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
237 assert_eq!(batch, streamed);
238 }
239
240 #[test]
241 fn non_finite_input_returns_none() {
242 let mut p = PearsonCorrelation::new(3).unwrap();
243 assert_eq!(p.update((f64::NAN, 1.0)), None);
244 assert_eq!(p.update((1.0, f64::INFINITY)), None);
245 assert_eq!(p.update((1.0, 2.0)), None);
247 assert_eq!(p.update((2.0, 5.0)), None);
248 assert!(p.update((3.0, 7.0)).is_some());
249 }
250}