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