Skip to main content

quantwave_core/indicators/
math.rs

1use crate::utils::RingBuffer as VecDeque;
2
3native_pointwise_1!(ACOS, f64::acos);
4native_pointwise_1!(ASIN, f64::asin);
5native_pointwise_1!(ATAN, f64::atan);
6native_pointwise_1!(CEIL, f64::ceil);
7native_pointwise_1!(COS, f64::cos);
8native_pointwise_1!(COSH, f64::cosh);
9native_pointwise_1!(EXP, f64::exp);
10native_pointwise_1!(FLOOR, f64::floor);
11native_pointwise_1!(LN, f64::ln);
12native_pointwise_1!(LOG10, f64::log10);
13native_pointwise_1!(SIN, f64::sin);
14native_pointwise_1!(SINH, f64::sinh);
15native_pointwise_1!(SQRT, f64::sqrt);
16native_pointwise_1!(TAN, f64::tan);
17native_pointwise_1!(TANH, f64::tanh);
18
19/// Root Mean Square (RMS)
20#[derive(Debug, Clone)]
21pub struct RMS {
22    period: usize,
23    history: VecDeque<f64>,
24    sum_sq: f64,
25}
26
27impl RMS {
28    pub fn new(period: usize) -> Self {
29        Self {
30            period,
31            history: VecDeque::with_capacity(period),
32            sum_sq: 0.0,
33        }
34    }
35}
36
37impl crate::traits::Next<f64> for RMS {
38    type Output = f64;
39
40    fn next(&mut self, input: f64) -> Self::Output {
41        let input_sq = input * input;
42        self.sum_sq += input_sq;
43        self.history.push_back(input_sq);
44
45        if self.history.len() > self.period
46            && let Some(old) = self.history.pop_front()
47        {
48            self.sum_sq -= old;
49        }
50
51        if self.history.is_empty() {
52            0.0
53        } else {
54            (self.sum_sq / self.history.len() as f64).sqrt()
55        }
56    }
57}
58
59/// Automatic Gain Control (AGC)
60///
61/// Normalizes a signal based on its decaying peak value.
62/// Commonly used in John Ehlers' oscillators to keep the signal within [-1, 1].
63#[derive(Debug, Clone)]
64pub struct AGC {
65    peak: f64,
66    decay: f64,
67}
68
69impl AGC {
70    pub fn new(decay: f64) -> Self {
71        Self {
72            peak: 0.0000001,
73            decay,
74        }
75    }
76}
77
78impl crate::traits::Next<f64> for AGC {
79    type Output = f64;
80
81    fn next(&mut self, input: f64) -> Self::Output {
82        self.peak *= self.decay;
83        let abs_input = input.abs();
84        if abs_input > self.peak {
85            self.peak = abs_input;
86        }
87
88        if self.peak != 0.0 {
89            input / self.peak
90        } else {
91            0.0
92        }
93    }
94}
95
96native_binary_2!(ADD, |a, b| a + b);
97native_binary_2!(SUB, |a, b| a - b);
98native_binary_2!(MULT, |a, b| a * b);
99native_binary_2!(DIV, |a, b| a / b);
100
101pub use crate::indicators::incremental::rolling::{MAX, MAXINDEX, MIN, MININDEX, SUM};
102impl From<usize> for MAX {
103    fn from(p: usize) -> Self {
104        Self::new(p)
105    }
106}
107impl From<usize> for MAXINDEX {
108    fn from(p: usize) -> Self {
109        Self::new(p)
110    }
111}
112impl From<usize> for MIN {
113    fn from(p: usize) -> Self {
114        Self::new(p)
115    }
116}
117impl From<usize> for MININDEX {
118    fn from(p: usize) -> Self {
119        Self::new(p)
120    }
121}
122impl From<usize> for SUM {
123    fn from(p: usize) -> Self {
124        Self::new(p)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::traits::Next;
132    use proptest::prelude::*;
133
134    proptest! {
135        #[test]
136        fn test_sqrt_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
137            let mut sqrt = SQRT::new();
138            let streaming_results: Vec<f64> = input.iter().map(|&x| sqrt.next(x)).collect();
139            let batch_results = talib_rs::math_transform::sqrt(&input);
140
141            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
142                if s.is_nan() {
143                    assert!(b.is_nan());
144                } else {
145                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
146                }
147            }
148        }
149
150        #[test]
151        fn test_add_parity(
152            in1 in prop::collection::vec(0.1..100.0, 1..100),
153            in2 in prop::collection::vec(0.1..100.0, 1..100)
154        ) {
155            let len = in1.len().min(in2.len());
156            if len == 0 { return Ok(()); }
157
158            let mut add = ADD::new();
159            let streaming_results: Vec<f64> = (0..len).map(|i| add.next((in1[i], in2[i]))).collect();
160            let batch_results = talib_rs::math_operator::add(&in1[..len], &in2[..len]).unwrap_or_else(|_| vec![f64::NAN; len]);
161
162            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
163                if s.is_nan() {
164                    assert!(b.is_nan());
165                } else {
166                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
167                }
168            }
169        }
170
171        #[test]
172        fn test_rms_parity(input in prop::collection::vec(0.1..100.0, 10..100)) {
173            let period = 10;
174            let mut rms = RMS::new(period);
175            let streaming_results: Vec<f64> = input.iter().map(|&x| rms.next(x)).collect();
176
177            let mut batch_results = Vec::with_capacity(input.len());
178            for i in 0..input.len() {
179                let start = if i + 1 > period { i + 1 - period } else { 0 };
180                let window = &input[start..i+1];
181                let sum_sq: f64 = window.iter().map(|&x| x*x).sum();
182                batch_results.push((sum_sq / window.len() as f64).sqrt());
183            }
184
185            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
186                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
187            }
188        }
189    }
190}