hyper_agent_core/
candle_buffer.rs1use hyper_ta::Candle;
2use std::collections::VecDeque;
3
4const DEFAULT_CAPACITY: usize = 200;
5
6pub struct CandleBuffer {
7 candles: VecDeque<Candle>,
8 capacity: usize,
9 symbol: String,
10}
11
12impl CandleBuffer {
13 pub fn new(symbol: String) -> Self {
14 Self::with_capacity(symbol, DEFAULT_CAPACITY)
15 }
16
17 pub fn with_capacity(symbol: String, capacity: usize) -> Self {
18 Self {
19 candles: VecDeque::with_capacity(capacity),
20 capacity,
21 symbol,
22 }
23 }
24
25 pub fn symbol(&self) -> &str {
26 &self.symbol
27 }
28
29 pub fn len(&self) -> usize {
30 self.candles.len()
31 }
32
33 pub fn is_empty(&self) -> bool {
34 self.candles.is_empty()
35 }
36
37 pub fn push(&mut self, candle: Candle) {
39 if self.candles.len() >= self.capacity {
40 self.candles.pop_front();
41 }
42 self.candles.push_back(candle);
43 }
44
45 pub fn last(&self) -> Option<&Candle> {
47 self.candles.back()
48 }
49
50 pub fn as_slice(&self) -> Vec<Candle> {
52 self.candles.iter().cloned().collect()
53 }
54
55 pub fn backfill(&mut self, candles: Vec<Candle>) {
59 let existing_times: std::collections::HashSet<u64> =
61 self.candles.iter().map(|c| c.time).collect();
62
63 let mut new_candles: Vec<Candle> = candles
64 .into_iter()
65 .filter(|c| !existing_times.contains(&c.time))
66 .collect();
67
68 new_candles.sort_by_key(|c| c.time);
70
71 let mut all: Vec<Candle> = self.candles.drain(..).collect();
73 all.extend(new_candles);
74 all.sort_by_key(|c| c.time);
75
76 if all.len() > self.capacity {
78 all = all.split_off(all.len() - self.capacity);
79 }
80
81 self.candles = all.into_iter().collect();
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 fn make_candle(time: u64, close: f64) -> Candle {
90 Candle {
91 time,
92 open: close - 1.0,
93 high: close + 1.0,
94 low: close - 2.0,
95 close,
96 volume: 100.0,
97 }
98 }
99
100 #[test]
101 fn push_and_len() {
102 let mut buf = CandleBuffer::new("BTC-PERP".into());
103 assert!(buf.is_empty());
104 buf.push(make_candle(1000, 50000.0));
105 assert_eq!(buf.len(), 1);
106 assert_eq!(buf.symbol(), "BTC-PERP");
107 }
108
109 #[test]
110 fn capacity_eviction() {
111 let mut buf = CandleBuffer::with_capacity("ETH".into(), 3);
112 buf.push(make_candle(1, 100.0));
113 buf.push(make_candle(2, 200.0));
114 buf.push(make_candle(3, 300.0));
115 buf.push(make_candle(4, 400.0));
116 assert_eq!(buf.len(), 3);
117 assert_eq!(buf.as_slice()[0].time, 2); assert_eq!(buf.last().unwrap().time, 4);
119 }
120
121 #[test]
122 fn backfill_dedup() {
123 let mut buf = CandleBuffer::with_capacity("BTC".into(), 5);
124 buf.push(make_candle(3, 300.0));
125 buf.push(make_candle(5, 500.0));
126
127 buf.backfill(vec![
129 make_candle(1, 100.0),
130 make_candle(3, 300.0), make_candle(4, 400.0),
132 ]);
133
134 assert_eq!(buf.len(), 4); let times: Vec<u64> = buf.as_slice().iter().map(|c| c.time).collect();
136 assert_eq!(times, vec![1, 3, 4, 5]);
137 }
138
139 #[test]
140 fn backfill_respects_capacity() {
141 let mut buf = CandleBuffer::with_capacity("BTC".into(), 3);
142 buf.backfill(vec![
143 make_candle(1, 100.0),
144 make_candle(2, 200.0),
145 make_candle(3, 300.0),
146 make_candle(4, 400.0),
147 make_candle(5, 500.0),
148 ]);
149 assert_eq!(buf.len(), 3);
150 assert_eq!(buf.as_slice()[0].time, 3); }
152
153 #[test]
154 fn last_returns_most_recent() {
155 let mut buf = CandleBuffer::new("SOL".into());
156 assert!(buf.last().is_none());
157 buf.push(make_candle(1, 10.0));
158 buf.push(make_candle(2, 20.0));
159 assert_eq!(buf.last().unwrap().close, 20.0);
160 }
161}