Skip to main content

quantwave_core/indicators/incremental/
rolling.rs

1//! Native O(1) rolling MAX / MIN / SUM / MAXINDEX / MININDEX (fixed period).
2
3use crate::traits::Next;
4use crate::utils::RingBuffer;
5
6#[derive(Debug, Clone)]
7pub struct RollingMax {
8    pub timeperiod: usize,
9    window: RingBuffer<f64>,
10}
11
12impl RollingMax {
13    pub fn new(timeperiod: usize) -> Self {
14        Self {
15            timeperiod,
16            window: RingBuffer::with_capacity(timeperiod),
17        }
18    }
19
20    fn push(&mut self, v: f64) -> f64 {
21        let p = self.timeperiod;
22        if self.window.len() >= p {
23            let _ = self.window.pop_front();
24        }
25        self.window.push_back(v);
26        if self.window.len() < p {
27            return f64::NAN;
28        }
29        let mut m = f64::NEG_INFINITY;
30        for &x in self.window.iter() {
31            if x > m {
32                m = x;
33            }
34        }
35        m
36    }
37}
38
39#[derive(Debug, Clone)]
40#[allow(non_camel_case_types)]
41pub struct MAX {
42    pub timeperiod: usize,
43    inner: RollingMax,
44}
45
46impl MAX {
47    pub fn new(timeperiod: usize) -> Self {
48        Self {
49            timeperiod,
50            inner: RollingMax::new(timeperiod),
51        }
52    }
53}
54
55impl Next<f64> for MAX {
56    type Output = f64;
57    fn next(&mut self, input: f64) -> Self::Output {
58        self.inner.push(input)
59    }
60}
61
62#[derive(Debug, Clone)]
63pub struct RollingMin {
64    pub timeperiod: usize,
65    window: RingBuffer<f64>,
66}
67
68impl RollingMin {
69    pub fn new(timeperiod: usize) -> Self {
70        Self {
71            timeperiod,
72            window: RingBuffer::with_capacity(timeperiod),
73        }
74    }
75
76    fn push(&mut self, v: f64) -> f64 {
77        let p = self.timeperiod;
78        if self.window.len() >= p {
79            let _ = self.window.pop_front();
80        }
81        self.window.push_back(v);
82        if self.window.len() < p {
83            return f64::NAN;
84        }
85        let mut m = f64::INFINITY;
86        for &x in self.window.iter() {
87            if x < m {
88                m = x;
89            }
90        }
91        m
92    }
93}
94
95#[derive(Debug, Clone)]
96#[allow(non_camel_case_types)]
97pub struct MIN {
98    pub timeperiod: usize,
99    inner: RollingMin,
100}
101
102impl MIN {
103    pub fn new(timeperiod: usize) -> Self {
104        Self {
105            timeperiod,
106            inner: RollingMin::new(timeperiod),
107        }
108    }
109}
110
111impl Next<f64> for MIN {
112    type Output = f64;
113    fn next(&mut self, input: f64) -> Self::Output {
114        self.inner.push(input)
115    }
116}
117
118#[derive(Debug, Clone)]
119pub struct RollingSum {
120    pub timeperiod: usize,
121    window: RingBuffer<f64>,
122    sum: f64,
123}
124
125impl RollingSum {
126    pub fn new(timeperiod: usize) -> Self {
127        Self {
128            timeperiod,
129            window: RingBuffer::with_capacity(timeperiod),
130            sum: 0.0,
131        }
132    }
133
134    fn push(&mut self, v: f64) -> f64 {
135        let p = self.timeperiod;
136        if self.window.len() >= p
137            && let Some(old) = self.window.pop_front()
138        {
139            self.sum -= old;
140        }
141        self.window.push_back(v);
142        self.sum += v;
143        if self.window.len() < p {
144            return f64::NAN;
145        }
146        self.sum
147    }
148}
149
150#[derive(Debug, Clone)]
151#[allow(non_camel_case_types)]
152pub struct SUM {
153    pub timeperiod: usize,
154    inner: RollingSum,
155}
156
157impl SUM {
158    pub fn new(timeperiod: usize) -> Self {
159        Self {
160            timeperiod,
161            inner: RollingSum::new(timeperiod),
162        }
163    }
164}
165
166impl Next<f64> for SUM {
167    type Output = f64;
168    fn next(&mut self, input: f64) -> Self::Output {
169        self.inner.push(input)
170    }
171}
172
173#[derive(Debug, Clone)]
174pub struct RollingMaxIndex {
175    pub timeperiod: usize,
176    window: RingBuffer<f64>,
177    bar_index: usize,
178}
179
180impl RollingMaxIndex {
181    pub fn new(timeperiod: usize) -> Self {
182        Self {
183            timeperiod,
184            window: RingBuffer::with_capacity(timeperiod),
185            bar_index: 0,
186        }
187    }
188
189    fn push(&mut self, v: f64) -> f64 {
190        let p = self.timeperiod;
191        if self.window.len() >= p {
192            let _ = self.window.pop_front();
193        }
194        self.window.push_back(v);
195        self.bar_index += 1;
196        if self.window.len() < p {
197            return f64::NAN;
198        }
199        let mut best_idx = 0usize;
200        let mut best_val = f64::NEG_INFINITY;
201        for (i, &x) in self.window.iter().enumerate() {
202            if x > best_val {
203                best_val = x;
204                best_idx = i;
205            }
206        }
207        (self.bar_index - p + best_idx) as f64
208    }
209}
210
211#[derive(Debug, Clone)]
212#[allow(non_camel_case_types)]
213pub struct MAXINDEX {
214    pub timeperiod: usize,
215    inner: RollingMaxIndex,
216}
217
218impl MAXINDEX {
219    pub fn new(timeperiod: usize) -> Self {
220        Self {
221            timeperiod,
222            inner: RollingMaxIndex::new(timeperiod),
223        }
224    }
225}
226
227impl Next<f64> for MAXINDEX {
228    type Output = f64;
229    fn next(&mut self, input: f64) -> Self::Output {
230        self.inner.push(input)
231    }
232}
233
234#[derive(Debug, Clone)]
235pub struct RollingMinIndex {
236    pub timeperiod: usize,
237    window: RingBuffer<f64>,
238    bar_index: usize,
239}
240
241impl RollingMinIndex {
242    pub fn new(timeperiod: usize) -> Self {
243        Self {
244            timeperiod,
245            window: RingBuffer::with_capacity(timeperiod),
246            bar_index: 0,
247        }
248    }
249
250    fn push(&mut self, v: f64) -> f64 {
251        let p = self.timeperiod;
252        if self.window.len() >= p {
253            let _ = self.window.pop_front();
254        }
255        self.window.push_back(v);
256        self.bar_index += 1;
257        if self.window.len() < p {
258            return f64::NAN;
259        }
260        let mut best_idx = 0usize;
261        let mut best_val = f64::INFINITY;
262        for (i, &x) in self.window.iter().enumerate() {
263            if x < best_val {
264                best_val = x;
265                best_idx = i;
266            }
267        }
268        (self.bar_index - p + best_idx) as f64
269    }
270}
271
272#[derive(Debug, Clone)]
273#[allow(non_camel_case_types)]
274pub struct MININDEX {
275    pub timeperiod: usize,
276    inner: RollingMinIndex,
277}
278
279impl MININDEX {
280    pub fn new(timeperiod: usize) -> Self {
281        Self {
282            timeperiod,
283            inner: RollingMinIndex::new(timeperiod),
284        }
285    }
286}
287
288impl Next<f64> for MININDEX {
289    type Output = f64;
290    fn next(&mut self, input: f64) -> Self::Output {
291        self.inner.push(input)
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use proptest::prelude::*;
299
300    proptest! {
301        #[test]
302        fn test_max_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
303            let period = 10;
304            let mut max = MAX::new(period);
305            let streaming: Vec<f64> = input.iter().map(|&x| max.next(x)).collect();
306            let batch = talib_rs::math_operator::max(&input, period)
307                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
308            for (s, b) in streaming.iter().zip(batch.iter()) {
309                if s.is_nan() { assert!(b.is_nan()); }
310                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
311            }
312        }
313    }
314}