1use crate::interbar_types::{InterBarFeatures, TradeSnapshot};
27use foldhash::fast::FixedState;
28use std::hash::{BuildHasher, Hash, Hasher};
29
30pub const INTERBAR_FEATURE_CACHE_CAPACITY: u64 = 256;
33
34fn hash_trade_window(lookback: &[&TradeSnapshot]) -> u64 {
39 if lookback.len() < 2 {
43 return lookback.len() as u64; }
45
46 let mut hasher = FixedState::default().build_hasher();
47
48 lookback.len().hash(&mut hasher);
50
51 let mut min_price = i64::MAX;
55 let mut max_price = i64::MIN;
56 let mut total_volume: u64 = 0;
60 let mut buy_count = 0usize;
61
62 for trade in lookback {
63 min_price = min_price.min(trade.price.0);
64 max_price = max_price.max(trade.price.0);
65 total_volume = total_volume.wrapping_add(trade.volume.0 as u64);
66 buy_count += (!trade.is_buyer_maker) as usize;
69 }
70
71 let price_range = (max_price - min_price) / 100;
75 price_range.hash(&mut hasher);
76
77 let avg_volume = if !lookback.is_empty() {
79 total_volume / lookback.len() as u64
80 } else {
81 0
82 };
83 avg_volume.hash(&mut hasher);
84
85 ((buy_count * 100 / lookback.len()) as u8).hash(&mut hasher);
87
88 hasher.finish()
89}
90
91#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
93pub struct InterBarCacheKey {
94 pub trade_count: usize,
96 pub window_hash: u64,
98}
99
100impl InterBarCacheKey {
101 pub fn from_lookback(lookback: &[&TradeSnapshot]) -> Self {
103 Self {
104 trade_count: lookback.len(),
105 window_hash: hash_trade_window(lookback),
106 }
107 }
108}
109
110#[derive(Debug)]
117pub struct InterBarFeatureCache {
118 cache: quick_cache::sync::Cache<InterBarCacheKey, InterBarFeatures>,
120}
121
122impl InterBarFeatureCache {
123 pub fn new() -> Self {
125 Self::with_capacity(INTERBAR_FEATURE_CACHE_CAPACITY)
126 }
127
128 pub fn with_capacity(capacity: u64) -> Self {
130 let cache = quick_cache::sync::Cache::new(capacity as usize);
131 Self { cache }
132 }
133
134 pub fn get(&self, key: &InterBarCacheKey) -> Option<InterBarFeatures> {
136 self.cache.get(key)
137 }
138
139 pub fn insert(&self, key: InterBarCacheKey, features: InterBarFeatures) {
141 self.cache.insert(key, features);
142 }
143
144 pub fn clear(&self) {
146 self.cache.clear();
147 }
148
149 pub fn stats(&self) -> (u64, u64) {
151 (self.cache.len() as u64, INTERBAR_FEATURE_CACHE_CAPACITY)
152 }
153}
154
155impl Default for InterBarFeatureCache {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::fixed_point::FixedPoint;
165
166 fn create_test_trade(price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
167 TradeSnapshot {
168 timestamp: 1000000,
169 price: FixedPoint((price * 1e8) as i64),
170 volume: FixedPoint((volume * 1e8) as i64),
171 is_buyer_maker,
172 turnover: (price * volume) as i128,
173 }
174 }
175
176 #[test]
177 fn test_cache_key_from_lookback() {
178 let trades = vec![
179 create_test_trade(100.0, 1.0, false),
180 create_test_trade(100.5, 1.5, true),
181 create_test_trade(100.2, 1.2, false),
182 ];
183 let refs: Vec<_> = trades.iter().collect();
184
185 let key = InterBarCacheKey::from_lookback(&refs);
186 assert_eq!(key.trade_count, 3);
187 assert!(key.window_hash > 0, "Window hash should be non-zero");
188 }
189
190 #[test]
191 fn test_cache_insert_and_retrieve() {
192 let cache = InterBarFeatureCache::new();
193 let key = InterBarCacheKey {
194 trade_count: 10,
195 window_hash: 12345,
196 };
197
198 let features = InterBarFeatures::default();
199 cache.insert(key, features.clone());
200
201 let retrieved = cache.get(&key);
202 assert!(retrieved.is_some());
203 }
204
205 #[test]
206 fn test_cache_miss() {
207 let cache = InterBarFeatureCache::new();
208 let key = InterBarCacheKey {
209 trade_count: 10,
210 window_hash: 12345,
211 };
212
213 let result = cache.get(&key);
214 assert!(result.is_none());
215 }
216
217 #[test]
218 fn test_cache_clear() {
219 let cache = InterBarFeatureCache::new();
220 let key = InterBarCacheKey {
221 trade_count: 10,
222 window_hash: 12345,
223 };
224
225 cache.insert(key, InterBarFeatures::default());
226 assert!(cache.get(&key).is_some());
227
228 cache.clear();
229 assert!(cache.get(&key).is_none());
230 }
231
232 #[test]
233 fn test_identical_trades_same_hash() {
234 let trade = create_test_trade(100.0, 1.0, false);
235 let trades = vec![trade.clone(), trade.clone(), trade];
236 let refs: Vec<_> = trades.iter().collect();
237
238 let key1 = InterBarCacheKey::from_lookback(&refs);
239
240 let trades2 = vec![
241 create_test_trade(100.0, 1.0, false),
242 create_test_trade(100.0, 1.0, false),
243 create_test_trade(100.0, 1.0, false),
244 ];
245 let refs2: Vec<_> = trades2.iter().collect();
246 let key2 = InterBarCacheKey::from_lookback(&refs2);
247
248 assert_eq!(key1, key2);
250 }
251
252 #[test]
253 fn test_similar_trades_same_hash() {
254 let trades1 = vec![
255 create_test_trade(100.0, 1.0, false),
256 create_test_trade(100.5, 1.5, true),
257 create_test_trade(100.2, 1.2, false),
258 ];
259 let refs1: Vec<_> = trades1.iter().collect();
260 let key1 = InterBarCacheKey::from_lookback(&refs1);
261
262 let trades2 = vec![
264 create_test_trade(100.01, 1.0, false),
265 create_test_trade(100.51, 1.5, true),
266 create_test_trade(100.21, 1.2, false),
267 ];
268 let refs2: Vec<_> = trades2.iter().collect();
269 let key2 = InterBarCacheKey::from_lookback(&refs2);
270
271 assert_eq!(key1.trade_count, key2.trade_count);
273 }
274
275 #[test]
276 fn test_cache_eviction_beyond_capacity() {
277 let capacity = 16u64;
278 let cache = InterBarFeatureCache::with_capacity(capacity);
279
280 let total = (capacity * 4) as usize;
282 for i in 0..total {
283 let key = InterBarCacheKey {
284 trade_count: i,
285 window_hash: i as u64 * 7919, };
287 cache.insert(key, InterBarFeatures::default());
288 }
289
290 let (count, _) = cache.stats();
292 assert!(
293 count <= capacity,
294 "cache count ({count}) should not exceed capacity ({capacity})"
295 );
296 assert!(count > 0, "cache should not be empty after inserts");
297 }
298
299 #[test]
302 fn test_hash_early_exit_empty_window() {
303 let refs: Vec<&TradeSnapshot> = vec![];
304 let key = InterBarCacheKey::from_lookback(&refs);
305 assert_eq!(key.trade_count, 0);
306 assert_eq!(key.window_hash, 0);
308 }
309
310 #[test]
311 fn test_hash_early_exit_single_trade() {
312 let trade = create_test_trade(100.0, 1.0, false);
313 let refs: Vec<_> = vec![&trade];
314 let key = InterBarCacheKey::from_lookback(&refs);
315 assert_eq!(key.trade_count, 1);
316 assert_eq!(key.window_hash, 1);
318 }
319
320 #[test]
321 fn test_hash_two_trades_not_sentinel() {
322 let t1 = create_test_trade(100.0, 1.0, false);
323 let t2 = create_test_trade(101.0, 2.0, true);
324 let refs: Vec<_> = vec![&t1, &t2];
325 let key = InterBarCacheKey::from_lookback(&refs);
326 assert_eq!(key.trade_count, 2);
327 assert!(
329 key.window_hash > 1,
330 "2-trade window should compute hash, not sentinel"
331 );
332 }
333
334 #[test]
335 fn test_hash_all_buyers_vs_all_sellers() {
336 let buyers = vec![
338 create_test_trade(100.0, 1.0, false),
339 create_test_trade(101.0, 1.0, false),
340 create_test_trade(100.5, 1.0, false),
341 ];
342 let buyer_refs: Vec<_> = buyers.iter().collect();
343 let key_buyers = InterBarCacheKey::from_lookback(&buyer_refs);
344
345 let sellers = vec![
347 create_test_trade(100.0, 1.0, true),
348 create_test_trade(101.0, 1.0, true),
349 create_test_trade(100.5, 1.0, true),
350 ];
351 let seller_refs: Vec<_> = sellers.iter().collect();
352 let key_sellers = InterBarCacheKey::from_lookback(&seller_refs);
353
354 assert_eq!(key_buyers.trade_count, key_sellers.trade_count);
356 assert_ne!(
357 key_buyers.window_hash, key_sellers.window_hash,
358 "All-buyer and all-seller windows should produce different hashes"
359 );
360 }
361
362 #[test]
363 fn test_hash_different_price_ranges() {
364 let tight = vec![
366 create_test_trade(100.0, 1.0, false),
367 create_test_trade(100.5, 1.0, true),
368 ];
369 let tight_refs: Vec<_> = tight.iter().collect();
370 let key_tight = InterBarCacheKey::from_lookback(&tight_refs);
371
372 let wide = vec![
374 create_test_trade(100.0, 1.0, false),
375 create_test_trade(110.0, 1.0, true),
376 ];
377 let wide_refs: Vec<_> = wide.iter().collect();
378 let key_wide = InterBarCacheKey::from_lookback(&wide_refs);
379
380 assert_ne!(
381 key_tight.window_hash, key_wide.window_hash,
382 "Different price ranges should produce different hashes"
383 );
384 }
385
386 #[test]
387 fn test_feature_value_round_trip() {
388 let cache = InterBarFeatureCache::new();
389 let key = InterBarCacheKey {
390 trade_count: 50,
391 window_hash: 99999,
392 };
393
394 let mut features = InterBarFeatures::default();
395 features.lookback_ofi = Some(0.75);
396 features.lookback_trade_count = Some(50);
397 features.lookback_intensity = Some(123.456);
398
399 cache.insert(key, features);
400 let retrieved = cache.get(&key).expect("should hit cache");
401
402 assert_eq!(retrieved.lookback_ofi, Some(0.75));
403 assert_eq!(retrieved.lookback_trade_count, Some(50));
404 assert_eq!(retrieved.lookback_intensity, Some(123.456));
405 }
406}