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 pub fn variance_ratio(&self) -> Option<f64> {
1307 use rust_decimal::prelude::ToPrimitive;
1308 let n = self.window.len();
1309 if n < 4 {
1310 return None;
1311 }
1312 let mid = n / 2;
1313 let first: Vec<f64> = self.window.iter().take(mid).filter_map(|v| v.to_f64()).collect();
1314 let second: Vec<f64> = self.window.iter().skip(mid).filter_map(|v| v.to_f64()).collect();
1315 let var = |vals: &[f64]| -> Option<f64> {
1316 let n_f = vals.len() as f64;
1317 if n_f < 2.0 { return None; }
1318 let mean = vals.iter().sum::<f64>() / n_f;
1319 Some(vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n_f - 1.0))
1320 };
1321 let v1 = var(&first)?;
1322 let v2 = var(&second)?;
1323 if v2 == 0.0 {
1324 return None;
1325 }
1326 Some(v1 / v2)
1327 }
1328
1329 pub fn z_score_trend_slope(&self) -> Option<f64> {
1336 use rust_decimal::prelude::ToPrimitive;
1337 let n = self.window.len();
1338 if n < 2 {
1339 return None;
1340 }
1341 let std_dev = self.std_dev()?;
1342 if std_dev == 0.0 {
1343 return None;
1344 }
1345 let mean = self.mean()?.to_f64()?;
1346 let z_vals: Vec<f64> = self
1347 .window
1348 .iter()
1349 .filter_map(|v| v.to_f64())
1350 .map(|v| (v - mean) / std_dev)
1351 .collect();
1352 if z_vals.len() < 2 {
1353 return None;
1354 }
1355 let n_f = z_vals.len() as f64;
1356 let x_mean = (n_f - 1.0) / 2.0;
1357 let z_mean = z_vals.iter().sum::<f64>() / n_f;
1358 let num: f64 = z_vals.iter().enumerate().map(|(i, &z)| (i as f64 - x_mean) * (z - z_mean)).sum();
1359 let den: f64 = (0..z_vals.len()).map(|i| (i as f64 - x_mean).powi(2)).sum();
1360 if den == 0.0 { return None; }
1361 Some(num / den)
1362 }
1363
1364 pub fn mean_absolute_change(&self) -> Option<f64> {
1368 use rust_decimal::prelude::ToPrimitive;
1369 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
1370 if vals.len() < 2 {
1371 return None;
1372 }
1373 let mac = vals.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / (vals.len() - 1) as f64;
1374 Some(mac)
1375 }
1376
1377 pub fn monotone_increase_fraction(&self) -> Option<f64> {
1384 let n = self.window.len();
1385 if n < 2 {
1386 return None;
1387 }
1388 let increasing = self.window
1389 .iter()
1390 .collect::<Vec<_>>()
1391 .windows(2)
1392 .filter(|w| w[1] > w[0])
1393 .count();
1394 Some(increasing as f64 / (n - 1) as f64)
1395 }
1396
1397 pub fn abs_max(&self) -> Option<Decimal> {
1401 self.window.iter().map(|v| v.abs()).reduce(|a, b| a.max(b))
1402 }
1403
1404 pub fn abs_min(&self) -> Option<Decimal> {
1408 self.window.iter().map(|v| v.abs()).reduce(|a, b| a.min(b))
1409 }
1410
1411 pub fn max_count(&self) -> Option<usize> {
1415 let mut tmp = MinMaxNormalizer::new(self.window_size).ok()?;
1416 for &v in &self.window {
1417 tmp.update(v);
1418 }
1419 let (_, max) = tmp.min_max()?;
1420 Some(self.window.iter().filter(|&&v| v == max).count())
1421 }
1422
1423 pub fn min_count(&self) -> Option<usize> {
1427 let mut tmp = MinMaxNormalizer::new(self.window_size).ok()?;
1428 for &v in &self.window {
1429 tmp.update(v);
1430 }
1431 let (min, _) = tmp.min_max()?;
1432 Some(self.window.iter().filter(|&&v| v == min).count())
1433 }
1434
1435 pub fn mean_ratio(&self) -> Option<f64> {
1440 use rust_decimal::prelude::ToPrimitive;
1441 let n = self.window.len();
1442 if n < 2 {
1443 return None;
1444 }
1445 let current_mean = self.mean()?;
1446 let half = (n / 2).max(1);
1447 let early_sum: Decimal = self.window.iter().take(half).copied().sum();
1448 let early_mean = early_sum / Decimal::from(half as i64);
1449 if early_mean.is_zero() {
1450 return None;
1451 }
1452 (current_mean / early_mean).to_f64()
1453 }
1454
1455 pub fn exponential_weighted_mean(&self, alpha: f64) -> Option<f64> {
1459 use rust_decimal::prelude::ToPrimitive;
1460 if self.window.is_empty() {
1461 return None;
1462 }
1463 let alpha = alpha.clamp(1e-6, 1.0);
1464 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
1465 if vals.is_empty() {
1466 return None;
1467 }
1468 let mut ewm = vals[0];
1469 for &v in &vals[1..] {
1470 ewm = alpha * v + (1.0 - alpha) * ewm;
1471 }
1472 Some(ewm)
1473 }
1474
1475 pub fn second_moment(&self) -> Option<f64> {
1477 use rust_decimal::prelude::ToPrimitive;
1478 if self.window.is_empty() {
1479 return None;
1480 }
1481 let sum: f64 = self.window.iter().filter_map(|v| v.to_f64()).map(|v| v * v).sum();
1482 Some(sum / self.window.len() as f64)
1483 }
1484
1485 pub fn range_over_mean(&self) -> Option<f64> {
1487 use rust_decimal::prelude::ToPrimitive;
1488 let max = self.window.iter().copied().max()?;
1489 let min = self.window.iter().copied().min()?;
1490 let mean = self.mean()?;
1491 if mean.is_zero() {
1492 return None;
1493 }
1494 ((max - min) / mean).to_f64()
1495 }
1496
1497 pub fn above_median_fraction(&self) -> Option<f64> {
1499 if self.window.is_empty() {
1500 return None;
1501 }
1502 let median = self.median()?;
1503 let count = self.window.iter().filter(|&&v| v > median).count();
1504 Some(count as f64 / self.window.len() as f64)
1505 }
1506
1507 pub fn interquartile_mean(&self) -> Option<f64> {
1511 use rust_decimal::prelude::ToPrimitive;
1512 if self.window.is_empty() {
1513 return None;
1514 }
1515 let q1 = self.percentile_value(0.25)?;
1516 let q3 = self.percentile_value(0.75)?;
1517 let iqr_vals: Vec<f64> = self.window
1518 .iter()
1519 .filter(|&&v| v > q1 && v < q3)
1520 .filter_map(|v| v.to_f64())
1521 .collect();
1522 if iqr_vals.is_empty() {
1523 return None;
1524 }
1525 Some(iqr_vals.iter().sum::<f64>() / iqr_vals.len() as f64)
1526 }
1527
1528 pub fn outlier_fraction(&self, threshold: f64) -> Option<f64> {
1530 use rust_decimal::prelude::ToPrimitive;
1531 if self.window.is_empty() {
1532 return None;
1533 }
1534 let std_dev = self.std_dev()?;
1535 let mean = self.mean()?.to_f64()?;
1536 if std_dev == 0.0 {
1537 return Some(0.0);
1538 }
1539 let count = self.window
1540 .iter()
1541 .filter_map(|v| v.to_f64())
1542 .filter(|&v| ((v - mean) / std_dev).abs() > threshold)
1543 .count();
1544 Some(count as f64 / self.window.len() as f64)
1545 }
1546
1547 pub fn sign_flip_count(&self) -> Option<usize> {
1549 if self.window.len() < 2 {
1550 return None;
1551 }
1552 let count = self.window
1553 .iter()
1554 .collect::<Vec<_>>()
1555 .windows(2)
1556 .filter(|w| w[0].is_sign_negative() != w[1].is_sign_negative())
1557 .count();
1558 Some(count)
1559 }
1560
1561 pub fn rms(&self) -> Option<f64> {
1563 use rust_decimal::prelude::ToPrimitive;
1564 if self.window.is_empty() {
1565 return None;
1566 }
1567 let sum_sq: f64 = self.window.iter().filter_map(|v| v.to_f64()).map(|v| v * v).sum();
1568 Some((sum_sq / self.window.len() as f64).sqrt())
1569 }
1570
1571 pub fn distinct_count(&self) -> usize {
1575 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
1576 sorted.sort();
1577 sorted.dedup();
1578 sorted.len()
1579 }
1580
1581 pub fn max_fraction(&self) -> Option<f64> {
1583 if self.window.is_empty() {
1584 return None;
1585 }
1586 let max = self.window.iter().copied().max()?;
1587 let count = self.window.iter().filter(|&&v| v == max).count();
1588 Some(count as f64 / self.window.len() as f64)
1589 }
1590
1591 pub fn min_fraction(&self) -> Option<f64> {
1593 if self.window.is_empty() {
1594 return None;
1595 }
1596 let min = self.window.iter().copied().min()?;
1597 let count = self.window.iter().filter(|&&v| v == min).count();
1598 Some(count as f64 / self.window.len() as f64)
1599 }
1600
1601 pub fn latest_minus_mean(&self) -> Option<f64> {
1603 use rust_decimal::prelude::ToPrimitive;
1604 let latest = self.latest()?;
1605 let mean = self.mean()?;
1606 (latest - mean).to_f64()
1607 }
1608
1609 pub fn latest_to_mean_ratio(&self) -> Option<f64> {
1611 use rust_decimal::prelude::ToPrimitive;
1612 let latest = self.latest()?;
1613 let mean = self.mean()?;
1614 if mean.is_zero() {
1615 return None;
1616 }
1617 (latest / mean).to_f64()
1618 }
1619
1620 pub fn below_mean_fraction(&self) -> Option<f64> {
1624 if self.window.is_empty() {
1625 return None;
1626 }
1627 let mean = self.mean()?;
1628 let count = self.window.iter().filter(|&&v| v < mean).count();
1629 Some(count as f64 / self.window.len() as f64)
1630 }
1631
1632 pub fn tail_variance(&self) -> Option<f64> {
1635 use rust_decimal::prelude::ToPrimitive;
1636 if self.window.len() < 4 {
1637 return None;
1638 }
1639 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
1640 sorted.sort();
1641 let n = sorted.len();
1642 let q1 = sorted[n / 4];
1643 let q3 = sorted[(3 * n) / 4];
1644 let tails: Vec<f64> = sorted
1645 .iter()
1646 .filter(|&&v| v < q1 || v > q3)
1647 .filter_map(|v| v.to_f64())
1648 .collect();
1649 if tails.len() < 2 {
1650 return None;
1651 }
1652 let nt = tails.len() as f64;
1653 let mean = tails.iter().sum::<f64>() / nt;
1654 let var = tails.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (nt - 1.0);
1655 Some(var)
1656 }
1657
1658 pub fn new_max_count(&self) -> usize {
1662 if self.window.is_empty() {
1663 return 0;
1664 }
1665 let vals: Vec<Decimal> = self.window.iter().copied().collect();
1666 let mut running = vals[0];
1667 let mut count = 1usize;
1668 for &v in vals.iter().skip(1) {
1669 if v > running {
1670 running = v;
1671 count += 1;
1672 }
1673 }
1674 count
1675 }
1676
1677 pub fn new_min_count(&self) -> usize {
1679 if self.window.is_empty() {
1680 return 0;
1681 }
1682 let vals: Vec<Decimal> = self.window.iter().copied().collect();
1683 let mut running = vals[0];
1684 let mut count = 1usize;
1685 for &v in vals.iter().skip(1) {
1686 if v < running {
1687 running = v;
1688 count += 1;
1689 }
1690 }
1691 count
1692 }
1693
1694 pub fn zero_fraction(&self) -> Option<f64> {
1696 if self.window.is_empty() {
1697 return None;
1698 }
1699 let count = self.window.iter().filter(|&&v| v == Decimal::ZERO).count();
1700 Some(count as f64 / self.window.len() as f64)
1701 }
1702
1703 pub fn cumulative_sum(&self) -> Decimal {
1707 self.window.iter().copied().sum()
1708 }
1709
1710 pub fn max_to_min_ratio(&self) -> Option<f64> {
1713 use rust_decimal::prelude::ToPrimitive;
1714 let max = self.window.iter().copied().max()?;
1715 let min = self.window.iter().copied().min()?;
1716 if min.is_zero() {
1717 return None;
1718 }
1719 (max / min).to_f64()
1720 }
1721
1722 pub fn above_midpoint_fraction(&self) -> Option<f64> {
1728 if self.window.is_empty() {
1729 return None;
1730 }
1731 let min = self.window.iter().copied().min()?;
1732 let max = self.window.iter().copied().max()?;
1733 let mid = (min + max) / Decimal::TWO;
1734 let count = self.window.iter().filter(|&&v| v > mid).count();
1735 Some(count as f64 / self.window.len() as f64)
1736 }
1737
1738 pub fn span_utilization(&self) -> Option<f64> {
1742 use rust_decimal::prelude::ToPrimitive;
1743 if self.window.is_empty() {
1744 return None;
1745 }
1746 let min = self.window.iter().copied().min()?;
1747 let max = self.window.iter().copied().max()?;
1748 let range = max - min;
1749 if range.is_zero() {
1750 return None;
1751 }
1752 let latest = *self.window.back()?;
1753 ((latest - min) / range).to_f64()
1754 }
1755
1756 pub fn positive_fraction(&self) -> Option<f64> {
1760 if self.window.is_empty() {
1761 return None;
1762 }
1763 let count = self.window.iter().filter(|&&v| v > Decimal::ZERO).count();
1764 Some(count as f64 / self.window.len() as f64)
1765 }
1766
1767}
1768
1769#[cfg(test)]
1770mod tests {
1771 use super::*;
1772 use rust_decimal_macros::dec;
1773
1774 fn norm(w: usize) -> MinMaxNormalizer {
1775 MinMaxNormalizer::new(w).unwrap()
1776 }
1777
1778 #[test]
1781 fn test_new_normalizer_is_empty() {
1782 let n = norm(4);
1783 assert!(n.is_empty());
1784 assert_eq!(n.len(), 0);
1785 }
1786
1787 #[test]
1788 fn test_minmax_is_full_false_before_capacity() {
1789 let mut n = norm(3);
1790 assert!(!n.is_full());
1791 n.update(dec!(1));
1792 n.update(dec!(2));
1793 assert!(!n.is_full());
1794 n.update(dec!(3));
1795 assert!(n.is_full());
1796 }
1797
1798 #[test]
1799 fn test_minmax_is_full_stays_true_after_eviction() {
1800 let mut n = norm(3);
1801 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
1802 n.update(v);
1803 }
1804 assert!(n.is_full()); }
1806
1807 #[test]
1808 fn test_new_zero_window_returns_error() {
1809 let result = MinMaxNormalizer::new(0);
1810 assert!(matches!(result, Err(StreamError::ConfigError { .. })));
1811 }
1812
1813 #[test]
1816 fn test_normalize_min_is_zero() {
1817 let mut n = norm(4);
1818 n.update(dec!(10));
1819 n.update(dec!(20));
1820 n.update(dec!(30));
1821 n.update(dec!(40));
1822 let v = n.normalize(dec!(10)).unwrap();
1823 assert!(
1824 (v - 0.0).abs() < 1e-10,
1825 "min should normalize to 0.0, got {v}"
1826 );
1827 }
1828
1829 #[test]
1830 fn test_normalize_max_is_one() {
1831 let mut n = norm(4);
1832 n.update(dec!(10));
1833 n.update(dec!(20));
1834 n.update(dec!(30));
1835 n.update(dec!(40));
1836 let v = n.normalize(dec!(40)).unwrap();
1837 assert!(
1838 (v - 1.0).abs() < 1e-10,
1839 "max should normalize to 1.0, got {v}"
1840 );
1841 }
1842
1843 #[test]
1844 fn test_normalize_midpoint_is_half() {
1845 let mut n = norm(4);
1846 n.update(dec!(0));
1847 n.update(dec!(100));
1848 let v = n.normalize(dec!(50)).unwrap();
1849 assert!((v - 0.5).abs() < 1e-10);
1850 }
1851
1852 #[test]
1853 fn test_normalize_result_clamped_below_zero() {
1854 let mut n = norm(4);
1855 n.update(dec!(50));
1856 n.update(dec!(100));
1857 let v = n.normalize(dec!(10)).unwrap();
1859 assert!(v >= 0.0);
1860 assert_eq!(v, 0.0);
1861 }
1862
1863 #[test]
1864 fn test_normalize_result_clamped_above_one() {
1865 let mut n = norm(4);
1866 n.update(dec!(50));
1867 n.update(dec!(100));
1868 let v = n.normalize(dec!(200)).unwrap();
1870 assert!(v <= 1.0);
1871 assert_eq!(v, 1.0);
1872 }
1873
1874 #[test]
1875 fn test_normalize_all_same_values_returns_zero() {
1876 let mut n = norm(4);
1877 n.update(dec!(5));
1878 n.update(dec!(5));
1879 n.update(dec!(5));
1880 let v = n.normalize(dec!(5)).unwrap();
1881 assert_eq!(v, 0.0);
1882 }
1883
1884 #[test]
1887 fn test_normalize_empty_window_returns_error() {
1888 let mut n = norm(4);
1889 let err = n.normalize(dec!(1)).unwrap_err();
1890 assert!(matches!(err, StreamError::NormalizationError { .. }));
1891 }
1892
1893 #[test]
1894 fn test_min_max_empty_returns_none() {
1895 let mut n = norm(4);
1896 assert!(n.min_max().is_none());
1897 }
1898
1899 #[test]
1904 fn test_rolling_window_evicts_oldest() {
1905 let mut n = norm(3);
1906 n.update(dec!(1)); n.update(dec!(5));
1908 n.update(dec!(10));
1909 n.update(dec!(20)); let (min, max) = n.min_max().unwrap();
1911 assert_eq!(min, dec!(5));
1912 assert_eq!(max, dec!(20));
1913 }
1914
1915 #[test]
1916 fn test_rolling_window_len_does_not_exceed_capacity() {
1917 let mut n = norm(3);
1918 for i in 0..10 {
1919 n.update(Decimal::from(i));
1920 }
1921 assert_eq!(n.len(), 3);
1922 }
1923
1924 #[test]
1927 fn test_reset_clears_window() {
1928 let mut n = norm(4);
1929 n.update(dec!(10));
1930 n.update(dec!(20));
1931 n.reset();
1932 assert!(n.is_empty());
1933 assert!(n.min_max().is_none());
1934 }
1935
1936 #[test]
1937 fn test_normalize_works_after_reset() {
1938 let mut n = norm(4);
1939 n.update(dec!(10));
1940 n.reset();
1941 n.update(dec!(0));
1942 n.update(dec!(100));
1943 let v = n.normalize(dec!(100)).unwrap();
1944 assert!((v - 1.0).abs() < 1e-10);
1945 }
1946
1947 #[test]
1950 fn test_streaming_updates_monotone_sequence() {
1951 let mut n = norm(5);
1952 let prices = [dec!(100), dec!(101), dec!(102), dec!(103), dec!(104), dec!(105)];
1953 for &p in &prices {
1954 n.update(p);
1955 }
1956 let v_min = n.normalize(dec!(101)).unwrap();
1958 let v_max = n.normalize(dec!(105)).unwrap();
1959 assert!((v_min - 0.0).abs() < 1e-10);
1960 assert!((v_max - 1.0).abs() < 1e-10);
1961 }
1962
1963 #[test]
1964 fn test_normalization_monotonicity_in_window() {
1965 let mut n = norm(10);
1966 for i in 0..10 {
1967 n.update(Decimal::from(i * 10));
1968 }
1969 let v0 = n.normalize(dec!(0)).unwrap();
1971 let v50 = n.normalize(dec!(50)).unwrap();
1972 let v90 = n.normalize(dec!(90)).unwrap();
1973 assert!(v0 < v50, "normalized values should be monotone");
1974 assert!(v50 < v90, "normalized values should be monotone");
1975 }
1976
1977 #[test]
1978 fn test_high_precision_input_preserved() {
1979 let mut n = norm(2);
1981 n.update(dec!(50000.00000000));
1982 n.update(dec!(50000.12345678));
1983 let (min, max) = n.min_max().unwrap();
1984 assert_eq!(min, dec!(50000.00000000));
1985 assert_eq!(max, dec!(50000.12345678));
1986 }
1987
1988 #[test]
1991 fn test_denormalize_empty_window_returns_error() {
1992 let mut n = norm(4);
1993 assert!(matches!(n.denormalize(0.5), Err(StreamError::NormalizationError { .. })));
1994 }
1995
1996 #[test]
1997 fn test_denormalize_roundtrip_min() {
1998 let mut n = norm(4);
1999 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
2000 n.update(v);
2001 }
2002 let normalized = n.normalize(dec!(10)).unwrap(); let back = n.denormalize(normalized).unwrap();
2004 assert!((back - dec!(10)).abs() < dec!(0.0001));
2005 }
2006
2007 #[test]
2008 fn test_denormalize_roundtrip_max() {
2009 let mut n = norm(4);
2010 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
2011 n.update(v);
2012 }
2013 let normalized = n.normalize(dec!(40)).unwrap(); let back = n.denormalize(normalized).unwrap();
2015 assert!((back - dec!(40)).abs() < dec!(0.0001));
2016 }
2017
2018 #[test]
2021 fn test_range_none_when_empty() {
2022 let mut n = norm(4);
2023 assert!(n.range().is_none());
2024 }
2025
2026 #[test]
2027 fn test_range_zero_when_all_same() {
2028 let mut n = norm(3);
2029 n.update(dec!(5));
2030 n.update(dec!(5));
2031 n.update(dec!(5));
2032 assert_eq!(n.range(), Some(dec!(0)));
2033 }
2034
2035 #[test]
2036 fn test_range_correct() {
2037 let mut n = norm(4);
2038 for v in [dec!(10), dec!(40), dec!(20), dec!(30)] {
2039 n.update(v);
2040 }
2041 assert_eq!(n.range(), Some(dec!(30))); }
2043
2044 #[test]
2047 fn test_midpoint_none_when_empty() {
2048 let mut n = norm(4);
2049 assert!(n.midpoint().is_none());
2050 }
2051
2052 #[test]
2053 fn test_midpoint_correct() {
2054 let mut n = norm(4);
2055 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
2056 n.update(v);
2057 }
2058 assert_eq!(n.midpoint(), Some(dec!(25)));
2060 }
2061
2062 #[test]
2063 fn test_midpoint_single_value() {
2064 let mut n = norm(4);
2065 n.update(dec!(42));
2066 assert_eq!(n.midpoint(), Some(dec!(42)));
2067 }
2068
2069 #[test]
2072 fn test_clamp_to_window_returns_value_unchanged_when_empty() {
2073 let mut n = norm(4);
2074 assert_eq!(n.clamp_to_window(dec!(50)), dec!(50));
2075 }
2076
2077 #[test]
2078 fn test_clamp_to_window_clamps_above_max() {
2079 let mut n = norm(4);
2080 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2081 assert_eq!(n.clamp_to_window(dec!(100)), dec!(30));
2082 }
2083
2084 #[test]
2085 fn test_clamp_to_window_clamps_below_min() {
2086 let mut n = norm(4);
2087 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2088 assert_eq!(n.clamp_to_window(dec!(5)), dec!(10));
2089 }
2090
2091 #[test]
2092 fn test_clamp_to_window_passthrough_when_in_range() {
2093 let mut n = norm(4);
2094 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2095 assert_eq!(n.clamp_to_window(dec!(15)), dec!(15));
2096 }
2097
2098 #[test]
2101 fn test_count_above_zero_when_empty() {
2102 let n = norm(4);
2103 assert_eq!(n.count_above(dec!(5)), 0);
2104 }
2105
2106 #[test]
2107 fn test_count_above_counts_strictly_above() {
2108 let mut n = norm(8);
2109 for v in [dec!(1), dec!(5), dec!(10), dec!(15)] { n.update(v); }
2110 assert_eq!(n.count_above(dec!(5)), 2); }
2112
2113 #[test]
2114 fn test_count_above_all_when_threshold_below_all() {
2115 let mut n = norm(4);
2116 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2117 assert_eq!(n.count_above(dec!(5)), 3);
2118 }
2119
2120 #[test]
2121 fn test_count_above_zero_when_threshold_above_all() {
2122 let mut n = norm(4);
2123 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2124 assert_eq!(n.count_above(dec!(100)), 0);
2125 }
2126
2127 #[test]
2130 fn test_count_below_zero_when_empty() {
2131 let n = norm(4);
2132 assert_eq!(n.count_below(dec!(5)), 0);
2133 }
2134
2135 #[test]
2136 fn test_count_below_counts_strictly_below() {
2137 let mut n = norm(8);
2138 for v in [dec!(1), dec!(5), dec!(10), dec!(15)] { n.update(v); }
2139 assert_eq!(n.count_below(dec!(10)), 2); }
2141
2142 #[test]
2143 fn test_count_below_all_when_threshold_above_all() {
2144 let mut n = norm(4);
2145 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2146 assert_eq!(n.count_below(dec!(100)), 3);
2147 }
2148
2149 #[test]
2150 fn test_count_below_zero_when_threshold_below_all() {
2151 let mut n = norm(4);
2152 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2153 assert_eq!(n.count_below(dec!(5)), 0);
2154 }
2155
2156 #[test]
2157 fn test_count_above_plus_count_below_leq_len() {
2158 let mut n = norm(5);
2159 for v in [dec!(1), dec!(5), dec!(5), dec!(10), dec!(20)] { n.update(v); }
2160 assert_eq!(n.count_above(dec!(5)) + n.count_below(dec!(5)), 3);
2163 }
2164
2165 #[test]
2168 fn test_normalized_range_none_when_empty() {
2169 let mut n = norm(4);
2170 assert!(n.normalized_range().is_none());
2171 }
2172
2173 #[test]
2174 fn test_normalized_range_zero_when_all_same() {
2175 let mut n = norm(4);
2176 for _ in 0..4 { n.update(dec!(5)); }
2177 assert_eq!(n.normalized_range(), Some(0.0));
2178 }
2179
2180 #[test]
2181 fn test_normalized_range_correct_value() {
2182 let mut n = norm(4);
2183 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2184 let nr = n.normalized_range().unwrap();
2186 assert!((nr - 0.75).abs() < 1e-10);
2187 }
2188
2189 #[test]
2192 fn test_normalize_clamp_in_range_equals_normalize() {
2193 let mut n = norm(4);
2194 for v in [dec!(0), dec!(25), dec!(75), dec!(100)] {
2195 n.update(v);
2196 }
2197 let clamped = n.normalize_clamp(dec!(50)).unwrap();
2198 let normal = n.normalize(dec!(50)).unwrap();
2199 assert!((clamped - normal).abs() < 1e-9);
2200 }
2201
2202 #[test]
2203 fn test_normalize_clamp_above_max_clamped_to_one() {
2204 let mut n = norm(3);
2205 for v in [dec!(0), dec!(50), dec!(100)] {
2206 n.update(v);
2207 }
2208 let clamped = n.normalize_clamp(dec!(200)).unwrap();
2210 assert!((clamped - 1.0).abs() < 1e-9, "expected 1.0 got {clamped}");
2211 }
2212
2213 #[test]
2214 fn test_normalize_clamp_below_min_clamped_to_zero() {
2215 let mut n = norm(3);
2216 for v in [dec!(10), dec!(50), dec!(100)] {
2217 n.update(v);
2218 }
2219 let clamped = n.normalize_clamp(dec!(-50)).unwrap();
2221 assert!((clamped - 0.0).abs() < 1e-9, "expected 0.0 got {clamped}");
2222 }
2223
2224 #[test]
2225 fn test_normalize_clamp_empty_window_returns_error() {
2226 let mut n = norm(4);
2227 assert!(n.normalize_clamp(dec!(5)).is_err());
2228 }
2229
2230 #[test]
2233 fn test_latest_none_when_empty() {
2234 let n = norm(5);
2235 assert_eq!(n.latest(), None);
2236 }
2237
2238 #[test]
2239 fn test_latest_returns_most_recent_value() {
2240 let mut n = norm(5);
2241 n.update(dec!(10));
2242 n.update(dec!(20));
2243 n.update(dec!(30));
2244 assert_eq!(n.latest(), Some(dec!(30)));
2245 }
2246
2247 #[test]
2248 fn test_latest_updates_on_each_push() {
2249 let mut n = norm(3);
2250 n.update(dec!(1));
2251 assert_eq!(n.latest(), Some(dec!(1)));
2252 n.update(dec!(5));
2253 assert_eq!(n.latest(), Some(dec!(5)));
2254 }
2255
2256 #[test]
2257 fn test_latest_returns_last_after_window_overflow() {
2258 let mut n = norm(2); n.update(dec!(100));
2260 n.update(dec!(200));
2261 n.update(dec!(300)); assert_eq!(n.latest(), Some(dec!(300)));
2263 }
2264
2265 #[test]
2268 fn test_minmax_cv_none_fewer_than_2_obs() {
2269 let mut n = norm(4);
2270 n.update(dec!(10));
2271 assert!(n.coefficient_of_variation().is_none());
2272 }
2273
2274 #[test]
2275 fn test_minmax_cv_none_when_mean_zero() {
2276 let mut n = norm(4);
2277 for v in [dec!(-5), dec!(5)] { n.update(v); }
2278 assert!(n.coefficient_of_variation().is_none());
2279 }
2280
2281 #[test]
2282 fn test_minmax_cv_positive_for_positive_mean() {
2283 let mut n = norm(4);
2284 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2285 let cv = n.coefficient_of_variation().unwrap();
2286 assert!(cv > 0.0, "CV should be positive");
2287 }
2288
2289 #[test]
2292 fn test_minmax_variance_none_fewer_than_2_obs() {
2293 let mut n = norm(5);
2294 n.update(dec!(10));
2295 assert!(n.variance().is_none());
2296 }
2297
2298 #[test]
2299 fn test_minmax_variance_zero_all_same() {
2300 let mut n = norm(4);
2301 for _ in 0..4 { n.update(dec!(5)); }
2302 assert_eq!(n.variance(), Some(dec!(0)));
2303 }
2304
2305 #[test]
2306 fn test_minmax_variance_correct_value() {
2307 let mut n = norm(4);
2308 for v in [dec!(5), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2311 let var = n.variance().unwrap();
2312 assert!((var.to_f64().unwrap() - 2.75).abs() < 1e-9);
2314 }
2315
2316 #[test]
2317 fn test_minmax_std_dev_none_fewer_than_2_obs() {
2318 let n = norm(4);
2319 assert!(n.std_dev().is_none());
2320 }
2321
2322 #[test]
2323 fn test_minmax_std_dev_zero_all_same() {
2324 let mut n = norm(3);
2325 for _ in 0..3 { n.update(dec!(7)); }
2326 assert_eq!(n.std_dev(), Some(0.0));
2327 }
2328
2329 #[test]
2330 fn test_minmax_std_dev_sqrt_of_variance() {
2331 let mut n = norm(4);
2332 for v in [dec!(5), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2333 let sd = n.std_dev().unwrap();
2334 let var = n.variance().unwrap().to_f64().unwrap();
2335 assert!((sd - var.sqrt()).abs() < 1e-9);
2336 }
2337
2338 #[test]
2341 fn test_minmax_kurtosis_none_fewer_than_4_observations() {
2342 let mut n = norm(5);
2343 n.update(dec!(1));
2344 n.update(dec!(2));
2345 n.update(dec!(3));
2346 assert!(n.kurtosis().is_none());
2347 }
2348
2349 #[test]
2350 fn test_minmax_kurtosis_some_with_4_observations() {
2351 let mut n = norm(4);
2352 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
2353 n.update(v);
2354 }
2355 assert!(n.kurtosis().is_some());
2356 }
2357
2358 #[test]
2359 fn test_minmax_kurtosis_none_all_same_value() {
2360 let mut n = norm(4);
2361 for _ in 0..4 {
2362 n.update(dec!(5));
2363 }
2364 assert!(n.kurtosis().is_none());
2366 }
2367
2368 #[test]
2369 fn test_minmax_kurtosis_uniform_distribution_is_negative() {
2370 let mut n = norm(10);
2372 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5),
2373 dec!(6), dec!(7), dec!(8), dec!(9), dec!(10)] {
2374 n.update(v);
2375 }
2376 let k = n.kurtosis().unwrap();
2377 assert!(k < 0.0, "uniform distribution should have negative excess kurtosis, got {k}");
2378 }
2379
2380 #[test]
2383 fn test_minmax_median_none_for_empty_window() {
2384 assert!(norm(4).median().is_none());
2385 }
2386
2387 #[test]
2388 fn test_minmax_median_odd_window() {
2389 let mut n = norm(5);
2390 for v in [dec!(3), dec!(1), dec!(5), dec!(2), dec!(4)] { n.update(v); }
2391 assert_eq!(n.median(), Some(dec!(3)));
2393 }
2394
2395 #[test]
2396 fn test_minmax_median_even_window() {
2397 let mut n = norm(4);
2398 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2399 assert_eq!(n.median(), Some(dec!(2.5)));
2401 }
2402
2403 #[test]
2406 fn test_minmax_sample_variance_none_for_single_obs() {
2407 let mut n = norm(4);
2408 n.update(dec!(10));
2409 assert!(n.sample_variance().is_none());
2410 }
2411
2412 #[test]
2413 fn test_minmax_sample_variance_larger_than_population_variance() {
2414 let mut n = norm(4);
2415 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2416 use rust_decimal::prelude::ToPrimitive;
2417 let pop_var = n.variance().unwrap().to_f64().unwrap();
2418 let sample_var = n.sample_variance().unwrap();
2419 assert!(sample_var > pop_var, "sample variance should exceed population variance");
2420 }
2421
2422 #[test]
2425 fn test_minmax_mad_none_for_empty_window() {
2426 assert!(norm(4).mad().is_none());
2427 }
2428
2429 #[test]
2430 fn test_minmax_mad_zero_for_identical_values() {
2431 let mut n = norm(4);
2432 for _ in 0..4 { n.update(dec!(5)); }
2433 assert_eq!(n.mad(), Some(dec!(0)));
2434 }
2435
2436 #[test]
2437 fn test_minmax_mad_correct_for_known_distribution() {
2438 let mut n = norm(5);
2439 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2440 assert_eq!(n.mad(), Some(dec!(1)));
2442 }
2443
2444 #[test]
2447 fn test_minmax_robust_z_none_for_empty_window() {
2448 assert!(norm(4).robust_z_score(dec!(10)).is_none());
2449 }
2450
2451 #[test]
2452 fn test_minmax_robust_z_none_when_mad_is_zero() {
2453 let mut n = norm(4);
2454 for _ in 0..4 { n.update(dec!(5)); }
2455 assert!(n.robust_z_score(dec!(5)).is_none());
2456 }
2457
2458 #[test]
2459 fn test_minmax_robust_z_positive_above_median() {
2460 let mut n = norm(5);
2461 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2462 let rz = n.robust_z_score(dec!(5)).unwrap();
2463 assert!(rz > 0.0, "robust z-score should be positive for value above median");
2464 }
2465
2466 #[test]
2467 fn test_minmax_robust_z_negative_below_median() {
2468 let mut n = norm(5);
2469 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2470 let rz = n.robust_z_score(dec!(1)).unwrap();
2471 assert!(rz < 0.0, "robust z-score should be negative for value below median");
2472 }
2473
2474 #[test]
2477 fn test_percentile_value_none_for_empty_window() {
2478 assert!(norm(4).percentile_value(0.5).is_none());
2479 }
2480
2481 #[test]
2482 fn test_percentile_value_min_at_zero() {
2483 let mut n = norm(5);
2484 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2485 assert_eq!(n.percentile_value(0.0), Some(dec!(10)));
2486 }
2487
2488 #[test]
2489 fn test_percentile_value_max_at_one() {
2490 let mut n = norm(5);
2491 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2492 assert_eq!(n.percentile_value(1.0), Some(dec!(50)));
2493 }
2494
2495 #[test]
2496 fn test_percentile_value_median_at_half() {
2497 let mut n = norm(5);
2498 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2499 assert_eq!(n.percentile_value(0.5), Some(dec!(30)));
2501 }
2502
2503 #[test]
2506 fn test_minmax_sum_none_for_empty_window() {
2507 assert!(norm(3).sum().is_none());
2508 }
2509
2510 #[test]
2511 fn test_minmax_sum_single_value() {
2512 let mut n = norm(3);
2513 n.update(dec!(7));
2514 assert_eq!(n.sum(), Some(dec!(7)));
2515 }
2516
2517 #[test]
2518 fn test_minmax_sum_multiple_values() {
2519 let mut n = norm(4);
2520 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2521 assert_eq!(n.sum(), Some(dec!(10)));
2522 }
2523
2524 #[test]
2527 fn test_minmax_is_outlier_false_for_empty_window() {
2528 assert!(!norm(3).is_outlier(dec!(100), 2.0));
2529 }
2530
2531 #[test]
2532 fn test_minmax_is_outlier_false_for_in_range_value() {
2533 let mut n = norm(5);
2534 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2535 assert!(!n.is_outlier(dec!(3), 2.0));
2536 }
2537
2538 #[test]
2539 fn test_minmax_is_outlier_true_for_extreme_value() {
2540 let mut n = norm(5);
2541 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2542 assert!(n.is_outlier(dec!(100), 2.0));
2543 }
2544
2545 #[test]
2548 fn test_minmax_trim_outliers_returns_all_when_no_outliers() {
2549 let mut n = norm(5);
2550 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2551 let trimmed = n.trim_outliers(10.0);
2552 assert_eq!(trimmed.len(), 5);
2553 }
2554
2555 #[test]
2556 fn test_minmax_trim_outliers_removes_extreme_values() {
2557 let mut n = norm(5);
2558 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2559 let trimmed = n.trim_outliers(0.0);
2561 assert_eq!(trimmed.len(), 1); }
2563
2564 #[test]
2567 fn test_minmax_z_score_of_latest_none_for_empty_window() {
2568 assert!(norm(3).z_score_of_latest().is_none());
2569 }
2570
2571 #[test]
2572 fn test_minmax_z_score_of_latest_zero_for_single_value() {
2573 let mut n = norm(5);
2574 n.update(dec!(10));
2575 assert!(n.z_score_of_latest().is_none());
2577 }
2578
2579 #[test]
2580 fn test_minmax_z_score_of_latest_positive_for_above_mean() {
2581 let mut n = norm(5);
2582 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(10)] { n.update(v); }
2583 let z = n.z_score_of_latest().unwrap();
2584 assert!(z > 0.0, "latest value is above mean → positive z-score");
2585 }
2586
2587 #[test]
2590 fn test_minmax_deviation_from_mean_none_for_empty_window() {
2591 assert!(norm(3).deviation_from_mean(dec!(5)).is_none());
2592 }
2593
2594 #[test]
2595 fn test_minmax_deviation_from_mean_zero_at_mean() {
2596 let mut n = norm(4);
2597 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2598 let dev = n.deviation_from_mean(dec!(2.5)).unwrap();
2600 assert!(dev.abs() < 1e-9);
2601 }
2602
2603 #[test]
2604 fn test_minmax_deviation_from_mean_positive_above_mean() {
2605 let mut n = norm(4);
2606 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2607 let dev = n.deviation_from_mean(dec!(5)).unwrap();
2608 assert!(dev > 0.0);
2609 }
2610
2611 #[test]
2614 fn test_minmax_range_f64_none_for_empty_window() {
2615 assert!(norm(3).range_f64().is_none());
2616 }
2617
2618 #[test]
2619 fn test_minmax_range_f64_correct() {
2620 let mut n = norm(4);
2621 for v in [dec!(5), dec!(15), dec!(10), dec!(20)] { n.update(v); }
2622 let r = n.range_f64().unwrap();
2624 assert!((r - 15.0).abs() < 1e-9);
2625 }
2626
2627 #[test]
2628 fn test_minmax_sum_f64_none_for_empty_window() {
2629 assert!(norm(3).sum_f64().is_none());
2630 }
2631
2632 #[test]
2633 fn test_minmax_sum_f64_correct() {
2634 let mut n = norm(4);
2635 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2636 let s = n.sum_f64().unwrap();
2637 assert!((s - 10.0).abs() < 1e-9);
2638 }
2639
2640 #[test]
2643 fn test_minmax_values_empty_for_empty_window() {
2644 assert!(norm(3).values().is_empty());
2645 }
2646
2647 #[test]
2648 fn test_minmax_values_preserves_insertion_order() {
2649 let mut n = norm(5);
2650 for v in [dec!(3), dec!(1), dec!(4), dec!(1), dec!(5)] { n.update(v); }
2651 assert_eq!(n.values(), vec![dec!(3), dec!(1), dec!(4), dec!(1), dec!(5)]);
2652 }
2653
2654 #[test]
2657 fn test_minmax_normalized_midpoint_none_for_empty_window() {
2658 assert!(norm(3).normalized_midpoint().is_none());
2659 }
2660
2661 #[test]
2662 fn test_minmax_normalized_midpoint_half_for_uniform_range() {
2663 let mut n = norm(4);
2664 for v in [dec!(0), dec!(10), dec!(20), dec!(30)] { n.update(v); }
2665 let mid = n.normalized_midpoint().unwrap();
2667 assert!((mid - 0.5).abs() < 1e-9);
2668 }
2669
2670 #[test]
2673 fn test_minmax_is_at_min_false_for_empty_window() {
2674 assert!(!norm(3).is_at_min(dec!(5)));
2675 }
2676
2677 #[test]
2678 fn test_minmax_is_at_min_true_for_minimum_value() {
2679 let mut n = norm(4);
2680 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2681 assert!(n.is_at_min(dec!(5)));
2682 }
2683
2684 #[test]
2685 fn test_minmax_is_at_min_false_for_non_minimum() {
2686 let mut n = norm(4);
2687 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2688 assert!(!n.is_at_min(dec!(10)));
2689 }
2690
2691 #[test]
2692 fn test_minmax_is_at_max_false_for_empty_window() {
2693 assert!(!norm(3).is_at_max(dec!(5)));
2694 }
2695
2696 #[test]
2697 fn test_minmax_is_at_max_true_for_maximum_value() {
2698 let mut n = norm(4);
2699 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2700 assert!(n.is_at_max(dec!(30)));
2701 }
2702
2703 #[test]
2704 fn test_minmax_is_at_max_false_for_non_maximum() {
2705 let mut n = norm(4);
2706 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2707 assert!(!n.is_at_max(dec!(20)));
2708 }
2709
2710 #[test]
2713 fn test_minmax_fraction_above_none_for_empty_window() {
2714 assert!(norm(3).fraction_above(dec!(5)).is_none());
2715 }
2716
2717 #[test]
2718 fn test_minmax_fraction_above_correct() {
2719 let mut n = norm(5);
2720 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2721 let frac = n.fraction_above(dec!(3)).unwrap();
2723 assert!((frac - 0.4).abs() < 1e-9);
2724 }
2725
2726 #[test]
2727 fn test_minmax_fraction_below_none_for_empty_window() {
2728 assert!(norm(3).fraction_below(dec!(5)).is_none());
2729 }
2730
2731 #[test]
2732 fn test_minmax_fraction_below_correct() {
2733 let mut n = norm(5);
2734 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2735 let frac = n.fraction_below(dec!(3)).unwrap();
2737 assert!((frac - 0.4).abs() < 1e-9);
2738 }
2739
2740 #[test]
2743 fn test_minmax_window_values_above_empty_window() {
2744 assert!(norm(3).window_values_above(dec!(5)).is_empty());
2745 }
2746
2747 #[test]
2748 fn test_minmax_window_values_above_filters_correctly() {
2749 let mut n = norm(5);
2750 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2751 let above = n.window_values_above(dec!(5));
2752 assert_eq!(above.len(), 2);
2753 assert!(above.contains(&dec!(7)));
2754 assert!(above.contains(&dec!(9)));
2755 }
2756
2757 #[test]
2758 fn test_minmax_window_values_below_empty_window() {
2759 assert!(norm(3).window_values_below(dec!(5)).is_empty());
2760 }
2761
2762 #[test]
2763 fn test_minmax_window_values_below_filters_correctly() {
2764 let mut n = norm(5);
2765 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2766 let below = n.window_values_below(dec!(5));
2767 assert_eq!(below.len(), 2);
2768 assert!(below.contains(&dec!(1)));
2769 assert!(below.contains(&dec!(3)));
2770 }
2771
2772 #[test]
2775 fn test_minmax_percentile_rank_none_for_empty_window() {
2776 assert!(norm(3).percentile_rank(dec!(5)).is_none());
2777 }
2778
2779 #[test]
2780 fn test_minmax_percentile_rank_correct() {
2781 let mut n = norm(5);
2782 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2783 let rank = n.percentile_rank(dec!(3)).unwrap();
2785 assert!((rank - 0.6).abs() < 1e-9);
2786 }
2787
2788 #[test]
2791 fn test_minmax_count_equal_zero_for_no_match() {
2792 let mut n = norm(3);
2793 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2794 assert_eq!(n.count_equal(dec!(99)), 0);
2795 }
2796
2797 #[test]
2798 fn test_minmax_count_equal_counts_duplicates() {
2799 let mut n = norm(5);
2800 for v in [dec!(5), dec!(5), dec!(3), dec!(5), dec!(2)] { n.update(v); }
2801 assert_eq!(n.count_equal(dec!(5)), 3);
2802 }
2803
2804 #[test]
2807 fn test_minmax_rolling_range_none_for_empty() {
2808 assert!(norm(3).rolling_range().is_none());
2809 }
2810
2811 #[test]
2812 fn test_minmax_rolling_range_correct() {
2813 let mut n = norm(5);
2814 for v in [dec!(10), dec!(50), dec!(30), dec!(20), dec!(40)] { n.update(v); }
2815 assert_eq!(n.rolling_range(), Some(dec!(40)));
2816 }
2817
2818 #[test]
2821 fn test_minmax_skewness_none_for_fewer_than_3() {
2822 let mut n = norm(5);
2823 n.update(dec!(1)); n.update(dec!(2));
2824 assert!(n.skewness().is_none());
2825 }
2826
2827 #[test]
2828 fn test_minmax_skewness_near_zero_for_symmetric_data() {
2829 let mut n = norm(5);
2830 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2831 let s = n.skewness().unwrap();
2832 assert!(s.abs() < 0.5);
2833 }
2834
2835 #[test]
2838 fn test_minmax_kurtosis_none_for_fewer_than_4() {
2839 let mut n = norm(5);
2840 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2841 assert!(n.kurtosis().is_none());
2842 }
2843
2844 #[test]
2845 fn test_minmax_kurtosis_returns_f64_for_populated_window() {
2846 let mut n = norm(5);
2847 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2848 assert!(n.kurtosis().is_some());
2849 }
2850
2851 #[test]
2854 fn test_minmax_autocorrelation_none_for_single_value() {
2855 let mut n = norm(3);
2856 n.update(dec!(1));
2857 assert!(n.autocorrelation_lag1().is_none());
2858 }
2859
2860 #[test]
2861 fn test_minmax_autocorrelation_positive_for_trending_data() {
2862 let mut n = norm(5);
2863 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2864 let ac = n.autocorrelation_lag1().unwrap();
2865 assert!(ac > 0.0);
2866 }
2867
2868 #[test]
2871 fn test_minmax_trend_consistency_none_for_single_value() {
2872 let mut n = norm(3);
2873 n.update(dec!(1));
2874 assert!(n.trend_consistency().is_none());
2875 }
2876
2877 #[test]
2878 fn test_minmax_trend_consistency_one_for_strictly_rising() {
2879 let mut n = norm(5);
2880 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2881 let tc = n.trend_consistency().unwrap();
2882 assert!((tc - 1.0).abs() < 1e-9);
2883 }
2884
2885 #[test]
2886 fn test_minmax_trend_consistency_zero_for_strictly_falling() {
2887 let mut n = norm(5);
2888 for v in [dec!(5), dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
2889 let tc = n.trend_consistency().unwrap();
2890 assert!((tc - 0.0).abs() < 1e-9);
2891 }
2892
2893 #[test]
2896 fn test_minmax_cov_none_for_single_value() {
2897 let mut n = norm(3);
2898 n.update(dec!(10));
2899 assert!(n.coefficient_of_variation().is_none());
2900 }
2901
2902 #[test]
2903 fn test_minmax_cov_positive_for_varied_data() {
2904 let mut n = norm(5);
2905 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2906 let cov = n.coefficient_of_variation().unwrap();
2907 assert!(cov > 0.0);
2908 }
2909
2910 #[test]
2913 fn test_minmax_mean_absolute_deviation_none_for_empty() {
2914 assert!(norm(3).mean_absolute_deviation().is_none());
2915 }
2916
2917 #[test]
2918 fn test_minmax_mean_absolute_deviation_zero_for_identical_values() {
2919 let mut n = norm(3);
2920 for v in [dec!(5), dec!(5), dec!(5)] { n.update(v); }
2921 let mad = n.mean_absolute_deviation().unwrap();
2922 assert!((mad - 0.0).abs() < 1e-9);
2923 }
2924
2925 #[test]
2926 fn test_minmax_mean_absolute_deviation_positive_for_varied_data() {
2927 let mut n = norm(4);
2928 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2929 let mad = n.mean_absolute_deviation().unwrap();
2930 assert!(mad > 0.0);
2931 }
2932
2933 #[test]
2936 fn test_minmax_percentile_of_latest_none_for_empty() {
2937 assert!(norm(3).percentile_of_latest().is_none());
2938 }
2939
2940 #[test]
2941 fn test_minmax_percentile_of_latest_returns_some_after_update() {
2942 let mut n = norm(4);
2943 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2944 assert!(n.percentile_of_latest().is_some());
2945 }
2946
2947 #[test]
2948 fn test_minmax_percentile_of_latest_max_has_high_rank() {
2949 let mut n = norm(5);
2950 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2951 let rank = n.percentile_of_latest().unwrap();
2952 assert!(rank >= 0.9, "max value should have rank near 1.0, got {}", rank);
2953 }
2954
2955 #[test]
2958 fn test_minmax_tail_ratio_none_for_empty() {
2959 assert!(norm(4).tail_ratio().is_none());
2960 }
2961
2962 #[test]
2963 fn test_minmax_tail_ratio_one_for_identical_values() {
2964 let mut n = norm(4);
2965 for _ in 0..4 { n.update(dec!(7)); }
2966 let r = n.tail_ratio().unwrap();
2968 assert!((r - 1.0).abs() < 1e-9, "expected 1.0, got {}", r);
2969 }
2970
2971 #[test]
2972 fn test_minmax_tail_ratio_above_one_with_outlier() {
2973 let mut n = norm(5);
2974 for v in [dec!(1), dec!(1), dec!(1), dec!(1), dec!(10)] { n.update(v); }
2975 let r = n.tail_ratio().unwrap();
2976 assert!(r > 1.0, "outlier should push ratio above 1.0, got {}", r);
2977 }
2978
2979 #[test]
2982 fn test_minmax_z_score_of_min_none_for_empty() {
2983 assert!(norm(4).z_score_of_min().is_none());
2984 }
2985
2986 #[test]
2987 fn test_minmax_z_score_of_max_none_for_empty() {
2988 assert!(norm(4).z_score_of_max().is_none());
2989 }
2990
2991 #[test]
2992 fn test_minmax_z_score_of_min_negative_for_varied_window() {
2993 let mut n = norm(5);
2994 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2995 let z = n.z_score_of_min().unwrap();
2997 assert!(z < 0.0, "z-score of min should be negative, got {}", z);
2998 }
2999
3000 #[test]
3001 fn test_minmax_z_score_of_max_positive_for_varied_window() {
3002 let mut n = norm(5);
3003 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3004 let z = n.z_score_of_max().unwrap();
3006 assert!(z > 0.0, "z-score of max should be positive, got {}", z);
3007 }
3008
3009 #[test]
3012 fn test_minmax_window_entropy_none_for_empty() {
3013 assert!(norm(4).window_entropy().is_none());
3014 }
3015
3016 #[test]
3017 fn test_minmax_window_entropy_zero_for_identical_values() {
3018 let mut n = norm(3);
3019 for _ in 0..3 { n.update(dec!(5)); }
3020 let e = n.window_entropy().unwrap();
3021 assert!((e - 0.0).abs() < 1e-9, "identical values should have zero entropy, got {}", e);
3022 }
3023
3024 #[test]
3025 fn test_minmax_window_entropy_positive_for_varied_values() {
3026 let mut n = norm(4);
3027 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3028 let e = n.window_entropy().unwrap();
3029 assert!(e > 0.0, "varied values should have positive entropy, got {}", e);
3030 }
3031
3032 #[test]
3035 fn test_minmax_normalized_std_dev_none_for_single_value() {
3036 let mut n = norm(4);
3037 n.update(dec!(5));
3038 assert!(n.normalized_std_dev().is_none());
3039 }
3040
3041 #[test]
3042 fn test_minmax_normalized_std_dev_positive_for_varied_values() {
3043 let mut n = norm(4);
3044 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3045 let r = n.normalized_std_dev().unwrap();
3046 assert!(r > 0.0, "expected positive normalized std dev, got {}", r);
3047 }
3048
3049 #[test]
3052 fn test_minmax_value_above_mean_count_none_for_empty() {
3053 assert!(norm(4).value_above_mean_count().is_none());
3054 }
3055
3056 #[test]
3057 fn test_minmax_value_above_mean_count_correct() {
3058 let mut n = norm(4);
3060 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3061 assert_eq!(n.value_above_mean_count().unwrap(), 2);
3062 }
3063
3064 #[test]
3067 fn test_minmax_consecutive_above_mean_none_for_empty() {
3068 assert!(norm(4).consecutive_above_mean().is_none());
3069 }
3070
3071 #[test]
3072 fn test_minmax_consecutive_above_mean_correct() {
3073 let mut n = norm(4);
3075 for v in [dec!(1), dec!(5), dec!(6), dec!(7)] { n.update(v); }
3076 assert_eq!(n.consecutive_above_mean().unwrap(), 3);
3077 }
3078
3079 #[test]
3082 fn test_minmax_above_threshold_fraction_none_for_empty() {
3083 assert!(norm(4).above_threshold_fraction(dec!(5)).is_none());
3084 }
3085
3086 #[test]
3087 fn test_minmax_above_threshold_fraction_correct() {
3088 let mut n = norm(4);
3089 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3090 let f = n.above_threshold_fraction(dec!(2)).unwrap();
3092 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
3093 }
3094
3095 #[test]
3096 fn test_minmax_below_threshold_fraction_none_for_empty() {
3097 assert!(norm(4).below_threshold_fraction(dec!(5)).is_none());
3098 }
3099
3100 #[test]
3101 fn test_minmax_below_threshold_fraction_correct() {
3102 let mut n = norm(4);
3103 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3104 let f = n.below_threshold_fraction(dec!(3)).unwrap();
3106 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
3107 }
3108
3109 #[test]
3112 fn test_minmax_lag_k_autocorrelation_none_for_zero_k() {
3113 let mut n = norm(5);
3114 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3115 assert!(n.lag_k_autocorrelation(0).is_none());
3116 }
3117
3118 #[test]
3119 fn test_minmax_lag_k_autocorrelation_none_when_k_gte_len() {
3120 let mut n = norm(3);
3121 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3122 assert!(n.lag_k_autocorrelation(3).is_none());
3123 }
3124
3125 #[test]
3126 fn test_minmax_lag_k_autocorrelation_positive_for_trend() {
3127 let mut n = norm(6);
3129 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6)] { n.update(v); }
3130 let ac = n.lag_k_autocorrelation(1).unwrap();
3131 assert!(ac > 0.0, "trending series should have positive AC, got {}", ac);
3132 }
3133
3134 #[test]
3137 fn test_minmax_half_life_estimate_none_for_fewer_than_3() {
3138 let mut n = norm(3);
3139 n.update(dec!(1)); n.update(dec!(2));
3140 assert!(n.half_life_estimate().is_none());
3141 }
3142
3143 #[test]
3144 fn test_minmax_half_life_estimate_some_for_mean_reverting() {
3145 let mut n = norm(6);
3147 for v in [dec!(10), dec!(5), dec!(10), dec!(5), dec!(10), dec!(5)] { n.update(v); }
3148 let _ = n.half_life_estimate();
3150 }
3151
3152 #[test]
3155 fn test_minmax_geometric_mean_none_for_empty() {
3156 assert!(norm(4).geometric_mean().is_none());
3157 }
3158
3159 #[test]
3160 fn test_minmax_geometric_mean_correct_for_powers_of_2() {
3161 let mut n = norm(4);
3163 for v in [dec!(1), dec!(2), dec!(4), dec!(8)] { n.update(v); }
3164 let gm = n.geometric_mean().unwrap();
3165 assert!((gm - 64.0f64.powf(0.25)).abs() < 1e-6, "got {}", gm);
3166 }
3167
3168 #[test]
3171 fn test_minmax_harmonic_mean_none_for_empty() {
3172 assert!(norm(4).harmonic_mean().is_none());
3173 }
3174
3175 #[test]
3176 fn test_minmax_harmonic_mean_none_when_any_zero() {
3177 let mut n = norm(2);
3178 n.update(dec!(0)); n.update(dec!(5));
3179 assert!(n.harmonic_mean().is_none());
3180 }
3181
3182 #[test]
3183 fn test_minmax_harmonic_mean_positive_for_positive_values() {
3184 let mut n = norm(4);
3185 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3186 let hm = n.harmonic_mean().unwrap();
3187 assert!(hm > 0.0 && hm < 4.0, "HM should be in (0, max), got {}", hm);
3188 }
3189
3190 #[test]
3193 fn test_minmax_range_normalized_value_none_for_empty() {
3194 assert!(norm(4).range_normalized_value(dec!(5)).is_none());
3195 }
3196
3197 #[test]
3198 fn test_minmax_range_normalized_value_zero_for_min() {
3199 let mut n = norm(4);
3200 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3201 let r = n.range_normalized_value(dec!(1)).unwrap();
3202 assert!((r - 0.0).abs() < 1e-9, "min value should normalize to 0, got {}", r);
3203 }
3204
3205 #[test]
3206 fn test_minmax_range_normalized_value_one_for_max() {
3207 let mut n = norm(4);
3208 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3209 let r = n.range_normalized_value(dec!(4)).unwrap();
3210 assert!((r - 1.0).abs() < 1e-9, "max value should normalize to 1, got {}", r);
3211 }
3212
3213 #[test]
3216 fn test_minmax_distance_from_median_none_for_empty() {
3217 assert!(norm(4).distance_from_median(dec!(5)).is_none());
3218 }
3219
3220 #[test]
3221 fn test_minmax_distance_from_median_zero_at_median() {
3222 let mut n = norm(5);
3223 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3224 let d = n.distance_from_median(dec!(3)).unwrap();
3226 assert!((d - 0.0).abs() < 1e-9, "distance from median should be 0, got {}", d);
3227 }
3228
3229 #[test]
3230 fn test_minmax_distance_from_median_positive_above() {
3231 let mut n = norm(5);
3232 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3233 let d = n.distance_from_median(dec!(5)).unwrap();
3234 assert!(d > 0.0, "value above median should give positive distance, got {}", d);
3235 }
3236
3237 #[test]
3238 fn test_minmax_momentum_none_for_single_value() {
3239 let mut n = norm(5);
3240 n.update(dec!(10));
3241 assert!(n.momentum().is_none());
3242 }
3243
3244 #[test]
3245 fn test_minmax_momentum_positive_for_rising_window() {
3246 let mut n = norm(3);
3247 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3248 let m = n.momentum().unwrap();
3249 assert!(m > 0.0, "rising window → positive momentum, got {}", m);
3250 }
3251
3252 #[test]
3253 fn test_minmax_value_rank_none_for_empty() {
3254 assert!(norm(4).value_rank(dec!(5)).is_none());
3255 }
3256
3257 #[test]
3258 fn test_minmax_value_rank_extremes() {
3259 let mut n = norm(4);
3260 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3261 let low = n.value_rank(dec!(0)).unwrap();
3263 assert!((low - 0.0).abs() < 1e-9, "got {}", low);
3264 let high = n.value_rank(dec!(5)).unwrap();
3266 assert!((high - 1.0).abs() < 1e-9, "got {}", high);
3267 }
3268
3269 #[test]
3270 fn test_minmax_coeff_of_variation_none_for_single_value() {
3271 let mut n = norm(5);
3272 n.update(dec!(10));
3273 assert!(n.coeff_of_variation().is_none());
3274 }
3275
3276 #[test]
3277 fn test_minmax_coeff_of_variation_positive_for_spread() {
3278 let mut n = norm(4);
3279 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
3280 let cv = n.coeff_of_variation().unwrap();
3281 assert!(cv > 0.0, "expected positive CV, got {}", cv);
3282 }
3283
3284 #[test]
3285 fn test_minmax_quantile_range_none_for_empty() {
3286 assert!(norm(4).quantile_range().is_none());
3287 }
3288
3289 #[test]
3290 fn test_minmax_quantile_range_non_negative() {
3291 let mut n = norm(5);
3292 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3293 let iqr = n.quantile_range().unwrap();
3294 assert!(iqr >= 0.0, "IQR should be non-negative, got {}", iqr);
3295 }
3296
3297 #[test]
3300 fn test_minmax_upper_quartile_none_for_empty() {
3301 assert!(norm(4).upper_quartile().is_none());
3302 }
3303
3304 #[test]
3305 fn test_minmax_lower_quartile_none_for_empty() {
3306 assert!(norm(4).lower_quartile().is_none());
3307 }
3308
3309 #[test]
3310 fn test_minmax_upper_ge_lower_quartile() {
3311 let mut n = norm(8);
3312 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6), dec!(7), dec!(8)] {
3313 n.update(v);
3314 }
3315 let q3 = n.upper_quartile().unwrap();
3316 let q1 = n.lower_quartile().unwrap();
3317 assert!(q3 >= q1, "Q3 ({}) should be >= Q1 ({})", q3, q1);
3318 }
3319
3320 #[test]
3323 fn test_minmax_sign_change_rate_none_for_fewer_than_3() {
3324 let mut n = norm(4);
3325 n.update(dec!(1));
3326 n.update(dec!(2));
3327 assert!(n.sign_change_rate().is_none());
3328 }
3329
3330 #[test]
3331 fn test_minmax_sign_change_rate_one_for_zigzag() {
3332 let mut n = norm(5);
3333 for v in [dec!(1), dec!(3), dec!(1), dec!(3), dec!(1)] { n.update(v); }
3335 let r = n.sign_change_rate().unwrap();
3336 assert!((r - 1.0).abs() < 1e-9, "zigzag should give 1.0, got {}", r);
3337 }
3338
3339 #[test]
3340 fn test_minmax_sign_change_rate_zero_for_monotone() {
3341 let mut n = norm(5);
3342 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3343 let r = n.sign_change_rate().unwrap();
3344 assert!((r - 0.0).abs() < 1e-9, "monotone should give 0.0, got {}", r);
3345 }
3346
3347 #[test]
3352 fn test_consecutive_below_mean_none_for_single_value() {
3353 let mut n = norm(5);
3354 n.update(dec!(10));
3355 assert!(n.consecutive_below_mean().is_none());
3356 }
3357
3358 #[test]
3359 fn test_consecutive_below_mean_zero_when_latest_above_mean() {
3360 let mut n = norm(5);
3361 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(100)] { n.update(v); }
3362 let c = n.consecutive_below_mean().unwrap();
3363 assert_eq!(c, 0, "latest above mean → streak=0, got {}", c);
3364 }
3365
3366 #[test]
3367 fn test_consecutive_below_mean_counts_trailing_below() {
3368 let mut n = norm(5);
3369 for v in [dec!(100), dec!(100), dec!(1), dec!(1), dec!(1)] { n.update(v); }
3370 let c = n.consecutive_below_mean().unwrap();
3371 assert!(c >= 3, "last 3 below mean → streak>=3, got {}", c);
3372 }
3373
3374 #[test]
3377 fn test_drift_rate_none_for_single_value() {
3378 let mut n = norm(5);
3379 n.update(dec!(10));
3380 assert!(n.drift_rate().is_none());
3381 }
3382
3383 #[test]
3384 fn test_drift_rate_positive_for_rising_series() {
3385 let mut n = norm(6);
3386 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6)] { n.update(v); }
3387 let d = n.drift_rate().unwrap();
3388 assert!(d > 0.0, "rising series → positive drift, got {}", d);
3389 }
3390
3391 #[test]
3392 fn test_drift_rate_negative_for_falling_series() {
3393 let mut n = norm(6);
3394 for v in [dec!(6), dec!(5), dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
3395 let d = n.drift_rate().unwrap();
3396 assert!(d < 0.0, "falling series → negative drift, got {}", d);
3397 }
3398
3399 #[test]
3402 fn test_peak_to_trough_ratio_none_for_empty() {
3403 assert!(norm(4).peak_to_trough_ratio().is_none());
3404 }
3405
3406 #[test]
3407 fn test_peak_to_trough_ratio_one_for_constant() {
3408 let mut n = norm(4);
3409 for v in [dec!(10), dec!(10), dec!(10), dec!(10)] { n.update(v); }
3410 let r = n.peak_to_trough_ratio().unwrap();
3411 assert!((r - 1.0).abs() < 1e-9, "constant → ratio=1, got {}", r);
3412 }
3413
3414 #[test]
3415 fn test_peak_to_trough_ratio_above_one_for_spread() {
3416 let mut n = norm(4);
3417 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
3418 let r = n.peak_to_trough_ratio().unwrap();
3419 assert!(r > 1.0, "spread → ratio>1, got {}", r);
3420 }
3421
3422 #[test]
3425 fn test_normalized_deviation_none_for_empty() {
3426 assert!(norm(4).normalized_deviation().is_none());
3427 }
3428
3429 #[test]
3430 fn test_normalized_deviation_none_for_constant() {
3431 let mut n = norm(4);
3432 for v in [dec!(5), dec!(5), dec!(5), dec!(5)] { n.update(v); }
3433 assert!(n.normalized_deviation().is_none());
3434 }
3435
3436 #[test]
3437 fn test_normalized_deviation_positive_for_latest_above_mean() {
3438 let mut n = norm(5);
3439 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(10)] { n.update(v); }
3440 let d = n.normalized_deviation().unwrap();
3441 assert!(d > 0.0, "latest above mean → positive deviation, got {}", d);
3442 }
3443
3444 #[test]
3447 fn test_window_cv_pct_none_for_single_value() {
3448 let mut n = norm(5);
3449 n.update(dec!(10));
3450 assert!(n.window_cv_pct().is_none());
3451 }
3452
3453 #[test]
3454 fn test_window_cv_pct_positive_for_varied_values() {
3455 let mut n = norm(4);
3456 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
3457 let cv = n.window_cv_pct().unwrap();
3458 assert!(cv > 0.0, "expected positive CV%, got {}", cv);
3459 }
3460
3461 #[test]
3464 fn test_latest_rank_pct_none_for_single_value() {
3465 let mut n = norm(5);
3466 n.update(dec!(10));
3467 assert!(n.latest_rank_pct().is_none());
3468 }
3469
3470 #[test]
3471 fn test_latest_rank_pct_one_for_max_value() {
3472 let mut n = norm(4);
3473 for v in [dec!(1), dec!(2), dec!(3), dec!(100)] { n.update(v); }
3474 let r = n.latest_rank_pct().unwrap();
3475 assert!((r - 1.0).abs() < 1e-9, "latest is max → rank=1, got {}", r);
3476 }
3477
3478 #[test]
3479 fn test_latest_rank_pct_zero_for_min_value() {
3480 let mut n = norm(4);
3481 for v in [dec!(10), dec!(20), dec!(30), dec!(1)] { n.update(v); }
3482 let r = n.latest_rank_pct().unwrap();
3483 assert!(r.abs() < 1e-9, "latest is min → rank=0, got {}", r);
3484 }
3485
3486 #[test]
3489 fn test_minmax_trimmed_mean_none_for_empty() {
3490 assert!(norm(4).trimmed_mean(0.1).is_none());
3491 }
3492
3493 #[test]
3494 fn test_minmax_trimmed_mean_equals_mean_at_zero_trim() {
3495 let mut n = norm(4);
3496 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
3497 let tm = n.trimmed_mean(0.0).unwrap();
3498 let m = n.mean().unwrap().to_f64().unwrap();
3499 assert!((tm - m).abs() < 1e-9, "0% trim should equal mean, got tm={} m={}", tm, m);
3500 }
3501
3502 #[test]
3503 fn test_minmax_trimmed_mean_reduces_effect_of_outlier() {
3504 let mut n = norm(5);
3505 for v in [dec!(10), dec!(10), dec!(10), dec!(10), dec!(1000)] { n.update(v); }
3506 let tm = n.trimmed_mean(0.2).unwrap();
3508 let m = n.mean().unwrap().to_f64().unwrap();
3509 assert!(tm < m, "trimmed mean should be less than mean when outlier is trimmed, tm={} m={}", tm, m);
3510 }
3511
3512 #[test]
3515 fn test_minmax_linear_trend_slope_none_for_single_value() {
3516 let mut n = norm(4);
3517 n.update(dec!(10));
3518 assert!(n.linear_trend_slope().is_none());
3519 }
3520
3521 #[test]
3522 fn test_minmax_linear_trend_slope_positive_for_rising() {
3523 let mut n = norm(4);
3524 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3525 let slope = n.linear_trend_slope().unwrap();
3526 assert!(slope > 0.0, "rising window → positive slope, got {}", slope);
3527 }
3528
3529 #[test]
3530 fn test_minmax_linear_trend_slope_negative_for_falling() {
3531 let mut n = norm(4);
3532 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
3533 let slope = n.linear_trend_slope().unwrap();
3534 assert!(slope < 0.0, "falling window → negative slope, got {}", slope);
3535 }
3536
3537 #[test]
3538 fn test_minmax_linear_trend_slope_zero_for_flat() {
3539 let mut n = norm(4);
3540 for v in [dec!(5), dec!(5), dec!(5), dec!(5)] { n.update(v); }
3541 let slope = n.linear_trend_slope().unwrap();
3542 assert!(slope.abs() < 1e-9, "flat window → slope=0, got {}", slope);
3543 }
3544
3545 #[test]
3548 fn test_minmax_variance_ratio_none_for_few_values() {
3549 let mut n = norm(3);
3550 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3551 assert!(n.variance_ratio().is_none());
3552 }
3553
3554 #[test]
3555 fn test_minmax_variance_ratio_gt_one_for_decreasing_vol() {
3556 let mut n = norm(6);
3557 for v in [dec!(1), dec!(10), dec!(1), dec!(5), dec!(6), dec!(5)] { n.update(v); }
3559 let r = n.variance_ratio().unwrap();
3560 assert!(r > 1.0, "first half more volatile → ratio > 1, got {}", r);
3561 }
3562
3563 #[test]
3566 fn test_minmax_z_score_trend_slope_none_for_single_value() {
3567 let mut n = norm(4);
3568 n.update(dec!(10));
3569 assert!(n.z_score_trend_slope().is_none());
3570 }
3571
3572 #[test]
3573 fn test_minmax_z_score_trend_slope_positive_for_rising() {
3574 let mut n = norm(5);
3575 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3576 let slope = n.z_score_trend_slope().unwrap();
3577 assert!(slope > 0.0, "rising window → positive z-score slope, got {}", slope);
3578 }
3579
3580 #[test]
3583 fn test_minmax_mean_absolute_change_none_for_single_value() {
3584 let mut n = norm(4);
3585 n.update(dec!(10));
3586 assert!(n.mean_absolute_change().is_none());
3587 }
3588
3589 #[test]
3590 fn test_minmax_mean_absolute_change_zero_for_constant() {
3591 let mut n = norm(4);
3592 for _ in 0..4 { n.update(dec!(5)); }
3593 let mac = n.mean_absolute_change().unwrap();
3594 assert!(mac.abs() < 1e-9, "constant window → MAC=0, got {}", mac);
3595 }
3596
3597 #[test]
3598 fn test_minmax_mean_absolute_change_positive_for_varying() {
3599 let mut n = norm(4);
3600 for v in [dec!(1), dec!(3), dec!(2), dec!(5)] { n.update(v); }
3601 let mac = n.mean_absolute_change().unwrap();
3602 assert!(mac > 0.0, "varying window → MAC > 0, got {}", mac);
3603 }
3604
3605 #[test]
3608 fn test_minmax_monotone_increase_fraction_none_for_single() {
3609 let mut n = norm(4);
3610 n.update(dec!(5));
3611 assert!(n.monotone_increase_fraction().is_none());
3612 }
3613
3614 #[test]
3615 fn test_minmax_monotone_increase_fraction_one_for_rising() {
3616 let mut n = norm(4);
3617 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3618 let f = n.monotone_increase_fraction().unwrap();
3619 assert!((f - 1.0).abs() < 1e-9, "all rising → fraction=1, got {}", f);
3620 }
3621
3622 #[test]
3623 fn test_minmax_abs_max_none_for_empty() {
3624 let n = norm(4);
3625 assert!(n.abs_max().is_none());
3626 }
3627
3628 #[test]
3629 fn test_minmax_abs_max_returns_max_absolute() {
3630 let mut n = norm(4);
3631 for v in [dec!(1), dec!(3), dec!(2)] { n.update(v); }
3632 assert_eq!(n.abs_max().unwrap(), dec!(3));
3633 }
3634
3635 #[test]
3636 fn test_minmax_max_count_none_for_empty() {
3637 let n = norm(4);
3638 assert!(n.max_count().is_none());
3639 }
3640
3641 #[test]
3642 fn test_minmax_max_count_correct() {
3643 let mut n = norm(4);
3644 for v in [dec!(1), dec!(5), dec!(3), dec!(5)] { n.update(v); }
3645 assert_eq!(n.max_count().unwrap(), 2);
3646 }
3647
3648 #[test]
3649 fn test_minmax_mean_ratio_none_for_single() {
3650 let mut n = norm(4);
3651 n.update(dec!(10));
3652 assert!(n.mean_ratio().is_none());
3653 }
3654
3655 #[test]
3658 fn test_minmax_exponential_weighted_mean_none_for_empty() {
3659 let n = norm(4);
3660 assert!(n.exponential_weighted_mean(0.5).is_none());
3661 }
3662
3663 #[test]
3664 fn test_minmax_exponential_weighted_mean_returns_value() {
3665 let mut n = norm(4);
3666 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3667 let ewm = n.exponential_weighted_mean(0.5).unwrap();
3668 assert!(ewm > 0.0, "EWM should be positive, got {}", ewm);
3669 }
3670
3671 #[test]
3672 fn test_minmax_peak_to_trough_none_for_empty() {
3673 let n = norm(4);
3674 assert!(n.peak_to_trough_ratio().is_none());
3675 }
3676
3677 #[test]
3678 fn test_minmax_peak_to_trough_correct() {
3679 let mut n = norm(4);
3680 for v in [dec!(2), dec!(4), dec!(1), dec!(8)] { n.update(v); }
3681 let r = n.peak_to_trough_ratio().unwrap();
3682 assert!((r - 8.0).abs() < 1e-9, "max=8, min=1 → ratio=8, got {}", r);
3683 }
3684
3685 #[test]
3686 fn test_minmax_second_moment_none_for_empty() {
3687 let n = norm(4);
3688 assert!(n.second_moment().is_none());
3689 }
3690
3691 #[test]
3692 fn test_minmax_second_moment_correct() {
3693 let mut n = norm(4);
3694 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3695 let m = n.second_moment().unwrap();
3697 assert!((m - 14.0 / 3.0).abs() < 1e-9, "second moment ≈ 4.667, got {}", m);
3698 }
3699
3700 #[test]
3701 fn test_minmax_range_over_mean_none_for_empty() {
3702 let n = norm(4);
3703 assert!(n.range_over_mean().is_none());
3704 }
3705
3706 #[test]
3707 fn test_minmax_range_over_mean_positive() {
3708 let mut n = norm(4);
3709 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3710 let r = n.range_over_mean().unwrap();
3711 assert!(r > 0.0, "range/mean should be positive, got {}", r);
3712 }
3713
3714 #[test]
3715 fn test_minmax_above_median_fraction_none_for_empty() {
3716 let n = norm(4);
3717 assert!(n.above_median_fraction().is_none());
3718 }
3719
3720 #[test]
3721 fn test_minmax_above_median_fraction_in_range() {
3722 let mut n = norm(4);
3723 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3724 let f = n.above_median_fraction().unwrap();
3725 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
3726 }
3727
3728 #[test]
3731 fn test_minmax_interquartile_mean_none_for_empty() {
3732 let n = norm(4);
3733 assert!(n.interquartile_mean().is_none());
3734 }
3735
3736 #[test]
3737 fn test_minmax_outlier_fraction_none_for_empty() {
3738 let n = norm(4);
3739 assert!(n.outlier_fraction(2.0).is_none());
3740 }
3741
3742 #[test]
3743 fn test_minmax_outlier_fraction_zero_for_constant() {
3744 let mut n = norm(4);
3745 for _ in 0..4 { n.update(dec!(5)); }
3746 let f = n.outlier_fraction(1.0).unwrap();
3747 assert!(f.abs() < 1e-9, "constant window → no outliers, got {}", f);
3748 }
3749
3750 #[test]
3751 fn test_minmax_sign_flip_count_none_for_single() {
3752 let mut n = norm(4);
3753 n.update(dec!(1));
3754 assert!(n.sign_flip_count().is_none());
3755 }
3756
3757 #[test]
3758 fn test_minmax_sign_flip_count_correct() {
3759 let mut n = norm(6);
3760 for v in [dec!(1), dec!(-1), dec!(1), dec!(-1)] { n.update(v); }
3761 let c = n.sign_flip_count().unwrap();
3762 assert_eq!(c, 3, "3 sign flips expected, got {}", c);
3763 }
3764
3765 #[test]
3766 fn test_minmax_rms_none_for_empty() {
3767 let n = norm(4);
3768 assert!(n.rms().is_none());
3769 }
3770
3771 #[test]
3772 fn test_minmax_rms_correct_for_unit_value() {
3773 let mut n = norm(4);
3774 for _ in 0..4 { n.update(dec!(1)); }
3775 let r = n.rms().unwrap();
3776 assert!((r - 1.0).abs() < 1e-9, "RMS of all-ones = 1.0, got {}", r);
3777 }
3778
3779 #[test]
3782 fn test_minmax_distinct_count_zero_for_empty() {
3783 let n = norm(4);
3784 assert_eq!(n.distinct_count(), 0);
3785 }
3786
3787 #[test]
3788 fn test_minmax_distinct_count_correct() {
3789 let mut n = norm(4);
3790 for v in [dec!(1), dec!(1), dec!(2), dec!(3)] { n.update(v); }
3791 assert_eq!(n.distinct_count(), 3);
3792 }
3793
3794 #[test]
3795 fn test_minmax_max_fraction_none_for_empty() {
3796 let n = norm(4);
3797 assert!(n.max_fraction().is_none());
3798 }
3799
3800 #[test]
3801 fn test_minmax_max_fraction_correct() {
3802 let mut n = norm(4);
3803 for v in [dec!(1), dec!(2), dec!(3), dec!(3)] { n.update(v); }
3804 let f = n.max_fraction().unwrap();
3805 assert!((f - 0.5).abs() < 1e-9, "2/4 are max → 0.5, got {}", f);
3807 }
3808
3809 #[test]
3810 fn test_minmax_latest_minus_mean_none_for_empty() {
3811 let n = norm(4);
3812 assert!(n.latest_minus_mean().is_none());
3813 }
3814
3815 #[test]
3816 fn test_minmax_latest_to_mean_ratio_none_for_empty() {
3817 let n = norm(4);
3818 assert!(n.latest_to_mean_ratio().is_none());
3819 }
3820
3821 #[test]
3822 fn test_minmax_latest_to_mean_ratio_one_for_constant() {
3823 let mut n = norm(4);
3824 for _ in 0..4 { n.update(dec!(5)); }
3825 let r = n.latest_to_mean_ratio().unwrap();
3826 assert!((r - 1.0).abs() < 1e-9, "latest=mean → ratio=1, got {}", r);
3827 }
3828
3829 #[test]
3832 fn test_minmax_below_mean_fraction_none_for_empty() {
3833 assert!(norm(4).below_mean_fraction().is_none());
3834 }
3835
3836 #[test]
3837 fn test_minmax_below_mean_fraction_symmetric_data() {
3838 let mut n = norm(4);
3839 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3840 let f = n.below_mean_fraction().unwrap();
3842 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
3843 }
3844
3845 #[test]
3846 fn test_minmax_tail_variance_none_for_small_window() {
3847 let mut n = norm(3);
3848 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3849 assert!(n.tail_variance().is_none());
3850 }
3851
3852 #[test]
3853 fn test_minmax_tail_variance_nonneg_for_varied_data() {
3854 let mut n = norm(6);
3855 for v in [dec!(1), dec!(2), dec!(5), dec!(6), dec!(9), dec!(10)] { n.update(v); }
3856 let tv = n.tail_variance().unwrap();
3857 assert!(tv >= 0.0, "tail variance should be non-negative, got {}", tv);
3858 }
3859
3860 #[test]
3863 fn test_minmax_new_max_count_zero_for_empty() {
3864 let n = norm(4);
3865 assert_eq!(n.new_max_count(), 0);
3866 }
3867
3868 #[test]
3869 fn test_minmax_new_max_count_all_rising() {
3870 let mut n = norm(4);
3871 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3872 assert_eq!(n.new_max_count(), 4, "each value is a new high");
3873 }
3874
3875 #[test]
3876 fn test_minmax_new_min_count_zero_for_empty() {
3877 let n = norm(4);
3878 assert_eq!(n.new_min_count(), 0);
3879 }
3880
3881 #[test]
3882 fn test_minmax_new_min_count_all_falling() {
3883 let mut n = norm(4);
3884 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
3885 assert_eq!(n.new_min_count(), 4, "each value is a new low");
3886 }
3887
3888 #[test]
3889 fn test_minmax_zero_fraction_none_for_empty() {
3890 let n = norm(4);
3891 assert!(n.zero_fraction().is_none());
3892 }
3893
3894 #[test]
3895 fn test_minmax_zero_fraction_zero_when_no_zeros() {
3896 let mut n = norm(4);
3897 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3898 let f = n.zero_fraction().unwrap();
3899 assert!(f.abs() < 1e-9, "no zeros → fraction=0, got {}", f);
3900 }
3901
3902 #[test]
3905 fn test_minmax_cumulative_sum_zero_for_empty() {
3906 let n = norm(4);
3907 assert_eq!(n.cumulative_sum(), rust_decimal::Decimal::ZERO);
3908 }
3909
3910 #[test]
3911 fn test_minmax_cumulative_sum_correct() {
3912 let mut n = norm(4);
3913 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3914 assert_eq!(n.cumulative_sum(), dec!(6));
3915 }
3916
3917 #[test]
3918 fn test_minmax_max_to_min_ratio_none_for_empty() {
3919 assert!(norm(4).max_to_min_ratio().is_none());
3920 }
3921
3922 #[test]
3923 fn test_minmax_max_to_min_ratio_one_for_constant() {
3924 let mut n = norm(4);
3925 for _ in 0..4 { n.update(dec!(5)); }
3926 let r = n.max_to_min_ratio().unwrap();
3927 assert!((r - 1.0).abs() < 1e-9, "constant window → ratio=1, got {}", r);
3928 }
3929
3930 #[test]
3933 fn test_minmax_above_midpoint_fraction_none_for_empty() {
3934 assert!(norm(4).above_midpoint_fraction().is_none());
3935 }
3936
3937 #[test]
3938 fn test_minmax_above_midpoint_fraction_half_for_symmetric() {
3939 let mut n = norm(4);
3940 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3941 let f = n.above_midpoint_fraction().unwrap();
3943 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
3944 }
3945
3946 #[test]
3947 fn test_minmax_span_utilization_none_for_empty() {
3948 assert!(norm(4).span_utilization().is_none());
3949 }
3950
3951 #[test]
3952 fn test_minmax_span_utilization_one_for_latest_at_max() {
3953 let mut n = norm(4);
3954 for v in [dec!(1), dec!(5), dec!(3), dec!(10)] { n.update(v); }
3955 let u = n.span_utilization().unwrap();
3957 assert!((u - 1.0).abs() < 1e-9, "latest=max → 1.0, got {}", u);
3958 }
3959
3960 #[test]
3961 fn test_minmax_positive_fraction_none_for_empty() {
3962 assert!(norm(4).positive_fraction().is_none());
3963 }
3964
3965 #[test]
3966 fn test_minmax_positive_fraction_half() {
3967 let mut n = norm(4);
3968 for v in [dec!(-1), dec!(0), dec!(1), dec!(2)] { n.update(v); }
3969 let f = n.positive_fraction().unwrap();
3971 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
3972 }
3973}
3974
3975pub struct ZScoreNormalizer {
3998 window_size: usize,
3999 window: VecDeque<Decimal>,
4000 sum: Decimal,
4001 sum_sq: Decimal,
4002}
4003
4004impl ZScoreNormalizer {
4005 pub fn new(window_size: usize) -> Result<Self, StreamError> {
4011 if window_size == 0 {
4012 return Err(StreamError::ConfigError {
4013 reason: "ZScoreNormalizer window_size must be > 0".into(),
4014 });
4015 }
4016 Ok(Self {
4017 window_size,
4018 window: VecDeque::with_capacity(window_size),
4019 sum: Decimal::ZERO,
4020 sum_sq: Decimal::ZERO,
4021 })
4022 }
4023
4024 pub fn update(&mut self, value: Decimal) {
4030 if self.window.len() == self.window_size {
4031 let evicted = self.window.pop_front().unwrap_or(Decimal::ZERO);
4032 self.sum -= evicted;
4033 self.sum_sq -= evicted * evicted;
4034 }
4035 self.window.push_back(value);
4036 self.sum += value;
4037 self.sum_sq += value * value;
4038 }
4039
4040 #[must_use = "z-score is returned; ignoring it loses the normalized value"]
4052 pub fn normalize(&self, value: Decimal) -> Result<f64, StreamError> {
4053 let n = self.window.len();
4054 if n == 0 {
4055 return Err(StreamError::NormalizationError {
4056 reason: "window is empty; call update() before normalize()".into(),
4057 });
4058 }
4059 if n < 2 {
4060 return Ok(0.0);
4061 }
4062 let std_dev = self.std_dev().unwrap_or(0.0);
4063 if std_dev < f64::EPSILON {
4064 return Ok(0.0);
4065 }
4066 let mean = self.mean().ok_or_else(|| StreamError::NormalizationError {
4067 reason: "mean unavailable".into(),
4068 })?;
4069 let diff = value - mean;
4070 let diff_f64 = diff.to_f64().ok_or_else(|| StreamError::NormalizationError {
4071 reason: "Decimal-to-f64 conversion failed for diff".into(),
4072 })?;
4073 Ok(diff_f64 / std_dev)
4074 }
4075
4076 pub fn mean(&self) -> Option<Decimal> {
4078 if self.window.is_empty() {
4079 return None;
4080 }
4081 let n = Decimal::from(self.window.len() as u64);
4082 Some(self.sum / n)
4083 }
4084
4085 pub fn std_dev(&self) -> Option<f64> {
4091 let n = self.window.len();
4092 if n == 0 {
4093 return None;
4094 }
4095 if n < 2 {
4096 return Some(0.0);
4097 }
4098 self.variance_f64().map(f64::sqrt)
4099 }
4100
4101 pub fn reset(&mut self) {
4103 self.window.clear();
4104 self.sum = Decimal::ZERO;
4105 self.sum_sq = Decimal::ZERO;
4106 }
4107
4108 pub fn len(&self) -> usize {
4110 self.window.len()
4111 }
4112
4113 pub fn is_empty(&self) -> bool {
4115 self.window.is_empty()
4116 }
4117
4118 pub fn window_size(&self) -> usize {
4120 self.window_size
4121 }
4122
4123 pub fn is_full(&self) -> bool {
4128 self.window.len() == self.window_size
4129 }
4130
4131 pub fn sum(&self) -> Option<Decimal> {
4136 if self.window.is_empty() {
4137 return None;
4138 }
4139 Some(self.sum)
4140 }
4141
4142 pub fn variance(&self) -> Option<Decimal> {
4147 let n = self.window.len();
4148 if n < 2 {
4149 return None;
4150 }
4151 let n_dec = Decimal::from(n as u64);
4152 let mean = self.sum / n_dec;
4153 let v = (self.sum_sq / n_dec) - mean * mean;
4154 Some(if v < Decimal::ZERO { Decimal::ZERO } else { v })
4155 }
4156
4157 pub fn std_dev_f64(&self) -> Option<f64> {
4161 self.variance_f64().map(|v| v.sqrt())
4162 }
4163
4164 pub fn variance_f64(&self) -> Option<f64> {
4168 use rust_decimal::prelude::ToPrimitive;
4169 self.variance()?.to_f64()
4170 }
4171
4172 pub fn normalize_batch(
4182 &mut self,
4183 values: &[Decimal],
4184 ) -> Result<Vec<f64>, StreamError> {
4185 values
4186 .iter()
4187 .map(|&v| {
4188 self.update(v);
4189 self.normalize(v)
4190 })
4191 .collect()
4192 }
4193
4194 pub fn is_outlier(&self, value: Decimal, z_threshold: f64) -> bool {
4199 use rust_decimal::prelude::ToPrimitive;
4200 if self.window.len() < 2 {
4201 return false;
4202 }
4203 let sd = self.std_dev().unwrap_or(0.0);
4204 if sd == 0.0 {
4205 return false;
4206 }
4207 let Some(mean_f64) = self.mean().and_then(|m| m.to_f64()) else { return false; };
4208 let val_f64 = value.to_f64().unwrap_or(mean_f64);
4209 ((val_f64 - mean_f64) / sd).abs() > z_threshold
4210 }
4211
4212 pub fn percentile_rank(&self, value: Decimal) -> Option<f64> {
4216 if self.window.is_empty() {
4217 return None;
4218 }
4219 let count = self.window.iter().filter(|&&v| v <= value).count();
4220 Some(count as f64 / self.window.len() as f64)
4221 }
4222
4223 pub fn running_min(&self) -> Option<Decimal> {
4227 self.window.iter().copied().reduce(Decimal::min)
4228 }
4229
4230 pub fn running_max(&self) -> Option<Decimal> {
4234 self.window.iter().copied().reduce(Decimal::max)
4235 }
4236
4237 pub fn window_range(&self) -> Option<Decimal> {
4241 let min = self.running_min()?;
4242 let max = self.running_max()?;
4243 Some(max - min)
4244 }
4245
4246 pub fn coefficient_of_variation(&self) -> Option<f64> {
4251 let mean = self.mean()?;
4252 if mean.is_zero() {
4253 return None;
4254 }
4255 let std_dev = self.std_dev()?;
4256 let mean_f = mean.abs().to_f64()?;
4257 Some(std_dev / mean_f)
4258 }
4259
4260 pub fn sample_variance(&self) -> Option<f64> {
4267 let sd = self.std_dev()?;
4268 Some(sd * sd)
4269 }
4270
4271 pub fn window_mean_f64(&self) -> Option<f64> {
4276 use rust_decimal::prelude::ToPrimitive;
4277 self.mean()?.to_f64()
4278 }
4279
4280 pub fn is_near_mean(&self, value: Decimal, sigma_tolerance: f64) -> bool {
4286 if self.window.len() < 2 {
4288 return false;
4289 }
4290 let Some(std_dev) = self.std_dev() else { return false; };
4291 if std_dev == 0.0 {
4292 return true;
4293 }
4294 let Some(mean) = self.mean() else { return false; };
4295 use rust_decimal::prelude::ToPrimitive;
4296 let diff = (value - mean).abs().to_f64().unwrap_or(f64::MAX);
4297 diff / std_dev <= sigma_tolerance
4298 }
4299
4300 pub fn window_sum(&self) -> Decimal {
4304 self.sum
4305 }
4306
4307 pub fn window_sum_f64(&self) -> f64 {
4311 use rust_decimal::prelude::ToPrimitive;
4312 self.sum.to_f64().unwrap_or(0.0)
4313 }
4314
4315 pub fn window_max_f64(&self) -> Option<f64> {
4319 use rust_decimal::prelude::ToPrimitive;
4320 self.running_max()?.to_f64()
4321 }
4322
4323 pub fn window_min_f64(&self) -> Option<f64> {
4327 use rust_decimal::prelude::ToPrimitive;
4328 self.running_min()?.to_f64()
4329 }
4330
4331 pub fn window_span_f64(&self) -> Option<f64> {
4335 use rust_decimal::prelude::ToPrimitive;
4336 self.window_range()?.to_f64()
4337 }
4338
4339 pub fn kurtosis(&self) -> Option<f64> {
4345 use rust_decimal::prelude::ToPrimitive;
4346 let n = self.window.len();
4347 if n < 4 {
4348 return None;
4349 }
4350 let n_f = n as f64;
4351 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4352 if vals.len() < n {
4353 return None;
4354 }
4355 let mean = vals.iter().sum::<f64>() / n_f;
4356 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
4357 let std_dev = variance.sqrt();
4358 if std_dev == 0.0 {
4359 return None;
4360 }
4361 let kurt = vals.iter().map(|v| ((v - mean) / std_dev).powi(4)).sum::<f64>() / n_f - 3.0;
4362 Some(kurt)
4363 }
4364
4365 pub fn skewness(&self) -> Option<f64> {
4371 use rust_decimal::prelude::ToPrimitive;
4372 let n = self.window.len();
4373 if n < 3 {
4374 return None;
4375 }
4376 let n_f = n as f64;
4377 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4378 if vals.len() < n {
4379 return None;
4380 }
4381 let mean = vals.iter().sum::<f64>() / n_f;
4382 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
4383 let std_dev = variance.sqrt();
4384 if std_dev == 0.0 {
4385 return None;
4386 }
4387 let skew = vals.iter().map(|v| ((v - mean) / std_dev).powi(3)).sum::<f64>() / n_f;
4388 Some(skew)
4389 }
4390
4391 pub fn is_extreme(&self, value: Decimal, sigma: f64) -> bool {
4396 self.normalize(value).ok().map_or(false, |z| z.abs() > sigma)
4397 }
4398
4399 pub fn latest(&self) -> Option<Decimal> {
4401 self.window.back().copied()
4402 }
4403
4404 pub fn median(&self) -> Option<Decimal> {
4406 if self.window.is_empty() { return None; }
4407 let mut vals: Vec<Decimal> = self.window.iter().copied().collect();
4408 vals.sort();
4409 let mid = vals.len() / 2;
4410 if vals.len() % 2 == 0 {
4411 Some((vals[mid - 1] + vals[mid]) / Decimal::TWO)
4412 } else {
4413 Some(vals[mid])
4414 }
4415 }
4416
4417 pub fn percentile(&self, value: Decimal) -> Option<f64> {
4421 self.percentile_rank(value)
4422 }
4423
4424 pub fn interquartile_range(&self) -> Option<Decimal> {
4429 let n = self.window.len();
4430 if n < 4 {
4431 return None;
4432 }
4433 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
4434 sorted.sort();
4435 let q1_idx = n / 4;
4436 let q3_idx = 3 * n / 4;
4437 Some(sorted[q3_idx] - sorted[q1_idx])
4438 }
4439
4440 pub fn ema_z_score(value: Decimal, alpha: f64, ema_mean: &mut f64, ema_var: &mut f64) -> Option<f64> {
4447 use rust_decimal::prelude::ToPrimitive;
4448 let v = value.to_f64()?;
4449 let delta = v - *ema_mean;
4450 *ema_mean += alpha * delta;
4451 *ema_var = (1.0 - alpha) * (*ema_var + alpha * delta * delta);
4452 let std = ema_var.sqrt();
4453 if std == 0.0 { return None; }
4454 Some((v - *ema_mean) / std)
4455 }
4456
4457 pub fn z_score_of_latest(&self) -> Option<f64> {
4461 let latest = self.latest()?;
4462 self.normalize(latest).ok()
4463 }
4464
4465 pub fn ema_of_z_scores(&self, alpha: f64) -> Option<f64> {
4470 let n = self.window.len();
4471 if n < 2 {
4472 return None;
4473 }
4474 let mut ema: Option<f64> = None;
4475 for &value in &self.window {
4476 if let Ok(z) = self.normalize(value) {
4477 ema = Some(match ema {
4478 None => z,
4479 Some(prev) => alpha * z + (1.0 - alpha) * prev,
4480 });
4481 }
4482 }
4483 ema
4484 }
4485
4486 pub fn add_observation(&mut self, value: Decimal) -> &mut Self {
4488 self.update(value);
4489 self
4490 }
4491
4492 pub fn deviation_from_mean(&self, value: Decimal) -> Option<f64> {
4496 use rust_decimal::prelude::ToPrimitive;
4497 let mean = self.mean()?.to_f64()?;
4498 value.to_f64().map(|v| v - mean)
4499 }
4500
4501 pub fn trim_outliers(&self, sigma: f64) -> Vec<Decimal> {
4506 use rust_decimal::prelude::ToPrimitive;
4507 if self.window.is_empty() { return vec![]; }
4508 let Some(mean) = self.mean() else { return vec![]; };
4509 let std = match self.std_dev() {
4510 Some(s) if s > 0.0 => s,
4511 _ => return self.window.iter().copied().collect(),
4512 };
4513 let Some(mean_f64) = mean.to_f64() else { return vec![]; };
4514 self.window.iter().copied()
4515 .filter(|v| {
4516 v.to_f64().map_or(false, |vf| ((vf - mean_f64) / std).abs() <= sigma)
4517 })
4518 .collect()
4519 }
4520
4521 pub fn rolling_zscore_batch(&mut self, values: &[Decimal]) -> Vec<Option<f64>> {
4527 values.iter().map(|&v| {
4528 self.update(v);
4529 self.normalize(v).ok()
4530 }).collect()
4531 }
4532
4533 pub fn rolling_mean_change(&self) -> Option<f64> {
4539 let n = self.window.len();
4540 if n < 2 {
4541 return None;
4542 }
4543 let mid = n / 2;
4544 let first: Decimal = self.window.iter().take(mid).copied().sum::<Decimal>()
4545 / Decimal::from(mid as u64);
4546 let second: Decimal = self.window.iter().skip(mid).copied().sum::<Decimal>()
4547 / Decimal::from((n - mid) as u64);
4548 (second - first).to_f64()
4549 }
4550
4551 pub fn count_positive_z_scores(&self) -> usize {
4555 self.window
4556 .iter()
4557 .filter(|&&v| self.normalize(v).map_or(false, |z| z > 0.0))
4558 .count()
4559 }
4560
4561 pub fn is_mean_stable(&self, threshold: f64) -> bool {
4566 self.rolling_mean_change().map_or(false, |c| c.abs() < threshold)
4567 }
4568
4569 pub fn above_threshold_count(&self, z_threshold: f64) -> usize {
4573 self.window
4574 .iter()
4575 .filter(|&&v| {
4576 self.normalize(v)
4577 .map_or(false, |z| z.abs() > z_threshold)
4578 })
4579 .count()
4580 }
4581
4582 pub fn mad(&self) -> Option<Decimal> {
4587 let med = self.median()?;
4588 let mut deviations: Vec<Decimal> = self.window.iter().map(|&x| (x - med).abs()).collect();
4589 deviations.sort();
4590 let n = deviations.len();
4591 if n == 0 { return None; }
4592 let mid = n / 2;
4593 if n % 2 == 0 {
4594 Some((deviations[mid - 1] + deviations[mid]) / Decimal::TWO)
4595 } else {
4596 Some(deviations[mid])
4597 }
4598 }
4599
4600 pub fn robust_z_score(&self, value: Decimal) -> Option<f64> {
4605 use rust_decimal::prelude::ToPrimitive;
4606 let med = self.median()?;
4607 let mad = self.mad()?;
4608 if mad.is_zero() { return None; }
4609 ((value - med) / mad).to_f64()
4610 }
4611
4612 pub fn count_above(&self, threshold: Decimal) -> usize {
4614 self.window.iter().filter(|&&v| v > threshold).count()
4615 }
4616
4617 pub fn count_below(&self, threshold: Decimal) -> usize {
4619 self.window.iter().filter(|&&v| v < threshold).count()
4620 }
4621
4622 pub fn percentile_value(&self, p: f64) -> Option<Decimal> {
4627 if self.window.is_empty() {
4628 return None;
4629 }
4630 let p = p.clamp(0.0, 1.0);
4631 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
4632 sorted.sort();
4633 let n = sorted.len();
4634 if n == 1 {
4635 return Some(sorted[0]);
4636 }
4637 let idx = p * (n - 1) as f64;
4638 let lo = idx.floor() as usize;
4639 let hi = idx.ceil() as usize;
4640 if lo == hi {
4641 Some(sorted[lo])
4642 } else {
4643 let frac = Decimal::try_from(idx - lo as f64).ok()?;
4644 Some(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
4645 }
4646 }
4647
4648 pub fn ewma(&self, alpha: f64) -> Option<f64> {
4654 use rust_decimal::prelude::ToPrimitive;
4655 let alpha = alpha.clamp(1e-9, 1.0);
4656 let mut iter = self.window.iter();
4657 let first = iter.next()?.to_f64()?;
4658 let result = iter.fold(first, |acc, &v| {
4659 let vf = v.to_f64().unwrap_or(acc);
4660 alpha * vf + (1.0 - alpha) * acc
4661 });
4662 Some(result)
4663 }
4664
4665 pub fn midpoint(&self) -> Option<Decimal> {
4669 let lo = self.running_min()?;
4670 let hi = self.running_max()?;
4671 Some((lo + hi) / Decimal::from(2u64))
4672 }
4673
4674 pub fn clamp_to_window(&self, value: Decimal) -> Decimal {
4678 match (self.running_min(), self.running_max()) {
4679 (Some(lo), Some(hi)) => value.max(lo).min(hi),
4680 _ => value,
4681 }
4682 }
4683
4684 pub fn fraction_above_mid(&self) -> Option<f64> {
4689 let lo = self.running_min()?;
4690 let hi = self.running_max()?;
4691 if lo == hi {
4692 return None;
4693 }
4694 let mid = (lo + hi) / Decimal::from(2u64);
4695 let above = self.window.iter().filter(|&&v| v > mid).count();
4696 Some(above as f64 / self.window.len() as f64)
4697 }
4698
4699 pub fn normalized_range(&self) -> Option<f64> {
4704 use rust_decimal::prelude::ToPrimitive;
4705 let span = self.window_range()?;
4706 let mean = self.mean()?;
4707 if mean.is_zero() {
4708 return None;
4709 }
4710 (span / mean).to_f64()
4711 }
4712
4713 pub fn min_max(&self) -> Option<(Decimal, Decimal)> {
4717 Some((self.running_min()?, self.running_max()?))
4718 }
4719
4720 pub fn values(&self) -> Vec<Decimal> {
4722 self.window.iter().copied().collect()
4723 }
4724
4725 pub fn above_zero_fraction(&self) -> Option<f64> {
4729 if self.window.is_empty() {
4730 return None;
4731 }
4732 let above = self.window.iter().filter(|&&v| v > Decimal::ZERO).count();
4733 Some(above as f64 / self.window.len() as f64)
4734 }
4735
4736 pub fn z_score_opt(&self, value: Decimal) -> Option<f64> {
4742 self.normalize(value).ok()
4743 }
4744
4745 pub fn is_stable(&self, z_threshold: f64) -> bool {
4750 self.z_score_of_latest()
4751 .map_or(false, |z| z.abs() <= z_threshold)
4752 }
4753
4754 pub fn fraction_above(&self, threshold: Decimal) -> Option<f64> {
4758 if self.window.is_empty() {
4759 return None;
4760 }
4761 Some(self.count_above(threshold) as f64 / self.window.len() as f64)
4762 }
4763
4764 pub fn fraction_below(&self, threshold: Decimal) -> Option<f64> {
4768 if self.window.is_empty() {
4769 return None;
4770 }
4771 Some(self.count_below(threshold) as f64 / self.window.len() as f64)
4772 }
4773
4774 pub fn window_values_above(&self, threshold: Decimal) -> Vec<Decimal> {
4776 self.window.iter().copied().filter(|&v| v > threshold).collect()
4777 }
4778
4779 pub fn window_values_below(&self, threshold: Decimal) -> Vec<Decimal> {
4781 self.window.iter().copied().filter(|&v| v < threshold).collect()
4782 }
4783
4784 pub fn count_equal(&self, value: Decimal) -> usize {
4786 self.window.iter().filter(|&&v| v == value).count()
4787 }
4788
4789 pub fn rolling_range(&self) -> Option<Decimal> {
4793 let lo = self.running_min()?;
4794 let hi = self.running_max()?;
4795 Some(hi - lo)
4796 }
4797
4798 pub fn autocorrelation_lag1(&self) -> Option<f64> {
4802 use rust_decimal::prelude::ToPrimitive;
4803 let n = self.window.len();
4804 if n < 2 {
4805 return None;
4806 }
4807 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4808 if vals.len() < 2 {
4809 return None;
4810 }
4811 let mean = vals.iter().sum::<f64>() / vals.len() as f64;
4812 let var: f64 = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / vals.len() as f64;
4813 if var == 0.0 {
4814 return None;
4815 }
4816 let cov: f64 = vals.windows(2).map(|w| (w[0] - mean) * (w[1] - mean)).sum::<f64>()
4817 / (vals.len() - 1) as f64;
4818 Some(cov / var)
4819 }
4820
4821 pub fn trend_consistency(&self) -> Option<f64> {
4825 let n = self.window.len();
4826 if n < 2 {
4827 return None;
4828 }
4829 let up = self.window.iter().collect::<Vec<_>>().windows(2)
4830 .filter(|w| w[1] > w[0]).count();
4831 Some(up as f64 / (n - 1) as f64)
4832 }
4833
4834 pub fn mean_absolute_deviation(&self) -> Option<f64> {
4838 use rust_decimal::prelude::ToPrimitive;
4839 if self.window.is_empty() {
4840 return None;
4841 }
4842 let n = self.window.len();
4843 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4844 let mean = vals.iter().sum::<f64>() / n as f64;
4845 let mad = vals.iter().map(|v| (v - mean).abs()).sum::<f64>() / n as f64;
4846 Some(mad)
4847 }
4848
4849 pub fn percentile_of_latest(&self) -> Option<f64> {
4854 let latest = self.latest()?;
4855 self.percentile(latest)
4856 }
4857
4858 pub fn tail_ratio(&self) -> Option<f64> {
4864 use rust_decimal::prelude::ToPrimitive;
4865 let max = self.running_max()?;
4866 let p75 = self.percentile_value(0.75)?;
4867 if p75.is_zero() {
4868 return None;
4869 }
4870 (max / p75).to_f64()
4871 }
4872
4873 pub fn z_score_of_min(&self) -> Option<f64> {
4877 let min = self.running_min()?;
4878 self.z_score_opt(min)
4879 }
4880
4881 pub fn z_score_of_max(&self) -> Option<f64> {
4885 let max = self.running_max()?;
4886 self.z_score_opt(max)
4887 }
4888
4889 pub fn window_entropy(&self) -> Option<f64> {
4895 if self.window.is_empty() {
4896 return None;
4897 }
4898 let n = self.window.len() as f64;
4899 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
4900 for v in &self.window {
4901 *counts.entry(v.to_string()).or_insert(0) += 1;
4902 }
4903 let entropy: f64 = counts.values().map(|&c| {
4904 let p = c as f64 / n;
4905 -p * p.ln()
4906 }).sum();
4907 Some(entropy)
4908 }
4909
4910 pub fn normalized_std_dev(&self) -> Option<f64> {
4912 self.coefficient_of_variation()
4913 }
4914
4915 pub fn value_above_mean_count(&self) -> Option<usize> {
4919 let mean = self.mean()?;
4920 Some(self.window.iter().filter(|&&v| v > mean).count())
4921 }
4922
4923 pub fn consecutive_above_mean(&self) -> Option<usize> {
4927 let mean = self.mean()?;
4928 let mut max_run = 0usize;
4929 let mut current = 0usize;
4930 for &v in &self.window {
4931 if v > mean {
4932 current += 1;
4933 if current > max_run {
4934 max_run = current;
4935 }
4936 } else {
4937 current = 0;
4938 }
4939 }
4940 Some(max_run)
4941 }
4942
4943 pub fn above_threshold_fraction(&self, threshold: Decimal) -> Option<f64> {
4947 if self.window.is_empty() {
4948 return None;
4949 }
4950 let count = self.window.iter().filter(|&&v| v > threshold).count();
4951 Some(count as f64 / self.window.len() as f64)
4952 }
4953
4954 pub fn below_threshold_fraction(&self, threshold: Decimal) -> Option<f64> {
4958 if self.window.is_empty() {
4959 return None;
4960 }
4961 let count = self.window.iter().filter(|&&v| v < threshold).count();
4962 Some(count as f64 / self.window.len() as f64)
4963 }
4964
4965 pub fn lag_k_autocorrelation(&self, k: usize) -> Option<f64> {
4969 use rust_decimal::prelude::ToPrimitive;
4970 let n = self.window.len();
4971 if k == 0 || k >= n {
4972 return None;
4973 }
4974 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4975 if vals.len() != n {
4976 return None;
4977 }
4978 let mean = vals.iter().sum::<f64>() / n as f64;
4979 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
4980 if var == 0.0 {
4981 return None;
4982 }
4983 let m = n - k;
4984 let cov: f64 = (0..m).map(|i| (vals[i] - mean) * (vals[i + k] - mean)).sum::<f64>() / m as f64;
4985 Some(cov / var)
4986 }
4987
4988 pub fn half_life_estimate(&self) -> Option<f64> {
4994 use rust_decimal::prelude::ToPrimitive;
4995 let n = self.window.len();
4996 if n < 3 {
4997 return None;
4998 }
4999 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5000 if vals.len() != n {
5001 return None;
5002 }
5003 let diffs: Vec<f64> = vals.windows(2).map(|w| w[1] - w[0]).collect();
5004 let lagged: Vec<f64> = vals[..n - 1].to_vec();
5005 let nf = diffs.len() as f64;
5006 let mean_l = lagged.iter().sum::<f64>() / nf;
5007 let mean_d = diffs.iter().sum::<f64>() / nf;
5008 let cov: f64 = lagged.iter().zip(diffs.iter()).map(|(l, d)| (l - mean_l) * (d - mean_d)).sum::<f64>();
5009 let var: f64 = lagged.iter().map(|l| (l - mean_l).powi(2)).sum::<f64>();
5010 if var == 0.0 {
5011 return None;
5012 }
5013 let beta = cov / var;
5014 if beta >= 0.0 {
5015 return None;
5016 }
5017 let lambda = (1.0 + beta).abs().ln();
5018 if lambda == 0.0 {
5019 return None;
5020 }
5021 Some(-std::f64::consts::LN_2 / lambda)
5022 }
5023
5024 pub fn geometric_mean(&self) -> Option<f64> {
5029 use rust_decimal::prelude::ToPrimitive;
5030 if self.window.is_empty() {
5031 return None;
5032 }
5033 let logs: Vec<f64> = self.window.iter()
5034 .filter_map(|v| v.to_f64())
5035 .filter_map(|f| if f > 0.0 { Some(f.ln()) } else { None })
5036 .collect();
5037 if logs.len() != self.window.len() {
5038 return None;
5039 }
5040 Some((logs.iter().sum::<f64>() / logs.len() as f64).exp())
5041 }
5042
5043 pub fn harmonic_mean(&self) -> Option<f64> {
5048 use rust_decimal::prelude::ToPrimitive;
5049 if self.window.is_empty() {
5050 return None;
5051 }
5052 let reciprocals: Vec<f64> = self.window.iter()
5053 .filter_map(|v| v.to_f64())
5054 .filter_map(|f| if f != 0.0 { Some(1.0 / f) } else { None })
5055 .collect();
5056 if reciprocals.len() != self.window.len() {
5057 return None;
5058 }
5059 let n = reciprocals.len() as f64;
5060 Some(n / reciprocals.iter().sum::<f64>())
5061 }
5062
5063 pub fn range_normalized_value(&self, value: Decimal) -> Option<f64> {
5067 use rust_decimal::prelude::ToPrimitive;
5068 let min = self.running_min()?;
5069 let max = self.running_max()?;
5070 let range = max - min;
5071 if range.is_zero() {
5072 return None;
5073 }
5074 ((value - min) / range).to_f64()
5075 }
5076
5077 pub fn distance_from_median(&self, value: Decimal) -> Option<f64> {
5081 use rust_decimal::prelude::ToPrimitive;
5082 let med = self.median()?;
5083 (value - med).to_f64()
5084 }
5085
5086 pub fn momentum(&self) -> Option<f64> {
5091 use rust_decimal::prelude::ToPrimitive;
5092 if self.window.len() < 2 {
5093 return None;
5094 }
5095 let oldest = *self.window.front()?;
5096 let latest = *self.window.back()?;
5097 (latest - oldest).to_f64()
5098 }
5099
5100 pub fn value_rank(&self, value: Decimal) -> Option<f64> {
5105 if self.window.is_empty() {
5106 return None;
5107 }
5108 let n = self.window.len();
5109 let below = self.window.iter().filter(|&&v| v < value).count();
5110 Some(below as f64 / n as f64)
5111 }
5112
5113 pub fn coeff_of_variation(&self) -> Option<f64> {
5118 use rust_decimal::prelude::ToPrimitive;
5119 let n = self.window.len();
5120 if n < 2 {
5121 return None;
5122 }
5123 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5124 if vals.len() < 2 {
5125 return None;
5126 }
5127 let nf = vals.len() as f64;
5128 let mean = vals.iter().sum::<f64>() / nf;
5129 if mean == 0.0 {
5130 return None;
5131 }
5132 let std_dev = (vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (nf - 1.0)).sqrt();
5133 Some(std_dev / mean.abs())
5134 }
5135
5136 pub fn quantile_range(&self) -> Option<f64> {
5140 use rust_decimal::prelude::ToPrimitive;
5141 let q3 = self.percentile_value(0.75)?;
5142 let q1 = self.percentile_value(0.25)?;
5143 (q3 - q1).to_f64()
5144 }
5145
5146 pub fn upper_quartile(&self) -> Option<Decimal> {
5150 self.percentile_value(0.75)
5151 }
5152
5153 pub fn lower_quartile(&self) -> Option<Decimal> {
5157 self.percentile_value(0.25)
5158 }
5159
5160 pub fn sign_change_rate(&self) -> Option<f64> {
5166 let n = self.window.len();
5167 if n < 3 {
5168 return None;
5169 }
5170 let vals: Vec<&Decimal> = self.window.iter().collect();
5171 let diffs: Vec<i32> = vals
5172 .windows(2)
5173 .map(|w| {
5174 if w[1] > w[0] { 1 } else if w[1] < w[0] { -1 } else { 0 }
5175 })
5176 .collect();
5177 let total_pairs = (diffs.len() - 1) as f64;
5178 if total_pairs == 0.0 {
5179 return None;
5180 }
5181 let changes = diffs
5182 .windows(2)
5183 .filter(|w| w[0] != 0 && w[1] != 0 && w[0] != w[1])
5184 .count();
5185 Some(changes as f64 / total_pairs)
5186 }
5187
5188 pub fn trimmed_mean(&self, p: f64) -> Option<f64> {
5196 use rust_decimal::prelude::ToPrimitive;
5197 if self.window.is_empty() {
5198 return None;
5199 }
5200 let p = p.clamp(0.0, 0.499);
5201 let mut sorted: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5202 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
5203 let n = sorted.len();
5204 let trim = (n as f64 * p).floor() as usize;
5205 let trimmed = &sorted[trim..n - trim];
5206 if trimmed.is_empty() {
5207 return None;
5208 }
5209 Some(trimmed.iter().sum::<f64>() / trimmed.len() as f64)
5210 }
5211
5212 pub fn linear_trend_slope(&self) -> Option<f64> {
5217 use rust_decimal::prelude::ToPrimitive;
5218 let n = self.window.len();
5219 if n < 2 {
5220 return None;
5221 }
5222 let n_f = n as f64;
5223 let x_mean = (n_f - 1.0) / 2.0;
5224 let y_vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5225 if y_vals.len() < 2 {
5226 return None;
5227 }
5228 let y_mean = y_vals.iter().sum::<f64>() / y_vals.len() as f64;
5229 let numerator: f64 = y_vals
5230 .iter()
5231 .enumerate()
5232 .map(|(i, &y)| (i as f64 - x_mean) * (y - y_mean))
5233 .sum();
5234 let denominator: f64 = (0..n).map(|i| (i as f64 - x_mean).powi(2)).sum();
5235 if denominator == 0.0 {
5236 return None;
5237 }
5238 Some(numerator / denominator)
5239 }
5240
5241 pub fn variance_ratio(&self) -> Option<f64> {
5249 use rust_decimal::prelude::ToPrimitive;
5250 let n = self.window.len();
5251 if n < 4 {
5252 return None;
5253 }
5254 let mid = n / 2;
5255 let first: Vec<f64> = self.window.iter().take(mid).filter_map(|v| v.to_f64()).collect();
5256 let second: Vec<f64> = self.window.iter().skip(mid).filter_map(|v| v.to_f64()).collect();
5257 let var = |vals: &[f64]| -> Option<f64> {
5258 let n_f = vals.len() as f64;
5259 if n_f < 2.0 { return None; }
5260 let mean = vals.iter().sum::<f64>() / n_f;
5261 Some(vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n_f - 1.0))
5262 };
5263 let v1 = var(&first)?;
5264 let v2 = var(&second)?;
5265 if v2 == 0.0 {
5266 return None;
5267 }
5268 Some(v1 / v2)
5269 }
5270
5271 pub fn z_score_trend_slope(&self) -> Option<f64> {
5277 use rust_decimal::prelude::ToPrimitive;
5278 let n = self.window.len();
5279 if n < 2 {
5280 return None;
5281 }
5282 let mean_dec = self.mean()?;
5283 let std_dev = self.std_dev()?;
5284 if std_dev == 0.0 {
5285 return None;
5286 }
5287 let mean_f = mean_dec.to_f64()?;
5288 let z_vals: Vec<f64> = self
5289 .window
5290 .iter()
5291 .filter_map(|v| v.to_f64())
5292 .map(|v| (v - mean_f) / std_dev)
5293 .collect();
5294 if z_vals.len() < 2 {
5295 return None;
5296 }
5297 let n_f = z_vals.len() as f64;
5298 let x_mean = (n_f - 1.0) / 2.0;
5299 let z_mean = z_vals.iter().sum::<f64>() / n_f;
5300 let num: f64 = z_vals.iter().enumerate().map(|(i, &z)| (i as f64 - x_mean) * (z - z_mean)).sum();
5301 let den: f64 = (0..z_vals.len()).map(|i| (i as f64 - x_mean).powi(2)).sum();
5302 if den == 0.0 { return None; }
5303 Some(num / den)
5304 }
5305
5306 pub fn mean_absolute_change(&self) -> Option<f64> {
5310 use rust_decimal::prelude::ToPrimitive;
5311 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5312 if vals.len() < 2 {
5313 return None;
5314 }
5315 let mac = vals.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / (vals.len() - 1) as f64;
5316 Some(mac)
5317 }
5318
5319 pub fn monotone_increase_fraction(&self) -> Option<f64> {
5323 let vals: Vec<Decimal> = self.window.iter().copied().collect();
5324 let n = vals.len();
5325 if n < 2 {
5326 return None;
5327 }
5328 let inc = vals.windows(2).filter(|w| w[1] > w[0]).count();
5329 Some(inc as f64 / (n - 1) as f64)
5330 }
5331
5332 pub fn abs_max(&self) -> Option<Decimal> {
5334 self.window.iter().map(|v| v.abs()).reduce(|a, b| a.max(b))
5335 }
5336
5337 pub fn abs_min(&self) -> Option<Decimal> {
5339 self.window.iter().map(|v| v.abs()).reduce(|a, b| a.min(b))
5340 }
5341
5342 pub fn max_count(&self) -> Option<usize> {
5344 let max = self.window.iter().copied().reduce(|a, b| a.max(b))?;
5345 Some(self.window.iter().filter(|&&v| v == max).count())
5346 }
5347
5348 pub fn min_count(&self) -> Option<usize> {
5350 let min = self.window.iter().copied().reduce(|a, b| a.min(b))?;
5351 Some(self.window.iter().filter(|&&v| v == min).count())
5352 }
5353
5354 pub fn mean_ratio(&self) -> Option<f64> {
5356 use rust_decimal::prelude::ToPrimitive;
5357 let n = self.window.len();
5358 if n < 2 {
5359 return None;
5360 }
5361 let current_mean = self.mean()?;
5362 let half = (n / 2).max(1);
5363 let early_sum: Decimal = self.window.iter().take(half).copied().sum();
5364 let early_mean = early_sum / Decimal::from(half as i64);
5365 if early_mean.is_zero() {
5366 return None;
5367 }
5368 (current_mean / early_mean).to_f64()
5369 }
5370
5371 pub fn exponential_weighted_mean(&self, alpha: f64) -> Option<f64> {
5375 use rust_decimal::prelude::ToPrimitive;
5376 if self.window.is_empty() {
5377 return None;
5378 }
5379 let alpha = alpha.clamp(1e-6, 1.0);
5380 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5381 if vals.is_empty() {
5382 return None;
5383 }
5384 let mut ewm = vals[0];
5385 for &v in &vals[1..] {
5386 ewm = alpha * v + (1.0 - alpha) * ewm;
5387 }
5388 Some(ewm)
5389 }
5390
5391 pub fn peak_to_trough_ratio(&self) -> Option<f64> {
5393 use rust_decimal::prelude::ToPrimitive;
5394 let max = self.window.iter().copied().reduce(|a, b| a.max(b))?;
5395 let min = self.window.iter().copied().reduce(|a, b| a.min(b))?;
5396 if min.is_zero() {
5397 return None;
5398 }
5399 (max / min).to_f64()
5400 }
5401
5402 pub fn second_moment(&self) -> Option<f64> {
5404 use rust_decimal::prelude::ToPrimitive;
5405 if self.window.is_empty() {
5406 return None;
5407 }
5408 let sum: f64 = self.window.iter().filter_map(|v| v.to_f64()).map(|v| v * v).sum();
5409 Some(sum / self.window.len() as f64)
5410 }
5411
5412 pub fn range_over_mean(&self) -> Option<f64> {
5414 use rust_decimal::prelude::ToPrimitive;
5415 let max = self.window.iter().copied().reduce(|a, b| a.max(b))?;
5416 let min = self.window.iter().copied().reduce(|a, b| a.min(b))?;
5417 let mean = self.mean()?;
5418 if mean.is_zero() {
5419 return None;
5420 }
5421 ((max - min) / mean).to_f64()
5422 }
5423
5424 pub fn above_median_fraction(&self) -> Option<f64> {
5426 if self.window.is_empty() {
5427 return None;
5428 }
5429 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
5430 sorted.sort();
5431 let mid = sorted.len() / 2;
5432 let median = if sorted.len() % 2 == 0 {
5433 (sorted[mid - 1] + sorted[mid]) / Decimal::from(2)
5434 } else {
5435 sorted[mid]
5436 };
5437 let count = self.window.iter().filter(|&&v| v > median).count();
5438 Some(count as f64 / self.window.len() as f64)
5439 }
5440
5441 pub fn interquartile_mean(&self) -> Option<f64> {
5445 use rust_decimal::prelude::ToPrimitive;
5446 if self.window.is_empty() {
5447 return None;
5448 }
5449 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
5450 sorted.sort();
5451 let n = sorted.len();
5452 let q1_idx = n / 4;
5453 let q3_idx = (3 * n) / 4;
5454 let iqr_vals: Vec<f64> = sorted[q1_idx..q3_idx]
5455 .iter()
5456 .filter_map(|v| v.to_f64())
5457 .collect();
5458 if iqr_vals.is_empty() {
5459 return None;
5460 }
5461 Some(iqr_vals.iter().sum::<f64>() / iqr_vals.len() as f64)
5462 }
5463
5464 pub fn outlier_fraction(&self, threshold: f64) -> Option<f64> {
5466 use rust_decimal::prelude::ToPrimitive;
5467 if self.window.is_empty() {
5468 return None;
5469 }
5470 let std_dev = self.std_dev()?;
5471 let mean = self.mean()?.to_f64()?;
5472 if std_dev == 0.0 {
5473 return Some(0.0);
5474 }
5475 let count = self.window
5476 .iter()
5477 .filter_map(|v| v.to_f64())
5478 .filter(|&v| ((v - mean) / std_dev).abs() > threshold)
5479 .count();
5480 Some(count as f64 / self.window.len() as f64)
5481 }
5482
5483 pub fn sign_flip_count(&self) -> Option<usize> {
5485 if self.window.len() < 2 {
5486 return None;
5487 }
5488 let count = self.window
5489 .iter()
5490 .collect::<Vec<_>>()
5491 .windows(2)
5492 .filter(|w| w[0].is_sign_negative() != w[1].is_sign_negative())
5493 .count();
5494 Some(count)
5495 }
5496
5497 pub fn rms(&self) -> Option<f64> {
5499 use rust_decimal::prelude::ToPrimitive;
5500 if self.window.is_empty() {
5501 return None;
5502 }
5503 let sum_sq: f64 = self.window.iter().filter_map(|v| v.to_f64()).map(|v| v * v).sum();
5504 Some((sum_sq / self.window.len() as f64).sqrt())
5505 }
5506
5507 pub fn distinct_count(&self) -> usize {
5511 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
5512 sorted.sort();
5513 sorted.dedup();
5514 sorted.len()
5515 }
5516
5517 pub fn max_fraction(&self) -> Option<f64> {
5519 if self.window.is_empty() {
5520 return None;
5521 }
5522 let max = self.window.iter().copied().max()?;
5523 let count = self.window.iter().filter(|&&v| v == max).count();
5524 Some(count as f64 / self.window.len() as f64)
5525 }
5526
5527 pub fn min_fraction(&self) -> Option<f64> {
5529 if self.window.is_empty() {
5530 return None;
5531 }
5532 let min = self.window.iter().copied().min()?;
5533 let count = self.window.iter().filter(|&&v| v == min).count();
5534 Some(count as f64 / self.window.len() as f64)
5535 }
5536
5537 pub fn latest_minus_mean(&self) -> Option<f64> {
5539 use rust_decimal::prelude::ToPrimitive;
5540 let latest = self.latest()?;
5541 let mean = self.mean()?;
5542 (latest - mean).to_f64()
5543 }
5544
5545 pub fn latest_to_mean_ratio(&self) -> Option<f64> {
5547 use rust_decimal::prelude::ToPrimitive;
5548 let latest = self.latest()?;
5549 let mean = self.mean()?;
5550 if mean.is_zero() {
5551 return None;
5552 }
5553 (latest / mean).to_f64()
5554 }
5555
5556 pub fn below_mean_fraction(&self) -> Option<f64> {
5560 if self.window.is_empty() {
5561 return None;
5562 }
5563 let mean = self.mean()?;
5564 let count = self.window.iter().filter(|&&v| v < mean).count();
5565 Some(count as f64 / self.window.len() as f64)
5566 }
5567
5568 pub fn tail_variance(&self) -> Option<f64> {
5571 use rust_decimal::prelude::ToPrimitive;
5572 if self.window.len() < 4 {
5573 return None;
5574 }
5575 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
5576 sorted.sort();
5577 let n = sorted.len();
5578 let q1 = sorted[n / 4];
5579 let q3 = sorted[(3 * n) / 4];
5580 let tails: Vec<f64> = sorted
5581 .iter()
5582 .filter(|&&v| v < q1 || v > q3)
5583 .filter_map(|v| v.to_f64())
5584 .collect();
5585 if tails.len() < 2 {
5586 return None;
5587 }
5588 let nt = tails.len() as f64;
5589 let mean = tails.iter().sum::<f64>() / nt;
5590 let var = tails.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (nt - 1.0);
5591 Some(var)
5592 }
5593
5594 pub fn new_max_count(&self) -> usize {
5598 if self.window.is_empty() {
5599 return 0;
5600 }
5601 let vals: Vec<Decimal> = self.window.iter().copied().collect();
5602 let mut running = vals[0];
5603 let mut count = 1usize;
5604 for &v in vals.iter().skip(1) {
5605 if v > running {
5606 running = v;
5607 count += 1;
5608 }
5609 }
5610 count
5611 }
5612
5613 pub fn new_min_count(&self) -> usize {
5615 if self.window.is_empty() {
5616 return 0;
5617 }
5618 let vals: Vec<Decimal> = self.window.iter().copied().collect();
5619 let mut running = vals[0];
5620 let mut count = 1usize;
5621 for &v in vals.iter().skip(1) {
5622 if v < running {
5623 running = v;
5624 count += 1;
5625 }
5626 }
5627 count
5628 }
5629
5630 pub fn zero_fraction(&self) -> Option<f64> {
5632 if self.window.is_empty() {
5633 return None;
5634 }
5635 let count = self.window.iter().filter(|&&v| v == Decimal::ZERO).count();
5636 Some(count as f64 / self.window.len() as f64)
5637 }
5638
5639 pub fn cumulative_sum(&self) -> Decimal {
5643 self.window.iter().copied().sum()
5644 }
5645
5646 pub fn max_to_min_ratio(&self) -> Option<f64> {
5649 use rust_decimal::prelude::ToPrimitive;
5650 let max = self.window.iter().copied().max()?;
5651 let min = self.window.iter().copied().min()?;
5652 if min.is_zero() {
5653 return None;
5654 }
5655 (max / min).to_f64()
5656 }
5657
5658 pub fn above_midpoint_fraction(&self) -> Option<f64> {
5664 if self.window.is_empty() {
5665 return None;
5666 }
5667 let min = self.window.iter().copied().min()?;
5668 let max = self.window.iter().copied().max()?;
5669 let mid = (min + max) / Decimal::TWO;
5670 let count = self.window.iter().filter(|&&v| v > mid).count();
5671 Some(count as f64 / self.window.len() as f64)
5672 }
5673
5674 pub fn positive_fraction(&self) -> Option<f64> {
5678 if self.window.is_empty() {
5679 return None;
5680 }
5681 let count = self.window.iter().filter(|&&v| v > Decimal::ZERO).count();
5682 Some(count as f64 / self.window.len() as f64)
5683 }
5684
5685 pub fn above_mean_fraction(&self) -> Option<f64> {
5689 use rust_decimal::prelude::ToPrimitive;
5690 if self.window.is_empty() {
5691 return None;
5692 }
5693 let n = self.window.len() as u32;
5694 let mean = self.window.iter().copied().sum::<Decimal>() / Decimal::from(n);
5695 let count = self.window.iter().filter(|&&v| v > mean).count();
5696 Some(count as f64 / self.window.len() as f64)
5697 }
5698
5699}
5700
5701#[cfg(test)]
5702mod zscore_tests {
5703 use super::*;
5704 use rust_decimal_macros::dec;
5705
5706 fn znorm(w: usize) -> ZScoreNormalizer {
5707 ZScoreNormalizer::new(w).unwrap()
5708 }
5709
5710 #[test]
5711 fn test_zscore_new_zero_window_returns_error() {
5712 assert!(matches!(
5713 ZScoreNormalizer::new(0),
5714 Err(StreamError::ConfigError { .. })
5715 ));
5716 }
5717
5718 #[test]
5719 fn test_zscore_is_full_false_before_capacity() {
5720 let mut n = znorm(3);
5721 assert!(!n.is_full());
5722 n.update(dec!(1));
5723 n.update(dec!(2));
5724 assert!(!n.is_full());
5725 n.update(dec!(3));
5726 assert!(n.is_full());
5727 }
5728
5729 #[test]
5730 fn test_zscore_is_full_stays_true_after_eviction() {
5731 let mut n = znorm(3);
5732 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
5733 n.update(v);
5734 }
5735 assert!(n.is_full());
5736 }
5737
5738 #[test]
5739 fn test_zscore_empty_window_returns_error() {
5740 let n = znorm(4);
5741 assert!(matches!(
5742 n.normalize(dec!(1)),
5743 Err(StreamError::NormalizationError { .. })
5744 ));
5745 }
5746
5747 #[test]
5748 fn test_zscore_single_value_returns_zero() {
5749 let mut n = znorm(4);
5750 n.update(dec!(50));
5751 assert_eq!(n.normalize(dec!(50)).unwrap(), 0.0);
5752 }
5753
5754 #[test]
5755 fn test_zscore_mean_is_zero() {
5756 let mut n = znorm(5);
5757 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] {
5758 n.update(v);
5759 }
5760 let z = n.normalize(dec!(30)).unwrap();
5762 assert!((z - 0.0).abs() < 1e-9, "z-score of mean should be 0, got {z}");
5763 }
5764
5765 #[test]
5766 fn test_zscore_symmetric_around_mean() {
5767 let mut n = znorm(4);
5768 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
5769 n.update(v);
5770 }
5771 let z_low = n.normalize(dec!(15)).unwrap();
5773 let z_high = n.normalize(dec!(35)).unwrap();
5774 assert!((z_low.abs() - z_high.abs()).abs() < 1e-9);
5775 assert!(z_low < 0.0, "below-mean z-score should be negative");
5776 assert!(z_high > 0.0, "above-mean z-score should be positive");
5777 }
5778
5779 #[test]
5780 fn test_zscore_all_same_returns_zero() {
5781 let mut n = znorm(4);
5782 for _ in 0..4 {
5783 n.update(dec!(100));
5784 }
5785 assert_eq!(n.normalize(dec!(100)).unwrap(), 0.0);
5786 }
5787
5788 #[test]
5789 fn test_zscore_rolling_window_eviction() {
5790 let mut n = znorm(3);
5791 n.update(dec!(1));
5792 n.update(dec!(2));
5793 n.update(dec!(3));
5794 n.update(dec!(100));
5796 let z = n.normalize(dec!(100)).unwrap();
5798 assert!(z > 0.0);
5799 }
5800
5801 #[test]
5802 fn test_zscore_reset_clears_state() {
5803 let mut n = znorm(4);
5804 for v in [dec!(10), dec!(20), dec!(30)] {
5805 n.update(v);
5806 }
5807 n.reset();
5808 assert!(n.is_empty());
5809 assert!(n.mean().is_none());
5810 assert!(matches!(
5811 n.normalize(dec!(1)),
5812 Err(StreamError::NormalizationError { .. })
5813 ));
5814 }
5815
5816 #[test]
5817 fn test_zscore_len_and_window_size() {
5818 let mut n = znorm(5);
5819 assert_eq!(n.len(), 0);
5820 assert!(n.is_empty());
5821 n.update(dec!(1));
5822 n.update(dec!(2));
5823 assert_eq!(n.len(), 2);
5824 assert_eq!(n.window_size(), 5);
5825 }
5826
5827 #[test]
5830 fn test_std_dev_none_when_empty() {
5831 let n = znorm(5);
5832 assert!(n.std_dev().is_none());
5833 }
5834
5835 #[test]
5836 fn test_std_dev_zero_with_one_observation() {
5837 let mut n = znorm(5);
5838 n.update(dec!(42));
5839 assert_eq!(n.std_dev(), Some(0.0));
5840 }
5841
5842 #[test]
5843 fn test_std_dev_zero_when_all_same() {
5844 let mut n = znorm(4);
5845 for _ in 0..4 {
5846 n.update(dec!(10));
5847 }
5848 let sd = n.std_dev().unwrap();
5849 assert!(sd < f64::EPSILON);
5850 }
5851
5852 #[test]
5853 fn test_std_dev_positive_for_varying_values() {
5854 let mut n = znorm(4);
5855 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
5856 n.update(v);
5857 }
5858 let sd = n.std_dev().unwrap();
5859 assert!((sd - 11.18).abs() < 0.01);
5861 }
5862
5863 #[test]
5866 fn test_variance_none_when_fewer_than_two_observations() {
5867 let mut n = znorm(5);
5868 assert!(n.variance().is_none());
5869 n.update(dec!(10));
5870 assert!(n.variance().is_none());
5871 }
5872
5873 #[test]
5874 fn test_variance_zero_for_identical_values() {
5875 let mut n = znorm(4);
5876 for _ in 0..4 {
5877 n.update(dec!(7));
5878 }
5879 assert_eq!(n.variance().unwrap(), dec!(0));
5880 }
5881
5882 #[test]
5883 fn test_variance_correct_for_known_values() {
5884 let mut n = znorm(4);
5885 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
5886 n.update(v);
5887 }
5888 let var = n.variance().unwrap();
5890 let var_f64 = f64::try_from(var).unwrap();
5891 assert!((var_f64 - 125.0).abs() < 0.01, "expected 125 got {var_f64}");
5892 }
5893
5894 #[test]
5897 fn test_normalize_batch_same_length_as_input() {
5898 let mut n = znorm(5);
5899 let vals = [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)];
5900 let out = n.normalize_batch(&vals).unwrap();
5901 assert_eq!(out.len(), vals.len());
5902 }
5903
5904 #[test]
5905 fn test_normalize_batch_last_value_matches_single_normalize() {
5906 let mut n1 = znorm(5);
5907 let vals = [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)];
5908 let batch = n1.normalize_batch(&vals).unwrap();
5909
5910 let mut n2 = znorm(5);
5911 for &v in &vals {
5912 n2.update(v);
5913 }
5914 let single = n2.normalize(dec!(50)).unwrap();
5915 assert!((batch[4] - single).abs() < 1e-9);
5916 }
5917
5918 #[test]
5919 fn test_sum_empty_returns_none() {
5920 let n = znorm(4);
5921 assert!(n.sum().is_none());
5922 }
5923
5924 #[test]
5925 fn test_sum_matches_manual() {
5926 let mut n = znorm(4);
5927 n.update(dec!(10));
5928 n.update(dec!(20));
5929 n.update(dec!(30));
5930 assert_eq!(n.sum().unwrap(), dec!(60));
5932 }
5933
5934 #[test]
5935 fn test_sum_evicts_old_values() {
5936 let mut n = znorm(2);
5937 n.update(dec!(10));
5938 n.update(dec!(20));
5939 n.update(dec!(30)); assert_eq!(n.sum().unwrap(), dec!(50));
5942 }
5943
5944 #[test]
5945 fn test_std_dev_single_observation_returns_some_zero() {
5946 let mut n = znorm(5);
5947 n.update(dec!(10));
5948 assert!(n.std_dev().is_none() || n.std_dev().unwrap() == 0.0);
5951 }
5952
5953 #[test]
5954 fn test_std_dev_constant_window_is_zero() {
5955 let mut n = znorm(4);
5956 for _ in 0..4 {
5957 n.update(dec!(5));
5958 }
5959 let sd = n.std_dev().unwrap();
5960 assert!(sd.abs() < 1e-9, "expected 0.0 got {sd}");
5961 }
5962
5963 #[test]
5964 fn test_std_dev_known_population() {
5965 let mut n = znorm(8);
5967 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
5968 n.update(v);
5969 }
5970 let sd = n.std_dev().unwrap();
5971 assert!((sd - 2.0).abs() < 1e-6, "expected ~2.0 got {sd}");
5972 }
5973
5974 #[test]
5977 fn test_window_range_none_when_empty() {
5978 let n = znorm(5);
5979 assert!(n.window_range().is_none());
5980 }
5981
5982 #[test]
5983 fn test_window_range_correct_value() {
5984 let mut n = znorm(5);
5985 n.update(dec!(10));
5986 n.update(dec!(20));
5987 n.update(dec!(15));
5988 assert_eq!(n.window_range().unwrap(), dec!(10));
5990 }
5991
5992 #[test]
5993 fn test_coefficient_of_variation_none_when_empty() {
5994 let n = znorm(5);
5995 assert!(n.coefficient_of_variation().is_none());
5996 }
5997
5998 #[test]
5999 fn test_coefficient_of_variation_none_when_mean_zero() {
6000 let mut n = znorm(5);
6001 n.update(dec!(-5));
6002 n.update(dec!(5)); assert!(n.coefficient_of_variation().is_none());
6004 }
6005
6006 #[test]
6007 fn test_coefficient_of_variation_positive_for_nonzero_mean() {
6008 let mut n = znorm(8);
6009 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
6010 n.update(v);
6011 }
6012 let cv = n.coefficient_of_variation().unwrap();
6014 assert!((cv - 0.4).abs() < 1e-5, "expected ~0.4 got {cv}");
6015 }
6016
6017 #[test]
6020 fn test_sample_variance_none_when_empty() {
6021 let n = znorm(5);
6022 assert!(n.sample_variance().is_none());
6023 }
6024
6025 #[test]
6026 fn test_sample_variance_zero_for_constant_window() {
6027 let mut n = znorm(3);
6028 n.update(dec!(7));
6029 n.update(dec!(7));
6030 n.update(dec!(7));
6031 assert!(n.sample_variance().unwrap().abs() < 1e-10);
6032 }
6033
6034 #[test]
6035 fn test_sample_variance_equals_std_dev_squared() {
6036 let mut n = znorm(8);
6037 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
6038 n.update(v);
6039 }
6040 let variance = n.sample_variance().unwrap();
6042 let sd = n.std_dev().unwrap();
6043 assert!((variance - sd * sd).abs() < 1e-10);
6044 }
6045
6046 #[test]
6049 fn test_window_mean_f64_none_when_empty() {
6050 let n = znorm(5);
6051 assert!(n.window_mean_f64().is_none());
6052 }
6053
6054 #[test]
6055 fn test_window_mean_f64_correct_value() {
6056 let mut n = znorm(4);
6057 n.update(dec!(10));
6058 n.update(dec!(20));
6059 let m = n.window_mean_f64().unwrap();
6061 assert!((m - 15.0).abs() < 1e-10);
6062 }
6063
6064 #[test]
6065 fn test_window_mean_f64_matches_decimal_mean() {
6066 let mut n = znorm(8);
6067 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
6068 n.update(v);
6069 }
6070 use rust_decimal::prelude::ToPrimitive;
6071 let expected = n.mean().unwrap().to_f64().unwrap();
6072 assert!((n.window_mean_f64().unwrap() - expected).abs() < 1e-10);
6073 }
6074
6075 #[test]
6078 fn test_kurtosis_none_when_fewer_than_4_observations() {
6079 let mut n = znorm(5);
6080 n.update(dec!(1));
6081 n.update(dec!(2));
6082 n.update(dec!(3));
6083 assert!(n.kurtosis().is_none());
6084 }
6085
6086 #[test]
6087 fn test_kurtosis_returns_some_with_4_observations() {
6088 let mut n = znorm(4);
6089 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6090 n.update(v);
6091 }
6092 assert!(n.kurtosis().is_some());
6093 }
6094
6095 #[test]
6096 fn test_kurtosis_none_when_all_same_value() {
6097 let mut n = znorm(4);
6098 for _ in 0..4 {
6099 n.update(dec!(5));
6100 }
6101 assert!(n.kurtosis().is_none());
6103 }
6104
6105 #[test]
6106 fn test_kurtosis_uniform_distribution_is_negative() {
6107 let mut n = znorm(10);
6109 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5),
6110 dec!(6), dec!(7), dec!(8), dec!(9), dec!(10)] {
6111 n.update(v);
6112 }
6113 let k = n.kurtosis().unwrap();
6114 assert!(k < 0.0, "expected negative excess kurtosis for uniform dist, got {k}");
6116 }
6117
6118 #[test]
6120 fn test_is_near_mean_false_with_fewer_than_two_obs() {
6121 let mut n = znorm(5);
6122 n.update(dec!(10));
6123 assert!(!n.is_near_mean(dec!(10), 1.0));
6124 }
6125
6126 #[test]
6127 fn test_is_near_mean_true_within_one_sigma() {
6128 let mut n = znorm(10);
6129 for _ in 0..9 {
6131 n.update(dec!(10));
6132 }
6133 n.update(dec!(20));
6134 assert!(n.is_near_mean(dec!(11), 1.0));
6136 }
6137
6138 #[test]
6139 fn test_is_near_mean_false_when_far_from_mean() {
6140 let mut n = znorm(5);
6141 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] {
6142 n.update(v);
6143 }
6144 assert!(!n.is_near_mean(dec!(100), 2.0));
6146 }
6147
6148 #[test]
6149 fn test_is_near_mean_true_when_all_identical_any_value() {
6150 let mut n = znorm(4);
6151 for _ in 0..4 {
6152 n.update(dec!(7));
6153 }
6154 assert!(n.is_near_mean(dec!(999), 0.0));
6156 }
6157
6158 #[test]
6160 fn test_window_sum_f64_zero_on_empty() {
6161 let n = znorm(5);
6162 assert_eq!(n.window_sum_f64(), 0.0);
6163 }
6164
6165 #[test]
6166 fn test_window_sum_f64_correct_after_updates() {
6167 let mut n = znorm(5);
6168 n.update(dec!(10));
6169 n.update(dec!(20));
6170 n.update(dec!(30));
6171 assert!((n.window_sum_f64() - 60.0).abs() < 1e-10);
6172 }
6173
6174 #[test]
6175 fn test_window_sum_f64_rolls_out_old_values() {
6176 let mut n = znorm(2);
6177 n.update(dec!(100));
6178 n.update(dec!(200));
6179 n.update(dec!(300)); assert!((n.window_sum_f64() - 500.0).abs() < 1e-10);
6182 }
6183
6184 #[test]
6187 fn test_zscore_latest_none_when_empty() {
6188 let n = znorm(5);
6189 assert!(n.latest().is_none());
6190 }
6191
6192 #[test]
6193 fn test_zscore_latest_returns_most_recent() {
6194 let mut n = znorm(5);
6195 n.update(dec!(10));
6196 n.update(dec!(20));
6197 assert_eq!(n.latest(), Some(dec!(20)));
6198 }
6199
6200 #[test]
6201 fn test_zscore_latest_updates_on_roll() {
6202 let mut n = znorm(2);
6203 n.update(dec!(1));
6204 n.update(dec!(2));
6205 n.update(dec!(3)); assert_eq!(n.latest(), Some(dec!(3)));
6207 }
6208
6209 #[test]
6211 fn test_window_max_f64_none_on_empty() {
6212 let n = znorm(5);
6213 assert!(n.window_max_f64().is_none());
6214 }
6215
6216 #[test]
6217 fn test_window_max_f64_correct_value() {
6218 let mut n = znorm(5);
6219 for v in [dec!(3), dec!(7), dec!(1), dec!(5)] {
6220 n.update(v);
6221 }
6222 assert!((n.window_max_f64().unwrap() - 7.0).abs() < 1e-10);
6223 }
6224
6225 #[test]
6226 fn test_window_min_f64_none_on_empty() {
6227 let n = znorm(5);
6228 assert!(n.window_min_f64().is_none());
6229 }
6230
6231 #[test]
6232 fn test_window_min_f64_correct_value() {
6233 let mut n = znorm(5);
6234 for v in [dec!(3), dec!(7), dec!(1), dec!(5)] {
6235 n.update(v);
6236 }
6237 assert!((n.window_min_f64().unwrap() - 1.0).abs() < 1e-10);
6238 }
6239
6240 #[test]
6243 fn test_percentile_none_when_empty() {
6244 let n = znorm(5);
6245 assert!(n.percentile(dec!(10)).is_none());
6246 }
6247
6248 #[test]
6249 fn test_percentile_one_when_all_lte_value() {
6250 let mut n = znorm(4);
6251 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6252 n.update(v);
6253 }
6254 assert!((n.percentile(dec!(4)).unwrap() - 1.0).abs() < 1e-9);
6255 }
6256
6257 #[test]
6258 fn test_percentile_zero_when_all_gt_value() {
6259 let mut n = znorm(4);
6260 for v in [dec!(5), dec!(6), dec!(7), dec!(8)] {
6261 n.update(v);
6262 }
6263 assert_eq!(n.percentile(dec!(4)).unwrap(), 0.0);
6265 }
6266
6267 #[test]
6268 fn test_percentile_half_at_median() {
6269 let mut n = znorm(4);
6270 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6271 n.update(v);
6272 }
6273 assert!((n.percentile(dec!(2)).unwrap() - 0.5).abs() < 1e-9);
6275 }
6276
6277 #[test]
6280 fn test_zscore_iqr_none_fewer_than_4_observations() {
6281 let mut n = znorm(5);
6282 for v in [dec!(1), dec!(2), dec!(3)] {
6283 n.update(v);
6284 }
6285 assert!(n.interquartile_range().is_none());
6286 }
6287
6288 #[test]
6289 fn test_zscore_iqr_some_with_4_observations() {
6290 let mut n = znorm(4);
6291 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6292 n.update(v);
6293 }
6294 assert!(n.interquartile_range().is_some());
6295 }
6296
6297 #[test]
6298 fn test_zscore_iqr_zero_when_all_same() {
6299 let mut n = znorm(4);
6300 for _ in 0..4 {
6301 n.update(dec!(5));
6302 }
6303 assert_eq!(n.interquartile_range(), Some(dec!(0)));
6304 }
6305
6306 #[test]
6307 fn test_zscore_iqr_correct_for_sorted_data() {
6308 let mut n = znorm(8);
6310 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6), dec!(7), dec!(8)] {
6311 n.update(v);
6312 }
6313 assert_eq!(n.interquartile_range(), Some(dec!(4)));
6314 }
6315
6316 #[test]
6319 fn test_z_score_of_latest_none_when_empty() {
6320 let n = znorm(5);
6321 assert!(n.z_score_of_latest().is_none());
6322 }
6323
6324 #[test]
6325 fn test_z_score_of_latest_zero_when_all_same() {
6326 let mut n = znorm(4);
6327 for _ in 0..4 {
6328 n.update(dec!(5));
6329 }
6330 assert_eq!(n.z_score_of_latest(), Some(0.0));
6332 }
6333
6334 #[test]
6335 fn test_z_score_of_latest_returns_some_with_variance() {
6336 let mut n = znorm(4);
6337 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6338 n.update(v);
6339 }
6340 assert!(n.z_score_of_latest().is_some());
6342 }
6343
6344 #[test]
6345 fn test_deviation_from_mean_none_when_empty() {
6346 let n = znorm(5);
6347 assert!(n.deviation_from_mean(dec!(10)).is_none());
6348 }
6349
6350 #[test]
6351 fn test_deviation_from_mean_correct() {
6352 let mut n = znorm(4);
6353 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6354 n.update(v);
6355 }
6356 let d = n.deviation_from_mean(dec!(4)).unwrap();
6358 assert!((d - 1.5).abs() < 1e-9);
6359 }
6360
6361 #[test]
6364 fn test_add_observation_same_as_update() {
6365 let mut n1 = znorm(4);
6366 let mut n2 = znorm(4);
6367 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6368 n1.update(v);
6369 n2.add_observation(v);
6370 }
6371 assert_eq!(n1.mean(), n2.mean());
6372 }
6373
6374 #[test]
6375 fn test_add_observation_chainable() {
6376 let mut n = znorm(4);
6377 n.add_observation(dec!(1))
6378 .add_observation(dec!(2))
6379 .add_observation(dec!(3));
6380 assert_eq!(n.len(), 3);
6381 }
6382
6383 #[test]
6386 fn test_variance_f64_none_when_single_observation() {
6387 let mut n = znorm(4);
6388 n.update(dec!(5));
6389 assert!(n.variance_f64().is_none());
6390 }
6391
6392 #[test]
6393 fn test_variance_f64_zero_when_all_same() {
6394 let mut n = znorm(4);
6395 for _ in 0..4 { n.update(dec!(5)); }
6396 assert_eq!(n.variance_f64(), Some(0.0));
6397 }
6398
6399 #[test]
6400 fn test_variance_f64_positive_with_spread() {
6401 let mut n = znorm(4);
6402 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6403 assert!(n.variance_f64().unwrap() > 0.0);
6404 }
6405
6406 #[test]
6409 fn test_ema_of_z_scores_none_when_single_value() {
6410 let mut n = znorm(4);
6411 n.update(dec!(5));
6412 assert!(n.ema_of_z_scores(0.5).is_none());
6413 }
6414
6415 #[test]
6416 fn test_ema_of_z_scores_returns_some_with_variance() {
6417 let mut n = znorm(4);
6418 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6419 n.update(v);
6420 }
6421 let ema = n.ema_of_z_scores(0.3);
6422 assert!(ema.is_some());
6423 }
6424
6425 #[test]
6426 fn test_ema_of_z_scores_zero_when_all_same() {
6427 let mut n = znorm(4);
6428 for _ in 0..4 { n.update(dec!(5)); }
6429 assert_eq!(n.ema_of_z_scores(0.5), Some(0.0));
6431 }
6432
6433 #[test]
6436 fn test_std_dev_f64_none_when_single_observation() {
6437 let mut n = znorm(4);
6438 n.update(dec!(5));
6439 assert!(n.std_dev_f64().is_none());
6440 }
6441
6442 #[test]
6443 fn test_std_dev_f64_zero_when_all_same() {
6444 let mut n = znorm(4);
6445 for _ in 0..4 { n.update(dec!(5)); }
6446 assert_eq!(n.std_dev_f64(), Some(0.0));
6447 }
6448
6449 #[test]
6450 fn test_std_dev_f64_equals_sqrt_of_variance() {
6451 let mut n = znorm(4);
6452 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6453 let var = n.variance_f64().unwrap();
6454 let std = n.std_dev_f64().unwrap();
6455 assert!((std - var.sqrt()).abs() < 1e-12);
6456 }
6457
6458 #[test]
6461 fn test_rolling_mean_change_none_when_one_observation() {
6462 let mut n = znorm(4);
6463 n.update(dec!(5));
6464 assert!(n.rolling_mean_change().is_none());
6465 }
6466
6467 #[test]
6468 fn test_rolling_mean_change_positive_when_rising() {
6469 let mut n = znorm(4);
6470 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6471 let change = n.rolling_mean_change().unwrap();
6473 assert!((change - 2.0).abs() < 1e-9);
6474 }
6475
6476 #[test]
6477 fn test_rolling_mean_change_negative_when_falling() {
6478 let mut n = znorm(4);
6479 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
6480 let change = n.rolling_mean_change().unwrap();
6481 assert!(change < 0.0);
6482 }
6483
6484 #[test]
6485 fn test_rolling_mean_change_zero_when_flat() {
6486 let mut n = znorm(4);
6487 for _ in 0..4 { n.update(dec!(7)); }
6488 let change = n.rolling_mean_change().unwrap();
6489 assert!(change.abs() < 1e-9);
6490 }
6491
6492 #[test]
6495 fn test_window_span_f64_none_when_empty() {
6496 let n = znorm(4);
6497 assert!(n.window_span_f64().is_none());
6498 }
6499
6500 #[test]
6501 fn test_window_span_f64_zero_when_all_same() {
6502 let mut n = znorm(4);
6503 for _ in 0..4 { n.update(dec!(5)); }
6504 assert_eq!(n.window_span_f64(), Some(0.0));
6505 }
6506
6507 #[test]
6508 fn test_window_span_f64_correct_value() {
6509 let mut n = znorm(4);
6510 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
6511 assert!((n.window_span_f64().unwrap() - 30.0).abs() < 1e-9);
6513 }
6514
6515 #[test]
6518 fn test_count_positive_z_scores_zero_when_empty() {
6519 let n = znorm(4);
6520 assert_eq!(n.count_positive_z_scores(), 0);
6521 }
6522
6523 #[test]
6524 fn test_count_positive_z_scores_zero_when_all_same() {
6525 let mut n = znorm(4);
6526 for _ in 0..4 { n.update(dec!(5)); }
6527 assert_eq!(n.count_positive_z_scores(), 0);
6528 }
6529
6530 #[test]
6531 fn test_count_positive_z_scores_half_above_mean() {
6532 let mut n = znorm(4);
6533 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6534 assert_eq!(n.count_positive_z_scores(), 2);
6536 }
6537
6538 #[test]
6541 fn test_above_threshold_count_zero_when_empty() {
6542 let n = znorm(4);
6543 assert_eq!(n.above_threshold_count(1.0), 0);
6544 }
6545
6546 #[test]
6547 fn test_above_threshold_count_zero_when_all_same() {
6548 let mut n = znorm(4);
6549 for _ in 0..4 { n.update(dec!(5)); }
6550 assert_eq!(n.above_threshold_count(0.5), 0);
6551 }
6552
6553 #[test]
6554 fn test_above_threshold_count_correct_with_extremes() {
6555 let mut n = znorm(6);
6556 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(100)] { n.update(v); }
6557 assert!(n.above_threshold_count(1.0) >= 1);
6559 }
6560}
6561
6562#[cfg(test)]
6563mod minmax_extra_tests {
6564 use super::*;
6565 use rust_decimal_macros::dec;
6566
6567 fn norm(w: usize) -> MinMaxNormalizer {
6568 MinMaxNormalizer::new(w).unwrap()
6569 }
6570
6571 #[test]
6574 fn test_fraction_above_mid_none_when_empty() {
6575 let mut n = norm(4);
6576 assert!(n.fraction_above_mid().is_none());
6577 }
6578
6579 #[test]
6580 fn test_fraction_above_mid_zero_when_all_same() {
6581 let mut n = norm(4);
6582 for _ in 0..4 { n.update(dec!(5)); }
6583 assert_eq!(n.fraction_above_mid(), Some(0.0));
6584 }
6585
6586 #[test]
6587 fn test_fraction_above_mid_half_when_symmetric() {
6588 let mut n = norm(4);
6589 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6590 let f = n.fraction_above_mid().unwrap();
6592 assert!((f - 0.5).abs() < 1e-10);
6593 }
6594}
6595
6596#[cfg(test)]
6597mod zscore_stability_tests {
6598 use super::*;
6599 use rust_decimal_macros::dec;
6600
6601 fn znorm(w: usize) -> ZScoreNormalizer {
6602 ZScoreNormalizer::new(w).unwrap()
6603 }
6604
6605 #[test]
6608 fn test_is_mean_stable_false_when_window_too_small() {
6609 let n = znorm(4);
6610 assert!(!n.is_mean_stable(1.0));
6611 }
6612
6613 #[test]
6614 fn test_is_mean_stable_true_when_flat() {
6615 let mut n = znorm(4);
6616 for _ in 0..4 { n.update(dec!(5)); }
6617 assert!(n.is_mean_stable(0.001));
6618 }
6619
6620 #[test]
6621 fn test_is_mean_stable_false_when_trending() {
6622 let mut n = znorm(4);
6623 for v in [dec!(1), dec!(2), dec!(10), dec!(20)] { n.update(v); }
6624 assert!(!n.is_mean_stable(0.5));
6625 }
6626
6627 #[test]
6630 fn test_zscore_count_above_zero_for_empty_window() {
6631 assert_eq!(znorm(4).count_above(dec!(10)), 0);
6632 }
6633
6634 #[test]
6635 fn test_zscore_count_above_correct() {
6636 let mut n = znorm(5);
6637 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6638 assert_eq!(n.count_above(dec!(3)), 2);
6640 }
6641
6642 #[test]
6643 fn test_zscore_count_below_correct() {
6644 let mut n = znorm(5);
6645 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6646 assert_eq!(n.count_below(dec!(3)), 2);
6648 }
6649
6650 #[test]
6651 fn test_zscore_count_above_excludes_at_threshold() {
6652 let mut n = znorm(3);
6653 for v in [dec!(5), dec!(5), dec!(5)] { n.update(v); }
6654 assert_eq!(n.count_above(dec!(5)), 0);
6655 assert_eq!(n.count_below(dec!(5)), 0);
6656 }
6657
6658 #[test]
6661 fn test_zscore_skewness_none_for_fewer_than_3_obs() {
6662 let mut n = znorm(5);
6663 n.update(dec!(10));
6664 n.update(dec!(20));
6665 assert!(n.skewness().is_none());
6666 }
6667
6668 #[test]
6669 fn test_zscore_skewness_none_for_all_identical() {
6670 let mut n = znorm(4);
6671 for _ in 0..4 { n.update(dec!(5)); }
6672 assert!(n.skewness().is_none());
6673 }
6674
6675 #[test]
6676 fn test_zscore_skewness_near_zero_for_symmetric_distribution() {
6677 let mut n = znorm(5);
6678 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6679 let skew = n.skewness().unwrap();
6680 assert!(skew.abs() < 0.01, "symmetric distribution should have ~0 skewness, got {skew}");
6681 }
6682
6683 #[test]
6686 fn test_zscore_percentile_value_none_for_empty_window() {
6687 assert!(znorm(4).percentile_value(0.5).is_none());
6688 }
6689
6690 #[test]
6691 fn test_zscore_percentile_value_min_at_zero() {
6692 let mut n = znorm(5);
6693 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
6694 assert_eq!(n.percentile_value(0.0), Some(dec!(10)));
6695 }
6696
6697 #[test]
6698 fn test_zscore_percentile_value_max_at_one() {
6699 let mut n = znorm(5);
6700 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
6701 assert_eq!(n.percentile_value(1.0), Some(dec!(50)));
6702 }
6703
6704 #[test]
6707 fn test_zscore_ewma_none_for_empty_window() {
6708 assert!(znorm(4).ewma(0.5).is_none());
6709 }
6710
6711 #[test]
6712 fn test_zscore_ewma_equals_value_for_single_obs() {
6713 let mut n = znorm(4);
6714 n.update(dec!(42));
6715 assert!((n.ewma(0.5).unwrap() - 42.0).abs() < 1e-10);
6716 }
6717
6718 #[test]
6719 fn test_zscore_ewma_weights_recent_more_with_high_alpha() {
6720 let mut n = znorm(4);
6722 for v in [dec!(10), dec!(20), dec!(30), dec!(100)] { n.update(v); }
6723 let ewma = n.ewma(1.0).unwrap();
6724 assert!((ewma - 100.0).abs() < 1e-10);
6725 }
6726
6727 #[test]
6728 fn test_zscore_fraction_above_mid_none_for_empty_window() {
6729 let n = znorm(3);
6730 assert!(n.fraction_above_mid().is_none());
6731 }
6732
6733 #[test]
6734 fn test_zscore_fraction_above_mid_none_when_all_equal() {
6735 let mut n = znorm(3);
6736 for _ in 0..3 { n.update(dec!(5)); }
6737 assert!(n.fraction_above_mid().is_none());
6738 }
6739
6740 #[test]
6741 fn test_zscore_fraction_above_mid_half_above() {
6742 let mut n = znorm(4);
6743 for v in [dec!(0), dec!(10), dec!(6), dec!(4)] { n.update(v); }
6744 let frac = n.fraction_above_mid().unwrap();
6746 assert!((frac - 0.5).abs() < 1e-9);
6747 }
6748
6749 #[test]
6750 fn test_zscore_normalized_range_none_for_empty_window() {
6751 let n = znorm(3);
6752 assert!(n.normalized_range().is_none());
6753 }
6754
6755 #[test]
6756 fn test_zscore_normalized_range_zero_for_uniform_window() {
6757 let mut n = znorm(3);
6758 for _ in 0..3 { n.update(dec!(10)); }
6759 assert_eq!(n.normalized_range(), Some(0.0));
6760 }
6761
6762 #[test]
6763 fn test_zscore_normalized_range_positive_for_varying_window() {
6764 let mut n = znorm(3);
6765 for v in [dec!(8), dec!(10), dec!(12)] { n.update(v); }
6766 let nr = n.normalized_range().unwrap();
6768 assert!((nr - 0.4).abs() < 1e-9);
6769 }
6770
6771 #[test]
6774 fn test_zscore_midpoint_none_for_empty_window() {
6775 assert!(znorm(3).midpoint().is_none());
6776 }
6777
6778 #[test]
6779 fn test_zscore_midpoint_correct_for_known_range() {
6780 let mut n = znorm(4);
6781 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
6782 assert_eq!(n.midpoint(), Some(dec!(25)));
6784 }
6785
6786 #[test]
6789 fn test_zscore_clamp_returns_value_unchanged_on_empty_window() {
6790 let n = znorm(3);
6791 assert_eq!(n.clamp_to_window(dec!(50)), dec!(50));
6792 }
6793
6794 #[test]
6795 fn test_zscore_clamp_clamps_to_min() {
6796 let mut n = znorm(3);
6797 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
6798 assert_eq!(n.clamp_to_window(dec!(-5)), dec!(10));
6799 }
6800
6801 #[test]
6802 fn test_zscore_clamp_clamps_to_max() {
6803 let mut n = znorm(3);
6804 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
6805 assert_eq!(n.clamp_to_window(dec!(100)), dec!(30));
6806 }
6807
6808 #[test]
6809 fn test_zscore_clamp_passes_through_in_range_value() {
6810 let mut n = znorm(3);
6811 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
6812 assert_eq!(n.clamp_to_window(dec!(15)), dec!(15));
6813 }
6814
6815 #[test]
6818 fn test_zscore_min_max_none_for_empty_window() {
6819 assert!(znorm(3).min_max().is_none());
6820 }
6821
6822 #[test]
6823 fn test_zscore_min_max_returns_correct_pair() {
6824 let mut n = znorm(4);
6825 for v in [dec!(5), dec!(15), dec!(10), dec!(20)] { n.update(v); }
6826 assert_eq!(n.min_max(), Some((dec!(5), dec!(20))));
6827 }
6828
6829 #[test]
6830 fn test_zscore_min_max_single_value() {
6831 let mut n = znorm(3);
6832 n.update(dec!(42));
6833 assert_eq!(n.min_max(), Some((dec!(42), dec!(42))));
6834 }
6835
6836 #[test]
6839 fn test_zscore_values_empty_for_empty_window() {
6840 assert!(znorm(3).values().is_empty());
6841 }
6842
6843 #[test]
6844 fn test_zscore_values_preserves_insertion_order() {
6845 let mut n = znorm(4);
6846 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
6847 assert_eq!(n.values(), vec![dec!(10), dec!(20), dec!(30), dec!(40)]);
6848 }
6849
6850 #[test]
6853 fn test_zscore_above_zero_fraction_none_for_empty_window() {
6854 assert!(znorm(3).above_zero_fraction().is_none());
6855 }
6856
6857 #[test]
6858 fn test_zscore_above_zero_fraction_zero_for_all_negative() {
6859 let mut n = znorm(3);
6860 for v in [dec!(-3), dec!(-2), dec!(-1)] { n.update(v); }
6861 assert_eq!(n.above_zero_fraction(), Some(0.0));
6862 }
6863
6864 #[test]
6865 fn test_zscore_above_zero_fraction_one_for_all_positive() {
6866 let mut n = znorm(3);
6867 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
6868 assert_eq!(n.above_zero_fraction(), Some(1.0));
6869 }
6870
6871 #[test]
6872 fn test_zscore_above_zero_fraction_half_for_mixed() {
6873 let mut n = znorm(4);
6874 for v in [dec!(-2), dec!(-1), dec!(1), dec!(2)] { n.update(v); }
6875 let frac = n.above_zero_fraction().unwrap();
6876 assert!((frac - 0.5).abs() < 1e-9);
6877 }
6878
6879 #[test]
6882 fn test_zscore_opt_none_for_empty_window() {
6883 assert!(znorm(3).z_score_opt(dec!(10)).is_none());
6884 }
6885
6886 #[test]
6887 fn test_zscore_opt_matches_normalize_for_populated_window() {
6888 let mut n = znorm(4);
6889 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
6890 let z_opt = n.z_score_opt(dec!(25)).unwrap();
6891 let z_norm = n.normalize(dec!(25)).unwrap();
6892 assert!((z_opt - z_norm).abs() < 1e-12);
6893 }
6894
6895 #[test]
6898 fn test_zscore_is_stable_false_for_empty_window() {
6899 assert!(!znorm(3).is_stable(2.0));
6900 }
6901
6902 #[test]
6903 fn test_zscore_is_stable_true_for_near_mean_value() {
6904 let mut n = znorm(5);
6905 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(30)] { n.update(v); }
6906 assert!(n.is_stable(2.0));
6908 }
6909
6910 #[test]
6911 fn test_zscore_is_stable_false_for_extreme_value() {
6912 let mut n = znorm(5);
6913 for v in [dec!(10), dec!(10), dec!(10), dec!(10), dec!(100)] { n.update(v); }
6914 assert!(!n.is_stable(1.0));
6916 }
6917
6918 #[test]
6921 fn test_zscore_window_values_above_via_znorm_empty() {
6922 assert!(znorm(3).window_values_above(dec!(5)).is_empty());
6923 }
6924
6925 #[test]
6926 fn test_zscore_window_values_above_via_znorm_filters() {
6927 let mut n = znorm(5);
6928 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
6929 let above = n.window_values_above(dec!(5));
6930 assert_eq!(above.len(), 2);
6931 assert!(above.contains(&dec!(7)));
6932 assert!(above.contains(&dec!(9)));
6933 }
6934
6935 #[test]
6936 fn test_zscore_window_values_below_via_znorm_empty() {
6937 assert!(znorm(3).window_values_below(dec!(5)).is_empty());
6938 }
6939
6940 #[test]
6941 fn test_zscore_window_values_below_via_znorm_filters() {
6942 let mut n = znorm(5);
6943 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
6944 let below = n.window_values_below(dec!(5));
6945 assert_eq!(below.len(), 2);
6946 assert!(below.contains(&dec!(1)));
6947 assert!(below.contains(&dec!(3)));
6948 }
6949
6950 #[test]
6953 fn test_zscore_fraction_above_none_for_empty_window() {
6954 assert!(znorm(3).fraction_above(dec!(5)).is_none());
6955 }
6956
6957 #[test]
6958 fn test_zscore_fraction_above_correct() {
6959 let mut n = znorm(5);
6960 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6961 let frac = n.fraction_above(dec!(3)).unwrap();
6963 assert!((frac - 0.4).abs() < 1e-9);
6964 }
6965
6966 #[test]
6967 fn test_zscore_fraction_below_none_for_empty_window() {
6968 assert!(znorm(3).fraction_below(dec!(5)).is_none());
6969 }
6970
6971 #[test]
6972 fn test_zscore_fraction_below_correct() {
6973 let mut n = znorm(5);
6974 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6975 let frac = n.fraction_below(dec!(3)).unwrap();
6977 assert!((frac - 0.4).abs() < 1e-9);
6978 }
6979
6980 #[test]
6983 fn test_zscore_window_values_above_empty_window() {
6984 assert!(znorm(3).window_values_above(dec!(0)).is_empty());
6985 }
6986
6987 #[test]
6988 fn test_zscore_window_values_above_filters_correctly() {
6989 let mut n = znorm(5);
6990 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
6991 let above = n.window_values_above(dec!(5));
6992 assert_eq!(above.len(), 2);
6993 assert!(above.contains(&dec!(7)));
6994 assert!(above.contains(&dec!(9)));
6995 }
6996
6997 #[test]
6998 fn test_zscore_window_values_below_empty_window() {
6999 assert!(znorm(3).window_values_below(dec!(0)).is_empty());
7000 }
7001
7002 #[test]
7003 fn test_zscore_window_values_below_filters_correctly() {
7004 let mut n = znorm(5);
7005 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
7006 let below = n.window_values_below(dec!(5));
7007 assert_eq!(below.len(), 2);
7008 assert!(below.contains(&dec!(1)));
7009 assert!(below.contains(&dec!(3)));
7010 }
7011
7012 #[test]
7015 fn test_zscore_percentile_rank_none_for_empty_window() {
7016 assert!(znorm(3).percentile_rank(dec!(5)).is_none());
7017 }
7018
7019 #[test]
7020 fn test_zscore_percentile_rank_correct() {
7021 let mut n = znorm(5);
7022 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7023 let rank = n.percentile_rank(dec!(3)).unwrap();
7025 assert!((rank - 0.6).abs() < 1e-9);
7026 }
7027
7028 #[test]
7031 fn test_zscore_count_equal_zero_for_no_match() {
7032 let mut n = znorm(3);
7033 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7034 assert_eq!(n.count_equal(dec!(99)), 0);
7035 }
7036
7037 #[test]
7038 fn test_zscore_count_equal_counts_duplicates() {
7039 let mut n = znorm(5);
7040 for v in [dec!(5), dec!(5), dec!(3), dec!(5), dec!(2)] { n.update(v); }
7041 assert_eq!(n.count_equal(dec!(5)), 3);
7042 }
7043
7044 #[test]
7047 fn test_zscore_median_none_for_empty_window() {
7048 assert!(znorm(3).median().is_none());
7049 }
7050
7051 #[test]
7052 fn test_zscore_median_correct_for_odd_count() {
7053 let mut n = znorm(5);
7054 for v in [dec!(3), dec!(1), dec!(5), dec!(4), dec!(2)] { n.update(v); }
7055 assert_eq!(n.median(), Some(dec!(3)));
7057 }
7058
7059 #[test]
7062 fn test_zscore_rolling_range_none_for_empty() {
7063 assert!(znorm(3).rolling_range().is_none());
7064 }
7065
7066 #[test]
7067 fn test_zscore_rolling_range_correct() {
7068 let mut n = znorm(5);
7069 for v in [dec!(10), dec!(50), dec!(30), dec!(20), dec!(40)] { n.update(v); }
7070 assert_eq!(n.rolling_range(), Some(dec!(40)));
7071 }
7072
7073 #[test]
7076 fn test_zscore_skewness_none_for_fewer_than_3() {
7077 let mut n = znorm(5);
7078 n.update(dec!(1)); n.update(dec!(2));
7079 assert!(n.skewness().is_none());
7080 }
7081
7082 #[test]
7083 fn test_zscore_skewness_near_zero_for_symmetric_data() {
7084 let mut n = znorm(5);
7085 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7086 let s = n.skewness().unwrap();
7087 assert!(s.abs() < 0.5);
7088 }
7089
7090 #[test]
7093 fn test_zscore_kurtosis_none_for_fewer_than_4() {
7094 let mut n = znorm(5);
7095 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7096 assert!(n.kurtosis().is_none());
7097 }
7098
7099 #[test]
7100 fn test_zscore_kurtosis_returns_f64_for_populated_window() {
7101 let mut n = znorm(5);
7102 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7103 assert!(n.kurtosis().is_some());
7104 }
7105
7106 #[test]
7109 fn test_zscore_autocorrelation_none_for_single_value() {
7110 let mut n = znorm(3);
7111 n.update(dec!(1));
7112 assert!(n.autocorrelation_lag1().is_none());
7113 }
7114
7115 #[test]
7116 fn test_zscore_autocorrelation_positive_for_trending_data() {
7117 let mut n = znorm(5);
7118 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7119 let ac = n.autocorrelation_lag1().unwrap();
7120 assert!(ac > 0.0);
7121 }
7122
7123 #[test]
7126 fn test_zscore_trend_consistency_none_for_single_value() {
7127 let mut n = znorm(3);
7128 n.update(dec!(1));
7129 assert!(n.trend_consistency().is_none());
7130 }
7131
7132 #[test]
7133 fn test_zscore_trend_consistency_one_for_strictly_rising() {
7134 let mut n = znorm(5);
7135 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7136 let tc = n.trend_consistency().unwrap();
7137 assert!((tc - 1.0).abs() < 1e-9);
7138 }
7139
7140 #[test]
7141 fn test_zscore_trend_consistency_zero_for_strictly_falling() {
7142 let mut n = znorm(5);
7143 for v in [dec!(5), dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
7144 let tc = n.trend_consistency().unwrap();
7145 assert!((tc - 0.0).abs() < 1e-9);
7146 }
7147
7148 #[test]
7151 fn test_zscore_cov_none_for_empty_window() {
7152 assert!(znorm(3).coefficient_of_variation().is_none());
7153 }
7154
7155 #[test]
7156 fn test_zscore_cov_positive_for_varied_data() {
7157 let mut n = znorm(5);
7158 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
7159 let cov = n.coefficient_of_variation().unwrap();
7160 assert!(cov > 0.0);
7161 }
7162
7163 #[test]
7166 fn test_zscore_mad_none_for_empty() {
7167 assert!(znorm(3).mean_absolute_deviation().is_none());
7168 }
7169
7170 #[test]
7171 fn test_zscore_mad_zero_for_identical_values() {
7172 let mut n = znorm(3);
7173 for v in [dec!(5), dec!(5), dec!(5)] { n.update(v); }
7174 let mad = n.mean_absolute_deviation().unwrap();
7175 assert!((mad - 0.0).abs() < 1e-9);
7176 }
7177
7178 #[test]
7179 fn test_zscore_mad_positive_for_varied_data() {
7180 let mut n = znorm(4);
7181 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7182 let mad = n.mean_absolute_deviation().unwrap();
7183 assert!(mad > 0.0);
7184 }
7185
7186 #[test]
7189 fn test_zscore_percentile_of_latest_none_for_empty() {
7190 assert!(znorm(3).percentile_of_latest().is_none());
7191 }
7192
7193 #[test]
7194 fn test_zscore_percentile_of_latest_returns_some_after_update() {
7195 let mut n = znorm(4);
7196 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7197 assert!(n.percentile_of_latest().is_some());
7198 }
7199
7200 #[test]
7201 fn test_zscore_percentile_of_latest_max_has_high_rank() {
7202 let mut n = znorm(5);
7203 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7204 let rank = n.percentile_of_latest().unwrap();
7205 assert!(rank >= 0.9, "max value should have rank near 1.0, got {}", rank);
7206 }
7207
7208 #[test]
7211 fn test_zscore_tail_ratio_none_for_empty() {
7212 assert!(znorm(4).tail_ratio().is_none());
7213 }
7214
7215 #[test]
7216 fn test_zscore_tail_ratio_one_for_identical_values() {
7217 let mut n = znorm(4);
7218 for _ in 0..4 { n.update(dec!(7)); }
7219 let r = n.tail_ratio().unwrap();
7220 assert!((r - 1.0).abs() < 1e-9, "expected 1.0, got {}", r);
7221 }
7222
7223 #[test]
7224 fn test_zscore_tail_ratio_above_one_with_outlier() {
7225 let mut n = znorm(5);
7226 for v in [dec!(1), dec!(1), dec!(1), dec!(1), dec!(10)] { n.update(v); }
7227 let r = n.tail_ratio().unwrap();
7228 assert!(r > 1.0, "outlier should push ratio above 1.0, got {}", r);
7229 }
7230
7231 #[test]
7234 fn test_zscore_z_score_of_min_none_for_empty() {
7235 assert!(znorm(4).z_score_of_min().is_none());
7236 }
7237
7238 #[test]
7239 fn test_zscore_z_score_of_max_none_for_empty() {
7240 assert!(znorm(4).z_score_of_max().is_none());
7241 }
7242
7243 #[test]
7244 fn test_zscore_z_score_of_min_negative_for_varied_window() {
7245 let mut n = znorm(5);
7246 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7247 let z = n.z_score_of_min().unwrap();
7248 assert!(z < 0.0, "z-score of min should be negative, got {}", z);
7249 }
7250
7251 #[test]
7252 fn test_zscore_z_score_of_max_positive_for_varied_window() {
7253 let mut n = znorm(5);
7254 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7255 let z = n.z_score_of_max().unwrap();
7256 assert!(z > 0.0, "z-score of max should be positive, got {}", z);
7257 }
7258
7259 #[test]
7262 fn test_zscore_window_entropy_none_for_empty() {
7263 assert!(znorm(4).window_entropy().is_none());
7264 }
7265
7266 #[test]
7267 fn test_zscore_window_entropy_zero_for_identical_values() {
7268 let mut n = znorm(3);
7269 for _ in 0..3 { n.update(dec!(5)); }
7270 let e = n.window_entropy().unwrap();
7271 assert!((e - 0.0).abs() < 1e-9, "identical values should have zero entropy, got {}", e);
7272 }
7273
7274 #[test]
7275 fn test_zscore_window_entropy_positive_for_varied_values() {
7276 let mut n = znorm(4);
7277 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7278 let e = n.window_entropy().unwrap();
7279 assert!(e > 0.0, "varied values should have positive entropy, got {}", e);
7280 }
7281
7282 #[test]
7285 fn test_zscore_normalized_std_dev_none_for_empty() {
7286 assert!(znorm(4).normalized_std_dev().is_none());
7287 }
7288
7289 #[test]
7290 fn test_zscore_normalized_std_dev_positive_for_varied_values() {
7291 let mut n = znorm(4);
7292 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7293 let r = n.normalized_std_dev().unwrap();
7294 assert!(r > 0.0, "expected positive normalized std dev, got {}", r);
7295 }
7296
7297 #[test]
7300 fn test_zscore_value_above_mean_count_none_for_empty() {
7301 assert!(znorm(4).value_above_mean_count().is_none());
7302 }
7303
7304 #[test]
7305 fn test_zscore_value_above_mean_count_correct() {
7306 let mut n = znorm(4);
7307 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7308 assert_eq!(n.value_above_mean_count().unwrap(), 2);
7310 }
7311
7312 #[test]
7315 fn test_zscore_consecutive_above_mean_none_for_empty() {
7316 assert!(znorm(4).consecutive_above_mean().is_none());
7317 }
7318
7319 #[test]
7320 fn test_zscore_consecutive_above_mean_correct() {
7321 let mut n = znorm(4);
7322 for v in [dec!(1), dec!(5), dec!(6), dec!(7)] { n.update(v); }
7323 assert_eq!(n.consecutive_above_mean().unwrap(), 3);
7325 }
7326
7327 #[test]
7330 fn test_zscore_above_threshold_fraction_none_for_empty() {
7331 assert!(znorm(4).above_threshold_fraction(dec!(5)).is_none());
7332 }
7333
7334 #[test]
7335 fn test_zscore_above_threshold_fraction_correct() {
7336 let mut n = znorm(4);
7337 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7338 let f = n.above_threshold_fraction(dec!(2)).unwrap();
7339 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
7340 }
7341
7342 #[test]
7343 fn test_zscore_below_threshold_fraction_none_for_empty() {
7344 assert!(znorm(4).below_threshold_fraction(dec!(5)).is_none());
7345 }
7346
7347 #[test]
7348 fn test_zscore_below_threshold_fraction_correct() {
7349 let mut n = znorm(4);
7350 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7351 let f = n.below_threshold_fraction(dec!(3)).unwrap();
7352 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
7353 }
7354
7355 #[test]
7358 fn test_zscore_lag_k_autocorrelation_none_for_zero_k() {
7359 let mut n = znorm(5);
7360 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7361 assert!(n.lag_k_autocorrelation(0).is_none());
7362 }
7363
7364 #[test]
7365 fn test_zscore_lag_k_autocorrelation_none_when_k_gte_len() {
7366 let mut n = znorm(3);
7367 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7368 assert!(n.lag_k_autocorrelation(3).is_none());
7369 }
7370
7371 #[test]
7372 fn test_zscore_lag_k_autocorrelation_positive_for_trend() {
7373 let mut n = znorm(6);
7374 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6)] { n.update(v); }
7375 let ac = n.lag_k_autocorrelation(1).unwrap();
7376 assert!(ac > 0.0, "trending series should have positive AC, got {}", ac);
7377 }
7378
7379 #[test]
7382 fn test_zscore_half_life_estimate_none_for_fewer_than_3() {
7383 let mut n = znorm(3);
7384 n.update(dec!(1)); n.update(dec!(2));
7385 assert!(n.half_life_estimate().is_none());
7386 }
7387
7388 #[test]
7389 fn test_zscore_half_life_no_panic_for_alternating() {
7390 let mut n = znorm(6);
7391 for v in [dec!(10), dec!(5), dec!(10), dec!(5), dec!(10), dec!(5)] { n.update(v); }
7392 let _ = n.half_life_estimate();
7393 }
7394
7395 #[test]
7398 fn test_zscore_geometric_mean_none_for_empty() {
7399 assert!(znorm(4).geometric_mean().is_none());
7400 }
7401
7402 #[test]
7403 fn test_zscore_geometric_mean_correct_for_powers_of_2() {
7404 let mut n = znorm(4);
7405 for v in [dec!(1), dec!(2), dec!(4), dec!(8)] { n.update(v); }
7406 let gm = n.geometric_mean().unwrap();
7407 assert!((gm - 64.0f64.powf(0.25)).abs() < 1e-6, "got {}", gm);
7408 }
7409
7410 #[test]
7413 fn test_zscore_harmonic_mean_none_for_empty() {
7414 assert!(znorm(4).harmonic_mean().is_none());
7415 }
7416
7417 #[test]
7418 fn test_zscore_harmonic_mean_positive_for_positive_values() {
7419 let mut n = znorm(4);
7420 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7421 let hm = n.harmonic_mean().unwrap();
7422 assert!(hm > 0.0 && hm < 4.0, "HM should be in (0, max), got {}", hm);
7423 }
7424
7425 #[test]
7428 fn test_zscore_range_normalized_value_none_for_empty() {
7429 assert!(znorm(4).range_normalized_value(dec!(5)).is_none());
7430 }
7431
7432 #[test]
7433 fn test_zscore_range_normalized_value_in_range() {
7434 let mut n = znorm(4);
7435 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7436 let r = n.range_normalized_value(dec!(2)).unwrap();
7437 assert!(r >= 0.0 && r <= 1.0, "expected [0,1], got {}", r);
7438 }
7439
7440 #[test]
7443 fn test_zscore_distance_from_median_none_for_empty() {
7444 assert!(znorm(4).distance_from_median(dec!(5)).is_none());
7445 }
7446
7447 #[test]
7448 fn test_zscore_distance_from_median_zero_at_median() {
7449 let mut n = znorm(5);
7450 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7451 let d = n.distance_from_median(dec!(3)).unwrap();
7452 assert!((d - 0.0).abs() < 1e-9, "distance from median=3 should be 0, got {}", d);
7453 }
7454
7455 #[test]
7456 fn test_zscore_momentum_none_for_single_value() {
7457 let mut n = znorm(5);
7458 n.update(dec!(10));
7459 assert!(n.momentum().is_none());
7460 }
7461
7462 #[test]
7463 fn test_zscore_momentum_positive_for_rising_window() {
7464 let mut n = znorm(3);
7465 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7466 let m = n.momentum().unwrap();
7467 assert!(m > 0.0, "rising window → positive momentum, got {}", m);
7468 }
7469
7470 #[test]
7471 fn test_zscore_value_rank_none_for_empty() {
7472 assert!(znorm(4).value_rank(dec!(5)).is_none());
7473 }
7474
7475 #[test]
7476 fn test_zscore_value_rank_extremes() {
7477 let mut n = znorm(4);
7478 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7479 let low = n.value_rank(dec!(0)).unwrap();
7480 assert!((low - 0.0).abs() < 1e-9, "got {}", low);
7481 let high = n.value_rank(dec!(5)).unwrap();
7482 assert!((high - 1.0).abs() < 1e-9, "got {}", high);
7483 }
7484
7485 #[test]
7486 fn test_zscore_coeff_of_variation_none_for_single_value() {
7487 let mut n = znorm(5);
7488 n.update(dec!(10));
7489 assert!(n.coeff_of_variation().is_none());
7490 }
7491
7492 #[test]
7493 fn test_zscore_coeff_of_variation_positive_for_spread() {
7494 let mut n = znorm(4);
7495 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
7496 let cv = n.coeff_of_variation().unwrap();
7497 assert!(cv > 0.0, "expected positive CV, got {}", cv);
7498 }
7499
7500 #[test]
7501 fn test_zscore_quantile_range_none_for_empty() {
7502 assert!(znorm(4).quantile_range().is_none());
7503 }
7504
7505 #[test]
7506 fn test_zscore_quantile_range_non_negative() {
7507 let mut n = znorm(5);
7508 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7509 let iqr = n.quantile_range().unwrap();
7510 assert!(iqr >= 0.0, "IQR should be non-negative, got {}", iqr);
7511 }
7512
7513 #[test]
7516 fn test_zscore_upper_quartile_none_for_empty() {
7517 assert!(znorm(4).upper_quartile().is_none());
7518 }
7519
7520 #[test]
7521 fn test_zscore_lower_quartile_none_for_empty() {
7522 assert!(znorm(4).lower_quartile().is_none());
7523 }
7524
7525 #[test]
7526 fn test_zscore_upper_ge_lower_quartile() {
7527 let mut n = znorm(8);
7528 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50), dec!(60), dec!(70), dec!(80)] {
7529 n.update(v);
7530 }
7531 let q3 = n.upper_quartile().unwrap();
7532 let q1 = n.lower_quartile().unwrap();
7533 assert!(q3 >= q1, "Q3 ({}) should be >= Q1 ({})", q3, q1);
7534 }
7535
7536 #[test]
7539 fn test_zscore_sign_change_rate_none_for_fewer_than_3() {
7540 let mut n = znorm(4);
7541 n.update(dec!(1));
7542 n.update(dec!(2));
7543 assert!(n.sign_change_rate().is_none());
7544 }
7545
7546 #[test]
7547 fn test_zscore_sign_change_rate_one_for_zigzag() {
7548 let mut n = znorm(5);
7549 for v in [dec!(1), dec!(3), dec!(1), dec!(3), dec!(1)] { n.update(v); }
7550 let r = n.sign_change_rate().unwrap();
7551 assert!((r - 1.0).abs() < 1e-9, "zigzag should give 1.0, got {}", r);
7552 }
7553
7554 #[test]
7555 fn test_zscore_sign_change_rate_zero_for_monotone() {
7556 let mut n = znorm(5);
7557 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7558 let r = n.sign_change_rate().unwrap();
7559 assert!((r - 0.0).abs() < 1e-9, "monotone should give 0.0, got {}", r);
7560 }
7561
7562 #[test]
7565 fn test_zscore_trimmed_mean_none_for_empty() {
7566 assert!(znorm(4).trimmed_mean(0.1).is_none());
7567 }
7568
7569 #[test]
7570 fn test_zscore_trimmed_mean_equals_mean_at_zero_trim() {
7571 let mut n = znorm(4);
7572 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
7573 let tm = n.trimmed_mean(0.0).unwrap();
7574 let m = n.mean().unwrap().to_f64().unwrap();
7575 assert!((tm - m).abs() < 1e-9, "0% trim should equal mean, got tm={} m={}", tm, m);
7576 }
7577
7578 #[test]
7579 fn test_zscore_trimmed_mean_reduces_outlier_effect() {
7580 let mut n = znorm(5);
7581 for v in [dec!(10), dec!(10), dec!(10), dec!(10), dec!(1000)] { n.update(v); }
7582 let tm = n.trimmed_mean(0.2).unwrap();
7584 let m = n.mean().unwrap().to_f64().unwrap();
7585 assert!(tm < m, "trimmed mean should be less than mean when outlier trimmed, tm={} m={}", tm, m);
7586 }
7587
7588 #[test]
7591 fn test_zscore_linear_trend_slope_none_for_single_value() {
7592 let mut n = znorm(4);
7593 n.update(dec!(10));
7594 assert!(n.linear_trend_slope().is_none());
7595 }
7596
7597 #[test]
7598 fn test_zscore_linear_trend_slope_positive_for_rising() {
7599 let mut n = znorm(4);
7600 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7601 let slope = n.linear_trend_slope().unwrap();
7602 assert!(slope > 0.0, "rising window → positive slope, got {}", slope);
7603 }
7604
7605 #[test]
7606 fn test_zscore_linear_trend_slope_negative_for_falling() {
7607 let mut n = znorm(4);
7608 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
7609 let slope = n.linear_trend_slope().unwrap();
7610 assert!(slope < 0.0, "falling window → negative slope, got {}", slope);
7611 }
7612
7613 #[test]
7614 fn test_zscore_linear_trend_slope_zero_for_flat() {
7615 let mut n = znorm(4);
7616 for v in [dec!(5), dec!(5), dec!(5), dec!(5)] { n.update(v); }
7617 let slope = n.linear_trend_slope().unwrap();
7618 assert!(slope.abs() < 1e-9, "flat window → slope=0, got {}", slope);
7619 }
7620
7621 #[test]
7624 fn test_zscore_variance_ratio_none_for_few_values() {
7625 let mut n = znorm(3);
7626 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7627 assert!(n.variance_ratio().is_none());
7628 }
7629
7630 #[test]
7631 fn test_zscore_variance_ratio_gt_one_for_decreasing_vol() {
7632 let mut n = znorm(6);
7633 for v in [dec!(1), dec!(10), dec!(1), dec!(5), dec!(6), dec!(5)] { n.update(v); }
7635 let r = n.variance_ratio().unwrap();
7636 assert!(r > 1.0, "first half more volatile → ratio > 1, got {}", r);
7637 }
7638
7639 #[test]
7642 fn test_zscore_z_score_trend_slope_none_for_single_value() {
7643 let mut n = znorm(4);
7644 n.update(dec!(10));
7645 assert!(n.z_score_trend_slope().is_none());
7646 }
7647
7648 #[test]
7649 fn test_zscore_z_score_trend_slope_positive_for_rising() {
7650 let mut n = znorm(5);
7651 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7652 let slope = n.z_score_trend_slope().unwrap();
7653 assert!(slope > 0.0, "rising window → positive z-score slope, got {}", slope);
7654 }
7655
7656 #[test]
7659 fn test_zscore_mean_absolute_change_none_for_single_value() {
7660 let mut n = znorm(4);
7661 n.update(dec!(10));
7662 assert!(n.mean_absolute_change().is_none());
7663 }
7664
7665 #[test]
7666 fn test_zscore_mean_absolute_change_zero_for_constant() {
7667 let mut n = znorm(4);
7668 for _ in 0..4 { n.update(dec!(5)); }
7669 let mac = n.mean_absolute_change().unwrap();
7670 assert!(mac.abs() < 1e-9, "constant window → MAC=0, got {}", mac);
7671 }
7672
7673 #[test]
7674 fn test_zscore_mean_absolute_change_positive_for_varying() {
7675 let mut n = znorm(4);
7676 for v in [dec!(1), dec!(3), dec!(2), dec!(5)] { n.update(v); }
7677 let mac = n.mean_absolute_change().unwrap();
7678 assert!(mac > 0.0, "varying window → MAC > 0, got {}", mac);
7679 }
7680
7681 #[test]
7684 fn test_zscore_monotone_increase_fraction_none_for_single() {
7685 let mut n = znorm(4);
7686 n.update(dec!(5));
7687 assert!(n.monotone_increase_fraction().is_none());
7688 }
7689
7690 #[test]
7691 fn test_zscore_monotone_increase_fraction_one_for_rising() {
7692 let mut n = znorm(4);
7693 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7694 let f = n.monotone_increase_fraction().unwrap();
7695 assert!((f - 1.0).abs() < 1e-9, "all rising → fraction=1, got {}", f);
7696 }
7697
7698 #[test]
7699 fn test_zscore_abs_max_none_for_empty() {
7700 let n = znorm(4);
7701 assert!(n.abs_max().is_none());
7702 }
7703
7704 #[test]
7705 fn test_zscore_abs_max_returns_max_absolute() {
7706 let mut n = znorm(4);
7707 for v in [dec!(1), dec!(3), dec!(2)] { n.update(v); }
7708 assert_eq!(n.abs_max().unwrap(), dec!(3));
7709 }
7710
7711 #[test]
7712 fn test_zscore_max_count_none_for_empty() {
7713 let n = znorm(4);
7714 assert!(n.max_count().is_none());
7715 }
7716
7717 #[test]
7718 fn test_zscore_max_count_correct() {
7719 let mut n = znorm(4);
7720 for v in [dec!(1), dec!(5), dec!(3), dec!(5)] { n.update(v); }
7721 assert_eq!(n.max_count().unwrap(), 2);
7722 }
7723
7724 #[test]
7725 fn test_zscore_mean_ratio_none_for_single() {
7726 let mut n = znorm(4);
7727 n.update(dec!(10));
7728 assert!(n.mean_ratio().is_none());
7729 }
7730
7731 #[test]
7734 fn test_zscore_exponential_weighted_mean_none_for_empty() {
7735 let n = znorm(4);
7736 assert!(n.exponential_weighted_mean(0.5).is_none());
7737 }
7738
7739 #[test]
7740 fn test_zscore_exponential_weighted_mean_returns_value() {
7741 let mut n = znorm(4);
7742 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7743 let ewm = n.exponential_weighted_mean(0.5).unwrap();
7744 assert!(ewm > 0.0, "EWM should be positive, got {}", ewm);
7745 }
7746
7747 #[test]
7748 fn test_zscore_peak_to_trough_none_for_empty() {
7749 let n = znorm(4);
7750 assert!(n.peak_to_trough_ratio().is_none());
7751 }
7752
7753 #[test]
7754 fn test_zscore_peak_to_trough_correct() {
7755 let mut n = znorm(4);
7756 for v in [dec!(2), dec!(4), dec!(1), dec!(8)] { n.update(v); }
7757 let r = n.peak_to_trough_ratio().unwrap();
7758 assert!((r - 8.0).abs() < 1e-9, "max=8, min=1 → ratio=8, got {}", r);
7759 }
7760
7761 #[test]
7762 fn test_zscore_second_moment_none_for_empty() {
7763 let n = znorm(4);
7764 assert!(n.second_moment().is_none());
7765 }
7766
7767 #[test]
7768 fn test_zscore_second_moment_correct() {
7769 let mut n = znorm(4);
7770 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7771 let m = n.second_moment().unwrap();
7772 assert!((m - 14.0 / 3.0).abs() < 1e-9, "second moment ≈ 4.667, got {}", m);
7773 }
7774
7775 #[test]
7776 fn test_zscore_range_over_mean_none_for_empty() {
7777 let n = znorm(4);
7778 assert!(n.range_over_mean().is_none());
7779 }
7780
7781 #[test]
7782 fn test_zscore_range_over_mean_positive() {
7783 let mut n = znorm(4);
7784 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7785 let r = n.range_over_mean().unwrap();
7786 assert!(r > 0.0, "range/mean should be positive, got {}", r);
7787 }
7788
7789 #[test]
7790 fn test_zscore_above_median_fraction_none_for_empty() {
7791 let n = znorm(4);
7792 assert!(n.above_median_fraction().is_none());
7793 }
7794
7795 #[test]
7796 fn test_zscore_above_median_fraction_in_range() {
7797 let mut n = znorm(4);
7798 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7799 let f = n.above_median_fraction().unwrap();
7800 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
7801 }
7802
7803 #[test]
7806 fn test_zscore_interquartile_mean_none_for_empty() {
7807 let n = znorm(4);
7808 assert!(n.interquartile_mean().is_none());
7809 }
7810
7811 #[test]
7812 fn test_zscore_outlier_fraction_none_for_empty() {
7813 let n = znorm(4);
7814 assert!(n.outlier_fraction(2.0).is_none());
7815 }
7816
7817 #[test]
7818 fn test_zscore_outlier_fraction_zero_for_constant() {
7819 let mut n = znorm(4);
7820 for _ in 0..4 { n.update(dec!(5)); }
7821 let f = n.outlier_fraction(1.0).unwrap();
7822 assert!(f.abs() < 1e-9, "constant window → no outliers, got {}", f);
7823 }
7824
7825 #[test]
7826 fn test_zscore_sign_flip_count_none_for_single() {
7827 let mut n = znorm(4);
7828 n.update(dec!(1));
7829 assert!(n.sign_flip_count().is_none());
7830 }
7831
7832 #[test]
7833 fn test_zscore_sign_flip_count_correct() {
7834 let mut n = znorm(6);
7835 for v in [dec!(1), dec!(-1), dec!(1), dec!(-1)] { n.update(v); }
7836 let c = n.sign_flip_count().unwrap();
7837 assert_eq!(c, 3, "3 sign flips expected, got {}", c);
7838 }
7839
7840 #[test]
7841 fn test_zscore_rms_none_for_empty() {
7842 let n = znorm(4);
7843 assert!(n.rms().is_none());
7844 }
7845
7846 #[test]
7847 fn test_zscore_rms_positive_for_nonzero_values() {
7848 let mut n = znorm(4);
7849 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7850 let r = n.rms().unwrap();
7851 assert!(r > 0.0, "RMS should be positive, got {}", r);
7852 }
7853
7854 #[test]
7857 fn test_zscore_distinct_count_zero_for_empty() {
7858 let n = znorm(4);
7859 assert_eq!(n.distinct_count(), 0);
7860 }
7861
7862 #[test]
7863 fn test_zscore_distinct_count_correct() {
7864 let mut n = znorm(4);
7865 for v in [dec!(1), dec!(1), dec!(2), dec!(3)] { n.update(v); }
7866 assert_eq!(n.distinct_count(), 3);
7867 }
7868
7869 #[test]
7870 fn test_zscore_max_fraction_none_for_empty() {
7871 let n = znorm(4);
7872 assert!(n.max_fraction().is_none());
7873 }
7874
7875 #[test]
7876 fn test_zscore_max_fraction_correct() {
7877 let mut n = znorm(4);
7878 for v in [dec!(1), dec!(2), dec!(3), dec!(3)] { n.update(v); }
7879 let f = n.max_fraction().unwrap();
7880 assert!((f - 0.5).abs() < 1e-9, "2/4 are max → 0.5, got {}", f);
7881 }
7882
7883 #[test]
7884 fn test_zscore_latest_minus_mean_none_for_empty() {
7885 let n = znorm(4);
7886 assert!(n.latest_minus_mean().is_none());
7887 }
7888
7889 #[test]
7890 fn test_zscore_latest_to_mean_ratio_none_for_empty() {
7891 let n = znorm(4);
7892 assert!(n.latest_to_mean_ratio().is_none());
7893 }
7894
7895 #[test]
7896 fn test_zscore_latest_to_mean_ratio_one_for_constant() {
7897 let mut n = znorm(4);
7898 for _ in 0..4 { n.update(dec!(5)); }
7899 let r = n.latest_to_mean_ratio().unwrap();
7900 assert!((r - 1.0).abs() < 1e-9, "latest=mean → ratio=1, got {}", r);
7901 }
7902
7903 #[test]
7906 fn test_zscore_below_mean_fraction_none_for_empty() {
7907 assert!(znorm(4).below_mean_fraction().is_none());
7908 }
7909
7910 #[test]
7911 fn test_zscore_below_mean_fraction_symmetric_data() {
7912 let mut n = znorm(4);
7913 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7914 let f = n.below_mean_fraction().unwrap();
7916 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
7917 }
7918
7919 #[test]
7920 fn test_zscore_tail_variance_none_for_small_window() {
7921 let mut n = znorm(3);
7922 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7923 assert!(n.tail_variance().is_none());
7924 }
7925
7926 #[test]
7927 fn test_zscore_tail_variance_nonneg_for_varied_data() {
7928 let mut n = znorm(6);
7929 for v in [dec!(1), dec!(2), dec!(5), dec!(6), dec!(9), dec!(10)] { n.update(v); }
7930 let tv = n.tail_variance().unwrap();
7931 assert!(tv >= 0.0, "tail variance should be non-negative, got {}", tv);
7932 }
7933
7934 #[test]
7937 fn test_zscore_new_max_count_zero_for_empty() {
7938 let n = znorm(4);
7939 assert_eq!(n.new_max_count(), 0);
7940 }
7941
7942 #[test]
7943 fn test_zscore_new_max_count_all_rising() {
7944 let mut n = znorm(4);
7945 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7946 assert_eq!(n.new_max_count(), 4, "each value is a new high");
7947 }
7948
7949 #[test]
7950 fn test_zscore_new_min_count_zero_for_empty() {
7951 let n = znorm(4);
7952 assert_eq!(n.new_min_count(), 0);
7953 }
7954
7955 #[test]
7956 fn test_zscore_new_min_count_all_falling() {
7957 let mut n = znorm(4);
7958 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
7959 assert_eq!(n.new_min_count(), 4, "each value is a new low");
7960 }
7961
7962 #[test]
7963 fn test_zscore_zero_fraction_none_for_empty() {
7964 let n = znorm(4);
7965 assert!(n.zero_fraction().is_none());
7966 }
7967
7968 #[test]
7969 fn test_zscore_zero_fraction_zero_when_no_zeros() {
7970 let mut n = znorm(4);
7971 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7972 let f = n.zero_fraction().unwrap();
7973 assert!(f.abs() < 1e-9, "no zeros → fraction=0, got {}", f);
7974 }
7975
7976 #[test]
7979 fn test_zscore_cumulative_sum_zero_for_empty() {
7980 let n = znorm(4);
7981 assert_eq!(n.cumulative_sum(), rust_decimal::Decimal::ZERO);
7982 }
7983
7984 #[test]
7985 fn test_zscore_cumulative_sum_correct() {
7986 let mut n = znorm(4);
7987 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7988 assert_eq!(n.cumulative_sum(), dec!(6));
7989 }
7990
7991 #[test]
7992 fn test_zscore_max_to_min_ratio_none_for_empty() {
7993 assert!(znorm(4).max_to_min_ratio().is_none());
7994 }
7995
7996 #[test]
7997 fn test_zscore_max_to_min_ratio_one_for_constant() {
7998 let mut n = znorm(4);
7999 for _ in 0..4 { n.update(dec!(5)); }
8000 let r = n.max_to_min_ratio().unwrap();
8001 assert!((r - 1.0).abs() < 1e-9, "constant window → ratio=1, got {}", r);
8002 }
8003
8004 #[test]
8007 fn test_zscore_above_midpoint_fraction_none_for_empty() {
8008 assert!(znorm(4).above_midpoint_fraction().is_none());
8009 }
8010
8011 #[test]
8012 fn test_zscore_above_midpoint_fraction_half_for_symmetric() {
8013 let mut n = znorm(4);
8014 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
8015 let f = n.above_midpoint_fraction().unwrap();
8017 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
8018 }
8019
8020 #[test]
8021 fn test_zscore_positive_fraction_none_for_empty() {
8022 assert!(znorm(4).positive_fraction().is_none());
8023 }
8024
8025 #[test]
8026 fn test_zscore_positive_fraction_zero_for_all_nonpositive() {
8027 let mut n = znorm(3);
8028 for v in [dec!(-3), dec!(-1), dec!(0)] { n.update(v); }
8029 let f = n.positive_fraction().unwrap();
8030 assert!((f - 0.0).abs() < 1e-9, "no positives → 0.0, got {}", f);
8031 }
8032
8033 #[test]
8034 fn test_zscore_above_mean_fraction_none_for_empty() {
8035 assert!(znorm(4).above_mean_fraction().is_none());
8036 }
8037
8038 #[test]
8039 fn test_zscore_above_mean_fraction_half_for_symmetric() {
8040 let mut n = znorm(4);
8041 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
8042 let f = n.above_mean_fraction().unwrap();
8044 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
8045 }
8046}