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 self.min_max().map_or(value, |(min, max)| value.max(min).min(max))
215 }
216
217 pub fn midpoint(&mut self) -> Option<Decimal> {
221 let (min, max) = self.min_max()?;
222 Some((min + max) / Decimal::TWO)
223 }
224
225 pub fn reset(&mut self) {
227 self.window.clear();
228 self.cached_min = Decimal::MAX;
229 self.cached_max = Decimal::MIN;
230 self.dirty = false;
231 }
232
233 pub fn len(&self) -> usize {
235 self.window.len()
236 }
237
238 pub fn is_empty(&self) -> bool {
241 self.window.is_empty()
242 }
243
244 pub fn window_size(&self) -> usize {
246 self.window_size
247 }
248
249 pub fn is_full(&self) -> bool {
254 self.window.len() == self.window_size
255 }
256
257 pub fn min(&mut self) -> Option<Decimal> {
262 self.min_max().map(|(min, _)| min)
263 }
264
265 pub fn max(&mut self) -> Option<Decimal> {
270 self.min_max().map(|(_, max)| max)
271 }
272
273 pub fn mean(&self) -> Option<Decimal> {
277 if self.window.is_empty() {
278 return None;
279 }
280 let sum: Decimal = self.window.iter().copied().sum();
281 Some(sum / Decimal::from(self.window.len() as u64))
282 }
283
284 pub fn variance(&self) -> Option<Decimal> {
288 let n = self.window.len();
289 if n < 2 {
290 return None;
291 }
292 let mean = self.mean()?;
293 let variance = self
294 .window
295 .iter()
296 .map(|&v| { let d = v - mean; d * d })
297 .sum::<Decimal>()
298 / Decimal::from(n as u64);
299 Some(variance)
300 }
301
302 pub fn std_dev(&self) -> Option<f64> {
307 use rust_decimal::prelude::ToPrimitive;
308 self.variance()?.to_f64().map(f64::sqrt)
309 }
310
311 pub fn coefficient_of_variation(&self) -> Option<f64> {
316 use rust_decimal::prelude::ToPrimitive;
317 let mean = self.mean()?;
318 if mean.is_zero() {
319 return None;
320 }
321 let std_dev = self.std_dev()?;
322 let mean_f = mean.abs().to_f64()?;
323 Some(std_dev / mean_f)
324 }
325
326 pub fn normalize_batch(
335 &mut self,
336 values: &[rust_decimal::Decimal],
337 ) -> Result<Vec<f64>, crate::error::StreamError> {
338 values
339 .iter()
340 .map(|&v| {
341 self.update(v);
342 self.normalize(v)
343 })
344 .collect()
345 }
346
347 #[deprecated(since = "2.2.0", note = "Use `normalize()` instead — it already clamps to [0.0, 1.0]")]
357 pub fn normalize_clamp(
358 &mut self,
359 value: rust_decimal::Decimal,
360 ) -> Result<f64, crate::error::StreamError> {
361 self.normalize(value)
362 }
363
364 pub fn z_score(&self, value: Decimal) -> Option<f64> {
374 use rust_decimal::prelude::ToPrimitive;
375 let std_dev = self.std_dev()?; if std_dev == 0.0 {
377 return None;
378 }
379 let mean = self.mean()?;
380 let value_f64 = value.to_f64()?;
381 let mean_f64 = mean.to_f64()?;
382 Some((value_f64 - mean_f64) / std_dev)
383 }
384
385 pub fn percentile_rank(&self, value: rust_decimal::Decimal) -> Option<f64> {
393 if self.window.is_empty() {
394 return None;
395 }
396 let count_le = self
397 .window
398 .iter()
399 .filter(|&&v| v <= value)
400 .count();
401 Some(count_le as f64 / self.window.len() as f64)
402 }
403
404 pub fn count_above(&self, threshold: rust_decimal::Decimal) -> usize {
406 self.window.iter().filter(|&&v| v > threshold).count()
407 }
408
409 pub fn count_below(&self, threshold: rust_decimal::Decimal) -> usize {
413 self.window.iter().filter(|&&v| v < threshold).count()
414 }
415
416 pub fn percentile_value(&self, p: f64) -> Option<Decimal> {
421 if self.window.is_empty() {
422 return None;
423 }
424 let p = p.clamp(0.0, 1.0);
425 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
426 sorted.sort();
427 let n = sorted.len();
428 if n == 1 {
429 return Some(sorted[0]);
430 }
431 let idx = p * (n - 1) as f64;
432 let lo = idx.floor() as usize;
433 let hi = idx.ceil() as usize;
434 if lo == hi {
435 Some(sorted[lo])
436 } else {
437 let frac = Decimal::try_from(idx - lo as f64).ok()?;
438 Some(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
439 }
440 }
441
442 pub fn fraction_above_mid(&mut self) -> Option<f64> {
446 let (min, max) = self.min_max()?;
447 let mid = (min + max) / rust_decimal::Decimal::TWO;
448 let above = self.window.iter().filter(|&&v| v > mid).count();
449 Some(above as f64 / self.window.len() as f64)
450 }
451
452 pub fn normalized_range(&mut self) -> Option<f64> {
457 use rust_decimal::prelude::ToPrimitive;
458 let (min, max) = self.min_max()?;
459 if max.is_zero() {
460 return None;
461 }
462 ((max - min) / max).to_f64()
463 }
464
465 pub fn ewma(&self, alpha: f64) -> Option<f64> {
470 if self.window.is_empty() {
471 return None;
472 }
473 let alpha = alpha.clamp(f64::MIN_POSITIVE, 1.0);
474 let one_minus = 1.0 - alpha;
475 let mut ewma = self.window[0].to_f64().unwrap_or(0.0);
476 for &v in self.window.iter().skip(1) {
477 ewma = alpha * v.to_f64().unwrap_or(ewma) + one_minus * ewma;
478 }
479 Some(ewma)
480 }
481
482 pub fn interquartile_range(&self) -> Option<Decimal> {
487 let n = self.window.len();
488 if n < 4 {
489 return None;
490 }
491 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
492 sorted.sort();
493 let q1_idx = n / 4;
494 let q3_idx = 3 * n / 4;
495 Some(sorted[q3_idx] - sorted[q1_idx])
496 }
497
498 pub fn skewness(&self) -> Option<f64> {
503 use rust_decimal::prelude::ToPrimitive;
504 let n = self.window.len();
505 if n < 3 {
506 return None;
507 }
508 let n_f = n as f64;
509 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
510 if vals.len() < n {
511 return None;
512 }
513 let mean = vals.iter().sum::<f64>() / n_f;
514 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
515 let std_dev = variance.sqrt();
516 if std_dev == 0.0 {
517 return None;
518 }
519 let skew = vals.iter().map(|v| ((v - mean) / std_dev).powi(3)).sum::<f64>() / n_f;
520 Some(skew)
521 }
522
523 pub fn kurtosis(&self) -> Option<f64> {
532 use rust_decimal::prelude::ToPrimitive;
533 let n = self.window.len();
534 if n < 4 {
535 return None;
536 }
537 let n_f = n as f64;
538 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
539 if vals.len() < n {
540 return None;
541 }
542 let mean = vals.iter().sum::<f64>() / n_f;
543 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
544 let std_dev = variance.sqrt();
545 if std_dev == 0.0 {
546 return None;
547 }
548 let kurt = vals.iter().map(|v| ((v - mean) / std_dev).powi(4)).sum::<f64>() / n_f - 3.0;
549 Some(kurt)
550 }
551
552 pub fn median(&self) -> Option<Decimal> {
556 if self.window.is_empty() {
557 return None;
558 }
559 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
560 sorted.sort();
561 let n = sorted.len();
562 if n % 2 == 1 {
563 Some(sorted[n / 2])
564 } else {
565 Some((sorted[n / 2 - 1] + sorted[n / 2]) / Decimal::from(2u64))
566 }
567 }
568
569 pub fn sample_variance(&self) -> Option<f64> {
573 use rust_decimal::prelude::ToPrimitive;
574 let n = self.window.len();
575 if n < 2 {
576 return None;
577 }
578 let mean = self.mean()?.to_f64()?;
579 let sum_sq: f64 = self.window.iter()
580 .filter_map(|v| v.to_f64())
581 .map(|v| (v - mean).powi(2))
582 .sum();
583 Some(sum_sq / (n - 1) as f64)
584 }
585
586 pub fn mad(&self) -> Option<Decimal> {
590 let med = self.median()?;
591 let mut deviations: Vec<Decimal> = self.window.iter()
592 .map(|&v| (v - med).abs())
593 .collect();
594 deviations.sort();
595 let n = deviations.len();
596 if n % 2 == 1 {
597 Some(deviations[n / 2])
598 } else {
599 Some((deviations[n / 2 - 1] + deviations[n / 2]) / Decimal::from(2u64))
600 }
601 }
602
603 pub fn robust_z_score(&self, value: Decimal) -> Option<f64> {
610 use rust_decimal::prelude::ToPrimitive;
611 let med = self.median()?;
612 let mad = self.mad()?;
613 if mad.is_zero() {
614 return None;
615 }
616 let diff = (value - med) / mad;
617 Some(0.674_5 * diff.to_f64()?)
618 }
619
620 pub fn latest(&self) -> Option<Decimal> {
622 self.window.back().copied()
623 }
624
625 pub fn sum(&self) -> Option<Decimal> {
629 if self.window.is_empty() {
630 return None;
631 }
632 Some(self.window.iter().copied().sum())
633 }
634
635 pub fn is_outlier(&self, value: Decimal, z_threshold: f64) -> bool {
637 self.z_score(value).map_or(false, |z| z.abs() > z_threshold)
638 }
639
640 pub fn trim_outliers(&self, sigma: f64) -> Vec<Decimal> {
646 self.window
647 .iter()
648 .copied()
649 .filter(|&v| !self.is_outlier(v, sigma))
650 .collect()
651 }
652
653 pub fn z_score_of_latest(&self) -> Option<f64> {
658 self.z_score(self.latest()?)
659 }
660
661 pub fn deviation_from_mean(&self, value: Decimal) -> Option<f64> {
666 use rust_decimal::prelude::ToPrimitive;
667 let mean = self.mean()?;
668 (value - mean).to_f64()
669 }
670
671 pub fn range_f64(&mut self) -> Option<f64> {
675 use rust_decimal::prelude::ToPrimitive;
676 self.range()?.to_f64()
677 }
678
679 pub fn sum_f64(&self) -> Option<f64> {
683 use rust_decimal::prelude::ToPrimitive;
684 self.sum()?.to_f64()
685 }
686
687 pub fn values(&self) -> Vec<Decimal> {
689 self.window.iter().copied().collect()
690 }
691
692 pub fn normalized_midpoint(&mut self) -> Option<f64> {
696 let mid = self.midpoint()?;
697 self.normalize(mid).ok()
698 }
699
700 pub fn is_at_min(&mut self, value: Decimal) -> bool {
704 self.min().map_or(false, |m| value == m)
705 }
706
707 pub fn is_at_max(&mut self, value: Decimal) -> bool {
711 self.max().map_or(false, |m| value == m)
712 }
713
714 pub fn fraction_above(&self, threshold: Decimal) -> Option<f64> {
718 if self.window.is_empty() {
719 return None;
720 }
721 Some(self.count_above(threshold) as f64 / self.window.len() as f64)
722 }
723
724 pub fn fraction_below(&self, threshold: Decimal) -> Option<f64> {
728 if self.window.is_empty() {
729 return None;
730 }
731 Some(self.count_below(threshold) as f64 / self.window.len() as f64)
732 }
733
734 pub fn window_values_above(&self, threshold: Decimal) -> Vec<Decimal> {
736 self.window.iter().copied().filter(|&v| v > threshold).collect()
737 }
738
739 pub fn window_values_below(&self, threshold: Decimal) -> Vec<Decimal> {
741 self.window.iter().copied().filter(|&v| v < threshold).collect()
742 }
743
744 pub fn count_equal(&self, value: Decimal) -> usize {
746 self.window.iter().filter(|&&v| v == value).count()
747 }
748
749 pub fn rolling_range(&self) -> Option<Decimal> {
753 if self.window.is_empty() {
754 return None;
755 }
756 let lo = self.window.iter().copied().reduce(Decimal::min)?;
757 let hi = self.window.iter().copied().reduce(Decimal::max)?;
758 Some(hi - lo)
759 }
760
761 pub fn autocorrelation_lag1(&self) -> Option<f64> {
766 use rust_decimal::prelude::ToPrimitive;
767 let n = self.window.len();
768 if n < 2 {
769 return None;
770 }
771 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
772 if vals.len() < 2 {
773 return None;
774 }
775 let mean = vals.iter().sum::<f64>() / vals.len() as f64;
776 let var: f64 = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / vals.len() as f64;
777 if var == 0.0 {
778 return None;
779 }
780 let cov: f64 = vals.windows(2).map(|w| (w[0] - mean) * (w[1] - mean)).sum::<f64>()
781 / (vals.len() - 1) as f64;
782 Some(cov / var)
783 }
784
785 pub fn trend_consistency(&self) -> Option<f64> {
789 let n = self.window.len();
790 if n < 2 {
791 return None;
792 }
793 let up = self.window.iter().collect::<Vec<_>>().windows(2)
794 .filter(|w| w[1] > w[0]).count();
795 Some(up as f64 / (n - 1) as f64)
796 }
797
798 pub fn mean_absolute_deviation(&self) -> Option<f64> {
802 use rust_decimal::prelude::ToPrimitive;
803 if self.window.is_empty() {
804 return None;
805 }
806 let n = self.window.len();
807 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
808 let mean = vals.iter().sum::<f64>() / n as f64;
809 let mad = vals.iter().map(|v| (v - mean).abs()).sum::<f64>() / n as f64;
810 Some(mad)
811 }
812
813 pub fn percentile_of_latest(&self) -> Option<f64> {
818 let latest = self.latest()?;
819 self.percentile_rank(latest)
820 }
821
822 pub fn tail_ratio(&self) -> Option<f64> {
828 use rust_decimal::prelude::ToPrimitive;
829 let max = self.window.iter().copied().reduce(Decimal::max)?;
830 let p75 = self.percentile_value(0.75)?;
831 if p75.is_zero() {
832 return None;
833 }
834 (max / p75).to_f64()
835 }
836
837 pub fn z_score_of_min(&self) -> Option<f64> {
841 let min = self.window.iter().copied().reduce(Decimal::min)?;
842 self.z_score(min)
843 }
844
845 pub fn z_score_of_max(&self) -> Option<f64> {
849 let max = self.window.iter().copied().reduce(Decimal::max)?;
850 self.z_score(max)
851 }
852
853 pub fn window_entropy(&self) -> Option<f64> {
859 if self.window.is_empty() {
860 return None;
861 }
862 let n = self.window.len() as f64;
863 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
864 for v in &self.window {
865 *counts.entry(v.to_string()).or_insert(0) += 1;
866 }
867 let entropy: f64 = counts.values().map(|&c| {
868 let p = c as f64 / n;
869 -p * p.ln()
870 }).sum();
871 Some(entropy)
872 }
873
874 pub fn normalized_std_dev(&self) -> Option<f64> {
876 self.coefficient_of_variation()
877 }
878
879 pub fn value_above_mean_count(&self) -> Option<usize> {
883 let mean = self.mean()?;
884 Some(self.window.iter().filter(|&&v| v > mean).count())
885 }
886
887 pub fn consecutive_above_mean(&self) -> Option<usize> {
891 let mean = self.mean()?;
892 let mut max_run = 0usize;
893 let mut current = 0usize;
894 for &v in &self.window {
895 if v > mean {
896 current += 1;
897 if current > max_run {
898 max_run = current;
899 }
900 } else {
901 current = 0;
902 }
903 }
904 Some(max_run)
905 }
906
907 pub fn above_threshold_fraction(&self, threshold: Decimal) -> Option<f64> {
911 if self.window.is_empty() {
912 return None;
913 }
914 let count = self.window.iter().filter(|&&v| v > threshold).count();
915 Some(count as f64 / self.window.len() as f64)
916 }
917
918 pub fn below_threshold_fraction(&self, threshold: Decimal) -> Option<f64> {
922 if self.window.is_empty() {
923 return None;
924 }
925 let count = self.window.iter().filter(|&&v| v < threshold).count();
926 Some(count as f64 / self.window.len() as f64)
927 }
928
929 pub fn lag_k_autocorrelation(&self, k: usize) -> Option<f64> {
933 use rust_decimal::prelude::ToPrimitive;
934 let n = self.window.len();
935 if k == 0 || k >= n {
936 return None;
937 }
938 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
939 if vals.len() != n {
940 return None;
941 }
942 let mean = vals.iter().sum::<f64>() / n as f64;
943 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
944 if var == 0.0 {
945 return None;
946 }
947 let m = n - k;
948 let cov: f64 = (0..m).map(|i| (vals[i] - mean) * (vals[i + k] - mean)).sum::<f64>() / m as f64;
949 Some(cov / var)
950 }
951
952 pub fn half_life_estimate(&self) -> Option<f64> {
959 use rust_decimal::prelude::ToPrimitive;
960 let n = self.window.len();
961 if n < 3 {
962 return None;
963 }
964 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
965 if vals.len() != n {
966 return None;
967 }
968 let diffs: Vec<f64> = vals.windows(2).map(|w| w[1] - w[0]).collect();
969 let lagged: Vec<f64> = vals[..n - 1].to_vec();
970 let nf = diffs.len() as f64;
971 let mean_l = lagged.iter().sum::<f64>() / nf;
972 let mean_d = diffs.iter().sum::<f64>() / nf;
973 let cov: f64 = lagged.iter().zip(diffs.iter()).map(|(l, d)| (l - mean_l) * (d - mean_d)).sum::<f64>();
974 let var: f64 = lagged.iter().map(|l| (l - mean_l).powi(2)).sum::<f64>();
975 if var == 0.0 {
976 return None;
977 }
978 let beta = cov / var;
979 if beta >= 0.0 {
981 return None;
982 }
983 let lambda = (1.0 + beta).abs().ln();
984 if lambda == 0.0 {
985 return None;
986 }
987 Some(-std::f64::consts::LN_2 / lambda)
988 }
989
990 pub fn geometric_mean(&self) -> Option<f64> {
995 use rust_decimal::prelude::ToPrimitive;
996 if self.window.is_empty() {
997 return None;
998 }
999 let logs: Vec<f64> = self.window.iter()
1000 .filter_map(|v| v.to_f64())
1001 .filter_map(|f| if f > 0.0 { Some(f.ln()) } else { None })
1002 .collect();
1003 if logs.len() != self.window.len() {
1004 return None;
1005 }
1006 Some((logs.iter().sum::<f64>() / logs.len() as f64).exp())
1007 }
1008
1009 pub fn harmonic_mean(&self) -> Option<f64> {
1014 use rust_decimal::prelude::ToPrimitive;
1015 if self.window.is_empty() {
1016 return None;
1017 }
1018 let reciprocals: Vec<f64> = self.window.iter()
1019 .filter_map(|v| v.to_f64())
1020 .filter_map(|f| if f != 0.0 { Some(1.0 / f) } else { None })
1021 .collect();
1022 if reciprocals.len() != self.window.len() {
1023 return None;
1024 }
1025 let n = reciprocals.len() as f64;
1026 Some(n / reciprocals.iter().sum::<f64>())
1027 }
1028
1029 pub fn range_normalized_value(&self, value: Decimal) -> Option<f64> {
1033 use rust_decimal::prelude::ToPrimitive;
1034 let min = self.window.iter().copied().reduce(Decimal::min)?;
1035 let max = self.window.iter().copied().reduce(Decimal::max)?;
1036 let range = max - min;
1037 if range.is_zero() {
1038 return None;
1039 }
1040 ((value - min) / range).to_f64()
1041 }
1042
1043 pub fn distance_from_median(&self, value: Decimal) -> Option<f64> {
1047 use rust_decimal::prelude::ToPrimitive;
1048 let med = self.median()?;
1049 (value - med).to_f64()
1050 }
1051
1052 pub fn momentum(&self) -> Option<f64> {
1057 use rust_decimal::prelude::ToPrimitive;
1058 if self.window.len() < 2 {
1059 return None;
1060 }
1061 let oldest = *self.window.front()?;
1062 let latest = *self.window.back()?;
1063 (latest - oldest).to_f64()
1064 }
1065
1066 pub fn value_rank(&self, value: Decimal) -> Option<f64> {
1071 if self.window.is_empty() {
1072 return None;
1073 }
1074 let n = self.window.len();
1075 let below = self.window.iter().filter(|&&v| v < value).count();
1076 Some(below as f64 / n as f64)
1077 }
1078
1079 pub fn coeff_of_variation(&self) -> Option<f64> {
1084 use rust_decimal::prelude::ToPrimitive;
1085 let n = self.window.len();
1086 if n < 2 {
1087 return None;
1088 }
1089 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
1090 if vals.len() < 2 {
1091 return None;
1092 }
1093 let nf = vals.len() as f64;
1094 let mean = vals.iter().sum::<f64>() / nf;
1095 if mean == 0.0 {
1096 return None;
1097 }
1098 let std_dev = (vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (nf - 1.0)).sqrt();
1099 Some(std_dev / mean.abs())
1100 }
1101
1102 pub fn quantile_range(&self) -> Option<f64> {
1106 use rust_decimal::prelude::ToPrimitive;
1107 let q3 = self.percentile_value(0.75)?;
1108 let q1 = self.percentile_value(0.25)?;
1109 (q3 - q1).to_f64()
1110 }
1111
1112 pub fn upper_quartile(&self) -> Option<Decimal> {
1116 self.percentile_value(0.75)
1117 }
1118
1119 pub fn lower_quartile(&self) -> Option<Decimal> {
1123 self.percentile_value(0.25)
1124 }
1125
1126 pub fn sign_change_rate(&self) -> Option<f64> {
1134 let n = self.window.len();
1135 if n < 3 {
1136 return None;
1137 }
1138 let vals: Vec<&Decimal> = self.window.iter().collect();
1139 let diffs: Vec<i32> = vals
1140 .windows(2)
1141 .map(|w| {
1142 if w[1] > w[0] { 1 } else if w[1] < w[0] { -1 } else { 0 }
1143 })
1144 .collect();
1145 let total_pairs = (diffs.len() - 1) as f64;
1146 if total_pairs == 0.0 {
1147 return None;
1148 }
1149 let changes = diffs
1150 .windows(2)
1151 .filter(|w| w[0] != 0 && w[1] != 0 && w[0] != w[1])
1152 .count();
1153 Some(changes as f64 / total_pairs)
1154 }
1155
1156 pub fn consecutive_below_mean(&self) -> Option<usize> {
1163 if self.window.len() < 2 {
1164 return None;
1165 }
1166 let mean = self.mean()?;
1167 let count = self.window.iter().rev().take_while(|&&v| v < mean).count();
1168 Some(count)
1169 }
1170
1171 pub fn drift_rate(&self) -> Option<f64> {
1177 use rust_decimal::prelude::ToPrimitive;
1178 let n = self.window.len();
1179 if n < 2 {
1180 return None;
1181 }
1182 let mid = n / 2;
1183 let first_sum: Decimal = self.window.iter().take(mid).copied().sum();
1184 let second_sum: Decimal = self.window.iter().skip(mid).copied().sum();
1185 let mean1 = first_sum / Decimal::from(mid as i64);
1186 let mean2 = second_sum / Decimal::from((n - mid) as i64);
1187 if mean1.is_zero() {
1188 return None;
1189 }
1190 ((mean2 - mean1) / mean1.abs()).to_f64()
1191 }
1192
1193 pub fn peak_to_trough_ratio(&self) -> Option<f64> {
1197 use rust_decimal::prelude::ToPrimitive;
1198 let mut tmp = MinMaxNormalizer::new(self.window_size).ok()?;
1199 for &v in &self.window {
1200 tmp.update(v);
1201 }
1202 let (min, max) = tmp.min_max()?;
1203 if min.is_zero() {
1204 return None;
1205 }
1206 (max / min).to_f64()
1207 }
1208
1209 pub fn normalized_deviation(&self) -> Option<f64> {
1214 use rust_decimal::prelude::ToPrimitive;
1215 let latest = self.latest()?;
1216 let mean = self.mean()?;
1217 let range = self.rolling_range()?;
1218 if range.is_zero() {
1219 return None;
1220 }
1221 ((latest - mean) / range).to_f64()
1222 }
1223
1224 pub fn window_cv_pct(&self) -> Option<f64> {
1228 let cv = self.coefficient_of_variation()?;
1229 Some(cv * 100.0)
1230 }
1231
1232 pub fn latest_rank_pct(&self) -> Option<f64> {
1238 if self.window.len() < 2 {
1239 return None;
1240 }
1241 let latest = self.latest()?;
1242 let below = self.window.iter().filter(|&&v| v < latest).count();
1243 Some(below as f64 / (self.window.len() - 1) as f64)
1244 }
1245
1246 pub fn trimmed_mean(&self, p: f64) -> Option<f64> {
1254 use rust_decimal::prelude::ToPrimitive;
1255 if self.window.is_empty() {
1256 return None;
1257 }
1258 let p = p.clamp(0.0, 0.499);
1259 let mut sorted: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
1260 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1261 let n = sorted.len();
1262 let trim = (n as f64 * p).floor() as usize;
1263 let trimmed = &sorted[trim..n - trim];
1264 if trimmed.is_empty() {
1265 return None;
1266 }
1267 Some(trimmed.iter().sum::<f64>() / trimmed.len() as f64)
1268 }
1269
1270 pub fn linear_trend_slope(&self) -> Option<f64> {
1275 use rust_decimal::prelude::ToPrimitive;
1276 let n = self.window.len();
1277 if n < 2 {
1278 return None;
1279 }
1280 let n_f = n as f64;
1281 let x_mean = (n_f - 1.0) / 2.0;
1282 let y_vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
1283 if y_vals.len() < 2 {
1284 return None;
1285 }
1286 let y_mean = y_vals.iter().sum::<f64>() / y_vals.len() as f64;
1287 let numerator: f64 = y_vals
1288 .iter()
1289 .enumerate()
1290 .map(|(i, &y)| (i as f64 - x_mean) * (y - y_mean))
1291 .sum();
1292 let denominator: f64 = (0..n).map(|i| (i as f64 - x_mean).powi(2)).sum();
1293 if denominator == 0.0 {
1294 return None;
1295 }
1296 Some(numerator / denominator)
1297 }
1298
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303 use super::*;
1304 use rust_decimal_macros::dec;
1305
1306 fn norm(w: usize) -> MinMaxNormalizer {
1307 MinMaxNormalizer::new(w).unwrap()
1308 }
1309
1310 #[test]
1313 fn test_new_normalizer_is_empty() {
1314 let n = norm(4);
1315 assert!(n.is_empty());
1316 assert_eq!(n.len(), 0);
1317 }
1318
1319 #[test]
1320 fn test_minmax_is_full_false_before_capacity() {
1321 let mut n = norm(3);
1322 assert!(!n.is_full());
1323 n.update(dec!(1));
1324 n.update(dec!(2));
1325 assert!(!n.is_full());
1326 n.update(dec!(3));
1327 assert!(n.is_full());
1328 }
1329
1330 #[test]
1331 fn test_minmax_is_full_stays_true_after_eviction() {
1332 let mut n = norm(3);
1333 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
1334 n.update(v);
1335 }
1336 assert!(n.is_full()); }
1338
1339 #[test]
1340 fn test_new_zero_window_returns_error() {
1341 let result = MinMaxNormalizer::new(0);
1342 assert!(matches!(result, Err(StreamError::ConfigError { .. })));
1343 }
1344
1345 #[test]
1348 fn test_normalize_min_is_zero() {
1349 let mut n = norm(4);
1350 n.update(dec!(10));
1351 n.update(dec!(20));
1352 n.update(dec!(30));
1353 n.update(dec!(40));
1354 let v = n.normalize(dec!(10)).unwrap();
1355 assert!(
1356 (v - 0.0).abs() < 1e-10,
1357 "min should normalize to 0.0, got {v}"
1358 );
1359 }
1360
1361 #[test]
1362 fn test_normalize_max_is_one() {
1363 let mut n = norm(4);
1364 n.update(dec!(10));
1365 n.update(dec!(20));
1366 n.update(dec!(30));
1367 n.update(dec!(40));
1368 let v = n.normalize(dec!(40)).unwrap();
1369 assert!(
1370 (v - 1.0).abs() < 1e-10,
1371 "max should normalize to 1.0, got {v}"
1372 );
1373 }
1374
1375 #[test]
1376 fn test_normalize_midpoint_is_half() {
1377 let mut n = norm(4);
1378 n.update(dec!(0));
1379 n.update(dec!(100));
1380 let v = n.normalize(dec!(50)).unwrap();
1381 assert!((v - 0.5).abs() < 1e-10);
1382 }
1383
1384 #[test]
1385 fn test_normalize_result_clamped_below_zero() {
1386 let mut n = norm(4);
1387 n.update(dec!(50));
1388 n.update(dec!(100));
1389 let v = n.normalize(dec!(10)).unwrap();
1391 assert!(v >= 0.0);
1392 assert_eq!(v, 0.0);
1393 }
1394
1395 #[test]
1396 fn test_normalize_result_clamped_above_one() {
1397 let mut n = norm(4);
1398 n.update(dec!(50));
1399 n.update(dec!(100));
1400 let v = n.normalize(dec!(200)).unwrap();
1402 assert!(v <= 1.0);
1403 assert_eq!(v, 1.0);
1404 }
1405
1406 #[test]
1407 fn test_normalize_all_same_values_returns_zero() {
1408 let mut n = norm(4);
1409 n.update(dec!(5));
1410 n.update(dec!(5));
1411 n.update(dec!(5));
1412 let v = n.normalize(dec!(5)).unwrap();
1413 assert_eq!(v, 0.0);
1414 }
1415
1416 #[test]
1419 fn test_normalize_empty_window_returns_error() {
1420 let mut n = norm(4);
1421 let err = n.normalize(dec!(1)).unwrap_err();
1422 assert!(matches!(err, StreamError::NormalizationError { .. }));
1423 }
1424
1425 #[test]
1426 fn test_min_max_empty_returns_none() {
1427 let mut n = norm(4);
1428 assert!(n.min_max().is_none());
1429 }
1430
1431 #[test]
1436 fn test_rolling_window_evicts_oldest() {
1437 let mut n = norm(3);
1438 n.update(dec!(1)); n.update(dec!(5));
1440 n.update(dec!(10));
1441 n.update(dec!(20)); let (min, max) = n.min_max().unwrap();
1443 assert_eq!(min, dec!(5));
1444 assert_eq!(max, dec!(20));
1445 }
1446
1447 #[test]
1448 fn test_rolling_window_len_does_not_exceed_capacity() {
1449 let mut n = norm(3);
1450 for i in 0..10 {
1451 n.update(Decimal::from(i));
1452 }
1453 assert_eq!(n.len(), 3);
1454 }
1455
1456 #[test]
1459 fn test_reset_clears_window() {
1460 let mut n = norm(4);
1461 n.update(dec!(10));
1462 n.update(dec!(20));
1463 n.reset();
1464 assert!(n.is_empty());
1465 assert!(n.min_max().is_none());
1466 }
1467
1468 #[test]
1469 fn test_normalize_works_after_reset() {
1470 let mut n = norm(4);
1471 n.update(dec!(10));
1472 n.reset();
1473 n.update(dec!(0));
1474 n.update(dec!(100));
1475 let v = n.normalize(dec!(100)).unwrap();
1476 assert!((v - 1.0).abs() < 1e-10);
1477 }
1478
1479 #[test]
1482 fn test_streaming_updates_monotone_sequence() {
1483 let mut n = norm(5);
1484 let prices = [dec!(100), dec!(101), dec!(102), dec!(103), dec!(104), dec!(105)];
1485 for &p in &prices {
1486 n.update(p);
1487 }
1488 let v_min = n.normalize(dec!(101)).unwrap();
1490 let v_max = n.normalize(dec!(105)).unwrap();
1491 assert!((v_min - 0.0).abs() < 1e-10);
1492 assert!((v_max - 1.0).abs() < 1e-10);
1493 }
1494
1495 #[test]
1496 fn test_normalization_monotonicity_in_window() {
1497 let mut n = norm(10);
1498 for i in 0..10 {
1499 n.update(Decimal::from(i * 10));
1500 }
1501 let v0 = n.normalize(dec!(0)).unwrap();
1503 let v50 = n.normalize(dec!(50)).unwrap();
1504 let v90 = n.normalize(dec!(90)).unwrap();
1505 assert!(v0 < v50, "normalized values should be monotone");
1506 assert!(v50 < v90, "normalized values should be monotone");
1507 }
1508
1509 #[test]
1510 fn test_high_precision_input_preserved() {
1511 let mut n = norm(2);
1513 n.update(dec!(50000.00000000));
1514 n.update(dec!(50000.12345678));
1515 let (min, max) = n.min_max().unwrap();
1516 assert_eq!(min, dec!(50000.00000000));
1517 assert_eq!(max, dec!(50000.12345678));
1518 }
1519
1520 #[test]
1523 fn test_denormalize_empty_window_returns_error() {
1524 let mut n = norm(4);
1525 assert!(matches!(n.denormalize(0.5), Err(StreamError::NormalizationError { .. })));
1526 }
1527
1528 #[test]
1529 fn test_denormalize_roundtrip_min() {
1530 let mut n = norm(4);
1531 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
1532 n.update(v);
1533 }
1534 let normalized = n.normalize(dec!(10)).unwrap(); let back = n.denormalize(normalized).unwrap();
1536 assert!((back - dec!(10)).abs() < dec!(0.0001));
1537 }
1538
1539 #[test]
1540 fn test_denormalize_roundtrip_max() {
1541 let mut n = norm(4);
1542 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
1543 n.update(v);
1544 }
1545 let normalized = n.normalize(dec!(40)).unwrap(); let back = n.denormalize(normalized).unwrap();
1547 assert!((back - dec!(40)).abs() < dec!(0.0001));
1548 }
1549
1550 #[test]
1553 fn test_range_none_when_empty() {
1554 let mut n = norm(4);
1555 assert!(n.range().is_none());
1556 }
1557
1558 #[test]
1559 fn test_range_zero_when_all_same() {
1560 let mut n = norm(3);
1561 n.update(dec!(5));
1562 n.update(dec!(5));
1563 n.update(dec!(5));
1564 assert_eq!(n.range(), Some(dec!(0)));
1565 }
1566
1567 #[test]
1568 fn test_range_correct() {
1569 let mut n = norm(4);
1570 for v in [dec!(10), dec!(40), dec!(20), dec!(30)] {
1571 n.update(v);
1572 }
1573 assert_eq!(n.range(), Some(dec!(30))); }
1575
1576 #[test]
1579 fn test_midpoint_none_when_empty() {
1580 let mut n = norm(4);
1581 assert!(n.midpoint().is_none());
1582 }
1583
1584 #[test]
1585 fn test_midpoint_correct() {
1586 let mut n = norm(4);
1587 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
1588 n.update(v);
1589 }
1590 assert_eq!(n.midpoint(), Some(dec!(25)));
1592 }
1593
1594 #[test]
1595 fn test_midpoint_single_value() {
1596 let mut n = norm(4);
1597 n.update(dec!(42));
1598 assert_eq!(n.midpoint(), Some(dec!(42)));
1599 }
1600
1601 #[test]
1604 fn test_clamp_to_window_returns_value_unchanged_when_empty() {
1605 let mut n = norm(4);
1606 assert_eq!(n.clamp_to_window(dec!(50)), dec!(50));
1607 }
1608
1609 #[test]
1610 fn test_clamp_to_window_clamps_above_max() {
1611 let mut n = norm(4);
1612 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
1613 assert_eq!(n.clamp_to_window(dec!(100)), dec!(30));
1614 }
1615
1616 #[test]
1617 fn test_clamp_to_window_clamps_below_min() {
1618 let mut n = norm(4);
1619 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
1620 assert_eq!(n.clamp_to_window(dec!(5)), dec!(10));
1621 }
1622
1623 #[test]
1624 fn test_clamp_to_window_passthrough_when_in_range() {
1625 let mut n = norm(4);
1626 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
1627 assert_eq!(n.clamp_to_window(dec!(15)), dec!(15));
1628 }
1629
1630 #[test]
1633 fn test_count_above_zero_when_empty() {
1634 let n = norm(4);
1635 assert_eq!(n.count_above(dec!(5)), 0);
1636 }
1637
1638 #[test]
1639 fn test_count_above_counts_strictly_above() {
1640 let mut n = norm(8);
1641 for v in [dec!(1), dec!(5), dec!(10), dec!(15)] { n.update(v); }
1642 assert_eq!(n.count_above(dec!(5)), 2); }
1644
1645 #[test]
1646 fn test_count_above_all_when_threshold_below_all() {
1647 let mut n = norm(4);
1648 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
1649 assert_eq!(n.count_above(dec!(5)), 3);
1650 }
1651
1652 #[test]
1653 fn test_count_above_zero_when_threshold_above_all() {
1654 let mut n = norm(4);
1655 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
1656 assert_eq!(n.count_above(dec!(100)), 0);
1657 }
1658
1659 #[test]
1662 fn test_count_below_zero_when_empty() {
1663 let n = norm(4);
1664 assert_eq!(n.count_below(dec!(5)), 0);
1665 }
1666
1667 #[test]
1668 fn test_count_below_counts_strictly_below() {
1669 let mut n = norm(8);
1670 for v in [dec!(1), dec!(5), dec!(10), dec!(15)] { n.update(v); }
1671 assert_eq!(n.count_below(dec!(10)), 2); }
1673
1674 #[test]
1675 fn test_count_below_all_when_threshold_above_all() {
1676 let mut n = norm(4);
1677 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
1678 assert_eq!(n.count_below(dec!(100)), 3);
1679 }
1680
1681 #[test]
1682 fn test_count_below_zero_when_threshold_below_all() {
1683 let mut n = norm(4);
1684 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
1685 assert_eq!(n.count_below(dec!(5)), 0);
1686 }
1687
1688 #[test]
1689 fn test_count_above_plus_count_below_leq_len() {
1690 let mut n = norm(5);
1691 for v in [dec!(1), dec!(5), dec!(5), dec!(10), dec!(20)] { n.update(v); }
1692 assert_eq!(n.count_above(dec!(5)) + n.count_below(dec!(5)), 3);
1695 }
1696
1697 #[test]
1700 fn test_normalized_range_none_when_empty() {
1701 let mut n = norm(4);
1702 assert!(n.normalized_range().is_none());
1703 }
1704
1705 #[test]
1706 fn test_normalized_range_zero_when_all_same() {
1707 let mut n = norm(4);
1708 for _ in 0..4 { n.update(dec!(5)); }
1709 assert_eq!(n.normalized_range(), Some(0.0));
1710 }
1711
1712 #[test]
1713 fn test_normalized_range_correct_value() {
1714 let mut n = norm(4);
1715 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
1716 let nr = n.normalized_range().unwrap();
1718 assert!((nr - 0.75).abs() < 1e-10);
1719 }
1720
1721 #[test]
1724 fn test_normalize_clamp_in_range_equals_normalize() {
1725 let mut n = norm(4);
1726 for v in [dec!(0), dec!(25), dec!(75), dec!(100)] {
1727 n.update(v);
1728 }
1729 let clamped = n.normalize_clamp(dec!(50)).unwrap();
1730 let normal = n.normalize(dec!(50)).unwrap();
1731 assert!((clamped - normal).abs() < 1e-9);
1732 }
1733
1734 #[test]
1735 fn test_normalize_clamp_above_max_clamped_to_one() {
1736 let mut n = norm(3);
1737 for v in [dec!(0), dec!(50), dec!(100)] {
1738 n.update(v);
1739 }
1740 let clamped = n.normalize_clamp(dec!(200)).unwrap();
1742 assert!((clamped - 1.0).abs() < 1e-9, "expected 1.0 got {clamped}");
1743 }
1744
1745 #[test]
1746 fn test_normalize_clamp_below_min_clamped_to_zero() {
1747 let mut n = norm(3);
1748 for v in [dec!(10), dec!(50), dec!(100)] {
1749 n.update(v);
1750 }
1751 let clamped = n.normalize_clamp(dec!(-50)).unwrap();
1753 assert!((clamped - 0.0).abs() < 1e-9, "expected 0.0 got {clamped}");
1754 }
1755
1756 #[test]
1757 fn test_normalize_clamp_empty_window_returns_error() {
1758 let mut n = norm(4);
1759 assert!(n.normalize_clamp(dec!(5)).is_err());
1760 }
1761
1762 #[test]
1765 fn test_latest_none_when_empty() {
1766 let n = norm(5);
1767 assert_eq!(n.latest(), None);
1768 }
1769
1770 #[test]
1771 fn test_latest_returns_most_recent_value() {
1772 let mut n = norm(5);
1773 n.update(dec!(10));
1774 n.update(dec!(20));
1775 n.update(dec!(30));
1776 assert_eq!(n.latest(), Some(dec!(30)));
1777 }
1778
1779 #[test]
1780 fn test_latest_updates_on_each_push() {
1781 let mut n = norm(3);
1782 n.update(dec!(1));
1783 assert_eq!(n.latest(), Some(dec!(1)));
1784 n.update(dec!(5));
1785 assert_eq!(n.latest(), Some(dec!(5)));
1786 }
1787
1788 #[test]
1789 fn test_latest_returns_last_after_window_overflow() {
1790 let mut n = norm(2); n.update(dec!(100));
1792 n.update(dec!(200));
1793 n.update(dec!(300)); assert_eq!(n.latest(), Some(dec!(300)));
1795 }
1796
1797 #[test]
1800 fn test_minmax_cv_none_fewer_than_2_obs() {
1801 let mut n = norm(4);
1802 n.update(dec!(10));
1803 assert!(n.coefficient_of_variation().is_none());
1804 }
1805
1806 #[test]
1807 fn test_minmax_cv_none_when_mean_zero() {
1808 let mut n = norm(4);
1809 for v in [dec!(-5), dec!(5)] { n.update(v); }
1810 assert!(n.coefficient_of_variation().is_none());
1811 }
1812
1813 #[test]
1814 fn test_minmax_cv_positive_for_positive_mean() {
1815 let mut n = norm(4);
1816 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
1817 let cv = n.coefficient_of_variation().unwrap();
1818 assert!(cv > 0.0, "CV should be positive");
1819 }
1820
1821 #[test]
1824 fn test_minmax_variance_none_fewer_than_2_obs() {
1825 let mut n = norm(5);
1826 n.update(dec!(10));
1827 assert!(n.variance().is_none());
1828 }
1829
1830 #[test]
1831 fn test_minmax_variance_zero_all_same() {
1832 let mut n = norm(4);
1833 for _ in 0..4 { n.update(dec!(5)); }
1834 assert_eq!(n.variance(), Some(dec!(0)));
1835 }
1836
1837 #[test]
1838 fn test_minmax_variance_correct_value() {
1839 let mut n = norm(4);
1840 for v in [dec!(5), dec!(5), dec!(7), dec!(9)] { n.update(v); }
1843 let var = n.variance().unwrap();
1844 assert!((var.to_f64().unwrap() - 2.75).abs() < 1e-9);
1846 }
1847
1848 #[test]
1849 fn test_minmax_std_dev_none_fewer_than_2_obs() {
1850 let n = norm(4);
1851 assert!(n.std_dev().is_none());
1852 }
1853
1854 #[test]
1855 fn test_minmax_std_dev_zero_all_same() {
1856 let mut n = norm(3);
1857 for _ in 0..3 { n.update(dec!(7)); }
1858 assert_eq!(n.std_dev(), Some(0.0));
1859 }
1860
1861 #[test]
1862 fn test_minmax_std_dev_sqrt_of_variance() {
1863 let mut n = norm(4);
1864 for v in [dec!(5), dec!(5), dec!(7), dec!(9)] { n.update(v); }
1865 let sd = n.std_dev().unwrap();
1866 let var = n.variance().unwrap().to_f64().unwrap();
1867 assert!((sd - var.sqrt()).abs() < 1e-9);
1868 }
1869
1870 #[test]
1873 fn test_minmax_kurtosis_none_fewer_than_4_observations() {
1874 let mut n = norm(5);
1875 n.update(dec!(1));
1876 n.update(dec!(2));
1877 n.update(dec!(3));
1878 assert!(n.kurtosis().is_none());
1879 }
1880
1881 #[test]
1882 fn test_minmax_kurtosis_some_with_4_observations() {
1883 let mut n = norm(4);
1884 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
1885 n.update(v);
1886 }
1887 assert!(n.kurtosis().is_some());
1888 }
1889
1890 #[test]
1891 fn test_minmax_kurtosis_none_all_same_value() {
1892 let mut n = norm(4);
1893 for _ in 0..4 {
1894 n.update(dec!(5));
1895 }
1896 assert!(n.kurtosis().is_none());
1898 }
1899
1900 #[test]
1901 fn test_minmax_kurtosis_uniform_distribution_is_negative() {
1902 let mut n = norm(10);
1904 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5),
1905 dec!(6), dec!(7), dec!(8), dec!(9), dec!(10)] {
1906 n.update(v);
1907 }
1908 let k = n.kurtosis().unwrap();
1909 assert!(k < 0.0, "uniform distribution should have negative excess kurtosis, got {k}");
1910 }
1911
1912 #[test]
1915 fn test_minmax_median_none_for_empty_window() {
1916 assert!(norm(4).median().is_none());
1917 }
1918
1919 #[test]
1920 fn test_minmax_median_odd_window() {
1921 let mut n = norm(5);
1922 for v in [dec!(3), dec!(1), dec!(5), dec!(2), dec!(4)] { n.update(v); }
1923 assert_eq!(n.median(), Some(dec!(3)));
1925 }
1926
1927 #[test]
1928 fn test_minmax_median_even_window() {
1929 let mut n = norm(4);
1930 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
1931 assert_eq!(n.median(), Some(dec!(2.5)));
1933 }
1934
1935 #[test]
1938 fn test_minmax_sample_variance_none_for_single_obs() {
1939 let mut n = norm(4);
1940 n.update(dec!(10));
1941 assert!(n.sample_variance().is_none());
1942 }
1943
1944 #[test]
1945 fn test_minmax_sample_variance_larger_than_population_variance() {
1946 let mut n = norm(4);
1947 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
1948 use rust_decimal::prelude::ToPrimitive;
1949 let pop_var = n.variance().unwrap().to_f64().unwrap();
1950 let sample_var = n.sample_variance().unwrap();
1951 assert!(sample_var > pop_var, "sample variance should exceed population variance");
1952 }
1953
1954 #[test]
1957 fn test_minmax_mad_none_for_empty_window() {
1958 assert!(norm(4).mad().is_none());
1959 }
1960
1961 #[test]
1962 fn test_minmax_mad_zero_for_identical_values() {
1963 let mut n = norm(4);
1964 for _ in 0..4 { n.update(dec!(5)); }
1965 assert_eq!(n.mad(), Some(dec!(0)));
1966 }
1967
1968 #[test]
1969 fn test_minmax_mad_correct_for_known_distribution() {
1970 let mut n = norm(5);
1971 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
1972 assert_eq!(n.mad(), Some(dec!(1)));
1974 }
1975
1976 #[test]
1979 fn test_minmax_robust_z_none_for_empty_window() {
1980 assert!(norm(4).robust_z_score(dec!(10)).is_none());
1981 }
1982
1983 #[test]
1984 fn test_minmax_robust_z_none_when_mad_is_zero() {
1985 let mut n = norm(4);
1986 for _ in 0..4 { n.update(dec!(5)); }
1987 assert!(n.robust_z_score(dec!(5)).is_none());
1988 }
1989
1990 #[test]
1991 fn test_minmax_robust_z_positive_above_median() {
1992 let mut n = norm(5);
1993 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
1994 let rz = n.robust_z_score(dec!(5)).unwrap();
1995 assert!(rz > 0.0, "robust z-score should be positive for value above median");
1996 }
1997
1998 #[test]
1999 fn test_minmax_robust_z_negative_below_median() {
2000 let mut n = norm(5);
2001 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2002 let rz = n.robust_z_score(dec!(1)).unwrap();
2003 assert!(rz < 0.0, "robust z-score should be negative for value below median");
2004 }
2005
2006 #[test]
2009 fn test_percentile_value_none_for_empty_window() {
2010 assert!(norm(4).percentile_value(0.5).is_none());
2011 }
2012
2013 #[test]
2014 fn test_percentile_value_min_at_zero() {
2015 let mut n = norm(5);
2016 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2017 assert_eq!(n.percentile_value(0.0), Some(dec!(10)));
2018 }
2019
2020 #[test]
2021 fn test_percentile_value_max_at_one() {
2022 let mut n = norm(5);
2023 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2024 assert_eq!(n.percentile_value(1.0), Some(dec!(50)));
2025 }
2026
2027 #[test]
2028 fn test_percentile_value_median_at_half() {
2029 let mut n = norm(5);
2030 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2031 assert_eq!(n.percentile_value(0.5), Some(dec!(30)));
2033 }
2034
2035 #[test]
2038 fn test_minmax_sum_none_for_empty_window() {
2039 assert!(norm(3).sum().is_none());
2040 }
2041
2042 #[test]
2043 fn test_minmax_sum_single_value() {
2044 let mut n = norm(3);
2045 n.update(dec!(7));
2046 assert_eq!(n.sum(), Some(dec!(7)));
2047 }
2048
2049 #[test]
2050 fn test_minmax_sum_multiple_values() {
2051 let mut n = norm(4);
2052 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2053 assert_eq!(n.sum(), Some(dec!(10)));
2054 }
2055
2056 #[test]
2059 fn test_minmax_is_outlier_false_for_empty_window() {
2060 assert!(!norm(3).is_outlier(dec!(100), 2.0));
2061 }
2062
2063 #[test]
2064 fn test_minmax_is_outlier_false_for_in_range_value() {
2065 let mut n = norm(5);
2066 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2067 assert!(!n.is_outlier(dec!(3), 2.0));
2068 }
2069
2070 #[test]
2071 fn test_minmax_is_outlier_true_for_extreme_value() {
2072 let mut n = norm(5);
2073 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2074 assert!(n.is_outlier(dec!(100), 2.0));
2075 }
2076
2077 #[test]
2080 fn test_minmax_trim_outliers_returns_all_when_no_outliers() {
2081 let mut n = norm(5);
2082 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2083 let trimmed = n.trim_outliers(10.0);
2084 assert_eq!(trimmed.len(), 5);
2085 }
2086
2087 #[test]
2088 fn test_minmax_trim_outliers_removes_extreme_values() {
2089 let mut n = norm(5);
2090 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2091 let trimmed = n.trim_outliers(0.0);
2093 assert_eq!(trimmed.len(), 1); }
2095
2096 #[test]
2099 fn test_minmax_z_score_of_latest_none_for_empty_window() {
2100 assert!(norm(3).z_score_of_latest().is_none());
2101 }
2102
2103 #[test]
2104 fn test_minmax_z_score_of_latest_zero_for_single_value() {
2105 let mut n = norm(5);
2106 n.update(dec!(10));
2107 assert!(n.z_score_of_latest().is_none());
2109 }
2110
2111 #[test]
2112 fn test_minmax_z_score_of_latest_positive_for_above_mean() {
2113 let mut n = norm(5);
2114 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(10)] { n.update(v); }
2115 let z = n.z_score_of_latest().unwrap();
2116 assert!(z > 0.0, "latest value is above mean → positive z-score");
2117 }
2118
2119 #[test]
2122 fn test_minmax_deviation_from_mean_none_for_empty_window() {
2123 assert!(norm(3).deviation_from_mean(dec!(5)).is_none());
2124 }
2125
2126 #[test]
2127 fn test_minmax_deviation_from_mean_zero_at_mean() {
2128 let mut n = norm(4);
2129 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2130 let dev = n.deviation_from_mean(dec!(2.5)).unwrap();
2132 assert!(dev.abs() < 1e-9);
2133 }
2134
2135 #[test]
2136 fn test_minmax_deviation_from_mean_positive_above_mean() {
2137 let mut n = norm(4);
2138 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2139 let dev = n.deviation_from_mean(dec!(5)).unwrap();
2140 assert!(dev > 0.0);
2141 }
2142
2143 #[test]
2146 fn test_minmax_range_f64_none_for_empty_window() {
2147 assert!(norm(3).range_f64().is_none());
2148 }
2149
2150 #[test]
2151 fn test_minmax_range_f64_correct() {
2152 let mut n = norm(4);
2153 for v in [dec!(5), dec!(15), dec!(10), dec!(20)] { n.update(v); }
2154 let r = n.range_f64().unwrap();
2156 assert!((r - 15.0).abs() < 1e-9);
2157 }
2158
2159 #[test]
2160 fn test_minmax_sum_f64_none_for_empty_window() {
2161 assert!(norm(3).sum_f64().is_none());
2162 }
2163
2164 #[test]
2165 fn test_minmax_sum_f64_correct() {
2166 let mut n = norm(4);
2167 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2168 let s = n.sum_f64().unwrap();
2169 assert!((s - 10.0).abs() < 1e-9);
2170 }
2171
2172 #[test]
2175 fn test_minmax_values_empty_for_empty_window() {
2176 assert!(norm(3).values().is_empty());
2177 }
2178
2179 #[test]
2180 fn test_minmax_values_preserves_insertion_order() {
2181 let mut n = norm(5);
2182 for v in [dec!(3), dec!(1), dec!(4), dec!(1), dec!(5)] { n.update(v); }
2183 assert_eq!(n.values(), vec![dec!(3), dec!(1), dec!(4), dec!(1), dec!(5)]);
2184 }
2185
2186 #[test]
2189 fn test_minmax_normalized_midpoint_none_for_empty_window() {
2190 assert!(norm(3).normalized_midpoint().is_none());
2191 }
2192
2193 #[test]
2194 fn test_minmax_normalized_midpoint_half_for_uniform_range() {
2195 let mut n = norm(4);
2196 for v in [dec!(0), dec!(10), dec!(20), dec!(30)] { n.update(v); }
2197 let mid = n.normalized_midpoint().unwrap();
2199 assert!((mid - 0.5).abs() < 1e-9);
2200 }
2201
2202 #[test]
2205 fn test_minmax_is_at_min_false_for_empty_window() {
2206 assert!(!norm(3).is_at_min(dec!(5)));
2207 }
2208
2209 #[test]
2210 fn test_minmax_is_at_min_true_for_minimum_value() {
2211 let mut n = norm(4);
2212 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2213 assert!(n.is_at_min(dec!(5)));
2214 }
2215
2216 #[test]
2217 fn test_minmax_is_at_min_false_for_non_minimum() {
2218 let mut n = norm(4);
2219 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2220 assert!(!n.is_at_min(dec!(10)));
2221 }
2222
2223 #[test]
2224 fn test_minmax_is_at_max_false_for_empty_window() {
2225 assert!(!norm(3).is_at_max(dec!(5)));
2226 }
2227
2228 #[test]
2229 fn test_minmax_is_at_max_true_for_maximum_value() {
2230 let mut n = norm(4);
2231 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2232 assert!(n.is_at_max(dec!(30)));
2233 }
2234
2235 #[test]
2236 fn test_minmax_is_at_max_false_for_non_maximum() {
2237 let mut n = norm(4);
2238 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2239 assert!(!n.is_at_max(dec!(20)));
2240 }
2241
2242 #[test]
2245 fn test_minmax_fraction_above_none_for_empty_window() {
2246 assert!(norm(3).fraction_above(dec!(5)).is_none());
2247 }
2248
2249 #[test]
2250 fn test_minmax_fraction_above_correct() {
2251 let mut n = norm(5);
2252 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2253 let frac = n.fraction_above(dec!(3)).unwrap();
2255 assert!((frac - 0.4).abs() < 1e-9);
2256 }
2257
2258 #[test]
2259 fn test_minmax_fraction_below_none_for_empty_window() {
2260 assert!(norm(3).fraction_below(dec!(5)).is_none());
2261 }
2262
2263 #[test]
2264 fn test_minmax_fraction_below_correct() {
2265 let mut n = norm(5);
2266 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2267 let frac = n.fraction_below(dec!(3)).unwrap();
2269 assert!((frac - 0.4).abs() < 1e-9);
2270 }
2271
2272 #[test]
2275 fn test_minmax_window_values_above_empty_window() {
2276 assert!(norm(3).window_values_above(dec!(5)).is_empty());
2277 }
2278
2279 #[test]
2280 fn test_minmax_window_values_above_filters_correctly() {
2281 let mut n = norm(5);
2282 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2283 let above = n.window_values_above(dec!(5));
2284 assert_eq!(above.len(), 2);
2285 assert!(above.contains(&dec!(7)));
2286 assert!(above.contains(&dec!(9)));
2287 }
2288
2289 #[test]
2290 fn test_minmax_window_values_below_empty_window() {
2291 assert!(norm(3).window_values_below(dec!(5)).is_empty());
2292 }
2293
2294 #[test]
2295 fn test_minmax_window_values_below_filters_correctly() {
2296 let mut n = norm(5);
2297 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2298 let below = n.window_values_below(dec!(5));
2299 assert_eq!(below.len(), 2);
2300 assert!(below.contains(&dec!(1)));
2301 assert!(below.contains(&dec!(3)));
2302 }
2303
2304 #[test]
2307 fn test_minmax_percentile_rank_none_for_empty_window() {
2308 assert!(norm(3).percentile_rank(dec!(5)).is_none());
2309 }
2310
2311 #[test]
2312 fn test_minmax_percentile_rank_correct() {
2313 let mut n = norm(5);
2314 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2315 let rank = n.percentile_rank(dec!(3)).unwrap();
2317 assert!((rank - 0.6).abs() < 1e-9);
2318 }
2319
2320 #[test]
2323 fn test_minmax_count_equal_zero_for_no_match() {
2324 let mut n = norm(3);
2325 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2326 assert_eq!(n.count_equal(dec!(99)), 0);
2327 }
2328
2329 #[test]
2330 fn test_minmax_count_equal_counts_duplicates() {
2331 let mut n = norm(5);
2332 for v in [dec!(5), dec!(5), dec!(3), dec!(5), dec!(2)] { n.update(v); }
2333 assert_eq!(n.count_equal(dec!(5)), 3);
2334 }
2335
2336 #[test]
2339 fn test_minmax_rolling_range_none_for_empty() {
2340 assert!(norm(3).rolling_range().is_none());
2341 }
2342
2343 #[test]
2344 fn test_minmax_rolling_range_correct() {
2345 let mut n = norm(5);
2346 for v in [dec!(10), dec!(50), dec!(30), dec!(20), dec!(40)] { n.update(v); }
2347 assert_eq!(n.rolling_range(), Some(dec!(40)));
2348 }
2349
2350 #[test]
2353 fn test_minmax_skewness_none_for_fewer_than_3() {
2354 let mut n = norm(5);
2355 n.update(dec!(1)); n.update(dec!(2));
2356 assert!(n.skewness().is_none());
2357 }
2358
2359 #[test]
2360 fn test_minmax_skewness_near_zero_for_symmetric_data() {
2361 let mut n = norm(5);
2362 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2363 let s = n.skewness().unwrap();
2364 assert!(s.abs() < 0.5);
2365 }
2366
2367 #[test]
2370 fn test_minmax_kurtosis_none_for_fewer_than_4() {
2371 let mut n = norm(5);
2372 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2373 assert!(n.kurtosis().is_none());
2374 }
2375
2376 #[test]
2377 fn test_minmax_kurtosis_returns_f64_for_populated_window() {
2378 let mut n = norm(5);
2379 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2380 assert!(n.kurtosis().is_some());
2381 }
2382
2383 #[test]
2386 fn test_minmax_autocorrelation_none_for_single_value() {
2387 let mut n = norm(3);
2388 n.update(dec!(1));
2389 assert!(n.autocorrelation_lag1().is_none());
2390 }
2391
2392 #[test]
2393 fn test_minmax_autocorrelation_positive_for_trending_data() {
2394 let mut n = norm(5);
2395 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2396 let ac = n.autocorrelation_lag1().unwrap();
2397 assert!(ac > 0.0);
2398 }
2399
2400 #[test]
2403 fn test_minmax_trend_consistency_none_for_single_value() {
2404 let mut n = norm(3);
2405 n.update(dec!(1));
2406 assert!(n.trend_consistency().is_none());
2407 }
2408
2409 #[test]
2410 fn test_minmax_trend_consistency_one_for_strictly_rising() {
2411 let mut n = norm(5);
2412 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2413 let tc = n.trend_consistency().unwrap();
2414 assert!((tc - 1.0).abs() < 1e-9);
2415 }
2416
2417 #[test]
2418 fn test_minmax_trend_consistency_zero_for_strictly_falling() {
2419 let mut n = norm(5);
2420 for v in [dec!(5), dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
2421 let tc = n.trend_consistency().unwrap();
2422 assert!((tc - 0.0).abs() < 1e-9);
2423 }
2424
2425 #[test]
2428 fn test_minmax_cov_none_for_single_value() {
2429 let mut n = norm(3);
2430 n.update(dec!(10));
2431 assert!(n.coefficient_of_variation().is_none());
2432 }
2433
2434 #[test]
2435 fn test_minmax_cov_positive_for_varied_data() {
2436 let mut n = norm(5);
2437 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2438 let cov = n.coefficient_of_variation().unwrap();
2439 assert!(cov > 0.0);
2440 }
2441
2442 #[test]
2445 fn test_minmax_mean_absolute_deviation_none_for_empty() {
2446 assert!(norm(3).mean_absolute_deviation().is_none());
2447 }
2448
2449 #[test]
2450 fn test_minmax_mean_absolute_deviation_zero_for_identical_values() {
2451 let mut n = norm(3);
2452 for v in [dec!(5), dec!(5), dec!(5)] { n.update(v); }
2453 let mad = n.mean_absolute_deviation().unwrap();
2454 assert!((mad - 0.0).abs() < 1e-9);
2455 }
2456
2457 #[test]
2458 fn test_minmax_mean_absolute_deviation_positive_for_varied_data() {
2459 let mut n = norm(4);
2460 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2461 let mad = n.mean_absolute_deviation().unwrap();
2462 assert!(mad > 0.0);
2463 }
2464
2465 #[test]
2468 fn test_minmax_percentile_of_latest_none_for_empty() {
2469 assert!(norm(3).percentile_of_latest().is_none());
2470 }
2471
2472 #[test]
2473 fn test_minmax_percentile_of_latest_returns_some_after_update() {
2474 let mut n = norm(4);
2475 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2476 assert!(n.percentile_of_latest().is_some());
2477 }
2478
2479 #[test]
2480 fn test_minmax_percentile_of_latest_max_has_high_rank() {
2481 let mut n = norm(5);
2482 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2483 let rank = n.percentile_of_latest().unwrap();
2484 assert!(rank >= 0.9, "max value should have rank near 1.0, got {}", rank);
2485 }
2486
2487 #[test]
2490 fn test_minmax_tail_ratio_none_for_empty() {
2491 assert!(norm(4).tail_ratio().is_none());
2492 }
2493
2494 #[test]
2495 fn test_minmax_tail_ratio_one_for_identical_values() {
2496 let mut n = norm(4);
2497 for _ in 0..4 { n.update(dec!(7)); }
2498 let r = n.tail_ratio().unwrap();
2500 assert!((r - 1.0).abs() < 1e-9, "expected 1.0, got {}", r);
2501 }
2502
2503 #[test]
2504 fn test_minmax_tail_ratio_above_one_with_outlier() {
2505 let mut n = norm(5);
2506 for v in [dec!(1), dec!(1), dec!(1), dec!(1), dec!(10)] { n.update(v); }
2507 let r = n.tail_ratio().unwrap();
2508 assert!(r > 1.0, "outlier should push ratio above 1.0, got {}", r);
2509 }
2510
2511 #[test]
2514 fn test_minmax_z_score_of_min_none_for_empty() {
2515 assert!(norm(4).z_score_of_min().is_none());
2516 }
2517
2518 #[test]
2519 fn test_minmax_z_score_of_max_none_for_empty() {
2520 assert!(norm(4).z_score_of_max().is_none());
2521 }
2522
2523 #[test]
2524 fn test_minmax_z_score_of_min_negative_for_varied_window() {
2525 let mut n = norm(5);
2526 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2527 let z = n.z_score_of_min().unwrap();
2529 assert!(z < 0.0, "z-score of min should be negative, got {}", z);
2530 }
2531
2532 #[test]
2533 fn test_minmax_z_score_of_max_positive_for_varied_window() {
2534 let mut n = norm(5);
2535 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2536 let z = n.z_score_of_max().unwrap();
2538 assert!(z > 0.0, "z-score of max should be positive, got {}", z);
2539 }
2540
2541 #[test]
2544 fn test_minmax_window_entropy_none_for_empty() {
2545 assert!(norm(4).window_entropy().is_none());
2546 }
2547
2548 #[test]
2549 fn test_minmax_window_entropy_zero_for_identical_values() {
2550 let mut n = norm(3);
2551 for _ in 0..3 { n.update(dec!(5)); }
2552 let e = n.window_entropy().unwrap();
2553 assert!((e - 0.0).abs() < 1e-9, "identical values should have zero entropy, got {}", e);
2554 }
2555
2556 #[test]
2557 fn test_minmax_window_entropy_positive_for_varied_values() {
2558 let mut n = norm(4);
2559 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2560 let e = n.window_entropy().unwrap();
2561 assert!(e > 0.0, "varied values should have positive entropy, got {}", e);
2562 }
2563
2564 #[test]
2567 fn test_minmax_normalized_std_dev_none_for_single_value() {
2568 let mut n = norm(4);
2569 n.update(dec!(5));
2570 assert!(n.normalized_std_dev().is_none());
2571 }
2572
2573 #[test]
2574 fn test_minmax_normalized_std_dev_positive_for_varied_values() {
2575 let mut n = norm(4);
2576 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2577 let r = n.normalized_std_dev().unwrap();
2578 assert!(r > 0.0, "expected positive normalized std dev, got {}", r);
2579 }
2580
2581 #[test]
2584 fn test_minmax_value_above_mean_count_none_for_empty() {
2585 assert!(norm(4).value_above_mean_count().is_none());
2586 }
2587
2588 #[test]
2589 fn test_minmax_value_above_mean_count_correct() {
2590 let mut n = norm(4);
2592 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2593 assert_eq!(n.value_above_mean_count().unwrap(), 2);
2594 }
2595
2596 #[test]
2599 fn test_minmax_consecutive_above_mean_none_for_empty() {
2600 assert!(norm(4).consecutive_above_mean().is_none());
2601 }
2602
2603 #[test]
2604 fn test_minmax_consecutive_above_mean_correct() {
2605 let mut n = norm(4);
2607 for v in [dec!(1), dec!(5), dec!(6), dec!(7)] { n.update(v); }
2608 assert_eq!(n.consecutive_above_mean().unwrap(), 3);
2609 }
2610
2611 #[test]
2614 fn test_minmax_above_threshold_fraction_none_for_empty() {
2615 assert!(norm(4).above_threshold_fraction(dec!(5)).is_none());
2616 }
2617
2618 #[test]
2619 fn test_minmax_above_threshold_fraction_correct() {
2620 let mut n = norm(4);
2621 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2622 let f = n.above_threshold_fraction(dec!(2)).unwrap();
2624 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
2625 }
2626
2627 #[test]
2628 fn test_minmax_below_threshold_fraction_none_for_empty() {
2629 assert!(norm(4).below_threshold_fraction(dec!(5)).is_none());
2630 }
2631
2632 #[test]
2633 fn test_minmax_below_threshold_fraction_correct() {
2634 let mut n = norm(4);
2635 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2636 let f = n.below_threshold_fraction(dec!(3)).unwrap();
2638 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
2639 }
2640
2641 #[test]
2644 fn test_minmax_lag_k_autocorrelation_none_for_zero_k() {
2645 let mut n = norm(5);
2646 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2647 assert!(n.lag_k_autocorrelation(0).is_none());
2648 }
2649
2650 #[test]
2651 fn test_minmax_lag_k_autocorrelation_none_when_k_gte_len() {
2652 let mut n = norm(3);
2653 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2654 assert!(n.lag_k_autocorrelation(3).is_none());
2655 }
2656
2657 #[test]
2658 fn test_minmax_lag_k_autocorrelation_positive_for_trend() {
2659 let mut n = norm(6);
2661 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6)] { n.update(v); }
2662 let ac = n.lag_k_autocorrelation(1).unwrap();
2663 assert!(ac > 0.0, "trending series should have positive AC, got {}", ac);
2664 }
2665
2666 #[test]
2669 fn test_minmax_half_life_estimate_none_for_fewer_than_3() {
2670 let mut n = norm(3);
2671 n.update(dec!(1)); n.update(dec!(2));
2672 assert!(n.half_life_estimate().is_none());
2673 }
2674
2675 #[test]
2676 fn test_minmax_half_life_estimate_some_for_mean_reverting() {
2677 let mut n = norm(6);
2679 for v in [dec!(10), dec!(5), dec!(10), dec!(5), dec!(10), dec!(5)] { n.update(v); }
2680 let _ = n.half_life_estimate();
2682 }
2683
2684 #[test]
2687 fn test_minmax_geometric_mean_none_for_empty() {
2688 assert!(norm(4).geometric_mean().is_none());
2689 }
2690
2691 #[test]
2692 fn test_minmax_geometric_mean_correct_for_powers_of_2() {
2693 let mut n = norm(4);
2695 for v in [dec!(1), dec!(2), dec!(4), dec!(8)] { n.update(v); }
2696 let gm = n.geometric_mean().unwrap();
2697 assert!((gm - 64.0f64.powf(0.25)).abs() < 1e-6, "got {}", gm);
2698 }
2699
2700 #[test]
2703 fn test_minmax_harmonic_mean_none_for_empty() {
2704 assert!(norm(4).harmonic_mean().is_none());
2705 }
2706
2707 #[test]
2708 fn test_minmax_harmonic_mean_none_when_any_zero() {
2709 let mut n = norm(2);
2710 n.update(dec!(0)); n.update(dec!(5));
2711 assert!(n.harmonic_mean().is_none());
2712 }
2713
2714 #[test]
2715 fn test_minmax_harmonic_mean_positive_for_positive_values() {
2716 let mut n = norm(4);
2717 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2718 let hm = n.harmonic_mean().unwrap();
2719 assert!(hm > 0.0 && hm < 4.0, "HM should be in (0, max), got {}", hm);
2720 }
2721
2722 #[test]
2725 fn test_minmax_range_normalized_value_none_for_empty() {
2726 assert!(norm(4).range_normalized_value(dec!(5)).is_none());
2727 }
2728
2729 #[test]
2730 fn test_minmax_range_normalized_value_zero_for_min() {
2731 let mut n = norm(4);
2732 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2733 let r = n.range_normalized_value(dec!(1)).unwrap();
2734 assert!((r - 0.0).abs() < 1e-9, "min value should normalize to 0, got {}", r);
2735 }
2736
2737 #[test]
2738 fn test_minmax_range_normalized_value_one_for_max() {
2739 let mut n = norm(4);
2740 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2741 let r = n.range_normalized_value(dec!(4)).unwrap();
2742 assert!((r - 1.0).abs() < 1e-9, "max value should normalize to 1, got {}", r);
2743 }
2744
2745 #[test]
2748 fn test_minmax_distance_from_median_none_for_empty() {
2749 assert!(norm(4).distance_from_median(dec!(5)).is_none());
2750 }
2751
2752 #[test]
2753 fn test_minmax_distance_from_median_zero_at_median() {
2754 let mut n = norm(5);
2755 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2756 let d = n.distance_from_median(dec!(3)).unwrap();
2758 assert!((d - 0.0).abs() < 1e-9, "distance from median should be 0, got {}", d);
2759 }
2760
2761 #[test]
2762 fn test_minmax_distance_from_median_positive_above() {
2763 let mut n = norm(5);
2764 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2765 let d = n.distance_from_median(dec!(5)).unwrap();
2766 assert!(d > 0.0, "value above median should give positive distance, got {}", d);
2767 }
2768
2769 #[test]
2770 fn test_minmax_momentum_none_for_single_value() {
2771 let mut n = norm(5);
2772 n.update(dec!(10));
2773 assert!(n.momentum().is_none());
2774 }
2775
2776 #[test]
2777 fn test_minmax_momentum_positive_for_rising_window() {
2778 let mut n = norm(3);
2779 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2780 let m = n.momentum().unwrap();
2781 assert!(m > 0.0, "rising window → positive momentum, got {}", m);
2782 }
2783
2784 #[test]
2785 fn test_minmax_value_rank_none_for_empty() {
2786 assert!(norm(4).value_rank(dec!(5)).is_none());
2787 }
2788
2789 #[test]
2790 fn test_minmax_value_rank_extremes() {
2791 let mut n = norm(4);
2792 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2793 let low = n.value_rank(dec!(0)).unwrap();
2795 assert!((low - 0.0).abs() < 1e-9, "got {}", low);
2796 let high = n.value_rank(dec!(5)).unwrap();
2798 assert!((high - 1.0).abs() < 1e-9, "got {}", high);
2799 }
2800
2801 #[test]
2802 fn test_minmax_coeff_of_variation_none_for_single_value() {
2803 let mut n = norm(5);
2804 n.update(dec!(10));
2805 assert!(n.coeff_of_variation().is_none());
2806 }
2807
2808 #[test]
2809 fn test_minmax_coeff_of_variation_positive_for_spread() {
2810 let mut n = norm(4);
2811 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2812 let cv = n.coeff_of_variation().unwrap();
2813 assert!(cv > 0.0, "expected positive CV, got {}", cv);
2814 }
2815
2816 #[test]
2817 fn test_minmax_quantile_range_none_for_empty() {
2818 assert!(norm(4).quantile_range().is_none());
2819 }
2820
2821 #[test]
2822 fn test_minmax_quantile_range_non_negative() {
2823 let mut n = norm(5);
2824 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2825 let iqr = n.quantile_range().unwrap();
2826 assert!(iqr >= 0.0, "IQR should be non-negative, got {}", iqr);
2827 }
2828
2829 #[test]
2832 fn test_minmax_upper_quartile_none_for_empty() {
2833 assert!(norm(4).upper_quartile().is_none());
2834 }
2835
2836 #[test]
2837 fn test_minmax_lower_quartile_none_for_empty() {
2838 assert!(norm(4).lower_quartile().is_none());
2839 }
2840
2841 #[test]
2842 fn test_minmax_upper_ge_lower_quartile() {
2843 let mut n = norm(8);
2844 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6), dec!(7), dec!(8)] {
2845 n.update(v);
2846 }
2847 let q3 = n.upper_quartile().unwrap();
2848 let q1 = n.lower_quartile().unwrap();
2849 assert!(q3 >= q1, "Q3 ({}) should be >= Q1 ({})", q3, q1);
2850 }
2851
2852 #[test]
2855 fn test_minmax_sign_change_rate_none_for_fewer_than_3() {
2856 let mut n = norm(4);
2857 n.update(dec!(1));
2858 n.update(dec!(2));
2859 assert!(n.sign_change_rate().is_none());
2860 }
2861
2862 #[test]
2863 fn test_minmax_sign_change_rate_one_for_zigzag() {
2864 let mut n = norm(5);
2865 for v in [dec!(1), dec!(3), dec!(1), dec!(3), dec!(1)] { n.update(v); }
2867 let r = n.sign_change_rate().unwrap();
2868 assert!((r - 1.0).abs() < 1e-9, "zigzag should give 1.0, got {}", r);
2869 }
2870
2871 #[test]
2872 fn test_minmax_sign_change_rate_zero_for_monotone() {
2873 let mut n = norm(5);
2874 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2875 let r = n.sign_change_rate().unwrap();
2876 assert!((r - 0.0).abs() < 1e-9, "monotone should give 0.0, got {}", r);
2877 }
2878
2879 #[test]
2884 fn test_consecutive_below_mean_none_for_single_value() {
2885 let mut n = norm(5);
2886 n.update(dec!(10));
2887 assert!(n.consecutive_below_mean().is_none());
2888 }
2889
2890 #[test]
2891 fn test_consecutive_below_mean_zero_when_latest_above_mean() {
2892 let mut n = norm(5);
2893 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(100)] { n.update(v); }
2894 let c = n.consecutive_below_mean().unwrap();
2895 assert_eq!(c, 0, "latest above mean → streak=0, got {}", c);
2896 }
2897
2898 #[test]
2899 fn test_consecutive_below_mean_counts_trailing_below() {
2900 let mut n = norm(5);
2901 for v in [dec!(100), dec!(100), dec!(1), dec!(1), dec!(1)] { n.update(v); }
2902 let c = n.consecutive_below_mean().unwrap();
2903 assert!(c >= 3, "last 3 below mean → streak>=3, got {}", c);
2904 }
2905
2906 #[test]
2909 fn test_drift_rate_none_for_single_value() {
2910 let mut n = norm(5);
2911 n.update(dec!(10));
2912 assert!(n.drift_rate().is_none());
2913 }
2914
2915 #[test]
2916 fn test_drift_rate_positive_for_rising_series() {
2917 let mut n = norm(6);
2918 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6)] { n.update(v); }
2919 let d = n.drift_rate().unwrap();
2920 assert!(d > 0.0, "rising series → positive drift, got {}", d);
2921 }
2922
2923 #[test]
2924 fn test_drift_rate_negative_for_falling_series() {
2925 let mut n = norm(6);
2926 for v in [dec!(6), dec!(5), dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
2927 let d = n.drift_rate().unwrap();
2928 assert!(d < 0.0, "falling series → negative drift, got {}", d);
2929 }
2930
2931 #[test]
2934 fn test_peak_to_trough_ratio_none_for_empty() {
2935 assert!(norm(4).peak_to_trough_ratio().is_none());
2936 }
2937
2938 #[test]
2939 fn test_peak_to_trough_ratio_one_for_constant() {
2940 let mut n = norm(4);
2941 for v in [dec!(10), dec!(10), dec!(10), dec!(10)] { n.update(v); }
2942 let r = n.peak_to_trough_ratio().unwrap();
2943 assert!((r - 1.0).abs() < 1e-9, "constant → ratio=1, got {}", r);
2944 }
2945
2946 #[test]
2947 fn test_peak_to_trough_ratio_above_one_for_spread() {
2948 let mut n = norm(4);
2949 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2950 let r = n.peak_to_trough_ratio().unwrap();
2951 assert!(r > 1.0, "spread → ratio>1, got {}", r);
2952 }
2953
2954 #[test]
2957 fn test_normalized_deviation_none_for_empty() {
2958 assert!(norm(4).normalized_deviation().is_none());
2959 }
2960
2961 #[test]
2962 fn test_normalized_deviation_none_for_constant() {
2963 let mut n = norm(4);
2964 for v in [dec!(5), dec!(5), dec!(5), dec!(5)] { n.update(v); }
2965 assert!(n.normalized_deviation().is_none());
2966 }
2967
2968 #[test]
2969 fn test_normalized_deviation_positive_for_latest_above_mean() {
2970 let mut n = norm(5);
2971 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(10)] { n.update(v); }
2972 let d = n.normalized_deviation().unwrap();
2973 assert!(d > 0.0, "latest above mean → positive deviation, got {}", d);
2974 }
2975
2976 #[test]
2979 fn test_window_cv_pct_none_for_single_value() {
2980 let mut n = norm(5);
2981 n.update(dec!(10));
2982 assert!(n.window_cv_pct().is_none());
2983 }
2984
2985 #[test]
2986 fn test_window_cv_pct_positive_for_varied_values() {
2987 let mut n = norm(4);
2988 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2989 let cv = n.window_cv_pct().unwrap();
2990 assert!(cv > 0.0, "expected positive CV%, got {}", cv);
2991 }
2992
2993 #[test]
2996 fn test_latest_rank_pct_none_for_single_value() {
2997 let mut n = norm(5);
2998 n.update(dec!(10));
2999 assert!(n.latest_rank_pct().is_none());
3000 }
3001
3002 #[test]
3003 fn test_latest_rank_pct_one_for_max_value() {
3004 let mut n = norm(4);
3005 for v in [dec!(1), dec!(2), dec!(3), dec!(100)] { n.update(v); }
3006 let r = n.latest_rank_pct().unwrap();
3007 assert!((r - 1.0).abs() < 1e-9, "latest is max → rank=1, got {}", r);
3008 }
3009
3010 #[test]
3011 fn test_latest_rank_pct_zero_for_min_value() {
3012 let mut n = norm(4);
3013 for v in [dec!(10), dec!(20), dec!(30), dec!(1)] { n.update(v); }
3014 let r = n.latest_rank_pct().unwrap();
3015 assert!(r.abs() < 1e-9, "latest is min → rank=0, got {}", r);
3016 }
3017}
3018
3019pub struct ZScoreNormalizer {
3042 window_size: usize,
3043 window: VecDeque<Decimal>,
3044 sum: Decimal,
3045 sum_sq: Decimal,
3046}
3047
3048impl ZScoreNormalizer {
3049 pub fn new(window_size: usize) -> Result<Self, StreamError> {
3055 if window_size == 0 {
3056 return Err(StreamError::ConfigError {
3057 reason: "ZScoreNormalizer window_size must be > 0".into(),
3058 });
3059 }
3060 Ok(Self {
3061 window_size,
3062 window: VecDeque::with_capacity(window_size),
3063 sum: Decimal::ZERO,
3064 sum_sq: Decimal::ZERO,
3065 })
3066 }
3067
3068 pub fn update(&mut self, value: Decimal) {
3074 if self.window.len() == self.window_size {
3075 let evicted = self.window.pop_front().unwrap_or(Decimal::ZERO);
3076 self.sum -= evicted;
3077 self.sum_sq -= evicted * evicted;
3078 }
3079 self.window.push_back(value);
3080 self.sum += value;
3081 self.sum_sq += value * value;
3082 }
3083
3084 #[must_use = "z-score is returned; ignoring it loses the normalized value"]
3096 pub fn normalize(&self, value: Decimal) -> Result<f64, StreamError> {
3097 let n = self.window.len();
3098 if n == 0 {
3099 return Err(StreamError::NormalizationError {
3100 reason: "window is empty; call update() before normalize()".into(),
3101 });
3102 }
3103 if n < 2 {
3104 return Ok(0.0);
3105 }
3106 let std_dev = self.std_dev().unwrap_or(0.0);
3107 if std_dev < f64::EPSILON {
3108 return Ok(0.0);
3109 }
3110 let mean = self.mean().ok_or_else(|| StreamError::NormalizationError {
3111 reason: "mean unavailable".into(),
3112 })?;
3113 let diff = value - mean;
3114 let diff_f64 = diff.to_f64().ok_or_else(|| StreamError::NormalizationError {
3115 reason: "Decimal-to-f64 conversion failed for diff".into(),
3116 })?;
3117 Ok(diff_f64 / std_dev)
3118 }
3119
3120 pub fn mean(&self) -> Option<Decimal> {
3122 if self.window.is_empty() {
3123 return None;
3124 }
3125 let n = Decimal::from(self.window.len() as u64);
3126 Some(self.sum / n)
3127 }
3128
3129 pub fn std_dev(&self) -> Option<f64> {
3135 let n = self.window.len();
3136 if n == 0 {
3137 return None;
3138 }
3139 if n < 2 {
3140 return Some(0.0);
3141 }
3142 self.variance_f64().map(f64::sqrt)
3143 }
3144
3145 pub fn reset(&mut self) {
3147 self.window.clear();
3148 self.sum = Decimal::ZERO;
3149 self.sum_sq = Decimal::ZERO;
3150 }
3151
3152 pub fn len(&self) -> usize {
3154 self.window.len()
3155 }
3156
3157 pub fn is_empty(&self) -> bool {
3159 self.window.is_empty()
3160 }
3161
3162 pub fn window_size(&self) -> usize {
3164 self.window_size
3165 }
3166
3167 pub fn is_full(&self) -> bool {
3172 self.window.len() == self.window_size
3173 }
3174
3175 pub fn sum(&self) -> Option<Decimal> {
3180 if self.window.is_empty() {
3181 return None;
3182 }
3183 Some(self.sum)
3184 }
3185
3186 pub fn variance(&self) -> Option<Decimal> {
3191 let n = self.window.len();
3192 if n < 2 {
3193 return None;
3194 }
3195 let n_dec = Decimal::from(n as u64);
3196 let mean = self.sum / n_dec;
3197 let v = (self.sum_sq / n_dec) - mean * mean;
3198 Some(if v < Decimal::ZERO { Decimal::ZERO } else { v })
3199 }
3200
3201 pub fn std_dev_f64(&self) -> Option<f64> {
3205 self.variance_f64().map(|v| v.sqrt())
3206 }
3207
3208 pub fn variance_f64(&self) -> Option<f64> {
3212 use rust_decimal::prelude::ToPrimitive;
3213 self.variance()?.to_f64()
3214 }
3215
3216 pub fn normalize_batch(
3226 &mut self,
3227 values: &[Decimal],
3228 ) -> Result<Vec<f64>, StreamError> {
3229 values
3230 .iter()
3231 .map(|&v| {
3232 self.update(v);
3233 self.normalize(v)
3234 })
3235 .collect()
3236 }
3237
3238 pub fn is_outlier(&self, value: Decimal, z_threshold: f64) -> bool {
3243 use rust_decimal::prelude::ToPrimitive;
3244 if self.window.len() < 2 {
3245 return false;
3246 }
3247 let sd = self.std_dev().unwrap_or(0.0);
3248 if sd == 0.0 {
3249 return false;
3250 }
3251 let Some(mean_f64) = self.mean().and_then(|m| m.to_f64()) else { return false; };
3252 let val_f64 = value.to_f64().unwrap_or(mean_f64);
3253 ((val_f64 - mean_f64) / sd).abs() > z_threshold
3254 }
3255
3256 pub fn percentile_rank(&self, value: Decimal) -> Option<f64> {
3260 if self.window.is_empty() {
3261 return None;
3262 }
3263 let count = self.window.iter().filter(|&&v| v <= value).count();
3264 Some(count as f64 / self.window.len() as f64)
3265 }
3266
3267 pub fn running_min(&self) -> Option<Decimal> {
3271 self.window.iter().copied().reduce(Decimal::min)
3272 }
3273
3274 pub fn running_max(&self) -> Option<Decimal> {
3278 self.window.iter().copied().reduce(Decimal::max)
3279 }
3280
3281 pub fn window_range(&self) -> Option<Decimal> {
3285 let min = self.running_min()?;
3286 let max = self.running_max()?;
3287 Some(max - min)
3288 }
3289
3290 pub fn coefficient_of_variation(&self) -> Option<f64> {
3295 let mean = self.mean()?;
3296 if mean.is_zero() {
3297 return None;
3298 }
3299 let std_dev = self.std_dev()?;
3300 let mean_f = mean.abs().to_f64()?;
3301 Some(std_dev / mean_f)
3302 }
3303
3304 pub fn sample_variance(&self) -> Option<f64> {
3311 let sd = self.std_dev()?;
3312 Some(sd * sd)
3313 }
3314
3315 pub fn window_mean_f64(&self) -> Option<f64> {
3320 use rust_decimal::prelude::ToPrimitive;
3321 self.mean()?.to_f64()
3322 }
3323
3324 pub fn is_near_mean(&self, value: Decimal, sigma_tolerance: f64) -> bool {
3330 if self.window.len() < 2 {
3332 return false;
3333 }
3334 let Some(std_dev) = self.std_dev() else { return false; };
3335 if std_dev == 0.0 {
3336 return true;
3337 }
3338 let Some(mean) = self.mean() else { return false; };
3339 use rust_decimal::prelude::ToPrimitive;
3340 let diff = (value - mean).abs().to_f64().unwrap_or(f64::MAX);
3341 diff / std_dev <= sigma_tolerance
3342 }
3343
3344 pub fn window_sum(&self) -> Decimal {
3348 self.sum
3349 }
3350
3351 pub fn window_sum_f64(&self) -> f64 {
3355 use rust_decimal::prelude::ToPrimitive;
3356 self.sum.to_f64().unwrap_or(0.0)
3357 }
3358
3359 pub fn window_max_f64(&self) -> Option<f64> {
3363 use rust_decimal::prelude::ToPrimitive;
3364 self.running_max()?.to_f64()
3365 }
3366
3367 pub fn window_min_f64(&self) -> Option<f64> {
3371 use rust_decimal::prelude::ToPrimitive;
3372 self.running_min()?.to_f64()
3373 }
3374
3375 pub fn window_span_f64(&self) -> Option<f64> {
3379 use rust_decimal::prelude::ToPrimitive;
3380 self.window_range()?.to_f64()
3381 }
3382
3383 pub fn kurtosis(&self) -> Option<f64> {
3389 use rust_decimal::prelude::ToPrimitive;
3390 let n = self.window.len();
3391 if n < 4 {
3392 return None;
3393 }
3394 let n_f = n as f64;
3395 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
3396 if vals.len() < n {
3397 return None;
3398 }
3399 let mean = vals.iter().sum::<f64>() / n_f;
3400 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
3401 let std_dev = variance.sqrt();
3402 if std_dev == 0.0 {
3403 return None;
3404 }
3405 let kurt = vals.iter().map(|v| ((v - mean) / std_dev).powi(4)).sum::<f64>() / n_f - 3.0;
3406 Some(kurt)
3407 }
3408
3409 pub fn skewness(&self) -> Option<f64> {
3415 use rust_decimal::prelude::ToPrimitive;
3416 let n = self.window.len();
3417 if n < 3 {
3418 return None;
3419 }
3420 let n_f = n as f64;
3421 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
3422 if vals.len() < n {
3423 return None;
3424 }
3425 let mean = vals.iter().sum::<f64>() / n_f;
3426 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
3427 let std_dev = variance.sqrt();
3428 if std_dev == 0.0 {
3429 return None;
3430 }
3431 let skew = vals.iter().map(|v| ((v - mean) / std_dev).powi(3)).sum::<f64>() / n_f;
3432 Some(skew)
3433 }
3434
3435 pub fn is_extreme(&self, value: Decimal, sigma: f64) -> bool {
3440 self.normalize(value).ok().map_or(false, |z| z.abs() > sigma)
3441 }
3442
3443 pub fn latest(&self) -> Option<Decimal> {
3445 self.window.back().copied()
3446 }
3447
3448 pub fn median(&self) -> Option<Decimal> {
3450 if self.window.is_empty() { return None; }
3451 let mut vals: Vec<Decimal> = self.window.iter().copied().collect();
3452 vals.sort();
3453 let mid = vals.len() / 2;
3454 if vals.len() % 2 == 0 {
3455 Some((vals[mid - 1] + vals[mid]) / Decimal::TWO)
3456 } else {
3457 Some(vals[mid])
3458 }
3459 }
3460
3461 pub fn percentile(&self, value: Decimal) -> Option<f64> {
3465 self.percentile_rank(value)
3466 }
3467
3468 pub fn interquartile_range(&self) -> Option<Decimal> {
3473 let n = self.window.len();
3474 if n < 4 {
3475 return None;
3476 }
3477 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
3478 sorted.sort();
3479 let q1_idx = n / 4;
3480 let q3_idx = 3 * n / 4;
3481 Some(sorted[q3_idx] - sorted[q1_idx])
3482 }
3483
3484 pub fn ema_z_score(value: Decimal, alpha: f64, ema_mean: &mut f64, ema_var: &mut f64) -> Option<f64> {
3491 use rust_decimal::prelude::ToPrimitive;
3492 let v = value.to_f64()?;
3493 let delta = v - *ema_mean;
3494 *ema_mean += alpha * delta;
3495 *ema_var = (1.0 - alpha) * (*ema_var + alpha * delta * delta);
3496 let std = ema_var.sqrt();
3497 if std == 0.0 { return None; }
3498 Some((v - *ema_mean) / std)
3499 }
3500
3501 pub fn z_score_of_latest(&self) -> Option<f64> {
3505 let latest = self.latest()?;
3506 self.normalize(latest).ok()
3507 }
3508
3509 pub fn ema_of_z_scores(&self, alpha: f64) -> Option<f64> {
3514 let n = self.window.len();
3515 if n < 2 {
3516 return None;
3517 }
3518 let mut ema: Option<f64> = None;
3519 for &value in &self.window {
3520 if let Ok(z) = self.normalize(value) {
3521 ema = Some(match ema {
3522 None => z,
3523 Some(prev) => alpha * z + (1.0 - alpha) * prev,
3524 });
3525 }
3526 }
3527 ema
3528 }
3529
3530 pub fn add_observation(&mut self, value: Decimal) -> &mut Self {
3532 self.update(value);
3533 self
3534 }
3535
3536 pub fn deviation_from_mean(&self, value: Decimal) -> Option<f64> {
3540 use rust_decimal::prelude::ToPrimitive;
3541 let mean = self.mean()?.to_f64()?;
3542 value.to_f64().map(|v| v - mean)
3543 }
3544
3545 pub fn trim_outliers(&self, sigma: f64) -> Vec<Decimal> {
3550 use rust_decimal::prelude::ToPrimitive;
3551 if self.window.is_empty() { return vec![]; }
3552 let Some(mean) = self.mean() else { return vec![]; };
3553 let std = match self.std_dev() {
3554 Some(s) if s > 0.0 => s,
3555 _ => return self.window.iter().copied().collect(),
3556 };
3557 let Some(mean_f64) = mean.to_f64() else { return vec![]; };
3558 self.window.iter().copied()
3559 .filter(|v| {
3560 v.to_f64().map_or(false, |vf| ((vf - mean_f64) / std).abs() <= sigma)
3561 })
3562 .collect()
3563 }
3564
3565 pub fn rolling_zscore_batch(&mut self, values: &[Decimal]) -> Vec<Option<f64>> {
3571 values.iter().map(|&v| {
3572 self.update(v);
3573 self.normalize(v).ok()
3574 }).collect()
3575 }
3576
3577 pub fn rolling_mean_change(&self) -> Option<f64> {
3583 let n = self.window.len();
3584 if n < 2 {
3585 return None;
3586 }
3587 let mid = n / 2;
3588 let first: Decimal = self.window.iter().take(mid).copied().sum::<Decimal>()
3589 / Decimal::from(mid as u64);
3590 let second: Decimal = self.window.iter().skip(mid).copied().sum::<Decimal>()
3591 / Decimal::from((n - mid) as u64);
3592 (second - first).to_f64()
3593 }
3594
3595 pub fn count_positive_z_scores(&self) -> usize {
3599 self.window
3600 .iter()
3601 .filter(|&&v| self.normalize(v).map_or(false, |z| z > 0.0))
3602 .count()
3603 }
3604
3605 pub fn is_mean_stable(&self, threshold: f64) -> bool {
3610 self.rolling_mean_change().map_or(false, |c| c.abs() < threshold)
3611 }
3612
3613 pub fn above_threshold_count(&self, z_threshold: f64) -> usize {
3617 self.window
3618 .iter()
3619 .filter(|&&v| {
3620 self.normalize(v)
3621 .map_or(false, |z| z.abs() > z_threshold)
3622 })
3623 .count()
3624 }
3625
3626 pub fn mad(&self) -> Option<Decimal> {
3631 let med = self.median()?;
3632 let mut deviations: Vec<Decimal> = self.window.iter().map(|&x| (x - med).abs()).collect();
3633 deviations.sort();
3634 let n = deviations.len();
3635 if n == 0 { return None; }
3636 let mid = n / 2;
3637 if n % 2 == 0 {
3638 Some((deviations[mid - 1] + deviations[mid]) / Decimal::TWO)
3639 } else {
3640 Some(deviations[mid])
3641 }
3642 }
3643
3644 pub fn robust_z_score(&self, value: Decimal) -> Option<f64> {
3649 use rust_decimal::prelude::ToPrimitive;
3650 let med = self.median()?;
3651 let mad = self.mad()?;
3652 if mad.is_zero() { return None; }
3653 ((value - med) / mad).to_f64()
3654 }
3655
3656 pub fn count_above(&self, threshold: Decimal) -> usize {
3658 self.window.iter().filter(|&&v| v > threshold).count()
3659 }
3660
3661 pub fn count_below(&self, threshold: Decimal) -> usize {
3663 self.window.iter().filter(|&&v| v < threshold).count()
3664 }
3665
3666 pub fn percentile_value(&self, p: f64) -> Option<Decimal> {
3671 if self.window.is_empty() {
3672 return None;
3673 }
3674 let p = p.clamp(0.0, 1.0);
3675 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
3676 sorted.sort();
3677 let n = sorted.len();
3678 if n == 1 {
3679 return Some(sorted[0]);
3680 }
3681 let idx = p * (n - 1) as f64;
3682 let lo = idx.floor() as usize;
3683 let hi = idx.ceil() as usize;
3684 if lo == hi {
3685 Some(sorted[lo])
3686 } else {
3687 let frac = Decimal::try_from(idx - lo as f64).ok()?;
3688 Some(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
3689 }
3690 }
3691
3692 pub fn ewma(&self, alpha: f64) -> Option<f64> {
3698 use rust_decimal::prelude::ToPrimitive;
3699 let alpha = alpha.clamp(1e-9, 1.0);
3700 let mut iter = self.window.iter();
3701 let first = iter.next()?.to_f64()?;
3702 let result = iter.fold(first, |acc, &v| {
3703 let vf = v.to_f64().unwrap_or(acc);
3704 alpha * vf + (1.0 - alpha) * acc
3705 });
3706 Some(result)
3707 }
3708
3709 pub fn midpoint(&self) -> Option<Decimal> {
3713 let lo = self.running_min()?;
3714 let hi = self.running_max()?;
3715 Some((lo + hi) / Decimal::from(2u64))
3716 }
3717
3718 pub fn clamp_to_window(&self, value: Decimal) -> Decimal {
3722 match (self.running_min(), self.running_max()) {
3723 (Some(lo), Some(hi)) => value.max(lo).min(hi),
3724 _ => value,
3725 }
3726 }
3727
3728 pub fn fraction_above_mid(&self) -> Option<f64> {
3733 let lo = self.running_min()?;
3734 let hi = self.running_max()?;
3735 if lo == hi {
3736 return None;
3737 }
3738 let mid = (lo + hi) / Decimal::from(2u64);
3739 let above = self.window.iter().filter(|&&v| v > mid).count();
3740 Some(above as f64 / self.window.len() as f64)
3741 }
3742
3743 pub fn normalized_range(&self) -> Option<f64> {
3748 use rust_decimal::prelude::ToPrimitive;
3749 let span = self.window_range()?;
3750 let mean = self.mean()?;
3751 if mean.is_zero() {
3752 return None;
3753 }
3754 (span / mean).to_f64()
3755 }
3756
3757 pub fn min_max(&self) -> Option<(Decimal, Decimal)> {
3761 Some((self.running_min()?, self.running_max()?))
3762 }
3763
3764 pub fn values(&self) -> Vec<Decimal> {
3766 self.window.iter().copied().collect()
3767 }
3768
3769 pub fn above_zero_fraction(&self) -> Option<f64> {
3773 if self.window.is_empty() {
3774 return None;
3775 }
3776 let above = self.window.iter().filter(|&&v| v > Decimal::ZERO).count();
3777 Some(above as f64 / self.window.len() as f64)
3778 }
3779
3780 pub fn z_score_opt(&self, value: Decimal) -> Option<f64> {
3786 self.normalize(value).ok()
3787 }
3788
3789 pub fn is_stable(&self, z_threshold: f64) -> bool {
3794 self.z_score_of_latest()
3795 .map_or(false, |z| z.abs() <= z_threshold)
3796 }
3797
3798 pub fn fraction_above(&self, threshold: Decimal) -> Option<f64> {
3802 if self.window.is_empty() {
3803 return None;
3804 }
3805 Some(self.count_above(threshold) as f64 / self.window.len() as f64)
3806 }
3807
3808 pub fn fraction_below(&self, threshold: Decimal) -> Option<f64> {
3812 if self.window.is_empty() {
3813 return None;
3814 }
3815 Some(self.count_below(threshold) as f64 / self.window.len() as f64)
3816 }
3817
3818 pub fn window_values_above(&self, threshold: Decimal) -> Vec<Decimal> {
3820 self.window.iter().copied().filter(|&v| v > threshold).collect()
3821 }
3822
3823 pub fn window_values_below(&self, threshold: Decimal) -> Vec<Decimal> {
3825 self.window.iter().copied().filter(|&v| v < threshold).collect()
3826 }
3827
3828 pub fn count_equal(&self, value: Decimal) -> usize {
3830 self.window.iter().filter(|&&v| v == value).count()
3831 }
3832
3833 pub fn rolling_range(&self) -> Option<Decimal> {
3837 let lo = self.running_min()?;
3838 let hi = self.running_max()?;
3839 Some(hi - lo)
3840 }
3841
3842 pub fn autocorrelation_lag1(&self) -> Option<f64> {
3846 use rust_decimal::prelude::ToPrimitive;
3847 let n = self.window.len();
3848 if n < 2 {
3849 return None;
3850 }
3851 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
3852 if vals.len() < 2 {
3853 return None;
3854 }
3855 let mean = vals.iter().sum::<f64>() / vals.len() as f64;
3856 let var: f64 = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / vals.len() as f64;
3857 if var == 0.0 {
3858 return None;
3859 }
3860 let cov: f64 = vals.windows(2).map(|w| (w[0] - mean) * (w[1] - mean)).sum::<f64>()
3861 / (vals.len() - 1) as f64;
3862 Some(cov / var)
3863 }
3864
3865 pub fn trend_consistency(&self) -> Option<f64> {
3869 let n = self.window.len();
3870 if n < 2 {
3871 return None;
3872 }
3873 let up = self.window.iter().collect::<Vec<_>>().windows(2)
3874 .filter(|w| w[1] > w[0]).count();
3875 Some(up as f64 / (n - 1) as f64)
3876 }
3877
3878 pub fn mean_absolute_deviation(&self) -> Option<f64> {
3882 use rust_decimal::prelude::ToPrimitive;
3883 if self.window.is_empty() {
3884 return None;
3885 }
3886 let n = self.window.len();
3887 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
3888 let mean = vals.iter().sum::<f64>() / n as f64;
3889 let mad = vals.iter().map(|v| (v - mean).abs()).sum::<f64>() / n as f64;
3890 Some(mad)
3891 }
3892
3893 pub fn percentile_of_latest(&self) -> Option<f64> {
3898 let latest = self.latest()?;
3899 self.percentile(latest)
3900 }
3901
3902 pub fn tail_ratio(&self) -> Option<f64> {
3908 use rust_decimal::prelude::ToPrimitive;
3909 let max = self.running_max()?;
3910 let p75 = self.percentile_value(0.75)?;
3911 if p75.is_zero() {
3912 return None;
3913 }
3914 (max / p75).to_f64()
3915 }
3916
3917 pub fn z_score_of_min(&self) -> Option<f64> {
3921 let min = self.running_min()?;
3922 self.z_score_opt(min)
3923 }
3924
3925 pub fn z_score_of_max(&self) -> Option<f64> {
3929 let max = self.running_max()?;
3930 self.z_score_opt(max)
3931 }
3932
3933 pub fn window_entropy(&self) -> Option<f64> {
3939 if self.window.is_empty() {
3940 return None;
3941 }
3942 let n = self.window.len() as f64;
3943 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
3944 for v in &self.window {
3945 *counts.entry(v.to_string()).or_insert(0) += 1;
3946 }
3947 let entropy: f64 = counts.values().map(|&c| {
3948 let p = c as f64 / n;
3949 -p * p.ln()
3950 }).sum();
3951 Some(entropy)
3952 }
3953
3954 pub fn normalized_std_dev(&self) -> Option<f64> {
3956 self.coefficient_of_variation()
3957 }
3958
3959 pub fn value_above_mean_count(&self) -> Option<usize> {
3963 let mean = self.mean()?;
3964 Some(self.window.iter().filter(|&&v| v > mean).count())
3965 }
3966
3967 pub fn consecutive_above_mean(&self) -> Option<usize> {
3971 let mean = self.mean()?;
3972 let mut max_run = 0usize;
3973 let mut current = 0usize;
3974 for &v in &self.window {
3975 if v > mean {
3976 current += 1;
3977 if current > max_run {
3978 max_run = current;
3979 }
3980 } else {
3981 current = 0;
3982 }
3983 }
3984 Some(max_run)
3985 }
3986
3987 pub fn above_threshold_fraction(&self, threshold: Decimal) -> Option<f64> {
3991 if self.window.is_empty() {
3992 return None;
3993 }
3994 let count = self.window.iter().filter(|&&v| v > threshold).count();
3995 Some(count as f64 / self.window.len() as f64)
3996 }
3997
3998 pub fn below_threshold_fraction(&self, threshold: Decimal) -> Option<f64> {
4002 if self.window.is_empty() {
4003 return None;
4004 }
4005 let count = self.window.iter().filter(|&&v| v < threshold).count();
4006 Some(count as f64 / self.window.len() as f64)
4007 }
4008
4009 pub fn lag_k_autocorrelation(&self, k: usize) -> Option<f64> {
4013 use rust_decimal::prelude::ToPrimitive;
4014 let n = self.window.len();
4015 if k == 0 || k >= n {
4016 return None;
4017 }
4018 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4019 if vals.len() != n {
4020 return None;
4021 }
4022 let mean = vals.iter().sum::<f64>() / n as f64;
4023 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
4024 if var == 0.0 {
4025 return None;
4026 }
4027 let m = n - k;
4028 let cov: f64 = (0..m).map(|i| (vals[i] - mean) * (vals[i + k] - mean)).sum::<f64>() / m as f64;
4029 Some(cov / var)
4030 }
4031
4032 pub fn half_life_estimate(&self) -> Option<f64> {
4038 use rust_decimal::prelude::ToPrimitive;
4039 let n = self.window.len();
4040 if n < 3 {
4041 return None;
4042 }
4043 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4044 if vals.len() != n {
4045 return None;
4046 }
4047 let diffs: Vec<f64> = vals.windows(2).map(|w| w[1] - w[0]).collect();
4048 let lagged: Vec<f64> = vals[..n - 1].to_vec();
4049 let nf = diffs.len() as f64;
4050 let mean_l = lagged.iter().sum::<f64>() / nf;
4051 let mean_d = diffs.iter().sum::<f64>() / nf;
4052 let cov: f64 = lagged.iter().zip(diffs.iter()).map(|(l, d)| (l - mean_l) * (d - mean_d)).sum::<f64>();
4053 let var: f64 = lagged.iter().map(|l| (l - mean_l).powi(2)).sum::<f64>();
4054 if var == 0.0 {
4055 return None;
4056 }
4057 let beta = cov / var;
4058 if beta >= 0.0 {
4059 return None;
4060 }
4061 let lambda = (1.0 + beta).abs().ln();
4062 if lambda == 0.0 {
4063 return None;
4064 }
4065 Some(-std::f64::consts::LN_2 / lambda)
4066 }
4067
4068 pub fn geometric_mean(&self) -> Option<f64> {
4073 use rust_decimal::prelude::ToPrimitive;
4074 if self.window.is_empty() {
4075 return None;
4076 }
4077 let logs: Vec<f64> = self.window.iter()
4078 .filter_map(|v| v.to_f64())
4079 .filter_map(|f| if f > 0.0 { Some(f.ln()) } else { None })
4080 .collect();
4081 if logs.len() != self.window.len() {
4082 return None;
4083 }
4084 Some((logs.iter().sum::<f64>() / logs.len() as f64).exp())
4085 }
4086
4087 pub fn harmonic_mean(&self) -> Option<f64> {
4092 use rust_decimal::prelude::ToPrimitive;
4093 if self.window.is_empty() {
4094 return None;
4095 }
4096 let reciprocals: Vec<f64> = self.window.iter()
4097 .filter_map(|v| v.to_f64())
4098 .filter_map(|f| if f != 0.0 { Some(1.0 / f) } else { None })
4099 .collect();
4100 if reciprocals.len() != self.window.len() {
4101 return None;
4102 }
4103 let n = reciprocals.len() as f64;
4104 Some(n / reciprocals.iter().sum::<f64>())
4105 }
4106
4107 pub fn range_normalized_value(&self, value: Decimal) -> Option<f64> {
4111 use rust_decimal::prelude::ToPrimitive;
4112 let min = self.running_min()?;
4113 let max = self.running_max()?;
4114 let range = max - min;
4115 if range.is_zero() {
4116 return None;
4117 }
4118 ((value - min) / range).to_f64()
4119 }
4120
4121 pub fn distance_from_median(&self, value: Decimal) -> Option<f64> {
4125 use rust_decimal::prelude::ToPrimitive;
4126 let med = self.median()?;
4127 (value - med).to_f64()
4128 }
4129
4130 pub fn momentum(&self) -> Option<f64> {
4135 use rust_decimal::prelude::ToPrimitive;
4136 if self.window.len() < 2 {
4137 return None;
4138 }
4139 let oldest = *self.window.front()?;
4140 let latest = *self.window.back()?;
4141 (latest - oldest).to_f64()
4142 }
4143
4144 pub fn value_rank(&self, value: Decimal) -> Option<f64> {
4149 if self.window.is_empty() {
4150 return None;
4151 }
4152 let n = self.window.len();
4153 let below = self.window.iter().filter(|&&v| v < value).count();
4154 Some(below as f64 / n as f64)
4155 }
4156
4157 pub fn coeff_of_variation(&self) -> Option<f64> {
4162 use rust_decimal::prelude::ToPrimitive;
4163 let n = self.window.len();
4164 if n < 2 {
4165 return None;
4166 }
4167 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4168 if vals.len() < 2 {
4169 return None;
4170 }
4171 let nf = vals.len() as f64;
4172 let mean = vals.iter().sum::<f64>() / nf;
4173 if mean == 0.0 {
4174 return None;
4175 }
4176 let std_dev = (vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (nf - 1.0)).sqrt();
4177 Some(std_dev / mean.abs())
4178 }
4179
4180 pub fn quantile_range(&self) -> Option<f64> {
4184 use rust_decimal::prelude::ToPrimitive;
4185 let q3 = self.percentile_value(0.75)?;
4186 let q1 = self.percentile_value(0.25)?;
4187 (q3 - q1).to_f64()
4188 }
4189
4190 pub fn upper_quartile(&self) -> Option<Decimal> {
4194 self.percentile_value(0.75)
4195 }
4196
4197 pub fn lower_quartile(&self) -> Option<Decimal> {
4201 self.percentile_value(0.25)
4202 }
4203
4204 pub fn sign_change_rate(&self) -> Option<f64> {
4210 let n = self.window.len();
4211 if n < 3 {
4212 return None;
4213 }
4214 let vals: Vec<&Decimal> = self.window.iter().collect();
4215 let diffs: Vec<i32> = vals
4216 .windows(2)
4217 .map(|w| {
4218 if w[1] > w[0] { 1 } else if w[1] < w[0] { -1 } else { 0 }
4219 })
4220 .collect();
4221 let total_pairs = (diffs.len() - 1) as f64;
4222 if total_pairs == 0.0 {
4223 return None;
4224 }
4225 let changes = diffs
4226 .windows(2)
4227 .filter(|w| w[0] != 0 && w[1] != 0 && w[0] != w[1])
4228 .count();
4229 Some(changes as f64 / total_pairs)
4230 }
4231
4232 pub fn trimmed_mean(&self, p: f64) -> Option<f64> {
4240 use rust_decimal::prelude::ToPrimitive;
4241 if self.window.is_empty() {
4242 return None;
4243 }
4244 let p = p.clamp(0.0, 0.499);
4245 let mut sorted: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4246 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4247 let n = sorted.len();
4248 let trim = (n as f64 * p).floor() as usize;
4249 let trimmed = &sorted[trim..n - trim];
4250 if trimmed.is_empty() {
4251 return None;
4252 }
4253 Some(trimmed.iter().sum::<f64>() / trimmed.len() as f64)
4254 }
4255
4256 pub fn linear_trend_slope(&self) -> Option<f64> {
4261 use rust_decimal::prelude::ToPrimitive;
4262 let n = self.window.len();
4263 if n < 2 {
4264 return None;
4265 }
4266 let n_f = n as f64;
4267 let x_mean = (n_f - 1.0) / 2.0;
4268 let y_vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4269 if y_vals.len() < 2 {
4270 return None;
4271 }
4272 let y_mean = y_vals.iter().sum::<f64>() / y_vals.len() as f64;
4273 let numerator: f64 = y_vals
4274 .iter()
4275 .enumerate()
4276 .map(|(i, &y)| (i as f64 - x_mean) * (y - y_mean))
4277 .sum();
4278 let denominator: f64 = (0..n).map(|i| (i as f64 - x_mean).powi(2)).sum();
4279 if denominator == 0.0 {
4280 return None;
4281 }
4282 Some(numerator / denominator)
4283 }
4284
4285}
4286
4287#[cfg(test)]
4288mod zscore_tests {
4289 use super::*;
4290 use rust_decimal_macros::dec;
4291
4292 fn znorm(w: usize) -> ZScoreNormalizer {
4293 ZScoreNormalizer::new(w).unwrap()
4294 }
4295
4296 #[test]
4297 fn test_zscore_new_zero_window_returns_error() {
4298 assert!(matches!(
4299 ZScoreNormalizer::new(0),
4300 Err(StreamError::ConfigError { .. })
4301 ));
4302 }
4303
4304 #[test]
4305 fn test_zscore_is_full_false_before_capacity() {
4306 let mut n = znorm(3);
4307 assert!(!n.is_full());
4308 n.update(dec!(1));
4309 n.update(dec!(2));
4310 assert!(!n.is_full());
4311 n.update(dec!(3));
4312 assert!(n.is_full());
4313 }
4314
4315 #[test]
4316 fn test_zscore_is_full_stays_true_after_eviction() {
4317 let mut n = znorm(3);
4318 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
4319 n.update(v);
4320 }
4321 assert!(n.is_full());
4322 }
4323
4324 #[test]
4325 fn test_zscore_empty_window_returns_error() {
4326 let n = znorm(4);
4327 assert!(matches!(
4328 n.normalize(dec!(1)),
4329 Err(StreamError::NormalizationError { .. })
4330 ));
4331 }
4332
4333 #[test]
4334 fn test_zscore_single_value_returns_zero() {
4335 let mut n = znorm(4);
4336 n.update(dec!(50));
4337 assert_eq!(n.normalize(dec!(50)).unwrap(), 0.0);
4338 }
4339
4340 #[test]
4341 fn test_zscore_mean_is_zero() {
4342 let mut n = znorm(5);
4343 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] {
4344 n.update(v);
4345 }
4346 let z = n.normalize(dec!(30)).unwrap();
4348 assert!((z - 0.0).abs() < 1e-9, "z-score of mean should be 0, got {z}");
4349 }
4350
4351 #[test]
4352 fn test_zscore_symmetric_around_mean() {
4353 let mut n = znorm(4);
4354 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
4355 n.update(v);
4356 }
4357 let z_low = n.normalize(dec!(15)).unwrap();
4359 let z_high = n.normalize(dec!(35)).unwrap();
4360 assert!((z_low.abs() - z_high.abs()).abs() < 1e-9);
4361 assert!(z_low < 0.0, "below-mean z-score should be negative");
4362 assert!(z_high > 0.0, "above-mean z-score should be positive");
4363 }
4364
4365 #[test]
4366 fn test_zscore_all_same_returns_zero() {
4367 let mut n = znorm(4);
4368 for _ in 0..4 {
4369 n.update(dec!(100));
4370 }
4371 assert_eq!(n.normalize(dec!(100)).unwrap(), 0.0);
4372 }
4373
4374 #[test]
4375 fn test_zscore_rolling_window_eviction() {
4376 let mut n = znorm(3);
4377 n.update(dec!(1));
4378 n.update(dec!(2));
4379 n.update(dec!(3));
4380 n.update(dec!(100));
4382 let z = n.normalize(dec!(100)).unwrap();
4384 assert!(z > 0.0);
4385 }
4386
4387 #[test]
4388 fn test_zscore_reset_clears_state() {
4389 let mut n = znorm(4);
4390 for v in [dec!(10), dec!(20), dec!(30)] {
4391 n.update(v);
4392 }
4393 n.reset();
4394 assert!(n.is_empty());
4395 assert!(n.mean().is_none());
4396 assert!(matches!(
4397 n.normalize(dec!(1)),
4398 Err(StreamError::NormalizationError { .. })
4399 ));
4400 }
4401
4402 #[test]
4403 fn test_zscore_len_and_window_size() {
4404 let mut n = znorm(5);
4405 assert_eq!(n.len(), 0);
4406 assert!(n.is_empty());
4407 n.update(dec!(1));
4408 n.update(dec!(2));
4409 assert_eq!(n.len(), 2);
4410 assert_eq!(n.window_size(), 5);
4411 }
4412
4413 #[test]
4416 fn test_std_dev_none_when_empty() {
4417 let n = znorm(5);
4418 assert!(n.std_dev().is_none());
4419 }
4420
4421 #[test]
4422 fn test_std_dev_zero_with_one_observation() {
4423 let mut n = znorm(5);
4424 n.update(dec!(42));
4425 assert_eq!(n.std_dev(), Some(0.0));
4426 }
4427
4428 #[test]
4429 fn test_std_dev_zero_when_all_same() {
4430 let mut n = znorm(4);
4431 for _ in 0..4 {
4432 n.update(dec!(10));
4433 }
4434 let sd = n.std_dev().unwrap();
4435 assert!(sd < f64::EPSILON);
4436 }
4437
4438 #[test]
4439 fn test_std_dev_positive_for_varying_values() {
4440 let mut n = znorm(4);
4441 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
4442 n.update(v);
4443 }
4444 let sd = n.std_dev().unwrap();
4445 assert!((sd - 11.18).abs() < 0.01);
4447 }
4448
4449 #[test]
4452 fn test_variance_none_when_fewer_than_two_observations() {
4453 let mut n = znorm(5);
4454 assert!(n.variance().is_none());
4455 n.update(dec!(10));
4456 assert!(n.variance().is_none());
4457 }
4458
4459 #[test]
4460 fn test_variance_zero_for_identical_values() {
4461 let mut n = znorm(4);
4462 for _ in 0..4 {
4463 n.update(dec!(7));
4464 }
4465 assert_eq!(n.variance().unwrap(), dec!(0));
4466 }
4467
4468 #[test]
4469 fn test_variance_correct_for_known_values() {
4470 let mut n = znorm(4);
4471 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
4472 n.update(v);
4473 }
4474 let var = n.variance().unwrap();
4476 let var_f64 = f64::try_from(var).unwrap();
4477 assert!((var_f64 - 125.0).abs() < 0.01, "expected 125 got {var_f64}");
4478 }
4479
4480 #[test]
4483 fn test_normalize_batch_same_length_as_input() {
4484 let mut n = znorm(5);
4485 let vals = [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)];
4486 let out = n.normalize_batch(&vals).unwrap();
4487 assert_eq!(out.len(), vals.len());
4488 }
4489
4490 #[test]
4491 fn test_normalize_batch_last_value_matches_single_normalize() {
4492 let mut n1 = znorm(5);
4493 let vals = [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)];
4494 let batch = n1.normalize_batch(&vals).unwrap();
4495
4496 let mut n2 = znorm(5);
4497 for &v in &vals {
4498 n2.update(v);
4499 }
4500 let single = n2.normalize(dec!(50)).unwrap();
4501 assert!((batch[4] - single).abs() < 1e-9);
4502 }
4503
4504 #[test]
4505 fn test_sum_empty_returns_none() {
4506 let n = znorm(4);
4507 assert!(n.sum().is_none());
4508 }
4509
4510 #[test]
4511 fn test_sum_matches_manual() {
4512 let mut n = znorm(4);
4513 n.update(dec!(10));
4514 n.update(dec!(20));
4515 n.update(dec!(30));
4516 assert_eq!(n.sum().unwrap(), dec!(60));
4518 }
4519
4520 #[test]
4521 fn test_sum_evicts_old_values() {
4522 let mut n = znorm(2);
4523 n.update(dec!(10));
4524 n.update(dec!(20));
4525 n.update(dec!(30)); assert_eq!(n.sum().unwrap(), dec!(50));
4528 }
4529
4530 #[test]
4531 fn test_std_dev_single_observation_returns_some_zero() {
4532 let mut n = znorm(5);
4533 n.update(dec!(10));
4534 assert!(n.std_dev().is_none() || n.std_dev().unwrap() == 0.0);
4537 }
4538
4539 #[test]
4540 fn test_std_dev_constant_window_is_zero() {
4541 let mut n = znorm(4);
4542 for _ in 0..4 {
4543 n.update(dec!(5));
4544 }
4545 let sd = n.std_dev().unwrap();
4546 assert!(sd.abs() < 1e-9, "expected 0.0 got {sd}");
4547 }
4548
4549 #[test]
4550 fn test_std_dev_known_population() {
4551 let mut n = znorm(8);
4553 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
4554 n.update(v);
4555 }
4556 let sd = n.std_dev().unwrap();
4557 assert!((sd - 2.0).abs() < 1e-6, "expected ~2.0 got {sd}");
4558 }
4559
4560 #[test]
4563 fn test_window_range_none_when_empty() {
4564 let n = znorm(5);
4565 assert!(n.window_range().is_none());
4566 }
4567
4568 #[test]
4569 fn test_window_range_correct_value() {
4570 let mut n = znorm(5);
4571 n.update(dec!(10));
4572 n.update(dec!(20));
4573 n.update(dec!(15));
4574 assert_eq!(n.window_range().unwrap(), dec!(10));
4576 }
4577
4578 #[test]
4579 fn test_coefficient_of_variation_none_when_empty() {
4580 let n = znorm(5);
4581 assert!(n.coefficient_of_variation().is_none());
4582 }
4583
4584 #[test]
4585 fn test_coefficient_of_variation_none_when_mean_zero() {
4586 let mut n = znorm(5);
4587 n.update(dec!(-5));
4588 n.update(dec!(5)); assert!(n.coefficient_of_variation().is_none());
4590 }
4591
4592 #[test]
4593 fn test_coefficient_of_variation_positive_for_nonzero_mean() {
4594 let mut n = znorm(8);
4595 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
4596 n.update(v);
4597 }
4598 let cv = n.coefficient_of_variation().unwrap();
4600 assert!((cv - 0.4).abs() < 1e-5, "expected ~0.4 got {cv}");
4601 }
4602
4603 #[test]
4606 fn test_sample_variance_none_when_empty() {
4607 let n = znorm(5);
4608 assert!(n.sample_variance().is_none());
4609 }
4610
4611 #[test]
4612 fn test_sample_variance_zero_for_constant_window() {
4613 let mut n = znorm(3);
4614 n.update(dec!(7));
4615 n.update(dec!(7));
4616 n.update(dec!(7));
4617 assert!(n.sample_variance().unwrap().abs() < 1e-10);
4618 }
4619
4620 #[test]
4621 fn test_sample_variance_equals_std_dev_squared() {
4622 let mut n = znorm(8);
4623 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
4624 n.update(v);
4625 }
4626 let variance = n.sample_variance().unwrap();
4628 let sd = n.std_dev().unwrap();
4629 assert!((variance - sd * sd).abs() < 1e-10);
4630 }
4631
4632 #[test]
4635 fn test_window_mean_f64_none_when_empty() {
4636 let n = znorm(5);
4637 assert!(n.window_mean_f64().is_none());
4638 }
4639
4640 #[test]
4641 fn test_window_mean_f64_correct_value() {
4642 let mut n = znorm(4);
4643 n.update(dec!(10));
4644 n.update(dec!(20));
4645 let m = n.window_mean_f64().unwrap();
4647 assert!((m - 15.0).abs() < 1e-10);
4648 }
4649
4650 #[test]
4651 fn test_window_mean_f64_matches_decimal_mean() {
4652 let mut n = znorm(8);
4653 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
4654 n.update(v);
4655 }
4656 use rust_decimal::prelude::ToPrimitive;
4657 let expected = n.mean().unwrap().to_f64().unwrap();
4658 assert!((n.window_mean_f64().unwrap() - expected).abs() < 1e-10);
4659 }
4660
4661 #[test]
4664 fn test_kurtosis_none_when_fewer_than_4_observations() {
4665 let mut n = znorm(5);
4666 n.update(dec!(1));
4667 n.update(dec!(2));
4668 n.update(dec!(3));
4669 assert!(n.kurtosis().is_none());
4670 }
4671
4672 #[test]
4673 fn test_kurtosis_returns_some_with_4_observations() {
4674 let mut n = znorm(4);
4675 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
4676 n.update(v);
4677 }
4678 assert!(n.kurtosis().is_some());
4679 }
4680
4681 #[test]
4682 fn test_kurtosis_none_when_all_same_value() {
4683 let mut n = znorm(4);
4684 for _ in 0..4 {
4685 n.update(dec!(5));
4686 }
4687 assert!(n.kurtosis().is_none());
4689 }
4690
4691 #[test]
4692 fn test_kurtosis_uniform_distribution_is_negative() {
4693 let mut n = znorm(10);
4695 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5),
4696 dec!(6), dec!(7), dec!(8), dec!(9), dec!(10)] {
4697 n.update(v);
4698 }
4699 let k = n.kurtosis().unwrap();
4700 assert!(k < 0.0, "expected negative excess kurtosis for uniform dist, got {k}");
4702 }
4703
4704 #[test]
4706 fn test_is_near_mean_false_with_fewer_than_two_obs() {
4707 let mut n = znorm(5);
4708 n.update(dec!(10));
4709 assert!(!n.is_near_mean(dec!(10), 1.0));
4710 }
4711
4712 #[test]
4713 fn test_is_near_mean_true_within_one_sigma() {
4714 let mut n = znorm(10);
4715 for _ in 0..9 {
4717 n.update(dec!(10));
4718 }
4719 n.update(dec!(20));
4720 assert!(n.is_near_mean(dec!(11), 1.0));
4722 }
4723
4724 #[test]
4725 fn test_is_near_mean_false_when_far_from_mean() {
4726 let mut n = znorm(5);
4727 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] {
4728 n.update(v);
4729 }
4730 assert!(!n.is_near_mean(dec!(100), 2.0));
4732 }
4733
4734 #[test]
4735 fn test_is_near_mean_true_when_all_identical_any_value() {
4736 let mut n = znorm(4);
4737 for _ in 0..4 {
4738 n.update(dec!(7));
4739 }
4740 assert!(n.is_near_mean(dec!(999), 0.0));
4742 }
4743
4744 #[test]
4746 fn test_window_sum_f64_zero_on_empty() {
4747 let n = znorm(5);
4748 assert_eq!(n.window_sum_f64(), 0.0);
4749 }
4750
4751 #[test]
4752 fn test_window_sum_f64_correct_after_updates() {
4753 let mut n = znorm(5);
4754 n.update(dec!(10));
4755 n.update(dec!(20));
4756 n.update(dec!(30));
4757 assert!((n.window_sum_f64() - 60.0).abs() < 1e-10);
4758 }
4759
4760 #[test]
4761 fn test_window_sum_f64_rolls_out_old_values() {
4762 let mut n = znorm(2);
4763 n.update(dec!(100));
4764 n.update(dec!(200));
4765 n.update(dec!(300)); assert!((n.window_sum_f64() - 500.0).abs() < 1e-10);
4768 }
4769
4770 #[test]
4773 fn test_zscore_latest_none_when_empty() {
4774 let n = znorm(5);
4775 assert!(n.latest().is_none());
4776 }
4777
4778 #[test]
4779 fn test_zscore_latest_returns_most_recent() {
4780 let mut n = znorm(5);
4781 n.update(dec!(10));
4782 n.update(dec!(20));
4783 assert_eq!(n.latest(), Some(dec!(20)));
4784 }
4785
4786 #[test]
4787 fn test_zscore_latest_updates_on_roll() {
4788 let mut n = znorm(2);
4789 n.update(dec!(1));
4790 n.update(dec!(2));
4791 n.update(dec!(3)); assert_eq!(n.latest(), Some(dec!(3)));
4793 }
4794
4795 #[test]
4797 fn test_window_max_f64_none_on_empty() {
4798 let n = znorm(5);
4799 assert!(n.window_max_f64().is_none());
4800 }
4801
4802 #[test]
4803 fn test_window_max_f64_correct_value() {
4804 let mut n = znorm(5);
4805 for v in [dec!(3), dec!(7), dec!(1), dec!(5)] {
4806 n.update(v);
4807 }
4808 assert!((n.window_max_f64().unwrap() - 7.0).abs() < 1e-10);
4809 }
4810
4811 #[test]
4812 fn test_window_min_f64_none_on_empty() {
4813 let n = znorm(5);
4814 assert!(n.window_min_f64().is_none());
4815 }
4816
4817 #[test]
4818 fn test_window_min_f64_correct_value() {
4819 let mut n = znorm(5);
4820 for v in [dec!(3), dec!(7), dec!(1), dec!(5)] {
4821 n.update(v);
4822 }
4823 assert!((n.window_min_f64().unwrap() - 1.0).abs() < 1e-10);
4824 }
4825
4826 #[test]
4829 fn test_percentile_none_when_empty() {
4830 let n = znorm(5);
4831 assert!(n.percentile(dec!(10)).is_none());
4832 }
4833
4834 #[test]
4835 fn test_percentile_one_when_all_lte_value() {
4836 let mut n = znorm(4);
4837 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
4838 n.update(v);
4839 }
4840 assert!((n.percentile(dec!(4)).unwrap() - 1.0).abs() < 1e-9);
4841 }
4842
4843 #[test]
4844 fn test_percentile_zero_when_all_gt_value() {
4845 let mut n = znorm(4);
4846 for v in [dec!(5), dec!(6), dec!(7), dec!(8)] {
4847 n.update(v);
4848 }
4849 assert_eq!(n.percentile(dec!(4)).unwrap(), 0.0);
4851 }
4852
4853 #[test]
4854 fn test_percentile_half_at_median() {
4855 let mut n = znorm(4);
4856 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
4857 n.update(v);
4858 }
4859 assert!((n.percentile(dec!(2)).unwrap() - 0.5).abs() < 1e-9);
4861 }
4862
4863 #[test]
4866 fn test_zscore_iqr_none_fewer_than_4_observations() {
4867 let mut n = znorm(5);
4868 for v in [dec!(1), dec!(2), dec!(3)] {
4869 n.update(v);
4870 }
4871 assert!(n.interquartile_range().is_none());
4872 }
4873
4874 #[test]
4875 fn test_zscore_iqr_some_with_4_observations() {
4876 let mut n = znorm(4);
4877 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
4878 n.update(v);
4879 }
4880 assert!(n.interquartile_range().is_some());
4881 }
4882
4883 #[test]
4884 fn test_zscore_iqr_zero_when_all_same() {
4885 let mut n = znorm(4);
4886 for _ in 0..4 {
4887 n.update(dec!(5));
4888 }
4889 assert_eq!(n.interquartile_range(), Some(dec!(0)));
4890 }
4891
4892 #[test]
4893 fn test_zscore_iqr_correct_for_sorted_data() {
4894 let mut n = znorm(8);
4896 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6), dec!(7), dec!(8)] {
4897 n.update(v);
4898 }
4899 assert_eq!(n.interquartile_range(), Some(dec!(4)));
4900 }
4901
4902 #[test]
4905 fn test_z_score_of_latest_none_when_empty() {
4906 let n = znorm(5);
4907 assert!(n.z_score_of_latest().is_none());
4908 }
4909
4910 #[test]
4911 fn test_z_score_of_latest_zero_when_all_same() {
4912 let mut n = znorm(4);
4913 for _ in 0..4 {
4914 n.update(dec!(5));
4915 }
4916 assert_eq!(n.z_score_of_latest(), Some(0.0));
4918 }
4919
4920 #[test]
4921 fn test_z_score_of_latest_returns_some_with_variance() {
4922 let mut n = znorm(4);
4923 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
4924 n.update(v);
4925 }
4926 assert!(n.z_score_of_latest().is_some());
4928 }
4929
4930 #[test]
4931 fn test_deviation_from_mean_none_when_empty() {
4932 let n = znorm(5);
4933 assert!(n.deviation_from_mean(dec!(10)).is_none());
4934 }
4935
4936 #[test]
4937 fn test_deviation_from_mean_correct() {
4938 let mut n = znorm(4);
4939 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
4940 n.update(v);
4941 }
4942 let d = n.deviation_from_mean(dec!(4)).unwrap();
4944 assert!((d - 1.5).abs() < 1e-9);
4945 }
4946
4947 #[test]
4950 fn test_add_observation_same_as_update() {
4951 let mut n1 = znorm(4);
4952 let mut n2 = znorm(4);
4953 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
4954 n1.update(v);
4955 n2.add_observation(v);
4956 }
4957 assert_eq!(n1.mean(), n2.mean());
4958 }
4959
4960 #[test]
4961 fn test_add_observation_chainable() {
4962 let mut n = znorm(4);
4963 n.add_observation(dec!(1))
4964 .add_observation(dec!(2))
4965 .add_observation(dec!(3));
4966 assert_eq!(n.len(), 3);
4967 }
4968
4969 #[test]
4972 fn test_variance_f64_none_when_single_observation() {
4973 let mut n = znorm(4);
4974 n.update(dec!(5));
4975 assert!(n.variance_f64().is_none());
4976 }
4977
4978 #[test]
4979 fn test_variance_f64_zero_when_all_same() {
4980 let mut n = znorm(4);
4981 for _ in 0..4 { n.update(dec!(5)); }
4982 assert_eq!(n.variance_f64(), Some(0.0));
4983 }
4984
4985 #[test]
4986 fn test_variance_f64_positive_with_spread() {
4987 let mut n = znorm(4);
4988 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
4989 assert!(n.variance_f64().unwrap() > 0.0);
4990 }
4991
4992 #[test]
4995 fn test_ema_of_z_scores_none_when_single_value() {
4996 let mut n = znorm(4);
4997 n.update(dec!(5));
4998 assert!(n.ema_of_z_scores(0.5).is_none());
4999 }
5000
5001 #[test]
5002 fn test_ema_of_z_scores_returns_some_with_variance() {
5003 let mut n = znorm(4);
5004 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
5005 n.update(v);
5006 }
5007 let ema = n.ema_of_z_scores(0.3);
5008 assert!(ema.is_some());
5009 }
5010
5011 #[test]
5012 fn test_ema_of_z_scores_zero_when_all_same() {
5013 let mut n = znorm(4);
5014 for _ in 0..4 { n.update(dec!(5)); }
5015 assert_eq!(n.ema_of_z_scores(0.5), Some(0.0));
5017 }
5018
5019 #[test]
5022 fn test_std_dev_f64_none_when_single_observation() {
5023 let mut n = znorm(4);
5024 n.update(dec!(5));
5025 assert!(n.std_dev_f64().is_none());
5026 }
5027
5028 #[test]
5029 fn test_std_dev_f64_zero_when_all_same() {
5030 let mut n = znorm(4);
5031 for _ in 0..4 { n.update(dec!(5)); }
5032 assert_eq!(n.std_dev_f64(), Some(0.0));
5033 }
5034
5035 #[test]
5036 fn test_std_dev_f64_equals_sqrt_of_variance() {
5037 let mut n = znorm(4);
5038 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5039 let var = n.variance_f64().unwrap();
5040 let std = n.std_dev_f64().unwrap();
5041 assert!((std - var.sqrt()).abs() < 1e-12);
5042 }
5043
5044 #[test]
5047 fn test_rolling_mean_change_none_when_one_observation() {
5048 let mut n = znorm(4);
5049 n.update(dec!(5));
5050 assert!(n.rolling_mean_change().is_none());
5051 }
5052
5053 #[test]
5054 fn test_rolling_mean_change_positive_when_rising() {
5055 let mut n = znorm(4);
5056 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5057 let change = n.rolling_mean_change().unwrap();
5059 assert!((change - 2.0).abs() < 1e-9);
5060 }
5061
5062 #[test]
5063 fn test_rolling_mean_change_negative_when_falling() {
5064 let mut n = znorm(4);
5065 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
5066 let change = n.rolling_mean_change().unwrap();
5067 assert!(change < 0.0);
5068 }
5069
5070 #[test]
5071 fn test_rolling_mean_change_zero_when_flat() {
5072 let mut n = znorm(4);
5073 for _ in 0..4 { n.update(dec!(7)); }
5074 let change = n.rolling_mean_change().unwrap();
5075 assert!(change.abs() < 1e-9);
5076 }
5077
5078 #[test]
5081 fn test_window_span_f64_none_when_empty() {
5082 let n = znorm(4);
5083 assert!(n.window_span_f64().is_none());
5084 }
5085
5086 #[test]
5087 fn test_window_span_f64_zero_when_all_same() {
5088 let mut n = znorm(4);
5089 for _ in 0..4 { n.update(dec!(5)); }
5090 assert_eq!(n.window_span_f64(), Some(0.0));
5091 }
5092
5093 #[test]
5094 fn test_window_span_f64_correct_value() {
5095 let mut n = znorm(4);
5096 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
5097 assert!((n.window_span_f64().unwrap() - 30.0).abs() < 1e-9);
5099 }
5100
5101 #[test]
5104 fn test_count_positive_z_scores_zero_when_empty() {
5105 let n = znorm(4);
5106 assert_eq!(n.count_positive_z_scores(), 0);
5107 }
5108
5109 #[test]
5110 fn test_count_positive_z_scores_zero_when_all_same() {
5111 let mut n = znorm(4);
5112 for _ in 0..4 { n.update(dec!(5)); }
5113 assert_eq!(n.count_positive_z_scores(), 0);
5114 }
5115
5116 #[test]
5117 fn test_count_positive_z_scores_half_above_mean() {
5118 let mut n = znorm(4);
5119 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5120 assert_eq!(n.count_positive_z_scores(), 2);
5122 }
5123
5124 #[test]
5127 fn test_above_threshold_count_zero_when_empty() {
5128 let n = znorm(4);
5129 assert_eq!(n.above_threshold_count(1.0), 0);
5130 }
5131
5132 #[test]
5133 fn test_above_threshold_count_zero_when_all_same() {
5134 let mut n = znorm(4);
5135 for _ in 0..4 { n.update(dec!(5)); }
5136 assert_eq!(n.above_threshold_count(0.5), 0);
5137 }
5138
5139 #[test]
5140 fn test_above_threshold_count_correct_with_extremes() {
5141 let mut n = znorm(6);
5142 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(100)] { n.update(v); }
5143 assert!(n.above_threshold_count(1.0) >= 1);
5145 }
5146}
5147
5148#[cfg(test)]
5149mod minmax_extra_tests {
5150 use super::*;
5151 use rust_decimal_macros::dec;
5152
5153 fn norm(w: usize) -> MinMaxNormalizer {
5154 MinMaxNormalizer::new(w).unwrap()
5155 }
5156
5157 #[test]
5160 fn test_fraction_above_mid_none_when_empty() {
5161 let mut n = norm(4);
5162 assert!(n.fraction_above_mid().is_none());
5163 }
5164
5165 #[test]
5166 fn test_fraction_above_mid_zero_when_all_same() {
5167 let mut n = norm(4);
5168 for _ in 0..4 { n.update(dec!(5)); }
5169 assert_eq!(n.fraction_above_mid(), Some(0.0));
5170 }
5171
5172 #[test]
5173 fn test_fraction_above_mid_half_when_symmetric() {
5174 let mut n = norm(4);
5175 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5176 let f = n.fraction_above_mid().unwrap();
5178 assert!((f - 0.5).abs() < 1e-10);
5179 }
5180}
5181
5182#[cfg(test)]
5183mod zscore_stability_tests {
5184 use super::*;
5185 use rust_decimal_macros::dec;
5186
5187 fn znorm(w: usize) -> ZScoreNormalizer {
5188 ZScoreNormalizer::new(w).unwrap()
5189 }
5190
5191 #[test]
5194 fn test_is_mean_stable_false_when_window_too_small() {
5195 let n = znorm(4);
5196 assert!(!n.is_mean_stable(1.0));
5197 }
5198
5199 #[test]
5200 fn test_is_mean_stable_true_when_flat() {
5201 let mut n = znorm(4);
5202 for _ in 0..4 { n.update(dec!(5)); }
5203 assert!(n.is_mean_stable(0.001));
5204 }
5205
5206 #[test]
5207 fn test_is_mean_stable_false_when_trending() {
5208 let mut n = znorm(4);
5209 for v in [dec!(1), dec!(2), dec!(10), dec!(20)] { n.update(v); }
5210 assert!(!n.is_mean_stable(0.5));
5211 }
5212
5213 #[test]
5216 fn test_zscore_count_above_zero_for_empty_window() {
5217 assert_eq!(znorm(4).count_above(dec!(10)), 0);
5218 }
5219
5220 #[test]
5221 fn test_zscore_count_above_correct() {
5222 let mut n = znorm(5);
5223 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5224 assert_eq!(n.count_above(dec!(3)), 2);
5226 }
5227
5228 #[test]
5229 fn test_zscore_count_below_correct() {
5230 let mut n = znorm(5);
5231 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5232 assert_eq!(n.count_below(dec!(3)), 2);
5234 }
5235
5236 #[test]
5237 fn test_zscore_count_above_excludes_at_threshold() {
5238 let mut n = znorm(3);
5239 for v in [dec!(5), dec!(5), dec!(5)] { n.update(v); }
5240 assert_eq!(n.count_above(dec!(5)), 0);
5241 assert_eq!(n.count_below(dec!(5)), 0);
5242 }
5243
5244 #[test]
5247 fn test_zscore_skewness_none_for_fewer_than_3_obs() {
5248 let mut n = znorm(5);
5249 n.update(dec!(10));
5250 n.update(dec!(20));
5251 assert!(n.skewness().is_none());
5252 }
5253
5254 #[test]
5255 fn test_zscore_skewness_none_for_all_identical() {
5256 let mut n = znorm(4);
5257 for _ in 0..4 { n.update(dec!(5)); }
5258 assert!(n.skewness().is_none());
5259 }
5260
5261 #[test]
5262 fn test_zscore_skewness_near_zero_for_symmetric_distribution() {
5263 let mut n = znorm(5);
5264 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5265 let skew = n.skewness().unwrap();
5266 assert!(skew.abs() < 0.01, "symmetric distribution should have ~0 skewness, got {skew}");
5267 }
5268
5269 #[test]
5272 fn test_zscore_percentile_value_none_for_empty_window() {
5273 assert!(znorm(4).percentile_value(0.5).is_none());
5274 }
5275
5276 #[test]
5277 fn test_zscore_percentile_value_min_at_zero() {
5278 let mut n = znorm(5);
5279 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
5280 assert_eq!(n.percentile_value(0.0), Some(dec!(10)));
5281 }
5282
5283 #[test]
5284 fn test_zscore_percentile_value_max_at_one() {
5285 let mut n = znorm(5);
5286 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
5287 assert_eq!(n.percentile_value(1.0), Some(dec!(50)));
5288 }
5289
5290 #[test]
5293 fn test_zscore_ewma_none_for_empty_window() {
5294 assert!(znorm(4).ewma(0.5).is_none());
5295 }
5296
5297 #[test]
5298 fn test_zscore_ewma_equals_value_for_single_obs() {
5299 let mut n = znorm(4);
5300 n.update(dec!(42));
5301 assert!((n.ewma(0.5).unwrap() - 42.0).abs() < 1e-10);
5302 }
5303
5304 #[test]
5305 fn test_zscore_ewma_weights_recent_more_with_high_alpha() {
5306 let mut n = znorm(4);
5308 for v in [dec!(10), dec!(20), dec!(30), dec!(100)] { n.update(v); }
5309 let ewma = n.ewma(1.0).unwrap();
5310 assert!((ewma - 100.0).abs() < 1e-10);
5311 }
5312
5313 #[test]
5314 fn test_zscore_fraction_above_mid_none_for_empty_window() {
5315 let n = znorm(3);
5316 assert!(n.fraction_above_mid().is_none());
5317 }
5318
5319 #[test]
5320 fn test_zscore_fraction_above_mid_none_when_all_equal() {
5321 let mut n = znorm(3);
5322 for _ in 0..3 { n.update(dec!(5)); }
5323 assert!(n.fraction_above_mid().is_none());
5324 }
5325
5326 #[test]
5327 fn test_zscore_fraction_above_mid_half_above() {
5328 let mut n = znorm(4);
5329 for v in [dec!(0), dec!(10), dec!(6), dec!(4)] { n.update(v); }
5330 let frac = n.fraction_above_mid().unwrap();
5332 assert!((frac - 0.5).abs() < 1e-9);
5333 }
5334
5335 #[test]
5336 fn test_zscore_normalized_range_none_for_empty_window() {
5337 let n = znorm(3);
5338 assert!(n.normalized_range().is_none());
5339 }
5340
5341 #[test]
5342 fn test_zscore_normalized_range_zero_for_uniform_window() {
5343 let mut n = znorm(3);
5344 for _ in 0..3 { n.update(dec!(10)); }
5345 assert_eq!(n.normalized_range(), Some(0.0));
5346 }
5347
5348 #[test]
5349 fn test_zscore_normalized_range_positive_for_varying_window() {
5350 let mut n = znorm(3);
5351 for v in [dec!(8), dec!(10), dec!(12)] { n.update(v); }
5352 let nr = n.normalized_range().unwrap();
5354 assert!((nr - 0.4).abs() < 1e-9);
5355 }
5356
5357 #[test]
5360 fn test_zscore_midpoint_none_for_empty_window() {
5361 assert!(znorm(3).midpoint().is_none());
5362 }
5363
5364 #[test]
5365 fn test_zscore_midpoint_correct_for_known_range() {
5366 let mut n = znorm(4);
5367 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
5368 assert_eq!(n.midpoint(), Some(dec!(25)));
5370 }
5371
5372 #[test]
5375 fn test_zscore_clamp_returns_value_unchanged_on_empty_window() {
5376 let n = znorm(3);
5377 assert_eq!(n.clamp_to_window(dec!(50)), dec!(50));
5378 }
5379
5380 #[test]
5381 fn test_zscore_clamp_clamps_to_min() {
5382 let mut n = znorm(3);
5383 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
5384 assert_eq!(n.clamp_to_window(dec!(-5)), dec!(10));
5385 }
5386
5387 #[test]
5388 fn test_zscore_clamp_clamps_to_max() {
5389 let mut n = znorm(3);
5390 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
5391 assert_eq!(n.clamp_to_window(dec!(100)), dec!(30));
5392 }
5393
5394 #[test]
5395 fn test_zscore_clamp_passes_through_in_range_value() {
5396 let mut n = znorm(3);
5397 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
5398 assert_eq!(n.clamp_to_window(dec!(15)), dec!(15));
5399 }
5400
5401 #[test]
5404 fn test_zscore_min_max_none_for_empty_window() {
5405 assert!(znorm(3).min_max().is_none());
5406 }
5407
5408 #[test]
5409 fn test_zscore_min_max_returns_correct_pair() {
5410 let mut n = znorm(4);
5411 for v in [dec!(5), dec!(15), dec!(10), dec!(20)] { n.update(v); }
5412 assert_eq!(n.min_max(), Some((dec!(5), dec!(20))));
5413 }
5414
5415 #[test]
5416 fn test_zscore_min_max_single_value() {
5417 let mut n = znorm(3);
5418 n.update(dec!(42));
5419 assert_eq!(n.min_max(), Some((dec!(42), dec!(42))));
5420 }
5421
5422 #[test]
5425 fn test_zscore_values_empty_for_empty_window() {
5426 assert!(znorm(3).values().is_empty());
5427 }
5428
5429 #[test]
5430 fn test_zscore_values_preserves_insertion_order() {
5431 let mut n = znorm(4);
5432 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
5433 assert_eq!(n.values(), vec![dec!(10), dec!(20), dec!(30), dec!(40)]);
5434 }
5435
5436 #[test]
5439 fn test_zscore_above_zero_fraction_none_for_empty_window() {
5440 assert!(znorm(3).above_zero_fraction().is_none());
5441 }
5442
5443 #[test]
5444 fn test_zscore_above_zero_fraction_zero_for_all_negative() {
5445 let mut n = znorm(3);
5446 for v in [dec!(-3), dec!(-2), dec!(-1)] { n.update(v); }
5447 assert_eq!(n.above_zero_fraction(), Some(0.0));
5448 }
5449
5450 #[test]
5451 fn test_zscore_above_zero_fraction_one_for_all_positive() {
5452 let mut n = znorm(3);
5453 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
5454 assert_eq!(n.above_zero_fraction(), Some(1.0));
5455 }
5456
5457 #[test]
5458 fn test_zscore_above_zero_fraction_half_for_mixed() {
5459 let mut n = znorm(4);
5460 for v in [dec!(-2), dec!(-1), dec!(1), dec!(2)] { n.update(v); }
5461 let frac = n.above_zero_fraction().unwrap();
5462 assert!((frac - 0.5).abs() < 1e-9);
5463 }
5464
5465 #[test]
5468 fn test_zscore_opt_none_for_empty_window() {
5469 assert!(znorm(3).z_score_opt(dec!(10)).is_none());
5470 }
5471
5472 #[test]
5473 fn test_zscore_opt_matches_normalize_for_populated_window() {
5474 let mut n = znorm(4);
5475 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
5476 let z_opt = n.z_score_opt(dec!(25)).unwrap();
5477 let z_norm = n.normalize(dec!(25)).unwrap();
5478 assert!((z_opt - z_norm).abs() < 1e-12);
5479 }
5480
5481 #[test]
5484 fn test_zscore_is_stable_false_for_empty_window() {
5485 assert!(!znorm(3).is_stable(2.0));
5486 }
5487
5488 #[test]
5489 fn test_zscore_is_stable_true_for_near_mean_value() {
5490 let mut n = znorm(5);
5491 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(30)] { n.update(v); }
5492 assert!(n.is_stable(2.0));
5494 }
5495
5496 #[test]
5497 fn test_zscore_is_stable_false_for_extreme_value() {
5498 let mut n = znorm(5);
5499 for v in [dec!(10), dec!(10), dec!(10), dec!(10), dec!(100)] { n.update(v); }
5500 assert!(!n.is_stable(1.0));
5502 }
5503
5504 #[test]
5507 fn test_zscore_window_values_above_via_znorm_empty() {
5508 assert!(znorm(3).window_values_above(dec!(5)).is_empty());
5509 }
5510
5511 #[test]
5512 fn test_zscore_window_values_above_via_znorm_filters() {
5513 let mut n = znorm(5);
5514 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
5515 let above = n.window_values_above(dec!(5));
5516 assert_eq!(above.len(), 2);
5517 assert!(above.contains(&dec!(7)));
5518 assert!(above.contains(&dec!(9)));
5519 }
5520
5521 #[test]
5522 fn test_zscore_window_values_below_via_znorm_empty() {
5523 assert!(znorm(3).window_values_below(dec!(5)).is_empty());
5524 }
5525
5526 #[test]
5527 fn test_zscore_window_values_below_via_znorm_filters() {
5528 let mut n = znorm(5);
5529 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
5530 let below = n.window_values_below(dec!(5));
5531 assert_eq!(below.len(), 2);
5532 assert!(below.contains(&dec!(1)));
5533 assert!(below.contains(&dec!(3)));
5534 }
5535
5536 #[test]
5539 fn test_zscore_fraction_above_none_for_empty_window() {
5540 assert!(znorm(3).fraction_above(dec!(5)).is_none());
5541 }
5542
5543 #[test]
5544 fn test_zscore_fraction_above_correct() {
5545 let mut n = znorm(5);
5546 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5547 let frac = n.fraction_above(dec!(3)).unwrap();
5549 assert!((frac - 0.4).abs() < 1e-9);
5550 }
5551
5552 #[test]
5553 fn test_zscore_fraction_below_none_for_empty_window() {
5554 assert!(znorm(3).fraction_below(dec!(5)).is_none());
5555 }
5556
5557 #[test]
5558 fn test_zscore_fraction_below_correct() {
5559 let mut n = znorm(5);
5560 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5561 let frac = n.fraction_below(dec!(3)).unwrap();
5563 assert!((frac - 0.4).abs() < 1e-9);
5564 }
5565
5566 #[test]
5569 fn test_zscore_window_values_above_empty_window() {
5570 assert!(znorm(3).window_values_above(dec!(0)).is_empty());
5571 }
5572
5573 #[test]
5574 fn test_zscore_window_values_above_filters_correctly() {
5575 let mut n = znorm(5);
5576 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
5577 let above = n.window_values_above(dec!(5));
5578 assert_eq!(above.len(), 2);
5579 assert!(above.contains(&dec!(7)));
5580 assert!(above.contains(&dec!(9)));
5581 }
5582
5583 #[test]
5584 fn test_zscore_window_values_below_empty_window() {
5585 assert!(znorm(3).window_values_below(dec!(0)).is_empty());
5586 }
5587
5588 #[test]
5589 fn test_zscore_window_values_below_filters_correctly() {
5590 let mut n = znorm(5);
5591 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
5592 let below = n.window_values_below(dec!(5));
5593 assert_eq!(below.len(), 2);
5594 assert!(below.contains(&dec!(1)));
5595 assert!(below.contains(&dec!(3)));
5596 }
5597
5598 #[test]
5601 fn test_zscore_percentile_rank_none_for_empty_window() {
5602 assert!(znorm(3).percentile_rank(dec!(5)).is_none());
5603 }
5604
5605 #[test]
5606 fn test_zscore_percentile_rank_correct() {
5607 let mut n = znorm(5);
5608 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5609 let rank = n.percentile_rank(dec!(3)).unwrap();
5611 assert!((rank - 0.6).abs() < 1e-9);
5612 }
5613
5614 #[test]
5617 fn test_zscore_count_equal_zero_for_no_match() {
5618 let mut n = znorm(3);
5619 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
5620 assert_eq!(n.count_equal(dec!(99)), 0);
5621 }
5622
5623 #[test]
5624 fn test_zscore_count_equal_counts_duplicates() {
5625 let mut n = znorm(5);
5626 for v in [dec!(5), dec!(5), dec!(3), dec!(5), dec!(2)] { n.update(v); }
5627 assert_eq!(n.count_equal(dec!(5)), 3);
5628 }
5629
5630 #[test]
5633 fn test_zscore_median_none_for_empty_window() {
5634 assert!(znorm(3).median().is_none());
5635 }
5636
5637 #[test]
5638 fn test_zscore_median_correct_for_odd_count() {
5639 let mut n = znorm(5);
5640 for v in [dec!(3), dec!(1), dec!(5), dec!(4), dec!(2)] { n.update(v); }
5641 assert_eq!(n.median(), Some(dec!(3)));
5643 }
5644
5645 #[test]
5648 fn test_zscore_rolling_range_none_for_empty() {
5649 assert!(znorm(3).rolling_range().is_none());
5650 }
5651
5652 #[test]
5653 fn test_zscore_rolling_range_correct() {
5654 let mut n = znorm(5);
5655 for v in [dec!(10), dec!(50), dec!(30), dec!(20), dec!(40)] { n.update(v); }
5656 assert_eq!(n.rolling_range(), Some(dec!(40)));
5657 }
5658
5659 #[test]
5662 fn test_zscore_skewness_none_for_fewer_than_3() {
5663 let mut n = znorm(5);
5664 n.update(dec!(1)); n.update(dec!(2));
5665 assert!(n.skewness().is_none());
5666 }
5667
5668 #[test]
5669 fn test_zscore_skewness_near_zero_for_symmetric_data() {
5670 let mut n = znorm(5);
5671 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5672 let s = n.skewness().unwrap();
5673 assert!(s.abs() < 0.5);
5674 }
5675
5676 #[test]
5679 fn test_zscore_kurtosis_none_for_fewer_than_4() {
5680 let mut n = znorm(5);
5681 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
5682 assert!(n.kurtosis().is_none());
5683 }
5684
5685 #[test]
5686 fn test_zscore_kurtosis_returns_f64_for_populated_window() {
5687 let mut n = znorm(5);
5688 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5689 assert!(n.kurtosis().is_some());
5690 }
5691
5692 #[test]
5695 fn test_zscore_autocorrelation_none_for_single_value() {
5696 let mut n = znorm(3);
5697 n.update(dec!(1));
5698 assert!(n.autocorrelation_lag1().is_none());
5699 }
5700
5701 #[test]
5702 fn test_zscore_autocorrelation_positive_for_trending_data() {
5703 let mut n = znorm(5);
5704 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5705 let ac = n.autocorrelation_lag1().unwrap();
5706 assert!(ac > 0.0);
5707 }
5708
5709 #[test]
5712 fn test_zscore_trend_consistency_none_for_single_value() {
5713 let mut n = znorm(3);
5714 n.update(dec!(1));
5715 assert!(n.trend_consistency().is_none());
5716 }
5717
5718 #[test]
5719 fn test_zscore_trend_consistency_one_for_strictly_rising() {
5720 let mut n = znorm(5);
5721 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5722 let tc = n.trend_consistency().unwrap();
5723 assert!((tc - 1.0).abs() < 1e-9);
5724 }
5725
5726 #[test]
5727 fn test_zscore_trend_consistency_zero_for_strictly_falling() {
5728 let mut n = znorm(5);
5729 for v in [dec!(5), dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
5730 let tc = n.trend_consistency().unwrap();
5731 assert!((tc - 0.0).abs() < 1e-9);
5732 }
5733
5734 #[test]
5737 fn test_zscore_cov_none_for_empty_window() {
5738 assert!(znorm(3).coefficient_of_variation().is_none());
5739 }
5740
5741 #[test]
5742 fn test_zscore_cov_positive_for_varied_data() {
5743 let mut n = znorm(5);
5744 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
5745 let cov = n.coefficient_of_variation().unwrap();
5746 assert!(cov > 0.0);
5747 }
5748
5749 #[test]
5752 fn test_zscore_mad_none_for_empty() {
5753 assert!(znorm(3).mean_absolute_deviation().is_none());
5754 }
5755
5756 #[test]
5757 fn test_zscore_mad_zero_for_identical_values() {
5758 let mut n = znorm(3);
5759 for v in [dec!(5), dec!(5), dec!(5)] { n.update(v); }
5760 let mad = n.mean_absolute_deviation().unwrap();
5761 assert!((mad - 0.0).abs() < 1e-9);
5762 }
5763
5764 #[test]
5765 fn test_zscore_mad_positive_for_varied_data() {
5766 let mut n = znorm(4);
5767 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5768 let mad = n.mean_absolute_deviation().unwrap();
5769 assert!(mad > 0.0);
5770 }
5771
5772 #[test]
5775 fn test_zscore_percentile_of_latest_none_for_empty() {
5776 assert!(znorm(3).percentile_of_latest().is_none());
5777 }
5778
5779 #[test]
5780 fn test_zscore_percentile_of_latest_returns_some_after_update() {
5781 let mut n = znorm(4);
5782 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5783 assert!(n.percentile_of_latest().is_some());
5784 }
5785
5786 #[test]
5787 fn test_zscore_percentile_of_latest_max_has_high_rank() {
5788 let mut n = znorm(5);
5789 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5790 let rank = n.percentile_of_latest().unwrap();
5791 assert!(rank >= 0.9, "max value should have rank near 1.0, got {}", rank);
5792 }
5793
5794 #[test]
5797 fn test_zscore_tail_ratio_none_for_empty() {
5798 assert!(znorm(4).tail_ratio().is_none());
5799 }
5800
5801 #[test]
5802 fn test_zscore_tail_ratio_one_for_identical_values() {
5803 let mut n = znorm(4);
5804 for _ in 0..4 { n.update(dec!(7)); }
5805 let r = n.tail_ratio().unwrap();
5806 assert!((r - 1.0).abs() < 1e-9, "expected 1.0, got {}", r);
5807 }
5808
5809 #[test]
5810 fn test_zscore_tail_ratio_above_one_with_outlier() {
5811 let mut n = znorm(5);
5812 for v in [dec!(1), dec!(1), dec!(1), dec!(1), dec!(10)] { n.update(v); }
5813 let r = n.tail_ratio().unwrap();
5814 assert!(r > 1.0, "outlier should push ratio above 1.0, got {}", r);
5815 }
5816
5817 #[test]
5820 fn test_zscore_z_score_of_min_none_for_empty() {
5821 assert!(znorm(4).z_score_of_min().is_none());
5822 }
5823
5824 #[test]
5825 fn test_zscore_z_score_of_max_none_for_empty() {
5826 assert!(znorm(4).z_score_of_max().is_none());
5827 }
5828
5829 #[test]
5830 fn test_zscore_z_score_of_min_negative_for_varied_window() {
5831 let mut n = znorm(5);
5832 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5833 let z = n.z_score_of_min().unwrap();
5834 assert!(z < 0.0, "z-score of min should be negative, got {}", z);
5835 }
5836
5837 #[test]
5838 fn test_zscore_z_score_of_max_positive_for_varied_window() {
5839 let mut n = znorm(5);
5840 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5841 let z = n.z_score_of_max().unwrap();
5842 assert!(z > 0.0, "z-score of max should be positive, got {}", z);
5843 }
5844
5845 #[test]
5848 fn test_zscore_window_entropy_none_for_empty() {
5849 assert!(znorm(4).window_entropy().is_none());
5850 }
5851
5852 #[test]
5853 fn test_zscore_window_entropy_zero_for_identical_values() {
5854 let mut n = znorm(3);
5855 for _ in 0..3 { n.update(dec!(5)); }
5856 let e = n.window_entropy().unwrap();
5857 assert!((e - 0.0).abs() < 1e-9, "identical values should have zero entropy, got {}", e);
5858 }
5859
5860 #[test]
5861 fn test_zscore_window_entropy_positive_for_varied_values() {
5862 let mut n = znorm(4);
5863 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5864 let e = n.window_entropy().unwrap();
5865 assert!(e > 0.0, "varied values should have positive entropy, got {}", e);
5866 }
5867
5868 #[test]
5871 fn test_zscore_normalized_std_dev_none_for_empty() {
5872 assert!(znorm(4).normalized_std_dev().is_none());
5873 }
5874
5875 #[test]
5876 fn test_zscore_normalized_std_dev_positive_for_varied_values() {
5877 let mut n = znorm(4);
5878 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5879 let r = n.normalized_std_dev().unwrap();
5880 assert!(r > 0.0, "expected positive normalized std dev, got {}", r);
5881 }
5882
5883 #[test]
5886 fn test_zscore_value_above_mean_count_none_for_empty() {
5887 assert!(znorm(4).value_above_mean_count().is_none());
5888 }
5889
5890 #[test]
5891 fn test_zscore_value_above_mean_count_correct() {
5892 let mut n = znorm(4);
5893 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5894 assert_eq!(n.value_above_mean_count().unwrap(), 2);
5896 }
5897
5898 #[test]
5901 fn test_zscore_consecutive_above_mean_none_for_empty() {
5902 assert!(znorm(4).consecutive_above_mean().is_none());
5903 }
5904
5905 #[test]
5906 fn test_zscore_consecutive_above_mean_correct() {
5907 let mut n = znorm(4);
5908 for v in [dec!(1), dec!(5), dec!(6), dec!(7)] { n.update(v); }
5909 assert_eq!(n.consecutive_above_mean().unwrap(), 3);
5911 }
5912
5913 #[test]
5916 fn test_zscore_above_threshold_fraction_none_for_empty() {
5917 assert!(znorm(4).above_threshold_fraction(dec!(5)).is_none());
5918 }
5919
5920 #[test]
5921 fn test_zscore_above_threshold_fraction_correct() {
5922 let mut n = znorm(4);
5923 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5924 let f = n.above_threshold_fraction(dec!(2)).unwrap();
5925 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
5926 }
5927
5928 #[test]
5929 fn test_zscore_below_threshold_fraction_none_for_empty() {
5930 assert!(znorm(4).below_threshold_fraction(dec!(5)).is_none());
5931 }
5932
5933 #[test]
5934 fn test_zscore_below_threshold_fraction_correct() {
5935 let mut n = znorm(4);
5936 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
5937 let f = n.below_threshold_fraction(dec!(3)).unwrap();
5938 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
5939 }
5940
5941 #[test]
5944 fn test_zscore_lag_k_autocorrelation_none_for_zero_k() {
5945 let mut n = znorm(5);
5946 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
5947 assert!(n.lag_k_autocorrelation(0).is_none());
5948 }
5949
5950 #[test]
5951 fn test_zscore_lag_k_autocorrelation_none_when_k_gte_len() {
5952 let mut n = znorm(3);
5953 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
5954 assert!(n.lag_k_autocorrelation(3).is_none());
5955 }
5956
5957 #[test]
5958 fn test_zscore_lag_k_autocorrelation_positive_for_trend() {
5959 let mut n = znorm(6);
5960 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6)] { n.update(v); }
5961 let ac = n.lag_k_autocorrelation(1).unwrap();
5962 assert!(ac > 0.0, "trending series should have positive AC, got {}", ac);
5963 }
5964
5965 #[test]
5968 fn test_zscore_half_life_estimate_none_for_fewer_than_3() {
5969 let mut n = znorm(3);
5970 n.update(dec!(1)); n.update(dec!(2));
5971 assert!(n.half_life_estimate().is_none());
5972 }
5973
5974 #[test]
5975 fn test_zscore_half_life_no_panic_for_alternating() {
5976 let mut n = znorm(6);
5977 for v in [dec!(10), dec!(5), dec!(10), dec!(5), dec!(10), dec!(5)] { n.update(v); }
5978 let _ = n.half_life_estimate();
5979 }
5980
5981 #[test]
5984 fn test_zscore_geometric_mean_none_for_empty() {
5985 assert!(znorm(4).geometric_mean().is_none());
5986 }
5987
5988 #[test]
5989 fn test_zscore_geometric_mean_correct_for_powers_of_2() {
5990 let mut n = znorm(4);
5991 for v in [dec!(1), dec!(2), dec!(4), dec!(8)] { n.update(v); }
5992 let gm = n.geometric_mean().unwrap();
5993 assert!((gm - 64.0f64.powf(0.25)).abs() < 1e-6, "got {}", gm);
5994 }
5995
5996 #[test]
5999 fn test_zscore_harmonic_mean_none_for_empty() {
6000 assert!(znorm(4).harmonic_mean().is_none());
6001 }
6002
6003 #[test]
6004 fn test_zscore_harmonic_mean_positive_for_positive_values() {
6005 let mut n = znorm(4);
6006 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6007 let hm = n.harmonic_mean().unwrap();
6008 assert!(hm > 0.0 && hm < 4.0, "HM should be in (0, max), got {}", hm);
6009 }
6010
6011 #[test]
6014 fn test_zscore_range_normalized_value_none_for_empty() {
6015 assert!(znorm(4).range_normalized_value(dec!(5)).is_none());
6016 }
6017
6018 #[test]
6019 fn test_zscore_range_normalized_value_in_range() {
6020 let mut n = znorm(4);
6021 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6022 let r = n.range_normalized_value(dec!(2)).unwrap();
6023 assert!(r >= 0.0 && r <= 1.0, "expected [0,1], got {}", r);
6024 }
6025
6026 #[test]
6029 fn test_zscore_distance_from_median_none_for_empty() {
6030 assert!(znorm(4).distance_from_median(dec!(5)).is_none());
6031 }
6032
6033 #[test]
6034 fn test_zscore_distance_from_median_zero_at_median() {
6035 let mut n = znorm(5);
6036 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6037 let d = n.distance_from_median(dec!(3)).unwrap();
6038 assert!((d - 0.0).abs() < 1e-9, "distance from median=3 should be 0, got {}", d);
6039 }
6040
6041 #[test]
6042 fn test_zscore_momentum_none_for_single_value() {
6043 let mut n = znorm(5);
6044 n.update(dec!(10));
6045 assert!(n.momentum().is_none());
6046 }
6047
6048 #[test]
6049 fn test_zscore_momentum_positive_for_rising_window() {
6050 let mut n = znorm(3);
6051 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
6052 let m = n.momentum().unwrap();
6053 assert!(m > 0.0, "rising window → positive momentum, got {}", m);
6054 }
6055
6056 #[test]
6057 fn test_zscore_value_rank_none_for_empty() {
6058 assert!(znorm(4).value_rank(dec!(5)).is_none());
6059 }
6060
6061 #[test]
6062 fn test_zscore_value_rank_extremes() {
6063 let mut n = znorm(4);
6064 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6065 let low = n.value_rank(dec!(0)).unwrap();
6066 assert!((low - 0.0).abs() < 1e-9, "got {}", low);
6067 let high = n.value_rank(dec!(5)).unwrap();
6068 assert!((high - 1.0).abs() < 1e-9, "got {}", high);
6069 }
6070
6071 #[test]
6072 fn test_zscore_coeff_of_variation_none_for_single_value() {
6073 let mut n = znorm(5);
6074 n.update(dec!(10));
6075 assert!(n.coeff_of_variation().is_none());
6076 }
6077
6078 #[test]
6079 fn test_zscore_coeff_of_variation_positive_for_spread() {
6080 let mut n = znorm(4);
6081 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
6082 let cv = n.coeff_of_variation().unwrap();
6083 assert!(cv > 0.0, "expected positive CV, got {}", cv);
6084 }
6085
6086 #[test]
6087 fn test_zscore_quantile_range_none_for_empty() {
6088 assert!(znorm(4).quantile_range().is_none());
6089 }
6090
6091 #[test]
6092 fn test_zscore_quantile_range_non_negative() {
6093 let mut n = znorm(5);
6094 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6095 let iqr = n.quantile_range().unwrap();
6096 assert!(iqr >= 0.0, "IQR should be non-negative, got {}", iqr);
6097 }
6098
6099 #[test]
6102 fn test_zscore_upper_quartile_none_for_empty() {
6103 assert!(znorm(4).upper_quartile().is_none());
6104 }
6105
6106 #[test]
6107 fn test_zscore_lower_quartile_none_for_empty() {
6108 assert!(znorm(4).lower_quartile().is_none());
6109 }
6110
6111 #[test]
6112 fn test_zscore_upper_ge_lower_quartile() {
6113 let mut n = znorm(8);
6114 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50), dec!(60), dec!(70), dec!(80)] {
6115 n.update(v);
6116 }
6117 let q3 = n.upper_quartile().unwrap();
6118 let q1 = n.lower_quartile().unwrap();
6119 assert!(q3 >= q1, "Q3 ({}) should be >= Q1 ({})", q3, q1);
6120 }
6121
6122 #[test]
6125 fn test_zscore_sign_change_rate_none_for_fewer_than_3() {
6126 let mut n = znorm(4);
6127 n.update(dec!(1));
6128 n.update(dec!(2));
6129 assert!(n.sign_change_rate().is_none());
6130 }
6131
6132 #[test]
6133 fn test_zscore_sign_change_rate_one_for_zigzag() {
6134 let mut n = znorm(5);
6135 for v in [dec!(1), dec!(3), dec!(1), dec!(3), dec!(1)] { n.update(v); }
6136 let r = n.sign_change_rate().unwrap();
6137 assert!((r - 1.0).abs() < 1e-9, "zigzag should give 1.0, got {}", r);
6138 }
6139
6140 #[test]
6141 fn test_zscore_sign_change_rate_zero_for_monotone() {
6142 let mut n = znorm(5);
6143 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6144 let r = n.sign_change_rate().unwrap();
6145 assert!((r - 0.0).abs() < 1e-9, "monotone should give 0.0, got {}", r);
6146 }
6147}