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}