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}