ta_statistics/
paired_statistics.rs

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