Skip to main content

quantwave_core/indicators/incremental/
overlap_ta.rs

1//! Native O(1) overlap indicators: TRIMA, KAMA, T3, MIDPOINT, MIDPRICE.
2
3use crate::indicators::incremental::rolling::{MAX, MIN};
4use crate::indicators::incremental::talib_ema::TalibEma;
5use crate::indicators::incremental::talib_sma::TalibSma;
6use crate::traits::Next;
7
8#[inline]
9fn trima_periods(period: usize) -> (usize, usize) {
10    if period % 2 == 0 {
11        (period / 2, period / 2 + 1)
12    } else {
13        let n = (period + 1) / 2;
14        (n, n)
15    }
16}
17
18/// Triangular Moving Average.
19#[derive(Debug, Clone)]
20#[allow(non_camel_case_types)]
21pub struct TRIMA {
22    pub timeperiod: usize,
23    sma1: TalibSma,
24    sma2: TalibSma,
25}
26
27impl TRIMA {
28    pub fn new(timeperiod: usize) -> Self {
29        let (n1, n2) = trima_periods(timeperiod);
30        Self {
31            timeperiod,
32            sma1: TalibSma::new(n1),
33            sma2: TalibSma::new(n2),
34        }
35    }
36}
37
38impl Next<f64> for TRIMA {
39    type Output = f64;
40
41    fn next(&mut self, input: f64) -> Self::Output {
42        let mid = self.sma1.next(input);
43        if mid.is_nan() {
44            f64::NAN
45        } else {
46            self.sma2.next(mid)
47        }
48    }
49}
50
51/// Midpoint over `timeperiod`.
52#[derive(Debug, Clone)]
53#[allow(non_camel_case_types)]
54pub struct MIDPOINT {
55    pub timeperiod: usize,
56    max: MAX,
57    min: MIN,
58}
59
60impl MIDPOINT {
61    pub fn new(timeperiod: usize) -> Self {
62        Self {
63            timeperiod,
64            max: MAX::new(timeperiod),
65            min: MIN::new(timeperiod),
66        }
67    }
68}
69
70impl Next<f64> for MIDPOINT {
71    type Output = f64;
72
73    fn next(&mut self, input: f64) -> Self::Output {
74        let hi = self.max.next(input);
75        let lo = self.min.next(input);
76        if hi.is_nan() || lo.is_nan() {
77            f64::NAN
78        } else {
79            (hi + lo) / 2.0
80        }
81    }
82}
83
84/// Midprice over `timeperiod`.
85#[derive(Debug, Clone)]
86#[allow(non_camel_case_types)]
87pub struct MIDPRICE {
88    pub timeperiod: usize,
89    max: MAX,
90    min: MIN,
91}
92
93impl MIDPRICE {
94    pub fn new(timeperiod: usize) -> Self {
95        Self {
96            timeperiod,
97            max: MAX::new(timeperiod),
98            min: MIN::new(timeperiod),
99        }
100    }
101}
102
103impl Next<(f64, f64)> for MIDPRICE {
104    type Output = f64;
105
106    fn next(&mut self, (high, low): (f64, f64)) -> Self::Output {
107        let hi = self.max.next(high);
108        let lo = self.min.next(low);
109        if hi.is_nan() || lo.is_nan() {
110            f64::NAN
111        } else {
112            (hi + lo) / 2.0
113        }
114    }
115}
116
117/// Kaufman Adaptive Moving Average (TA-Lib `overlap::kama`).
118#[derive(Debug, Clone)]
119#[allow(non_camel_case_types)]
120pub struct KAMA {
121    pub timeperiod: usize,
122    fast_sc: f64,
123    slow_sc: f64,
124    history: Vec<f64>,
125    prev_kama: f64,
126}
127
128impl KAMA {
129    pub fn new(timeperiod: usize) -> Self {
130        Self {
131            timeperiod,
132            fast_sc: 2.0 / (2.0 + 1.0),
133            slow_sc: 2.0 / (30.0 + 1.0),
134            history: Vec::new(),
135            prev_kama: 0.0,
136        }
137    }
138}
139
140impl Next<f64> for KAMA {
141    type Output = f64;
142
143    fn next(&mut self, input: f64) -> Self::Output {
144        self.history.push(input);
145        let p = self.timeperiod;
146        let n = self.history.len();
147
148        if n <= p {
149            return f64::NAN;
150        }
151        if n == p + 1 {
152            self.prev_kama = self.history[p - 1];
153        }
154
155        let today = n - 1;
156        let trailing = today - p;
157        let mut sum_roc1 = 0.0;
158        for i in 1..=p {
159            let idx_cur = today - i + 1;
160            let idx_prev = today - i;
161            sum_roc1 += (self.history[idx_cur] - self.history[idx_prev]).abs();
162        }
163        let sum_roc2 = (self.history[today] - self.history[trailing]).abs();
164        let er = if sum_roc1 != 0.0 {
165            sum_roc2 / sum_roc1
166        } else {
167            0.0
168        };
169        let sc = (er * (self.fast_sc - self.slow_sc) + self.slow_sc).powi(2);
170        self.prev_kama += sc * (input - self.prev_kama);
171        self.prev_kama
172    }
173}
174
175/// Tilson T3 — six cascaded TA-Lib EMAs with Tilson coefficients.
176#[derive(Debug, Clone)]
177#[allow(non_camel_case_types)]
178pub struct T3 {
179    pub timeperiod: usize,
180    pub v_factor: f64,
181    e1: TalibEma,
182    e2: TalibEma,
183    e3: TalibEma,
184    e4: TalibEma,
185    e5: TalibEma,
186    e6: TalibEma,
187}
188
189impl T3 {
190    pub fn new(timeperiod: usize, v_factor: f64) -> Self {
191        Self {
192            timeperiod,
193            v_factor,
194            e1: TalibEma::new(timeperiod),
195            e2: TalibEma::new(timeperiod),
196            e3: TalibEma::new(timeperiod),
197            e4: TalibEma::new(timeperiod),
198            e5: TalibEma::new(timeperiod),
199            e6: TalibEma::new(timeperiod),
200        }
201    }
202}
203
204impl Next<f64> for T3 {
205    type Output = f64;
206
207    fn next(&mut self, input: f64) -> Self::Output {
208        let v1 = self.e1.next(input);
209        let v2 = self.e2.next(v1);
210        let v3 = self.e3.next(v2);
211        let v4 = self.e4.next(v3);
212        let v5 = self.e5.next(v4);
213        let v6 = self.e6.next(v5);
214        if v6.is_nan() {
215            return f64::NAN;
216        }
217        let v = self.v_factor;
218        let c1 = -v.powi(3);
219        let c2 = 3.0 * v * v + 3.0 * v.powi(3);
220        let c3 = -6.0 * v * v - 3.0 * v - 3.0 * v.powi(3);
221        let c4 = 1.0 + 3.0 * v + v.powi(3) + 3.0 * v * v;
222        c1 * v6 + c2 * v5 + c3 * v4 + c4 * v3
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    use proptest::prelude::*;
231
232    proptest! {
233        #[test]
234        fn test_trima_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
235            let period = 10;
236            let mut trima = TRIMA::new(period);
237            let streaming: Vec<f64> = input.iter().map(|&x| trima.next(x)).collect();
238            let batch = talib_rs::overlap::trima(&input, period)
239                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
240            for (s, b) in streaming.iter().zip(batch.iter()) {
241                if s.is_nan() { assert!(b.is_nan()); }
242                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
243            }
244        }
245
246        #[test]
247        fn test_midpoint_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
248            let period = 10;
249            let mut mid = MIDPOINT::new(period);
250            let streaming: Vec<f64> = input.iter().map(|&x| mid.next(x)).collect();
251            let batch = talib_rs::overlap::midpoint(&input, period)
252                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
253            for (s, b) in streaming.iter().zip(batch.iter()) {
254                if s.is_nan() { assert!(b.is_nan()); }
255                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
256            }
257        }
258
259        #[test]
260        fn test_kama_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
261            let period = 10;
262            let mut kama = KAMA::new(period);
263            let streaming: Vec<f64> = input.iter().map(|&x| kama.next(x)).collect();
264            let batch = talib_rs::overlap::kama(&input, period)
265                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
266            for (s, b) in streaming.iter().zip(batch.iter()) {
267                if s.is_nan() { assert!(b.is_nan()); }
268                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
269            }
270        }
271
272        #[test]
273        fn test_t3_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
274            let period = 10;
275            let vf = 0.7;
276            let mut t3 = T3::new(period, vf);
277            let streaming: Vec<f64> = input.iter().map(|&x| t3.next(x)).collect();
278            let batch = talib_rs::overlap::t3(&input, period, vf)
279                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
280            for (s, b) in streaming.iter().zip(batch.iter()) {
281                if s.is_nan() { assert!(b.is_nan()); }
282                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
283            }
284        }
285    }
286}