Skip to main content

quantwave_core/indicators/
statistics.rs

1use 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/// Standard Deviation (Population)
47///
48/// Backed by [`RollingVariance`], a shifted-data accumulator with a
49/// deterministic periodic exact refresh. The previous `E[X²] - E[X]²` form lost
50/// ~1e-6 relative precision on slowly-varying series such as log-price
51/// (quantwave-qkft); this one stays at reference precision while remaining
52/// O(1) amortised per bar, so batch and streaming stay bit-identical.
53#[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            // Preserved from the original: a zero period yields no window.
80            return f64::NAN;
81        }
82        self.acc.push(input);
83        // Population stddev over the partial-or-full window, matching the
84        // original "emit from bar 0" warmup convention.
85        self.acc.stddev()
86    }
87}
88
89/// Linear Regression
90/// Returns the value of the regression line at the current bar.
91#[derive(Debug, Clone)]
92pub struct LinearRegression {
93    period: usize,
94    window: VecDeque<f64>,
95    // Precomputed x values and their sums
96    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            // For partial windows, we could recalculate x sums,
137            // but for TTM Squeeze, we'll wait for full window or return partial.
138            // Standard approach: wait for full window or adjust n.
139            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        // [10] -> mean 10, var 0
191        assert_eq!(sd.next(10.0), 0.0);
192        // [10, 20] -> mean 15, var (100+400)/2 - 225 = 250 - 225 = 25 -> std 5
193        assert_eq!(sd.next(20.0), 5.0);
194        // [10, 20, 30] -> mean 20, var (100+400+900)/3 - 400 = 1400/3 - 400 = 466.66 - 400 = 66.66 -> std 8.1649
195        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        // Perfect line: 1, 2, 3
202        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        // Line y = 2x + 5. x in [0, 1, 2]. y = [5, 7, 9]
208        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    /// quantwave-qkft: `StandardDeviation` on log-price must agree with a
220    /// compensated two-pass reference well inside the strict 1e-7 gate that the
221    /// old one-pass accumulator failed (~1e-6 relative).
222    #[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; // partial-window warmup
232            }
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    /// Warmup bars (partial windows) must also be at reference precision.
245    #[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};