quantwave_core/indicators/
statistics.rs1use crate::indicators::incremental::rolling_variance::RollingVariance;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::traits::Next;
4use crate::utils::RingBuffer as VecDeque;
5
6pub use crate::indicators::incremental::statistics_ta::{
7 TaBETA, TaCORREL, TaLINEARREG, TaLINEARREG_ANGLE, TaLINEARREG_INTERCEPT, TaLINEARREG_SLOPE,
8 TaSTDDEV, TaTSF, TaVAR,
9};
10impl From<usize> for TaBETA {
11 fn from(p: usize) -> Self {
12 Self::new(p)
13 }
14}
15impl From<usize> for TaCORREL {
16 fn from(p: usize) -> Self {
17 Self::new(p)
18 }
19}
20impl From<usize> for TaLINEARREG {
21 fn from(p: usize) -> Self {
22 Self::new(p)
23 }
24}
25impl From<usize> for TaLINEARREG_SLOPE {
26 fn from(p: usize) -> Self {
27 Self::new(p)
28 }
29}
30impl From<usize> for TaLINEARREG_INTERCEPT {
31 fn from(p: usize) -> Self {
32 Self::new(p)
33 }
34}
35impl From<usize> for TaLINEARREG_ANGLE {
36 fn from(p: usize) -> Self {
37 Self::new(p)
38 }
39}
40impl From<usize> for TaTSF {
41 fn from(p: usize) -> Self {
42 Self::new(p)
43 }
44}
45
46#[derive(Debug, Clone)]
54pub struct StandardDeviation {
55 period: usize,
56 acc: RollingVariance,
57}
58
59impl StandardDeviation {
60 pub fn new(period: usize) -> Self {
61 Self {
62 period,
63 acc: RollingVariance::new(period),
64 }
65 }
66}
67
68impl From<usize> for StandardDeviation {
69 fn from(period: usize) -> Self {
70 Self::new(period)
71 }
72}
73
74impl Next<f64> for StandardDeviation {
75 type Output = f64;
76
77 fn next(&mut self, input: f64) -> Self::Output {
78 if self.period == 0 {
79 return f64::NAN;
81 }
82 self.acc.push(input);
83 self.acc.stddev()
86 }
87}
88
89#[derive(Debug, Clone)]
92pub struct LinearRegression {
93 period: usize,
94 window: VecDeque<f64>,
95 sum_x: f64,
97 sum_x2: f64,
98}
99
100impl LinearRegression {
101 pub fn new(period: usize) -> Self {
102 let _n = period as f64;
103 let mut sum_x = 0.0;
104 let mut sum_x2 = 0.0;
105 for i in 0..period {
106 let x = i as f64;
107 sum_x += x;
108 sum_x2 += x * x;
109 }
110
111 Self {
112 period,
113 window: VecDeque::with_capacity(period),
114 sum_x,
115 sum_x2,
116 }
117 }
118}
119
120impl From<usize> for LinearRegression {
121 fn from(period: usize) -> Self {
122 Self::new(period)
123 }
124}
125
126impl Next<f64> for LinearRegression {
127 type Output = f64;
128
129 fn next(&mut self, input: f64) -> Self::Output {
130 self.window.push_back(input);
131 if self.window.len() > self.period {
132 self.window.pop_front();
133 }
134
135 if self.window.len() < self.period {
136 let n = self.window.len() as f64;
140 let mut sum_x = 0.0;
141 let mut sum_x2 = 0.0;
142 let mut sum_y = 0.0;
143 let mut sum_xy = 0.0;
144 for (i, &y) in self.window.iter().enumerate() {
145 let x = i as f64;
146 sum_x += x;
147 sum_x2 += x * x;
148 sum_y += y;
149 sum_xy += x * y;
150 }
151
152 let denominator = n * sum_x2 - sum_x * sum_x;
153 if denominator == 0.0 {
154 return input;
155 }
156
157 let b = (n * sum_xy - sum_x * sum_y) / denominator;
158 let a = (sum_y - b * sum_x) / n;
159 return a + b * (n - 1.0);
160 }
161
162 let n = self.period as f64;
163 let mut sum_y = 0.0;
164 let mut sum_xy = 0.0;
165 for (i, &y) in self.window.iter().enumerate() {
166 let x = i as f64;
167 sum_y += y;
168 sum_xy += x * y;
169 }
170
171 let denominator = n * self.sum_x2 - self.sum_x * self.sum_x;
172 if denominator == 0.0 {
173 return input;
174 }
175
176 let b = (n * sum_xy - self.sum_x * sum_y) / denominator;
177 let a = (sum_y - b * self.sum_x) / n;
178
179 a + b * (n - 1.0)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_stdev_basic() {
189 let mut sd = StandardDeviation::new(3);
190 assert_eq!(sd.next(10.0), 0.0);
192 assert_eq!(sd.next(20.0), 5.0);
194 approx::assert_relative_eq!(sd.next(30.0), 8.1649658092, epsilon = 1e-6);
196 }
197
198 #[test]
199 fn test_linreg_basic() {
200 let mut lr = LinearRegression::new(3);
201 lr.next(1.0);
203 lr.next(2.0);
204 let res = lr.next(3.0);
205 approx::assert_relative_eq!(res, 3.0);
206
207 let mut lr2 = LinearRegression::new(3);
209 lr2.next(5.0);
210 lr2.next(7.0);
211 let res2 = lr2.next(9.0);
212 approx::assert_relative_eq!(res2, 9.0);
213 }
214
215 use crate::test_utils::{
216 assert_bitwise_batch_streaming_parity, log_price_series, reference_stddev,
217 };
218
219 #[test]
223 fn stddev_matches_two_pass_reference_on_log_price() {
224 let period = 20;
225 let data = log_price_series(600);
226 let mut sd = StandardDeviation::new(period);
227 let mut worst: f64 = 0.0;
228 for (i, &x) in data.iter().enumerate() {
229 let got = sd.next(x);
230 if i + 1 < period {
231 continue; }
233 let reference = reference_stddev(&data[i + 1 - period..=i]);
234 assert!(reference > 0.0);
235 worst = worst.max(((got - reference) / reference).abs());
236 }
237 assert!(
238 worst < 1e-7,
239 "StandardDeviation vs two-pass reference on log-price: {worst:e} (gate 1e-7)"
240 );
241 assert!(worst < 1e-12, "expected ~1e-15 precision, got {worst:e}");
242 }
243
244 #[test]
246 fn stddev_partial_window_matches_reference() {
247 let period = 20;
248 let data = log_price_series(30);
249 let mut sd = StandardDeviation::new(period);
250 for (i, &x) in data.iter().enumerate() {
251 let got = sd.next(x);
252 let lo = (i + 1).saturating_sub(period);
253 let reference = reference_stddev(&data[lo..=i]);
254 if reference > 0.0 {
255 approx::assert_relative_eq!(got, reference, max_relative = 1e-12);
256 } else {
257 assert_eq!(got, 0.0);
258 }
259 }
260 }
261
262 #[test]
263 fn stddev_batch_streaming_bitwise_parity() {
264 assert_bitwise_batch_streaming_parity(&log_price_series(500), || {
265 StandardDeviation::new(20)
266 });
267 let prices: Vec<f64> = (0..500)
268 .map(|i| 2980.0 + (i as f64 * 0.31).sin() * 40.0)
269 .collect();
270 assert_bitwise_batch_streaming_parity(&prices, || StandardDeviation::new(30));
271 assert_bitwise_batch_streaming_parity(&prices, || LinearRegression::new(14));
272 }
273
274 use proptest::prelude::*;
275 proptest! {
276 #[test]
277 fn test_ta_stddev_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
278 let period = 10;
279 let nbdev = 1.0;
280 let mut ta_stddev = TaSTDDEV::new(period, nbdev);
281 let streaming_results: Vec<f64> = input.iter().map(|&x| ta_stddev.next(x)).collect();
282 let batch_results = talib_rs::statistic::stddev(&input, period, nbdev).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
283
284 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
285 if s.is_nan() {
286 assert!(b.is_nan());
287 } else {
288 approx::assert_relative_eq!(s, b, epsilon = 1e-6);
289 }
290 }
291 }
292
293 #[test]
294 fn test_ta_linearreg_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
295 let period = 10;
296 let mut ta_lr = TaLINEARREG::new(period);
297 let streaming_results: Vec<f64> = input.iter().map(|&x| ta_lr.next(x)).collect();
298 let batch_results = talib_rs::statistic::linearreg(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
299
300 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
301 if s.is_nan() {
302 assert!(b.is_nan());
303 } else {
304 approx::assert_relative_eq!(s, b, epsilon = 1e-6);
305 }
306 }
307 }
308 }
309}
310
311pub const STDDEV_METADATA: IndicatorMetadata = IndicatorMetadata {
312 name: "Standard Deviation",
313 description: "Standard Deviation is a statistical measure of market volatility.",
314 usage: "Use for statistical analysis of price series: linear regression, standard deviation, correlation coefficients, and other descriptive statistics used as indicator inputs.",
315 keywords: &["statistics", "classic", "volatility", "trend"],
316 ehlers_summary: "Standard statistical measures provide the mathematical foundation for many technical indicators. Linear regression finds the best-fit line through price, standard deviation quantifies dispersion, and correlation coefficients measure how closely two series move together — all are essential for quantitative strategy construction.",
317 params: &[ParamDef {
318 name: "period",
319 default: "14",
320 description: "Period",
321 }],
322 formula_source: "https://www.investopedia.com/terms/s/standarddeviation.asp",
323 formula_latex: r#"
324\[
325\sigma = \sqrt{ \frac{\sum (x_i - \mu)^2}{N} }
326\]
327"#,
328 gold_standard_file: "stddev.json",
329 category: "Classic",
330};
331
332pub const LINREG_METADATA: IndicatorMetadata = IndicatorMetadata {
333 name: "Linear Regression",
334 description: "Linear Regression plots a straight line that best fits the data prices.",
335 usage: "Use for statistical analysis of price series: linear regression, standard deviation, correlation coefficients, and other descriptive statistics used as indicator inputs.",
336 keywords: &["statistics", "classic", "volatility", "trend"],
337 ehlers_summary: "Standard statistical measures provide the mathematical foundation for many technical indicators. Linear regression finds the best-fit line through price, standard deviation quantifies dispersion, and correlation coefficients measure how closely two series move together — all are essential for quantitative strategy construction.",
338 params: &[ParamDef {
339 name: "period",
340 default: "14",
341 description: "Period",
342 }],
343 formula_source: "https://www.investopedia.com/terms/l/linearregression.asp",
344 formula_latex: r#"
345\[
346y = a + bx
347\]
348"#,
349 gold_standard_file: "linreg.json",
350 category: "Classic",
351};
352
353pub const CORREL_METADATA: IndicatorMetadata = IndicatorMetadata {
354 name: "Correlation Coefficient (CORREL)",
355 description: "A statistical measure that determines the degree to which two securities move in relation to each other.",
356 usage: "Use to measure the strength and direction of the linear relationship between two assets. Values range from -1.0 (inverse correlation) to +1.0 (perfect correlation).",
357 keywords: &["statistics", "correlation", "classic"],
358 ehlers_summary: "The Pearson Correlation Coefficient measures the strength and direction of a linear relationship between two price series. It is a fundamental tool for pair trading and portfolio diversification, allowing traders to quantify how much of a security's movement is explained by another. — StockCharts ChartSchool",
359 params: &[ParamDef {
360 name: "timeperiod",
361 default: "30",
362 description: "Lookback period",
363 }],
364 formula_source: "https://www.investopedia.com/terms/c/correlationcoefficient.asp",
365 formula_latex: r#"
366\[
367\rho_{X,Y} = \frac{\text{cov}(X,Y)}{\sigma_X \sigma_Y}
368\]
369"#,
370 gold_standard_file: "correl.json",
371 category: "Classic",
372};
373
374pub const BETA_METADATA: IndicatorMetadata = IndicatorMetadata {
375 name: "Beta (BETA)",
376 description: "A measure of a security's volatility in relation to the overall market.",
377 usage: "Use to understand the systematic risk of an asset. A beta of 1.0 indicates the asset moves with the market; >1.0 means it is more volatile, and <1.0 means it is less volatile.",
378 keywords: &["statistics", "risk", "classic", "volatility"],
379 ehlers_summary: "Beta is a measure of the volatility—or systematic risk—of a security or portfolio compared to the market as a whole. It is used in the Capital Asset Pricing Model (CAPM) to calculate the expected return of an asset based on its beta and expected market returns. — Investopedia",
380 params: &[ParamDef {
381 name: "timeperiod",
382 default: "30",
383 description: "Lookback period",
384 }],
385 formula_source: "https://www.investopedia.com/terms/b/beta.asp",
386 formula_latex: r#"
387\[
388\beta = \frac{\text{Cov}(R_i, R_m)}{\text{Var}(R_m)}
389\]
390"#,
391 gold_standard_file: "beta.json",
392 category: "Classic",
393};