quantwave_core/indicators/
statistics.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4
5pub use crate::indicators::incremental::statistics_ta::{
6 TaBETA, TaCORREL, TaLINEARREG, TaLINEARREG_ANGLE, TaLINEARREG_INTERCEPT, TaLINEARREG_SLOPE,
7 TaSTDDEV, TaTSF, TaVAR,
8};
9impl From<usize> for TaBETA {
10 fn from(p: usize) -> Self {
11 Self::new(p)
12 }
13}
14impl From<usize> for TaCORREL {
15 fn from(p: usize) -> Self {
16 Self::new(p)
17 }
18}
19impl From<usize> for TaLINEARREG {
20 fn from(p: usize) -> Self {
21 Self::new(p)
22 }
23}
24impl From<usize> for TaLINEARREG_SLOPE {
25 fn from(p: usize) -> Self {
26 Self::new(p)
27 }
28}
29impl From<usize> for TaLINEARREG_INTERCEPT {
30 fn from(p: usize) -> Self {
31 Self::new(p)
32 }
33}
34impl From<usize> for TaLINEARREG_ANGLE {
35 fn from(p: usize) -> Self {
36 Self::new(p)
37 }
38}
39impl From<usize> for TaTSF {
40 fn from(p: usize) -> Self {
41 Self::new(p)
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct StandardDeviation {
48 period: usize,
49 window: VecDeque<f64>,
50 sum: f64,
51 sum_sq: f64,
52}
53
54impl StandardDeviation {
55 pub fn new(period: usize) -> Self {
56 Self {
57 period,
58 window: VecDeque::with_capacity(period),
59 sum: 0.0,
60 sum_sq: 0.0,
61 }
62 }
63}
64
65impl From<usize> for StandardDeviation {
66 fn from(period: usize) -> Self {
67 Self::new(period)
68 }
69}
70
71impl Next<f64> for StandardDeviation {
72 type Output = f64;
73
74 fn next(&mut self, input: f64) -> Self::Output {
75 self.window.push_back(input);
76 self.sum += input;
77 self.sum_sq += input * input;
78
79 if self.window.len() > self.period
80 && let Some(oldest) = self.window.pop_front()
81 {
82 self.sum -= oldest;
83 self.sum_sq -= oldest * oldest;
84 }
85
86 let n = self.window.len() as f64;
87 let mean = self.sum / n;
88 let variance = (self.sum_sq / n) - (mean * mean);
89
90 variance.max(0.0).sqrt()
92 }
93}
94
95#[derive(Debug, Clone)]
98pub struct LinearRegression {
99 period: usize,
100 window: VecDeque<f64>,
101 sum_x: f64,
103 sum_x2: f64,
104}
105
106impl LinearRegression {
107 pub fn new(period: usize) -> Self {
108 let _n = period as f64;
109 let mut sum_x = 0.0;
110 let mut sum_x2 = 0.0;
111 for i in 0..period {
112 let x = i as f64;
113 sum_x += x;
114 sum_x2 += x * x;
115 }
116
117 Self {
118 period,
119 window: VecDeque::with_capacity(period),
120 sum_x,
121 sum_x2,
122 }
123 }
124}
125
126impl From<usize> for LinearRegression {
127 fn from(period: usize) -> Self {
128 Self::new(period)
129 }
130}
131
132impl Next<f64> for LinearRegression {
133 type Output = f64;
134
135 fn next(&mut self, input: f64) -> Self::Output {
136 self.window.push_back(input);
137 if self.window.len() > self.period {
138 self.window.pop_front();
139 }
140
141 if self.window.len() < self.period {
142 let n = self.window.len() as f64;
146 let mut sum_x = 0.0;
147 let mut sum_x2 = 0.0;
148 let mut sum_y = 0.0;
149 let mut sum_xy = 0.0;
150 for (i, &y) in self.window.iter().enumerate() {
151 let x = i as f64;
152 sum_x += x;
153 sum_x2 += x * x;
154 sum_y += y;
155 sum_xy += x * y;
156 }
157
158 let denominator = n * sum_x2 - sum_x * sum_x;
159 if denominator == 0.0 {
160 return input;
161 }
162
163 let b = (n * sum_xy - sum_x * sum_y) / denominator;
164 let a = (sum_y - b * sum_x) / n;
165 return a + b * (n - 1.0);
166 }
167
168 let n = self.period as f64;
169 let mut sum_y = 0.0;
170 let mut sum_xy = 0.0;
171 for (i, &y) in self.window.iter().enumerate() {
172 let x = i as f64;
173 sum_y += y;
174 sum_xy += x * y;
175 }
176
177 let denominator = n * self.sum_x2 - self.sum_x * self.sum_x;
178 if denominator == 0.0 {
179 return input;
180 }
181
182 let b = (n * sum_xy - self.sum_x * sum_y) / denominator;
183 let a = (sum_y - b * self.sum_x) / n;
184
185 a + b * (n - 1.0)
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_stdev_basic() {
195 let mut sd = StandardDeviation::new(3);
196 assert_eq!(sd.next(10.0), 0.0);
198 assert_eq!(sd.next(20.0), 5.0);
200 approx::assert_relative_eq!(sd.next(30.0), 8.1649658092, epsilon = 1e-6);
202 }
203
204 #[test]
205 fn test_linreg_basic() {
206 let mut lr = LinearRegression::new(3);
207 lr.next(1.0);
209 lr.next(2.0);
210 let res = lr.next(3.0);
211 approx::assert_relative_eq!(res, 3.0);
212
213 let mut lr2 = LinearRegression::new(3);
215 lr2.next(5.0);
216 lr2.next(7.0);
217 let res2 = lr2.next(9.0);
218 approx::assert_relative_eq!(res2, 9.0);
219 }
220
221 use proptest::prelude::*;
222 proptest! {
223 #[test]
224 fn test_ta_stddev_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
225 let period = 10;
226 let nbdev = 1.0;
227 let mut ta_stddev = TaSTDDEV::new(period, nbdev);
228 let streaming_results: Vec<f64> = input.iter().map(|&x| ta_stddev.next(x)).collect();
229 let batch_results = talib_rs::statistic::stddev(&input, period, nbdev).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
230
231 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
232 if s.is_nan() {
233 assert!(b.is_nan());
234 } else {
235 approx::assert_relative_eq!(s, b, epsilon = 1e-6);
236 }
237 }
238 }
239
240 #[test]
241 fn test_ta_linearreg_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
242 let period = 10;
243 let mut ta_lr = TaLINEARREG::new(period);
244 let streaming_results: Vec<f64> = input.iter().map(|&x| ta_lr.next(x)).collect();
245 let batch_results = talib_rs::statistic::linearreg(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
246
247 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
248 if s.is_nan() {
249 assert!(b.is_nan());
250 } else {
251 approx::assert_relative_eq!(s, b, epsilon = 1e-6);
252 }
253 }
254 }
255 }
256}
257
258pub const STDDEV_METADATA: IndicatorMetadata = IndicatorMetadata {
259 name: "Standard Deviation",
260 description: "Standard Deviation is a statistical measure of market volatility.",
261 usage: "Use for statistical analysis of price series: linear regression, standard deviation, correlation coefficients, and other descriptive statistics used as indicator inputs.",
262 keywords: &["statistics", "classic", "volatility", "trend"],
263 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.",
264 params: &[ParamDef {
265 name: "period",
266 default: "14",
267 description: "Period",
268 }],
269 formula_source: "https://www.investopedia.com/terms/s/standarddeviation.asp",
270 formula_latex: r#"
271\[
272\sigma = \sqrt{ \frac{\sum (x_i - \mu)^2}{N} }
273\]
274"#,
275 gold_standard_file: "stddev.json",
276 category: "Classic",
277};
278
279pub const LINREG_METADATA: IndicatorMetadata = IndicatorMetadata {
280 name: "Linear Regression",
281 description: "Linear Regression plots a straight line that best fits the data prices.",
282 usage: "Use for statistical analysis of price series: linear regression, standard deviation, correlation coefficients, and other descriptive statistics used as indicator inputs.",
283 keywords: &["statistics", "classic", "volatility", "trend"],
284 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.",
285 params: &[ParamDef {
286 name: "period",
287 default: "14",
288 description: "Period",
289 }],
290 formula_source: "https://www.investopedia.com/terms/l/linearregression.asp",
291 formula_latex: r#"
292\[
293y = a + bx
294\]
295"#,
296 gold_standard_file: "linreg.json",
297 category: "Classic",
298};
299
300pub const CORREL_METADATA: IndicatorMetadata = IndicatorMetadata {
301 name: "Correlation Coefficient (CORREL)",
302 description: "A statistical measure that determines the degree to which two securities move in relation to each other.",
303 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).",
304 keywords: &["statistics", "correlation", "classic"],
305 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",
306 params: &[ParamDef {
307 name: "timeperiod",
308 default: "30",
309 description: "Lookback period",
310 }],
311 formula_source: "https://www.investopedia.com/terms/c/correlationcoefficient.asp",
312 formula_latex: r#"
313\[
314\rho_{X,Y} = \frac{\text{cov}(X,Y)}{\sigma_X \sigma_Y}
315\]
316"#,
317 gold_standard_file: "correl.json",
318 category: "Classic",
319};
320
321pub const BETA_METADATA: IndicatorMetadata = IndicatorMetadata {
322 name: "Beta (BETA)",
323 description: "A measure of a security's volatility in relation to the overall market.",
324 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.",
325 keywords: &["statistics", "risk", "classic", "volatility"],
326 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",
327 params: &[ParamDef {
328 name: "timeperiod",
329 default: "30",
330 description: "Lookback period",
331 }],
332 formula_source: "https://www.investopedia.com/terms/b/beta.asp",
333 formula_latex: r#"
334\[
335\beta = \frac{\text{Cov}(R_i, R_m)}{\text{Var}(R_m)}
336\]
337"#,
338 gold_standard_file: "beta.json",
339 category: "Classic",
340};