1use crate::error::StreamError;
41use rust_decimal::Decimal;
42use rust_decimal::prelude::ToPrimitive;
43use std::collections::VecDeque;
44
45pub struct MinMaxNormalizer {
64 window_size: usize,
65 window: VecDeque<Decimal>,
66 cached_min: Decimal,
67 cached_max: Decimal,
68 dirty: bool,
69}
70
71impl MinMaxNormalizer {
72 pub fn new(window_size: usize) -> Result<Self, StreamError> {
78 if window_size == 0 {
79 return Err(StreamError::ConfigError {
80 reason: "MinMaxNormalizer window_size must be > 0".into(),
81 });
82 }
83 Ok(Self {
84 window_size,
85 window: VecDeque::with_capacity(window_size),
86 cached_min: Decimal::MAX,
87 cached_max: Decimal::MIN,
88 dirty: false,
89 })
90 }
91
92 pub fn update(&mut self, value: Decimal) {
101 if self.window.len() == self.window_size {
102 self.window.pop_front();
103 self.dirty = true;
104 }
105 self.window.push_back(value);
106 if !self.dirty {
108 if value < self.cached_min {
109 self.cached_min = value;
110 }
111 if value > self.cached_max {
112 self.cached_max = value;
113 }
114 }
115 }
116
117 fn recompute(&mut self) {
121 self.cached_min = Decimal::MAX;
122 self.cached_max = Decimal::MIN;
123 for &v in &self.window {
124 if v < self.cached_min {
125 self.cached_min = v;
126 }
127 if v > self.cached_max {
128 self.cached_max = v;
129 }
130 }
131 self.dirty = false;
132 }
133
134 pub fn min_max(&mut self) -> Option<(Decimal, Decimal)> {
140 if self.window.is_empty() {
141 return None;
142 }
143 if self.dirty {
144 self.recompute();
145 }
146 Some((self.cached_min, self.cached_max))
147 }
148
149 #[must_use = "normalized value is returned; ignoring it loses the result"]
162 pub fn normalize(&mut self, value: Decimal) -> Result<f64, StreamError> {
163 let (min, max) = self
164 .min_max()
165 .ok_or_else(|| StreamError::NormalizationError {
166 reason: "window is empty; call update() before normalize()".into(),
167 })?;
168 if max == min {
169 return Ok(0.0);
171 }
172 let normalized = (value - min) / (max - min);
173 let clamped = normalized.clamp(Decimal::ZERO, Decimal::ONE);
175 clamped.to_f64().ok_or_else(|| StreamError::NormalizationError {
176 reason: "Decimal-to-f64 conversion failed for normalized value".into(),
177 })
178 }
179
180 pub fn denormalize(&mut self, normalized: f64) -> Result<Decimal, StreamError> {
188 use rust_decimal::prelude::FromPrimitive;
189 let (min, max) = self
190 .min_max()
191 .ok_or_else(|| StreamError::NormalizationError {
192 reason: "window is empty; call update() before denormalize()".into(),
193 })?;
194 let scale = max - min;
195 let n_dec = Decimal::from_f64(normalized).ok_or_else(|| StreamError::NormalizationError {
196 reason: "normalized value is not a finite f64".into(),
197 })?;
198 Ok(n_dec * scale + min)
199 }
200
201 pub fn range(&mut self) -> Option<Decimal> {
206 let (min, max) = self.min_max()?;
207 Some(max - min)
208 }
209
210 pub fn clamp_to_window(&mut self, value: Decimal) -> Decimal {
214 match self.min_max() {
215 None => value,
216 Some((min, max)) => value.max(min).min(max),
217 }
218 }
219
220 pub fn midpoint(&mut self) -> Option<Decimal> {
224 let (min, max) = self.min_max()?;
225 Some((min + max) / Decimal::TWO)
226 }
227
228 pub fn reset(&mut self) {
230 self.window.clear();
231 self.cached_min = Decimal::MAX;
232 self.cached_max = Decimal::MIN;
233 self.dirty = false;
234 }
235
236 pub fn len(&self) -> usize {
238 self.window.len()
239 }
240
241 pub fn is_empty(&self) -> bool {
244 self.window.is_empty()
245 }
246
247 pub fn window_size(&self) -> usize {
249 self.window_size
250 }
251
252 pub fn is_full(&self) -> bool {
257 self.window.len() == self.window_size
258 }
259
260 pub fn min(&mut self) -> Option<Decimal> {
265 self.min_max().map(|(min, _)| min)
266 }
267
268 pub fn max(&mut self) -> Option<Decimal> {
273 self.min_max().map(|(_, max)| max)
274 }
275
276 pub fn mean(&self) -> Option<Decimal> {
280 if self.window.is_empty() {
281 return None;
282 }
283 let sum: Decimal = self.window.iter().copied().sum();
284 Some(sum / Decimal::from(self.window.len() as u64))
285 }
286
287 pub fn normalize_batch(
296 &mut self,
297 values: &[rust_decimal::Decimal],
298 ) -> Result<Vec<f64>, crate::error::StreamError> {
299 values
300 .iter()
301 .map(|&v| {
302 self.update(v);
303 self.normalize(v)
304 })
305 .collect()
306 }
307
308 pub fn normalize_clamp(
318 &mut self,
319 value: rust_decimal::Decimal,
320 ) -> Result<f64, crate::error::StreamError> {
321 self.normalize(value).map(|v| v.clamp(0.0, 1.0))
322 }
323
324 pub fn z_score(&self, value: Decimal) -> Option<f64> {
334 if self.window.len() < 2 {
335 return None;
336 }
337 let n = Decimal::from(self.window.len() as u64);
338 let mean: Decimal = self.window.iter().copied().sum::<Decimal>() / n;
339 let variance: Decimal = self
340 .window
341 .iter()
342 .map(|&v| { let d = v - mean; d * d })
343 .sum::<Decimal>()
344 / n;
345 if variance.is_zero() {
346 return None;
347 }
348 use rust_decimal::prelude::ToPrimitive;
349 let std_dev_f64 = variance.to_f64()?.sqrt();
350 let value_f64 = value.to_f64()?;
351 let mean_f64 = mean.to_f64()?;
352 Some((value_f64 - mean_f64) / std_dev_f64)
353 }
354
355 pub fn percentile_rank(&self, value: rust_decimal::Decimal) -> Option<f64> {
363 if self.window.is_empty() {
364 return None;
365 }
366 let count_le = self
367 .window
368 .iter()
369 .filter(|&&v| v <= value)
370 .count();
371 Some(count_le as f64 / self.window.len() as f64)
372 }
373
374 pub fn count_above(&self, threshold: rust_decimal::Decimal) -> usize {
376 self.window.iter().filter(|&&v| v > threshold).count()
377 }
378
379 pub fn fraction_above_mid(&mut self) -> Option<f64> {
383 let (min, max) = self.min_max()?;
384 let mid = (min + max) / rust_decimal::Decimal::TWO;
385 let above = self.window.iter().filter(|&&v| v > mid).count();
386 Some(above as f64 / self.window.len() as f64)
387 }
388
389 pub fn normalized_range(&mut self) -> Option<f64> {
394 use rust_decimal::prelude::ToPrimitive;
395 let (min, max) = self.min_max()?;
396 if max.is_zero() {
397 return None;
398 }
399 ((max - min) / max).to_f64()
400 }
401
402 pub fn ewma(&self, alpha: f64) -> Option<f64> {
407 if self.window.is_empty() {
408 return None;
409 }
410 let alpha = alpha.clamp(f64::MIN_POSITIVE, 1.0);
411 let one_minus = 1.0 - alpha;
412 let mut ewma = self.window[0].to_f64().unwrap_or(0.0);
413 for &v in self.window.iter().skip(1) {
414 ewma = alpha * v.to_f64().unwrap_or(ewma) + one_minus * ewma;
415 }
416 Some(ewma)
417 }
418
419 pub fn interquartile_range(&self) -> Option<Decimal> {
424 let n = self.window.len();
425 if n < 4 {
426 return None;
427 }
428 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
429 sorted.sort();
430 let q1_idx = n / 4;
431 let q3_idx = 3 * n / 4;
432 Some(sorted[q3_idx] - sorted[q1_idx])
433 }
434
435 pub fn skewness(&self) -> Option<f64> {
440 use rust_decimal::prelude::ToPrimitive;
441 let n = self.window.len();
442 if n < 3 {
443 return None;
444 }
445 let n_f = n as f64;
446 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
447 if vals.len() < n {
448 return None;
449 }
450 let mean = vals.iter().sum::<f64>() / n_f;
451 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
452 let std_dev = variance.sqrt();
453 if std_dev == 0.0 {
454 return None;
455 }
456 let skew = vals.iter().map(|v| ((v - mean) / std_dev).powi(3)).sum::<f64>() / n_f;
457 Some(skew)
458 }
459
460 pub fn latest(&self) -> Option<Decimal> {
462 self.window.back().copied()
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use rust_decimal_macros::dec;
470
471 fn norm(w: usize) -> MinMaxNormalizer {
472 MinMaxNormalizer::new(w).unwrap()
473 }
474
475 #[test]
478 fn test_new_normalizer_is_empty() {
479 let n = norm(4);
480 assert!(n.is_empty());
481 assert_eq!(n.len(), 0);
482 }
483
484 #[test]
485 fn test_minmax_is_full_false_before_capacity() {
486 let mut n = norm(3);
487 assert!(!n.is_full());
488 n.update(dec!(1));
489 n.update(dec!(2));
490 assert!(!n.is_full());
491 n.update(dec!(3));
492 assert!(n.is_full());
493 }
494
495 #[test]
496 fn test_minmax_is_full_stays_true_after_eviction() {
497 let mut n = norm(3);
498 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
499 n.update(v);
500 }
501 assert!(n.is_full()); }
503
504 #[test]
505 fn test_new_zero_window_returns_error() {
506 let result = MinMaxNormalizer::new(0);
507 assert!(matches!(result, Err(StreamError::ConfigError { .. })));
508 }
509
510 #[test]
513 fn test_normalize_min_is_zero() {
514 let mut n = norm(4);
515 n.update(dec!(10));
516 n.update(dec!(20));
517 n.update(dec!(30));
518 n.update(dec!(40));
519 let v = n.normalize(dec!(10)).unwrap();
520 assert!(
521 (v - 0.0).abs() < 1e-10,
522 "min should normalize to 0.0, got {v}"
523 );
524 }
525
526 #[test]
527 fn test_normalize_max_is_one() {
528 let mut n = norm(4);
529 n.update(dec!(10));
530 n.update(dec!(20));
531 n.update(dec!(30));
532 n.update(dec!(40));
533 let v = n.normalize(dec!(40)).unwrap();
534 assert!(
535 (v - 1.0).abs() < 1e-10,
536 "max should normalize to 1.0, got {v}"
537 );
538 }
539
540 #[test]
541 fn test_normalize_midpoint_is_half() {
542 let mut n = norm(4);
543 n.update(dec!(0));
544 n.update(dec!(100));
545 let v = n.normalize(dec!(50)).unwrap();
546 assert!((v - 0.5).abs() < 1e-10);
547 }
548
549 #[test]
550 fn test_normalize_result_clamped_below_zero() {
551 let mut n = norm(4);
552 n.update(dec!(50));
553 n.update(dec!(100));
554 let v = n.normalize(dec!(10)).unwrap();
556 assert!(v >= 0.0);
557 assert_eq!(v, 0.0);
558 }
559
560 #[test]
561 fn test_normalize_result_clamped_above_one() {
562 let mut n = norm(4);
563 n.update(dec!(50));
564 n.update(dec!(100));
565 let v = n.normalize(dec!(200)).unwrap();
567 assert!(v <= 1.0);
568 assert_eq!(v, 1.0);
569 }
570
571 #[test]
572 fn test_normalize_all_same_values_returns_zero() {
573 let mut n = norm(4);
574 n.update(dec!(5));
575 n.update(dec!(5));
576 n.update(dec!(5));
577 let v = n.normalize(dec!(5)).unwrap();
578 assert_eq!(v, 0.0);
579 }
580
581 #[test]
584 fn test_normalize_empty_window_returns_error() {
585 let mut n = norm(4);
586 let err = n.normalize(dec!(1)).unwrap_err();
587 assert!(matches!(err, StreamError::NormalizationError { .. }));
588 }
589
590 #[test]
591 fn test_min_max_empty_returns_none() {
592 let mut n = norm(4);
593 assert!(n.min_max().is_none());
594 }
595
596 #[test]
601 fn test_rolling_window_evicts_oldest() {
602 let mut n = norm(3);
603 n.update(dec!(1)); n.update(dec!(5));
605 n.update(dec!(10));
606 n.update(dec!(20)); let (min, max) = n.min_max().unwrap();
608 assert_eq!(min, dec!(5));
609 assert_eq!(max, dec!(20));
610 }
611
612 #[test]
613 fn test_rolling_window_len_does_not_exceed_capacity() {
614 let mut n = norm(3);
615 for i in 0..10 {
616 n.update(Decimal::from(i));
617 }
618 assert_eq!(n.len(), 3);
619 }
620
621 #[test]
624 fn test_reset_clears_window() {
625 let mut n = norm(4);
626 n.update(dec!(10));
627 n.update(dec!(20));
628 n.reset();
629 assert!(n.is_empty());
630 assert!(n.min_max().is_none());
631 }
632
633 #[test]
634 fn test_normalize_works_after_reset() {
635 let mut n = norm(4);
636 n.update(dec!(10));
637 n.reset();
638 n.update(dec!(0));
639 n.update(dec!(100));
640 let v = n.normalize(dec!(100)).unwrap();
641 assert!((v - 1.0).abs() < 1e-10);
642 }
643
644 #[test]
647 fn test_streaming_updates_monotone_sequence() {
648 let mut n = norm(5);
649 let prices = [dec!(100), dec!(101), dec!(102), dec!(103), dec!(104), dec!(105)];
650 for &p in &prices {
651 n.update(p);
652 }
653 let v_min = n.normalize(dec!(101)).unwrap();
655 let v_max = n.normalize(dec!(105)).unwrap();
656 assert!((v_min - 0.0).abs() < 1e-10);
657 assert!((v_max - 1.0).abs() < 1e-10);
658 }
659
660 #[test]
661 fn test_normalization_monotonicity_in_window() {
662 let mut n = norm(10);
663 for i in 0..10 {
664 n.update(Decimal::from(i * 10));
665 }
666 let v0 = n.normalize(dec!(0)).unwrap();
668 let v50 = n.normalize(dec!(50)).unwrap();
669 let v90 = n.normalize(dec!(90)).unwrap();
670 assert!(v0 < v50, "normalized values should be monotone");
671 assert!(v50 < v90, "normalized values should be monotone");
672 }
673
674 #[test]
675 fn test_high_precision_input_preserved() {
676 let mut n = norm(2);
678 n.update(dec!(50000.00000000));
679 n.update(dec!(50000.12345678));
680 let (min, max) = n.min_max().unwrap();
681 assert_eq!(min, dec!(50000.00000000));
682 assert_eq!(max, dec!(50000.12345678));
683 }
684
685 #[test]
688 fn test_denormalize_empty_window_returns_error() {
689 let mut n = norm(4);
690 assert!(matches!(n.denormalize(0.5), Err(StreamError::NormalizationError { .. })));
691 }
692
693 #[test]
694 fn test_denormalize_roundtrip_min() {
695 let mut n = norm(4);
696 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
697 n.update(v);
698 }
699 let normalized = n.normalize(dec!(10)).unwrap(); let back = n.denormalize(normalized).unwrap();
701 assert!((back - dec!(10)).abs() < dec!(0.0001));
702 }
703
704 #[test]
705 fn test_denormalize_roundtrip_max() {
706 let mut n = norm(4);
707 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
708 n.update(v);
709 }
710 let normalized = n.normalize(dec!(40)).unwrap(); let back = n.denormalize(normalized).unwrap();
712 assert!((back - dec!(40)).abs() < dec!(0.0001));
713 }
714
715 #[test]
718 fn test_range_none_when_empty() {
719 let mut n = norm(4);
720 assert!(n.range().is_none());
721 }
722
723 #[test]
724 fn test_range_zero_when_all_same() {
725 let mut n = norm(3);
726 n.update(dec!(5));
727 n.update(dec!(5));
728 n.update(dec!(5));
729 assert_eq!(n.range(), Some(dec!(0)));
730 }
731
732 #[test]
733 fn test_range_correct() {
734 let mut n = norm(4);
735 for v in [dec!(10), dec!(40), dec!(20), dec!(30)] {
736 n.update(v);
737 }
738 assert_eq!(n.range(), Some(dec!(30))); }
740
741 #[test]
744 fn test_midpoint_none_when_empty() {
745 let mut n = norm(4);
746 assert!(n.midpoint().is_none());
747 }
748
749 #[test]
750 fn test_midpoint_correct() {
751 let mut n = norm(4);
752 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
753 n.update(v);
754 }
755 assert_eq!(n.midpoint(), Some(dec!(25)));
757 }
758
759 #[test]
760 fn test_midpoint_single_value() {
761 let mut n = norm(4);
762 n.update(dec!(42));
763 assert_eq!(n.midpoint(), Some(dec!(42)));
764 }
765
766 #[test]
769 fn test_clamp_to_window_returns_value_unchanged_when_empty() {
770 let mut n = norm(4);
771 assert_eq!(n.clamp_to_window(dec!(50)), dec!(50));
772 }
773
774 #[test]
775 fn test_clamp_to_window_clamps_above_max() {
776 let mut n = norm(4);
777 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
778 assert_eq!(n.clamp_to_window(dec!(100)), dec!(30));
779 }
780
781 #[test]
782 fn test_clamp_to_window_clamps_below_min() {
783 let mut n = norm(4);
784 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
785 assert_eq!(n.clamp_to_window(dec!(5)), dec!(10));
786 }
787
788 #[test]
789 fn test_clamp_to_window_passthrough_when_in_range() {
790 let mut n = norm(4);
791 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
792 assert_eq!(n.clamp_to_window(dec!(15)), dec!(15));
793 }
794
795 #[test]
798 fn test_count_above_zero_when_empty() {
799 let n = norm(4);
800 assert_eq!(n.count_above(dec!(5)), 0);
801 }
802
803 #[test]
804 fn test_count_above_counts_strictly_above() {
805 let mut n = norm(8);
806 for v in [dec!(1), dec!(5), dec!(10), dec!(15)] { n.update(v); }
807 assert_eq!(n.count_above(dec!(5)), 2); }
809
810 #[test]
811 fn test_count_above_all_when_threshold_below_all() {
812 let mut n = norm(4);
813 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
814 assert_eq!(n.count_above(dec!(5)), 3);
815 }
816
817 #[test]
818 fn test_count_above_zero_when_threshold_above_all() {
819 let mut n = norm(4);
820 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
821 assert_eq!(n.count_above(dec!(100)), 0);
822 }
823
824 #[test]
827 fn test_normalized_range_none_when_empty() {
828 let mut n = norm(4);
829 assert!(n.normalized_range().is_none());
830 }
831
832 #[test]
833 fn test_normalized_range_zero_when_all_same() {
834 let mut n = norm(4);
835 for _ in 0..4 { n.update(dec!(5)); }
836 assert_eq!(n.normalized_range(), Some(0.0));
837 }
838
839 #[test]
840 fn test_normalized_range_correct_value() {
841 let mut n = norm(4);
842 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
843 let nr = n.normalized_range().unwrap();
845 assert!((nr - 0.75).abs() < 1e-10);
846 }
847
848 #[test]
851 fn test_normalize_clamp_in_range_equals_normalize() {
852 let mut n = norm(4);
853 for v in [dec!(0), dec!(25), dec!(75), dec!(100)] {
854 n.update(v);
855 }
856 let clamped = n.normalize_clamp(dec!(50)).unwrap();
857 let normal = n.normalize(dec!(50)).unwrap();
858 assert!((clamped - normal).abs() < 1e-9);
859 }
860
861 #[test]
862 fn test_normalize_clamp_above_max_clamped_to_one() {
863 let mut n = norm(3);
864 for v in [dec!(0), dec!(50), dec!(100)] {
865 n.update(v);
866 }
867 let clamped = n.normalize_clamp(dec!(200)).unwrap();
869 assert!((clamped - 1.0).abs() < 1e-9, "expected 1.0 got {clamped}");
870 }
871
872 #[test]
873 fn test_normalize_clamp_below_min_clamped_to_zero() {
874 let mut n = norm(3);
875 for v in [dec!(10), dec!(50), dec!(100)] {
876 n.update(v);
877 }
878 let clamped = n.normalize_clamp(dec!(-50)).unwrap();
880 assert!((clamped - 0.0).abs() < 1e-9, "expected 0.0 got {clamped}");
881 }
882
883 #[test]
884 fn test_normalize_clamp_empty_window_returns_error() {
885 let mut n = norm(4);
886 assert!(n.normalize_clamp(dec!(5)).is_err());
887 }
888
889 #[test]
892 fn test_latest_none_when_empty() {
893 let n = norm(5);
894 assert_eq!(n.latest(), None);
895 }
896
897 #[test]
898 fn test_latest_returns_most_recent_value() {
899 let mut n = norm(5);
900 n.update(dec!(10));
901 n.update(dec!(20));
902 n.update(dec!(30));
903 assert_eq!(n.latest(), Some(dec!(30)));
904 }
905
906 #[test]
907 fn test_latest_updates_on_each_push() {
908 let mut n = norm(3);
909 n.update(dec!(1));
910 assert_eq!(n.latest(), Some(dec!(1)));
911 n.update(dec!(5));
912 assert_eq!(n.latest(), Some(dec!(5)));
913 }
914
915 #[test]
916 fn test_latest_returns_last_after_window_overflow() {
917 let mut n = norm(2); n.update(dec!(100));
919 n.update(dec!(200));
920 n.update(dec!(300)); assert_eq!(n.latest(), Some(dec!(300)));
922 }
923}
924
925pub struct ZScoreNormalizer {
948 window_size: usize,
949 window: VecDeque<Decimal>,
950 sum: Decimal,
951 sum_sq: Decimal,
952}
953
954impl ZScoreNormalizer {
955 pub fn new(window_size: usize) -> Result<Self, StreamError> {
961 if window_size == 0 {
962 return Err(StreamError::ConfigError {
963 reason: "ZScoreNormalizer window_size must be > 0".into(),
964 });
965 }
966 Ok(Self {
967 window_size,
968 window: VecDeque::with_capacity(window_size),
969 sum: Decimal::ZERO,
970 sum_sq: Decimal::ZERO,
971 })
972 }
973
974 pub fn update(&mut self, value: Decimal) {
980 if self.window.len() == self.window_size {
981 let evicted = self.window.pop_front().unwrap_or(Decimal::ZERO);
982 self.sum -= evicted;
983 self.sum_sq -= evicted * evicted;
984 }
985 self.window.push_back(value);
986 self.sum += value;
987 self.sum_sq += value * value;
988 }
989
990 #[must_use = "z-score is returned; ignoring it loses the normalized value"]
1002 pub fn normalize(&self, value: Decimal) -> Result<f64, StreamError> {
1003 let n = self.window.len();
1004 if n == 0 {
1005 return Err(StreamError::NormalizationError {
1006 reason: "window is empty; call update() before normalize()".into(),
1007 });
1008 }
1009 if n < 2 {
1010 return Ok(0.0);
1011 }
1012 let n_dec = Decimal::from(n as u64);
1013 let mean = self.sum / n_dec;
1014 let variance = (self.sum_sq / n_dec) - mean * mean;
1016 let variance = if variance < Decimal::ZERO {
1018 Decimal::ZERO
1019 } else {
1020 variance
1021 };
1022 let var_f64 = variance.to_f64().ok_or_else(|| StreamError::NormalizationError {
1023 reason: "Decimal-to-f64 conversion failed for variance".into(),
1024 })?;
1025 let std_dev = var_f64.sqrt();
1026 if std_dev < f64::EPSILON {
1027 return Ok(0.0);
1028 }
1029 let diff = value - mean;
1030 let diff_f64 = diff.to_f64().ok_or_else(|| StreamError::NormalizationError {
1031 reason: "Decimal-to-f64 conversion failed for diff".into(),
1032 })?;
1033 Ok(diff_f64 / std_dev)
1034 }
1035
1036 pub fn mean(&self) -> Option<Decimal> {
1038 if self.window.is_empty() {
1039 return None;
1040 }
1041 let n = Decimal::from(self.window.len() as u64);
1042 Some(self.sum / n)
1043 }
1044
1045 pub fn std_dev(&self) -> Option<f64> {
1051 let n = self.window.len();
1052 if n == 0 {
1053 return None;
1054 }
1055 if n < 2 {
1056 return Some(0.0);
1057 }
1058 let n_dec = Decimal::from(n as u64);
1059 let mean = self.sum / n_dec;
1060 let variance = (self.sum_sq / n_dec) - mean * mean;
1061 let variance = if variance < Decimal::ZERO { Decimal::ZERO } else { variance };
1062 let var_f64 = variance.to_f64().unwrap_or(0.0);
1063 Some(var_f64.sqrt())
1064 }
1065
1066 pub fn reset(&mut self) {
1068 self.window.clear();
1069 self.sum = Decimal::ZERO;
1070 self.sum_sq = Decimal::ZERO;
1071 }
1072
1073 pub fn len(&self) -> usize {
1075 self.window.len()
1076 }
1077
1078 pub fn is_empty(&self) -> bool {
1080 self.window.is_empty()
1081 }
1082
1083 pub fn window_size(&self) -> usize {
1085 self.window_size
1086 }
1087
1088 pub fn is_full(&self) -> bool {
1093 self.window.len() == self.window_size
1094 }
1095
1096 pub fn sum(&self) -> Option<Decimal> {
1101 if self.window.is_empty() {
1102 return None;
1103 }
1104 Some(self.sum)
1105 }
1106
1107 pub fn variance(&self) -> Option<Decimal> {
1112 let n = self.window.len();
1113 if n < 2 {
1114 return None;
1115 }
1116 let n_dec = Decimal::from(n as u64);
1117 let mean = self.sum / n_dec;
1118 let v = (self.sum_sq / n_dec) - mean * mean;
1119 Some(if v < Decimal::ZERO { Decimal::ZERO } else { v })
1120 }
1121
1122 pub fn std_dev_f64(&self) -> Option<f64> {
1126 self.variance_f64().map(|v| v.sqrt())
1127 }
1128
1129 pub fn variance_f64(&self) -> Option<f64> {
1133 use rust_decimal::prelude::ToPrimitive;
1134 self.variance()?.to_f64()
1135 }
1136
1137 pub fn normalize_batch(
1147 &mut self,
1148 values: &[Decimal],
1149 ) -> Result<Vec<f64>, StreamError> {
1150 values
1151 .iter()
1152 .map(|&v| {
1153 self.update(v);
1154 self.normalize(v)
1155 })
1156 .collect()
1157 }
1158
1159 pub fn is_outlier(&self, value: Decimal, z_threshold: f64) -> bool {
1164 let n = self.window.len();
1165 if n < 2 {
1166 return false;
1167 }
1168 let n_dec = Decimal::from(n as u64);
1169 let mean = self.sum / n_dec;
1170 let variance = (self.sum_sq / n_dec) - mean * mean;
1171 let variance = if variance < Decimal::ZERO { Decimal::ZERO } else { variance };
1172 let sd = variance.to_f64().unwrap_or(0.0).sqrt();
1173 if sd == 0.0 {
1174 return false;
1175 }
1176 let mean_f64 = mean.to_f64().unwrap_or(0.0);
1177 let val_f64 = value.to_f64().unwrap_or(mean_f64);
1178 ((val_f64 - mean_f64) / sd).abs() > z_threshold
1179 }
1180
1181 pub fn percentile_rank(&self, value: Decimal) -> Option<f64> {
1185 if self.window.is_empty() {
1186 return None;
1187 }
1188 let count = self.window.iter().filter(|&&v| v <= value).count();
1189 Some(count as f64 / self.window.len() as f64)
1190 }
1191
1192 pub fn running_min(&self) -> Option<Decimal> {
1196 self.window.iter().copied().reduce(Decimal::min)
1197 }
1198
1199 pub fn running_max(&self) -> Option<Decimal> {
1203 self.window.iter().copied().reduce(Decimal::max)
1204 }
1205
1206 pub fn window_range(&self) -> Option<Decimal> {
1210 let min = self.running_min()?;
1211 let max = self.running_max()?;
1212 Some(max - min)
1213 }
1214
1215 pub fn coefficient_of_variation(&self) -> Option<f64> {
1220 let mean = self.mean()?;
1221 if mean.is_zero() {
1222 return None;
1223 }
1224 let std_dev = self.std_dev()?;
1225 let mean_f = mean.abs().to_f64()?;
1226 Some(std_dev / mean_f)
1227 }
1228
1229 pub fn sample_variance(&self) -> Option<f64> {
1234 let sd = self.std_dev()?;
1235 Some(sd * sd)
1236 }
1237
1238 pub fn window_mean_f64(&self) -> Option<f64> {
1243 use rust_decimal::prelude::ToPrimitive;
1244 self.mean()?.to_f64()
1245 }
1246
1247 pub fn is_near_mean(&self, value: Decimal, sigma_tolerance: f64) -> bool {
1253 let n = self.window.len();
1254 if n < 2 {
1255 return false;
1256 }
1257 let n_dec = Decimal::from(n as u64);
1258 use rust_decimal::prelude::ToPrimitive;
1259 let mean = self.sum / n_dec;
1260 let variance: Decimal = self.window.iter()
1261 .map(|&x| {
1262 let diff = x - mean;
1263 diff * diff
1264 })
1265 .sum::<Decimal>() / n_dec;
1266 let std_dev = variance.to_f64().unwrap_or(0.0).sqrt();
1267 if std_dev == 0.0 {
1268 return true;
1269 }
1270 let diff = (value - mean).abs().to_f64().unwrap_or(f64::MAX);
1271 diff / std_dev <= sigma_tolerance
1272 }
1273
1274 pub fn window_sum(&self) -> Decimal {
1278 self.sum
1279 }
1280
1281 pub fn window_sum_f64(&self) -> f64 {
1285 use rust_decimal::prelude::ToPrimitive;
1286 self.sum.to_f64().unwrap_or(0.0)
1287 }
1288
1289 pub fn window_max_f64(&self) -> Option<f64> {
1293 use rust_decimal::prelude::ToPrimitive;
1294 self.window.iter().max().and_then(|v| v.to_f64())
1295 }
1296
1297 pub fn window_min_f64(&self) -> Option<f64> {
1301 use rust_decimal::prelude::ToPrimitive;
1302 self.window.iter().min().and_then(|v| v.to_f64())
1303 }
1304
1305 pub fn window_span_f64(&self) -> Option<f64> {
1309 let max = self.window_max_f64()?;
1310 let min = self.window_min_f64()?;
1311 Some(max - min)
1312 }
1313
1314 pub fn kurtosis(&self) -> Option<f64> {
1320 use rust_decimal::prelude::ToPrimitive;
1321 let n = self.window.len();
1322 if n < 4 {
1323 return None;
1324 }
1325 let n_f = n as f64;
1326 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
1327 if vals.len() < n {
1328 return None;
1329 }
1330 let mean = vals.iter().sum::<f64>() / n_f;
1331 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
1332 let std_dev = variance.sqrt();
1333 if std_dev == 0.0 {
1334 return None;
1335 }
1336 let kurt = vals.iter().map(|v| ((v - mean) / std_dev).powi(4)).sum::<f64>() / n_f - 3.0;
1337 Some(kurt)
1338 }
1339
1340 pub fn is_extreme(&self, value: Decimal, sigma: f64) -> bool {
1345 self.normalize(value)
1346 .ok()
1347 .map(|z| z.abs() > sigma)
1348 .unwrap_or(false)
1349 }
1350
1351 pub fn latest(&self) -> Option<Decimal> {
1353 self.window.back().copied()
1354 }
1355
1356 pub fn median(&self) -> Option<Decimal> {
1358 if self.window.is_empty() { return None; }
1359 let mut vals: Vec<Decimal> = self.window.iter().copied().collect();
1360 vals.sort();
1361 let mid = vals.len() / 2;
1362 if vals.len() % 2 == 0 {
1363 Some((vals[mid - 1] + vals[mid]) / Decimal::TWO)
1364 } else {
1365 Some(vals[mid])
1366 }
1367 }
1368
1369 pub fn percentile(&self, value: Decimal) -> Option<f64> {
1373 let n = self.window.len();
1374 if n == 0 { return None; }
1375 let count = self.window.iter().filter(|&&v| v <= value).count();
1376 Some(count as f64 / n as f64)
1377 }
1378
1379 pub fn ema_z_score(value: Decimal, alpha: f64, ema_mean: &mut f64, ema_var: &mut f64) -> Option<f64> {
1386 use rust_decimal::prelude::ToPrimitive;
1387 let v = value.to_f64()?;
1388 let delta = v - *ema_mean;
1389 *ema_mean += alpha * delta;
1390 *ema_var = (1.0 - alpha) * (*ema_var + alpha * delta * delta);
1391 let std = ema_var.sqrt();
1392 if std == 0.0 { return None; }
1393 Some((v - *ema_mean) / std)
1394 }
1395
1396 pub fn z_score_of_latest(&self) -> Option<f64> {
1400 let latest = self.latest()?;
1401 self.normalize(latest).ok()
1402 }
1403
1404 pub fn ema_of_z_scores(&self, alpha: f64) -> Option<f64> {
1409 let n = self.window.len();
1410 if n < 2 {
1411 return None;
1412 }
1413 let mut ema: Option<f64> = None;
1414 for &value in &self.window {
1415 if let Ok(z) = self.normalize(value) {
1416 ema = Some(match ema {
1417 None => z,
1418 Some(prev) => alpha * z + (1.0 - alpha) * prev,
1419 });
1420 }
1421 }
1422 ema
1423 }
1424
1425 pub fn add_observation(&mut self, value: Decimal) -> &mut Self {
1427 self.update(value);
1428 self
1429 }
1430
1431 pub fn deviation_from_mean(&self, value: Decimal) -> Option<f64> {
1435 use rust_decimal::prelude::ToPrimitive;
1436 let mean = self.mean()?.to_f64()?;
1437 value.to_f64().map(|v| v - mean)
1438 }
1439
1440 pub fn trim_outliers(&self, sigma: f64) -> Vec<Decimal> {
1445 use rust_decimal::prelude::ToPrimitive;
1446 if self.window.is_empty() { return vec![]; }
1447 let mean = match self.mean() { Some(m) => m, None => return vec![] };
1448 let std = match self.std_dev().and_then(|s| s.to_f64()) {
1449 Some(s) if s > 0.0 => s,
1450 _ => return self.window.iter().copied().collect(),
1451 };
1452 let mean_f64 = match mean.to_f64() { Some(m) => m, None => return vec![] };
1453 self.window.iter().copied()
1454 .filter(|v| {
1455 v.to_f64()
1456 .map(|vf| ((vf - mean_f64) / std).abs() <= sigma)
1457 .unwrap_or(false)
1458 })
1459 .collect()
1460 }
1461
1462 pub fn rolling_zscore_batch(&mut self, values: &[Decimal]) -> Vec<Option<f64>> {
1468 values.iter().map(|&v| {
1469 self.update(v);
1470 self.normalize(v).ok()
1471 }).collect()
1472 }
1473
1474 pub fn rolling_mean_change(&self) -> Option<f64> {
1480 let n = self.window.len();
1481 if n < 2 {
1482 return None;
1483 }
1484 let mid = n / 2;
1485 let first: Decimal = self.window.iter().take(mid).copied().sum::<Decimal>()
1486 / Decimal::from(mid as u64);
1487 let second: Decimal = self.window.iter().skip(mid).copied().sum::<Decimal>()
1488 / Decimal::from((n - mid) as u64);
1489 (second - first).to_f64()
1490 }
1491
1492 pub fn count_positive_z_scores(&self) -> usize {
1496 self.window
1497 .iter()
1498 .filter(|&&v| self.normalize(v).map_or(false, |z| z > 0.0))
1499 .count()
1500 }
1501
1502 pub fn is_mean_stable(&self, threshold: f64) -> bool {
1507 match self.rolling_mean_change() {
1508 Some(change) => change.abs() < threshold,
1509 None => false,
1510 }
1511 }
1512
1513 pub fn above_threshold_count(&self, z_threshold: f64) -> usize {
1517 self.window
1518 .iter()
1519 .filter(|&&v| {
1520 self.normalize(v)
1521 .map_or(false, |z| z.abs() > z_threshold)
1522 })
1523 .count()
1524 }
1525
1526 pub fn mad(&self) -> Option<Decimal> {
1531 let med = self.median()?;
1532 let mut deviations: Vec<Decimal> = self.window.iter().map(|&x| (x - med).abs()).collect();
1533 deviations.sort();
1534 let n = deviations.len();
1535 if n == 0 { return None; }
1536 let mid = n / 2;
1537 if n % 2 == 0 {
1538 Some((deviations[mid - 1] + deviations[mid]) / Decimal::TWO)
1539 } else {
1540 Some(deviations[mid])
1541 }
1542 }
1543
1544 pub fn robust_z_score(&self, value: Decimal) -> Option<f64> {
1549 use rust_decimal::prelude::ToPrimitive;
1550 let med = self.median()?;
1551 let mad = self.mad()?;
1552 if mad.is_zero() { return None; }
1553 ((value - med) / mad).to_f64()
1554 }
1555}
1556
1557#[cfg(test)]
1558mod zscore_tests {
1559 use super::*;
1560 use rust_decimal_macros::dec;
1561
1562 fn znorm(w: usize) -> ZScoreNormalizer {
1563 ZScoreNormalizer::new(w).unwrap()
1564 }
1565
1566 #[test]
1567 fn test_zscore_new_zero_window_returns_error() {
1568 assert!(matches!(
1569 ZScoreNormalizer::new(0),
1570 Err(StreamError::ConfigError { .. })
1571 ));
1572 }
1573
1574 #[test]
1575 fn test_zscore_is_full_false_before_capacity() {
1576 let mut n = znorm(3);
1577 assert!(!n.is_full());
1578 n.update(dec!(1));
1579 n.update(dec!(2));
1580 assert!(!n.is_full());
1581 n.update(dec!(3));
1582 assert!(n.is_full());
1583 }
1584
1585 #[test]
1586 fn test_zscore_is_full_stays_true_after_eviction() {
1587 let mut n = znorm(3);
1588 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
1589 n.update(v);
1590 }
1591 assert!(n.is_full());
1592 }
1593
1594 #[test]
1595 fn test_zscore_empty_window_returns_error() {
1596 let n = znorm(4);
1597 assert!(matches!(
1598 n.normalize(dec!(1)),
1599 Err(StreamError::NormalizationError { .. })
1600 ));
1601 }
1602
1603 #[test]
1604 fn test_zscore_single_value_returns_zero() {
1605 let mut n = znorm(4);
1606 n.update(dec!(50));
1607 assert_eq!(n.normalize(dec!(50)).unwrap(), 0.0);
1608 }
1609
1610 #[test]
1611 fn test_zscore_mean_is_zero() {
1612 let mut n = znorm(5);
1613 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] {
1614 n.update(v);
1615 }
1616 let z = n.normalize(dec!(30)).unwrap();
1618 assert!((z - 0.0).abs() < 1e-9, "z-score of mean should be 0, got {z}");
1619 }
1620
1621 #[test]
1622 fn test_zscore_symmetric_around_mean() {
1623 let mut n = znorm(4);
1624 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
1625 n.update(v);
1626 }
1627 let z_low = n.normalize(dec!(15)).unwrap();
1629 let z_high = n.normalize(dec!(35)).unwrap();
1630 assert!((z_low.abs() - z_high.abs()).abs() < 1e-9);
1631 assert!(z_low < 0.0, "below-mean z-score should be negative");
1632 assert!(z_high > 0.0, "above-mean z-score should be positive");
1633 }
1634
1635 #[test]
1636 fn test_zscore_all_same_returns_zero() {
1637 let mut n = znorm(4);
1638 for _ in 0..4 {
1639 n.update(dec!(100));
1640 }
1641 assert_eq!(n.normalize(dec!(100)).unwrap(), 0.0);
1642 }
1643
1644 #[test]
1645 fn test_zscore_rolling_window_eviction() {
1646 let mut n = znorm(3);
1647 n.update(dec!(1));
1648 n.update(dec!(2));
1649 n.update(dec!(3));
1650 n.update(dec!(100));
1652 let z = n.normalize(dec!(100)).unwrap();
1654 assert!(z > 0.0);
1655 }
1656
1657 #[test]
1658 fn test_zscore_reset_clears_state() {
1659 let mut n = znorm(4);
1660 for v in [dec!(10), dec!(20), dec!(30)] {
1661 n.update(v);
1662 }
1663 n.reset();
1664 assert!(n.is_empty());
1665 assert!(n.mean().is_none());
1666 assert!(matches!(
1667 n.normalize(dec!(1)),
1668 Err(StreamError::NormalizationError { .. })
1669 ));
1670 }
1671
1672 #[test]
1673 fn test_zscore_len_and_window_size() {
1674 let mut n = znorm(5);
1675 assert_eq!(n.len(), 0);
1676 assert!(n.is_empty());
1677 n.update(dec!(1));
1678 n.update(dec!(2));
1679 assert_eq!(n.len(), 2);
1680 assert_eq!(n.window_size(), 5);
1681 }
1682
1683 #[test]
1686 fn test_std_dev_none_when_empty() {
1687 let n = znorm(5);
1688 assert!(n.std_dev().is_none());
1689 }
1690
1691 #[test]
1692 fn test_std_dev_zero_with_one_observation() {
1693 let mut n = znorm(5);
1694 n.update(dec!(42));
1695 assert_eq!(n.std_dev(), Some(0.0));
1696 }
1697
1698 #[test]
1699 fn test_std_dev_zero_when_all_same() {
1700 let mut n = znorm(4);
1701 for _ in 0..4 {
1702 n.update(dec!(10));
1703 }
1704 let sd = n.std_dev().unwrap();
1705 assert!(sd < f64::EPSILON);
1706 }
1707
1708 #[test]
1709 fn test_std_dev_positive_for_varying_values() {
1710 let mut n = znorm(4);
1711 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
1712 n.update(v);
1713 }
1714 let sd = n.std_dev().unwrap();
1715 assert!((sd - 11.18).abs() < 0.01);
1717 }
1718
1719 #[test]
1722 fn test_variance_none_when_fewer_than_two_observations() {
1723 let mut n = znorm(5);
1724 assert!(n.variance().is_none());
1725 n.update(dec!(10));
1726 assert!(n.variance().is_none());
1727 }
1728
1729 #[test]
1730 fn test_variance_zero_for_identical_values() {
1731 let mut n = znorm(4);
1732 for _ in 0..4 {
1733 n.update(dec!(7));
1734 }
1735 assert_eq!(n.variance().unwrap(), dec!(0));
1736 }
1737
1738 #[test]
1739 fn test_variance_correct_for_known_values() {
1740 let mut n = znorm(4);
1741 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
1742 n.update(v);
1743 }
1744 let var = n.variance().unwrap();
1746 let var_f64 = f64::try_from(var).unwrap();
1747 assert!((var_f64 - 125.0).abs() < 0.01, "expected 125 got {var_f64}");
1748 }
1749
1750 #[test]
1753 fn test_normalize_batch_same_length_as_input() {
1754 let mut n = znorm(5);
1755 let vals = [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)];
1756 let out = n.normalize_batch(&vals).unwrap();
1757 assert_eq!(out.len(), vals.len());
1758 }
1759
1760 #[test]
1761 fn test_normalize_batch_last_value_matches_single_normalize() {
1762 let mut n1 = znorm(5);
1763 let vals = [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)];
1764 let batch = n1.normalize_batch(&vals).unwrap();
1765
1766 let mut n2 = znorm(5);
1767 for &v in &vals {
1768 n2.update(v);
1769 }
1770 let single = n2.normalize(dec!(50)).unwrap();
1771 assert!((batch[4] - single).abs() < 1e-9);
1772 }
1773
1774 #[test]
1775 fn test_sum_empty_returns_none() {
1776 let n = znorm(4);
1777 assert!(n.sum().is_none());
1778 }
1779
1780 #[test]
1781 fn test_sum_matches_manual() {
1782 let mut n = znorm(4);
1783 n.update(dec!(10));
1784 n.update(dec!(20));
1785 n.update(dec!(30));
1786 assert_eq!(n.sum().unwrap(), dec!(60));
1788 }
1789
1790 #[test]
1791 fn test_sum_evicts_old_values() {
1792 let mut n = znorm(2);
1793 n.update(dec!(10));
1794 n.update(dec!(20));
1795 n.update(dec!(30)); assert_eq!(n.sum().unwrap(), dec!(50));
1798 }
1799
1800 #[test]
1801 fn test_std_dev_single_observation_returns_some_zero() {
1802 let mut n = znorm(5);
1803 n.update(dec!(10));
1804 assert!(n.std_dev().is_none() || n.std_dev().unwrap() == 0.0);
1807 }
1808
1809 #[test]
1810 fn test_std_dev_constant_window_is_zero() {
1811 let mut n = znorm(4);
1812 for _ in 0..4 {
1813 n.update(dec!(5));
1814 }
1815 let sd = n.std_dev().unwrap();
1816 assert!(sd.abs() < 1e-9, "expected 0.0 got {sd}");
1817 }
1818
1819 #[test]
1820 fn test_std_dev_known_population() {
1821 let mut n = znorm(8);
1823 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
1824 n.update(v);
1825 }
1826 let sd = n.std_dev().unwrap();
1827 assert!((sd - 2.0).abs() < 1e-6, "expected ~2.0 got {sd}");
1828 }
1829
1830 #[test]
1833 fn test_window_range_none_when_empty() {
1834 let n = znorm(5);
1835 assert!(n.window_range().is_none());
1836 }
1837
1838 #[test]
1839 fn test_window_range_correct_value() {
1840 let mut n = znorm(5);
1841 n.update(dec!(10));
1842 n.update(dec!(20));
1843 n.update(dec!(15));
1844 assert_eq!(n.window_range().unwrap(), dec!(10));
1846 }
1847
1848 #[test]
1849 fn test_coefficient_of_variation_none_when_empty() {
1850 let n = znorm(5);
1851 assert!(n.coefficient_of_variation().is_none());
1852 }
1853
1854 #[test]
1855 fn test_coefficient_of_variation_none_when_mean_zero() {
1856 let mut n = znorm(5);
1857 n.update(dec!(-5));
1858 n.update(dec!(5)); assert!(n.coefficient_of_variation().is_none());
1860 }
1861
1862 #[test]
1863 fn test_coefficient_of_variation_positive_for_nonzero_mean() {
1864 let mut n = znorm(8);
1865 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
1866 n.update(v);
1867 }
1868 let cv = n.coefficient_of_variation().unwrap();
1870 assert!((cv - 0.4).abs() < 1e-5, "expected ~0.4 got {cv}");
1871 }
1872
1873 #[test]
1876 fn test_sample_variance_none_when_empty() {
1877 let n = znorm(5);
1878 assert!(n.sample_variance().is_none());
1879 }
1880
1881 #[test]
1882 fn test_sample_variance_zero_for_constant_window() {
1883 let mut n = znorm(3);
1884 n.update(dec!(7));
1885 n.update(dec!(7));
1886 n.update(dec!(7));
1887 assert!(n.sample_variance().unwrap().abs() < 1e-10);
1888 }
1889
1890 #[test]
1891 fn test_sample_variance_equals_std_dev_squared() {
1892 let mut n = znorm(8);
1893 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
1894 n.update(v);
1895 }
1896 let variance = n.sample_variance().unwrap();
1898 let sd = n.std_dev().unwrap();
1899 assert!((variance - sd * sd).abs() < 1e-10);
1900 }
1901
1902 #[test]
1905 fn test_window_mean_f64_none_when_empty() {
1906 let n = znorm(5);
1907 assert!(n.window_mean_f64().is_none());
1908 }
1909
1910 #[test]
1911 fn test_window_mean_f64_correct_value() {
1912 let mut n = znorm(4);
1913 n.update(dec!(10));
1914 n.update(dec!(20));
1915 let m = n.window_mean_f64().unwrap();
1917 assert!((m - 15.0).abs() < 1e-10);
1918 }
1919
1920 #[test]
1921 fn test_window_mean_f64_matches_decimal_mean() {
1922 let mut n = znorm(8);
1923 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
1924 n.update(v);
1925 }
1926 use rust_decimal::prelude::ToPrimitive;
1927 let expected = n.mean().unwrap().to_f64().unwrap();
1928 assert!((n.window_mean_f64().unwrap() - expected).abs() < 1e-10);
1929 }
1930
1931 #[test]
1934 fn test_kurtosis_none_when_fewer_than_4_observations() {
1935 let mut n = znorm(5);
1936 n.update(dec!(1));
1937 n.update(dec!(2));
1938 n.update(dec!(3));
1939 assert!(n.kurtosis().is_none());
1940 }
1941
1942 #[test]
1943 fn test_kurtosis_returns_some_with_4_observations() {
1944 let mut n = znorm(4);
1945 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
1946 n.update(v);
1947 }
1948 assert!(n.kurtosis().is_some());
1949 }
1950
1951 #[test]
1952 fn test_kurtosis_none_when_all_same_value() {
1953 let mut n = znorm(4);
1954 for _ in 0..4 {
1955 n.update(dec!(5));
1956 }
1957 assert!(n.kurtosis().is_none());
1959 }
1960
1961 #[test]
1962 fn test_kurtosis_uniform_distribution_is_negative() {
1963 let mut n = znorm(10);
1965 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5),
1966 dec!(6), dec!(7), dec!(8), dec!(9), dec!(10)] {
1967 n.update(v);
1968 }
1969 let k = n.kurtosis().unwrap();
1970 assert!(k < 0.0, "expected negative excess kurtosis for uniform dist, got {k}");
1972 }
1973
1974 #[test]
1976 fn test_is_near_mean_false_with_fewer_than_two_obs() {
1977 let mut n = znorm(5);
1978 n.update(dec!(10));
1979 assert!(!n.is_near_mean(dec!(10), 1.0));
1980 }
1981
1982 #[test]
1983 fn test_is_near_mean_true_within_one_sigma() {
1984 let mut n = znorm(10);
1985 for _ in 0..9 {
1987 n.update(dec!(10));
1988 }
1989 n.update(dec!(20));
1990 assert!(n.is_near_mean(dec!(11), 1.0));
1992 }
1993
1994 #[test]
1995 fn test_is_near_mean_false_when_far_from_mean() {
1996 let mut n = znorm(5);
1997 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] {
1998 n.update(v);
1999 }
2000 assert!(!n.is_near_mean(dec!(100), 2.0));
2002 }
2003
2004 #[test]
2005 fn test_is_near_mean_true_when_all_identical_any_value() {
2006 let mut n = znorm(4);
2007 for _ in 0..4 {
2008 n.update(dec!(7));
2009 }
2010 assert!(n.is_near_mean(dec!(999), 0.0));
2012 }
2013
2014 #[test]
2016 fn test_window_sum_f64_zero_on_empty() {
2017 let n = znorm(5);
2018 assert_eq!(n.window_sum_f64(), 0.0);
2019 }
2020
2021 #[test]
2022 fn test_window_sum_f64_correct_after_updates() {
2023 let mut n = znorm(5);
2024 n.update(dec!(10));
2025 n.update(dec!(20));
2026 n.update(dec!(30));
2027 assert!((n.window_sum_f64() - 60.0).abs() < 1e-10);
2028 }
2029
2030 #[test]
2031 fn test_window_sum_f64_rolls_out_old_values() {
2032 let mut n = znorm(2);
2033 n.update(dec!(100));
2034 n.update(dec!(200));
2035 n.update(dec!(300)); assert!((n.window_sum_f64() - 500.0).abs() < 1e-10);
2038 }
2039
2040 #[test]
2043 fn test_zscore_latest_none_when_empty() {
2044 let n = znorm(5);
2045 assert!(n.latest().is_none());
2046 }
2047
2048 #[test]
2049 fn test_zscore_latest_returns_most_recent() {
2050 let mut n = znorm(5);
2051 n.update(dec!(10));
2052 n.update(dec!(20));
2053 assert_eq!(n.latest(), Some(dec!(20)));
2054 }
2055
2056 #[test]
2057 fn test_zscore_latest_updates_on_roll() {
2058 let mut n = znorm(2);
2059 n.update(dec!(1));
2060 n.update(dec!(2));
2061 n.update(dec!(3)); assert_eq!(n.latest(), Some(dec!(3)));
2063 }
2064
2065 #[test]
2067 fn test_window_max_f64_none_on_empty() {
2068 let n = znorm(5);
2069 assert!(n.window_max_f64().is_none());
2070 }
2071
2072 #[test]
2073 fn test_window_max_f64_correct_value() {
2074 let mut n = znorm(5);
2075 for v in [dec!(3), dec!(7), dec!(1), dec!(5)] {
2076 n.update(v);
2077 }
2078 assert!((n.window_max_f64().unwrap() - 7.0).abs() < 1e-10);
2079 }
2080
2081 #[test]
2082 fn test_window_min_f64_none_on_empty() {
2083 let n = znorm(5);
2084 assert!(n.window_min_f64().is_none());
2085 }
2086
2087 #[test]
2088 fn test_window_min_f64_correct_value() {
2089 let mut n = znorm(5);
2090 for v in [dec!(3), dec!(7), dec!(1), dec!(5)] {
2091 n.update(v);
2092 }
2093 assert!((n.window_min_f64().unwrap() - 1.0).abs() < 1e-10);
2094 }
2095
2096 #[test]
2099 fn test_percentile_none_when_empty() {
2100 let n = znorm(5);
2101 assert!(n.percentile(dec!(10)).is_none());
2102 }
2103
2104 #[test]
2105 fn test_percentile_one_when_all_lte_value() {
2106 let mut n = znorm(4);
2107 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
2108 n.update(v);
2109 }
2110 assert!((n.percentile(dec!(4)).unwrap() - 1.0).abs() < 1e-9);
2111 }
2112
2113 #[test]
2114 fn test_percentile_zero_when_all_gt_value() {
2115 let mut n = znorm(4);
2116 for v in [dec!(5), dec!(6), dec!(7), dec!(8)] {
2117 n.update(v);
2118 }
2119 assert_eq!(n.percentile(dec!(4)).unwrap(), 0.0);
2121 }
2122
2123 #[test]
2124 fn test_percentile_half_at_median() {
2125 let mut n = znorm(4);
2126 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
2127 n.update(v);
2128 }
2129 assert!((n.percentile(dec!(2)).unwrap() - 0.5).abs() < 1e-9);
2131 }
2132
2133 #[test]
2136 fn test_z_score_of_latest_none_when_empty() {
2137 let n = znorm(5);
2138 assert!(n.z_score_of_latest().is_none());
2139 }
2140
2141 #[test]
2142 fn test_z_score_of_latest_zero_when_all_same() {
2143 let mut n = znorm(4);
2144 for _ in 0..4 {
2145 n.update(dec!(5));
2146 }
2147 assert_eq!(n.z_score_of_latest(), Some(0.0));
2149 }
2150
2151 #[test]
2152 fn test_z_score_of_latest_returns_some_with_variance() {
2153 let mut n = znorm(4);
2154 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
2155 n.update(v);
2156 }
2157 assert!(n.z_score_of_latest().is_some());
2159 }
2160
2161 #[test]
2162 fn test_deviation_from_mean_none_when_empty() {
2163 let n = znorm(5);
2164 assert!(n.deviation_from_mean(dec!(10)).is_none());
2165 }
2166
2167 #[test]
2168 fn test_deviation_from_mean_correct() {
2169 let mut n = znorm(4);
2170 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
2171 n.update(v);
2172 }
2173 let d = n.deviation_from_mean(dec!(4)).unwrap();
2175 assert!((d - 1.5).abs() < 1e-9);
2176 }
2177
2178 #[test]
2181 fn test_add_observation_same_as_update() {
2182 let mut n1 = znorm(4);
2183 let mut n2 = znorm(4);
2184 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
2185 n1.update(v);
2186 n2.add_observation(v);
2187 }
2188 assert_eq!(n1.mean(), n2.mean());
2189 }
2190
2191 #[test]
2192 fn test_add_observation_chainable() {
2193 let mut n = znorm(4);
2194 n.add_observation(dec!(1))
2195 .add_observation(dec!(2))
2196 .add_observation(dec!(3));
2197 assert_eq!(n.len(), 3);
2198 }
2199
2200 #[test]
2203 fn test_variance_f64_none_when_single_observation() {
2204 let mut n = znorm(4);
2205 n.update(dec!(5));
2206 assert!(n.variance_f64().is_none());
2207 }
2208
2209 #[test]
2210 fn test_variance_f64_zero_when_all_same() {
2211 let mut n = znorm(4);
2212 for _ in 0..4 { n.update(dec!(5)); }
2213 assert_eq!(n.variance_f64(), Some(0.0));
2214 }
2215
2216 #[test]
2217 fn test_variance_f64_positive_with_spread() {
2218 let mut n = znorm(4);
2219 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2220 assert!(n.variance_f64().unwrap() > 0.0);
2221 }
2222
2223 #[test]
2226 fn test_ema_of_z_scores_none_when_single_value() {
2227 let mut n = znorm(4);
2228 n.update(dec!(5));
2229 assert!(n.ema_of_z_scores(0.5).is_none());
2230 }
2231
2232 #[test]
2233 fn test_ema_of_z_scores_returns_some_with_variance() {
2234 let mut n = znorm(4);
2235 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
2236 n.update(v);
2237 }
2238 let ema = n.ema_of_z_scores(0.3);
2239 assert!(ema.is_some());
2240 }
2241
2242 #[test]
2243 fn test_ema_of_z_scores_zero_when_all_same() {
2244 let mut n = znorm(4);
2245 for _ in 0..4 { n.update(dec!(5)); }
2246 assert_eq!(n.ema_of_z_scores(0.5), Some(0.0));
2248 }
2249
2250 #[test]
2253 fn test_std_dev_f64_none_when_single_observation() {
2254 let mut n = znorm(4);
2255 n.update(dec!(5));
2256 assert!(n.std_dev_f64().is_none());
2257 }
2258
2259 #[test]
2260 fn test_std_dev_f64_zero_when_all_same() {
2261 let mut n = znorm(4);
2262 for _ in 0..4 { n.update(dec!(5)); }
2263 assert_eq!(n.std_dev_f64(), Some(0.0));
2264 }
2265
2266 #[test]
2267 fn test_std_dev_f64_equals_sqrt_of_variance() {
2268 let mut n = znorm(4);
2269 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2270 let var = n.variance_f64().unwrap();
2271 let std = n.std_dev_f64().unwrap();
2272 assert!((std - var.sqrt()).abs() < 1e-12);
2273 }
2274
2275 #[test]
2278 fn test_rolling_mean_change_none_when_one_observation() {
2279 let mut n = znorm(4);
2280 n.update(dec!(5));
2281 assert!(n.rolling_mean_change().is_none());
2282 }
2283
2284 #[test]
2285 fn test_rolling_mean_change_positive_when_rising() {
2286 let mut n = znorm(4);
2287 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2288 let change = n.rolling_mean_change().unwrap();
2290 assert!((change - 2.0).abs() < 1e-9);
2291 }
2292
2293 #[test]
2294 fn test_rolling_mean_change_negative_when_falling() {
2295 let mut n = znorm(4);
2296 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
2297 let change = n.rolling_mean_change().unwrap();
2298 assert!(change < 0.0);
2299 }
2300
2301 #[test]
2302 fn test_rolling_mean_change_zero_when_flat() {
2303 let mut n = znorm(4);
2304 for _ in 0..4 { n.update(dec!(7)); }
2305 let change = n.rolling_mean_change().unwrap();
2306 assert!(change.abs() < 1e-9);
2307 }
2308
2309 #[test]
2312 fn test_window_span_f64_none_when_empty() {
2313 let n = znorm(4);
2314 assert!(n.window_span_f64().is_none());
2315 }
2316
2317 #[test]
2318 fn test_window_span_f64_zero_when_all_same() {
2319 let mut n = znorm(4);
2320 for _ in 0..4 { n.update(dec!(5)); }
2321 assert_eq!(n.window_span_f64(), Some(0.0));
2322 }
2323
2324 #[test]
2325 fn test_window_span_f64_correct_value() {
2326 let mut n = znorm(4);
2327 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2328 assert!((n.window_span_f64().unwrap() - 30.0).abs() < 1e-9);
2330 }
2331
2332 #[test]
2335 fn test_count_positive_z_scores_zero_when_empty() {
2336 let n = znorm(4);
2337 assert_eq!(n.count_positive_z_scores(), 0);
2338 }
2339
2340 #[test]
2341 fn test_count_positive_z_scores_zero_when_all_same() {
2342 let mut n = znorm(4);
2343 for _ in 0..4 { n.update(dec!(5)); }
2344 assert_eq!(n.count_positive_z_scores(), 0);
2345 }
2346
2347 #[test]
2348 fn test_count_positive_z_scores_half_above_mean() {
2349 let mut n = znorm(4);
2350 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2351 assert_eq!(n.count_positive_z_scores(), 2);
2353 }
2354
2355 #[test]
2358 fn test_above_threshold_count_zero_when_empty() {
2359 let n = znorm(4);
2360 assert_eq!(n.above_threshold_count(1.0), 0);
2361 }
2362
2363 #[test]
2364 fn test_above_threshold_count_zero_when_all_same() {
2365 let mut n = znorm(4);
2366 for _ in 0..4 { n.update(dec!(5)); }
2367 assert_eq!(n.above_threshold_count(0.5), 0);
2368 }
2369
2370 #[test]
2371 fn test_above_threshold_count_correct_with_extremes() {
2372 let mut n = znorm(6);
2373 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(100)] { n.update(v); }
2374 assert!(n.above_threshold_count(1.0) >= 1);
2376 }
2377}
2378
2379#[cfg(test)]
2380mod minmax_extra_tests {
2381 use super::*;
2382 use rust_decimal_macros::dec;
2383
2384 fn norm(w: usize) -> MinMaxNormalizer {
2385 MinMaxNormalizer::new(w).unwrap()
2386 }
2387
2388 #[test]
2391 fn test_fraction_above_mid_none_when_empty() {
2392 let mut n = norm(4);
2393 assert!(n.fraction_above_mid().is_none());
2394 }
2395
2396 #[test]
2397 fn test_fraction_above_mid_zero_when_all_same() {
2398 let mut n = norm(4);
2399 for _ in 0..4 { n.update(dec!(5)); }
2400 assert_eq!(n.fraction_above_mid(), Some(0.0));
2401 }
2402
2403 #[test]
2404 fn test_fraction_above_mid_half_when_symmetric() {
2405 let mut n = norm(4);
2406 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2407 let f = n.fraction_above_mid().unwrap();
2409 assert!((f - 0.5).abs() < 1e-10);
2410 }
2411}
2412
2413#[cfg(test)]
2414mod zscore_stability_tests {
2415 use super::*;
2416 use rust_decimal_macros::dec;
2417
2418 fn znorm(w: usize) -> ZScoreNormalizer {
2419 ZScoreNormalizer::new(w).unwrap()
2420 }
2421
2422 #[test]
2425 fn test_is_mean_stable_false_when_window_too_small() {
2426 let n = znorm(4);
2427 assert!(!n.is_mean_stable(1.0));
2428 }
2429
2430 #[test]
2431 fn test_is_mean_stable_true_when_flat() {
2432 let mut n = znorm(4);
2433 for _ in 0..4 { n.update(dec!(5)); }
2434 assert!(n.is_mean_stable(0.001));
2435 }
2436
2437 #[test]
2438 fn test_is_mean_stable_false_when_trending() {
2439 let mut n = znorm(4);
2440 for v in [dec!(1), dec!(2), dec!(10), dec!(20)] { n.update(v); }
2441 assert!(!n.is_mean_stable(0.5));
2442 }
2443}