Skip to main content

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