1use crate::fixed_point::FixedPoint;
34use crate::interbar_math::*;
35use crate::types::AggTrade;
36use rayon::join; use smallvec::SmallVec;
38use std::collections::VecDeque;
39use std::sync::LazyLock; pub use crate::interbar_types::{InterBarConfig, InterBarFeatures, LookbackMode, TradeSnapshot};
43
44static ENTROPY_CACHE_WARMUP: LazyLock<()> = LazyLock::new(|| {
47 crate::entropy_cache_global::warm_up_entropy_cache();
48});
49
50#[derive(Debug)]
52pub struct TradeHistory {
53 trades: VecDeque<TradeSnapshot>,
55 config: InterBarConfig,
57 protected_until: Option<i64>,
61 total_pushed: usize,
63 bar_close_indices: VecDeque<usize>,
67 pushes_since_prune_check: usize,
69 max_safe_capacity: usize,
71 entropy_cache: std::sync::Arc<parking_lot::RwLock<crate::interbar_math::EntropyCache>>,
75 feature_result_cache: Option<std::sync::Arc<parking_lot::RwLock<crate::interbar_cache::InterBarFeatureCache>>>,
79 adaptive_prune_batch: usize,
83 prune_stats: (usize, usize), last_binary_search_cache: parking_lot::Mutex<Option<(i64, usize)>>, lookahead_buffer: parking_lot::Mutex<VecDeque<(i64, usize)>>,
96}
97
98#[cold]
101#[inline(never)]
102fn default_interbar_features() -> InterBarFeatures {
103 InterBarFeatures::default()
104}
105
106impl TradeHistory {
107 pub fn new(config: InterBarConfig) -> Self {
112 Self::new_with_cache(config, None)
113 }
114
115 pub fn new_with_cache(
142 config: InterBarConfig,
143 external_cache: Option<std::sync::Arc<parking_lot::RwLock<crate::interbar_math::EntropyCache>>>,
144 ) -> Self {
145 let _ = &*ENTROPY_CACHE_WARMUP;
148
149 let capacity = match &config.lookback_mode {
152 LookbackMode::FixedCount(n) => *n, LookbackMode::FixedWindow(_) => 500, LookbackMode::BarRelative(_) => 1000, };
156 let max_safe_capacity = match &config.lookback_mode {
158 LookbackMode::FixedCount(n) => *n * 2, LookbackMode::FixedWindow(_) => 1500, LookbackMode::BarRelative(_) => 2000, };
162 let bar_capacity = match &config.lookback_mode {
165 LookbackMode::BarRelative(n_bars) => (*n_bars + 1).min(128),
166 _ => 128,
167 };
168
169 let entropy_cache = external_cache.unwrap_or_else(|| {
171 std::sync::Arc::new(parking_lot::RwLock::new(crate::interbar_math::EntropyCache::new()))
172 });
173
174 let feature_result_cache = Some(
176 std::sync::Arc::new(parking_lot::RwLock::new(
177 crate::interbar_cache::InterBarFeatureCache::new()
178 ))
179 );
180
181 let initial_prune_batch = match &config.lookback_mode {
183 LookbackMode::FixedCount(n) => std::cmp::max((*n / 10).max(5), 10),
184 _ => 10,
185 };
186
187 Self {
188 trades: VecDeque::with_capacity(capacity),
189 config,
190 protected_until: None,
191 total_pushed: 0,
192 bar_close_indices: VecDeque::with_capacity(bar_capacity),
193 pushes_since_prune_check: 0,
194 max_safe_capacity,
195 entropy_cache,
196 feature_result_cache,
197 adaptive_prune_batch: initial_prune_batch,
198 prune_stats: (0, 0),
199 last_binary_search_cache: parking_lot::Mutex::new(None), lookahead_buffer: parking_lot::Mutex::new(VecDeque::with_capacity(3)), }
202 }
203
204 pub fn push(&mut self, trade: &AggTrade) {
210 let snapshot = TradeSnapshot::from(trade);
211 self.trades.push_back(snapshot);
212 self.total_pushed += 1;
213 self.pushes_since_prune_check += 1;
214
215 let prune_batch_size = self.adaptive_prune_batch;
218
219 if self.pushes_since_prune_check >= prune_batch_size
221 || self.trades.len() > self.max_safe_capacity * 2
222 {
223 let trades_before = self.trades.len();
224 self.prune_if_needed();
225 let trades_after = self.trades.len();
226 let trades_removed = trades_before.saturating_sub(trades_after);
227
228 self.prune_stats.0 = self.prune_stats.0.saturating_add(trades_removed);
230 self.prune_stats.1 = self.prune_stats.1.saturating_add(1);
231
232 if self.prune_stats.1 > 0 && self.prune_stats.1.is_multiple_of(10) {
234 let avg_removed = self.prune_stats.0 / self.prune_stats.1;
235 let removal_efficiency = if trades_before > 0 {
236 (avg_removed * 100) / (trades_before + avg_removed)
237 } else {
238 0
239 };
240
241 if removal_efficiency < 10 {
243 self.adaptive_prune_batch = std::cmp::min(
244 self.adaptive_prune_batch * 2,
245 self.max_safe_capacity / 4, );
247 } else if removal_efficiency > 30 {
248 self.adaptive_prune_batch = std::cmp::max(
250 self.adaptive_prune_batch / 2,
251 5, );
253 }
254
255 self.prune_stats = (0, 0);
257 }
258
259 self.pushes_since_prune_check = 0;
260 }
261 }
262
263 pub fn on_bar_open(&mut self, bar_open_time: i64) {
269 self.protected_until = Some(bar_open_time);
272 }
273
274 pub fn on_bar_close(&mut self) {
280 if let LookbackMode::BarRelative(n_bars) = &self.config.lookback_mode {
282 self.bar_close_indices.push_back(self.total_pushed);
283 while self.bar_close_indices.len() > *n_bars + 1 {
285 self.bar_close_indices.pop_front();
286 }
287 }
288 }
290
291 fn prune_if_needed(&mut self) {
297 if self.trades.len() > self.max_safe_capacity {
300 self.prune();
301 }
302 }
303
304 fn prune(&mut self) {
312 match &self.config.lookback_mode {
313 LookbackMode::FixedCount(n) => {
314 let max_trades = *n * 2;
316 while self.trades.len() > max_trades {
317 if let Some(front) = self.trades.front() {
319 if let Some(protected) = self.protected_until {
320 if front.timestamp < protected {
321 break;
323 }
324 }
325 }
326 self.trades.pop_front();
327 }
328 }
329 LookbackMode::FixedWindow(window_us) => {
330 let newest_timestamp = self.trades.back().map(|t| t.timestamp).unwrap_or(0);
332 let cutoff = newest_timestamp - window_us;
333
334 while let Some(front) = self.trades.front() {
335 if let Some(protected) = self.protected_until {
337 if front.timestamp < protected {
338 break;
339 }
340 }
341 if front.timestamp < cutoff {
343 self.trades.pop_front();
344 } else {
345 break;
346 }
347 }
348 }
349 LookbackMode::BarRelative(n_bars) => {
350 if self.bar_close_indices.len() <= *n_bars {
365 return;
368 }
369
370 let oldest_boundary = self.bar_close_indices.front().copied().unwrap_or(0);
374 let keep_count = self.total_pushed - oldest_boundary;
375
376 while self.trades.len() > keep_count {
378 self.trades.pop_front();
379 }
380 }
381 }
382 }
383
384 #[inline]
406 pub fn has_lookback_trades(&self, bar_open_time: i64) -> bool {
407 if self.trades.is_empty() {
409 return false;
410 }
411
412 {
414 let cache = self.last_binary_search_cache.lock();
415 if let Some((cached_time, cached_idx)) = *cache {
416 if cached_time == bar_open_time {
417 return cached_idx > 0;
418 }
419 }
420 }
421
422 let idx = self.trades.partition_point(|trade| trade.timestamp < bar_open_time);
425 *self.last_binary_search_cache.lock() = Some((bar_open_time, idx));
426 idx > 0
427 }
428
429 fn compute_search_hint(&self) -> Option<(bool, usize)> {
437 let buffer = self.lookahead_buffer.lock();
438 if buffer.len() < 2 {
439 return None;
440 }
441
442 let prev = buffer[buffer.len() - 2]; let curr = buffer[buffer.len() - 1];
445
446 let ts_delta = curr.0.saturating_sub(prev.0);
447 let idx_delta = (curr.1 as i64) - (prev.1 as i64);
448
449 if ts_delta > 0 && idx_delta != 0 {
451 let should_check_higher = idx_delta > 0;
452 Some((should_check_higher, curr.1))
453 } else {
454 None
455 }
456 }
457
458 pub fn get_lookback_trades(&self, bar_open_time: i64) -> SmallVec<[&TradeSnapshot; 256]> {
459 {
461 let cache = self.last_binary_search_cache.lock();
462 if let Some((cached_time, cached_idx)) = *cache {
463 if cached_time == bar_open_time {
464 let cutoff_idx = cached_idx;
465 drop(cache);
466 let mut result = SmallVec::new();
467 for i in 0..cutoff_idx {
468 result.push(&self.trades[i]);
469 }
470 return result;
471 }
472 }
473 }
474
475 #[inline(always)]
479 fn ts_partition_point(trades: &std::collections::VecDeque<TradeSnapshot>, bar_open_time: i64) -> usize {
480 trades.partition_point(|trade| trade.timestamp < bar_open_time)
481 }
482
483 let cutoff_idx = if let Some((should_check_higher, last_idx)) = self.compute_search_hint() {
484 let check_region_end = if should_check_higher {
485 std::cmp::min(last_idx + (last_idx / 2), self.trades.len())
486 } else {
487 last_idx
488 };
489
490 if check_region_end > 0
492 && check_region_end == self.trades.len()
493 && self.trades[check_region_end - 1].timestamp < bar_open_time
494 {
495 check_region_end
496 } else {
497 ts_partition_point(&self.trades, bar_open_time)
498 }
499 } else {
500 ts_partition_point(&self.trades, bar_open_time)
501 };
502
503 *self.last_binary_search_cache.lock() = Some((bar_open_time, cutoff_idx));
505
506 {
508 let mut buffer = self.lookahead_buffer.lock();
509 buffer.push_back((bar_open_time, cutoff_idx));
510 if buffer.len() > 3 {
511 buffer.pop_front();
512 }
513 }
514
515 let mut result = SmallVec::new();
517 for i in 0..cutoff_idx {
518 result.push(&self.trades[i]);
519 }
520 result
521 }
522
523 pub fn buffer_stats(&self) -> (usize, usize, usize, usize) {
527 (
528 self.trades.len(),
529 self.max_safe_capacity,
530 self.adaptive_prune_batch,
531 self.prune_stats.0, )
533 }
534
535 pub fn compute_features(&self, bar_open_time: i64) -> InterBarFeatures {
549 if self.trades.is_empty() {
555 return default_interbar_features();
556 }
557
558 let lookback = self.get_lookback_trades(bar_open_time);
559
560 if lookback.is_empty() {
561 return default_interbar_features();
562 }
563
564 let cache_key = self.feature_result_cache.as_ref().map(|_| {
567 crate::interbar_cache::InterBarCacheKey::from_lookback(&lookback)
568 });
569 if let (Some(cache), Some(key)) = (&self.feature_result_cache, &cache_key) {
570 if let Some(cache_guard) = cache.try_read() {
571 if let Some(cached_features) = cache_guard.get(key) {
572 return cached_features;
573 }
574 drop(cache_guard);
575 }
576 }
577
578 let mut features = InterBarFeatures::default();
579
580 self.compute_tier1_features(&lookback, &mut features);
582
583 let cache = if self.config.compute_tier2 || self.config.compute_tier3 {
586 Some(crate::interbar_math::extract_lookback_cache(&lookback))
587 } else {
588 None
589 };
590
591 const TIER2_PARALLEL_THRESHOLD_BASE: usize = 80; const TIER3_PARALLEL_THRESHOLD_BASE: usize = 150; static CPU_COUNT: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
604 let cpu_count = *CPU_COUNT.get_or_init(num_cpus::get);
605 let tier2_threshold = if cpu_count == 1 {
606 usize::MAX } else {
608 TIER2_PARALLEL_THRESHOLD_BASE / cpu_count.max(2)
609 };
610
611 let tier3_threshold = if cpu_count == 1 {
612 usize::MAX } else {
614 TIER3_PARALLEL_THRESHOLD_BASE / cpu_count.max(2)
615 };
616
617 let tier2_can_parallelize = self.config.compute_tier2 && lookback.len() >= tier2_threshold;
619 let tier3_can_parallelize = self.config.compute_tier3 && lookback.len() >= tier3_threshold;
620
621 match (tier2_can_parallelize, tier3_can_parallelize) {
622 (true, true) => {
624 let (tier2_features, tier3_features) = join(
625 || self.compute_tier2_features(&lookback, cache.as_ref()),
626 || self.compute_tier3_features(&lookback, cache.as_ref()),
627 );
628 features.merge_tier2(&tier2_features);
629 features.merge_tier3(&tier3_features);
630 }
631 (true, false) => {
633 let tier2_features = self.compute_tier2_features(&lookback, cache.as_ref());
634 features.merge_tier2(&tier2_features);
635 if self.config.compute_tier3 {
636 let tier3_features = self.compute_tier3_features(&lookback, cache.as_ref());
637 features.merge_tier3(&tier3_features);
638 }
639 }
640 (false, true) => {
642 if self.config.compute_tier2 {
643 let tier2_features = self.compute_tier2_features(&lookback, cache.as_ref());
644 features.merge_tier2(&tier2_features);
645 }
646 let tier3_features = self.compute_tier3_features(&lookback, cache.as_ref());
647 features.merge_tier3(&tier3_features);
648 }
649 (false, false) => {
651 if self.config.compute_tier2 {
652 let tier2_features = self.compute_tier2_features(&lookback, cache.as_ref());
653 features.merge_tier2(&tier2_features);
654 }
655 if self.config.compute_tier3 {
656 let tier3_features = self.compute_tier3_features(&lookback, cache.as_ref());
657 features.merge_tier3(&tier3_features);
658 }
659 }
660 }
661
662 if let (Some(cache), Some(key)) = (&self.feature_result_cache, cache_key) {
665 if let Some(cache_guard) = cache.try_write() {
666 cache_guard.insert(key, features);
667 }
668 }
669
670 features
671 }
672
673 #[inline]
676 fn compute_tier1_features(&self, lookback: &[&TradeSnapshot], features: &mut InterBarFeatures) {
677 let n = lookback.len();
678 if n == 0 {
679 return;
680 }
681
682 features.lookback_trade_count = Some(n as u32);
684
685 let mut buy_vol = 0.0_f64;
690 let mut sell_vol = 0.0_f64;
691 let mut buy_count = 0_u32;
692 let mut sell_count = 0_u32;
693 let mut total_turnover = 0_i128;
694 let mut total_volume_fp = 0_i128;
695 let mut low = i64::MAX;
696 let mut high = i64::MIN;
697
698 for t in lookback.iter() {
699 total_turnover += t.turnover;
700 total_volume_fp += t.volume.0 as i128;
701 low = low.min(t.price.0);
702 high = high.max(t.price.0);
703
704 let vol = t.volume.to_f64();
707 let is_seller_mask = t.is_buyer_maker as u32 as f64;
708 sell_vol += vol * is_seller_mask;
709 buy_vol += vol * (1.0 - is_seller_mask);
710
711 let is_seller_count = t.is_buyer_maker as u32;
712 sell_count += is_seller_count;
713 buy_count += 1 - is_seller_count;
714 }
715
716 let total_vol = buy_vol + sell_vol;
717
718 features.lookback_ofi = Some(if total_vol > f64::EPSILON {
720 (buy_vol - sell_vol) / total_vol
721 } else {
722 0.0
723 });
724
725 let total_count = buy_count + sell_count;
727 features.lookback_count_imbalance = Some(if total_count > 0 {
728 (buy_count as f64 - sell_count as f64) / total_count as f64
729 } else {
730 0.0
731 });
732
733 let first_ts = lookback[0].timestamp;
736 let last_ts = lookback[n - 1].timestamp;
737 let duration_us = last_ts - first_ts;
738 features.lookback_duration_us = Some(duration_us);
739
740 let duration_sec = duration_us as f64 * 1e-6;
743 features.lookback_intensity = Some(if duration_sec > f64::EPSILON {
744 n as f64 / duration_sec
745 } else {
746 n as f64 });
748
749 features.lookback_vwap = Some(if total_volume_fp > 0 {
751 let vwap_raw = total_turnover / total_volume_fp;
752 FixedPoint(vwap_raw as i64)
753 } else {
754 FixedPoint(0)
755 });
756
757 let range = (high - low) as f64;
759 let vwap_val = features.lookback_vwap.as_ref().map(|v| v.0).unwrap_or(0);
760 features.lookback_vwap_position = Some(if range > f64::EPSILON {
761 (vwap_val - low) as f64 / range
762 } else {
763 0.5 });
765 }
766
767 #[inline]
775 fn compute_tier2_features(
776 &self,
777 lookback: &[&TradeSnapshot],
778 cache: Option<&crate::interbar_math::LookbackCache>,
779 ) -> InterBarFeatures {
780 let mut features = InterBarFeatures::default();
781 let n = lookback.len();
782
783 let cache_owned;
787 let cache = match cache {
788 Some(c) => c, None => {
790 cache_owned = crate::interbar_math::extract_lookback_cache(lookback);
792 &cache_owned
793 }
794 };
795
796 if n >= 2 {
798 features.lookback_kyle_lambda = Some(compute_kyle_lambda(lookback));
799 }
800
801 if n >= 2 {
803 features.lookback_burstiness = Some(compute_burstiness(lookback));
804 }
805
806 if n >= 3 {
809 let mean_vol = cache.total_volume / n as f64;
810 let (skew, kurt) = crate::interbar_math::compute_volume_moments_with_mean(&cache.volumes, mean_vol);
811 features.lookback_volume_skew = Some(skew);
812 if n >= 4 {
814 features.lookback_volume_kurt = Some(kurt);
815 }
816 }
817
818 if n >= 1 {
821 let range = cache.high - cache.low;
822 features.lookback_price_range = Some(if cache.open > f64::EPSILON {
823 range / cache.open
824 } else {
825 0.0
826 });
827 }
828
829 features
830 }
831
832 #[inline]
844 fn compute_tier3_features(
845 &self,
846 lookback: &[&TradeSnapshot],
847 cache: Option<&crate::interbar_math::LookbackCache>,
848 ) -> InterBarFeatures {
849 let mut features = InterBarFeatures::default();
850 let n = lookback.len();
851
852 let cache_owned;
856 let cache = match cache {
857 Some(c) => c, None => {
859 cache_owned = crate::interbar_math::extract_lookback_cache(lookback);
861 &cache_owned
862 }
863 };
864 let prices = &cache.prices;
866 let (open, high, low, close) = (cache.open, cache.high, cache.low, cache.close);
867
868 if prices.is_empty() || !cache.all_prices_finite {
872 return features; }
874
875 if n >= 2 {
877 features.lookback_kaufman_er = Some(compute_kaufman_er(prices));
878 }
879
880 if n >= 1 {
882 features.lookback_garman_klass_vol = Some(compute_garman_klass_with_ohlc(open, high, low, close));
883 }
884
885 let mut entropy_value: Option<f64> = None;
891 if n >= 60 {
892 let entropy = if let Some(cache) = self.entropy_cache.try_read() {
896 let cache_result = crate::interbar_math::compute_entropy_adaptive_cached_readonly(
898 prices,
899 &cache,
900 );
901
902 if let Some(result) = cache_result {
903 result
905 } else {
906 drop(cache);
908 let mut cache_guard = self.entropy_cache.write();
909 crate::interbar_math::compute_entropy_adaptive_cached(prices, &mut cache_guard)
910 }
911 } else {
912 let mut cache_guard = self.entropy_cache.write();
914 crate::interbar_math::compute_entropy_adaptive_cached(prices, &mut cache_guard)
915 };
916
917 entropy_value = Some(entropy);
918 features.lookback_permutation_entropy = Some(entropy);
919 }
920
921 if n >= 64 {
926 let should_skip_hurst = entropy_value.is_some_and(|e| e > 0.75);
928
929 if should_skip_hurst {
930 features.lookback_hurst = Some(0.5);
933 } else {
934 features.lookback_hurst = Some(compute_hurst_dfa(prices));
936 }
937 }
938
939 features
940 }
941
942 pub fn reset_bar_boundaries(&mut self) {
948 self.bar_close_indices.clear();
949 }
950
951 pub fn clear(&mut self) {
953 self.trades.clear();
954 }
955
956 pub fn len(&self) -> usize {
958 self.trades.len()
959 }
960
961 pub fn is_empty(&self) -> bool {
963 self.trades.is_empty()
964 }
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970
971 fn create_test_snapshot(
973 timestamp: i64,
974 price: f64,
975 volume: f64,
976 is_buyer_maker: bool,
977 ) -> TradeSnapshot {
978 let price_fp = FixedPoint((price * 1e8) as i64);
979 let volume_fp = FixedPoint((volume * 1e8) as i64);
980 TradeSnapshot {
981 timestamp,
982 price: price_fp,
983 volume: volume_fp,
984 is_buyer_maker,
985 turnover: (price_fp.0 as i128) * (volume_fp.0 as i128),
986 }
987 }
988
989 #[test]
992 fn test_ofi_all_buys() {
993 let mut history = TradeHistory::new(InterBarConfig::default());
994
995 for i in 0..10 {
997 let trade = AggTrade {
998 agg_trade_id: i,
999 price: FixedPoint(5000000000000), volume: FixedPoint(100000000), first_trade_id: i,
1002 last_trade_id: i,
1003 timestamp: i * 1000,
1004 is_buyer_maker: false, is_best_match: None,
1006 };
1007 history.push(&trade);
1008 }
1009
1010 let features = history.compute_features(10000);
1011
1012 assert!(
1013 (features.lookback_ofi.unwrap() - 1.0).abs() < f64::EPSILON,
1014 "OFI should be 1.0 for all buys, got {}",
1015 features.lookback_ofi.unwrap()
1016 );
1017 }
1018
1019 #[test]
1020 fn test_ofi_all_sells() {
1021 let mut history = TradeHistory::new(InterBarConfig::default());
1022
1023 for i in 0..10 {
1025 let trade = AggTrade {
1026 agg_trade_id: i,
1027 price: FixedPoint(5000000000000),
1028 volume: FixedPoint(100000000),
1029 first_trade_id: i,
1030 last_trade_id: i,
1031 timestamp: i * 1000,
1032 is_buyer_maker: true, is_best_match: None,
1034 };
1035 history.push(&trade);
1036 }
1037
1038 let features = history.compute_features(10000);
1039
1040 assert!(
1041 (features.lookback_ofi.unwrap() - (-1.0)).abs() < f64::EPSILON,
1042 "OFI should be -1.0 for all sells, got {}",
1043 features.lookback_ofi.unwrap()
1044 );
1045 }
1046
1047 #[test]
1048 fn test_ofi_balanced() {
1049 let mut history = TradeHistory::new(InterBarConfig::default());
1050
1051 for i in 0..10 {
1053 let trade = AggTrade {
1054 agg_trade_id: i,
1055 price: FixedPoint(5000000000000),
1056 volume: FixedPoint(100000000),
1057 first_trade_id: i,
1058 last_trade_id: i,
1059 timestamp: i * 1000,
1060 is_buyer_maker: i % 2 == 0, is_best_match: None,
1062 };
1063 history.push(&trade);
1064 }
1065
1066 let features = history.compute_features(10000);
1067
1068 assert!(
1069 features.lookback_ofi.unwrap().abs() < f64::EPSILON,
1070 "OFI should be 0.0 for balanced volumes, got {}",
1071 features.lookback_ofi.unwrap()
1072 );
1073 }
1074
1075 #[test]
1078 fn test_burstiness_regular_intervals() {
1079 let t0 = create_test_snapshot(0, 100.0, 1.0, false);
1080 let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
1081 let t2 = create_test_snapshot(2000, 100.0, 1.0, false);
1082 let t3 = create_test_snapshot(3000, 100.0, 1.0, false);
1083 let t4 = create_test_snapshot(4000, 100.0, 1.0, false);
1084 let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3, &t4];
1085
1086 let b = compute_burstiness(&lookback);
1087
1088 assert!(
1090 (b - (-1.0)).abs() < 0.01,
1091 "Burstiness should be -1 for regular intervals, got {}",
1092 b
1093 );
1094 }
1095
1096 #[test]
1099 fn test_kaufman_er_perfect_trend() {
1100 let prices = vec![100.0, 101.0, 102.0, 103.0, 104.0];
1101 let er = compute_kaufman_er(&prices);
1102
1103 assert!(
1104 (er - 1.0).abs() < f64::EPSILON,
1105 "Kaufman ER should be 1.0 for perfect trend, got {}",
1106 er
1107 );
1108 }
1109
1110 #[test]
1111 fn test_kaufman_er_round_trip() {
1112 let prices = vec![100.0, 102.0, 104.0, 102.0, 100.0];
1113 let er = compute_kaufman_er(&prices);
1114
1115 assert!(
1116 er.abs() < f64::EPSILON,
1117 "Kaufman ER should be 0.0 for round trip, got {}",
1118 er
1119 );
1120 }
1121
1122 #[test]
1125 fn test_permutation_entropy_monotonic() {
1126 let prices: Vec<f64> = (1..=100).map(|i| i as f64).collect();
1128 let pe = compute_permutation_entropy(&prices);
1129
1130 assert!(
1131 pe.abs() < f64::EPSILON,
1132 "PE should be 0 for monotonic, got {}",
1133 pe
1134 );
1135 }
1136
1137 #[test]
1140 fn test_lookback_excludes_current_bar_trades() {
1141 let mut history = TradeHistory::new(InterBarConfig::default());
1142
1143 for i in 0..4 {
1145 let trade = AggTrade {
1146 agg_trade_id: i,
1147 price: FixedPoint(5000000000000),
1148 volume: FixedPoint(100000000),
1149 first_trade_id: i,
1150 last_trade_id: i,
1151 timestamp: i * 1000,
1152 is_buyer_maker: false,
1153 is_best_match: None,
1154 };
1155 history.push(&trade);
1156 }
1157
1158 let lookback = history.get_lookback_trades(2000);
1160
1161 assert_eq!(lookback.len(), 2, "Should have 2 trades before bar open");
1163
1164 for trade in &lookback {
1165 assert!(
1166 trade.timestamp < 2000,
1167 "Trade at {} should be before bar open at 2000",
1168 trade.timestamp
1169 );
1170 }
1171 }
1172
1173 #[test]
1176 fn test_count_imbalance_bounded() {
1177 let mut history = TradeHistory::new(InterBarConfig::default());
1178
1179 for i in 0..100 {
1181 let trade = AggTrade {
1182 agg_trade_id: i,
1183 price: FixedPoint(5000000000000),
1184 volume: FixedPoint((i % 10 + 1) * 100000000),
1185 first_trade_id: i,
1186 last_trade_id: i,
1187 timestamp: i * 1000,
1188 is_buyer_maker: i % 3 == 0,
1189 is_best_match: None,
1190 };
1191 history.push(&trade);
1192 }
1193
1194 let features = history.compute_features(100000);
1195 let imb = features.lookback_count_imbalance.unwrap();
1196
1197 assert!(
1198 imb >= -1.0 && imb <= 1.0,
1199 "Count imbalance should be in [-1, 1], got {}",
1200 imb
1201 );
1202 }
1203
1204 #[test]
1205 fn test_vwap_position_bounded() {
1206 let mut history = TradeHistory::new(InterBarConfig::default());
1207
1208 for i in 0..20 {
1210 let price = 50000.0 + (i as f64 * 10.0);
1211 let trade = AggTrade {
1212 agg_trade_id: i,
1213 price: FixedPoint((price * 1e8) as i64),
1214 volume: FixedPoint(100000000),
1215 first_trade_id: i,
1216 last_trade_id: i,
1217 timestamp: i * 1000,
1218 is_buyer_maker: false,
1219 is_best_match: None,
1220 };
1221 history.push(&trade);
1222 }
1223
1224 let features = history.compute_features(20000);
1225 let pos = features.lookback_vwap_position.unwrap();
1226
1227 assert!(
1228 pos >= 0.0 && pos <= 1.0,
1229 "VWAP position should be in [0, 1], got {}",
1230 pos
1231 );
1232 }
1233
1234 #[test]
1235 fn test_hurst_soft_clamp_bounded() {
1236 for raw_h in [-10.0, -1.0, 0.0, 0.5, 1.0, 2.0, 10.0] {
1239 let clamped = soft_clamp_hurst(raw_h);
1240 assert!(
1241 clamped >= 0.0 && clamped <= 1.0,
1242 "Hurst {} soft-clamped to {} should be in [0, 1]",
1243 raw_h,
1244 clamped
1245 );
1246 }
1247
1248 let h_half = soft_clamp_hurst(0.5);
1250 assert!(
1251 (h_half - 0.5).abs() < f64::EPSILON,
1252 "Hurst 0.5 should map to 0.5, got {}",
1253 h_half
1254 );
1255 }
1256
1257 #[test]
1260 fn test_empty_lookback() {
1261 let history = TradeHistory::new(InterBarConfig::default());
1262 let features = history.compute_features(1000);
1263
1264 assert!(
1265 features.lookback_trade_count.is_none() || features.lookback_trade_count == Some(0)
1266 );
1267 }
1268
1269 #[test]
1270 fn test_single_trade_lookback() {
1271 let mut history = TradeHistory::new(InterBarConfig::default());
1272
1273 let trade = AggTrade {
1274 agg_trade_id: 0,
1275 price: FixedPoint(5000000000000),
1276 volume: FixedPoint(100000000),
1277 first_trade_id: 0,
1278 last_trade_id: 0,
1279 timestamp: 0,
1280 is_buyer_maker: false,
1281 is_best_match: None,
1282 };
1283 history.push(&trade);
1284
1285 let features = history.compute_features(1000);
1286
1287 assert_eq!(features.lookback_trade_count, Some(1));
1288 assert_eq!(features.lookback_duration_us, Some(0)); }
1290
1291 #[test]
1292 fn test_kyle_lambda_zero_imbalance() {
1293 let t0 = create_test_snapshot(0, 100.0, 1.0, false); let t1 = create_test_snapshot(1000, 102.0, 1.0, true); let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1];
1297
1298 let lambda = compute_kyle_lambda(&lookback);
1299
1300 assert!(
1301 lambda.is_finite(),
1302 "Kyle lambda should be finite, got {}",
1303 lambda
1304 );
1305 assert!(
1306 lambda.abs() < f64::EPSILON,
1307 "Kyle lambda should be 0 for zero imbalance"
1308 );
1309 }
1310
1311 #[test]
1314 fn test_kyle_lambda_strong_buy_pressure() {
1315 let trades: Vec<TradeSnapshot> = (0..5)
1317 .map(|i| create_test_snapshot(i * 1000, 100.0 + i as f64, 1.0, false))
1318 .chain((5..7).map(|i| create_test_snapshot(i * 1000, 100.0 + i as f64, 1.0, true)))
1319 .collect();
1320 let lookback: Vec<&TradeSnapshot> = trades.iter().collect();
1321
1322 let lambda = compute_kyle_lambda(&lookback);
1323 assert!(lambda > 0.0, "Buy pressure should yield positive lambda, got {}", lambda);
1324 assert!(lambda.is_finite(), "Kyle lambda should be finite");
1325 }
1326
1327 #[test]
1328 fn test_kyle_lambda_strong_sell_pressure() {
1329 let t0 = create_test_snapshot(0, 100.0, 1.0, false); let t1 = create_test_snapshot(1000, 99.9, 5.0, true); let t2 = create_test_snapshot(2000, 99.8, 5.0, true); let t3 = create_test_snapshot(3000, 99.7, 5.0, true); let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3];
1335
1336 let lambda = compute_kyle_lambda(&lookback);
1337 assert!(lambda.is_finite(), "Kyle lambda should be finite");
1338 }
1340
1341 #[test]
1342 fn test_burstiness_single_trade() {
1343 let t0 = create_test_snapshot(0, 100.0, 1.0, false);
1345 let lookback: Vec<&TradeSnapshot> = vec![&t0];
1346
1347 let b = compute_burstiness(&lookback);
1348 assert!(
1349 b.is_finite(),
1350 "Burstiness with single trade should be finite, got {}",
1351 b
1352 );
1353 }
1354
1355 #[test]
1356 fn test_burstiness_two_trades() {
1357 let t0 = create_test_snapshot(0, 100.0, 1.0, false);
1359 let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
1360 let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1];
1361
1362 let b = compute_burstiness(&lookback);
1363 assert!(
1364 (b - (-1.0)).abs() < 0.01,
1365 "Burstiness with uniform inter-arrivals should be -1, got {}",
1366 b
1367 );
1368 }
1369
1370 #[test]
1371 fn test_burstiness_bursty_arrivals() {
1372 let t0 = create_test_snapshot(0, 100.0, 1.0, false);
1374 let t1 = create_test_snapshot(100, 100.0, 1.0, false);
1375 let t2 = create_test_snapshot(200, 100.0, 1.0, false);
1376 let t3 = create_test_snapshot(5000, 100.0, 1.0, false);
1377 let t4 = create_test_snapshot(10000, 100.0, 1.0, false);
1378 let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3, &t4];
1379
1380 let b = compute_burstiness(&lookback);
1381 assert!(
1382 b > -1.0 && b <= 1.0,
1383 "Burstiness should be bounded [-1, 1], got {}",
1384 b
1385 );
1386 }
1387
1388 #[test]
1389 fn test_volume_skew_right_skewed() {
1390 let t0 = create_test_snapshot(0, 100.0, 0.1, false);
1392 let t1 = create_test_snapshot(1000, 100.0, 0.1, false);
1393 let t2 = create_test_snapshot(2000, 100.0, 0.1, false);
1394 let t3 = create_test_snapshot(3000, 100.0, 0.1, false);
1395 let t4 = create_test_snapshot(4000, 100.0, 10.0, false); let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3, &t4];
1397
1398 let skew = compute_volume_moments(&lookback).0;
1399 assert!(skew > 0.0, "Right-skewed volume should have positive skewness, got {}", skew);
1400 assert!(skew.is_finite(), "Skewness must be finite");
1401 }
1402
1403 #[test]
1404 fn test_volume_kurtosis_heavy_tails() {
1405 let t0 = create_test_snapshot(0, 100.0, 0.01, false);
1407 let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
1408 let t2 = create_test_snapshot(2000, 100.0, 1.0, false);
1409 let t3 = create_test_snapshot(3000, 100.0, 1.0, false);
1410 let t4 = create_test_snapshot(4000, 100.0, 100.0, false);
1411 let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3, &t4];
1412
1413 let kurtosis = compute_volume_moments(&lookback).1;
1414 assert!(kurtosis > 0.0, "Heavy-tailed distribution should have positive kurtosis, got {}", kurtosis);
1415 assert!(kurtosis.is_finite(), "Kurtosis must be finite");
1416 }
1417
1418 #[test]
1419 fn test_volume_skew_symmetric() {
1420 let t0 = create_test_snapshot(0, 100.0, 1.0, false);
1422 let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
1423 let t2 = create_test_snapshot(2000, 100.0, 1.0, false);
1424 let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2];
1425
1426 let skew = compute_volume_moments(&lookback).0;
1427 assert!(
1428 skew.abs() < f64::EPSILON,
1429 "Symmetric volume distribution should have near-zero skewness, got {}",
1430 skew
1431 );
1432 }
1433
1434 #[test]
1435 fn test_kyle_lambda_price_unchanged() {
1436 let t0 = create_test_snapshot(0, 100.0, 1.0, false);
1438 let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
1439 let t2 = create_test_snapshot(2000, 100.0, 1.0, false);
1440 let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2];
1441
1442 let lambda = compute_kyle_lambda(&lookback);
1443 assert!(
1444 lambda.is_finite(),
1445 "Kyle lambda should be finite even with no price change, got {}",
1446 lambda
1447 );
1448 }
1449
1450 fn make_trade(id: i64, timestamp: i64) -> AggTrade {
1454 AggTrade {
1455 agg_trade_id: id,
1456 price: FixedPoint(5000000000000), volume: FixedPoint(100000000), first_trade_id: id,
1459 last_trade_id: id,
1460 timestamp,
1461 is_buyer_maker: false,
1462 is_best_match: None,
1463 }
1464 }
1465
1466 #[test]
1467 fn test_bar_relative_bootstrap_keeps_all_trades() {
1468 let config = InterBarConfig {
1470 lookback_mode: LookbackMode::BarRelative(3),
1471 compute_tier2: false,
1472 compute_tier3: false,
1473 };
1474 let mut history = TradeHistory::new(config);
1475
1476 for i in 0..100 {
1478 history.push(&make_trade(i, i * 1000));
1479 }
1480
1481 assert_eq!(history.len(), 100, "Bootstrap phase should keep all trades");
1482 }
1483
1484 #[test]
1485 fn test_bar_relative_prunes_after_bar_close() {
1486 let config = InterBarConfig {
1487 lookback_mode: LookbackMode::BarRelative(2),
1488 compute_tier2: false,
1489 compute_tier3: false,
1490 };
1491 let mut history = TradeHistory::new(config);
1492
1493 for i in 0..10 {
1495 history.push(&make_trade(i, i * 1000));
1496 }
1497 history.on_bar_close(); for i in 10..30 {
1501 history.push(&make_trade(i, i * 1000));
1502 }
1503 history.on_bar_close(); for i in 30..35 {
1507 history.push(&make_trade(i, i * 1000));
1508 }
1509 history.on_bar_close(); history.push(&make_trade(35, 35000));
1525
1526 assert_eq!(
1530 history.len(),
1531 36,
1532 "All trades preserved below max_safe_capacity (2000), got {}",
1533 history.len()
1534 );
1535 }
1536
1537 #[test]
1538 fn test_bar_relative_mixed_bar_sizes() {
1539 let config = InterBarConfig {
1540 lookback_mode: LookbackMode::BarRelative(2),
1541 compute_tier2: false,
1542 compute_tier3: false,
1543 };
1544 let mut history = TradeHistory::new(config);
1545
1546 for i in 0..5 {
1548 history.push(&make_trade(i, i * 1000));
1549 }
1550 history.on_bar_close();
1551
1552 for i in 5..55 {
1554 history.push(&make_trade(i, i * 1000));
1555 }
1556 history.on_bar_close();
1557
1558 for i in 55..58 {
1560 history.push(&make_trade(i, i * 1000));
1561 }
1562 history.on_bar_close();
1563
1564 history.push(&make_trade(58, 58000));
1566
1567 assert_eq!(
1571 history.len(),
1572 59,
1573 "All trades preserved below max_safe_capacity (2000), got {}",
1574 history.len()
1575 );
1576 }
1577
1578 #[test]
1579 fn test_bar_relative_lookback_features_computed() {
1580 let config = InterBarConfig {
1581 lookback_mode: LookbackMode::BarRelative(3),
1582 compute_tier2: false,
1583 compute_tier3: false,
1584 };
1585 let mut history = TradeHistory::new(config);
1586
1587 for i in 0..20 {
1589 let price = 50000.0 + (i as f64 * 10.0);
1590 let trade = AggTrade {
1591 agg_trade_id: i,
1592 price: FixedPoint((price * 1e8) as i64),
1593 volume: FixedPoint(100000000),
1594 first_trade_id: i,
1595 last_trade_id: i,
1596 timestamp: i * 1000,
1597 is_buyer_maker: i % 2 == 0,
1598 is_best_match: None,
1599 };
1600 history.push(&trade);
1601 }
1602 history.on_bar_close();
1604
1605 history.on_bar_open(20000);
1607
1608 let features = history.compute_features(20000);
1610
1611 assert_eq!(features.lookback_trade_count, Some(20));
1613 assert!(features.lookback_ofi.is_some());
1614 assert!(features.lookback_intensity.is_some());
1615 }
1616
1617 #[test]
1618 fn test_bar_relative_reset_bar_boundaries() {
1619 let config = InterBarConfig {
1620 lookback_mode: LookbackMode::BarRelative(2),
1621 compute_tier2: false,
1622 compute_tier3: false,
1623 };
1624 let mut history = TradeHistory::new(config);
1625
1626 for i in 0..10 {
1628 history.push(&make_trade(i, i * 1000));
1629 }
1630 history.on_bar_close();
1631
1632 assert_eq!(history.bar_close_indices.len(), 1);
1633
1634 history.reset_bar_boundaries();
1636
1637 assert!(
1638 history.bar_close_indices.is_empty(),
1639 "bar_close_indices should be empty after reset"
1640 );
1641 assert_eq!(
1643 history.len(),
1644 10,
1645 "Trades should persist after boundary reset"
1646 );
1647 }
1648
1649 #[test]
1650 fn test_bar_relative_on_bar_close_limits_indices() {
1651 let config = InterBarConfig {
1652 lookback_mode: LookbackMode::BarRelative(2),
1653 compute_tier2: false,
1654 compute_tier3: false,
1655 };
1656 let mut history = TradeHistory::new(config);
1657
1658 for bar_num in 0..5 {
1660 for i in 0..5 {
1661 history.push(&make_trade(bar_num * 5 + i, (bar_num * 5 + i) * 1000));
1662 }
1663 history.on_bar_close();
1664 }
1665
1666 assert!(
1668 history.bar_close_indices.len() <= 3,
1669 "Should keep at most n+1 boundaries, got {}",
1670 history.bar_close_indices.len()
1671 );
1672 }
1673
1674 #[test]
1675 fn test_bar_relative_does_not_affect_fixed_count() {
1676 let config = InterBarConfig {
1678 lookback_mode: LookbackMode::FixedCount(10),
1679 compute_tier2: false,
1680 compute_tier3: false,
1681 };
1682 let mut history = TradeHistory::new(config);
1683
1684 for i in 0..30 {
1685 history.push(&make_trade(i, i * 1000));
1686 }
1687 history.on_bar_close();
1689
1690 assert!(
1692 history.len() <= 20,
1693 "FixedCount(10) should keep at most 20 trades, got {}",
1694 history.len()
1695 );
1696 assert!(
1697 history.bar_close_indices.is_empty(),
1698 "FixedCount should not track bar boundaries"
1699 );
1700 }
1701
1702 #[test]
1705 fn test_volume_moments_numerical_accuracy() {
1706 let price_fp = FixedPoint((100.0 * 1e8) as i64);
1709 let snapshots: Vec<TradeSnapshot> = (1..=5_i64)
1710 .map(|v| {
1711 let volume_fp = FixedPoint((v as f64 * 1e8) as i64);
1712 TradeSnapshot {
1713 price: price_fp,
1714 volume: volume_fp,
1715 timestamp: v * 1000,
1716 is_buyer_maker: false,
1717 turnover: price_fp.0 as i128 * volume_fp.0 as i128,
1718 }
1719 })
1720 .collect();
1721 let refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
1722 let (skew, kurt) = compute_volume_moments(&refs);
1723
1724 assert!(
1726 skew.abs() < 1e-10,
1727 "Symmetric distribution should have skewness ≈ 0, got {skew}"
1728 );
1729 assert!(
1731 (kurt - (-1.3)).abs() < 0.1,
1732 "Uniform-like kurtosis should be ≈ -1.3, got {kurt}"
1733 );
1734 }
1735
1736 #[test]
1737 fn test_volume_moments_edge_cases() {
1738 let price_fp = FixedPoint((100.0 * 1e8) as i64);
1739
1740 let v1 = FixedPoint((1.0 * 1e8) as i64);
1742 let v2 = FixedPoint((2.0 * 1e8) as i64);
1743 let s1 = TradeSnapshot {
1744 price: price_fp,
1745 volume: v1,
1746 timestamp: 1000,
1747 is_buyer_maker: false,
1748 turnover: price_fp.0 as i128 * v1.0 as i128,
1749 };
1750 let s2 = TradeSnapshot {
1751 price: price_fp,
1752 volume: v2,
1753 timestamp: 2000,
1754 is_buyer_maker: false,
1755 turnover: price_fp.0 as i128 * v2.0 as i128,
1756 };
1757 let refs: Vec<&TradeSnapshot> = vec![&s1, &s2];
1758 let (skew, kurt) = compute_volume_moments(&refs);
1759 assert_eq!(skew, 0.0, "n < 3 should return 0");
1760 assert_eq!(kurt, 0.0, "n < 3 should return 0");
1761
1762 let vol = FixedPoint((5.0 * 1e8) as i64);
1764 let same: Vec<TradeSnapshot> = (0..10_i64)
1765 .map(|i| TradeSnapshot {
1766 price: price_fp,
1767 volume: vol,
1768 timestamp: i * 1000,
1769 is_buyer_maker: false,
1770 turnover: price_fp.0 as i128 * vol.0 as i128,
1771 })
1772 .collect();
1773 let refs: Vec<&TradeSnapshot> = same.iter().collect();
1774 let (skew, kurt) = compute_volume_moments(&refs);
1775 assert_eq!(skew, 0.0, "All same volume should return 0");
1776 assert_eq!(kurt, 0.0, "All same volume should return 0");
1777 }
1778
1779 #[test]
1782 fn test_optimization_edge_case_zero_trades() {
1783 let history = TradeHistory::new(InterBarConfig::default());
1785
1786 let features = history.compute_features(1000);
1788
1789 assert!(features.lookback_ofi.is_none());
1791 assert!(features.lookback_kyle_lambda.is_none());
1792 assert!(features.lookback_hurst.is_none());
1793 }
1794
1795 #[test]
1796 fn test_optimization_edge_case_large_lookback() {
1797 let config = InterBarConfig {
1800 lookback_mode: LookbackMode::FixedCount(500),
1801 ..Default::default()
1802 };
1803 let mut history = TradeHistory::new(config);
1804
1805 for i in 0..600_i64 {
1807 let snapshot = create_test_snapshot(i * 1000, 100.0, 10.0, i % 2 == 0);
1808 history.push(&AggTrade {
1809 agg_trade_id: i,
1810 price: snapshot.price,
1811 volume: snapshot.volume,
1812 first_trade_id: i,
1813 last_trade_id: i,
1814 timestamp: snapshot.timestamp,
1815 is_buyer_maker: snapshot.is_buyer_maker,
1816 is_best_match: Some(false),
1817 });
1818 }
1819
1820 let lookback = history.get_lookback_trades(599000);
1822 assert!(
1823 lookback.len() <= 600, "Lookback should be <= 600 trades, got {}", lookback.len()
1825 );
1826
1827 let features = history.compute_features(599000);
1829
1830 assert!(features.lookback_trade_count.is_some(), "Trade count should be computed");
1832 assert!(features.lookback_ofi.is_some(), "OFI should be computed");
1833 }
1834
1835 #[test]
1836 fn test_optimization_edge_case_single_trade() {
1837 let mut history = TradeHistory::new(InterBarConfig::default());
1839
1840 let snapshot = create_test_snapshot(1000, 100.0, 10.0, false);
1841 history.push(&AggTrade {
1842 agg_trade_id: 1,
1843 price: snapshot.price,
1844 volume: snapshot.volume,
1845 first_trade_id: 1,
1846 last_trade_id: 1,
1847 timestamp: snapshot.timestamp,
1848 is_buyer_maker: snapshot.is_buyer_maker,
1849 is_best_match: Some(false),
1850 });
1851
1852 let features = history.compute_features(2000);
1853
1854 assert!(features.lookback_trade_count.is_some());
1856 assert!(features.lookback_hurst.is_none());
1858 }
1859
1860 #[test]
1861 fn test_optimization_many_trades() {
1862 let mut history = TradeHistory::new(InterBarConfig::default());
1864
1865 for i in 0..300_i64 {
1867 let snapshot = create_test_snapshot(
1868 i * 1000,
1869 100.0 + (i as f64 % 10.0),
1870 10.0 + (i as f64 % 5.0),
1871 i % 2 == 0,
1872 );
1873 history.push(&AggTrade {
1874 agg_trade_id: i,
1875 price: snapshot.price,
1876 volume: snapshot.volume,
1877 first_trade_id: i,
1878 last_trade_id: i,
1879 timestamp: snapshot.timestamp,
1880 is_buyer_maker: snapshot.is_buyer_maker,
1881 is_best_match: Some(false),
1882 });
1883 }
1884
1885 let lookback = history.get_lookback_trades(299000);
1887
1888 let features = history.compute_features(299000);
1890
1891 assert!(features.lookback_kyle_lambda.is_some(), "Kyle lambda should be computed");
1893 assert!(features.lookback_burstiness.is_some(), "Burstiness should be computed");
1894
1895 if lookback.len() >= 60 {
1897 assert!(features.lookback_hurst.is_some(), "Hurst should be computed");
1898 assert!(features.lookback_permutation_entropy.is_some(), "Entropy should be computed");
1899 }
1900 }
1901
1902 #[test]
1903 fn test_trade_history_with_external_cache() {
1904 use crate::entropy_cache_global::get_global_entropy_cache;
1906
1907 let _local_history = TradeHistory::new(InterBarConfig::default());
1909 let global_cache = get_global_entropy_cache();
1913 let _shared_history = TradeHistory::new_with_cache(InterBarConfig::default(), Some(global_cache.clone()));
1914 }
1918
1919 #[test]
1920 fn test_feature_result_cache_hit_miss() {
1921 use crate::types::AggTrade;
1923
1924 fn create_test_trade(price: f64, volume: f64, is_buyer_maker: bool) -> AggTrade {
1925 AggTrade {
1926 agg_trade_id: 1,
1927 timestamp: 1000000,
1928 price: FixedPoint((price * 1e8) as i64),
1929 volume: FixedPoint((volume * 1e8) as i64),
1930 first_trade_id: 1,
1931 last_trade_id: 1,
1932 is_buyer_maker,
1933 is_best_match: Some(true),
1934 }
1935 }
1936
1937 let mut history = TradeHistory::new(InterBarConfig {
1939 lookback_mode: LookbackMode::FixedCount(50),
1940 compute_tier2: false,
1941 compute_tier3: false,
1942 });
1943
1944 let trades = vec![
1946 create_test_trade(100.0, 1.0, false),
1947 create_test_trade(100.5, 1.5, true),
1948 create_test_trade(100.2, 1.2, false),
1949 ];
1950
1951 for trade in &trades {
1952 history.push(trade);
1953 }
1954
1955 let features1 = history.compute_features(2000000);
1957 assert!(features1.lookback_trade_count == Some(3));
1958
1959 let features2 = history.compute_features(2000000);
1961 assert!(features2.lookback_trade_count == Some(3));
1962
1963 assert_eq!(features1.lookback_ofi, features2.lookback_ofi);
1965 assert_eq!(features1.lookback_count_imbalance, features2.lookback_count_imbalance);
1966 }
1967
1968 #[test]
1969 fn test_feature_result_cache_multiple_computations() {
1970 use crate::types::AggTrade;
1972
1973 fn create_test_trade(price: f64, volume: f64, timestamp: i64, is_buyer_maker: bool) -> AggTrade {
1974 AggTrade {
1975 agg_trade_id: 1,
1976 timestamp,
1977 price: FixedPoint((price * 1e8) as i64),
1978 volume: FixedPoint((volume * 1e8) as i64),
1979 first_trade_id: 1,
1980 last_trade_id: 1,
1981 is_buyer_maker,
1982 is_best_match: Some(true),
1983 }
1984 }
1985
1986 let mut history = TradeHistory::new(InterBarConfig {
1987 lookback_mode: LookbackMode::FixedCount(50),
1988 compute_tier2: false,
1989 compute_tier3: false,
1990 });
1991
1992 let trades = vec![
1994 create_test_trade(100.0, 1.0, 1000000, false),
1995 create_test_trade(100.5, 1.5, 2000000, true),
1996 create_test_trade(100.2, 1.2, 3000000, false),
1997 create_test_trade(100.1, 1.1, 4000000, true),
1998 ];
1999
2000 for trade in &trades {
2001 history.push(trade);
2002 }
2003
2004 let features1 = history.compute_features(5000000); assert_eq!(features1.lookback_trade_count, Some(4));
2007 let ofi1 = features1.lookback_ofi;
2008
2009 let features2 = history.compute_features(5000000);
2011 assert_eq!(features2.lookback_trade_count, Some(4));
2012 assert_eq!(features2.lookback_ofi, ofi1, "Cache hit should return identical OFI");
2013
2014 let features3 = history.compute_features(3500000); assert_eq!(features3.lookback_trade_count, Some(3));
2017
2018 let features4 = history.compute_features(5000000);
2020 assert_eq!(features4.lookback_ofi, ofi1, "Cache reuse should return identical results");
2021 }
2022
2023 #[test]
2024 fn test_feature_result_cache_different_windows() {
2025 use crate::types::AggTrade;
2027
2028 fn create_test_trade(price: f64, volume: f64, timestamp: i64, is_buyer_maker: bool) -> AggTrade {
2029 AggTrade {
2030 agg_trade_id: 1,
2031 timestamp,
2032 price: FixedPoint((price * 1e8) as i64),
2033 volume: FixedPoint((volume * 1e8) as i64),
2034 first_trade_id: 1,
2035 last_trade_id: 1,
2036 is_buyer_maker,
2037 is_best_match: Some(true),
2038 }
2039 }
2040
2041 let mut history = TradeHistory::new(InterBarConfig {
2042 lookback_mode: LookbackMode::FixedCount(100),
2043 compute_tier2: false,
2044 compute_tier3: false,
2045 });
2046
2047 for i in 0..10 {
2049 let trade = create_test_trade(
2050 100.0 + (i as f64 * 0.1),
2051 1.0 + (i as f64 * 0.01),
2052 1000000 + (i as i64 * 100000), i % 2 == 0,
2054 );
2055 history.push(&trade);
2056 }
2057
2058 let features1 = history.compute_features(2000000);
2060 assert_eq!(features1.lookback_trade_count, Some(10));
2061
2062 for i in 10..15 {
2064 let trade = create_test_trade(
2065 100.0 + (i as f64 * 0.1),
2066 1.0 + (i as f64 * 0.01),
2067 2000000 + (i as i64 * 100000), i % 2 == 0,
2069 );
2070 history.push(&trade);
2071 }
2072
2073 let features2 = history.compute_features(2000000);
2075 assert_eq!(features2.lookback_trade_count, Some(10));
2076
2077 assert_eq!(features1.lookback_ofi, features2.lookback_ofi);
2079 }
2080
2081 #[test]
2082 fn test_adaptive_pruning_batch_size_tracked() {
2083 use crate::types::AggTrade;
2085
2086 fn create_test_trade(price: f64, timestamp: i64) -> AggTrade {
2087 AggTrade {
2088 agg_trade_id: 1,
2089 timestamp,
2090 price: FixedPoint((price * 1e8) as i64),
2091 volume: FixedPoint((1.0 * 1e8) as i64),
2092 first_trade_id: 1,
2093 last_trade_id: 1,
2094 is_buyer_maker: false,
2095 is_best_match: Some(true),
2096 }
2097 }
2098
2099 let mut history = TradeHistory::new(InterBarConfig {
2100 lookback_mode: LookbackMode::FixedCount(100),
2101 compute_tier2: false,
2102 compute_tier3: false,
2103 });
2104
2105 let initial_batch = history.adaptive_prune_batch;
2106 assert!(initial_batch > 0, "Initial batch size should be positive");
2107
2108 for i in 0..100 {
2110 let trade = create_test_trade(
2111 100.0 + (i as f64 * 0.01),
2112 1_000_000 + (i as i64 * 100),
2113 );
2114 history.push(&trade);
2115 }
2116
2117 assert!(
2119 history.adaptive_prune_batch > 0 && history.adaptive_prune_batch <= initial_batch * 4,
2120 "Batch size should be reasonable"
2121 );
2122 }
2123
2124 #[test]
2125 fn test_adaptive_pruning_deferred() {
2126 use crate::types::AggTrade;
2128
2129 fn create_test_trade(price: f64, timestamp: i64) -> AggTrade {
2130 AggTrade {
2131 agg_trade_id: 1,
2132 timestamp,
2133 price: FixedPoint((price * 1e8) as i64),
2134 volume: FixedPoint((1.0 * 1e8) as i64),
2135 first_trade_id: 1,
2136 last_trade_id: 1,
2137 is_buyer_maker: false,
2138 is_best_match: Some(true),
2139 }
2140 }
2141
2142 let mut history = TradeHistory::new(InterBarConfig {
2143 lookback_mode: LookbackMode::FixedCount(50),
2144 compute_tier2: false,
2145 compute_tier3: false,
2146 });
2147
2148 let max_capacity = history.max_safe_capacity;
2149
2150 for i in 0..300 {
2152 let trade = create_test_trade(
2153 100.0 + (i as f64 * 0.01),
2154 1_000_000 + (i as i64 * 100),
2155 );
2156 history.push(&trade);
2157 }
2158
2159 assert!(
2162 history.trades.len() <= max_capacity * 3,
2163 "Trade count should be controlled by deferred pruning"
2164 );
2165 }
2166
2167 #[test]
2168 fn test_adaptive_pruning_stats_tracking() {
2169 use crate::types::AggTrade;
2171
2172 fn create_test_trade(price: f64, timestamp: i64) -> AggTrade {
2173 AggTrade {
2174 agg_trade_id: 1,
2175 timestamp,
2176 price: FixedPoint((price * 1e8) as i64),
2177 volume: FixedPoint((1.0 * 1e8) as i64),
2178 first_trade_id: 1,
2179 last_trade_id: 1,
2180 is_buyer_maker: false,
2181 is_best_match: Some(true),
2182 }
2183 }
2184
2185 let mut history = TradeHistory::new(InterBarConfig {
2186 lookback_mode: LookbackMode::FixedCount(100),
2187 compute_tier2: false,
2188 compute_tier3: false,
2189 });
2190
2191 assert_eq!(history.prune_stats, (0, 0), "Initial stats should be zero");
2193
2194 for i in 0..2000 {
2196 let trade = create_test_trade(
2197 100.0 + (i as f64 * 0.01),
2198 1_000_000 + (i as i64 * 100),
2199 );
2200 history.push(&trade);
2201 }
2202
2203 assert!(
2207 history.prune_stats.0 <= 2000 && history.prune_stats.1 <= 10,
2208 "Pruning stats should be reasonable"
2209 );
2210 }
2211
2212 fn make_agg_trade(id: i64, price: f64, timestamp: i64) -> AggTrade {
2215 AggTrade {
2216 agg_trade_id: id,
2217 price: FixedPoint((price * 1e8) as i64),
2218 volume: FixedPoint(100000000), first_trade_id: id,
2220 last_trade_id: id,
2221 timestamp,
2222 is_buyer_maker: false,
2223 is_best_match: None,
2224 }
2225 }
2226
2227 #[test]
2228 fn test_get_lookback_empty_history() {
2229 let history = TradeHistory::new(InterBarConfig::default());
2230 let lookback = history.get_lookback_trades(1000);
2231 assert!(lookback.is_empty(), "Empty history should return empty lookback");
2232 }
2233
2234 #[test]
2235 fn test_has_lookback_empty_history() {
2236 let history = TradeHistory::new(InterBarConfig::default());
2237 assert!(!history.has_lookback_trades(1000), "Empty history should have no lookback");
2238 }
2239
2240 #[test]
2241 fn test_get_lookback_all_trades_after_bar_open() {
2242 let mut history = TradeHistory::new(InterBarConfig::default());
2243 for i in 0..5 {
2244 history.push(&make_agg_trade(i, 100.0, 2000 + i));
2245 }
2246 let lookback = history.get_lookback_trades(1000);
2247 assert!(lookback.is_empty(), "All trades after bar_open_time should yield empty lookback");
2248 }
2249
2250 #[test]
2251 fn test_compute_features_minimum_lookback() {
2252 let mut history = TradeHistory::new(InterBarConfig::default());
2253 history.push(&make_agg_trade(1, 100.0, 1000));
2254 history.push(&make_agg_trade(2, 101.0, 2000));
2255
2256 let features = history.compute_features(3000);
2257 assert!(features.lookback_ofi.is_some(), "OFI should compute with 2 trades");
2258 assert_eq!(features.lookback_trade_count, Some(2));
2259 }
2260
2261 #[test]
2262 fn test_has_lookback_cache_hit_path() {
2263 let mut history = TradeHistory::new(InterBarConfig::default());
2264 for i in 0..10 {
2265 history.push(&make_agg_trade(i, 100.0, i * 100));
2266 }
2267 let has1 = history.has_lookback_trades(500);
2268 let has2 = history.has_lookback_trades(500);
2269 assert_eq!(has1, has2, "Cache hit should return same result");
2270 assert!(has1, "Should have lookback trades before ts=500");
2271 }
2272
2273 #[test]
2274 fn test_get_lookback_trades_at_exact_timestamp() {
2275 let mut history = TradeHistory::new(InterBarConfig::default());
2276 for i in 1..=3i64 {
2277 history.push(&make_agg_trade(i, 100.0, i * 100));
2278 }
2279 let lookback = history.get_lookback_trades(200);
2281 assert_eq!(lookback.len(), 1, "Should get 1 trade before ts=200");
2282 assert_eq!(lookback[0].timestamp, 100);
2283 }
2284
2285 #[test]
2288 fn test_buffer_stats_empty_history() {
2289 let history = TradeHistory::new(InterBarConfig::default());
2290 let (trades_len, max_capacity, _batch, trades_pruned) = history.buffer_stats();
2291 assert_eq!(trades_len, 0, "Empty history should have 0 trades");
2292 assert!(max_capacity > 0, "max_safe_capacity should be positive");
2293 assert_eq!(trades_pruned, 0, "No trades should have been pruned");
2294 }
2295
2296 #[test]
2297 fn test_buffer_stats_after_pushes() {
2298 let mut history = TradeHistory::new(InterBarConfig::default());
2299 for i in 0..5 {
2300 history.push(&make_agg_trade(i, 100.0, i * 100));
2301 }
2302 let (trades_len, _max_capacity, _batch, _trades_pruned) = history.buffer_stats();
2303 assert_eq!(trades_len, 5, "Should have 5 trades after 5 pushes");
2304 }
2305
2306 #[test]
2307 fn test_has_lookback_no_trades_before_open() {
2308 let mut history = TradeHistory::new(InterBarConfig::default());
2309 for i in 0..5 {
2311 history.push(&make_agg_trade(i, 100.0, 1000 + i * 100));
2312 }
2313 assert!(history.has_lookback_trades(1000 + 200), "Should have lookback before ts=1200");
2315 assert!(!history.has_lookback_trades(1000), "No trades before first trade timestamp");
2317 assert!(!history.has_lookback_trades(500), "No trades before ts=500");
2319 }
2320
2321 #[test]
2322 fn test_has_lookback_all_trades_before_open() {
2323 let mut history = TradeHistory::new(InterBarConfig::default());
2324 for i in 0..5 {
2325 history.push(&make_agg_trade(i, 100.0, i * 100));
2326 }
2327 assert!(history.has_lookback_trades(999), "All trades should be lookback");
2329 }
2330
2331 #[test]
2332 fn test_buffer_stats_len_matches_is_empty() {
2333 let history = TradeHistory::new(InterBarConfig::default());
2334 assert!(history.is_empty(), "New history should be empty");
2335 assert_eq!(history.len(), 0, "New history length should be 0");
2336
2337 let mut history2 = TradeHistory::new(InterBarConfig::default());
2338 history2.push(&make_agg_trade(1, 100.0, 1000));
2339 assert!(!history2.is_empty(), "History with 1 trade should not be empty");
2340 assert_eq!(history2.len(), 1, "History length should be 1");
2341 }
2342
2343 #[test]
2348 fn test_tier2_features_computed_when_enabled() {
2349 let config = InterBarConfig {
2350 lookback_mode: LookbackMode::FixedCount(500),
2351 compute_tier2: true,
2352 compute_tier3: false,
2353 };
2354 let mut history = TradeHistory::new(config);
2355
2356 for i in 0..120i64 {
2358 let price = 50000.0 + (i as f64 * 0.7).sin() * 50.0;
2359 let volume = 1.0 + (i % 5) as f64 * 0.5;
2360 let trade = AggTrade {
2361 agg_trade_id: i,
2362 price: FixedPoint((price * 1e8) as i64),
2363 volume: FixedPoint((volume * 1e8) as i64),
2364 first_trade_id: i,
2365 last_trade_id: i,
2366 timestamp: i * 500, is_buyer_maker: i % 3 == 0, is_best_match: None,
2369 };
2370 history.push(&trade);
2371 }
2372
2373 let features = history.compute_features(120 * 500);
2374
2375 assert!(features.lookback_trade_count.is_some(), "trade_count should be Some");
2377 assert!(features.lookback_ofi.is_some(), "ofi should be Some");
2378
2379 assert!(features.lookback_kyle_lambda.is_some(), "kyle_lambda should be Some with tier2 enabled");
2381 assert!(features.lookback_burstiness.is_some(), "burstiness should be Some with tier2 enabled");
2382 assert!(features.lookback_volume_skew.is_some(), "volume_skew should be Some with tier2 enabled");
2383 assert!(features.lookback_volume_kurt.is_some(), "volume_kurt should be Some with tier2 enabled");
2384 assert!(features.lookback_price_range.is_some(), "price_range should be Some with tier2 enabled");
2385
2386 assert!(features.lookback_kaufman_er.is_none(), "kaufman_er should be None with tier3 disabled");
2388 assert!(features.lookback_hurst.is_none(), "hurst should be None with tier3 disabled");
2389 }
2390
2391 #[test]
2392 fn test_tier3_features_computed_when_enabled() {
2393 let config = InterBarConfig {
2394 lookback_mode: LookbackMode::FixedCount(500),
2395 compute_tier2: false,
2396 compute_tier3: true,
2397 };
2398 let mut history = TradeHistory::new(config);
2399
2400 for i in 0..120i64 {
2402 let price = 50000.0 + (i as f64 * 0.7).sin() * 50.0;
2403 let trade = AggTrade {
2404 agg_trade_id: i,
2405 price: FixedPoint((price * 1e8) as i64),
2406 volume: FixedPoint((1.5 * 1e8) as i64),
2407 first_trade_id: i,
2408 last_trade_id: i,
2409 timestamp: i * 500,
2410 is_buyer_maker: i % 2 == 0,
2411 is_best_match: None,
2412 };
2413 history.push(&trade);
2414 }
2415
2416 let features = history.compute_features(120 * 500);
2417
2418 assert!(features.lookback_trade_count.is_some(), "trade_count should be Some");
2420
2421 assert!(features.lookback_kyle_lambda.is_none(), "kyle_lambda should be None with tier2 disabled");
2423 assert!(features.lookback_burstiness.is_none(), "burstiness should be None with tier2 disabled");
2424
2425 assert!(features.lookback_kaufman_er.is_some(), "kaufman_er should be Some with tier3 enabled");
2427 assert!(features.lookback_garman_klass_vol.is_some(), "garman_klass should be Some with tier3 enabled");
2428 }
2429
2430 #[test]
2431 fn test_all_tiers_enabled_parallel_dispatch() {
2432 let config = InterBarConfig {
2433 lookback_mode: LookbackMode::FixedCount(500),
2434 compute_tier2: true,
2435 compute_tier3: true,
2436 };
2437 let mut history = TradeHistory::new(config);
2438
2439 for i in 0..200i64 {
2441 let price = 50000.0 + (i as f64 * 0.3).sin() * 100.0;
2442 let volume = 0.5 + (i % 7) as f64 * 0.3;
2443 let trade = AggTrade {
2444 agg_trade_id: i,
2445 price: FixedPoint((price * 1e8) as i64),
2446 volume: FixedPoint((volume * 1e8) as i64),
2447 first_trade_id: i,
2448 last_trade_id: i + 2, timestamp: i * 1000,
2450 is_buyer_maker: i % 4 == 0, is_best_match: None,
2452 };
2453 history.push(&trade);
2454 }
2455
2456 let features = history.compute_features(200 * 1000);
2457
2458 assert!(features.lookback_trade_count.is_some(), "trade_count");
2460 assert!(features.lookback_ofi.is_some(), "ofi");
2461 assert!(features.lookback_intensity.is_some(), "intensity");
2462 assert!(features.lookback_vwap.is_some(), "vwap");
2463
2464 assert!(features.lookback_kyle_lambda.is_some(), "kyle_lambda");
2466 assert!(features.lookback_burstiness.is_some(), "burstiness");
2467 assert!(features.lookback_volume_skew.is_some(), "volume_skew");
2468 assert!(features.lookback_volume_kurt.is_some(), "volume_kurt");
2469 assert!(features.lookback_price_range.is_some(), "price_range");
2470
2471 assert!(features.lookback_kaufman_er.is_some(), "kaufman_er");
2473 assert!(features.lookback_garman_klass_vol.is_some(), "garman_klass_vol");
2474
2475 assert!(features.lookback_ofi.unwrap().is_finite(), "ofi should be finite");
2477 assert!(features.lookback_kyle_lambda.unwrap().is_finite(), "kyle_lambda should be finite");
2478 assert!(features.lookback_kaufman_er.unwrap().is_finite(), "kaufman_er should be finite");
2479 }
2480}