Skip to main content

quantwave_core/indicators/
statistics.rs

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