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 {
85            if let Some(oldest) = self.window.pop_front() {
86                self.sum -= oldest;
87                self.sum_sq -= oldest * oldest;
88            }
89        }
90
91        let n = self.window.len() as f64;
92        let mean = self.sum / n;
93        let variance = (self.sum_sq / n) - (mean * mean);
94
95        // Handle floating point precision issues
96        variance.max(0.0).sqrt()
97    }
98}
99
100/// Linear Regression
101/// Returns the value of the regression line at the current bar.
102#[derive(Debug, Clone)]
103pub struct LinearRegression {
104    period: usize,
105    window: VecDeque<f64>,
106    // Precomputed x values and their sums
107    sum_x: f64,
108    sum_x2: f64,
109}
110
111impl LinearRegression {
112    pub fn new(period: usize) -> Self {
113        let _n = period as f64;
114        let mut sum_x = 0.0;
115        let mut sum_x2 = 0.0;
116        for i in 0..period {
117            let x = i as f64;
118            sum_x += x;
119            sum_x2 += x * x;
120        }
121
122        Self {
123            period,
124            window: VecDeque::with_capacity(period),
125            sum_x,
126            sum_x2,
127        }
128    }
129}
130
131impl From<usize> for LinearRegression {
132    fn from(period: usize) -> Self {
133        Self::new(period)
134    }
135}
136
137impl Next<f64> for LinearRegression {
138    type Output = f64;
139
140    fn next(&mut self, input: f64) -> Self::Output {
141        self.window.push_back(input);
142        if self.window.len() > self.period {
143            self.window.pop_front();
144        }
145
146        if self.window.len() < self.period {
147            // For partial windows, we could recalculate x sums,
148            // but for TTM Squeeze, we'll wait for full window or return partial.
149            // Standard approach: wait for full window or adjust n.
150            let n = self.window.len() as f64;
151            let mut sum_x = 0.0;
152            let mut sum_x2 = 0.0;
153            let mut sum_y = 0.0;
154            let mut sum_xy = 0.0;
155            for (i, &y) in self.window.iter().enumerate() {
156                let x = i as f64;
157                sum_x += x;
158                sum_x2 += x * x;
159                sum_y += y;
160                sum_xy += x * y;
161            }
162
163            let denominator = n * sum_x2 - sum_x * sum_x;
164            if denominator == 0.0 {
165                return input;
166            }
167
168            let b = (n * sum_xy - sum_x * sum_y) / denominator;
169            let a = (sum_y - b * sum_x) / n;
170            return a + b * (n - 1.0);
171        }
172
173        let n = self.period as f64;
174        let mut sum_y = 0.0;
175        let mut sum_xy = 0.0;
176        for (i, &y) in self.window.iter().enumerate() {
177            let x = i as f64;
178            sum_y += y;
179            sum_xy += x * y;
180        }
181
182        let denominator = n * self.sum_x2 - self.sum_x * self.sum_x;
183        if denominator == 0.0 {
184            return input;
185        }
186
187        let b = (n * sum_xy - self.sum_x * sum_y) / denominator;
188        let a = (sum_y - b * self.sum_x) / n;
189
190        a + b * (n - 1.0)
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_stdev_basic() {
200        let mut sd = StandardDeviation::new(3);
201        // [10] -> mean 10, var 0
202        assert_eq!(sd.next(10.0), 0.0);
203        // [10, 20] -> mean 15, var (100+400)/2 - 225 = 250 - 225 = 25 -> std 5
204        assert_eq!(sd.next(20.0), 5.0);
205        // [10, 20, 30] -> mean 20, var (100+400+900)/3 - 400 = 1400/3 - 400 = 466.66 - 400 = 66.66 -> std 8.1649
206        approx::assert_relative_eq!(sd.next(30.0), 8.1649658092, epsilon = 1e-6);
207    }
208
209    #[test]
210    fn test_linreg_basic() {
211        let mut lr = LinearRegression::new(3);
212        // Perfect line: 1, 2, 3
213        lr.next(1.0);
214        lr.next(2.0);
215        let res = lr.next(3.0);
216        approx::assert_relative_eq!(res, 3.0);
217
218        // Line y = 2x + 5. x in [0, 1, 2]. y = [5, 7, 9]
219        let mut lr2 = LinearRegression::new(3);
220        lr2.next(5.0);
221        lr2.next(7.0);
222        let res2 = lr2.next(9.0);
223        approx::assert_relative_eq!(res2, 9.0);
224    }
225
226    use proptest::prelude::*;
227    proptest! {
228        #[test]
229        fn test_ta_stddev_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
230            let period = 10;
231            let nbdev = 1.0;
232            let mut ta_stddev = TaSTDDEV::new(period, nbdev);
233            let streaming_results: Vec<f64> = input.iter().map(|&x| ta_stddev.next(x)).collect();
234            let batch_results = talib_rs::statistic::stddev(&input, period, nbdev).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
235
236            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
237                if s.is_nan() {
238                    assert!(b.is_nan());
239                } else {
240                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
241                }
242            }
243        }
244
245        #[test]
246        fn test_ta_linearreg_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
247            let period = 10;
248            let mut ta_lr = TaLINEARREG::new(period);
249            let streaming_results: Vec<f64> = input.iter().map(|&x| ta_lr.next(x)).collect();
250            let batch_results = talib_rs::statistic::linearreg(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
251
252            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
253                if s.is_nan() {
254                    assert!(b.is_nan());
255                } else {
256                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
257                }
258            }
259        }
260    }
261}
262
263pub const STDDEV_METADATA: IndicatorMetadata = IndicatorMetadata {
264    name: "Standard Deviation",
265    description: "Standard Deviation is a statistical measure of market volatility.",
266    params: &[ParamDef {
267        name: "period",
268        default: "14",
269        description: "Period",
270    }],
271    formula_source: "https://www.investopedia.com/terms/s/standarddeviation.asp",
272    formula_latex: r#"
273\[
274\sigma = \sqrt{ \frac{\sum (x_i - \mu)^2}{N} }
275\]
276"#,
277    gold_standard_file: "stddev.json",
278    category: "Classic",
279};
280
281pub const LINREG_METADATA: IndicatorMetadata = IndicatorMetadata {
282    name: "Linear Regression",
283    description: "Linear Regression plots a straight line that best fits the data prices.",
284    params: &[ParamDef {
285        name: "period",
286        default: "14",
287        description: "Period",
288    }],
289    formula_source: "https://www.investopedia.com/terms/l/linearregression.asp",
290    formula_latex: r#"
291\[
292y = a + bx
293\]
294"#,
295    gold_standard_file: "linreg.json",
296    category: "Classic",
297};