1use crate::interbar_types::TradeSnapshot;
12use libm; use smallvec::SmallVec; use opendeviationbar_hurst; use wide::f64x2; #[derive(Debug, Clone)]
35pub struct LookbackCache {
36 pub prices: SmallVec<[f64; 256]>,
38 pub volumes: SmallVec<[f64; 256]>,
40 pub open: f64,
42 pub high: f64,
43 pub low: f64,
44 pub close: f64,
45 pub first_volume: f64,
47 pub total_volume: f64,
49 pub all_prices_finite: bool,
52 pub all_volumes_finite: bool,
55}
56
57#[cold]
60#[inline(never)]
61fn empty_lookback_cache() -> LookbackCache {
62 LookbackCache {
63 prices: SmallVec::new(),
64 volumes: SmallVec::new(),
65 open: 0.0,
66 high: 0.0,
67 low: 0.0,
68 close: 0.0,
69 first_volume: 0.0,
70 total_volume: 0.0,
71 all_prices_finite: true,
72 all_volumes_finite: true,
73 }
74}
75
76#[inline]
88pub fn extract_lookback_cache(lookback: &[&TradeSnapshot]) -> LookbackCache {
89 if lookback.is_empty() {
90 return empty_lookback_cache();
91 }
92
93 let first_trade = &lookback[0];
95 let last_trade = &lookback[lookback.len() - 1];
96
97 let mut cache = LookbackCache {
98 prices: SmallVec::with_capacity(lookback.len()),
99 volumes: SmallVec::with_capacity(lookback.len()),
100 open: first_trade.price.to_f64(),
101 high: f64::MIN,
102 low: f64::MAX,
103 close: last_trade.price.to_f64(),
104 first_volume: first_trade.volume.to_f64(),
105 total_volume: 0.0,
106 all_prices_finite: true,
107 all_volumes_finite: true,
108 };
109
110 for trade in lookback {
113 let p = trade.price.to_f64();
114 let v = trade.volume.to_f64();
115 cache.prices.push(p);
116 cache.volumes.push(v);
117 cache.total_volume += v;
118 cache.all_prices_finite &= p.is_finite();
120 cache.all_volumes_finite &= v.is_finite();
121 cache.high = cache.high.max(p);
123 cache.low = cache.low.min(p);
124 }
125
126 cache
127}
128
129#[inline]
156pub fn accumulate_buy_sell_branchless(trades: &[&TradeSnapshot]) -> (f64, f64) {
157 let n = trades.len();
158 let mut buy_vol = 0.0;
159 let mut sell_vol = 0.0;
160
161 let pairs = n / 2;
163 for i in 0..pairs {
164 let t1 = &trades[i * 2];
165 let t2 = &trades[i * 2 + 1];
166
167 let vol1 = t1.volume.to_f64();
168 let vol2 = t2.volume.to_f64();
169
170 let is_buyer_mask1 = t1.is_buyer_maker as u32 as f64;
174 let is_buyer_mask2 = t2.is_buyer_maker as u32 as f64;
175
176 sell_vol += vol1 * is_buyer_mask1;
180 buy_vol += vol1 * (1.0 - is_buyer_mask1);
181
182 sell_vol += vol2 * is_buyer_mask2;
183 buy_vol += vol2 * (1.0 - is_buyer_mask2);
184 }
185
186 if n % 2 == 1 {
188 let t = &trades[n - 1];
189 let vol = t.volume.to_f64();
190 let is_buyer_mask = t.is_buyer_maker as u32 as f64;
191
192 sell_vol += vol * is_buyer_mask;
193 buy_vol += vol * (1.0 - is_buyer_mask);
194 }
195
196 (buy_vol, sell_vol)
197}
198
199#[inline]
217pub fn compute_ofi_branchless(trades: &[&TradeSnapshot]) -> f64 {
218 let n = trades.len();
219 let mut buy_vol = 0.0;
220 let mut sell_vol = 0.0;
221
222 let pairs = n / 2;
225 for i in 0..pairs {
226 let t1 = &trades[i * 2];
227 let t2 = &trades[i * 2 + 1];
228
229 let vol1 = t1.volume.to_f64();
230 let vol2 = t2.volume.to_f64();
231
232 let mask1 = t1.is_buyer_maker as u32 as f64;
235 let mask2 = t2.is_buyer_maker as u32 as f64;
236
237 sell_vol += vol1 * mask1;
239 buy_vol += vol1 * (1.0 - mask1);
240
241 sell_vol += vol2 * mask2;
242 buy_vol += vol2 * (1.0 - mask2);
243 }
244
245 if n % 2 == 1 {
247 let t = &trades[n - 1];
248 let vol = t.volume.to_f64();
249 let mask = t.is_buyer_maker as u32 as f64;
250
251 sell_vol += vol * mask;
252 buy_vol += vol * (1.0 - mask);
253 }
254
255 let total_vol = buy_vol + sell_vol;
256
257 if total_vol > f64::EPSILON {
263 (buy_vol - sell_vol) / total_vol
264 } else {
265 0.0
266 }
267}
268
269pub struct EntropyCache {
287 cache: quick_cache::sync::Cache<u64, f64>,
291 hits: std::sync::Arc<std::sync::atomic::AtomicUsize>,
293 misses: std::sync::Arc<std::sync::atomic::AtomicUsize>,
295}
296
297impl EntropyCache {
298 pub fn new() -> Self {
300 Self {
301 cache: quick_cache::sync::Cache::new(128),
302 hits: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
303 misses: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
304 }
305 }
306
307 pub fn with_capacity(capacity: u64) -> Self {
319 Self {
320 cache: quick_cache::sync::Cache::new(capacity as usize),
321 hits: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
322 misses: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
323 }
324 }
325
326 fn price_hash(prices: &[f64]) -> u64 {
328 use foldhash::fast::FixedState;
329 use std::hash::{BuildHasher, Hash, Hasher};
330
331 let mut hasher = FixedState::default().build_hasher();
334
335 #[allow(unsafe_code)]
341 {
342 let price_bits: &[u64] = unsafe {
346 std::slice::from_raw_parts(
347 prices.as_ptr() as *const u64,
348 prices.len(),
349 )
350 };
351
352 price_bits.hash(&mut hasher);
354 }
355
356 hasher.finish()
357 }
358
359 pub fn get(&self, prices: &[f64]) -> Option<f64> {
362 if prices.is_empty() {
363 return None;
364 }
365
366 let hash = Self::price_hash(prices);
367 match self.cache.get(&hash) {
368 Some(entropy) => {
369 self.hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
370 Some(entropy)
371 }
372 None => {
373 self.misses.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
374 None
375 }
376 }
377 }
378
379 pub fn insert(&mut self, prices: &[f64], entropy: f64) {
381 if prices.is_empty() {
382 return;
383 }
384
385 let hash = Self::price_hash(prices);
386 self.cache.insert(hash, entropy);
387 }
388
389 pub fn metrics(&self) -> (usize, usize, f64) {
392 let hits = self.hits.load(std::sync::atomic::Ordering::Relaxed);
393 let misses = self.misses.load(std::sync::atomic::Ordering::Relaxed);
394 let total = hits + misses;
395 let hit_ratio = if total > 0 {
396 (hits as f64 / total as f64) * 100.0
397 } else {
398 0.0
399 };
400 (hits, misses, hit_ratio)
401 }
402
403 pub fn reset_metrics(&mut self) {
405 self.hits.store(0, std::sync::atomic::Ordering::Relaxed);
406 self.misses.store(0, std::sync::atomic::Ordering::Relaxed);
407 }
408}
409
410impl std::fmt::Debug for EntropyCache {
411 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412 let (hits, misses, hit_ratio) = self.metrics();
413 f.debug_struct("EntropyCache")
414 .field("cache_size", &"quick_cache(max_128)")
415 .field("hits", &hits)
416 .field("misses", &misses)
417 .field("hit_ratio_percent", &format!("{:.1}%", hit_ratio))
418 .finish()
419 }
420}
421
422impl Default for EntropyCache {
423 fn default() -> Self {
424 Self::new()
425 }
426}
427
428#[cfg(any(feature = "simd-burstiness", feature = "simd-kyle-lambda"))]
429mod simd {
430 use crate::interbar_types::TradeSnapshot;
439 use smallvec::SmallVec;
440 use wide::f64x4;
441
442 pub(crate) fn compute_burstiness_simd(lookback: &[&TradeSnapshot]) -> f64 {
451 if lookback.len() < 2 {
452 return 0.0;
453 }
454
455 let inter_arrivals = compute_inter_arrivals_simd(lookback);
457 let inv_n = 1.0 / inter_arrivals.len() as f64;
459
460 let mu = sum_f64_simd(&inter_arrivals) * inv_n;
462
463 let variance = variance_f64_simd(&inter_arrivals, mu, inv_n);
465 let sigma = variance.sqrt();
466
467 let denominator = sigma + mu;
471 let numerator = sigma - mu;
472
473 numerator / denominator.max(f64::EPSILON)
475 }
476
477 #[inline]
480 fn compute_inter_arrivals_simd(lookback: &[&TradeSnapshot]) -> SmallVec<[f64; 256]> {
482 let n = lookback.len();
483 if n < 2 {
484 return SmallVec::new();
485 }
486
487 let mut inter_arrivals: SmallVec<[f64; 256]> = smallvec::smallvec![0.0; n - 1];
488
489 let iter_count = (n - 1) / 4;
491 for i in 0..iter_count {
492 let idx = i * 4;
493 for j in 0..4 {
494 inter_arrivals[idx + j] =
495 (lookback[idx + j + 1].timestamp - lookback[idx + j].timestamp) as f64;
496 }
497 }
498
499 let remainder = (n - 1) % 4;
501 if remainder > 0 {
502 let idx = iter_count * 4;
503 for j in 0..remainder {
504 inter_arrivals[idx + j] =
505 (lookback[idx + j + 1].timestamp - lookback[idx + j].timestamp) as f64;
506 }
507 }
508
509 inter_arrivals
510 }
511
512 #[inline]
515 fn sum_f64_simd(values: &[f64]) -> f64 {
516 if values.is_empty() {
517 return 0.0;
518 }
519
520 let chunks = values.len() / 4;
522 let mut sum_vec = f64x4::splat(0.0);
523
524 for i in 0..chunks {
525 let idx = i * 4;
526 let chunk = f64x4::new([values[idx], values[idx + 1], values[idx + 2], values[idx + 3]]);
527 sum_vec += chunk;
528 }
529
530 let simd_sum: [f64; 4] = sum_vec.into();
532 let mut total = simd_sum[0] + simd_sum[1] + simd_sum[2] + simd_sum[3];
533
534 let remainder = values.len() % 4;
536 for j in 0..remainder {
537 total += values[chunks * 4 + j];
538 }
539
540 total
541 }
542
543 #[inline]
546 fn variance_f64_simd(values: &[f64], mu: f64, inv_n: f64) -> f64 {
548 if values.is_empty() {
549 return 0.0;
550 }
551
552 let mu_vec = f64x4::splat(mu);
553 let chunks = values.len() / 4;
554 let mut sum_sq_vec = f64x4::splat(0.0);
555
556 for i in 0..chunks {
557 let idx = i * 4;
558 let chunk = f64x4::new([values[idx], values[idx + 1], values[idx + 2], values[idx + 3]]);
559 let deviations = chunk - mu_vec;
560 let squared = deviations * deviations;
561 sum_sq_vec += squared;
562 }
563
564 let simd_sums: [f64; 4] = sum_sq_vec.into();
566 let mut sum_sq = simd_sums[0] + simd_sums[1] + simd_sums[2] + simd_sums[3];
567
568 let remainder = values.len() % 4;
570 for j in 0..remainder {
571 let v = values[chunks * 4 + j] - mu;
572 sum_sq += v * v;
573 }
574
575 sum_sq * inv_n
576 }
577
578 #[cfg(test)]
579 mod tests {
580 use super::*;
581
582 fn create_test_snapshot(ts: i64, price: f64, volume: f64) -> TradeSnapshot {
583 TradeSnapshot {
584 timestamp: ts,
585 price: crate::FixedPoint((price * 1e8) as i64),
586 volume: crate::FixedPoint((volume * 1e8) as i64),
587 is_buyer_maker: false,
588 turnover: (price * volume * 1e8) as i128,
589 }
590 }
591
592 #[test]
593 fn test_burstiness_simd_edge_case_empty() {
594 let lookback: Vec<&TradeSnapshot> = vec![];
595 assert_eq!(compute_burstiness_simd(&lookback), 0.0);
596 }
597
598 #[test]
599 fn test_burstiness_simd_edge_case_single() {
600 let t0 = create_test_snapshot(0, 100.0, 1.0);
601 let lookback = vec![&t0];
602 assert_eq!(compute_burstiness_simd(&lookback), 0.0);
603 }
604
605 #[test]
606 fn test_burstiness_simd_regular_intervals() {
607 let t0 = create_test_snapshot(0, 100.0, 1.0);
609 let t1 = create_test_snapshot(1000, 100.0, 1.0);
610 let t2 = create_test_snapshot(2000, 100.0, 1.0);
611 let t3 = create_test_snapshot(3000, 100.0, 1.0);
612 let t4 = create_test_snapshot(4000, 100.0, 1.0);
613 let lookback = vec![&t0, &t1, &t2, &t3, &t4];
614
615 let b = compute_burstiness_simd(&lookback);
616 assert!((b - (-1.0)).abs() < 0.01);
618 }
619
620 #[test]
621 fn test_burstiness_simd_clustered_arrivals() {
622 let t0 = create_test_snapshot(0, 100.0, 1.0);
624 let t1 = create_test_snapshot(10, 100.0, 1.0);
625 let t2 = create_test_snapshot(20, 100.0, 1.0);
626 let t3 = create_test_snapshot(5000, 100.0, 1.0); let t4 = create_test_snapshot(5010, 100.0, 1.0);
628 let t5 = create_test_snapshot(5020, 100.0, 1.0);
629 let lookback = vec![&t0, &t1, &t2, &t3, &t4, &t5];
630
631 let b = compute_burstiness_simd(&lookback);
632 assert!(b > 0.0);
634 assert!(b <= 1.0);
635 }
636
637 #[test]
638 fn test_burstiness_simd_bounds() {
639 let t0 = create_test_snapshot(0, 100.0, 1.0);
640 let t1 = create_test_snapshot(100, 100.0, 1.0);
641 let t2 = create_test_snapshot(200, 100.0, 1.0);
642 let t3 = create_test_snapshot(300, 100.0, 1.0);
643 let lookback = vec![&t0, &t1, &t2, &t3];
644
645 let b = compute_burstiness_simd(&lookback);
646 assert!(b >= -1.0 && b <= 1.0);
647 }
648
649 #[test]
650 fn test_simd_remainder_handling() {
651 let trades: Vec<_> = (0..7)
653 .map(|i| create_test_snapshot((i * 100) as i64, 100.0, 1.0))
654 .collect();
655 let trade_refs: Vec<_> = trades.iter().collect();
656
657 let b = compute_burstiness_simd(&trade_refs);
658 assert!(b >= -1.0 && b <= 1.0);
660 }
661 }
662
663 pub(crate) fn compute_kyle_lambda_simd(lookback: &[&TradeSnapshot]) -> f64 {
674 let n = lookback.len();
675
676 if n < 2 {
677 return 0.0;
678 }
679
680 let first_price = lookback[0].price.to_f64();
683 let last_price = lookback[n - 1].price.to_f64();
684
685 let (buy_vol, sell_vol) = if n > 500 {
687 accumulate_volumes_simd_wide(lookback, true)
689 } else {
690 accumulate_volumes_simd_wide(lookback, false)
692 };
693
694 let total_vol = buy_vol + sell_vol;
695 let first_price_abs = first_price.abs();
696
697 if buy_vol >= total_vol - f64::EPSILON {
699 return if first_price_abs > f64::EPSILON {
700 (last_price - first_price) / first_price
701 } else {
702 0.0
703 };
704 } else if sell_vol >= total_vol - f64::EPSILON {
705 return if first_price_abs > f64::EPSILON {
706 -((last_price - first_price) / first_price)
707 } else {
708 0.0
709 };
710 }
711
712 let normalized_imbalance = if total_vol > f64::EPSILON {
713 (buy_vol - sell_vol) / total_vol
714 } else {
715 0.0
716 };
717
718 let imbalance_abs = normalized_imbalance.abs();
722 if imbalance_abs <= f64::EPSILON {
723 return 0.0; }
725
726 let imbalance_valid = 1.0; let price_valid = if first_price_abs > f64::EPSILON { 1.0 } else { 0.0 };
729 let both_valid = imbalance_valid * price_valid;
730
731 let price_change = if first_price_abs > f64::EPSILON {
732 (last_price - first_price) / first_price
733 } else {
734 0.0
735 };
736
737 if both_valid > 0.0 {
738 price_change / normalized_imbalance
739 } else {
740 0.0
741 }
742 }
743
744 #[inline]
747 fn accumulate_volumes_simd_wide(lookback: &[&TradeSnapshot], subsample: bool) -> (f64, f64) {
748 let mut buy_vol = 0.0;
749 let mut sell_vol = 0.0;
750
751 if subsample {
752 for trade in lookback.iter().step_by(5) {
755 let vol = trade.volume.to_f64();
756 let is_buyer_mask = trade.is_buyer_maker as u32 as f64;
757
758 buy_vol += vol * (1.0 - is_buyer_mask);
761 sell_vol += vol * is_buyer_mask;
762 }
763 } else {
764 let n = lookback.len();
768 let pairs = n / 2;
769
770 for i in 0..pairs {
771 let idx = i * 2;
772 let t0 = lookback[idx];
773 let t1 = lookback[idx + 1];
774
775 let vol0 = t0.volume.to_f64();
776 let vol1 = t1.volume.to_f64();
777
778 let is_buyer_mask0 = t0.is_buyer_maker as u32 as f64;
780 let is_buyer_mask1 = t1.is_buyer_maker as u32 as f64;
781
782 buy_vol += vol0 * (1.0 - is_buyer_mask0);
785 sell_vol += vol0 * is_buyer_mask0;
786
787 buy_vol += vol1 * (1.0 - is_buyer_mask1);
788 sell_vol += vol1 * is_buyer_mask1;
789 }
790
791 if n % 2 == 1 {
793 let last_trade = lookback[n - 1];
794 let vol = last_trade.volume.to_f64();
795 let is_buyer_mask = last_trade.is_buyer_maker as u32 as f64;
796
797 buy_vol += vol * (1.0 - is_buyer_mask);
798 sell_vol += vol * is_buyer_mask;
799 }
800 }
801
802 (buy_vol, sell_vol)
803 }
804}
805
806#[inline]
828pub fn compute_kyle_lambda(lookback: &[&TradeSnapshot]) -> f64 {
829 #[cfg(feature = "simd-kyle-lambda")]
831 {
832 simd::compute_kyle_lambda_simd(lookback)
833 }
834
835 #[cfg(not(feature = "simd-kyle-lambda"))]
836 {
837 compute_kyle_lambda_scalar(lookback)
838 }
839}
840
841#[allow(dead_code)] #[inline]
845fn compute_kyle_lambda_scalar(lookback: &[&TradeSnapshot]) -> f64 {
846 let n = lookback.len();
847
848 if n < 2 {
849 return 0.0;
850 }
851
852 let first_price = lookback[0].price.to_f64();
855 let last_price = lookback[n - 1].price.to_f64();
856
857 let (buy_vol, sell_vol) = if n > 500 {
859 lookback.iter().step_by(5).fold((0.0, 0.0), |acc, t| {
862 if t.is_buyer_maker {
863 (acc.0, acc.1 + t.volume.to_f64())
864 } else {
865 (acc.0 + t.volume.to_f64(), acc.1)
866 }
867 })
868 } else {
869 let mut buy_vol = 0.0;
874 let mut sell_vol = 0.0;
875
876 let pairs = n / 2;
878 for i in 0..pairs {
879 let t1 = &lookback[i * 2];
880 let t2 = &lookback[i * 2 + 1];
881
882 let vol1 = t1.volume.to_f64();
884 let vol2 = t2.volume.to_f64();
885
886 if t1.is_buyer_maker {
887 sell_vol += vol1;
888 } else {
889 buy_vol += vol1;
890 }
891
892 if t2.is_buyer_maker {
893 sell_vol += vol2;
894 } else {
895 buy_vol += vol2;
896 }
897 }
898
899 if n % 2 == 1 {
901 let t = &lookback[n - 1];
902 let vol = t.volume.to_f64();
903 if t.is_buyer_maker {
904 sell_vol += vol;
905 } else {
906 buy_vol += vol;
907 }
908 }
909
910 (buy_vol, sell_vol)
911 };
912
913 let total_vol = buy_vol + sell_vol;
914 let first_price_abs = first_price.abs();
915
916 if buy_vol >= total_vol - f64::EPSILON {
920 return if first_price_abs > f64::EPSILON {
922 (last_price - first_price) / first_price
923 } else {
924 0.0
925 };
926 } else if sell_vol >= total_vol - f64::EPSILON {
927 return if first_price_abs > f64::EPSILON {
929 -((last_price - first_price) / first_price)
930 } else {
931 0.0
932 };
933 }
934
935 let normalized_imbalance = if total_vol > f64::EPSILON {
936 (buy_vol - sell_vol) / total_vol
937 } else {
938 0.0
939 };
940
941 let imbalance_abs = normalized_imbalance.abs();
945 if imbalance_abs <= f64::EPSILON {
946 return 0.0; }
948
949 let imbalance_valid = 1.0; let price_valid = if first_price_abs > f64::EPSILON { 1.0 } else { 0.0 };
955 let both_valid = imbalance_valid * price_valid; let price_change = if first_price_abs > f64::EPSILON {
959 (last_price - first_price) / first_price
960 } else {
961 0.0
962 };
963
964 if both_valid > 0.0 {
966 price_change / normalized_imbalance
967 } else {
968 0.0
969 }
970}
971
972#[inline]
985pub fn compute_burstiness(lookback: &[&TradeSnapshot]) -> f64 {
986 #[cfg(feature = "simd-burstiness")]
988 {
989 simd::compute_burstiness_simd(lookback)
990 }
991
992 #[cfg(not(feature = "simd-burstiness"))]
993 {
994 compute_burstiness_scalar(lookback)
995 }
996}
997
998#[allow(dead_code)] #[inline]
1002fn compute_burstiness_scalar(lookback: &[&TradeSnapshot]) -> f64 {
1003 if lookback.len() < 2 {
1004 return 0.0;
1005 }
1006
1007 let mut mean = 0.0;
1010 let mut m2 = 0.0; let mut count = 0.0;
1012
1013 for i in 1..lookback.len() {
1014 let delta_t = (lookback[i].timestamp - lookback[i - 1].timestamp) as f64;
1015 count += 1.0;
1016 let delta = delta_t - mean;
1017 mean += delta / count;
1018 let delta2 = delta_t - mean;
1019 m2 += delta * delta2;
1020 }
1021
1022 let variance = m2 / count;
1023 let sigma = variance.sqrt();
1024
1025 let denominator = sigma + mean;
1028 let numerator = sigma - mean;
1029 numerator / denominator.max(f64::EPSILON)
1030}
1031
1032#[inline]
1040pub fn compute_volume_moments(lookback: &[&TradeSnapshot]) -> (f64, f64) {
1041 let n = lookback.len() as f64;
1042
1043 if n < 3.0 {
1044 return (0.0, 0.0);
1045 }
1046
1047 let n_inv = 1.0 / n;
1049
1050 let sum_vol = lookback.iter().fold(0.0, |acc, t| acc + t.volume.to_f64());
1052 let mu = sum_vol * n_inv;
1053
1054 let (m2, m3, m4) = lookback.iter().fold((0.0, 0.0, 0.0), |(m2, m3, m4), t| {
1056 let v = t.volume.to_f64();
1057 let d = v - mu;
1058 let d2 = d * d;
1059 (m2 + d2, m3 + d2 * d, m4 + d2 * d2)
1060 });
1061 let m2 = m2 * n_inv;
1062 let m3 = m3 * n_inv;
1063 let m4 = m4 * n_inv;
1064
1065 let sigma = m2.sqrt();
1066
1067 if sigma < f64::EPSILON {
1068 return (0.0, 0.0); }
1070
1071 let sigma2 = sigma * sigma;
1073 let sigma3 = sigma2 * sigma;
1074 let sigma4 = sigma2 * sigma2;
1075
1076 let skewness = m3 / sigma3;
1077 let kurtosis = m4 / sigma4 - 3.0; (skewness, kurtosis)
1080}
1081
1082#[inline]
1094pub fn compute_volume_moments_cached(volumes: &[f64]) -> (f64, f64) {
1095 let n = volumes.len() as f64;
1096 if n < 3.0 {
1097 return (0.0, 0.0);
1098 }
1099 let sum_vol: f64 = volumes.iter().sum();
1100 compute_volume_moments_with_mean(volumes, sum_vol / n)
1101}
1102
1103#[inline]
1112pub fn compute_volume_moments_with_mean(volumes: &[f64], mu: f64) -> (f64, f64) {
1113 let n = volumes.len() as f64;
1114
1115 if n < 3.0 {
1116 return (0.0, 0.0);
1117 }
1118
1119 let (m2, m3, m4) = volumes.iter().fold((0.0, 0.0, 0.0), |(m2, m3, m4), &v| {
1121 let d = v - mu;
1122 let d2 = d * d;
1123 (m2 + d2, m3 + d2 * d, m4 + d2 * d2)
1124 });
1125 let n_inv = 1.0 / n;
1127 let m2 = m2 * n_inv;
1128 let m3 = m3 * n_inv;
1129 let m4 = m4 * n_inv;
1130
1131 let sigma = m2.sqrt();
1132
1133 if sigma < f64::EPSILON {
1134 return (0.0, 0.0); }
1136
1137 let sigma2 = sigma * sigma;
1139 let sigma3 = sigma2 * sigma;
1140 let sigma4 = sigma2 * sigma2;
1141
1142 let skewness = m3 / sigma3;
1143 let kurtosis = m4 / sigma4 - 3.0; (skewness, kurtosis)
1146}
1147
1148#[inline]
1156pub fn compute_kaufman_er(prices: &[f64]) -> f64 {
1157 if prices.len() < 2 {
1158 return 0.0;
1159 }
1160
1161 let n = prices.len();
1163 let net_movement = (prices[n - 1] - prices[0]).abs();
1164
1165 use wide::f64x4;
1168
1169 let mut volatility_vec = f64x4::splat(0.0);
1170
1171 let chunks = (n - 1) / 4;
1173 for chunk_idx in 0..chunks {
1174 let i = chunk_idx * 4 + 1;
1175 let diff1 = (prices[i] - prices[i - 1]).abs();
1176 let diff2 = (prices[i + 1] - prices[i]).abs();
1177 let diff3 = (prices[i + 2] - prices[i + 1]).abs();
1178 let diff4 = (prices[i + 3] - prices[i + 2]).abs();
1179 volatility_vec += f64x4::new([diff1, diff2, diff3, diff4]);
1180 }
1181
1182 let arr: [f64; 4] = volatility_vec.into();
1184 let mut volatility = arr[0] + arr[1] + arr[2] + arr[3];
1185
1186 let remainder = (n - 1) % 4;
1188 for i in (chunks * 4 + 1)..(chunks * 4 + 1 + remainder) {
1189 if i < n {
1190 volatility += (prices[i] - prices[i - 1]).abs();
1191 }
1192 }
1193
1194 if volatility > f64::EPSILON {
1195 net_movement / volatility
1196 } else {
1197 0.0 }
1199}
1200
1201const GARMAN_KLASS_COEFFICIENT: f64 = 0.3862943611198906;
1205
1206const LN_2_FACTORIAL: f64 = std::f64::consts::LN_2;
1209
1210const LN_3_FACTORIAL: f64 = 1.791759469228055;
1213
1214#[inline]
1223pub fn compute_garman_klass(lookback: &[&TradeSnapshot]) -> f64 {
1224 if lookback.is_empty() {
1225 return 0.0;
1226 }
1227
1228 let n = lookback.len();
1230 let o = lookback[0].price.to_f64();
1231 let c = lookback[n - 1].price.to_f64();
1232 let (l, h) = lookback.iter().fold((f64::MAX, f64::MIN), |acc, t| {
1233 let p = t.price.to_f64();
1234 (acc.0.min(p), acc.1.max(p))
1235 });
1236
1237 if o <= f64::EPSILON || l <= f64::EPSILON || h <= f64::EPSILON {
1239 return 0.0;
1240 }
1241
1242 let log_hl = libm::log(h / l);
1244 let log_co = libm::log(c / o);
1245
1246 let variance = 0.5 * (log_hl * log_hl) - GARMAN_KLASS_COEFFICIENT * (log_co * log_co);
1248
1249 if variance > 0.0 {
1251 variance.sqrt()
1252 } else {
1253 0.0 }
1255}
1256
1257#[inline]
1264pub fn compute_garman_klass_with_ohlc(open: f64, high: f64, low: f64, close: f64) -> f64 {
1265 if open <= f64::EPSILON || low <= f64::EPSILON || high <= f64::EPSILON {
1267 return 0.0;
1268 }
1269
1270 let log_hl = libm::log(high / low);
1272 let log_co = libm::log(close / open);
1273
1274 let variance = 0.5 * (log_hl * log_hl) - GARMAN_KLASS_COEFFICIENT * (log_co * log_co);
1276
1277 if variance > 0.0 {
1278 variance.sqrt()
1279 } else {
1280 0.0
1281 }
1282}
1283
1284#[inline]
1296pub fn compute_hurst_dfa(prices: &[f64]) -> f64 {
1297 const MIN_SAMPLES: usize = 64;
1301 if prices.len() < MIN_SAMPLES {
1302 return 0.5; }
1304
1305 let h = opendeviationbar_hurst::rssimple(prices);
1308
1309 soft_clamp_hurst(h)
1311}
1312
1313#[inline]
1319pub(crate) fn soft_clamp_hurst(h: f64) -> f64 {
1323 crate::intrabar::normalization_lut::soft_clamp_hurst_lut(h)
1324}
1325
1326#[inline(always)]
1356pub fn compute_permutation_entropy(prices: &[f64]) -> f64 {
1357 let n = prices.len();
1358
1359 if n < 10 {
1360 return 1.0; }
1362
1363 if n >= 30 {
1367 compute_permutation_entropy_m3_cached_batch(prices)
1370 } else {
1371 compute_permutation_entropy_m2(prices)
1374 }
1375}
1376
1377#[inline]
1382fn compute_permutation_entropy_m3_cached_batch(prices: &[f64]) -> f64 {
1383 compute_permutation_entropy_m3_simd_batch(prices)
1386}
1387
1388#[inline]
1392fn compute_permutation_entropy_m2(prices: &[f64]) -> f64 {
1393 debug_assert!(prices.len() >= 10);
1394
1395 let mut all_ascending = true;
1399 for i in 0..prices.len() - 1 {
1400 if prices[i] > prices[i + 1] {
1401 all_ascending = false;
1402 break;
1403 }
1404 }
1405
1406 if all_ascending {
1407 return 0.0; }
1409
1410 let mut counts = [0u16; 2]; let n_patterns = prices.len() - 1;
1412
1413 for i in 0..n_patterns {
1414 let idx = if prices[i] <= prices[i + 1] { 0 } else { 1 };
1415 counts[idx] += 1;
1416 }
1417
1418 let total = n_patterns as f64;
1420 let reciprocal = 1.0 / total;
1423 let entropy: f64 = counts
1426 .iter()
1427 .fold(0.0, |acc, &c| {
1428 if c > 0 {
1429 let p = (c as f64) * reciprocal;
1430 acc + (-p * libm::log(p)) } else {
1432 acc
1433 }
1434 });
1435
1436 entropy / LN_2_FACTORIAL }
1438
1439#[inline]
1450fn compute_permutation_entropy_m3_simd_batch(prices: &[f64]) -> f64 {
1451 let n = prices.len();
1452 let n_patterns = n - 2;
1453
1454 let mut is_monotonic_inc = true;
1456 let mut is_monotonic_dec = true;
1457 for i in 0..n - 1 {
1458 let cmp = (prices[i] > prices[i + 1]) as u8;
1459 is_monotonic_inc &= cmp == 0;
1460 is_monotonic_dec &= cmp == 1;
1461 if !is_monotonic_inc && !is_monotonic_dec {
1462 break;
1463 }
1464 }
1465
1466 if is_monotonic_inc || is_monotonic_dec {
1467 return 0.0; }
1469
1470 let mut pattern_counts: [u16; 6] = [0; 6];
1473
1474 let simd_bulk_patterns = (n_patterns / 16) * 16;
1478
1479 let mut i = 0;
1480 while i < simd_bulk_patterns {
1481 let p0 = ordinal_pattern_index_m3(prices[i], prices[i + 1], prices[i + 2]);
1484 let p1 = ordinal_pattern_index_m3(prices[i + 1], prices[i + 2], prices[i + 3]);
1485 let p2 = ordinal_pattern_index_m3(prices[i + 2], prices[i + 3], prices[i + 4]);
1486 let p3 = ordinal_pattern_index_m3(prices[i + 3], prices[i + 4], prices[i + 5]);
1487 let p4 = ordinal_pattern_index_m3(prices[i + 4], prices[i + 5], prices[i + 6]);
1488 let p5 = ordinal_pattern_index_m3(prices[i + 5], prices[i + 6], prices[i + 7]);
1489 let p6 = ordinal_pattern_index_m3(prices[i + 6], prices[i + 7], prices[i + 8]);
1490 let p7 = ordinal_pattern_index_m3(prices[i + 7], prices[i + 8], prices[i + 9]);
1491 let p8 = ordinal_pattern_index_m3(prices[i + 8], prices[i + 9], prices[i + 10]);
1492 let p9 = ordinal_pattern_index_m3(prices[i + 9], prices[i + 10], prices[i + 11]);
1493 let p10 = ordinal_pattern_index_m3(prices[i + 10], prices[i + 11], prices[i + 12]);
1494 let p11 = ordinal_pattern_index_m3(prices[i + 11], prices[i + 12], prices[i + 13]);
1495 let p12 = ordinal_pattern_index_m3(prices[i + 12], prices[i + 13], prices[i + 14]);
1496 let p13 = ordinal_pattern_index_m3(prices[i + 13], prices[i + 14], prices[i + 15]);
1497 let p14 = ordinal_pattern_index_m3(prices[i + 14], prices[i + 15], prices[i + 16]);
1498 let p15 = ordinal_pattern_index_m3(prices[i + 15], prices[i + 16], prices[i + 17]);
1499
1500 pattern_counts[p0] += 1;
1502 pattern_counts[p1] += 1;
1503 pattern_counts[p2] += 1;
1504 pattern_counts[p3] += 1;
1505 pattern_counts[p4] += 1;
1506 pattern_counts[p5] += 1;
1507 pattern_counts[p6] += 1;
1508 pattern_counts[p7] += 1;
1509 pattern_counts[p8] += 1;
1510 pattern_counts[p9] += 1;
1511 pattern_counts[p10] += 1;
1512 pattern_counts[p11] += 1;
1513 pattern_counts[p12] += 1;
1514 pattern_counts[p13] += 1;
1515 pattern_counts[p14] += 1;
1516 pattern_counts[p15] += 1;
1517
1518 i += 16;
1519 }
1520
1521 let remainder_patterns = n_patterns - simd_bulk_patterns;
1523 let remainder_8x = (remainder_patterns / 8) * 8;
1524 let mut j = simd_bulk_patterns;
1525
1526 while j < simd_bulk_patterns + remainder_8x {
1527 let p0 = ordinal_pattern_index_m3(prices[j], prices[j + 1], prices[j + 2]);
1528 let p1 = ordinal_pattern_index_m3(prices[j + 1], prices[j + 2], prices[j + 3]);
1529 let p2 = ordinal_pattern_index_m3(prices[j + 2], prices[j + 3], prices[j + 4]);
1530 let p3 = ordinal_pattern_index_m3(prices[j + 3], prices[j + 4], prices[j + 5]);
1531 let p4 = ordinal_pattern_index_m3(prices[j + 4], prices[j + 5], prices[j + 6]);
1532 let p5 = ordinal_pattern_index_m3(prices[j + 5], prices[j + 6], prices[j + 7]);
1533 let p6 = ordinal_pattern_index_m3(prices[j + 6], prices[j + 7], prices[j + 8]);
1534 let p7 = ordinal_pattern_index_m3(prices[j + 7], prices[j + 8], prices[j + 9]);
1535
1536 pattern_counts[p0] += 1;
1537 pattern_counts[p1] += 1;
1538 pattern_counts[p2] += 1;
1539 pattern_counts[p3] += 1;
1540 pattern_counts[p4] += 1;
1541 pattern_counts[p5] += 1;
1542 pattern_counts[p6] += 1;
1543 pattern_counts[p7] += 1;
1544
1545 j += 8;
1546 }
1547
1548 for k in (simd_bulk_patterns + remainder_8x)..n_patterns {
1550 let pattern_idx = ordinal_pattern_index_m3(prices[k], prices[k + 1], prices[k + 2]);
1551 pattern_counts[pattern_idx] += 1;
1552 }
1553
1554 let inv_total = 1.0 / n_patterns as f64;
1557 let entropy: f64 = pattern_counts
1560 .iter()
1561 .fold(0.0, |acc, &count| {
1562 if count > 0 {
1563 let p = count as f64 * inv_total;
1564 acc + (-p * libm::log(p)) } else {
1566 acc
1567 }
1568 });
1569
1570 entropy / LN_3_FACTORIAL }
1572
1573#[inline(always)]
1590pub(crate) fn ordinal_pattern_index_m3(a: f64, b: f64, c: f64) -> usize {
1591 const LOOKUP: [usize; 8] = [
1605 5, 0, 3, 2, 4, 1, 0, 0, ];
1614
1615 let ab = (a <= b) as usize;
1616 let bc = (b <= c) as usize;
1617 let ac = (a <= c) as usize;
1618
1619 LOOKUP[(ab << 2) | (bc << 1) | ac]
1620}
1621
1622#[inline]
1643pub fn extract_ohlc_batch(lookback: &[&TradeSnapshot]) -> (f64, f64, f64, f64) {
1644 if lookback.is_empty() {
1645 return (0.0, 0.0, 0.0, 0.0);
1646 }
1647
1648 let n = lookback.len();
1650 let open = lookback[0].price.to_f64();
1651 let close = lookback[n - 1].price.to_f64();
1652
1653 let (high, low) = lookback.iter().fold((f64::MIN, f64::MAX), |acc, t| {
1654 let p = t.price.to_f64();
1655 (acc.0.max(p), acc.1.min(p))
1656 });
1657
1658 (open, high, low, close)
1659}
1660
1661#[inline]
1668pub fn extract_prices_and_ohlc_cached(
1669 lookback: &[&TradeSnapshot],
1670) -> (SmallVec<[f64; 256]>, (f64, f64, f64, f64)) {
1671 if lookback.is_empty() {
1672 return (SmallVec::new(), (0.0, 0.0, 0.0, 0.0));
1673 }
1674
1675 let n = lookback.len();
1677 let open = lookback[0].price.to_f64();
1678 let close = lookback[n - 1].price.to_f64();
1679
1680 let mut prices = SmallVec::with_capacity(lookback.len());
1682 let mut high = f64::MIN;
1683 let mut low = f64::MAX;
1684
1685 for trade in lookback {
1686 let p = trade.price.to_f64();
1687 prices.push(p);
1688 if p > high {
1689 high = p;
1690 }
1691 if p < low {
1692 low = p;
1693 }
1694 }
1695
1696 (prices, (open, high, low, close))
1697}
1698
1699#[inline]
1720pub fn compute_approximate_entropy(prices: &[f64], m: usize, r: f64) -> f64 {
1721 let n = prices.len();
1722
1723 if n < m + 1 {
1724 return 0.0;
1725 }
1726
1727 let phi_m = compute_phi(prices, m, r);
1729
1730 let phi_m1 = compute_phi(prices, m + 1, r);
1732
1733 ((phi_m - phi_m1) / libm::log(n as f64)).max(0.0).min(1.0)
1736}
1737
1738#[inline]
1753fn patterns_within_distance_simd(p1: &[f64], p2: &[f64], r: f64, m: usize) -> bool {
1754 if m == 2 && p1.len() >= 2 && p2.len() >= 2 {
1756 let v1 = f64x2::new([p1[0], p1[1]]);
1758 let v2 = f64x2::new([p2[0], p2[1]]);
1759 let diffs = (v1 - v2).abs();
1760
1761 let d0 = diffs.to_array()[0];
1764 let d1 = diffs.to_array()[1];
1765 d0 <= r && d1 <= r
1766 } else {
1767 let mut is_within_distance = true;
1769 for k in 0..m.min(p1.len()).min(p2.len()) {
1770 if (p1[k] - p2[k]).abs() > r {
1771 is_within_distance = false;
1772 break;
1773 }
1774 }
1775 is_within_distance
1776 }
1777}
1778
1779fn compute_phi_sampled(prices: &[f64], m: usize, r: f64) -> f64 {
1795 let n = prices.len();
1796 if n < m {
1797 return 0.0;
1798 }
1799
1800 let num_patterns = n - m + 1;
1801
1802 let sample_interval = if num_patterns >= 1000 {
1804 4 } else if num_patterns >= 500 {
1806 3 } else if num_patterns >= 300 {
1808 2 } else {
1810 1 };
1812
1813 let mut count = 0usize;
1814
1815 if sample_interval == 1 {
1816 for i in 0..num_patterns {
1818 let p1 = &prices[i..i + m];
1819 for j in (i + 1)..num_patterns {
1820 let p2 = &prices[j..j + m];
1821 if patterns_within_distance_simd(p1, p2, r, m) {
1822 count += 1;
1823 }
1824 }
1825 }
1826 } else {
1827 for i in (0..num_patterns).step_by(sample_interval) {
1829 let p1 = &prices[i..i + m];
1830 for j in ((i + sample_interval)..num_patterns).step_by(sample_interval) {
1831 let p2 = &prices[j..j + m];
1832 if patterns_within_distance_simd(p1, p2, r, m) {
1833 count += 1;
1834 }
1835 }
1836 }
1837
1838 let interval_f64 = sample_interval as f64;
1842 count = (count as f64 * (interval_f64 * interval_f64)).round() as usize;
1843 }
1844
1845 if count == 0 {
1847 return 0.0;
1848 }
1849
1850 let inv_total_pairs = 2.0 / (num_patterns as f64 * (num_patterns - 1) as f64);
1852 let c = count as f64 * inv_total_pairs;
1853 -c * libm::log(c) }
1855
1856fn compute_phi(prices: &[f64], m: usize, r: f64) -> f64 {
1857 let n = prices.len();
1858 if n < m {
1859 return 0.0;
1860 }
1861
1862 let num_patterns = n - m + 1;
1863
1864 if num_patterns > 300 {
1868 return compute_phi_sampled(prices, m, r);
1869 }
1870
1871 let mut count = 0usize;
1873
1874 for i in 0..num_patterns {
1875 let p1 = &prices[i..i + m];
1876 for j in (i + 1)..num_patterns {
1877 let p2 = &prices[j..j + m];
1878
1879 if patterns_within_distance_simd(p1, p2, r, m) {
1881 count += 1;
1882 }
1883 }
1884 }
1885
1886 if count == 0 {
1888 return 0.0;
1889 }
1890
1891 let inv_total_pairs = 2.0 / (num_patterns as f64 * (num_patterns - 1) as f64);
1893 let c = count as f64 * inv_total_pairs;
1894 -c * libm::log(c) }
1896
1897#[inline]
1913pub fn compute_entropy_adaptive(prices: &[f64]) -> f64 {
1914 let n = prices.len();
1915
1916 if n < 500 {
1918 return compute_permutation_entropy(prices);
1919 }
1920
1921 let n_inv = 1.0 / n as f64;
1924 let mean = prices.iter().sum::<f64>() * n_inv;
1925 let variance = prices.iter().map(|p| { let d = p - mean; d * d }).sum::<f64>() * n_inv;
1927 let std = variance.sqrt();
1928 let r = 0.2 * std;
1929
1930 compute_approximate_entropy(prices, 2, r)
1931}
1932
1933#[inline]
1949pub fn compute_entropy_adaptive_cached_readonly(
1950 prices: &[f64],
1951 cache: &EntropyCache,
1952) -> Option<f64> {
1953 let n = prices.len();
1954
1955 if n < 500 {
1957 cache.get(prices)
1958 } else {
1959 None
1961 }
1962}
1963
1964#[inline]
1965pub fn compute_entropy_adaptive_cached(
1966 prices: &[f64],
1967 cache: &mut EntropyCache,
1968) -> f64 {
1969 let n = prices.len();
1970
1971 if n < 500 {
1973 if let Some(cached_entropy) = cache.get(prices) {
1975 return cached_entropy;
1976 }
1977
1978 let entropy = compute_permutation_entropy(prices);
1980 cache.insert(prices, entropy);
1981 return entropy;
1982 }
1983
1984 let n_inv = 1.0 / n as f64;
1987 let mean = prices.iter().sum::<f64>() * n_inv;
1988 let variance = prices.iter().map(|p| { let d = p - mean; d * d }).sum::<f64>() * n_inv;
1990 let std = variance.sqrt();
1991 let r = 0.2 * std;
1992
1993 compute_approximate_entropy(prices, 2, r)
1994}
1995
1996#[cfg(test)]
1997mod approximate_entropy_tests {
1998 use super::*;
1999
2000 #[test]
2001 fn test_apen_deterministic_series() {
2002 let series: Vec<f64> = (0..100).map(|i| (i as f64) * 1.0).collect();
2004 let apen = compute_approximate_entropy(&series, 2, 0.1);
2005 println!("Deterministic series ApEn: {:.4}", apen);
2006 assert!(apen < 0.5, "Regular series should have low entropy");
2007 }
2008
2009 #[test]
2010 fn test_apen_random_series() {
2011 let trades = crate::test_data_loader::load_real_btcusdt_10k().unwrap();
2014 let series: Vec<f64> = trades.iter().take(500).map(|t| t.price.to_f64()).collect();
2015 let mean = series.iter().sum::<f64>() / series.len() as f64;
2016 let variance = series.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / series.len() as f64;
2017 let r = 0.2 * variance.sqrt();
2018 let apen = compute_approximate_entropy(&series, 2, r);
2019 println!("Real BTCUSDT ApEn (500 trades, r={:.4}): {:.4}", r, apen);
2020 assert!(apen >= 0.0 && apen <= 1.0, "ApEn must be in [0,1], got {}", apen);
2021 }
2022
2023 #[test]
2024 fn test_apen_short_series() {
2025 let series = vec![1.0, 2.0];
2027 let apen = compute_approximate_entropy(&series, 2, 0.5);
2028 assert_eq!(apen, 0.0, "Too-short series should return 0");
2029 }
2030
2031 #[test]
2032 fn test_adaptive_entropy_switches_at_threshold() {
2033 let small_series: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
2035 let large_series: Vec<f64> = (0..1000).map(|i| i as f64 * 0.01).collect();
2036
2037 let ent_small = compute_entropy_adaptive(&small_series);
2038 let ent_large = compute_entropy_adaptive(&large_series);
2039
2040 println!("Small series entropy (n=100): {:.4}", ent_small);
2041 println!("Large series entropy (n=1000): {:.4}", ent_large);
2042
2043 assert!(ent_small >= 0.0 && ent_small <= 1.0);
2045 assert!(ent_large >= 0.0 && ent_large <= 1.0);
2046 }
2047}
2048
2049#[cfg(test)]
2050mod entropy_adaptive_apen_tests {
2051 use super::*;
2052
2053 #[test]
2054 fn test_adaptive_entropy_boundary_499_uses_pe() {
2055 let prices: Vec<f64> = (0..499).map(|i| 100.0 + (i as f64 * 0.01)).collect();
2057 let ent = compute_entropy_adaptive(&prices);
2058 assert!(ent >= 0.0 && ent <= 1.0, "PE result should be in [0, 1], got {ent}");
2059 }
2060
2061 #[test]
2062 fn test_adaptive_entropy_boundary_500_uses_apen() {
2063 let prices: Vec<f64> = (0..500).map(|i| 100.0 + (i as f64 * 0.01)).collect();
2065 let ent = compute_entropy_adaptive(&prices);
2066 assert!(ent >= 0.0 && ent <= 1.0, "ApEn result should be in [0, 1], got {ent}");
2067 }
2068
2069 #[test]
2070 fn test_adaptive_entropy_large_trending() {
2071 let prices: Vec<f64> = (0..1000).map(|i| 100.0 + i as f64).collect();
2073 let ent = compute_entropy_adaptive(&prices);
2074 assert!(ent >= 0.0 && ent <= 1.0, "Entropy out of bounds: {ent}");
2075 assert!(ent < 0.5, "Trending series should have low entropy, got {ent}");
2076 }
2077
2078 #[test]
2079 fn test_adaptive_entropy_large_random() {
2080 let mut rng = 42u64;
2082 let prices: Vec<f64> = (0..600)
2083 .map(|_| {
2084 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
2085 100.0 + ((rng >> 33) as f64 / (1u64 << 31) as f64) * 50.0
2086 })
2087 .collect();
2088 let ent = compute_entropy_adaptive(&prices);
2089 assert!(ent >= 0.0 && ent <= 1.0, "Entropy out of bounds: {ent}");
2090 }
2091
2092 #[test]
2093 fn test_adaptive_entropy_cached_large_window_not_cached() {
2094 let prices: Vec<f64> = (0..600).map(|i| 100.0 + (i as f64).sin() * 10.0).collect();
2097 let uncached = compute_entropy_adaptive(&prices);
2098 let mut cache = EntropyCache::new();
2099 let cached = compute_entropy_adaptive_cached(&prices, &mut cache);
2100 assert!(
2101 (uncached - cached).abs() < 1e-10,
2102 "Cached and uncached should match for large windows: {uncached} vs {cached}"
2103 );
2104 }
2105
2106 #[test]
2107 fn test_adaptive_entropy_constant_price_large() {
2108 let prices: Vec<f64> = vec![100.0; 500];
2110 let ent = compute_entropy_adaptive(&prices);
2111 assert!(ent >= 0.0 && ent <= 1.0, "Entropy out of bounds: {ent}");
2112 }
2113}
2114
2115#[cfg(test)]
2116mod hurst_accuracy_tests {
2117 use super::*;
2118
2119 #[test]
2120 fn test_hurst_accuracy_trending() {
2121 let mut prices = vec![0.0; 256];
2123 for i in 0..256 {
2124 prices[i] = i as f64 * 1.0; }
2126
2127 let dfa_h = compute_hurst_dfa(&prices);
2128 let rs_h = opendeviationbar_hurst::rssimple(&prices);
2129
2130 println!("Trending series:");
2131 println!(" DFA H = {:.4}", dfa_h);
2132 println!(" R/S H = {:.4}", rs_h);
2133 println!(" Both > 0.5? DFA={}, RS={}", dfa_h > 0.5, rs_h > 0.5);
2134
2135 assert!(dfa_h > 0.5, "DFA should detect trending");
2137 assert!(rs_h > 0.5, "R/S should detect trending");
2138 }
2139
2140 #[test]
2141 fn test_hurst_accuracy_mean_reverting() {
2142 let mut prices = vec![0.5; 256];
2144 for i in 0..256 {
2145 prices[i] = if i % 2 == 0 { 0.0 } else { 1.0 };
2146 }
2147
2148 let dfa_h = compute_hurst_dfa(&prices);
2149 let rs_h = opendeviationbar_hurst::rssimple(&prices);
2150
2151 println!("Mean-reverting series:");
2152 println!(" DFA H = {:.4}", dfa_h);
2153 println!(" R/S H = {:.4}", rs_h);
2154 println!(" Both < 0.5? DFA={}, RS={}", dfa_h < 0.5, rs_h < 0.5);
2155
2156 assert!(dfa_h < 0.5, "DFA should detect mean-reversion");
2158 assert!(rs_h < 0.5, "R/S should detect mean-reversion");
2159 }
2160
2161 #[test]
2162 fn test_hurst_accuracy_random_walk() {
2163 let mut prices = vec![0.0; 256];
2165 let mut rng = 12345u64;
2166 prices[0] = 0.0;
2167
2168 for i in 1..256 {
2169 rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
2170 let step = if (rng >> 16) & 1 == 0 { 1.0 } else { -1.0 };
2171 prices[i] = prices[i - 1] + step;
2172 }
2173
2174 let dfa_h = compute_hurst_dfa(&prices);
2175 let rs_h = opendeviationbar_hurst::rssimple(&prices);
2176
2177 println!("Random walk series:");
2178 println!(" DFA H = {:.4}", dfa_h);
2179 println!(" R/S H = {:.4}", rs_h);
2180 println!(" Both ≈ 0.5? DFA={:.2}, RS={:.2}", dfa_h, rs_h);
2181 }
2182
2183 #[test]
2187 fn test_hurst_edge_case_empty() {
2188 let prices: Vec<f64> = vec![];
2189 let h = compute_hurst_dfa(&prices);
2190 assert_eq!(h, 0.5, "Empty prices should return neutral (0.5)");
2191 }
2192
2193 #[test]
2194 fn test_hurst_edge_case_insufficient_samples() {
2195 let prices: Vec<f64> = (0..32).map(|i| 100.0 + i as f64).collect();
2197 let h = compute_hurst_dfa(&prices);
2198 assert_eq!(
2199 h, 0.5,
2200 "Less than 64 samples should return neutral (0.5)"
2201 );
2202 }
2203
2204 #[test]
2205 fn test_hurst_edge_case_constant_prices() {
2206 let prices = vec![100.0; 100];
2209 let h = compute_hurst_dfa(&prices);
2210 if !h.is_nan() {
2213 assert!(h >= 0.0 && h <= 1.0, "Hurst should be in [0,1] if not NaN");
2214 }
2215 }
2216
2217 #[test]
2218 fn test_hurst_bounds_stress() {
2219 let scenarios = vec![
2221 ("linear", (0..256).map(|i| 100.0 + i as f64).collect::<Vec<_>>()),
2222 (
2223 "sawtooth",
2224 (0..256)
2225 .map(|i| if i % 2 == 0 { 100.0 } else { 101.0 })
2226 .collect::<Vec<_>>(),
2227 ),
2228 ];
2229
2230 for (name, prices) in scenarios {
2231 let h = compute_hurst_dfa(&prices);
2232 assert!(
2233 h >= 0.0 && h <= 1.0,
2234 "Hurst({}) must be in [0,1], got {}",
2235 name,
2236 h
2237 );
2238 assert!(!h.is_nan(), "Hurst({}) must not be NaN", name);
2239 }
2240 }
2241
2242 #[test]
2243 fn test_garman_klass_edge_case_empty() {
2244 use crate::interbar_types::TradeSnapshot;
2245
2246 let snapshot: Vec<TradeSnapshot> = vec![];
2248 let snapshot_refs: Vec<&TradeSnapshot> = snapshot.iter().collect();
2249 let vol = compute_garman_klass(&snapshot_refs);
2250 assert_eq!(vol, 0.0, "Empty lookback should return 0");
2251 }
2252
2253 #[test]
2254 fn test_garman_klass_edge_case_constant_price() {
2255 use crate::{FixedPoint, interbar_types::TradeSnapshot};
2256
2257 let prices = vec![100.0; 50];
2259 let snapshots: Vec<TradeSnapshot> = prices
2260 .iter()
2261 .enumerate()
2262 .map(|(i, &price)| {
2263 let price_fp =
2264 FixedPoint::from_str(&format!("{:.8}", price)).expect("valid price");
2265 let vol_fp = FixedPoint::from_str("1.00000000").expect("valid volume");
2266 let turnover_f64 = price_fp.to_f64() * vol_fp.to_f64();
2267 TradeSnapshot {
2268 price: price_fp,
2269 volume: vol_fp,
2270 timestamp: 1000 + (i as i64 * 100),
2271 is_buyer_maker: false,
2272 turnover: (turnover_f64 * 1e8) as i128,
2273 }
2274 })
2275 .collect();
2276 let snapshot_refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
2277 let vol = compute_garman_klass(&snapshot_refs);
2278 assert_eq!(vol, 0.0, "Constant price should give 0 volatility");
2279 }
2280
2281 #[test]
2282 fn test_garman_klass_bounds() {
2283 use crate::{FixedPoint, interbar_types::TradeSnapshot};
2284
2285 let prices = vec![100.0, 105.0, 103.0, 108.0, 102.0];
2287 let snapshots: Vec<TradeSnapshot> = prices
2288 .iter()
2289 .enumerate()
2290 .map(|(i, &price)| {
2291 let price_fp =
2292 FixedPoint::from_str(&format!("{:.8}", price)).expect("valid price");
2293 let vol_fp = FixedPoint::from_str("1.00000000").expect("valid volume");
2294 let turnover_f64 = price_fp.to_f64() * vol_fp.to_f64();
2295 TradeSnapshot {
2296 price: price_fp,
2297 volume: vol_fp,
2298 timestamp: 1000 + (i as i64 * 100),
2299 is_buyer_maker: false,
2300 turnover: (turnover_f64 * 1e8) as i128,
2301 }
2302 })
2303 .collect();
2304 let snapshot_refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
2305 let vol = compute_garman_klass(&snapshot_refs);
2306 assert!(vol >= 0.0, "Garman-Klass volatility must be non-negative");
2307 assert!(!vol.is_nan(), "Garman-Klass must not be NaN");
2308 }
2309
2310 #[test]
2311 fn test_permutation_entropy_edge_case_empty() {
2312 let prices: Vec<f64> = vec![];
2313 let entropy = compute_permutation_entropy(&prices);
2314 assert_eq!(
2315 entropy, 1.0,
2316 "Empty prices should return max entropy (1.0)"
2317 );
2318 }
2319
2320 #[test]
2321 fn test_permutation_entropy_edge_case_insufficient_data() {
2322 let prices: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();
2324 let entropy = compute_permutation_entropy(&prices);
2325 assert_eq!(entropy, 0.0, "Monotonic sequence should have zero entropy");
2326 }
2327
2328 #[test]
2329 fn test_permutation_entropy_bounds() {
2330 let prices: Vec<f64> = (0..100).map(|i| 100.0 + (i % 3) as f64).collect();
2332 let entropy = compute_permutation_entropy(&prices);
2333 assert!(
2334 entropy >= 0.0 && entropy <= 1.0,
2335 "Entropy must be in [0,1], got {}",
2336 entropy
2337 );
2338 assert!(!entropy.is_nan(), "Entropy must not be NaN");
2339 }
2340
2341 #[test]
2342 fn test_kaufman_er_edge_case_empty() {
2343 let prices: Vec<f64> = vec![];
2344 let er = compute_kaufman_er(&prices);
2345 assert_eq!(er, 0.0, "Empty prices should give ER=0");
2346 }
2347
2348 #[test]
2349 fn test_kaufman_er_edge_case_constant_prices() {
2350 let prices = vec![100.0; 50];
2351 let er = compute_kaufman_er(&prices);
2352 assert_eq!(er, 0.0, "Constant prices should give ER=0");
2353 }
2354
2355 #[test]
2356 fn test_kaufman_er_bounds() {
2357 let prices: Vec<f64> = (0..100).map(|i| 100.0 + i as f64).collect();
2359 let er = compute_kaufman_er(&prices);
2360 assert!(er >= 0.0 && er <= 1.0, "ER must be in [0,1], got {}", er);
2361 assert!(!er.is_nan(), "ER must not be NaN");
2362 }
2363
2364 #[test]
2365 fn test_ordinal_pattern_index_coverage() {
2366 let test_cases = vec![
2369 (0.0, 1.0, 2.0, 0), (0.0, 2.0, 1.0, 1), (1.0, 0.0, 2.0, 2), (2.0, 0.0, 1.0, 3), (1.0, 2.0, 0.0, 4), (2.0, 1.0, 0.0, 5), ];
2376
2377 for (a, b, c, expected) in test_cases {
2378 let idx = ordinal_pattern_index_m3(a, b, c);
2379 assert_eq!(
2380 idx, expected,
2381 "Pattern ({},{},{}) should map to index {} but got {}",
2382 a, b, c, expected, idx
2383 );
2384 }
2385 }
2386
2387 #[test]
2389 fn test_kyle_lambda_edge_case_empty() {
2390 let kyle_lambda = compute_kyle_lambda(&[]);
2391 assert_eq!(kyle_lambda, 0.0, "Empty lookback should return 0");
2392 }
2393
2394 #[test]
2395 fn test_kyle_lambda_edge_case_single_trade() {
2396 use crate::interbar_types::TradeSnapshot;
2397 let snapshot = TradeSnapshot {
2398 timestamp: 1000000,
2399 price: crate::FixedPoint::from_str("100.0").unwrap(),
2400 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2401 is_buyer_maker: true,
2402 turnover: (100 * 1) as i128 * 100000000i128,
2403 };
2404 let kyle_lambda = compute_kyle_lambda(&[&snapshot]);
2405 assert_eq!(kyle_lambda, 0.0, "Single trade should return 0 (insufficient data)");
2406 }
2407
2408 #[test]
2409 fn test_kyle_lambda_zero_imbalance() {
2410 use crate::interbar_types::TradeSnapshot;
2411 let trades = vec![
2413 TradeSnapshot {
2414 timestamp: 1000000,
2415 price: crate::FixedPoint::from_str("100.0").unwrap(),
2416 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2417 is_buyer_maker: true,
2418 turnover: (100 * 1) as i128 * 100000000i128,
2419 },
2420 TradeSnapshot {
2421 timestamp: 1000100,
2422 price: crate::FixedPoint::from_str("100.5").unwrap(),
2423 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2424 is_buyer_maker: false, turnover: (100 * 1) as i128 * 100000000i128,
2426 },
2427 ];
2428 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2429 let kyle_lambda = compute_kyle_lambda(&refs);
2430 assert_eq!(kyle_lambda, 0.0, "Zero imbalance should return 0");
2431 }
2432
2433 #[test]
2434 fn test_kyle_lambda_positive_trend_buy_pressure() {
2435 use crate::interbar_types::TradeSnapshot;
2436 let trades = vec![
2439 TradeSnapshot {
2440 timestamp: 1000000,
2441 price: crate::FixedPoint::from_str("100.0").unwrap(),
2442 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2443 is_buyer_maker: true, turnover: (100 * 1) as i128 * 100000000i128,
2445 },
2446 TradeSnapshot {
2447 timestamp: 1000100,
2448 price: crate::FixedPoint::from_str("101.0").unwrap(),
2449 volume: crate::FixedPoint::from_str("10.0").unwrap(),
2450 is_buyer_maker: false, turnover: (101 * 10) as i128 * 100000000i128,
2452 },
2453 ];
2454 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2455 let kyle_lambda = compute_kyle_lambda(&refs);
2456 assert!(kyle_lambda > 0.0, "Buy pressure with price increase should give positive kyle_lambda, got {}", kyle_lambda);
2458 }
2459
2460 #[test]
2461 fn test_kyle_lambda_bounded() {
2462 use crate::interbar_types::TradeSnapshot;
2463 for _i in 0..10 {
2465 let trades = vec![
2466 TradeSnapshot {
2467 timestamp: 1000000,
2468 price: crate::FixedPoint::from_str("100.0").unwrap(),
2469 volume: crate::FixedPoint::from_str("5.0").unwrap(),
2470 is_buyer_maker: true,
2471 turnover: (100 * 5) as i128 * 100000000i128,
2472 },
2473 TradeSnapshot {
2474 timestamp: 1000100,
2475 price: crate::FixedPoint::from_str("105.0").unwrap(),
2476 volume: crate::FixedPoint::from_str("2.0").unwrap(),
2477 is_buyer_maker: false,
2478 turnover: (105 * 2) as i128 * 100000000i128,
2479 },
2480 ];
2481 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2482 let kyle_lambda = compute_kyle_lambda(&refs);
2483 assert!(kyle_lambda.is_finite(), "Kyle lambda must be finite, got {}", kyle_lambda);
2484 }
2485 }
2486
2487 #[test]
2489 fn test_burstiness_edge_case_empty() {
2490 let burstiness = compute_burstiness(&[]);
2491 assert_eq!(burstiness, 0.0, "Empty lookback should return 0");
2492 }
2493
2494 #[test]
2495 fn test_burstiness_single_trade() {
2496 use crate::interbar_types::TradeSnapshot;
2497 let snapshot = TradeSnapshot {
2498 timestamp: 1000000,
2499 price: crate::FixedPoint::from_str("100.0").unwrap(),
2500 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2501 is_buyer_maker: true,
2502 turnover: (100 * 1) as i128 * 100000000i128,
2503 };
2504 let burstiness = compute_burstiness(&[&snapshot]);
2505 assert_eq!(burstiness, 0.0, "Single trade should return 0 (insufficient data)");
2506 }
2507
2508 #[test]
2509 fn test_burstiness_bounds() {
2510 use crate::interbar_types::TradeSnapshot;
2511 let mut trades = Vec::with_capacity(20);
2513 for i in 0..20 {
2514 trades.push(TradeSnapshot {
2515 timestamp: 1000000 + (i * 100) as i64,
2516 price: crate::FixedPoint::from_str("100.0").unwrap(),
2517 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2518 is_buyer_maker: i % 2 == 0,
2519 turnover: (100 * 1) as i128 * 100000000i128,
2520 });
2521 }
2522 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2523 let burstiness = compute_burstiness(&refs);
2524 assert!(burstiness >= -1.0 && burstiness <= 1.0, "Burstiness must be in [-1, 1], got {}", burstiness);
2525 }
2526
2527 #[test]
2529 fn test_kaufman_er_trending_market() {
2530 let mut prices = Vec::with_capacity(50);
2532 let mut price = 100.0;
2533 for _ in 0..50 {
2534 price += 0.1; prices.push(price);
2536 }
2537 let er = compute_kaufman_er(&prices);
2538 assert!(er > 0.5, "Strong trending market should have high efficiency ratio, got {}", er);
2539 }
2540
2541 #[test]
2542 fn test_kaufman_er_ranging_market() {
2543 let mut prices = Vec::with_capacity(50);
2545 for i in 0..50 {
2546 let price = 100.0 + if (i % 2) == 0 { 0.1 } else { -0.1 };
2547 prices.push(price);
2548 }
2549 let er = compute_kaufman_er(&prices);
2550 assert!(er < 0.3, "Ranging market should have low efficiency ratio, got {}", er);
2551 }
2552
2553 #[test]
2557 fn test_kyle_lambda_negative_trend_sell_pressure() {
2558 use crate::interbar_types::TradeSnapshot;
2559 let trades = vec![
2561 TradeSnapshot {
2562 timestamp: 1000000,
2563 price: crate::FixedPoint::from_str("101.0").unwrap(),
2564 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2565 is_buyer_maker: false, turnover: (101 * 1) as i128 * 100000000i128,
2567 },
2568 TradeSnapshot {
2569 timestamp: 1000100,
2570 price: crate::FixedPoint::from_str("100.0").unwrap(),
2571 volume: crate::FixedPoint::from_str("10.0").unwrap(),
2572 is_buyer_maker: true, turnover: (100 * 10) as i128 * 100000000i128,
2574 },
2575 ];
2576 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2577 let kyle_lambda = compute_kyle_lambda(&refs);
2578 assert!(kyle_lambda > 0.0, "Sell pressure with price decrease should give positive kyle_lambda");
2581 }
2582
2583 #[test]
2584 fn test_kyle_lambda_zero_price_movement() {
2585 use crate::interbar_types::TradeSnapshot;
2586 let trades = vec![
2588 TradeSnapshot {
2589 timestamp: 1000000,
2590 price: crate::FixedPoint::from_str("100.0").unwrap(),
2591 volume: crate::FixedPoint::from_str("5.0").unwrap(),
2592 is_buyer_maker: false, turnover: (100 * 5) as i128 * 100000000i128,
2594 },
2595 TradeSnapshot {
2596 timestamp: 1000100,
2597 price: crate::FixedPoint::from_str("100.0").unwrap(),
2598 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2599 is_buyer_maker: true, turnover: (100 * 1) as i128 * 100000000i128,
2601 },
2602 ];
2603 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2604 let kyle_lambda = compute_kyle_lambda(&refs);
2605 assert_eq!(kyle_lambda, 0.0, "Zero price movement should give 0");
2607 }
2608
2609 #[test]
2610 fn test_kyle_lambda_tiny_prices() {
2611 use crate::interbar_types::TradeSnapshot;
2612 let trades = vec![
2614 TradeSnapshot {
2615 timestamp: 1000000,
2616 price: crate::FixedPoint::from_str("0.001").unwrap(),
2617 volume: crate::FixedPoint::from_str("100000.0").unwrap(),
2618 is_buyer_maker: true,
2619 turnover: (1 * 100000) as i128 * 100000000i128,
2620 },
2621 TradeSnapshot {
2622 timestamp: 1000100,
2623 price: crate::FixedPoint::from_str("0.002").unwrap(),
2624 volume: crate::FixedPoint::from_str("50000.0").unwrap(),
2625 is_buyer_maker: false,
2626 turnover: (2 * 50000) as i128 * 100000000i128,
2627 },
2628 ];
2629 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2630 let kyle_lambda = compute_kyle_lambda(&refs);
2631 assert!(kyle_lambda.is_finite(), "Should handle tiny prices without NaN/Inf");
2632 }
2633
2634 #[test]
2635 fn test_kyle_lambda_opposing_flows() {
2636 use crate::interbar_types::TradeSnapshot;
2637 let trades = vec![
2639 TradeSnapshot {
2640 timestamp: 1000000,
2641 price: crate::FixedPoint::from_str("100.0").unwrap(),
2642 volume: crate::FixedPoint::from_str("10.0").unwrap(),
2643 is_buyer_maker: false, turnover: (100 * 10) as i128 * 100000000i128,
2645 },
2646 TradeSnapshot {
2647 timestamp: 1000100,
2648 price: crate::FixedPoint::from_str("99.0").unwrap(),
2649 volume: crate::FixedPoint::from_str("5.0").unwrap(),
2650 is_buyer_maker: true, turnover: (99 * 5) as i128 * 100000000i128,
2652 },
2653 ];
2654 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2655 let kyle_lambda = compute_kyle_lambda(&refs);
2656 assert!(kyle_lambda < 0.0, "Price moving against order flow should give negative kyle_lambda");
2658 }
2659
2660 #[test]
2662 fn test_burstiness_clustered_arrivals() {
2663 use crate::interbar_types::TradeSnapshot;
2664 let mut trades = Vec::with_capacity(15);
2666 for i in 0..10 {
2668 trades.push(TradeSnapshot {
2669 timestamp: 1000000 + (i * 10) as i64,
2670 price: crate::FixedPoint::from_str("100.0").unwrap(),
2671 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2672 is_buyer_maker: i % 2 == 0,
2673 turnover: (100 * 1) as i128 * 100000000i128,
2674 });
2675 }
2676 for i in 0..5 {
2678 trades.push(TradeSnapshot {
2679 timestamp: 1000100 + 1000 + (i * 10) as i64,
2680 price: crate::FixedPoint::from_str("100.0").unwrap(),
2681 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2682 is_buyer_maker: i % 2 == 0,
2683 turnover: (100 * 1) as i128 * 100000000i128,
2684 });
2685 }
2686 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2687 let burstiness = compute_burstiness(&refs);
2688 assert!(burstiness > 0.0, "Clustered arrivals should have positive burstiness, got {}", burstiness);
2690 assert!(burstiness <= 1.0, "Burstiness should be bounded by 1.0");
2691 }
2692
2693 #[test]
2694 fn test_burstiness_perfectly_regular() {
2695 use crate::interbar_types::TradeSnapshot;
2696 let mut trades = Vec::with_capacity(20);
2698 for i in 0..20 {
2699 trades.push(TradeSnapshot {
2700 timestamp: 1000000 + (i * 100) as i64,
2701 price: crate::FixedPoint::from_str("100.0").unwrap(),
2702 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2703 is_buyer_maker: i % 2 == 0,
2704 turnover: (100 * 1) as i128 * 100000000i128,
2705 });
2706 }
2707 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2708 let burstiness = compute_burstiness(&refs);
2709 assert!(burstiness < 0.0, "Regular periodic arrivals should have negative burstiness, got {}", burstiness);
2711 }
2712
2713 #[test]
2714 fn test_burstiness_extreme_gap() {
2715 use crate::interbar_types::TradeSnapshot;
2716 let mut trades = Vec::with_capacity(5);
2718 for i in 0..5 {
2720 trades.push(TradeSnapshot {
2721 timestamp: 1000000 + (i as i64),
2722 price: crate::FixedPoint::from_str("100.0").unwrap(),
2723 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2724 is_buyer_maker: i % 2 == 0,
2725 turnover: (100 * 1) as i128 * 100000000i128,
2726 });
2727 }
2728 trades.push(TradeSnapshot {
2730 timestamp: 1000000 + 100000,
2731 price: crate::FixedPoint::from_str("100.0").unwrap(),
2732 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2733 is_buyer_maker: false,
2734 turnover: (100 * 1) as i128 * 100000000i128,
2735 });
2736 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2737 let burstiness = compute_burstiness(&refs);
2738 assert!(burstiness > 0.0, "Extreme gap should produce positive burstiness");
2740 assert!(burstiness <= 1.0, "Burstiness should be bounded");
2741 }
2742
2743 #[test]
2745 fn test_garman_klass_high_volatility() {
2746 use crate::{FixedPoint, interbar_types::TradeSnapshot};
2747 let prices = vec![100.0, 150.0, 120.0, 180.0, 110.0];
2749 let snapshots: Vec<TradeSnapshot> = prices
2750 .iter()
2751 .enumerate()
2752 .map(|(i, &price)| {
2753 let price_fp = FixedPoint::from_str(&format!("{:.8}", price)).expect("valid price");
2754 let vol_fp = FixedPoint::from_str("1.00000000").expect("valid volume");
2755 let turnover_f64 = price_fp.to_f64() * vol_fp.to_f64();
2756 TradeSnapshot {
2757 price: price_fp,
2758 volume: vol_fp,
2759 timestamp: 1000 + (i as i64 * 100),
2760 is_buyer_maker: false,
2761 turnover: (turnover_f64 * 1e8) as i128,
2762 }
2763 })
2764 .collect();
2765 let snapshot_refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
2766 let vol = compute_garman_klass(&snapshot_refs);
2767 assert!(vol > 0.0, "High volatility scenario should produce non-zero volatility");
2768 assert!(!vol.is_nan(), "Garman-Klass must not be NaN");
2769 }
2770
2771 #[test]
2772 fn test_garman_klass_extreme_ohlc_ratios() {
2773 use crate::{FixedPoint, interbar_types::TradeSnapshot};
2774 let prices = vec![100.0, 1000.0, 200.0]; let snapshots: Vec<TradeSnapshot> = prices
2777 .iter()
2778 .enumerate()
2779 .map(|(i, &price)| {
2780 let price_fp = FixedPoint::from_str(&format!("{:.8}", price)).expect("valid price");
2781 let vol_fp = FixedPoint::from_str("1.00000000").expect("valid volume");
2782 let turnover_f64 = price_fp.to_f64() * vol_fp.to_f64();
2783 TradeSnapshot {
2784 price: price_fp,
2785 volume: vol_fp,
2786 timestamp: 1000 + (i as i64 * 100),
2787 is_buyer_maker: false,
2788 turnover: (turnover_f64 * 1e8) as i128,
2789 }
2790 })
2791 .collect();
2792 let snapshot_refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
2793 let vol = compute_garman_klass(&snapshot_refs);
2794 assert!(vol >= 0.0, "Garman-Klass must be non-negative");
2796 assert!(vol.is_finite(), "Garman-Klass must be finite");
2797 }
2798
2799 #[test]
2801 fn test_permutation_entropy_deterministic_pattern() {
2802 let prices: Vec<f64> = (0..100).map(|i| i as f64).collect();
2804 let entropy = compute_permutation_entropy(&prices);
2805 assert!(entropy >= 0.0 && entropy <= 1.0, "Entropy must be in [0,1]");
2807 }
2808
2809 #[test]
2810 fn test_permutation_entropy_oscillating_pattern() {
2811 let mut prices = Vec::with_capacity(100);
2813 for i in 0..100 {
2814 prices.push(if i % 3 == 0 { 100.0 } else if i % 3 == 1 { 101.0 } else { 99.0 });
2815 }
2816 let entropy = compute_permutation_entropy(&prices);
2817 assert!(entropy >= 0.0 && entropy <= 1.0, "Entropy must be in [0,1]");
2819 assert!(!entropy.is_nan(), "Entropy must not be NaN");
2820 }
2821
2822 #[test]
2824 fn test_kaufman_er_single_large_move() {
2825 let mut prices = Vec::with_capacity(50);
2827 for i in 0..50 {
2828 prices.push(100.0 + i as f64); }
2830 let er = compute_kaufman_er(&prices);
2831 assert!(er > 0.9, "Perfect trend should have ER > 0.9, got {}", er);
2833 }
2834
2835 #[test]
2836 fn test_kaufman_er_noise_dominated() {
2837 let mut prices = Vec::new();
2839 let mut rng = 12345u64;
2840 prices.push(100.0);
2841 for _ in 1..100 {
2842 rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
2843 let noise = ((rng >> 16) as f64 % 200.0) - 100.0; let new_price = prices.last().unwrap() + noise * 0.0001; prices.push(new_price);
2846 }
2847 let er = compute_kaufman_er(&prices);
2848 assert!(er < 0.5, "Noise-dominated market should have ER < 0.5, got {}", er);
2850 assert!(!er.is_nan(), "ER must be finite");
2851 }
2852
2853 #[test]
2855 fn test_hurst_strong_reverting_pattern() {
2856 let mut prices = vec![100.0; 200];
2858 for i in 0..200 {
2859 prices[i] = if i % 2 == 0 { 99.0 } else { 101.0 };
2860 }
2861 let h = compute_hurst_dfa(&prices);
2862 assert!(h < 0.5, "Strong mean reverting should have H < 0.5, got {}", h);
2863 assert!(h.is_finite(), "Hurst must be finite");
2864 }
2865
2866 #[test]
2867 fn test_hurst_extreme_volatility() {
2868 let mut prices = vec![100.0; 200];
2870 for i in 0..200 {
2871 prices[i] = match i % 4 {
2872 0 => 100.0,
2873 1 => 200.0, 2 => 150.0,
2875 _ => 50.0, };
2877 }
2878 let h = compute_hurst_dfa(&prices);
2879 assert!(h >= 0.0 && h <= 1.0, "Hurst must be in [0,1] even for extreme volatility");
2880 }
2881
2882 #[test]
2884 fn test_volume_moments_constant_volume() {
2885 use crate::interbar_types::TradeSnapshot;
2886 let trades: Vec<TradeSnapshot> = (0..20)
2888 .map(|i| TradeSnapshot {
2889 timestamp: 1000000 + (i as i64 * 100),
2890 price: crate::FixedPoint::from_str("100.0").unwrap(),
2891 volume: crate::FixedPoint::from_str("1.0").unwrap(),
2892 is_buyer_maker: i % 2 == 0,
2893 turnover: (100 * 1) as i128 * 100000000i128,
2894 })
2895 .collect();
2896 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2897 let (skew, kurt) = compute_volume_moments(&refs);
2898 assert_eq!(skew, 0.0, "Constant volume should have zero skewness");
2899 assert_eq!(kurt, 0.0, "Constant volume should have zero kurtosis");
2900 }
2901
2902 #[test]
2903 fn test_volume_moments_right_skewed() {
2904 use crate::interbar_types::TradeSnapshot;
2905 let volumes = vec![1.0, 1.0, 1.0, 1.0, 100.0]; let trades: Vec<TradeSnapshot> = volumes
2908 .iter()
2909 .enumerate()
2910 .map(|(i, &vol)| TradeSnapshot {
2911 timestamp: 1000000 + (i as i64 * 100),
2912 price: crate::FixedPoint::from_str("100.0").unwrap(),
2913 volume: crate::FixedPoint::from_str(&format!("{:.8}", vol)).unwrap(),
2914 is_buyer_maker: i % 2 == 0,
2915 turnover: (100.0 * vol * 1e8) as i128,
2916 })
2917 .collect();
2918 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2919 let (skew, _kurt) = compute_volume_moments(&refs);
2920 assert!(skew > 0.0, "Right-skewed volume should have positive skewness, got {}", skew);
2922 }
2923
2924 #[test]
2925 fn test_volume_moments_heavy_tails() {
2926 use crate::interbar_types::TradeSnapshot;
2927 let mut volumes = vec![1.0; 18]; volumes.push(100.0); volumes.push(100.0); let trades: Vec<TradeSnapshot> = volumes
2933 .iter()
2934 .enumerate()
2935 .map(|(i, &vol)| TradeSnapshot {
2936 timestamp: 1000000 + (i as i64 * 100),
2937 price: crate::FixedPoint::from_str("100.0").unwrap(),
2938 volume: crate::FixedPoint::from_str(&format!("{:.8}", vol)).unwrap(),
2939 is_buyer_maker: i % 2 == 0,
2940 turnover: (100.0 * vol * 1e8) as i128,
2941 })
2942 .collect();
2943 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
2944 let (_skew, kurt) = compute_volume_moments(&refs);
2945 assert!(kurt > 0.0, "Heavy-tailed distribution should have positive kurtosis, got {}", kurt);
2947 }
2948
2949 #[test]
2951 fn test_ordinal_pattern_equal_values() {
2952 let test_cases = vec![
2955 (1.0, 1.0, 2.0), (1.0, 2.0, 2.0), (1.0, 1.0, 1.0), (2.0, 2.0, 1.0), ];
2960 for (a, b, c) in test_cases {
2961 let idx = ordinal_pattern_index_m3(a, b, c);
2962 assert!(idx < 6, "Pattern index must be < 6, got {}", idx);
2964 }
2965 }
2966
2967 #[test]
2971 fn test_adaptive_permutation_entropy_m2_small_window() {
2972 let trades = crate::test_data_loader::load_real_btcusdt_10k().unwrap();
2975 let prices: Vec<f64> = trades.iter().take(15).map(|t| t.price.to_f64()).collect();
2976 let entropy = compute_permutation_entropy(&prices);
2977 assert!(entropy >= 0.0 && entropy <= 1.0, "Entropy should be normalized [0,1]");
2978 assert!(entropy < 1.0, "M=2 adaptive path should return meaningful entropy for real data, got {}", entropy);
2980 }
2981
2982 #[test]
2983 fn test_adaptive_permutation_entropy_m2_deterministic() {
2984 let prices: Vec<f64> = (0..15).map(|i| i as f64).collect();
2986 let entropy = compute_permutation_entropy(&prices);
2987 assert!(entropy < 0.3, "Monotonic sequence should have low entropy, got {}", entropy);
2988 }
2989
2990 #[test]
2991 fn test_adaptive_permutation_entropy_m2_m3_transition() {
2992 let mut prices: Vec<f64> = (0..20).map(|i| (i as f64 * 0.5).sin()).collect();
2994 let entropy_boundary = compute_permutation_entropy(&prices);
2995
2996 prices.push(21.0);
2997 let entropy_m3 = compute_permutation_entropy(&prices);
2998
2999 assert!(entropy_boundary >= 0.0 && entropy_boundary <= 1.0);
3001 assert!(entropy_m3 >= 0.0 && entropy_m3 <= 1.0);
3002 }
3003
3004 #[test]
3005 fn test_adaptive_permutation_entropy_insufficient_data() {
3006 let prices = vec![1.0, 2.0];
3008 let entropy = compute_permutation_entropy(&prices);
3009 assert_eq!(entropy, 1.0, "Insufficient data should return max entropy");
3010 }
3011
3012 #[test]
3014 fn test_simd_entropy_16_pattern_boundary() {
3015 let prices: Vec<f64> = (0..18).map(|i| 100.0 + (i as f64 * 0.1)).collect();
3017 let entropy = compute_permutation_entropy(&prices);
3018 assert!(
3019 entropy >= 0.0 && entropy <= 1.0,
3020 "Entropy at 16-pattern boundary should be in [0,1], got {}",
3021 entropy
3022 );
3023 assert!(
3024 !entropy.is_nan(),
3025 "Entropy must not be NaN at 16-pattern boundary"
3026 );
3027 }
3028
3029 #[test]
3030 fn test_simd_entropy_32_pattern_boundary() {
3031 let prices: Vec<f64> = (0..34).map(|i| 100.0 + (i as f64 * 0.05)).collect();
3033 let entropy = compute_permutation_entropy(&prices);
3034 assert!(
3035 entropy >= 0.0 && entropy <= 1.0,
3036 "Entropy at 32-pattern boundary should be in [0,1], got {}",
3037 entropy
3038 );
3039 assert!(!entropy.is_nan(), "Entropy must not be NaN at 32-pattern boundary");
3040 }
3041
3042 #[test]
3043 fn test_simd_entropy_100_mixed_pattern() {
3044 let prices: Vec<f64> = (0..100)
3046 .map(|i| 100.0 + ((i as f64).sin() * 10.0))
3047 .collect();
3048 let entropy = compute_permutation_entropy(&prices);
3049 assert!(
3050 entropy >= 0.0 && entropy <= 1.0,
3051 "Mixed pattern entropy should be in [0,1], got {}",
3052 entropy
3053 );
3054 assert!(
3055 entropy > 0.3,
3056 "Mixed pattern should have non-trivial entropy, got {}",
3057 entropy
3058 );
3059 assert!(!entropy.is_nan(), "Entropy must not be NaN for mixed pattern");
3060 }
3061
3062 #[test]
3063 fn test_simd_entropy_500_large_lookback() {
3064 let prices: Vec<f64> = (0..500)
3066 .map(|i| 100.0 + ((i as f64 * 0.1).sin() * 5.0))
3067 .collect();
3068 let entropy = compute_permutation_entropy(&prices);
3069 assert!(
3070 entropy >= 0.0 && entropy <= 1.0,
3071 "Large lookback entropy should be in [0,1], got {}",
3072 entropy
3073 );
3074 assert!(
3075 !entropy.is_nan(),
3076 "Entropy must not be NaN for 500-element lookback"
3077 );
3078 }
3079
3080 #[test]
3081 fn test_simd_entropy_alternating_pattern() {
3082 let mut prices = Vec::new();
3084 for i in 0..50 {
3085 if i % 2 == 0 {
3086 prices.push(100.0);
3087 } else {
3088 prices.push(101.0);
3089 }
3090 }
3091 let entropy = compute_permutation_entropy(&prices);
3092 assert!(
3093 entropy >= 0.0 && entropy <= 1.0,
3094 "Alternating pattern entropy should be in [0,1], got {}",
3095 entropy
3096 );
3097 assert!(
3098 entropy < 0.5,
3099 "Alternating pattern should have low entropy, got {}",
3100 entropy
3101 );
3102 }
3103
3104 #[test]
3105 fn test_simd_entropy_monotonic_increasing() {
3106 let prices: Vec<f64> = (0..100).map(|i| i as f64).collect();
3108 let entropy = compute_permutation_entropy(&prices);
3109 assert_eq!(entropy, 0.0, "Monotonic increasing should yield zero entropy");
3110 }
3111
3112 #[test]
3113 fn test_simd_entropy_monotonic_decreasing() {
3114 let prices: Vec<f64> = (0..100).map(|i| 100.0 - i as f64).collect();
3116 let entropy = compute_permutation_entropy(&prices);
3117 assert_eq!(entropy, 0.0, "Monotonic decreasing should yield zero entropy");
3118 }
3119
3120 #[test]
3121 fn test_simd_entropy_noise_pattern() {
3122 let prices: Vec<f64> = (0..200)
3124 .map(|i| {
3125 let angle = (i as f64) * std::f64::consts::PI / 32.0;
3126 100.0 + angle.sin() * 5.0 + (i % 7) as f64 * 0.3
3127 })
3128 .collect();
3129 let entropy = compute_permutation_entropy(&prices);
3130 assert!(
3131 entropy >= 0.0 && entropy <= 1.0,
3132 "Noisy pattern entropy should be in [0,1], got {}",
3133 entropy
3134 );
3135 assert!(!entropy.is_nan(), "Entropy must not be NaN for noisy pattern");
3136 }
3137
3138 #[test]
3139 fn test_simd_entropy_edge_case_15_patterns() {
3140 let prices: Vec<f64> = (0..17).map(|i| 100.0 + (i as f64 * 0.2)).collect();
3142 let entropy = compute_permutation_entropy(&prices);
3143 assert!(
3144 entropy >= 0.0 && entropy <= 1.0,
3145 "15-pattern entropy should be in [0,1], got {}",
3146 entropy
3147 );
3148 }
3149
3150 #[test]
3151 fn test_simd_entropy_edge_case_17_patterns() {
3152 let prices: Vec<f64> = (0..19).map(|i| 100.0 + (i as f64 * 0.15)).collect();
3154 let entropy = compute_permutation_entropy(&prices);
3155 assert!(
3156 entropy >= 0.0 && entropy <= 1.0,
3157 "17-pattern entropy should be in [0,1], got {}",
3158 entropy
3159 );
3160 }
3161
3162 #[test]
3164 fn test_kyle_lambda_zero_imbalance_extended() {
3165 use crate::interbar_types::TradeSnapshot;
3166 let trades: Vec<TradeSnapshot> = vec![
3168 TradeSnapshot {
3169 timestamp: 1000,
3170 price: crate::FixedPoint::from_str("100.0").unwrap(),
3171 volume: crate::FixedPoint::from_str("10.0").unwrap(),
3172 is_buyer_maker: true,
3173 turnover: 1_000_000_000i128,
3174 },
3175 TradeSnapshot {
3176 timestamp: 2000,
3177 price: crate::FixedPoint::from_str("101.0").unwrap(),
3178 volume: crate::FixedPoint::from_str("10.0").unwrap(),
3179 is_buyer_maker: false,
3180 turnover: 1_010_000_000i128,
3181 },
3182 ];
3183 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3184 let lambda = compute_kyle_lambda(&refs);
3185 assert_eq!(lambda, 0.0, "Zero imbalance should yield zero lambda");
3186 }
3187
3188 #[test]
3189 fn test_kyle_lambda_strong_buy_pressure_extended() {
3190 use crate::interbar_types::TradeSnapshot;
3191 let trades = crate::test_data_loader::load_real_btcusdt_10k().unwrap();
3193 let snapshots: Vec<TradeSnapshot> = trades.iter().take(200).map(TradeSnapshot::from).collect();
3194 let refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
3195 let lambda = compute_kyle_lambda(&refs);
3196 assert!(lambda.is_finite(), "Kyle lambda must be finite, got {}", lambda);
3197 assert!(lambda != 0.0, "Real market data should yield non-zero lambda");
3199 }
3200
3201 #[test]
3203 fn test_burstiness_regular_arrivals_extended() {
3204 use crate::interbar_types::TradeSnapshot;
3205 let trades = crate::test_data_loader::load_real_btcusdt_10k().unwrap();
3207 let snapshots: Vec<TradeSnapshot> = trades.iter().take(200).map(TradeSnapshot::from).collect();
3208 let refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
3209 let burst = compute_burstiness(&refs);
3210 assert!(burst.is_finite(), "Burstiness must be finite, got {}", burst);
3211 assert!(burst >= -1.0 && burst <= 1.0, "Burstiness out of range, got {}", burst);
3213 }
3214
3215 #[test]
3216 fn test_burstiness_clustered_arrivals_extended() {
3217 use crate::interbar_types::TradeSnapshot;
3218 let timestamp = 1000i64;
3220 let trades: Vec<TradeSnapshot> = (0..20)
3221 .map(|i| {
3222 let ts = if i < 10 {
3223 timestamp + (i as i64 * 100) } else {
3225 timestamp + 1_000_000 + ((i - 10) as i64 * 100) };
3227 TradeSnapshot {
3228 timestamp: ts,
3229 price: crate::FixedPoint::from_str("100.0").unwrap(),
3230 volume: crate::FixedPoint::from_str("1.0").unwrap(),
3231 is_buyer_maker: i % 2 == 0,
3232 turnover: 100_000_000i128,
3233 }
3234 })
3235 .collect();
3236 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3237 let burst = compute_burstiness(&refs);
3238 assert!(burst > 0.3, "Clustered arrivals should have high burstiness, got {}", burst);
3239 }
3240
3241 #[test]
3243 fn test_hurst_soft_clamp_boundary_extended() {
3244 assert!(soft_clamp_hurst(0.0) >= 0.0 && soft_clamp_hurst(0.0) <= 1.0);
3246 assert!(soft_clamp_hurst(1.0) >= 0.0 && soft_clamp_hurst(1.0) <= 1.0);
3247 assert!(soft_clamp_hurst(2.0) >= 0.0 && soft_clamp_hurst(2.0) <= 1.0);
3248 assert!(soft_clamp_hurst(-10.0) >= 0.0 && soft_clamp_hurst(-10.0) <= 1.0);
3250 }
3251
3252 #[test]
3253 fn test_hurst_monotonicity_extended() {
3254 let trending: Vec<f64> = (0..256).map(|i| i as f64).collect();
3256 let mean_reverting = vec![0.5; 256];
3257
3258 let h_trending = compute_hurst_dfa(&trending);
3259 let h_mean_revert = compute_hurst_dfa(&mean_reverting);
3260
3261 assert!(h_trending > h_mean_revert, "Trending should have higher H than mean-reverting");
3263 }
3264
3265 #[test]
3267 fn test_feature_consistency_normal_market_extended() {
3268 use crate::interbar_types::TradeSnapshot;
3269 let trades = crate::test_data_loader::load_real_btcusdt_10k().unwrap();
3271 let snapshots: Vec<TradeSnapshot> = trades.iter().take(200).map(TradeSnapshot::from).collect();
3272 let refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
3273
3274 let kyle = compute_kyle_lambda(&refs);
3276 let burst = compute_burstiness(&refs);
3277 let (skew, kurt) = compute_volume_moments(&refs);
3278
3279 assert!(kyle.is_finite(), "Kyle lambda must be finite");
3280 assert!(burst.is_finite(), "Burstiness must be finite");
3281 assert!(skew.is_finite(), "Skewness must be finite");
3282 assert!(kurt.is_finite(), "Kurtosis must be finite");
3283 }
3284
3285 #[test]
3288 fn test_entropy_cache_hit() {
3289 let prices = vec![1.0, 2.0, 1.5, 3.0, 2.5, 1.0, 2.0, 1.5, 3.0, 2.5];
3291 let mut cache = EntropyCache::new();
3292
3293 let entropy1 = compute_entropy_adaptive_cached(&prices, &mut cache);
3295
3296 let entropy2 = compute_entropy_adaptive_cached(&prices, &mut cache);
3298
3299 assert_eq!(entropy1, entropy2, "Cached value should match original");
3301 }
3302
3303 #[test]
3304 fn test_entropy_cache_different_sequences() {
3305 let prices1: Vec<f64> = (0..100).map(|i| 100.0 + (i as f64).sin()).collect();
3308 let prices2: Vec<f64> = (0..100).map(|i| 100.0 + (i as f64).cos()).collect();
3309
3310 let mut cache = EntropyCache::new();
3311
3312 let entropy1 = compute_entropy_adaptive_cached(&prices1, &mut cache);
3313 let entropy2 = compute_entropy_adaptive_cached(&prices2, &mut cache);
3314
3315 assert!(entropy1.is_finite(), "Entropy1 must be finite");
3317 assert!(entropy2.is_finite(), "Entropy2 must be finite");
3318 assert!(entropy1 >= 0.0, "Entropy1 must be non-negative");
3319 assert!(entropy2 >= 0.0, "Entropy2 must be non-negative");
3320 }
3321
3322 #[test]
3323 fn test_entropy_cache_vs_uncached() {
3324 let prices: Vec<f64> = (0..100)
3326 .map(|i| 100.0 + (i as f64 * 0.5).sin() * 10.0)
3327 .collect();
3328
3329 let mut cache = EntropyCache::new();
3330
3331 let entropy_cached = compute_entropy_adaptive_cached(&prices, &mut cache);
3332 let entropy_uncached = compute_entropy_adaptive(&prices);
3333
3334 assert_eq!(entropy_cached, entropy_uncached, "Cached and uncached must produce identical results");
3336 }
3337
3338 #[test]
3341 fn test_entropy_cache_with_capacity() {
3342 let mut cache = EntropyCache::with_capacity(512);
3344 let prices = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3345
3346 let entropy = compute_entropy_adaptive(&prices);
3348 cache.insert(&prices, entropy);
3349 let cached = cache.get(&prices);
3350 assert_eq!(cached, Some(entropy), "with_capacity cache should store and retrieve");
3351 }
3352
3353 #[test]
3354 fn test_entropy_cache_with_capacity_zero() {
3355 let cache = EntropyCache::with_capacity(0);
3357 let (hits, misses, ratio) = cache.metrics();
3358 assert_eq!((hits, misses), (0, 0));
3359 assert_eq!(ratio, 0.0);
3360 }
3361
3362 #[test]
3363 fn test_entropy_cache_metrics_tracking() {
3364 let mut cache = EntropyCache::new();
3365
3366 let (h, m, ratio) = cache.metrics();
3368 assert_eq!((h, m), (0, 0), "initial metrics should be zero");
3369 assert_eq!(ratio, 0.0, "initial ratio should be 0.0");
3370
3371 let prices_a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
3373 assert!(cache.get(&prices_a).is_none());
3374 let (h, m, ratio) = cache.metrics();
3375 assert_eq!((h, m), (0, 1), "should have 1 miss");
3376 assert_eq!(ratio, 0.0, "0 hits / 1 total = 0%");
3377
3378 cache.insert(&prices_a, 0.42);
3380 assert!(cache.get(&prices_a).is_some());
3381 let (h, m, ratio) = cache.metrics();
3382 assert_eq!((h, m), (1, 1), "should have 1 hit + 1 miss");
3383 assert!((ratio - 50.0).abs() < 0.01, "1/2 = 50%, got {ratio}");
3384 }
3385
3386 #[test]
3387 fn test_entropy_cache_reset_metrics() {
3388 let mut cache = EntropyCache::new();
3389
3390 let prices = vec![10.0, 20.0, 30.0, 40.0, 50.0];
3392 cache.get(&prices); cache.insert(&prices, 0.5);
3394 cache.get(&prices); let (h, m, _) = cache.metrics();
3397 assert!(h + m > 0, "should have accumulated metrics");
3398
3399 cache.reset_metrics();
3401 let (h, m, ratio) = cache.metrics();
3402 assert_eq!((h, m), (0, 0), "reset should clear all counters");
3403 assert_eq!(ratio, 0.0, "reset ratio should be 0.0");
3404
3405 cache.get(&prices); let (h, m, ratio) = cache.metrics();
3408 assert_eq!((h, m), (1, 0), "should track after reset");
3409 assert!((ratio - 100.0).abs() < 0.01, "100% hit rate");
3410 }
3411}
3412
3413#[cfg(test)]
3417#[cfg(any(feature = "simd-burstiness", feature = "simd-kyle-lambda"))]
3418mod simd_parity_tests {
3419 use super::*;
3420 use crate::interbar_types::TradeSnapshot;
3421 use crate::FixedPoint;
3422
3423 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
3424 TradeSnapshot {
3425 timestamp: ts,
3426 price: FixedPoint((price * 1e8) as i64),
3427 volume: FixedPoint((volume * 1e8) as i64),
3428 is_buyer_maker,
3429 turnover: (price * volume * 1e8) as i128,
3430 }
3431 }
3432
3433 fn generate_trade_sequence(n: usize, seed: u64) -> Vec<TradeSnapshot> {
3435 let mut rng = seed;
3436 let mut ts = 1_000_000i64;
3437 let base_price = 50000.0;
3438
3439 (0..n)
3440 .map(|_| {
3441 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
3443 let r = ((rng >> 33) as f64) / (u32::MAX as f64);
3444
3445 let delta = 10 + ((r * 4990.0) as i64);
3447 ts += delta;
3448
3449 let price = base_price + (r - 0.5) * 100.0;
3450 let volume = 0.01 + r * 2.0;
3451 let is_buyer = rng % 2 == 0;
3452
3453 make_snapshot(ts, price, volume, is_buyer)
3454 })
3455 .collect()
3456 }
3457
3458 #[cfg(feature = "simd-burstiness")]
3459 #[test]
3460 fn test_burstiness_simd_scalar_parity_small() {
3461 let trades = generate_trade_sequence(5, 42);
3463 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3464
3465 let simd_result = simd::compute_burstiness_simd(&refs);
3466 let scalar_result = compute_burstiness_scalar(&refs);
3467
3468 assert!(
3469 (simd_result - scalar_result).abs() < 1e-10,
3470 "Burstiness SIMD/scalar divergence on small window: SIMD={simd_result}, scalar={scalar_result}"
3471 );
3472 }
3473
3474 #[cfg(feature = "simd-burstiness")]
3475 #[test]
3476 fn test_burstiness_simd_scalar_parity_medium() {
3477 let trades = generate_trade_sequence(100, 123);
3479 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3480
3481 let simd_result = simd::compute_burstiness_simd(&refs);
3482 let scalar_result = compute_burstiness_scalar(&refs);
3483
3484 assert!(
3485 (simd_result - scalar_result).abs() < 1e-10,
3486 "Burstiness SIMD/scalar divergence on medium window: SIMD={simd_result}, scalar={scalar_result}"
3487 );
3488 }
3489
3490 #[cfg(feature = "simd-burstiness")]
3491 #[test]
3492 fn test_burstiness_simd_scalar_parity_large() {
3493 let trades = generate_trade_sequence(500, 456);
3495 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3496
3497 let simd_result = simd::compute_burstiness_simd(&refs);
3498 let scalar_result = compute_burstiness_scalar(&refs);
3499
3500 assert!(
3501 (simd_result - scalar_result).abs() < 1e-8,
3502 "Burstiness SIMD/scalar divergence on large window: SIMD={simd_result}, scalar={scalar_result}"
3503 );
3504 }
3505
3506 #[cfg(feature = "simd-burstiness")]
3507 #[test]
3508 fn test_burstiness_simd_scalar_parity_edge_cases() {
3509 let t0 = make_snapshot(0, 100.0, 1.0, false);
3511 let t1 = make_snapshot(1000, 101.0, 1.0, true);
3512 let refs = vec![&t0, &t1];
3513
3514 let simd_result = simd::compute_burstiness_simd(&refs);
3515 let scalar_result = compute_burstiness_scalar(&refs);
3516 assert!(
3517 (simd_result - scalar_result).abs() < 1e-10,
3518 "Burstiness parity failed on 2 trades: SIMD={simd_result}, scalar={scalar_result}"
3519 );
3520
3521 let t2 = make_snapshot(2000, 102.0, 1.0, false);
3523 let refs = vec![&t0, &t1, &t2];
3524
3525 let simd_result = simd::compute_burstiness_simd(&refs);
3526 let scalar_result = compute_burstiness_scalar(&refs);
3527 assert!(
3528 (simd_result - scalar_result).abs() < 1e-10,
3529 "Burstiness parity failed on 3 trades: SIMD={simd_result}, scalar={scalar_result}"
3530 );
3531
3532 let t3 = make_snapshot(3000, 103.0, 1.0, true);
3534 let refs = vec![&t0, &t1, &t2, &t3];
3535
3536 let simd_result = simd::compute_burstiness_simd(&refs);
3537 let scalar_result = compute_burstiness_scalar(&refs);
3538 assert!(
3539 (simd_result - scalar_result).abs() < 1e-10,
3540 "Burstiness parity failed on 4 trades: SIMD={simd_result}, scalar={scalar_result}"
3541 );
3542 }
3543
3544 #[cfg(feature = "simd-kyle-lambda")]
3545 #[test]
3546 fn test_kyle_lambda_simd_scalar_parity_small() {
3547 let trades = generate_trade_sequence(10, 789);
3548 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3549
3550 let simd_result = simd::compute_kyle_lambda_simd(&refs);
3551 let scalar_result = compute_kyle_lambda_scalar(&refs);
3552
3553 assert!(
3554 (simd_result - scalar_result).abs() < 1e-8,
3555 "Kyle Lambda SIMD/scalar divergence on small window: SIMD={simd_result}, scalar={scalar_result}"
3556 );
3557 }
3558
3559 #[cfg(feature = "simd-kyle-lambda")]
3560 #[test]
3561 fn test_kyle_lambda_simd_scalar_parity_medium() {
3562 let trades = generate_trade_sequence(200, 101);
3563 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3564
3565 let simd_result = simd::compute_kyle_lambda_simd(&refs);
3566 let scalar_result = compute_kyle_lambda_scalar(&refs);
3567
3568 assert!(
3569 (simd_result - scalar_result).abs() < 1e-6,
3570 "Kyle Lambda SIMD/scalar divergence on medium window: SIMD={simd_result}, scalar={scalar_result}"
3571 );
3572 }
3573
3574 #[cfg(feature = "simd-kyle-lambda")]
3575 #[test]
3576 fn test_kyle_lambda_simd_scalar_parity_large() {
3577 let trades = generate_trade_sequence(600, 202);
3579 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3580
3581 let simd_result = simd::compute_kyle_lambda_simd(&refs);
3582 let scalar_result = compute_kyle_lambda_scalar(&refs);
3583
3584 assert!(
3586 (simd_result - scalar_result).abs() < 1e-4,
3587 "Kyle Lambda SIMD/scalar divergence on large window: SIMD={simd_result}, scalar={scalar_result}"
3588 );
3589 }
3590
3591 #[cfg(feature = "simd-kyle-lambda")]
3592 #[test]
3593 fn test_kyle_lambda_simd_scalar_parity_edge_cases() {
3594 let t0 = make_snapshot(0, 50000.0, 1.0, false);
3596 let t1 = make_snapshot(1000, 50010.0, 2.0, true);
3597 let refs = vec![&t0, &t1];
3598
3599 let simd_result = simd::compute_kyle_lambda_simd(&refs);
3600 let scalar_result = compute_kyle_lambda_scalar(&refs);
3601 assert!(
3602 (simd_result - scalar_result).abs() < 1e-10,
3603 "Kyle Lambda parity failed on 2 trades: SIMD={simd_result}, scalar={scalar_result}"
3604 );
3605
3606 let all_buys: Vec<TradeSnapshot> = (0..20)
3608 .map(|i| make_snapshot(i * 100, 50000.0 + i as f64, 1.0, false))
3609 .collect();
3610 let refs: Vec<&TradeSnapshot> = all_buys.iter().collect();
3611
3612 let simd_result = simd::compute_kyle_lambda_simd(&refs);
3613 let scalar_result = compute_kyle_lambda_scalar(&refs);
3614 assert!(
3615 (simd_result - scalar_result).abs() < 1e-8,
3616 "Kyle Lambda parity failed on all-buys: SIMD={simd_result}, scalar={scalar_result}"
3617 );
3618
3619 let all_sells: Vec<TradeSnapshot> = (0..20)
3621 .map(|i| make_snapshot(i * 100, 50000.0 + i as f64, 1.0, true))
3622 .collect();
3623 let refs: Vec<&TradeSnapshot> = all_sells.iter().collect();
3624
3625 let simd_result = simd::compute_kyle_lambda_simd(&refs);
3626 let scalar_result = compute_kyle_lambda_scalar(&refs);
3627 assert!(
3628 (simd_result - scalar_result).abs() < 1e-8,
3629 "Kyle Lambda parity failed on all-sells: SIMD={simd_result}, scalar={scalar_result}"
3630 );
3631 }
3632
3633 #[cfg(all(feature = "simd-burstiness", feature = "simd-kyle-lambda"))]
3634 #[test]
3635 fn test_simd_parity_sweep_window_sizes() {
3636 for size in [2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 255, 256] {
3638 let trades = generate_trade_sequence(size, size as u64 * 37);
3639 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3640
3641 let burst_simd = simd::compute_burstiness_simd(&refs);
3642 let burst_scalar = compute_burstiness_scalar(&refs);
3643 assert!(
3644 (burst_simd - burst_scalar).abs() < 1e-8,
3645 "Burstiness parity failed at size {size}: SIMD={burst_simd}, scalar={burst_scalar}"
3646 );
3647
3648 let kyle_simd = simd::compute_kyle_lambda_simd(&refs);
3649 let kyle_scalar = compute_kyle_lambda_scalar(&refs);
3650 assert!(
3651 (kyle_simd - kyle_scalar).abs() < 1e-6,
3652 "Kyle Lambda parity failed at size {size}: SIMD={kyle_simd}, scalar={kyle_scalar}"
3653 );
3654 }
3655 }
3656}
3657
3658#[cfg(test)]
3662mod branchless_ofi_tests {
3663 use super::*;
3664 use crate::interbar_types::TradeSnapshot;
3665 use crate::FixedPoint;
3666
3667 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
3668 TradeSnapshot {
3669 timestamp: ts,
3670 price: FixedPoint((price * 1e8) as i64),
3671 volume: FixedPoint((volume * 1e8) as i64),
3672 is_buyer_maker,
3673 turnover: (price * volume * 1e8) as i128,
3674 }
3675 }
3676
3677 #[test]
3678 fn test_accumulate_all_buys() {
3679 let trades = vec![
3680 make_snapshot(1000, 50000.0, 1.0, false),
3681 make_snapshot(2000, 50000.0, 2.0, false),
3682 make_snapshot(3000, 50000.0, 3.0, false),
3683 ];
3684 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3685 let (buy, sell) = accumulate_buy_sell_branchless(&refs);
3686 assert!((buy - 6.0).abs() < 1e-6, "buy={buy}, expected 6.0");
3687 assert!(sell.abs() < 1e-6, "sell={sell}, expected 0.0");
3688 }
3689
3690 #[test]
3691 fn test_accumulate_all_sells() {
3692 let trades = vec![
3693 make_snapshot(1000, 50000.0, 1.0, true),
3694 make_snapshot(2000, 50000.0, 2.0, true),
3695 make_snapshot(3000, 50000.0, 3.0, true),
3696 ];
3697 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3698 let (buy, sell) = accumulate_buy_sell_branchless(&refs);
3699 assert!(buy.abs() < 1e-6, "buy={buy}, expected 0.0");
3700 assert!((sell - 6.0).abs() < 1e-6, "sell={sell}, expected 6.0");
3701 }
3702
3703 #[test]
3704 fn test_accumulate_balanced() {
3705 let trades = vec![
3706 make_snapshot(1000, 50000.0, 5.0, false),
3707 make_snapshot(2000, 50000.0, 5.0, true),
3708 ];
3709 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3710 let (buy, sell) = accumulate_buy_sell_branchless(&refs);
3711 assert!((buy - 5.0).abs() < 1e-6, "buy={buy}, expected 5.0");
3712 assert!((sell - 5.0).abs() < 1e-6, "sell={sell}, expected 5.0");
3713 }
3714
3715 #[test]
3716 fn test_accumulate_empty() {
3717 let refs: Vec<&TradeSnapshot> = vec![];
3718 let (buy, sell) = accumulate_buy_sell_branchless(&refs);
3719 assert_eq!(buy, 0.0);
3720 assert_eq!(sell, 0.0);
3721 }
3722
3723 #[test]
3724 fn test_accumulate_single_trade() {
3725 let trades = vec![make_snapshot(1000, 50000.0, 7.5, false)];
3726 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3727 let (buy, sell) = accumulate_buy_sell_branchless(&refs);
3728 assert!((buy - 7.5).abs() < 1e-6, "buy={buy}, expected 7.5");
3729 assert!(sell.abs() < 1e-6, "sell={sell}, expected 0.0");
3730 }
3731
3732 #[test]
3733 fn test_accumulate_odd_count_remainder_path() {
3734 let trades = vec![
3736 make_snapshot(1000, 50000.0, 1.0, false),
3737 make_snapshot(2000, 50000.0, 2.0, true),
3738 make_snapshot(3000, 50000.0, 4.0, false),
3739 ];
3740 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3741 let (buy, sell) = accumulate_buy_sell_branchless(&refs);
3742 assert!((buy - 5.0).abs() < 1e-6, "buy={buy}, expected 5.0 (1+4)");
3743 assert!((sell - 2.0).abs() < 1e-6, "sell={sell}, expected 2.0");
3744 }
3745
3746 #[test]
3747 fn test_ofi_branchless_all_buys() {
3748 let trades = vec![
3749 make_snapshot(1000, 50000.0, 1.0, false),
3750 make_snapshot(2000, 50000.0, 1.0, false),
3751 ];
3752 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3753 let ofi = compute_ofi_branchless(&refs);
3754 assert!((ofi - 1.0).abs() < 1e-10, "ofi={ofi}, expected 1.0");
3755 }
3756
3757 #[test]
3758 fn test_ofi_branchless_all_sells() {
3759 let trades = vec![
3760 make_snapshot(1000, 50000.0, 1.0, true),
3761 make_snapshot(2000, 50000.0, 1.0, true),
3762 ];
3763 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3764 let ofi = compute_ofi_branchless(&refs);
3765 assert!((ofi - (-1.0)).abs() < 1e-10, "ofi={ofi}, expected -1.0");
3766 }
3767
3768 #[test]
3769 fn test_ofi_branchless_balanced() {
3770 let trades = vec![
3771 make_snapshot(1000, 50000.0, 3.0, false),
3772 make_snapshot(2000, 50000.0, 3.0, true),
3773 ];
3774 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3775 let ofi = compute_ofi_branchless(&refs);
3776 assert!(ofi.abs() < 1e-10, "ofi={ofi}, expected 0.0");
3777 }
3778
3779 #[test]
3780 fn test_ofi_branchless_empty() {
3781 let refs: Vec<&TradeSnapshot> = vec![];
3782 let ofi = compute_ofi_branchless(&refs);
3783 assert_eq!(ofi, 0.0, "empty trades should return 0.0");
3784 }
3785
3786 #[test]
3787 fn test_ofi_branchless_single_trade() {
3788 let trades = vec![make_snapshot(1000, 50000.0, 10.0, false)];
3789 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3790 let ofi = compute_ofi_branchless(&refs);
3791 assert!((ofi - 1.0).abs() < 1e-10, "ofi={ofi}, expected 1.0 for single buy");
3792 }
3793
3794 #[test]
3795 fn test_ofi_branchless_bounded() {
3796 let trades = vec![
3798 make_snapshot(1000, 50000.0, 100.0, false),
3799 make_snapshot(2000, 50000.0, 0.001, true),
3800 make_snapshot(3000, 50000.0, 50.0, false),
3801 ];
3802 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3803 let ofi = compute_ofi_branchless(&refs);
3804 assert!(ofi >= -1.0 && ofi <= 1.0, "ofi={ofi} out of [-1, 1]");
3805 assert!(ofi > 0.0, "ofi should be positive (buy-dominated)");
3806 }
3807}
3808
3809#[cfg(test)]
3810mod extract_cache_tests {
3811 use super::*;
3812 use crate::interbar_types::TradeSnapshot;
3813 use crate::FixedPoint;
3814
3815 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
3816 TradeSnapshot {
3817 timestamp: ts,
3818 price: FixedPoint((price * 1e8) as i64),
3819 volume: FixedPoint((volume * 1e8) as i64),
3820 is_buyer_maker,
3821 turnover: (price * volume * 1e8) as i128,
3822 }
3823 }
3824
3825 #[test]
3826 fn test_lookback_cache_empty() {
3827 let refs: Vec<&TradeSnapshot> = vec![];
3828 let cache = extract_lookback_cache(&refs);
3829 assert!(cache.prices.is_empty());
3830 assert!(cache.volumes.is_empty());
3831 assert_eq!(cache.total_volume, 0.0);
3832 }
3833
3834 #[test]
3835 fn test_lookback_cache_single_trade() {
3836 let trades = vec![make_snapshot(1000, 50000.0, 2.5, false)];
3837 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3838 let cache = extract_lookback_cache(&refs);
3839 assert_eq!(cache.prices.len(), 1);
3840 assert!((cache.open - 50000.0).abs() < 1e-6);
3841 assert!((cache.close - 50000.0).abs() < 1e-6);
3842 assert!((cache.high - 50000.0).abs() < 1e-6);
3843 assert!((cache.low - 50000.0).abs() < 1e-6);
3844 assert!((cache.total_volume - 2.5).abs() < 1e-6);
3845 }
3846
3847 #[test]
3848 fn test_lookback_cache_ascending_prices() {
3849 let trades = vec![
3850 make_snapshot(1000, 100.0, 1.0, false),
3851 make_snapshot(2000, 200.0, 2.0, false),
3852 make_snapshot(3000, 300.0, 3.0, false),
3853 ];
3854 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3855 let cache = extract_lookback_cache(&refs);
3856 assert!((cache.open - 100.0).abs() < 1e-6);
3857 assert!((cache.close - 300.0).abs() < 1e-6);
3858 assert!((cache.high - 300.0).abs() < 1e-6);
3859 assert!((cache.low - 100.0).abs() < 1e-6);
3860 assert!((cache.total_volume - 6.0).abs() < 1e-6);
3861 }
3862
3863 #[test]
3864 fn test_lookback_cache_ohlc_invariants() {
3865 let trades = vec![
3867 make_snapshot(1000, 50.0, 1.0, false),
3868 make_snapshot(2000, 100.0, 1.0, true),
3869 make_snapshot(3000, 30.0, 1.0, false),
3870 make_snapshot(4000, 80.0, 1.0, true),
3871 ];
3872 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3873 let cache = extract_lookback_cache(&refs);
3874 assert!(cache.high >= cache.open, "high >= open");
3875 assert!(cache.high >= cache.close, "high >= close");
3876 assert!(cache.low <= cache.open, "low <= open");
3877 assert!(cache.low <= cache.close, "low <= close");
3878 assert!((cache.high - 100.0).abs() < 1e-6);
3879 assert!((cache.low - 30.0).abs() < 1e-6);
3880 }
3881
3882 #[test]
3883 fn test_lookback_cache_all_same_price() {
3884 let trades = vec![
3885 make_snapshot(1000, 42000.0, 1.0, false),
3886 make_snapshot(2000, 42000.0, 2.0, true),
3887 make_snapshot(3000, 42000.0, 3.0, false),
3888 ];
3889 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3890 let cache = extract_lookback_cache(&refs);
3891 assert!((cache.high - cache.low).abs() < 1e-6, "same price: high == low");
3892 assert!((cache.open - cache.close).abs() < 1e-6, "same price: open == close");
3893 }
3894
3895 #[test]
3896 fn test_ohlc_batch_empty() {
3897 let refs: Vec<&TradeSnapshot> = vec![];
3898 let (o, h, l, c) = extract_ohlc_batch(&refs);
3899 assert_eq!(o, 0.0);
3900 assert_eq!(h, 0.0);
3901 assert_eq!(l, 0.0);
3902 assert_eq!(c, 0.0);
3903 }
3904
3905 #[test]
3906 fn test_ohlc_batch_matches_cache() {
3907 let trades = vec![
3908 make_snapshot(1000, 50.0, 1.0, false),
3909 make_snapshot(2000, 80.0, 2.0, true),
3910 make_snapshot(3000, 30.0, 3.0, false),
3911 make_snapshot(4000, 60.0, 4.0, true),
3912 ];
3913 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3914 let cache = extract_lookback_cache(&refs);
3915 let (o, h, l, c) = extract_ohlc_batch(&refs);
3916 assert!((o - cache.open).abs() < 1e-10, "open mismatch");
3917 assert!((h - cache.high).abs() < 1e-10, "high mismatch");
3918 assert!((l - cache.low).abs() < 1e-10, "low mismatch");
3919 assert!((c - cache.close).abs() < 1e-10, "close mismatch");
3920 }
3921}
3922
3923#[cfg(test)]
3924mod extract_prices_ohlc_cached_tests {
3925 use super::*;
3926 use crate::interbar_types::TradeSnapshot;
3927 use crate::FixedPoint;
3928
3929 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
3930 TradeSnapshot {
3931 timestamp: ts,
3932 price: FixedPoint((price * 1e8) as i64),
3933 volume: FixedPoint((volume * 1e8) as i64),
3934 is_buyer_maker,
3935 turnover: (price * volume * 1e8) as i128,
3936 }
3937 }
3938
3939 #[test]
3940 fn test_empty() {
3941 let refs: Vec<&TradeSnapshot> = vec![];
3942 let (prices, (o, h, l, c)) = extract_prices_and_ohlc_cached(&refs);
3943 assert!(prices.is_empty());
3944 assert_eq!(o, 0.0);
3945 assert_eq!(h, 0.0);
3946 assert_eq!(l, 0.0);
3947 assert_eq!(c, 0.0);
3948 }
3949
3950 #[test]
3951 fn test_single_trade() {
3952 let trades = vec![make_snapshot(1000, 42000.0, 1.0, false)];
3953 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3954 let (prices, (o, h, l, c)) = extract_prices_and_ohlc_cached(&refs);
3955 assert_eq!(prices.len(), 1);
3956 assert!((o - c).abs() < 1e-6, "single trade: open == close");
3957 assert!((h - l).abs() < 1e-6, "single trade: high == low");
3958 }
3959
3960 #[test]
3961 fn test_ohlc_invariants() {
3962 let trades = vec![
3963 make_snapshot(1000, 100.0, 1.0, false),
3964 make_snapshot(2000, 150.0, 1.0, true),
3965 make_snapshot(3000, 80.0, 1.0, false),
3966 make_snapshot(4000, 120.0, 1.0, true),
3967 ];
3968 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3969 let (prices, (o, h, l, c)) = extract_prices_and_ohlc_cached(&refs);
3970 assert_eq!(prices.len(), 4);
3971 assert!(h >= o && h >= c, "high must be >= open and close");
3972 assert!(l <= o && l <= c, "low must be <= open and close");
3973 assert!((o - 100.0).abs() < 1e-6);
3975 assert!((h - 150.0).abs() < 1e-6);
3976 assert!((l - 80.0).abs() < 1e-6);
3977 assert!((c - 120.0).abs() < 1e-6);
3978 }
3979
3980 #[test]
3981 fn test_prices_match_lookback_cache() {
3982 let trades = vec![
3983 make_snapshot(1000, 50.0, 2.0, false),
3984 make_snapshot(2000, 80.0, 3.0, true),
3985 make_snapshot(3000, 30.0, 1.0, false),
3986 ];
3987 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
3988 let (prices, (o, h, l, c)) = extract_prices_and_ohlc_cached(&refs);
3989 let cache = extract_lookback_cache(&refs);
3990 assert_eq!(prices.len(), cache.prices.len());
3992 for (a, b) in prices.iter().zip(cache.prices.iter()) {
3993 assert!((a - b).abs() < 1e-10, "price mismatch: {a} vs {b}");
3994 }
3995 assert!((o - cache.open).abs() < 1e-10);
3996 assert!((h - cache.high).abs() < 1e-10);
3997 assert!((l - cache.low).abs() < 1e-10);
3998 assert!((c - cache.close).abs() < 1e-10);
3999 }
4000}
4001
4002#[cfg(test)]
4003mod entropy_readonly_cache_tests {
4004 use super::*;
4005
4006 #[test]
4007 fn test_readonly_cache_miss_returns_none() {
4008 let cache = EntropyCache::new();
4009 let prices: Vec<f64> = (0..100).map(|i| 100.0 + i as f64).collect();
4010 let result = compute_entropy_adaptive_cached_readonly(&prices, &cache);
4011 assert!(result.is_none(), "Empty cache should return None");
4012 }
4013
4014 #[test]
4015 fn test_readonly_cache_hit_returns_value() {
4016 let mut cache = EntropyCache::new();
4017 let prices: Vec<f64> = (0..100).map(|i| 100.0 + i as f64).collect();
4018 let expected = compute_entropy_adaptive_cached(&prices, &mut cache);
4020 let result = compute_entropy_adaptive_cached_readonly(&prices, &cache);
4022 assert!(result.is_some(), "Populated cache should return Some");
4023 assert!((result.unwrap() - expected).abs() < 1e-10,
4024 "Cached value mismatch: {} vs {expected}", result.unwrap());
4025 }
4026
4027 #[test]
4028 fn test_readonly_large_window_returns_none() {
4029 let cache = EntropyCache::new();
4031 let prices: Vec<f64> = (0..600).map(|i| 100.0 + i as f64 * 0.1).collect();
4032 let result = compute_entropy_adaptive_cached_readonly(&prices, &cache);
4033 assert!(result.is_none(), "Large window (n>=500) should return None from readonly");
4034 }
4035}
4036
4037#[cfg(test)]
4038mod entropy_adaptive_cached_tests {
4039 use super::*;
4040
4041 #[test]
4042 fn test_cached_matches_uncached_pe_path() {
4043 let prices: Vec<f64> = (0..200).map(|i| 100.0 + (i as f64 * 0.7).sin() * 5.0).collect();
4045 let uncached = compute_entropy_adaptive(&prices);
4046 let mut cache = EntropyCache::new();
4047 let cached = compute_entropy_adaptive_cached(&prices, &mut cache);
4048 assert!(
4049 (uncached - cached).abs() < 1e-14,
4050 "PE path mismatch: uncached={uncached}, cached={cached}"
4051 );
4052 }
4053
4054 #[test]
4055 fn test_cached_matches_uncached_apen_path() {
4056 let prices: Vec<f64> = (0..600).map(|i| 100.0 + (i as f64 * 0.3).sin() * 10.0).collect();
4058 let uncached = compute_entropy_adaptive(&prices);
4059 let mut cache = EntropyCache::new();
4060 let cached = compute_entropy_adaptive_cached(&prices, &mut cache);
4061 assert!(
4062 (uncached - cached).abs() < 1e-14,
4063 "ApEn path mismatch: uncached={uncached}, cached={cached}"
4064 );
4065 }
4066
4067 #[test]
4068 fn test_cached_second_call_hits_cache() {
4069 let prices: Vec<f64> = (0..100).map(|i| 50.0 + i as f64).collect();
4070 let mut cache = EntropyCache::new();
4071
4072 let first = compute_entropy_adaptive_cached(&prices, &mut cache);
4073 let (hits_before, misses_before, _) = cache.metrics();
4074
4075 let second = compute_entropy_adaptive_cached(&prices, &mut cache);
4076 let (hits_after, _misses_after, _) = cache.metrics();
4077
4078 assert!(
4079 (first - second).abs() < 1e-14,
4080 "Same prices must produce identical entropy"
4081 );
4082 assert_eq!(hits_after, hits_before + 1, "Second call should be a cache hit");
4083 assert_eq!(misses_before, 1, "First call should be a cache miss");
4084 }
4085
4086 #[test]
4087 fn test_cached_different_sequences_miss() {
4088 let mut cache = EntropyCache::new();
4089 let prices_a: Vec<f64> = (0..100).map(|i| 100.0 + i as f64).collect();
4090 let prices_b: Vec<f64> = (0..100).map(|i| 200.0 + i as f64 * 2.0).collect();
4091
4092 let _ea = compute_entropy_adaptive_cached(&prices_a, &mut cache);
4093 let (_, misses_after_a, _) = cache.metrics();
4094 assert_eq!(misses_after_a, 1);
4095
4096 let _eb = compute_entropy_adaptive_cached(&prices_b, &mut cache);
4097 let (_, misses_after_b, _) = cache.metrics();
4098 assert_eq!(misses_after_b, 2, "Different sequence should miss cache");
4099 }
4100}
4101
4102#[cfg(test)]
4103mod gk_negative_variance_tests {
4104 use super::*;
4105
4106 #[test]
4107 fn test_gk_negative_variance_returns_zero() {
4108 let gk = compute_garman_klass_with_ohlc(100.0, 101.0, 99.0, 200.0);
4112 assert_eq!(gk, 0.0, "Negative variance should return 0.0, got {gk}");
4113 }
4114
4115 #[test]
4116 fn test_gk_borderline_variance() {
4117 let gk = compute_garman_klass_with_ohlc(100.0, 115.0, 95.0, 110.0);
4120 assert!(gk >= 0.0, "GK should never be negative, got {gk}");
4121 assert!(gk.is_finite(), "GK must be finite");
4122 }
4123}
4124
4125#[cfg(test)]
4126mod kyle_kaufman_edge_tests {
4127 use super::*;
4128 use crate::interbar_types::TradeSnapshot;
4129 use crate::FixedPoint;
4130
4131 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
4132 TradeSnapshot {
4133 timestamp: ts,
4134 price: FixedPoint((price * 1e8) as i64),
4135 volume: FixedPoint((volume * 1e8) as i64),
4136 is_buyer_maker,
4137 turnover: (price * volume * 1e8) as i128,
4138 }
4139 }
4140
4141 #[test]
4144 fn test_kyle_lambda_single_trade() {
4145 let trades = vec![make_snapshot(1000, 50000.0, 1.0, false)];
4146 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4147 let kl = compute_kyle_lambda(&refs);
4148 assert_eq!(kl, 0.0, "single trade → 0.0 (n < 2)");
4149 }
4150
4151 #[test]
4152 fn test_kyle_lambda_empty() {
4153 let refs: Vec<&TradeSnapshot> = vec![];
4154 let kl = compute_kyle_lambda(&refs);
4155 assert_eq!(kl, 0.0, "empty → 0.0");
4156 }
4157
4158 #[test]
4159 fn test_kyle_lambda_zero_price_change() {
4160 let trades = vec![
4161 make_snapshot(1000, 50000.0, 1.0, false),
4162 make_snapshot(2000, 50000.0, 2.0, true),
4163 ];
4164 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4165 let kl = compute_kyle_lambda(&refs);
4166 assert_eq!(kl, 0.0, "no price change → kyle lambda = 0");
4167 }
4168
4169 #[test]
4170 fn test_kyle_lambda_balanced_volume() {
4171 let trades = vec![
4172 make_snapshot(1000, 50000.0, 5.0, false),
4173 make_snapshot(2000, 51000.0, 5.0, true),
4174 ];
4175 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4176 let kl = compute_kyle_lambda(&refs);
4177 assert_eq!(kl, 0.0, "balanced volume → zero imbalance → 0.0");
4178 }
4179
4180 #[test]
4181 fn test_kyle_lambda_all_same_price() {
4182 let trades = vec![
4183 make_snapshot(1000, 42000.0, 1.0, false),
4184 make_snapshot(2000, 42000.0, 2.0, false),
4185 make_snapshot(3000, 42000.0, 3.0, true),
4186 ];
4187 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4188 let kl = compute_kyle_lambda(&refs);
4189 assert!(kl.is_finite(), "kyle lambda must be finite for same-price");
4190 }
4191
4192 #[test]
4193 fn test_kyle_lambda_no_panic_large_window() {
4194 let trades: Vec<_> = (0..150)
4196 .map(|i| make_snapshot(i * 1000, 50000.0 + (i as f64) * 0.1, 1.0, i % 3 == 0))
4197 .collect();
4198 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4199 let kl = compute_kyle_lambda(&refs);
4200 assert!(kl.is_finite(), "kyle lambda must be finite for large window: {kl}");
4201 }
4202
4203 #[test]
4206 fn test_kaufman_er_single_price() {
4207 let prices = vec![50000.0];
4208 let er = compute_kaufman_er(&prices);
4209 assert_eq!(er, 0.0, "single price → 0.0 (n < 2)");
4210 }
4211
4212 #[test]
4213 fn test_kaufman_er_empty() {
4214 let prices: Vec<f64> = vec![];
4215 let er = compute_kaufman_er(&prices);
4216 assert_eq!(er, 0.0, "empty → 0.0");
4217 }
4218
4219 #[test]
4220 fn test_kaufman_er_monotonic_up() {
4221 let prices: Vec<f64> = (0..10).map(|i| 100.0 + i as f64).collect();
4222 let er = compute_kaufman_er(&prices);
4223 assert!((er - 1.0).abs() < 1e-10, "monotonic → ER = 1.0, got {er}");
4224 }
4225
4226 #[test]
4227 fn test_kaufman_er_monotonic_down() {
4228 let prices: Vec<f64> = (0..10).map(|i| 100.0 - i as f64).collect();
4229 let er = compute_kaufman_er(&prices);
4230 assert!((er - 1.0).abs() < 1e-10, "monotonic down → ER = 1.0, got {er}");
4231 }
4232
4233 #[test]
4234 fn test_kaufman_er_all_same_price() {
4235 let prices = vec![42000.0; 20];
4236 let er = compute_kaufman_er(&prices);
4237 assert_eq!(er, 0.0, "all same → ER = 0.0 (zero volatility)");
4238 }
4239
4240 #[test]
4241 fn test_kaufman_er_bounded_zigzag() {
4242 let prices: Vec<f64> = (0..20).map(|i| if i % 2 == 0 { 100.0 } else { 110.0 }).collect();
4244 let er = compute_kaufman_er(&prices);
4245 assert!(er >= 0.0 && er <= 1.0, "ER must be in [0, 1], got {er}");
4246 assert!(er < 0.1, "zigzag should have low ER, got {er}");
4247 }
4248
4249 #[test]
4250 fn test_kaufman_er_two_prices() {
4251 let prices = vec![100.0, 200.0];
4252 let er = compute_kaufman_er(&prices);
4253 assert!((er - 1.0).abs() < 1e-10, "two prices with change → ER = 1.0, got {er}");
4254 }
4255}
4256
4257#[cfg(test)]
4258mod volume_moments_edge_tests {
4259 use super::*;
4260 use crate::interbar_types::TradeSnapshot;
4261 use crate::FixedPoint;
4262
4263 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
4264 TradeSnapshot {
4265 timestamp: ts,
4266 price: FixedPoint((price * 1e8) as i64),
4267 volume: FixedPoint((volume * 1e8) as i64),
4268 is_buyer_maker,
4269 turnover: (price * volume * 1e8) as i128,
4270 }
4271 }
4272
4273 #[test]
4274 fn test_moments_empty() {
4275 let refs: Vec<&TradeSnapshot> = vec![];
4276 let (s, k) = compute_volume_moments(&refs);
4277 assert_eq!(s, 0.0);
4278 assert_eq!(k, 0.0);
4279 }
4280
4281 #[test]
4282 fn test_moments_one_trade() {
4283 let trades = vec![make_snapshot(1000, 50000.0, 5.0, false)];
4284 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4285 let (s, k) = compute_volume_moments(&refs);
4286 assert_eq!(s, 0.0, "n < 3 → 0.0");
4287 assert_eq!(k, 0.0, "n < 3 → 0.0");
4288 }
4289
4290 #[test]
4291 fn test_moments_two_trades() {
4292 let trades = vec![
4293 make_snapshot(1000, 50000.0, 1.0, false),
4294 make_snapshot(2000, 50000.0, 10.0, true),
4295 ];
4296 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4297 let (s, k) = compute_volume_moments(&refs);
4298 assert_eq!(s, 0.0, "n < 3 → 0.0");
4299 assert_eq!(k, 0.0, "n < 3 → 0.0");
4300 }
4301
4302 #[test]
4303 fn test_moments_symmetric_distribution() {
4304 let trades = vec![
4306 make_snapshot(1000, 50000.0, 1.0, false),
4307 make_snapshot(2000, 50000.0, 5.0, true),
4308 make_snapshot(3000, 50000.0, 9.0, false),
4309 make_snapshot(4000, 50000.0, 5.0, true),
4310 make_snapshot(5000, 50000.0, 1.0, false),
4311 ];
4312 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4313 let (s, _k) = compute_volume_moments(&refs);
4314 assert!(s.abs() < 0.5, "symmetric distribution → skew near 0, got {s}");
4315 }
4316
4317 #[test]
4318 fn test_moments_cached_matches_trade_version() {
4319 let trades = vec![
4320 make_snapshot(1000, 50000.0, 1.0, false),
4321 make_snapshot(2000, 50000.0, 3.0, true),
4322 make_snapshot(3000, 50000.0, 7.0, false),
4323 make_snapshot(4000, 50000.0, 2.0, true),
4324 make_snapshot(5000, 50000.0, 5.0, false),
4325 ];
4326 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4327 let (s1, k1) = compute_volume_moments(&refs);
4328
4329 let volumes: Vec<f64> = trades.iter().map(|t| t.volume.to_f64()).collect();
4330 let (s2, k2) = compute_volume_moments_cached(&volumes);
4331
4332 assert!(
4333 (s1 - s2).abs() < 1e-10,
4334 "skew mismatch: trade={s1}, cached={s2}"
4335 );
4336 assert!(
4337 (k1 - k2).abs() < 1e-10,
4338 "kurt mismatch: trade={k1}, cached={k2}"
4339 );
4340 }
4341
4342 #[test]
4343 fn test_moments_cached_empty() {
4344 let (s, k) = compute_volume_moments_cached(&[]);
4345 assert_eq!(s, 0.0);
4346 assert_eq!(k, 0.0);
4347 }
4348}
4349
4350#[cfg(test)]
4351mod volume_moments_with_mean_tests {
4352 use super::*;
4353 use crate::interbar_types::TradeSnapshot;
4354 use crate::FixedPoint;
4355
4356 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
4357 TradeSnapshot {
4358 timestamp: ts,
4359 price: FixedPoint((price * 1e8) as i64),
4360 volume: FixedPoint((volume * 1e8) as i64),
4361 is_buyer_maker,
4362 turnover: (price * volume * 1e8) as i128,
4363 }
4364 }
4365
4366 #[test]
4367 fn test_with_mean_matches_standard() {
4368 let trades = vec![
4369 make_snapshot(1000, 50000.0, 1.0, false),
4370 make_snapshot(2000, 50000.0, 3.0, true),
4371 make_snapshot(3000, 50000.0, 7.0, false),
4372 make_snapshot(4000, 50000.0, 2.0, true),
4373 make_snapshot(5000, 50000.0, 5.0, false),
4374 ];
4375 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4376 let (s1, k1) = compute_volume_moments(&refs);
4377
4378 let volumes: Vec<f64> = trades.iter().map(|t| t.volume.to_f64()).collect();
4379 let mean = volumes.iter().sum::<f64>() / volumes.len() as f64;
4380 let (s2, k2) = compute_volume_moments_with_mean(&volumes, mean);
4381
4382 assert!(
4383 (s1 - s2).abs() < 1e-10,
4384 "skew mismatch: standard={s1}, with_mean={s2}"
4385 );
4386 assert!(
4387 (k1 - k2).abs() < 1e-10,
4388 "kurt mismatch: standard={k1}, with_mean={k2}"
4389 );
4390 }
4391
4392 #[test]
4393 fn test_with_mean_too_few() {
4394 let (s, k) = compute_volume_moments_with_mean(&[1.0, 2.0], 1.5);
4395 assert_eq!(s, 0.0);
4396 assert_eq!(k, 0.0);
4397 }
4398
4399 #[test]
4400 fn test_with_mean_constant_volumes() {
4401 let vols = vec![5.0; 10];
4402 let (s, k) = compute_volume_moments_with_mean(&vols, 5.0);
4403 assert_eq!(s, 0.0, "constant volumes → zero skewness");
4404 assert_eq!(k, 0.0, "constant volumes → zero kurtosis");
4405 }
4406
4407 #[test]
4408 fn test_with_mean_right_skewed() {
4409 let vols = vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 100.0];
4411 let mean = vols.iter().sum::<f64>() / vols.len() as f64;
4412 let (s, _k) = compute_volume_moments_with_mean(&vols, mean);
4413 assert!(s > 0.5, "right-skewed distribution → positive skew, got {s}");
4414 }
4415}
4416
4417#[cfg(test)]
4418mod garman_klass_ohlc_tests {
4419 use super::*;
4420 use crate::interbar_types::TradeSnapshot;
4421 use crate::FixedPoint;
4422
4423 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
4424 TradeSnapshot {
4425 timestamp: ts,
4426 price: FixedPoint((price * 1e8) as i64),
4427 volume: FixedPoint((volume * 1e8) as i64),
4428 is_buyer_maker,
4429 turnover: (price * volume * 1e8) as i128,
4430 }
4431 }
4432
4433 #[test]
4434 fn test_ohlc_matches_trade_version() {
4435 let trades = vec![
4437 make_snapshot(1000, 100.0, 1.0, false), make_snapshot(2000, 110.0, 1.0, true), make_snapshot(3000, 95.0, 1.0, false), make_snapshot(4000, 105.0, 1.0, true), ];
4442 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4443 let gk_trades = compute_garman_klass(&refs);
4444 let gk_ohlc = compute_garman_klass_with_ohlc(100.0, 110.0, 95.0, 105.0);
4445
4446 assert!(
4447 (gk_trades - gk_ohlc).abs() < 1e-10,
4448 "GK mismatch: trades={gk_trades}, ohlc={gk_ohlc}"
4449 );
4450 }
4451
4452 #[test]
4453 fn test_ohlc_flat_price() {
4454 let gk = compute_garman_klass_with_ohlc(100.0, 100.0, 100.0, 100.0);
4455 assert_eq!(gk, 0.0, "flat price → zero volatility");
4456 }
4457
4458 #[test]
4459 fn test_ohlc_zero_price_guard() {
4460 let gk = compute_garman_klass_with_ohlc(0.0, 100.0, 50.0, 75.0);
4461 assert_eq!(gk, 0.0, "zero open → guard returns 0");
4462 }
4463
4464 #[test]
4465 fn test_ohlc_high_volatility() {
4466 let gk = compute_garman_klass_with_ohlc(100.0, 200.0, 100.0, 150.0);
4468 assert!(gk > 0.0, "wide range → positive volatility");
4469 assert!(gk > 0.3, "very wide range → high volatility, got {gk}");
4470 }
4471
4472 #[test]
4473 fn test_ohlc_close_equals_open() {
4474 let gk = compute_garman_klass_with_ohlc(100.0, 110.0, 90.0, 100.0);
4476 assert!(gk > 0.0, "range exists even if close==open");
4477 }
4478}
4479
4480#[cfg(test)]
4481mod burstiness_edge_tests {
4482 use super::*;
4483 use crate::interbar_types::TradeSnapshot;
4484 use crate::FixedPoint;
4485
4486 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
4487 TradeSnapshot {
4488 timestamp: ts,
4489 price: FixedPoint((price * 1e8) as i64),
4490 volume: FixedPoint((volume * 1e8) as i64),
4491 is_buyer_maker,
4492 turnover: (price * volume * 1e8) as i128,
4493 }
4494 }
4495
4496 #[test]
4497 fn test_burstiness_empty() {
4498 let refs: Vec<&TradeSnapshot> = vec![];
4499 let b = compute_burstiness(&refs);
4500 assert_eq!(b, 0.0, "empty → 0.0");
4501 }
4502
4503 #[test]
4504 fn test_burstiness_single_trade() {
4505 let trades = vec![make_snapshot(1000, 50000.0, 1.0, false)];
4506 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4507 let b = compute_burstiness(&refs);
4508 assert_eq!(b, 0.0, "single trade → 0.0 (n < 2)");
4509 }
4510
4511 #[test]
4512 fn test_burstiness_two_trades() {
4513 let trades = vec![
4514 make_snapshot(1000, 50000.0, 1.0, false),
4515 make_snapshot(2000, 50000.0, 1.0, true),
4516 ];
4517 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4518 let b = compute_burstiness(&refs);
4519 assert!(b.is_finite(), "burstiness must be finite for 2 trades");
4521 assert!(b >= -1.0 && b <= 1.0, "burstiness out of [-1, 1]: {b}");
4522 }
4523
4524 #[test]
4525 fn test_burstiness_regular_intervals() {
4526 let trades: Vec<_> = (0..20)
4528 .map(|i| make_snapshot(i * 1000, 50000.0, 1.0, false))
4529 .collect();
4530 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4531 let b = compute_burstiness(&refs);
4532 assert!((b - (-1.0)).abs() < 0.01, "regular intervals → B ≈ -1, got {b}");
4533 }
4534
4535 #[test]
4536 fn test_burstiness_same_timestamp() {
4537 let trades: Vec<_> = (0..10)
4539 .map(|_| make_snapshot(1000, 50000.0, 1.0, false))
4540 .collect();
4541 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4542 let b = compute_burstiness(&refs);
4543 assert!(b.is_finite(), "same-timestamp must not produce NaN");
4544 }
4545
4546 #[test]
4547 fn test_burstiness_bounded() {
4548 let trades = vec![
4550 make_snapshot(1000, 50000.0, 1.0, false),
4551 make_snapshot(1001, 50000.0, 1.0, true), make_snapshot(1002, 50000.0, 1.0, false), make_snapshot(5000, 50000.0, 1.0, true), make_snapshot(5001, 50000.0, 1.0, false), make_snapshot(10000, 50000.0, 1.0, true), ];
4557 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4558 let b = compute_burstiness(&refs);
4559 assert!(b >= -1.0 && b <= 1.0, "burstiness out of [-1, 1]: {b}");
4560 assert!(b > 0.0, "variable intervals should show positive burstiness, got {b}");
4561 }
4562}
4563
4564#[cfg(test)]
4565mod permutation_entropy_edge_tests {
4566 use super::*;
4567
4568 #[test]
4569 fn test_pe_insufficient_data() {
4570 let prices = vec![100.0; 5]; let pe = compute_permutation_entropy(&prices);
4572 assert_eq!(pe, 1.0, "n < 10 → 1.0 (insufficient data)");
4573 }
4574
4575 #[test]
4576 fn test_pe_exactly_10_m2_path() {
4577 let prices: Vec<f64> = (0..10).map(|i| 100.0 + i as f64).collect();
4579 let pe = compute_permutation_entropy(&prices);
4580 assert_eq!(pe, 0.0, "monotonic ascending → PE = 0.0 (early exit)");
4581 }
4582
4583 #[test]
4584 fn test_pe_exactly_30_m3_path() {
4585 let prices: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();
4587 let pe = compute_permutation_entropy(&prices);
4588 assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
4589 assert!(pe < 0.2, "monotonic ascending → low PE, got {pe}");
4590 }
4591
4592 #[test]
4593 fn test_pe_zigzag_high_entropy() {
4594 let prices: Vec<f64> = (0..40).map(|i| if i % 2 == 0 { 100.0 } else { 110.0 }).collect();
4596 let pe = compute_permutation_entropy(&prices);
4597 assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
4598 }
4599
4600 #[test]
4601 fn test_pe_all_same_price() {
4602 let prices = vec![42000.0; 50];
4604 let pe = compute_permutation_entropy(&prices);
4605 assert!(pe.is_finite(), "same-price must not produce NaN");
4606 assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
4607 }
4608
4609 #[test]
4610 fn test_pe_monotonic_descending() {
4611 let prices: Vec<f64> = (0..35).map(|i| 200.0 - i as f64).collect();
4612 let pe = compute_permutation_entropy(&prices);
4613 assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
4614 assert!(pe < 0.3, "monotonic descending → low PE, got {pe}");
4615 }
4616
4617 #[test]
4618 fn test_pe_m2_zigzag() {
4619 let prices: Vec<f64> = (0..20).map(|i| if i % 2 == 0 { 100.0 } else { 110.0 }).collect();
4621 let pe = compute_permutation_entropy(&prices);
4622 assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
4623 assert!(pe > 0.8, "perfect zigzag (M=2) should have high PE, got {pe}");
4625 }
4626
4627 #[test]
4628 fn test_pe_large_window_dominant_pattern() {
4629 let prices: Vec<f64> = (0..600).map(|i| 100.0 + i as f64 * 0.01).collect();
4634 let pe = compute_permutation_entropy(&prices);
4635 assert_eq!(pe, 0.0, "600-trade monotonic should yield PE=0, got {pe}");
4637 }
4638
4639 #[test]
4640 fn test_pe_large_window_near_uniform() {
4641 let prices: Vec<f64> = (0..500).map(|i| 100.0 + (i as f64 * 0.73).sin() * 5.0).collect();
4644 let pe = compute_permutation_entropy(&prices);
4645 assert!(pe > 0.5, "varied 500-trade series should have moderate-high PE, got {pe}");
4646 assert!(pe <= 1.0, "PE must be <= 1.0, got {pe}");
4647 }
4648}
4649
4650#[cfg(test)]
4651mod garman_hurst_edge_tests {
4652 use super::*;
4653 use crate::interbar_types::TradeSnapshot;
4654 use crate::FixedPoint;
4655
4656 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
4657 TradeSnapshot {
4658 timestamp: ts,
4659 price: FixedPoint((price * 1e8) as i64),
4660 volume: FixedPoint((volume * 1e8) as i64),
4661 is_buyer_maker,
4662 turnover: (price * volume * 1e8) as i128,
4663 }
4664 }
4665
4666 #[test]
4669 fn test_gk_empty() {
4670 let refs: Vec<&TradeSnapshot> = vec![];
4671 let gk = compute_garman_klass(&refs);
4672 assert_eq!(gk, 0.0);
4673 }
4674
4675 #[test]
4676 fn test_gk_single_trade() {
4677 let trades = vec![make_snapshot(1000, 50000.0, 1.0, false)];
4678 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4679 let gk = compute_garman_klass(&refs);
4680 assert_eq!(gk, 0.0, "single trade → GK = 0.0");
4682 }
4683
4684 #[test]
4685 fn test_gk_all_same_price() {
4686 let trades = vec![
4687 make_snapshot(1000, 42000.0, 1.0, false),
4688 make_snapshot(2000, 42000.0, 2.0, true),
4689 make_snapshot(3000, 42000.0, 3.0, false),
4690 ];
4691 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4692 let gk = compute_garman_klass(&refs);
4693 assert_eq!(gk, 0.0, "same price → zero range → GK = 0.0");
4694 }
4695
4696 #[test]
4697 fn test_gk_positive_for_volatile_window() {
4698 let trades = vec![
4699 make_snapshot(1000, 100.0, 1.0, false),
4700 make_snapshot(2000, 120.0, 1.0, true),
4701 make_snapshot(3000, 80.0, 1.0, false),
4702 make_snapshot(4000, 110.0, 1.0, true),
4703 ];
4704 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4705 let gk = compute_garman_klass(&refs);
4706 assert!(gk > 0.0, "volatile window → GK > 0, got {gk}");
4707 assert!(gk.is_finite(), "GK must be finite");
4708 }
4709
4710 #[test]
4711 fn test_gk_with_ohlc_matches_trade_version() {
4712 let trades = vec![
4713 make_snapshot(1000, 100.0, 1.0, false),
4714 make_snapshot(2000, 150.0, 1.0, true),
4715 make_snapshot(3000, 80.0, 1.0, false),
4716 make_snapshot(4000, 120.0, 1.0, true),
4717 ];
4718 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4719 let gk_trades = compute_garman_klass(&refs);
4720 let gk_ohlc = compute_garman_klass_with_ohlc(100.0, 150.0, 80.0, 120.0);
4721 assert!(
4722 (gk_trades - gk_ohlc).abs() < 1e-10,
4723 "trade vs OHLC mismatch: {gk_trades} vs {gk_ohlc}"
4724 );
4725 }
4726
4727 #[test]
4728 fn test_gk_with_ohlc_zero_price() {
4729 let gk = compute_garman_klass_with_ohlc(0.0, 100.0, 50.0, 75.0);
4730 assert_eq!(gk, 0.0, "zero open → guard returns 0.0");
4731 }
4732
4733 #[test]
4736 fn test_hurst_insufficient_data() {
4737 let prices: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();
4738 let h = compute_hurst_dfa(&prices);
4739 assert_eq!(h, 0.5, "< 64 samples → neutral 0.5");
4740 }
4741
4742 #[test]
4743 fn test_hurst_constant_prices() {
4744 let prices = vec![42000.0; 100];
4745 let h = compute_hurst_dfa(&prices);
4746 assert!(h.is_finite(), "constant prices must not produce NaN/Inf");
4747 assert!(h >= 0.0 && h <= 1.0, "Hurst must be in [0, 1], got {h}");
4748 }
4749
4750 #[test]
4751 fn test_hurst_trending_up() {
4752 let prices: Vec<f64> = (0..100).map(|i| 100.0 + i as f64 * 0.5).collect();
4753 let h = compute_hurst_dfa(&prices);
4754 assert!(h >= 0.0 && h <= 1.0, "Hurst must be in [0, 1], got {h}");
4755 assert!(h >= 0.45, "trending series: Hurst should be >= 0.45, got {h}");
4757 }
4758
4759 #[test]
4760 fn test_hurst_exactly_64_samples() {
4761 let prices: Vec<f64> = (0..64).map(|i| 100.0 + (i as f64).sin() * 10.0).collect();
4762 let h = compute_hurst_dfa(&prices);
4763 assert!(h.is_finite(), "exactly 64 samples must not panic");
4764 assert!(h >= 0.0 && h <= 1.0, "Hurst must be in [0, 1], got {h}");
4765 }
4766}
4767
4768#[cfg(test)]
4769mod soft_clamp_hurst_edge_tests {
4770 use super::*;
4771
4772 #[test]
4773 fn test_soft_clamp_extreme_negative() {
4774 let h = soft_clamp_hurst(-100.0);
4775 assert!(h >= 0.0 && h <= 1.0, "Extreme negative should clamp to [0,1], got {h}");
4776 assert!(h < 0.01, "h=-100 should clamp near 0, got {h}");
4777 }
4778
4779 #[test]
4780 fn test_soft_clamp_extreme_positive() {
4781 let h = soft_clamp_hurst(100.0);
4782 assert!(h >= 0.0 && h <= 1.0, "Extreme positive should clamp to [0,1], got {h}");
4783 assert!(h > 0.99, "h=100 should clamp near 1, got {h}");
4784 }
4785
4786 #[test]
4787 fn test_soft_clamp_at_half() {
4788 let h = soft_clamp_hurst(0.5);
4789 assert!((h - 0.5).abs() < 0.05, "h=0.5 should map near 0.5, got {h}");
4790 }
4791
4792 #[test]
4793 fn test_soft_clamp_nan_input() {
4794 let h = soft_clamp_hurst(f64::NAN);
4795 assert!(!h.is_normal() || (h >= 0.0 && h <= 1.0),
4798 "NaN input should produce NaN or clamped value, got {h}");
4799 }
4800
4801 #[test]
4802 fn test_soft_clamp_monotonicity() {
4803 let values = [-5.0, -1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 2.0, 5.0];
4805 for pair in values.windows(2) {
4806 let a = soft_clamp_hurst(pair[0]);
4807 let b = soft_clamp_hurst(pair[1]);
4808 assert!(b >= a, "soft_clamp should be monotonic: f({}) = {a} > f({}) = {b}", pair[0], pair[1]);
4809 }
4810 }
4811
4812 #[test]
4813 fn test_soft_clamp_infinity() {
4814 let h_pos = soft_clamp_hurst(f64::INFINITY);
4815 let h_neg = soft_clamp_hurst(f64::NEG_INFINITY);
4816 assert!(h_pos >= 0.0 && h_pos <= 1.0 || h_pos.is_nan(),
4818 "Inf should clamp or NaN, got {h_pos}");
4819 assert!(h_neg >= 0.0 && h_neg <= 1.0 || h_neg.is_nan(),
4820 "-Inf should clamp or NaN, got {h_neg}");
4821 }
4822}
4823
4824#[cfg(test)]
4825mod lookback_cache_finite_tests {
4826 use super::*;
4827 use crate::interbar_types::TradeSnapshot;
4828 use crate::FixedPoint;
4829
4830 fn make_snapshot(price: f64, volume: f64) -> TradeSnapshot {
4831 TradeSnapshot {
4832 timestamp: 1000,
4833 price: FixedPoint((price * 1e8) as i64),
4834 volume: FixedPoint((volume * 1e8) as i64),
4835 is_buyer_maker: false,
4836 turnover: (price * volume * 1e8) as i128,
4837 }
4838 }
4839
4840 #[test]
4841 fn test_all_prices_finite_normal() {
4842 let trades = vec![
4843 make_snapshot(100.0, 1.0),
4844 make_snapshot(101.0, 2.0),
4845 make_snapshot(99.5, 1.5),
4846 ];
4847 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4848 let cache = extract_lookback_cache(&refs);
4849 assert!(cache.all_prices_finite, "Normal prices should be finite");
4850 }
4851
4852 #[test]
4853 fn test_all_prices_finite_empty() {
4854 let refs: Vec<&TradeSnapshot> = vec![];
4855 let cache = extract_lookback_cache(&refs);
4856 assert!(cache.all_prices_finite, "Empty lookback should default to finite=true");
4857 }
4858
4859 #[test]
4860 fn test_all_prices_finite_single() {
4861 let trades = vec![make_snapshot(50000.0, 1.0)];
4862 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4863 let cache = extract_lookback_cache(&refs);
4864 assert!(cache.all_prices_finite, "Single normal price should be finite");
4865 }
4866
4867 #[test]
4868 fn test_all_volumes_finite_normal() {
4869 let trades = vec![
4870 make_snapshot(100.0, 1.0),
4871 make_snapshot(101.0, 2.5),
4872 make_snapshot(99.5, 0.5),
4873 ];
4874 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4875 let cache = extract_lookback_cache(&refs);
4876 assert!(cache.all_volumes_finite, "Normal volumes should be finite");
4877 }
4878
4879 #[test]
4880 fn test_all_volumes_finite_empty() {
4881 let refs: Vec<&TradeSnapshot> = vec![];
4882 let cache = extract_lookback_cache(&refs);
4883 assert!(cache.all_volumes_finite, "Empty lookback should default to volumes finite=true");
4884 }
4885}
4886
4887#[cfg(test)]
4888mod extract_lookback_cache_comprehensive_tests {
4889 use super::*;
4890 use crate::interbar_types::TradeSnapshot;
4891 use crate::FixedPoint;
4892
4893 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
4894 TradeSnapshot {
4895 timestamp: ts,
4896 price: FixedPoint((price * 1e8) as i64),
4897 volume: FixedPoint((volume * 1e8) as i64),
4898 is_buyer_maker,
4899 turnover: (price * volume * 1e8) as i128,
4900 }
4901 }
4902
4903 #[test]
4904 fn test_ohlc_extraction_correct() {
4905 let trades = vec![
4906 make_snapshot(1000, 100.0, 1.0, false), make_snapshot(2000, 110.0, 2.0, true), make_snapshot(3000, 90.0, 1.5, false), make_snapshot(4000, 105.0, 3.0, true), ];
4911 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4912 let cache = extract_lookback_cache(&refs);
4913
4914 assert!((cache.open - 100.0).abs() < 1e-6, "open should be first price");
4915 assert!((cache.close - 105.0).abs() < 1e-6, "close should be last price");
4916 assert!((cache.high - 110.0).abs() < 1e-6, "high should be max price");
4917 assert!((cache.low - 90.0).abs() < 1e-6, "low should be min price");
4918 }
4919
4920 #[test]
4921 fn test_total_volume_accumulation() {
4922 let trades = vec![
4923 make_snapshot(1000, 100.0, 1.0, false),
4924 make_snapshot(2000, 101.0, 2.5, true),
4925 make_snapshot(3000, 99.0, 0.5, false),
4926 ];
4927 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4928 let cache = extract_lookback_cache(&refs);
4929
4930 let expected_total = 1.0 + 2.5 + 0.5;
4931 assert!((cache.total_volume - expected_total).abs() < 1e-6,
4932 "total_volume={}, expected={}", cache.total_volume, expected_total);
4933 }
4934
4935 #[test]
4936 fn test_prices_vector_matches_trades() {
4937 let trades = vec![
4938 make_snapshot(1000, 100.0, 1.0, false),
4939 make_snapshot(2000, 105.0, 2.0, true),
4940 make_snapshot(3000, 95.0, 1.0, false),
4941 ];
4942 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4943 let cache = extract_lookback_cache(&refs);
4944
4945 assert_eq!(cache.prices.len(), 3);
4946 assert!((cache.prices[0] - 100.0).abs() < 1e-6);
4947 assert!((cache.prices[1] - 105.0).abs() < 1e-6);
4948 assert!((cache.prices[2] - 95.0).abs() < 1e-6);
4949 }
4950
4951 #[test]
4952 fn test_first_volume_is_first_trade() {
4953 let trades = vec![
4954 make_snapshot(1000, 100.0, 7.5, false),
4955 make_snapshot(2000, 101.0, 2.0, true),
4956 ];
4957 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4958 let cache = extract_lookback_cache(&refs);
4959 assert!((cache.first_volume - 7.5).abs() < 1e-6);
4960 }
4961
4962 #[test]
4963 fn test_single_trade_ohlc_all_same() {
4964 let trades = vec![make_snapshot(1000, 50000.0, 1.0, false)];
4965 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4966 let cache = extract_lookback_cache(&refs);
4967
4968 assert!((cache.open - 50000.0).abs() < 1e-6);
4969 assert!((cache.close - 50000.0).abs() < 1e-6);
4970 assert!((cache.high - 50000.0).abs() < 1e-6);
4971 assert!((cache.low - 50000.0).abs() < 1e-6);
4972 }
4973
4974 #[test]
4975 fn test_high_low_invariants() {
4976 let trades = vec![
4977 make_snapshot(1000, 100.0, 1.0, false),
4978 make_snapshot(2000, 200.0, 1.0, true),
4979 make_snapshot(3000, 50.0, 1.0, false),
4980 make_snapshot(4000, 150.0, 1.0, true),
4981 ];
4982 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
4983 let cache = extract_lookback_cache(&refs);
4984
4985 assert!(cache.high >= cache.low, "high must >= low");
4986 assert!(cache.high >= cache.open, "high must >= open");
4987 assert!(cache.high >= cache.close, "high must >= close");
4988 assert!(cache.low <= cache.open, "low must <= open");
4989 assert!(cache.low <= cache.close, "low must <= close");
4990 }
4991}
4992
4993#[cfg(test)]
4994mod proptest_bounds {
4995 use super::*;
4996 use crate::interbar_types::TradeSnapshot;
4997 use crate::FixedPoint;
4998 use proptest::prelude::*;
4999
5000 fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
5001 TradeSnapshot {
5002 timestamp: ts,
5003 price: FixedPoint((price * 1e8) as i64),
5004 volume: FixedPoint((volume * 1e8) as i64),
5005 is_buyer_maker,
5006 turnover: (price * volume * 1e8) as i128,
5007 }
5008 }
5009
5010 fn price_sequence(min_len: usize, max_len: usize) -> impl Strategy<Value = Vec<f64>> {
5012 prop::collection::vec(1.0..=100_000.0_f64, min_len..=max_len)
5013 }
5014
5015 fn volume_pair() -> impl Strategy<Value = (f64, f64)> {
5017 (0.001..=1e9_f64, 0.001..=1e9_f64)
5018 }
5019
5020 proptest! {
5021 #[test]
5023 fn ofi_always_bounded((buy_vol, sell_vol) in volume_pair()) {
5024 let total = buy_vol + sell_vol;
5025 if total > f64::EPSILON {
5026 let ofi = (buy_vol - sell_vol) / total;
5027 prop_assert!(ofi >= -1.0 - f64::EPSILON && ofi <= 1.0 + f64::EPSILON,
5028 "OFI={ofi} out of [-1, 1] for buy={buy_vol}, sell={sell_vol}");
5029 }
5030 }
5031
5032 #[test]
5034 fn kaufman_er_always_bounded(prices in price_sequence(2, 200)) {
5035 let er = compute_kaufman_er(&prices);
5036 prop_assert!(er >= 0.0 && er <= 1.0 + f64::EPSILON,
5037 "Kaufman ER={er} out of [0, 1] for {}-trade window", prices.len());
5038 }
5039
5040 #[test]
5042 fn permutation_entropy_always_bounded(prices in price_sequence(60, 300)) {
5043 let pe = compute_permutation_entropy(&prices);
5044 prop_assert!(pe >= 0.0 && pe <= 1.0 + f64::EPSILON,
5045 "PE={pe} out of [0, 1] for {}-trade window", prices.len());
5046 }
5047
5048 #[test]
5050 fn garman_klass_non_negative(prices in price_sequence(2, 100)) {
5051 let first = prices[0];
5052 let last = *prices.last().unwrap();
5053 let high = prices.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
5054 let low = prices.iter().cloned().fold(f64::INFINITY, f64::min);
5055 let gk = compute_garman_klass_with_ohlc(first, high, low, last);
5056 prop_assert!(gk >= 0.0,
5057 "GK={gk} negative for OHLC({first}, {high}, {low}, {last})");
5058 }
5059
5060 #[test]
5062 fn burstiness_scalar_always_bounded(
5063 n in 3_usize..100,
5064 seed in 0_u64..10000,
5065 ) {
5066 let mut rng = seed;
5068 let mut ts = 0i64;
5069 let trades: Vec<TradeSnapshot> = (0..n).map(|_| {
5070 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
5071 let delta = 1 + ((rng >> 33) % 10000) as i64;
5072 ts += delta;
5073 make_snapshot(ts, 100.0, 1.0, rng % 2 == 0)
5074 }).collect();
5075 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
5076
5077 let b = compute_burstiness_scalar(&refs);
5078 prop_assert!(b >= -1.0 - f64::EPSILON && b <= 1.0 + f64::EPSILON,
5079 "Burstiness={b} out of [-1, 1] for n={n}");
5080 }
5081
5082 #[test]
5084 fn hurst_dfa_always_bounded(prices in price_sequence(64, 300)) {
5085 let h = compute_hurst_dfa(&prices);
5086 prop_assert!(h >= 0.0 && h <= 1.0,
5087 "Hurst={h} out of [0, 1] for {}-trade window", prices.len());
5088 }
5089
5090 #[test]
5092 fn approximate_entropy_non_negative(prices in price_sequence(10, 200)) {
5093 let std_dev = {
5094 let mean = prices.iter().sum::<f64>() / prices.len() as f64;
5095 let var = prices.iter().map(|p| (p - mean) * (p - mean)).sum::<f64>() / prices.len() as f64;
5096 var.sqrt()
5097 };
5098 if std_dev > f64::EPSILON {
5099 let r = 0.2 * std_dev;
5100 let apen = compute_approximate_entropy(&prices, 2, r);
5101 prop_assert!(apen >= 0.0,
5102 "ApEn={apen} negative for {}-trade window", prices.len());
5103 }
5104 }
5105
5106 #[test]
5108 fn lookback_cache_ohlc_invariants(
5109 n in 1_usize..50,
5110 seed in 0_u64..10000,
5111 ) {
5112 let mut rng = seed;
5113 let trades: Vec<TradeSnapshot> = (0..n).map(|i| {
5114 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
5115 let price = 100.0 + ((rng >> 33) as f64 / u32::MAX as f64) * 50.0;
5116 make_snapshot(i as i64 * 1000, price, 1.0, rng % 2 == 0)
5117 }).collect();
5118 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
5119
5120 let cache = extract_lookback_cache(&refs);
5121 prop_assert!(cache.high >= cache.open, "high < open");
5122 prop_assert!(cache.high >= cache.close, "high < close");
5123 prop_assert!(cache.low <= cache.open, "low > open");
5124 prop_assert!(cache.low <= cache.close, "low > close");
5125 prop_assert!(cache.total_volume >= 0.0, "total_volume negative");
5126 prop_assert_eq!(cache.prices.len(), n, "prices length mismatch");
5127 prop_assert_eq!(cache.volumes.len(), n, "volumes length mismatch");
5128 }
5129
5130 #[test]
5132 fn lookback_cache_finite_invariant(
5133 n in 1_usize..50,
5134 seed in 0_u64..10000,
5135 ) {
5136 let mut rng = seed;
5137 let trades: Vec<TradeSnapshot> = (0..n).map(|i| {
5138 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
5139 let price = 100.0 + ((rng >> 33) as f64 / u32::MAX as f64) * 50.0;
5140 let volume = 0.1 + ((rng >> 17) as f64 / u32::MAX as f64) * 10.0;
5141 make_snapshot(i as i64 * 1000, price, volume, rng % 2 == 0)
5142 }).collect();
5143 let refs: Vec<&TradeSnapshot> = trades.iter().collect();
5144
5145 let cache = extract_lookback_cache(&refs);
5146 let expected_prices_finite = cache.prices.iter().all(|p| p.is_finite());
5147 let expected_volumes_finite = cache.volumes.iter().all(|v| v.is_finite());
5148 prop_assert_eq!(cache.all_prices_finite, expected_prices_finite,
5149 "all_prices_finite mismatch");
5150 prop_assert_eq!(cache.all_volumes_finite, expected_volumes_finite,
5151 "all_volumes_finite mismatch");
5152 }
5153 }
5154}
5155
5156#[cfg(test)]
5158mod hurst_pe_stability_tests {
5159 use super::*;
5160
5161 #[test]
5162 fn test_hurst_constant_prices_must_not_be_nan() {
5163 let prices = vec![42000.0; 100];
5166 let h = compute_hurst_dfa(&prices);
5167 assert!(h.is_finite(), "Constant prices must not produce NaN, got {h}");
5169 assert!(h >= 0.0 && h <= 1.0, "Hurst must be in [0,1], got {h}");
5170 }
5171
5172 #[test]
5173 fn test_hurst_alternating_pattern_mean_reverting() {
5174 let prices: Vec<f64> = (0..128)
5176 .map(|i| if i % 2 == 0 { 100.0 } else { 101.0 })
5177 .collect();
5178 let h = compute_hurst_dfa(&prices);
5179 assert!(h.is_finite() && h >= 0.0 && h <= 1.0, "Hurst bounded: {h}");
5180 assert!(h < 0.45, "Alternating pattern should have H < 0.45 (mean-reverting): {h}");
5181 }
5182
5183 #[test]
5184 fn test_hurst_perfect_linear_trend() {
5185 let prices: Vec<f64> = (0..128).map(|i| 100.0 + i as f64 * 0.5).collect();
5187 let h = compute_hurst_dfa(&prices);
5188 assert!(h.is_finite() && h >= 0.0 && h <= 1.0, "Hurst bounded: {h}");
5189 assert!(h > 0.7, "Perfect trend should have H > 0.7 (trending): {h}");
5190 }
5191
5192 #[test]
5193 fn test_hurst_boundary_63_vs_64_vs_65() {
5194 let base_prices: Vec<f64> = (0..65).map(|i| 100.0 + (i as f64 * 0.3).sin()).collect();
5196
5197 let h63 = compute_hurst_dfa(&base_prices[..63]);
5198 let h64 = compute_hurst_dfa(&base_prices[..64]);
5199 let h65 = compute_hurst_dfa(&base_prices[..65]);
5200
5201 assert!((h63 - 0.5).abs() < f64::EPSILON, "n=63 must be 0.5, got {h63}");
5203
5204 assert!(h64.is_finite() && h64 >= 0.0 && h64 <= 1.0, "n=64 bounded: {h64}");
5206 assert!(h65.is_finite() && h65 >= 0.0 && h65 <= 1.0, "n=65 bounded: {h65}");
5207
5208 assert!((h64 - h65).abs() < 0.3, "Hurst(64) and Hurst(65) should be similar: {h64} vs {h65}");
5210 }
5211
5212 #[test]
5213 fn test_pe_m2_vs_m3_boundary_29_vs_30() {
5214 let prices_29: Vec<f64> = (0..29).map(|i| 100.0 + i as f64).collect();
5216 let prices_30: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();
5217
5218 let pe29 = compute_permutation_entropy(&prices_29); let pe30 = compute_permutation_entropy(&prices_30); assert!(pe29 >= 0.0 && pe29 <= 1.0, "PE(29) bounded: {pe29}");
5223 assert!(pe30 >= 0.0 && pe30 <= 1.0, "PE(30) bounded: {pe30}");
5224 assert!(pe29 < 0.05, "Monotonic M=2 → PE near 0: {pe29}");
5225 assert!(pe30 < 0.05, "Monotonic M=3 → PE near 0: {pe30}");
5226 }
5227
5228 #[test]
5229 fn test_pe_monotonic_decreasing_early_exit() {
5230 let prices: Vec<f64> = (0..50).map(|i| 200.0 - i as f64 * 0.5).collect();
5232 let pe = compute_permutation_entropy(&prices);
5233 assert!(pe.is_finite() && pe >= 0.0, "PE finite: {pe}");
5234 assert!(pe < 0.01, "Monotonic decreasing → PE ≈ 0: {pe}");
5235 }
5236
5237 #[test]
5238 fn test_pe_m2_perfect_alternation_maximum_entropy() {
5239 let prices: Vec<f64> = (0..20).map(|i| if i % 2 == 0 { 100.0 } else { 101.0 }).collect();
5242 let pe = compute_permutation_entropy(&prices); assert!(pe.is_finite() && pe >= 0.0 && pe <= 1.0, "PE bounded: {pe}");
5244 assert!(pe > 0.95, "Perfect alternation → PE ≈ 1.0: {pe}");
5245 }
5246
5247 #[test]
5248 fn test_pe_exactly_60_samples_m3() {
5249 let prices: Vec<f64> = (0..60).map(|i| 100.0 + (i as f64 * 0.5).sin() * 10.0).collect();
5251 let pe = compute_permutation_entropy(&prices);
5252 assert!(pe.is_finite() && pe >= 0.0 && pe <= 1.0, "PE(60) bounded: {pe}");
5253 }
5254}
5255
5256#[cfg(test)]
5258mod tier2_edge_case_tests {
5259 use super::*;
5260
5261 #[test]
5262 fn test_garman_klass_with_ohlc_zero_volatility() {
5263 let gk = compute_garman_klass_with_ohlc(100.0, 100.0, 100.0, 100.0);
5265 assert_eq!(gk, 0.0, "Constant OHLC → GK = 0");
5266 }
5267
5268 #[test]
5269 fn test_garman_klass_with_ohlc_negative_variance() {
5270 let gk = compute_garman_klass_with_ohlc(99.0, 100.001, 99.999, 101.0);
5272 assert!(gk >= 0.0, "GK must be non-negative: {gk}");
5274 assert!(gk.is_finite(), "GK must be finite");
5275 }
5276
5277 #[test]
5278 fn test_garman_klass_with_ohlc_zero_price_guard() {
5279 assert_eq!(compute_garman_klass_with_ohlc(0.0, 100.0, 50.0, 75.0), 0.0);
5280 assert_eq!(compute_garman_klass_with_ohlc(100.0, 100.0, 0.0, 75.0), 0.0);
5281 }
5282
5283 #[test]
5284 fn test_volume_moments_cached_insufficient_data() {
5285 assert_eq!(compute_volume_moments_cached(&[]), (0.0, 0.0));
5286 assert_eq!(compute_volume_moments_cached(&[1.0]), (0.0, 0.0));
5287 assert_eq!(compute_volume_moments_cached(&[1.0, 2.0]), (0.0, 0.0));
5288 }
5289
5290 #[test]
5291 fn test_volume_moments_cached_constant_volume() {
5292 let vols = vec![5.0; 100];
5293 let (skew, kurt) = compute_volume_moments_cached(&vols);
5294 assert_eq!(skew, 0.0, "Constant volume → skewness = 0");
5295 assert_eq!(kurt, 0.0, "Constant volume → kurtosis = 0");
5296 }
5297
5298 #[test]
5299 fn test_volume_moments_with_mean_matches_cached() {
5300 let vols = vec![1.0, 2.0, 3.0, 10.0, 0.5, 7.0, 4.0];
5301 let mean = vols.iter().sum::<f64>() / vols.len() as f64;
5302 let (skew1, kurt1) = compute_volume_moments_cached(&vols);
5303 let (skew2, kurt2) = compute_volume_moments_with_mean(&vols, mean);
5304 assert!((skew1 - skew2).abs() < 1e-10, "Skewness parity: {skew1} vs {skew2}");
5305 assert!((kurt1 - kurt2).abs() < 1e-10, "Kurtosis parity: {kurt1} vs {kurt2}");
5306 }
5307
5308 #[test]
5309 fn test_kaufman_er_perfect_trend() {
5310 let prices: Vec<f64> = (0..20).map(|i| 100.0 + i as f64).collect();
5312 let er = compute_kaufman_er(&prices);
5313 assert!((er - 1.0).abs() < 1e-10, "Perfect trend → ER = 1.0: {er}");
5314 }
5315
5316 #[test]
5317 fn test_kaufman_er_pure_noise_returns_to_start() {
5318 let prices = vec![100.0, 105.0, 95.0, 110.0, 90.0, 100.0];
5320 let er = compute_kaufman_er(&prices);
5321 assert_eq!(er, 0.0, "Return-to-start → ER = 0: {er}");
5322 }
5323}