Skip to main content

kestrel_chartkit/
series.rs

1use std::collections::VecDeque;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6/// Sliding historical lookback buffer for streaming time series values.
7/// Supports 0-indexed reverse access where `0` is the current bar, `1` is the previous bar, etc.
8#[derive(Debug, Clone, PartialEq)]
9#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10pub struct Series<T> {
11    capacity: usize,
12    buffer: VecDeque<T>,
13}
14
15impl<T> Series<T> {
16    /// Creates a new `Series` with a maximum lookback history capacity.
17    pub fn new(capacity: usize) -> Self {
18        Self {
19            capacity: capacity.max(1),
20            buffer: VecDeque::with_capacity(capacity.max(1)),
21        }
22    }
23
24    /// Pushes a new value onto the series, evicting the oldest value if capacity is reached.
25    pub fn push(&mut self, value: T) {
26        if self.buffer.len() >= self.capacity {
27            self.buffer.pop_back();
28        }
29        self.buffer.push_front(value);
30    }
31
32    /// Returns a reference to the value at `offset` bars ago (0 = current, 1 = previous, etc.).
33    pub fn get(&self, offset: usize) -> Option<&T> {
34        self.buffer.get(offset)
35    }
36
37    /// Returns a reference to the latest value (offset 0).
38    pub fn latest(&self) -> Option<&T> {
39        self.buffer.front()
40    }
41
42    /// Returns the number of items currently in history.
43    pub fn len(&self) -> usize {
44        self.buffer.len()
45    }
46
47    /// Returns true if the series is empty.
48    pub fn is_empty(&self) -> bool {
49        self.buffer.is_empty()
50    }
51
52    /// Clears the series history.
53    pub fn reset(&mut self) {
54        self.buffer.clear();
55    }
56
57    /// Returns the number of bars since `predicate` evaluated to true.
58    pub fn barssince<F>(&self, predicate: F) -> Option<usize>
59    where
60        F: Fn(&T) -> bool,
61    {
62        for (i, val) in self.buffer.iter().enumerate() {
63            if predicate(val) {
64                return Some(i);
65            }
66        }
67        None
68    }
69
70    /// Returns the value when `predicate` evaluated to true `occurrence` times ago (0-indexed).
71    pub fn valuewhen<F>(&self, predicate: F, occurrence: usize) -> Option<&T>
72    where
73        F: Fn(&T) -> bool,
74    {
75        let mut count = 0;
76        for val in self.buffer.iter() {
77            if predicate(val) {
78                if count == occurrence {
79                    return Some(val);
80                }
81                count += 1;
82            }
83        }
84        None
85    }
86}
87
88impl Series<f64> {
89    /// Returns the maximum value in the last `n` bars.
90    pub fn highest(&self, n: usize) -> Option<f64> {
91        if n == 0 || self.buffer.is_empty() {
92            return None;
93        }
94        self.buffer
95            .iter()
96            .take(n)
97            .copied()
98            .filter(|v| v.is_finite())
99            .fold(None, |max, val| match max {
100                None => Some(val),
101                Some(m) => Some(m.max(val)),
102            })
103    }
104
105    /// Returns the minimum value in the last `n` bars.
106    pub fn lowest(&self, n: usize) -> Option<f64> {
107        if n == 0 || self.buffer.is_empty() {
108            return None;
109        }
110        self.buffer
111            .iter()
112            .take(n)
113            .copied()
114            .filter(|v| v.is_finite())
115            .fold(None, |min, val| match min {
116                None => Some(val),
117                Some(m) => Some(m.min(val)),
118            })
119    }
120
121    /// Returns the offset (0..n-1) of the highest value in the last `n` bars.
122    pub fn highestbars(&self, n: usize) -> Option<usize> {
123        if n == 0 || self.buffer.is_empty() {
124            return None;
125        }
126        let mut max_val = f64::NEG_INFINITY;
127        let mut max_idx = None;
128
129        for (i, &val) in self.buffer.iter().take(n).enumerate() {
130            if val.is_finite() && val > max_val {
131                max_val = val;
132                max_idx = Some(i);
133            }
134        }
135        max_idx
136    }
137
138    /// Returns the offset (0..n-1) of the lowest value in the last `n` bars.
139    pub fn lowestbars(&self, n: usize) -> Option<usize> {
140        if n == 0 || self.buffer.is_empty() {
141            return None;
142        }
143        let mut min_val = f64::INFINITY;
144        let mut min_idx = None;
145
146        for (i, &val) in self.buffer.iter().take(n).enumerate() {
147            if val.is_finite() && val < min_val {
148                min_val = val;
149                min_idx = Some(i);
150            }
151        }
152        min_idx
153    }
154}
155
156/// Helper functions for time series calculations and crossovers.
157pub struct SeriesEvents;
158
159impl SeriesEvents {
160    /// Returns difference `series[0] - series[1]`.
161    pub fn change(series: &Series<f64>) -> Option<f64> {
162        if let (Some(&curr), Some(&prev)) = (series.get(0), series.get(1)) {
163            Some(curr - prev)
164        } else {
165            None
166        }
167    }
168
169    /// Returns true if the series has been strictly increasing over the last `length` bars.
170    pub fn rising(series: &Series<f64>, length: usize) -> bool {
171        if length == 0 || series.len() <= length {
172            return false;
173        }
174        for i in 0..length {
175            if let (Some(&curr), Some(&prev)) = (series.get(i), series.get(i + 1)) {
176                if curr <= prev {
177                    return false;
178                }
179            } else {
180                return false;
181            }
182        }
183        true
184    }
185
186    /// Returns true if the series has been strictly decreasing over the last `length` bars.
187    pub fn falling(series: &Series<f64>, length: usize) -> bool {
188        if length == 0 || series.len() <= length {
189            return false;
190        }
191        for i in 0..length {
192            if let (Some(&curr), Some(&prev)) = (series.get(i), series.get(i + 1)) {
193                if curr >= prev {
194                    return false;
195                }
196            } else {
197                return false;
198            }
199        }
200        true
201    }
202
203    /// Returns true if series `a` crossed series `b` in either direction on the latest bar.
204    pub fn cross(a: &Series<f64>, b: &Series<f64>) -> bool {
205        Self::crossover(a, b) || Self::crossunder(a, b)
206    }
207
208    /// Returns true if series `a` crossed above series `b` (`a[1] <= b[1]` and `a[0] > b[0]`).
209    pub fn crossover(a: &Series<f64>, b: &Series<f64>) -> bool {
210        if let (Some(&a0), Some(&a1), Some(&b0), Some(&b1)) =
211            (a.get(0), a.get(1), b.get(0), b.get(1))
212        {
213            a1 <= b1 && a0 > b0
214        } else {
215            false
216        }
217    }
218
219    /// Returns true if series `a` crossed below series `b` (`a[1] >= b[1]` and `a[0] < b[0]`).
220    pub fn crossunder(a: &Series<f64>, b: &Series<f64>) -> bool {
221        if let (Some(&a0), Some(&a1), Some(&b0), Some(&b1)) =
222            (a.get(0), a.get(1), b.get(0), b.get(1))
223        {
224            a1 >= b1 && a0 < b0
225        } else {
226            false
227        }
228    }
229
230    /// Returns the sum of all finite values in `series`.
231    pub fn cum(series: &Series<f64>) -> f64 {
232        (0..series.len())
233            .filter_map(|i| series.get(i).copied())
234            .filter(|v| v.is_finite())
235            .sum()
236    }
237
238    /// Returns the source value at the requested occurrence of a separate condition series.
239    pub fn value_when<'a, T>(
240        condition: &Series<bool>,
241        source: &'a Series<T>,
242        occurrence: usize,
243    ) -> Option<&'a T> {
244        let mut seen = 0;
245        for offset in 0..condition.len().min(source.len()) {
246            if condition.get(offset) == Some(&true) {
247                if seen == occurrence {
248                    return source.get(offset);
249                }
250                seen += 1;
251            }
252        }
253        None
254    }
255}
256
257/// Unbounded streaming cumulative sum, independent of a `Series` lookback capacity.
258#[derive(Debug, Clone, Copy, PartialEq, Default)]
259pub struct CumulativeSum {
260    value: f64,
261}
262
263impl CumulativeSum {
264    pub fn update(&mut self, value: f64) -> f64 {
265        if value.is_finite() {
266            self.value += value;
267        }
268        self.value
269    }
270
271    pub fn value(&self) -> f64 {
272        self.value
273    }
274
275    pub fn reset(&mut self) {
276        self.value = 0.0;
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_series_basic_lookback() {
286        let mut s = Series::new(5);
287        s.push(10.0);
288        s.push(20.0);
289        s.push(30.0);
290
291        assert_eq!(s.get(0), Some(&30.0));
292        assert_eq!(s.get(1), Some(&20.0));
293        assert_eq!(s.get(2), Some(&10.0));
294        assert_eq!(s.get(3), None);
295
296        assert_eq!(s.highest(3), Some(30.0));
297        assert_eq!(s.lowest(3), Some(10.0));
298        assert_eq!(s.highestbars(3), Some(0));
299        assert_eq!(s.lowestbars(3), Some(2));
300    }
301
302    #[test]
303    fn test_series_events_crossover() {
304        let mut a = Series::new(5);
305        let mut b = Series::new(5);
306
307        a.push(10.0);
308        b.push(15.0);
309
310        a.push(20.0);
311        b.push(15.0);
312
313        assert!(SeriesEvents::crossover(&a, &b));
314        assert!(!SeriesEvents::crossunder(&a, &b));
315    }
316
317    #[test]
318    fn value_when_uses_a_separate_condition_series() {
319        let mut condition = Series::new(4);
320        let mut source = Series::new(4);
321        for (matches, value) in [(true, 10), (false, 20), (true, 30)] {
322            condition.push(matches);
323            source.push(value);
324        }
325        assert_eq!(SeriesEvents::value_when(&condition, &source, 0), Some(&30));
326        assert_eq!(SeriesEvents::value_when(&condition, &source, 1), Some(&10));
327    }
328
329    #[test]
330    fn cumulative_sum_survives_a_small_lookback_capacity() {
331        let mut cumulative = CumulativeSum::default();
332        assert_eq!(cumulative.update(1.0), 1.0);
333        assert_eq!(cumulative.update(2.0), 3.0);
334        assert_eq!(cumulative.update(3.0), 6.0);
335    }
336}