Skip to main content

quantwave_core/indicators/
statistics.rs

1use 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/// Standard Deviation (Population)
46#[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        // Handle floating point precision issues
91        variance.max(0.0).sqrt()
92    }
93}
94
95/// Linear Regression
96/// Returns the value of the regression line at the current bar.
97#[derive(Debug, Clone)]
98pub struct LinearRegression {
99    period: usize,
100    window: VecDeque<f64>,
101    // Precomputed x values and their sums
102    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            // For partial windows, we could recalculate x sums,
143            // but for TTM Squeeze, we'll wait for full window or return partial.
144            // Standard approach: wait for full window or adjust n.
145            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        // [10] -> mean 10, var 0
197        assert_eq!(sd.next(10.0), 0.0);
198        // [10, 20] -> mean 15, var (100+400)/2 - 225 = 250 - 225 = 25 -> std 5
199        assert_eq!(sd.next(20.0), 5.0);
200        // [10, 20, 30] -> mean 20, var (100+400+900)/3 - 400 = 1400/3 - 400 = 466.66 - 400 = 66.66 -> std 8.1649
201        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        // Perfect line: 1, 2, 3
208        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        // Line y = 2x + 5. x in [0, 1, 2]. y = [5, 7, 9]
214        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};