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