Skip to main content

finance_solution/stocks/ta/
ring.rs

1//! Fixed-capacity ring buffer for incremental TA windows (private helper).
2//!
3//! Also: monotonic sliding max/min ([`SlidingMax`] / [`SlidingMin`]) for O(1)
4//! amortized window extrema (Stoch HH/LL, Donchian).
5
6use std::collections::VecDeque;
7
8/// Ring of `f64` with optional running sum (SMA / RVOL / rolling VWAP).
9#[derive(Clone, Debug)]
10pub(crate) struct RingF64 {
11    buf: Vec<f64>,
12    /// Next write index.
13    head: usize,
14    /// Number of valid elements ≤ capacity.
15    len: usize,
16    sum: f64,
17}
18
19impl RingF64 {
20    pub(crate) fn with_capacity(cap: usize) -> Self {
21        debug_assert!(cap >= 1);
22        Self {
23            buf: vec![0.0; cap],
24            head: 0,
25            len: 0,
26            sum: 0.0,
27        }
28    }
29
30    pub(crate) fn capacity(&self) -> usize {
31        self.buf.len()
32    }
33
34    pub(crate) fn len(&self) -> usize {
35        self.len
36    }
37
38    pub(crate) fn is_full(&self) -> bool {
39        self.len == self.buf.len()
40    }
41
42    pub(crate) fn sum(&self) -> f64 {
43        self.sum
44    }
45
46    /// Oldest sample still in the ring (`None` if empty).
47    ///
48    /// When full, this is the value that the next [`push`](Self::push) will evict.
49    pub(crate) fn oldest(&self) -> Option<f64> {
50        if self.len == 0 {
51            return None;
52        }
53        let cap = self.buf.len();
54        let start = if self.len < cap { 0 } else { self.head };
55        Some(self.buf[start])
56    }
57
58    pub(crate) fn clear(&mut self) {
59        self.head = 0;
60        self.len = 0;
61        self.sum = 0.0;
62    }
63
64    /// Push value; if full, overwrites oldest and adjusts sum.
65    ///
66    /// Returns the **evicted** oldest value when the ring was already full.
67    pub(crate) fn push(&mut self, value: f64) -> Option<f64> {
68        let cap = self.buf.len();
69        if self.len < cap {
70            self.buf[self.head] = value;
71            self.sum += value;
72            self.head = (self.head + 1) % cap;
73            self.len += 1;
74            None
75        } else {
76            let old = self.buf[self.head];
77            self.sum += value - old;
78            self.buf[self.head] = value;
79            self.head = (self.head + 1) % cap;
80            Some(old)
81        }
82    }
83
84    /// Logical order oldest → newest into `out` (cleared first).
85    pub(crate) fn copy_ordered(&self, out: &mut Vec<f64>) {
86        out.clear();
87        if self.len == 0 {
88            return;
89        }
90        let cap = self.buf.len();
91        let start = if self.len < cap { 0 } else { self.head };
92        for i in 0..self.len {
93            out.push(self.buf[(start + i) % cap]);
94        }
95    }
96
97    pub(crate) fn mean(&self) -> Option<f64> {
98        if self.len == 0 {
99            None
100        } else {
101            Some(self.sum / self.len as f64)
102        }
103    }
104
105    pub(crate) fn max(&self) -> Option<f64> {
106        if self.len == 0 {
107            return None;
108        }
109        let cap = self.buf.len();
110        let start = if self.len < cap { 0 } else { self.head };
111        let mut m = f64::NEG_INFINITY;
112        for i in 0..self.len {
113            m = m.max(self.buf[(start + i) % cap]);
114        }
115        Some(m)
116    }
117
118    pub(crate) fn min(&self) -> Option<f64> {
119        if self.len == 0 {
120            return None;
121        }
122        let cap = self.buf.len();
123        let start = if self.len < cap { 0 } else { self.head };
124        let mut m = f64::INFINITY;
125        for i in 0..self.len {
126            m = m.min(self.buf[(start + i) % cap]);
127        }
128        Some(m)
129    }
130}
131
132/// Ring of (price*volume, volume) pairs for rolling VWAP.
133#[derive(Clone, Debug)]
134pub(crate) struct RingPv {
135    pv: RingF64,
136    vol: RingF64,
137}
138
139impl RingPv {
140    pub(crate) fn with_capacity(cap: usize) -> Self {
141        Self {
142            pv: RingF64::with_capacity(cap),
143            vol: RingF64::with_capacity(cap),
144        }
145    }
146
147    pub(crate) fn clear(&mut self) {
148        self.pv.clear();
149        self.vol.clear();
150    }
151
152    pub(crate) fn is_full(&self) -> bool {
153        self.pv.is_full()
154    }
155
156    pub(crate) fn len(&self) -> usize {
157        self.pv.len()
158    }
159
160    pub(crate) fn push(&mut self, price: f64, volume: f64) {
161        let _ = self.pv.push(price * volume);
162        let _ = self.vol.push(volume);
163    }
164
165    pub(crate) fn vwap(&self) -> Option<f64> {
166        let v = self.vol.sum();
167        if v > 0.0 {
168            Some(self.pv.sum() / v)
169        } else {
170            None
171        }
172    }
173}
174
175// ---------------------------------------------------------------------------
176// Sliding window max / min (monotonic deques) — amortized O(1) per push
177// ---------------------------------------------------------------------------
178
179/// Sliding-window maximum over the last `window` samples.
180///
181/// Classic mono-decreasing deque of (sequence id, value). Each `push` is
182/// amortized O(1); `max()` is O(1).
183#[derive(Clone, Debug)]
184pub(crate) struct SlidingMax {
185    /// Decreasing values (front = max). Equal values keep the newest index.
186    dq: VecDeque<(u64, f64)>,
187    next_id: u64,
188    window: usize,
189    /// Samples currently in the logical window (≤ `window`).
190    count: usize,
191}
192
193impl SlidingMax {
194    pub(crate) fn with_window(window: usize) -> Self {
195        debug_assert!(window >= 1);
196        Self {
197            dq: VecDeque::with_capacity(window),
198            next_id: 0,
199            window,
200            count: 0,
201        }
202    }
203
204    pub(crate) fn clear(&mut self) {
205        self.dq.clear();
206        self.next_id = 0;
207        self.count = 0;
208    }
209
210    pub(crate) fn is_full(&self) -> bool {
211        self.count == self.window
212    }
213
214    pub(crate) fn max(&self) -> Option<f64> {
215        self.dq.front().map(|(_, v)| *v)
216    }
217
218    /// Push a sample; when the window was full, the oldest sample expires first.
219    ///
220    /// Returns the current window max (always `Some` after the first push).
221    pub(crate) fn push(&mut self, value: f64) -> Option<f64> {
222        if self.count == self.window {
223            let drop_id = self.next_id - self.window as u64;
224            if let Some(&(id, _)) = self.dq.front() {
225                if id == drop_id {
226                    self.dq.pop_front();
227                }
228            }
229        } else {
230            self.count += 1;
231        }
232        while let Some(&(_, back_v)) = self.dq.back() {
233            if back_v <= value {
234                self.dq.pop_back();
235            } else {
236                break;
237            }
238        }
239        self.dq.push_back((self.next_id, value));
240        self.next_id += 1;
241        self.max()
242    }
243}
244
245/// Sliding-window minimum over the last `window` samples (mono-increasing deque).
246#[derive(Clone, Debug)]
247pub(crate) struct SlidingMin {
248    dq: VecDeque<(u64, f64)>,
249    next_id: u64,
250    window: usize,
251    count: usize,
252}
253
254impl SlidingMin {
255    pub(crate) fn with_window(window: usize) -> Self {
256        debug_assert!(window >= 1);
257        Self {
258            dq: VecDeque::with_capacity(window),
259            next_id: 0,
260            window,
261            count: 0,
262        }
263    }
264
265    pub(crate) fn clear(&mut self) {
266        self.dq.clear();
267        self.next_id = 0;
268        self.count = 0;
269    }
270
271    pub(crate) fn is_full(&self) -> bool {
272        self.count == self.window
273    }
274
275    pub(crate) fn min(&self) -> Option<f64> {
276        self.dq.front().map(|(_, v)| *v)
277    }
278
279    pub(crate) fn push(&mut self, value: f64) -> Option<f64> {
280        if self.count == self.window {
281            let drop_id = self.next_id - self.window as u64;
282            if let Some(&(id, _)) = self.dq.front() {
283                if id == drop_id {
284                    self.dq.pop_front();
285                }
286            }
287        } else {
288            self.count += 1;
289        }
290        while let Some(&(_, back_v)) = self.dq.back() {
291            if back_v >= value {
292                self.dq.pop_back();
293            } else {
294                break;
295            }
296        }
297        self.dq.push_back((self.next_id, value));
298        self.next_id += 1;
299        self.min()
300    }
301}
302
303#[cfg(test)]
304mod sliding_tests {
305    use super::*;
306
307    #[test]
308    fn sliding_max_matches_scan() {
309        let data = [1.0, 3.0, 2.0, 5.0, 4.0, 0.0, 6.0, 1.0];
310        let w = 3usize;
311        let mut sm = SlidingMax::with_window(w);
312        for (i, &v) in data.iter().enumerate() {
313            let got = sm.push(v).unwrap();
314            let start = i.saturating_sub(w - 1);
315            let exp = data[start..=i]
316                .iter()
317                .cloned()
318                .fold(f64::NEG_INFINITY, f64::max);
319            assert!((got - exp).abs() < 1e-15, "i={i} got={got} exp={exp}");
320            assert_eq!(sm.is_full(), i + 1 >= w);
321        }
322    }
323
324    #[test]
325    fn sliding_min_matches_scan() {
326        let data = [4.0, 2.0, 3.0, 1.0, 5.0, 0.5, 2.0];
327        let w = 4usize;
328        let mut sm = SlidingMin::with_window(w);
329        for (i, &v) in data.iter().enumerate() {
330            let got = sm.push(v).unwrap();
331            let start = i.saturating_sub(w - 1);
332            let exp = data[start..=i]
333                .iter()
334                .cloned()
335                .fold(f64::INFINITY, f64::min);
336            assert!((got - exp).abs() < 1e-15, "i={i} got={got} exp={exp}");
337        }
338    }
339}