ta_statistics/paired_statistics.rs
1use num_traits::Float;
2
3use crate::rolling::RollingMoments;
4
5type Kbn<T> = compensated_summation::KahanBabuskaNeumaier<T>;
6
7/// A structure that computes various statistics over a fixed-size window of paired values.
8///
9/// `PairedStatistics<T>` maintains a circular buffer of paired values and computes statistical measures
10/// such as covariance, correlation, beta, etc.
11///
12/// The structure automatically updates statistics as new values are added and old values
13/// are removed from the window, making it efficient for rolling statistics analysis.
14#[derive(Debug, Clone)]
15pub struct PairedStatistics<T> {
16 moments_x: RollingMoments<T>,
17 moments_y: RollingMoments<T>,
18 sum_xy: Kbn<T>,
19 ddof: bool,
20}
21
22impl<T> PairedStatistics<T>
23where
24 T: Default + Clone + Float,
25{
26 /// Creates a new `PairedStatistics` instance with the specified period.
27 ///
28 /// # Arguments
29 ///
30 /// * `period` - The period of the statistics
31 ///
32 /// # Returns
33 ///
34 /// * `Self` - The `PairedStatistics` instance
35 pub fn new(period: usize) -> Self {
36 Self {
37 moments_x: RollingMoments::new(period),
38 moments_y: RollingMoments::new(period),
39 sum_xy: Kbn::default(),
40 ddof: false,
41 }
42 }
43
44 /// Returns the period of the statistics
45 ///
46 /// # Returns
47 ///
48 /// * `usize` - The period of the statistics
49 pub fn period(&self) -> usize {
50 self.moments_x.period()
51 }
52
53 /// Resets the statistics
54 ///
55 /// # Returns
56 ///
57 /// * `&mut Self` - The statistics object
58 pub fn reset(&mut self) -> &mut Self {
59 self.moments_x.reset();
60 self.moments_y.reset();
61 self.sum_xy = Default::default();
62 self
63 }
64
65 /// Recomputes the paired statistics, could be called to avoid
66 /// prolonged compounding of floating rounding errors
67 ///
68 /// # Returns
69 ///
70 /// * `&mut Self` - The rolling moments object
71 pub fn recompute(&mut self) -> &mut Self {
72 self.moments_x.recompute();
73 self.moments_y.recompute();
74 for (&x, &y) in self.moments_x.iter().zip(self.moments_y.iter()) {
75 self.sum_xy += x * y;
76 }
77 self
78 }
79
80 /// Updates the paired statistical calculations with a new value pair in the time series
81 ///
82 /// Incorporates a new data point pair into the rolling window, maintaining the specified
83 /// window size by removing the oldest pair when necessary. This core method provides
84 /// the foundation for all paired statistical measures that examine relationships
85 /// between two variables.
86 ///
87 /// # Arguments
88 ///
89 /// * `value` - A tuple containing the paired values (x, y) to incorporate into calculations
90 ///
91 /// # Returns
92 ///
93 /// * `&mut Self` - The updated statistics object for method chaining
94 pub fn next(&mut self, (x, y): (T, T)) -> &mut Self {
95 self.moments_x.next(x);
96 self.moments_y.next(y);
97
98 if self.moments_x.is_ready() {
99 if let Some((px, py)) = self.moments_x.popped().zip(self.moments_y.popped()) {
100 self.sum_xy -= px * py;
101 }
102 }
103
104 self.sum_xy += x * y;
105
106 self
107 }
108
109 /// Returns the Delta Degrees of Freedom
110 ///
111 /// # Returns
112 ///
113 /// * `bool` - The Delta Degrees of Freedom
114 pub const fn ddof(&self) -> bool {
115 self.ddof
116 }
117
118 /// Sets the Delta Degrees of Freedom
119 ///
120 /// # Arguments
121 ///
122 /// * `ddof` - The Delta Degrees of Freedom
123 ///
124 /// # Returns
125 ///
126 /// * `&mut Self` - The statistics object
127 pub const fn set_ddof(&mut self, ddof: bool) -> &mut Self {
128 self.ddof = ddof;
129 self
130 }
131 /// Returns the mean of the values in the rolling window
132 ///
133 /// # Returns
134 ///
135 /// * `Option<(T, T)>` - The mean of the values in the window, or `None` if the window is not full
136 fn mean(&self) -> Option<(T, T)> {
137 self.moments_x.mean().zip(self.moments_y.mean())
138 }
139
140 /// Returns the mean of the product of the values in the rolling window
141 ///
142 /// # Returns
143 ///
144 /// * `Option<(T, T)>` - The mean of the product of the values in the window, or `None` if the window is not full
145 fn mean_prod(&self) -> Option<(T, T)> {
146 if !self.moments_x.is_ready() {
147 return None;
148 }
149
150 let n = T::from(self.period())?;
151 let mp = self.sum_xy.total() / n;
152 Some((mp, mp))
153 }
154
155 /// Returns the variance of the values in the rolling window
156 ///
157 /// # Returns
158 ///
159 /// * `Option<(T, T)>` - The variance of the values in the window, or `None` if the window is not full
160 fn variance(&self) -> Option<(T, T)> {
161 self.moments_x.variance().zip(self.moments_y.variance())
162 }
163
164 /// Returns the standard deviation of the values in the rolling window
165 ///
166 /// # Returns
167 ///
168 /// * `Option<(T, T)>` - The standard deviation of the values in the window, or `None` if the window is not full
169 fn stddev(&self) -> Option<(T, T)> {
170 self.moments_x.stddev().zip(self.moments_y.stddev())
171 }
172
173 /// Returns the covariance of the paired values in the rolling window
174 ///
175 /// Covariance measures how two variables change together, indicating the direction
176 /// of their linear relationship. This fundamental measure of association provides:
177 ///
178 /// - Directional relationship analysis between paired time series
179 /// - Foundation for correlation, beta, and regression calculations
180 /// - Raw measurement of how variables move in tandem
181 /// - Basis for portfolio diversification and risk assessments
182 ///
183 /// # Returns
184 ///
185 /// * `Option<T>` - The covariance of the values in the window, or `None` if the window is not full
186 ///
187 /// # Examples
188 ///
189 /// ```
190 /// use ta_statistics::PairedStatistics;
191 /// use assert_approx_eq::assert_approx_eq;
192 ///
193 /// let mut stats = PairedStatistics::new(3);
194 /// let mut results = vec![];
195 /// let inputs = [(2.0, 1.0), (4.0, 3.0), (6.0, 2.0), (8.0, 5.0), (10.0, 7.0)];
196 /// inputs.iter().for_each(|i| {
197 /// stats.next(*i).cov().map(|v| results.push(v));
198 /// });
199 ///
200 /// let expected: [f64; 3] = [0.6667, 1.3333, 3.3333];
201 /// for (i, e) in expected.iter().enumerate() {
202 /// assert_approx_eq!(e, results[i], 0.001);
203 /// }
204 ///
205 /// stats.reset().set_ddof(true);
206 /// results = vec![];
207 /// inputs.iter().for_each(|i| {
208 /// stats.next(*i).cov().map(|v| results.push(v));
209 /// });
210 ///
211 /// let expected: [f64; 3] = [1.0, 2.0, 5.0];
212 /// for (i, e) in expected.iter().enumerate() {
213 /// assert_approx_eq!(e, results[i], 0.0001);
214 /// }
215 /// ```
216 pub fn cov(&self) -> Option<T> {
217 let (mean_x, mean_y) = self.mean()?;
218 let (mean_xy, _) = self.mean_prod()?;
219
220 let cov = mean_xy - mean_x * mean_y;
221
222 let n = T::from(self.period())?;
223 if self.ddof() {
224 Some(cov * (n / (n - T::one())))
225 } else {
226 Some(cov)
227 }
228 }
229
230 /// Returns the correlation coefficient (Pearson's r) of paired values in the rolling window
231 ///
232 /// Correlation normalizes covariance by the product of standard deviations, producing
233 /// a standardized measure of linear relationship strength between -1 and 1:
234 ///
235 /// - Quantifies the strength and direction of relationships between variables
236 /// - Enables cross-pair comparison on a standardized scale
237 /// - Provides the foundation for statistical arbitrage models
238 /// - Identifies regime changes in intermarket relationships
239 ///
240 /// # Returns
241 ///
242 /// * `Option<T>` - The correlation coefficient in the window, or `None` if the window is not full
243 ///
244 /// # Examples
245 ///
246 /// ```
247 /// use ta_statistics::PairedStatistics;
248 /// use assert_approx_eq::assert_approx_eq;
249 ///
250 /// let mut stats = PairedStatistics::new(3);
251 /// let mut results = vec![];
252 /// let inputs = [
253 /// (0.496714, 0.115991),
254 /// (-0.138264, -0.329650),
255 /// (0.647689, 0.574363),
256 /// (1.523030, 0.109481),
257 /// (-0.234153, -1.026366),
258 /// (-0.234137, -0.445040),
259 /// (1.579213, 0.599033),
260 /// (0.767435, 0.694328),
261 /// (-0.469474, -0.782644),
262 /// (0.542560, -0.326360)
263 /// ];
264 ///
265 /// inputs.iter().for_each(|i| {
266 /// stats.next(*i).corr().map(|v| results.push(v));
267 /// });
268 /// let expected: [f64; 8] = [0.939464, 0.458316, 0.691218, 0.859137, 0.935658, 0.858379, 0.895148, 0.842302,];
269 /// for (i, e) in expected.iter().enumerate() {
270 /// assert_approx_eq!(e, results[i], 0.0001);
271 /// }
272 ///
273 /// ```
274 pub fn corr(&self) -> Option<T> {
275 self.cov()
276 .zip(self.stddev())
277 .and_then(|(cov, (stddev_x, stddev_y))| {
278 if stddev_x.is_zero() || stddev_y.is_zero() {
279 None
280 } else {
281 Some(cov / (stddev_x * stddev_y))
282 }
283 })
284 }
285
286 /// Returns the beta coefficient of the paired values in the rolling window
287 ///
288 /// Beta measures the relative volatility between two time series, indicating
289 /// the sensitivity of one variable to changes in another:
290 ///
291 /// - Quantifies systematic risk exposure between related instruments
292 /// - Determines optimal hedge ratios for risk management
293 /// - Provides relative sensitivity analysis for pair relationships
294 /// - Serves as a key input for factor modeling and attribution analysis
295 ///
296 /// # Returns
297 ///
298 /// * `Option<T>` - The beta coefficient in the window, or `None` if the window is not full
299 ///
300 /// # Examples
301 ///
302 /// ```
303 /// use ta_statistics::PairedStatistics;
304 /// use assert_approx_eq::assert_approx_eq;
305 ///
306 /// let mut stats = PairedStatistics::new(3);
307 /// let mut results = vec![];
308 /// let inputs = [
309 /// (0.015, 0.010),
310 /// (0.025, 0.015),
311 /// (-0.010, -0.005),
312 /// (0.030, 0.020),
313 /// (0.005, 0.010),
314 /// (-0.015, -0.010),
315 /// (0.020, 0.015),
316 /// ];
317 ///
318 /// inputs.iter().for_each(|i| {
319 /// stats.next(*i).beta().map(|v| results.push(v));
320 /// });
321 ///
322 /// let expected: [f64; 5] = [1.731, 1.643, 1.553, 1.429, 1.286];
323 /// for (i, e) in expected.iter().enumerate() {
324 /// assert_approx_eq!(e, results[i], 0.001);
325 /// }
326 /// ```
327 pub fn beta(&self) -> Option<T> {
328 self.cov().zip(self.variance()).and_then(
329 |(cov, (_, var))| {
330 if var.is_zero() { None } else { Some(cov / var) }
331 },
332 )
333 }
334}