1use crate::indicators::incremental::rolling_variance::RollingVariance;
4use crate::traits::Next;
5use crate::utils::RingBuffer;
6
7#[inline]
8fn linreg_coeffs(ys: &[f64]) -> (f64, f64) {
9 let n = ys.len() as f64;
10 let mut sum_x = 0.0;
11 let mut sum_x2 = 0.0;
12 let mut sum_y = 0.0;
13 let mut sum_xy = 0.0;
14 for (i, &y) in ys.iter().enumerate() {
15 let x = i as f64;
16 sum_x += x;
17 sum_x2 += x * x;
18 sum_y += y;
19 sum_xy += x * y;
20 }
21 let denom = n.mul_add(sum_x2, -sum_x * sum_x);
22 if denom == 0.0 {
23 return (f64::NAN, f64::NAN);
24 }
25 let slope = n.mul_add(sum_xy, -sum_x * sum_y) / denom;
26 let intercept = (sum_y - slope * sum_x) / n;
27 (slope, intercept)
28}
29
30#[derive(Debug, Clone)]
33struct RollingWindow {
34 acc: RollingVariance,
35}
36
37impl RollingWindow {
38 fn new(period: usize) -> Self {
39 Self {
40 acc: RollingVariance::new(period),
41 }
42 }
43
44 fn push(&mut self, v: f64) -> bool {
45 self.acc.push(v)
46 }
47
48 fn values(&self) -> Vec<f64> {
49 self.acc.values()
50 }
51}
52
53#[derive(Debug, Clone)]
54#[allow(non_camel_case_types)]
55pub struct TaSTDDEV {
56 pub timeperiod: usize,
57 pub nbdev: f64,
58 inner: RollingWindow,
59}
60
61impl TaSTDDEV {
62 pub fn new(timeperiod: usize, nbdev: f64) -> Self {
63 Self {
64 timeperiod,
65 nbdev,
66 inner: RollingWindow::new(timeperiod),
67 }
68 }
69}
70
71impl Next<f64> for TaSTDDEV {
72 type Output = f64;
73
74 fn next(&mut self, input: f64) -> Self::Output {
75 if !self.inner.push(input) {
76 return f64::NAN;
77 }
78 self.inner.acc.stddev() * self.nbdev
79 }
80}
81
82#[derive(Debug, Clone)]
83#[allow(non_camel_case_types)]
84pub struct TaVAR {
85 pub timeperiod: usize,
86 pub nbdev: f64,
87 inner: RollingWindow,
88}
89
90impl TaVAR {
91 pub fn new(timeperiod: usize, nbdev: f64) -> Self {
92 Self {
93 timeperiod,
94 nbdev,
95 inner: RollingWindow::new(timeperiod),
96 }
97 }
98}
99
100impl Next<f64> for TaVAR {
101 type Output = f64;
102
103 fn next(&mut self, input: f64) -> Self::Output {
104 if !self.inner.push(input) {
105 return f64::NAN;
106 }
107 self.inner.acc.variance()
111 }
112}
113
114#[derive(Debug, Clone)]
115struct LinRegCore {
116 _period: usize,
117 inner: RollingWindow,
118}
119
120impl LinRegCore {
121 fn new(period: usize) -> Self {
122 Self {
123 _period: period,
124 inner: RollingWindow::new(period),
125 }
126 }
127
128 fn push(&mut self, v: f64) -> Option<(f64, f64)> {
129 if !self.inner.push(v) {
130 return None;
131 }
132 Some(linreg_coeffs(&self.inner.values()))
133 }
134}
135
136#[derive(Debug, Clone)]
137#[allow(non_camel_case_types)]
138pub struct TaLINEARREG {
139 pub timeperiod: usize,
140 core: LinRegCore,
141}
142
143impl TaLINEARREG {
144 pub fn new(timeperiod: usize) -> Self {
145 Self {
146 timeperiod,
147 core: LinRegCore::new(timeperiod),
148 }
149 }
150}
151
152impl Next<f64> for TaLINEARREG {
153 type Output = f64;
154
155 fn next(&mut self, input: f64) -> Self::Output {
156 let Some((slope, intercept)) = self.core.push(input) else {
157 return f64::NAN;
158 };
159 let x = (self.timeperiod - 1) as f64;
160 intercept + slope * x
161 }
162}
163
164#[derive(Debug, Clone)]
165#[allow(non_camel_case_types)]
166pub struct TaLINEARREG_SLOPE {
167 pub timeperiod: usize,
168 core: LinRegCore,
169}
170
171impl TaLINEARREG_SLOPE {
172 pub fn new(timeperiod: usize) -> Self {
173 Self {
174 timeperiod,
175 core: LinRegCore::new(timeperiod),
176 }
177 }
178}
179
180impl Next<f64> for TaLINEARREG_SLOPE {
181 type Output = f64;
182
183 fn next(&mut self, input: f64) -> Self::Output {
184 match self.core.push(input) {
185 Some((slope, _)) => slope,
186 None => f64::NAN,
187 }
188 }
189}
190
191#[derive(Debug, Clone)]
192#[allow(non_camel_case_types)]
193pub struct TaLINEARREG_INTERCEPT {
194 pub timeperiod: usize,
195 core: LinRegCore,
196}
197
198impl TaLINEARREG_INTERCEPT {
199 pub fn new(timeperiod: usize) -> Self {
200 Self {
201 timeperiod,
202 core: LinRegCore::new(timeperiod),
203 }
204 }
205}
206
207impl Next<f64> for TaLINEARREG_INTERCEPT {
208 type Output = f64;
209
210 fn next(&mut self, input: f64) -> Self::Output {
211 match self.core.push(input) {
212 Some((_, intercept)) => intercept,
213 None => f64::NAN,
214 }
215 }
216}
217
218#[derive(Debug, Clone)]
219#[allow(non_camel_case_types)]
220pub struct TaLINEARREG_ANGLE {
221 pub timeperiod: usize,
222 core: LinRegCore,
223}
224
225impl TaLINEARREG_ANGLE {
226 pub fn new(timeperiod: usize) -> Self {
227 Self {
228 timeperiod,
229 core: LinRegCore::new(timeperiod),
230 }
231 }
232}
233
234impl Next<f64> for TaLINEARREG_ANGLE {
235 type Output = f64;
236
237 fn next(&mut self, input: f64) -> Self::Output {
238 match self.core.push(input) {
239 Some((slope, _)) => slope.atan().to_degrees(),
240 None => f64::NAN,
241 }
242 }
243}
244
245#[derive(Debug, Clone)]
246#[allow(non_camel_case_types)]
247pub struct TaTSF {
248 pub timeperiod: usize,
249 core: LinRegCore,
250}
251
252impl TaTSF {
253 pub fn new(timeperiod: usize) -> Self {
254 Self {
255 timeperiod,
256 core: LinRegCore::new(timeperiod),
257 }
258 }
259}
260
261impl Next<f64> for TaTSF {
262 type Output = f64;
263
264 fn next(&mut self, input: f64) -> Self::Output {
265 let Some((slope, intercept)) = self.core.push(input) else {
266 return f64::NAN;
267 };
268 intercept + slope * self.timeperiod as f64
269 }
270}
271
272#[derive(Debug, Clone)]
273struct DualWindow {
274 period: usize,
275 xs: RingBuffer<f64>,
276 ys: RingBuffer<f64>,
277}
278
279impl DualWindow {
280 fn new(period: usize) -> Self {
281 Self {
282 period,
283 xs: RingBuffer::with_capacity(period),
284 ys: RingBuffer::with_capacity(period),
285 }
286 }
287
288 fn push(&mut self, x: f64, y: f64) -> bool {
289 if self.xs.len() >= self.period {
290 let _ = self.xs.pop_front();
291 let _ = self.ys.pop_front();
292 }
293 self.xs.push_back(x);
294 self.ys.push_back(y);
295 self.xs.len() >= self.period
296 }
297
298 fn xs_values(&self) -> Vec<f64> {
299 self.xs.iter().cloned().collect()
300 }
301
302 fn ys_values(&self) -> Vec<f64> {
303 self.ys.iter().cloned().collect()
304 }
305}
306
307#[inline]
308fn correl_beta(xs: &[f64], ys: &[f64]) -> (f64, f64) {
309 let n = xs.len() as f64;
310 let mut sum_x = 0.0;
311 let mut sum_y = 0.0;
312 let mut sum_x2 = 0.0;
313 let mut sum_y2 = 0.0;
314 let mut sum_xy = 0.0;
315 for i in 0..xs.len() {
316 let x = xs[i];
317 let y = ys[i];
318 sum_x += x;
319 sum_y += y;
320 sum_x2 += x * x;
321 sum_y2 += y * y;
322 sum_xy += x * y;
323 }
324 let cov = n.mul_add(sum_xy, -sum_x * sum_y);
325 let var_x = n.mul_add(sum_x2, -sum_x * sum_x);
326 let var_y = n.mul_add(sum_y2, -sum_y * sum_y);
327 if var_x == 0.0 || var_y == 0.0 {
328 return (f64::NAN, f64::NAN);
329 }
330 let correl = cov / (var_x.sqrt() * var_y.sqrt());
331 let beta = cov / var_x;
333 (correl, beta)
334}
335
336#[derive(Debug, Clone)]
337#[allow(non_camel_case_types)]
338pub struct TaCORREL {
339 pub timeperiod: usize,
340 inner: DualWindow,
341}
342
343impl TaCORREL {
344 pub fn new(timeperiod: usize) -> Self {
345 Self {
346 timeperiod,
347 inner: DualWindow::new(timeperiod),
348 }
349 }
350}
351
352impl Next<(f64, f64)> for TaCORREL {
353 type Output = f64;
354
355 fn next(&mut self, (x, y): (f64, f64)) -> Self::Output {
356 if !self.inner.push(x, y) {
357 return f64::NAN;
358 }
359 correl_beta(&self.inner.xs_values(), &self.inner.ys_values()).0
360 }
361}
362
363#[derive(Debug, Clone)]
365#[allow(non_camel_case_types)]
366pub struct TaBETA {
367 pub timeperiod: usize,
368 prev0: Option<f64>,
369 prev1: Option<f64>,
370 rx: RingBuffer<f64>,
371 ry: RingBuffer<f64>,
372 sx: f64,
373 sy: f64,
374 sxx: f64,
375 sxy: f64,
376}
377
378impl TaBETA {
379 pub fn new(timeperiod: usize) -> Self {
380 Self {
381 timeperiod,
382 prev0: None,
383 prev1: None,
384 rx: RingBuffer::with_capacity(timeperiod),
385 ry: RingBuffer::with_capacity(timeperiod),
386 sx: 0.0,
387 sy: 0.0,
388 sxx: 0.0,
389 sxy: 0.0,
390 }
391 }
392
393 fn push_return(&mut self, x: f64, y: f64) -> Option<f64> {
394 let p = self.timeperiod;
395 if self.rx.len() >= p
396 && let (Some(ox), Some(oy)) = (self.rx.pop_front(), self.ry.pop_front())
397 {
398 self.sx -= ox;
399 self.sy -= oy;
400 self.sxx -= ox * ox;
401 self.sxy -= ox * oy;
402 }
403 self.rx.push_back(x);
404 self.ry.push_back(y);
405 self.sx += x;
406 self.sy += y;
407 self.sxx += x * x;
408 self.sxy += x * y;
409 if self.rx.len() < p {
410 return None;
411 }
412 let n = p as f64;
413 let denom = n * self.sxx - self.sx * self.sx;
414 Some(if denom > 0.0 {
415 (n * self.sxy - self.sx * self.sy) / denom
416 } else {
417 0.0
418 })
419 }
420}
421
422impl Next<(f64, f64)> for TaBETA {
423 type Output = f64;
424
425 fn next(&mut self, (v0, v1): (f64, f64)) -> Self::Output {
426 let (Some(p0), Some(p1)) = (self.prev0, self.prev1) else {
427 self.prev0 = Some(v0);
428 self.prev1 = Some(v1);
429 return f64::NAN;
430 };
431 self.prev0 = Some(v0);
432 self.prev1 = Some(v1);
433 if p0 == 0.0 || p1 == 0.0 {
434 return f64::NAN;
435 }
436 let rx = (v0 - p0) / p0;
437 let ry = (v1 - p1) / p1;
438 self.push_return(rx, ry).unwrap_or(f64::NAN)
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::test_utils::{
446 assert_bitwise_batch_streaming_parity, log_price_series, reference_stddev,
447 };
448 use proptest::prelude::*;
449
450 #[test]
457 fn ta_stddev_matches_two_pass_reference_on_log_price() {
458 let period = 20;
459 let data = log_price_series(600);
460 let mut ind = TaSTDDEV::new(period, 1.0);
461 let mut worst: f64 = 0.0;
462 for (i, &x) in data.iter().enumerate() {
463 let got = ind.next(x);
464 if got.is_nan() {
465 continue;
466 }
467 let reference = reference_stddev(&data[i + 1 - period..=i]);
468 assert!(reference > 0.0);
469 worst = worst.max(((got - reference) / reference).abs());
470 }
471 assert!(
472 worst < 1e-7,
473 "TaSTDDEV vs two-pass reference on log-price: {worst:e} (gate 1e-7)"
474 );
475 assert!(worst < 1e-12, "expected ~1e-15 precision, got {worst:e}");
477 }
478
479 #[test]
480 fn ta_var_matches_two_pass_reference_on_log_price() {
481 let period = 20;
482 let data = log_price_series(600);
483 let mut ind = TaVAR::new(period, 1.0);
484 let mut worst: f64 = 0.0;
485 for (i, &x) in data.iter().enumerate() {
486 let got = ind.next(x);
487 if got.is_nan() {
488 continue;
489 }
490 let s = reference_stddev(&data[i + 1 - period..=i]);
491 let reference = s * s;
492 assert!(reference > 0.0);
493 worst = worst.max(((got - reference) / reference).abs());
494 }
495 assert!(worst < 1e-12, "TaVAR vs reference on log-price: {worst:e}");
496 }
497
498 #[test]
502 fn ta_stddev_batch_streaming_bitwise_parity() {
503 let data = log_price_series(500);
504 assert_bitwise_batch_streaming_parity(&data, || TaSTDDEV::new(20, 2.0));
505 assert_bitwise_batch_streaming_parity(&data, || TaVAR::new(14, 1.0));
506 let prices: Vec<f64> = (0..500)
507 .map(|i| 2980.0 + (i as f64 * 0.31).sin() * 40.0)
508 .collect();
509 assert_bitwise_batch_streaming_parity(&prices, || TaSTDDEV::new(20, 2.0));
510 assert_bitwise_batch_streaming_parity(&prices, || TaVAR::new(14, 1.0));
511 }
512
513 #[test]
516 fn linreg_family_batch_streaming_bitwise_parity() {
517 let data = log_price_series(400);
518 assert_bitwise_batch_streaming_parity(&data, || TaLINEARREG::new(14));
519 assert_bitwise_batch_streaming_parity(&data, || TaLINEARREG_SLOPE::new(14));
520 assert_bitwise_batch_streaming_parity(&data, || TaLINEARREG_INTERCEPT::new(14));
521 assert_bitwise_batch_streaming_parity(&data, || TaLINEARREG_ANGLE::new(14));
522 assert_bitwise_batch_streaming_parity(&data, || TaTSF::new(14));
523 }
524
525 proptest! {
526 #[test]
527 fn test_ta_beta_parity(
528 x in prop::collection::vec(1.0..100.0, 15..100),
529 y in prop::collection::vec(1.0..100.0, 15..100)
530 ) {
531 let len = x.len().min(y.len());
532 let period = 14;
533 let mut ind = TaBETA::new(period);
534 let streaming: Vec<f64> = (0..len).map(|i| ind.next((x[i], y[i]))).collect();
535 let batch = talib_rs::statistic::beta(&x[..len], &y[..len], period)
536 .unwrap_or_else(|_| vec![f64::NAN; len]);
537 for (s, b) in streaming.iter().zip(batch.iter()) {
538 if s.is_nan() { assert!(b.is_nan()); }
539 else if !b.is_nan() { approx::assert_relative_eq!(s, b, epsilon = 1e-5); }
540 }
541 }
542 #[test]
543 fn test_ta_stddev_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
544 let period = 10;
545 let nbdev = 1.0;
546 let mut s = TaSTDDEV::new(period, nbdev);
547 let streaming: Vec<f64> = input.iter().map(|&x| s.next(x)).collect();
548 let batch = talib_rs::statistic::stddev(&input, period, nbdev)
549 .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
550 for (a, b) in streaming.iter().zip(batch.iter()) {
551 if a.is_nan() { assert!(b.is_nan()); }
552 else { approx::assert_relative_eq!(a, b, epsilon = 1e-6); }
553 }
554 }
555
556 #[test]
557 fn test_ta_linearreg_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
558 let period = 10;
559 let mut s = TaLINEARREG::new(period);
560 let streaming: Vec<f64> = input.iter().map(|&x| s.next(x)).collect();
561 let batch = talib_rs::statistic::linearreg(&input, period)
562 .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
563 for (a, b) in streaming.iter().zip(batch.iter()) {
564 if a.is_nan() { assert!(b.is_nan()); }
565 else { approx::assert_relative_eq!(a, b, epsilon = 1e-6); }
566 }
567 }
568
569 #[test]
570 fn test_ta_correl_parity(
571 x in prop::collection::vec(0.1..100.0, 1..100),
572 y in prop::collection::vec(0.1..100.0, 1..100)
573 ) {
574 let len = x.len().min(y.len());
575 let period = 10;
576 let mut s = TaCORREL::new(period);
577 let streaming: Vec<f64> = (0..len).map(|i| s.next((x[i], y[i]))).collect();
578 let batch = talib_rs::statistic::correl(&x[..len], &y[..len], period)
579 .unwrap_or_else(|_| vec![f64::NAN; len]);
580 for (a, b) in streaming.iter().zip(batch.iter()) {
581 if a.is_nan() { assert!(b.is_nan()); }
582 else { approx::assert_relative_eq!(a, b, epsilon = 1e-6); }
583 }
584 }
585 }
586}