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}