Skip to main content

pine_core/
series_buffer.rs

1//! A rolling window of a series' past values, for stateful builtins.
2//!
3//! A builtin declares one as a `#[state]` field, so the buffer is owned by the
4//! call site and survives across bars.
5
6use std::collections::VecDeque;
7
8/// The most values any single call site retains. Matches the lookback Pine
9/// allows, and bounds memory when a script asks for an absurd length.
10pub const MAX_LOOKBACK: usize = 5000;
11
12/// A capped window of past values, newest first.
13#[derive(Debug, Clone)]
14pub struct SeriesBuffer<T> {
15    /// Newest first: `values[0]` is the current bar.
16    values: VecDeque<T>,
17}
18
19impl<T> Default for SeriesBuffer<T> {
20    fn default() -> Self {
21        Self {
22            values: VecDeque::new(),
23        }
24    }
25}
26
27impl<T> SeriesBuffer<T> {
28    /// Record this bar's value, retaining at most `capacity` of them.
29    pub fn push(&mut self, value: T, capacity: usize) {
30        self.values.push_front(value);
31        let capacity = capacity.clamp(1, MAX_LOOKBACK);
32        while self.values.len() > capacity {
33            self.values.pop_back();
34        }
35    }
36
37    /// The value `offset` bars back, or `None` if the buffer has not seen that
38    /// many bars yet. `offset` 0 is the current bar.
39    pub fn get(&self, offset: usize) -> Option<&T> {
40        self.values.get(offset)
41    }
42
43    /// How many bars are retained.
44    pub fn len(&self) -> usize {
45        self.values.len()
46    }
47
48    pub fn is_empty(&self) -> bool {
49        self.values.is_empty()
50    }
51
52    /// The retained values, newest first.
53    pub fn iter(&self) -> impl Iterator<Item = &T> {
54        self.values.iter()
55    }
56}
57
58impl SeriesBuffer<f64> {
59    /// The newest `n` values, newest first. Shorter than `n` while warming up.
60    pub fn window(&self, n: usize) -> Vec<f64> {
61        self.values.iter().take(n).copied().collect()
62    }
63
64    /// Record this bar's value and return the `length` values to compute over,
65    /// newest first — or `None` while fewer than `length` bars have been seen.
66    ///
67    /// Pine yields na until a series has enough history to answer, so warming
68    /// up is the buffer's business rather than each builtin's.
69    pub fn observe(&mut self, value: f64, length: usize) -> Option<Vec<f64>> {
70        self.push(value, length);
71        (self.len() >= length).then(|| self.window(length))
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn keeps_newest_values_up_to_capacity() {
81        let mut buf = SeriesBuffer::default();
82        for value in [1.0, 2.0, 3.0, 4.0] {
83            buf.push(value, 3);
84        }
85
86        assert_eq!(buf.len(), 3);
87        assert_eq!(buf.window(3), vec![4.0, 3.0, 2.0]);
88        assert_eq!(buf.get(0), Some(&4.0));
89        assert_eq!(buf.get(2), Some(&2.0));
90        assert_eq!(buf.get(3), None);
91    }
92
93    #[test]
94    fn window_is_short_while_warming_up() {
95        let mut buf = SeriesBuffer::default();
96        buf.push(1.0, 5);
97        buf.push(2.0, 5);
98
99        assert_eq!(buf.window(5), vec![2.0, 1.0]);
100    }
101
102    #[test]
103    fn observe_withholds_a_window_until_it_fills() {
104        let mut buf = SeriesBuffer::default();
105
106        assert_eq!(buf.observe(1.0, 3), None);
107        assert_eq!(buf.observe(2.0, 3), None);
108        assert_eq!(buf.observe(3.0, 3), Some(vec![3.0, 2.0, 1.0]));
109        assert_eq!(buf.observe(4.0, 3), Some(vec![4.0, 3.0, 2.0]));
110    }
111
112    #[test]
113    fn capacity_is_bounded_by_max_lookback() {
114        let mut buf = SeriesBuffer::default();
115        for value in 0..(MAX_LOOKBACK + 10) {
116            buf.push(value as f64, usize::MAX);
117        }
118
119        assert_eq!(buf.len(), MAX_LOOKBACK);
120    }
121}