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 pub fn window_iqr(&self) -> Option<Decimal> {
1773 if self.window.is_empty() {
1774 return None;
1775 }
1776 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
1777 sorted.sort();
1778 let n = sorted.len();
1779 let q1 = sorted[n / 4];
1780 let q3 = sorted[(3 * n) / 4];
1781 Some(q3 - q1)
1782 }
1783
1784 pub fn run_length_mean(&self) -> Option<f64> {
1788 if self.window.len() < 2 {
1789 return None;
1790 }
1791 let vals: Vec<Decimal> = self.window.iter().copied().collect();
1792 let mut runs: Vec<usize> = Vec::new();
1793 let mut run_len = 1usize;
1794 for w in vals.windows(2) {
1795 if w[1] >= w[0] {
1796 run_len += 1;
1797 } else {
1798 runs.push(run_len);
1799 run_len = 1;
1800 }
1801 }
1802 runs.push(run_len);
1803 Some(runs.iter().sum::<usize>() as f64 / runs.len() as f64)
1804 }
1805
1806}
1807
1808#[cfg(test)]
1809mod tests {
1810 use super::*;
1811 use rust_decimal_macros::dec;
1812
1813 fn norm(w: usize) -> MinMaxNormalizer {
1814 MinMaxNormalizer::new(w).unwrap()
1815 }
1816
1817 #[test]
1820 fn test_new_normalizer_is_empty() {
1821 let n = norm(4);
1822 assert!(n.is_empty());
1823 assert_eq!(n.len(), 0);
1824 }
1825
1826 #[test]
1827 fn test_minmax_is_full_false_before_capacity() {
1828 let mut n = norm(3);
1829 assert!(!n.is_full());
1830 n.update(dec!(1));
1831 n.update(dec!(2));
1832 assert!(!n.is_full());
1833 n.update(dec!(3));
1834 assert!(n.is_full());
1835 }
1836
1837 #[test]
1838 fn test_minmax_is_full_stays_true_after_eviction() {
1839 let mut n = norm(3);
1840 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
1841 n.update(v);
1842 }
1843 assert!(n.is_full()); }
1845
1846 #[test]
1847 fn test_new_zero_window_returns_error() {
1848 let result = MinMaxNormalizer::new(0);
1849 assert!(matches!(result, Err(StreamError::ConfigError { .. })));
1850 }
1851
1852 #[test]
1855 fn test_normalize_min_is_zero() {
1856 let mut n = norm(4);
1857 n.update(dec!(10));
1858 n.update(dec!(20));
1859 n.update(dec!(30));
1860 n.update(dec!(40));
1861 let v = n.normalize(dec!(10)).unwrap();
1862 assert!(
1863 (v - 0.0).abs() < 1e-10,
1864 "min should normalize to 0.0, got {v}"
1865 );
1866 }
1867
1868 #[test]
1869 fn test_normalize_max_is_one() {
1870 let mut n = norm(4);
1871 n.update(dec!(10));
1872 n.update(dec!(20));
1873 n.update(dec!(30));
1874 n.update(dec!(40));
1875 let v = n.normalize(dec!(40)).unwrap();
1876 assert!(
1877 (v - 1.0).abs() < 1e-10,
1878 "max should normalize to 1.0, got {v}"
1879 );
1880 }
1881
1882 #[test]
1883 fn test_normalize_midpoint_is_half() {
1884 let mut n = norm(4);
1885 n.update(dec!(0));
1886 n.update(dec!(100));
1887 let v = n.normalize(dec!(50)).unwrap();
1888 assert!((v - 0.5).abs() < 1e-10);
1889 }
1890
1891 #[test]
1892 fn test_normalize_result_clamped_below_zero() {
1893 let mut n = norm(4);
1894 n.update(dec!(50));
1895 n.update(dec!(100));
1896 let v = n.normalize(dec!(10)).unwrap();
1898 assert!(v >= 0.0);
1899 assert_eq!(v, 0.0);
1900 }
1901
1902 #[test]
1903 fn test_normalize_result_clamped_above_one() {
1904 let mut n = norm(4);
1905 n.update(dec!(50));
1906 n.update(dec!(100));
1907 let v = n.normalize(dec!(200)).unwrap();
1909 assert!(v <= 1.0);
1910 assert_eq!(v, 1.0);
1911 }
1912
1913 #[test]
1914 fn test_normalize_all_same_values_returns_zero() {
1915 let mut n = norm(4);
1916 n.update(dec!(5));
1917 n.update(dec!(5));
1918 n.update(dec!(5));
1919 let v = n.normalize(dec!(5)).unwrap();
1920 assert_eq!(v, 0.0);
1921 }
1922
1923 #[test]
1926 fn test_normalize_empty_window_returns_error() {
1927 let mut n = norm(4);
1928 let err = n.normalize(dec!(1)).unwrap_err();
1929 assert!(matches!(err, StreamError::NormalizationError { .. }));
1930 }
1931
1932 #[test]
1933 fn test_min_max_empty_returns_none() {
1934 let mut n = norm(4);
1935 assert!(n.min_max().is_none());
1936 }
1937
1938 #[test]
1943 fn test_rolling_window_evicts_oldest() {
1944 let mut n = norm(3);
1945 n.update(dec!(1)); n.update(dec!(5));
1947 n.update(dec!(10));
1948 n.update(dec!(20)); let (min, max) = n.min_max().unwrap();
1950 assert_eq!(min, dec!(5));
1951 assert_eq!(max, dec!(20));
1952 }
1953
1954 #[test]
1955 fn test_rolling_window_len_does_not_exceed_capacity() {
1956 let mut n = norm(3);
1957 for i in 0..10 {
1958 n.update(Decimal::from(i));
1959 }
1960 assert_eq!(n.len(), 3);
1961 }
1962
1963 #[test]
1966 fn test_reset_clears_window() {
1967 let mut n = norm(4);
1968 n.update(dec!(10));
1969 n.update(dec!(20));
1970 n.reset();
1971 assert!(n.is_empty());
1972 assert!(n.min_max().is_none());
1973 }
1974
1975 #[test]
1976 fn test_normalize_works_after_reset() {
1977 let mut n = norm(4);
1978 n.update(dec!(10));
1979 n.reset();
1980 n.update(dec!(0));
1981 n.update(dec!(100));
1982 let v = n.normalize(dec!(100)).unwrap();
1983 assert!((v - 1.0).abs() < 1e-10);
1984 }
1985
1986 #[test]
1989 fn test_streaming_updates_monotone_sequence() {
1990 let mut n = norm(5);
1991 let prices = [dec!(100), dec!(101), dec!(102), dec!(103), dec!(104), dec!(105)];
1992 for &p in &prices {
1993 n.update(p);
1994 }
1995 let v_min = n.normalize(dec!(101)).unwrap();
1997 let v_max = n.normalize(dec!(105)).unwrap();
1998 assert!((v_min - 0.0).abs() < 1e-10);
1999 assert!((v_max - 1.0).abs() < 1e-10);
2000 }
2001
2002 #[test]
2003 fn test_normalization_monotonicity_in_window() {
2004 let mut n = norm(10);
2005 for i in 0..10 {
2006 n.update(Decimal::from(i * 10));
2007 }
2008 let v0 = n.normalize(dec!(0)).unwrap();
2010 let v50 = n.normalize(dec!(50)).unwrap();
2011 let v90 = n.normalize(dec!(90)).unwrap();
2012 assert!(v0 < v50, "normalized values should be monotone");
2013 assert!(v50 < v90, "normalized values should be monotone");
2014 }
2015
2016 #[test]
2017 fn test_high_precision_input_preserved() {
2018 let mut n = norm(2);
2020 n.update(dec!(50000.00000000));
2021 n.update(dec!(50000.12345678));
2022 let (min, max) = n.min_max().unwrap();
2023 assert_eq!(min, dec!(50000.00000000));
2024 assert_eq!(max, dec!(50000.12345678));
2025 }
2026
2027 #[test]
2030 fn test_denormalize_empty_window_returns_error() {
2031 let mut n = norm(4);
2032 assert!(matches!(n.denormalize(0.5), Err(StreamError::NormalizationError { .. })));
2033 }
2034
2035 #[test]
2036 fn test_denormalize_roundtrip_min() {
2037 let mut n = norm(4);
2038 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
2039 n.update(v);
2040 }
2041 let normalized = n.normalize(dec!(10)).unwrap(); let back = n.denormalize(normalized).unwrap();
2043 assert!((back - dec!(10)).abs() < dec!(0.0001));
2044 }
2045
2046 #[test]
2047 fn test_denormalize_roundtrip_max() {
2048 let mut n = norm(4);
2049 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
2050 n.update(v);
2051 }
2052 let normalized = n.normalize(dec!(40)).unwrap(); let back = n.denormalize(normalized).unwrap();
2054 assert!((back - dec!(40)).abs() < dec!(0.0001));
2055 }
2056
2057 #[test]
2060 fn test_range_none_when_empty() {
2061 let mut n = norm(4);
2062 assert!(n.range().is_none());
2063 }
2064
2065 #[test]
2066 fn test_range_zero_when_all_same() {
2067 let mut n = norm(3);
2068 n.update(dec!(5));
2069 n.update(dec!(5));
2070 n.update(dec!(5));
2071 assert_eq!(n.range(), Some(dec!(0)));
2072 }
2073
2074 #[test]
2075 fn test_range_correct() {
2076 let mut n = norm(4);
2077 for v in [dec!(10), dec!(40), dec!(20), dec!(30)] {
2078 n.update(v);
2079 }
2080 assert_eq!(n.range(), Some(dec!(30))); }
2082
2083 #[test]
2086 fn test_midpoint_none_when_empty() {
2087 let mut n = norm(4);
2088 assert!(n.midpoint().is_none());
2089 }
2090
2091 #[test]
2092 fn test_midpoint_correct() {
2093 let mut n = norm(4);
2094 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
2095 n.update(v);
2096 }
2097 assert_eq!(n.midpoint(), Some(dec!(25)));
2099 }
2100
2101 #[test]
2102 fn test_midpoint_single_value() {
2103 let mut n = norm(4);
2104 n.update(dec!(42));
2105 assert_eq!(n.midpoint(), Some(dec!(42)));
2106 }
2107
2108 #[test]
2111 fn test_clamp_to_window_returns_value_unchanged_when_empty() {
2112 let mut n = norm(4);
2113 assert_eq!(n.clamp_to_window(dec!(50)), dec!(50));
2114 }
2115
2116 #[test]
2117 fn test_clamp_to_window_clamps_above_max() {
2118 let mut n = norm(4);
2119 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2120 assert_eq!(n.clamp_to_window(dec!(100)), dec!(30));
2121 }
2122
2123 #[test]
2124 fn test_clamp_to_window_clamps_below_min() {
2125 let mut n = norm(4);
2126 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2127 assert_eq!(n.clamp_to_window(dec!(5)), dec!(10));
2128 }
2129
2130 #[test]
2131 fn test_clamp_to_window_passthrough_when_in_range() {
2132 let mut n = norm(4);
2133 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2134 assert_eq!(n.clamp_to_window(dec!(15)), dec!(15));
2135 }
2136
2137 #[test]
2140 fn test_count_above_zero_when_empty() {
2141 let n = norm(4);
2142 assert_eq!(n.count_above(dec!(5)), 0);
2143 }
2144
2145 #[test]
2146 fn test_count_above_counts_strictly_above() {
2147 let mut n = norm(8);
2148 for v in [dec!(1), dec!(5), dec!(10), dec!(15)] { n.update(v); }
2149 assert_eq!(n.count_above(dec!(5)), 2); }
2151
2152 #[test]
2153 fn test_count_above_all_when_threshold_below_all() {
2154 let mut n = norm(4);
2155 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2156 assert_eq!(n.count_above(dec!(5)), 3);
2157 }
2158
2159 #[test]
2160 fn test_count_above_zero_when_threshold_above_all() {
2161 let mut n = norm(4);
2162 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2163 assert_eq!(n.count_above(dec!(100)), 0);
2164 }
2165
2166 #[test]
2169 fn test_count_below_zero_when_empty() {
2170 let n = norm(4);
2171 assert_eq!(n.count_below(dec!(5)), 0);
2172 }
2173
2174 #[test]
2175 fn test_count_below_counts_strictly_below() {
2176 let mut n = norm(8);
2177 for v in [dec!(1), dec!(5), dec!(10), dec!(15)] { n.update(v); }
2178 assert_eq!(n.count_below(dec!(10)), 2); }
2180
2181 #[test]
2182 fn test_count_below_all_when_threshold_above_all() {
2183 let mut n = norm(4);
2184 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2185 assert_eq!(n.count_below(dec!(100)), 3);
2186 }
2187
2188 #[test]
2189 fn test_count_below_zero_when_threshold_below_all() {
2190 let mut n = norm(4);
2191 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
2192 assert_eq!(n.count_below(dec!(5)), 0);
2193 }
2194
2195 #[test]
2196 fn test_count_above_plus_count_below_leq_len() {
2197 let mut n = norm(5);
2198 for v in [dec!(1), dec!(5), dec!(5), dec!(10), dec!(20)] { n.update(v); }
2199 assert_eq!(n.count_above(dec!(5)) + n.count_below(dec!(5)), 3);
2202 }
2203
2204 #[test]
2207 fn test_normalized_range_none_when_empty() {
2208 let mut n = norm(4);
2209 assert!(n.normalized_range().is_none());
2210 }
2211
2212 #[test]
2213 fn test_normalized_range_zero_when_all_same() {
2214 let mut n = norm(4);
2215 for _ in 0..4 { n.update(dec!(5)); }
2216 assert_eq!(n.normalized_range(), Some(0.0));
2217 }
2218
2219 #[test]
2220 fn test_normalized_range_correct_value() {
2221 let mut n = norm(4);
2222 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2223 let nr = n.normalized_range().unwrap();
2225 assert!((nr - 0.75).abs() < 1e-10);
2226 }
2227
2228 #[test]
2231 fn test_normalize_clamp_in_range_equals_normalize() {
2232 let mut n = norm(4);
2233 for v in [dec!(0), dec!(25), dec!(75), dec!(100)] {
2234 n.update(v);
2235 }
2236 let clamped = n.normalize_clamp(dec!(50)).unwrap();
2237 let normal = n.normalize(dec!(50)).unwrap();
2238 assert!((clamped - normal).abs() < 1e-9);
2239 }
2240
2241 #[test]
2242 fn test_normalize_clamp_above_max_clamped_to_one() {
2243 let mut n = norm(3);
2244 for v in [dec!(0), dec!(50), dec!(100)] {
2245 n.update(v);
2246 }
2247 let clamped = n.normalize_clamp(dec!(200)).unwrap();
2249 assert!((clamped - 1.0).abs() < 1e-9, "expected 1.0 got {clamped}");
2250 }
2251
2252 #[test]
2253 fn test_normalize_clamp_below_min_clamped_to_zero() {
2254 let mut n = norm(3);
2255 for v in [dec!(10), dec!(50), dec!(100)] {
2256 n.update(v);
2257 }
2258 let clamped = n.normalize_clamp(dec!(-50)).unwrap();
2260 assert!((clamped - 0.0).abs() < 1e-9, "expected 0.0 got {clamped}");
2261 }
2262
2263 #[test]
2264 fn test_normalize_clamp_empty_window_returns_error() {
2265 let mut n = norm(4);
2266 assert!(n.normalize_clamp(dec!(5)).is_err());
2267 }
2268
2269 #[test]
2272 fn test_latest_none_when_empty() {
2273 let n = norm(5);
2274 assert_eq!(n.latest(), None);
2275 }
2276
2277 #[test]
2278 fn test_latest_returns_most_recent_value() {
2279 let mut n = norm(5);
2280 n.update(dec!(10));
2281 n.update(dec!(20));
2282 n.update(dec!(30));
2283 assert_eq!(n.latest(), Some(dec!(30)));
2284 }
2285
2286 #[test]
2287 fn test_latest_updates_on_each_push() {
2288 let mut n = norm(3);
2289 n.update(dec!(1));
2290 assert_eq!(n.latest(), Some(dec!(1)));
2291 n.update(dec!(5));
2292 assert_eq!(n.latest(), Some(dec!(5)));
2293 }
2294
2295 #[test]
2296 fn test_latest_returns_last_after_window_overflow() {
2297 let mut n = norm(2); n.update(dec!(100));
2299 n.update(dec!(200));
2300 n.update(dec!(300)); assert_eq!(n.latest(), Some(dec!(300)));
2302 }
2303
2304 #[test]
2307 fn test_minmax_cv_none_fewer_than_2_obs() {
2308 let mut n = norm(4);
2309 n.update(dec!(10));
2310 assert!(n.coefficient_of_variation().is_none());
2311 }
2312
2313 #[test]
2314 fn test_minmax_cv_none_when_mean_zero() {
2315 let mut n = norm(4);
2316 for v in [dec!(-5), dec!(5)] { n.update(v); }
2317 assert!(n.coefficient_of_variation().is_none());
2318 }
2319
2320 #[test]
2321 fn test_minmax_cv_positive_for_positive_mean() {
2322 let mut n = norm(4);
2323 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2324 let cv = n.coefficient_of_variation().unwrap();
2325 assert!(cv > 0.0, "CV should be positive");
2326 }
2327
2328 #[test]
2331 fn test_minmax_variance_none_fewer_than_2_obs() {
2332 let mut n = norm(5);
2333 n.update(dec!(10));
2334 assert!(n.variance().is_none());
2335 }
2336
2337 #[test]
2338 fn test_minmax_variance_zero_all_same() {
2339 let mut n = norm(4);
2340 for _ in 0..4 { n.update(dec!(5)); }
2341 assert_eq!(n.variance(), Some(dec!(0)));
2342 }
2343
2344 #[test]
2345 fn test_minmax_variance_correct_value() {
2346 let mut n = norm(4);
2347 for v in [dec!(5), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2350 let var = n.variance().unwrap();
2351 assert!((var.to_f64().unwrap() - 2.75).abs() < 1e-9);
2353 }
2354
2355 #[test]
2356 fn test_minmax_std_dev_none_fewer_than_2_obs() {
2357 let n = norm(4);
2358 assert!(n.std_dev().is_none());
2359 }
2360
2361 #[test]
2362 fn test_minmax_std_dev_zero_all_same() {
2363 let mut n = norm(3);
2364 for _ in 0..3 { n.update(dec!(7)); }
2365 assert_eq!(n.std_dev(), Some(0.0));
2366 }
2367
2368 #[test]
2369 fn test_minmax_std_dev_sqrt_of_variance() {
2370 let mut n = norm(4);
2371 for v in [dec!(5), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2372 let sd = n.std_dev().unwrap();
2373 let var = n.variance().unwrap().to_f64().unwrap();
2374 assert!((sd - var.sqrt()).abs() < 1e-9);
2375 }
2376
2377 #[test]
2380 fn test_minmax_kurtosis_none_fewer_than_4_observations() {
2381 let mut n = norm(5);
2382 n.update(dec!(1));
2383 n.update(dec!(2));
2384 n.update(dec!(3));
2385 assert!(n.kurtosis().is_none());
2386 }
2387
2388 #[test]
2389 fn test_minmax_kurtosis_some_with_4_observations() {
2390 let mut n = norm(4);
2391 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
2392 n.update(v);
2393 }
2394 assert!(n.kurtosis().is_some());
2395 }
2396
2397 #[test]
2398 fn test_minmax_kurtosis_none_all_same_value() {
2399 let mut n = norm(4);
2400 for _ in 0..4 {
2401 n.update(dec!(5));
2402 }
2403 assert!(n.kurtosis().is_none());
2405 }
2406
2407 #[test]
2408 fn test_minmax_kurtosis_uniform_distribution_is_negative() {
2409 let mut n = norm(10);
2411 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5),
2412 dec!(6), dec!(7), dec!(8), dec!(9), dec!(10)] {
2413 n.update(v);
2414 }
2415 let k = n.kurtosis().unwrap();
2416 assert!(k < 0.0, "uniform distribution should have negative excess kurtosis, got {k}");
2417 }
2418
2419 #[test]
2422 fn test_minmax_median_none_for_empty_window() {
2423 assert!(norm(4).median().is_none());
2424 }
2425
2426 #[test]
2427 fn test_minmax_median_odd_window() {
2428 let mut n = norm(5);
2429 for v in [dec!(3), dec!(1), dec!(5), dec!(2), dec!(4)] { n.update(v); }
2430 assert_eq!(n.median(), Some(dec!(3)));
2432 }
2433
2434 #[test]
2435 fn test_minmax_median_even_window() {
2436 let mut n = norm(4);
2437 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2438 assert_eq!(n.median(), Some(dec!(2.5)));
2440 }
2441
2442 #[test]
2445 fn test_minmax_sample_variance_none_for_single_obs() {
2446 let mut n = norm(4);
2447 n.update(dec!(10));
2448 assert!(n.sample_variance().is_none());
2449 }
2450
2451 #[test]
2452 fn test_minmax_sample_variance_larger_than_population_variance() {
2453 let mut n = norm(4);
2454 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
2455 use rust_decimal::prelude::ToPrimitive;
2456 let pop_var = n.variance().unwrap().to_f64().unwrap();
2457 let sample_var = n.sample_variance().unwrap();
2458 assert!(sample_var > pop_var, "sample variance should exceed population variance");
2459 }
2460
2461 #[test]
2464 fn test_minmax_mad_none_for_empty_window() {
2465 assert!(norm(4).mad().is_none());
2466 }
2467
2468 #[test]
2469 fn test_minmax_mad_zero_for_identical_values() {
2470 let mut n = norm(4);
2471 for _ in 0..4 { n.update(dec!(5)); }
2472 assert_eq!(n.mad(), Some(dec!(0)));
2473 }
2474
2475 #[test]
2476 fn test_minmax_mad_correct_for_known_distribution() {
2477 let mut n = norm(5);
2478 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2479 assert_eq!(n.mad(), Some(dec!(1)));
2481 }
2482
2483 #[test]
2486 fn test_minmax_robust_z_none_for_empty_window() {
2487 assert!(norm(4).robust_z_score(dec!(10)).is_none());
2488 }
2489
2490 #[test]
2491 fn test_minmax_robust_z_none_when_mad_is_zero() {
2492 let mut n = norm(4);
2493 for _ in 0..4 { n.update(dec!(5)); }
2494 assert!(n.robust_z_score(dec!(5)).is_none());
2495 }
2496
2497 #[test]
2498 fn test_minmax_robust_z_positive_above_median() {
2499 let mut n = norm(5);
2500 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2501 let rz = n.robust_z_score(dec!(5)).unwrap();
2502 assert!(rz > 0.0, "robust z-score should be positive for value above median");
2503 }
2504
2505 #[test]
2506 fn test_minmax_robust_z_negative_below_median() {
2507 let mut n = norm(5);
2508 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2509 let rz = n.robust_z_score(dec!(1)).unwrap();
2510 assert!(rz < 0.0, "robust z-score should be negative for value below median");
2511 }
2512
2513 #[test]
2516 fn test_percentile_value_none_for_empty_window() {
2517 assert!(norm(4).percentile_value(0.5).is_none());
2518 }
2519
2520 #[test]
2521 fn test_percentile_value_min_at_zero() {
2522 let mut n = norm(5);
2523 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2524 assert_eq!(n.percentile_value(0.0), Some(dec!(10)));
2525 }
2526
2527 #[test]
2528 fn test_percentile_value_max_at_one() {
2529 let mut n = norm(5);
2530 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2531 assert_eq!(n.percentile_value(1.0), Some(dec!(50)));
2532 }
2533
2534 #[test]
2535 fn test_percentile_value_median_at_half() {
2536 let mut n = norm(5);
2537 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2538 assert_eq!(n.percentile_value(0.5), Some(dec!(30)));
2540 }
2541
2542 #[test]
2545 fn test_minmax_sum_none_for_empty_window() {
2546 assert!(norm(3).sum().is_none());
2547 }
2548
2549 #[test]
2550 fn test_minmax_sum_single_value() {
2551 let mut n = norm(3);
2552 n.update(dec!(7));
2553 assert_eq!(n.sum(), Some(dec!(7)));
2554 }
2555
2556 #[test]
2557 fn test_minmax_sum_multiple_values() {
2558 let mut n = norm(4);
2559 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2560 assert_eq!(n.sum(), Some(dec!(10)));
2561 }
2562
2563 #[test]
2566 fn test_minmax_is_outlier_false_for_empty_window() {
2567 assert!(!norm(3).is_outlier(dec!(100), 2.0));
2568 }
2569
2570 #[test]
2571 fn test_minmax_is_outlier_false_for_in_range_value() {
2572 let mut n = norm(5);
2573 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2574 assert!(!n.is_outlier(dec!(3), 2.0));
2575 }
2576
2577 #[test]
2578 fn test_minmax_is_outlier_true_for_extreme_value() {
2579 let mut n = norm(5);
2580 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2581 assert!(n.is_outlier(dec!(100), 2.0));
2582 }
2583
2584 #[test]
2587 fn test_minmax_trim_outliers_returns_all_when_no_outliers() {
2588 let mut n = norm(5);
2589 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2590 let trimmed = n.trim_outliers(10.0);
2591 assert_eq!(trimmed.len(), 5);
2592 }
2593
2594 #[test]
2595 fn test_minmax_trim_outliers_removes_extreme_values() {
2596 let mut n = norm(5);
2597 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2598 let trimmed = n.trim_outliers(0.0);
2600 assert_eq!(trimmed.len(), 1); }
2602
2603 #[test]
2606 fn test_minmax_z_score_of_latest_none_for_empty_window() {
2607 assert!(norm(3).z_score_of_latest().is_none());
2608 }
2609
2610 #[test]
2611 fn test_minmax_z_score_of_latest_zero_for_single_value() {
2612 let mut n = norm(5);
2613 n.update(dec!(10));
2614 assert!(n.z_score_of_latest().is_none());
2616 }
2617
2618 #[test]
2619 fn test_minmax_z_score_of_latest_positive_for_above_mean() {
2620 let mut n = norm(5);
2621 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(10)] { n.update(v); }
2622 let z = n.z_score_of_latest().unwrap();
2623 assert!(z > 0.0, "latest value is above mean → positive z-score");
2624 }
2625
2626 #[test]
2629 fn test_minmax_deviation_from_mean_none_for_empty_window() {
2630 assert!(norm(3).deviation_from_mean(dec!(5)).is_none());
2631 }
2632
2633 #[test]
2634 fn test_minmax_deviation_from_mean_zero_at_mean() {
2635 let mut n = norm(4);
2636 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2637 let dev = n.deviation_from_mean(dec!(2.5)).unwrap();
2639 assert!(dev.abs() < 1e-9);
2640 }
2641
2642 #[test]
2643 fn test_minmax_deviation_from_mean_positive_above_mean() {
2644 let mut n = norm(4);
2645 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2646 let dev = n.deviation_from_mean(dec!(5)).unwrap();
2647 assert!(dev > 0.0);
2648 }
2649
2650 #[test]
2653 fn test_minmax_range_f64_none_for_empty_window() {
2654 assert!(norm(3).range_f64().is_none());
2655 }
2656
2657 #[test]
2658 fn test_minmax_range_f64_correct() {
2659 let mut n = norm(4);
2660 for v in [dec!(5), dec!(15), dec!(10), dec!(20)] { n.update(v); }
2661 let r = n.range_f64().unwrap();
2663 assert!((r - 15.0).abs() < 1e-9);
2664 }
2665
2666 #[test]
2667 fn test_minmax_sum_f64_none_for_empty_window() {
2668 assert!(norm(3).sum_f64().is_none());
2669 }
2670
2671 #[test]
2672 fn test_minmax_sum_f64_correct() {
2673 let mut n = norm(4);
2674 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2675 let s = n.sum_f64().unwrap();
2676 assert!((s - 10.0).abs() < 1e-9);
2677 }
2678
2679 #[test]
2682 fn test_minmax_values_empty_for_empty_window() {
2683 assert!(norm(3).values().is_empty());
2684 }
2685
2686 #[test]
2687 fn test_minmax_values_preserves_insertion_order() {
2688 let mut n = norm(5);
2689 for v in [dec!(3), dec!(1), dec!(4), dec!(1), dec!(5)] { n.update(v); }
2690 assert_eq!(n.values(), vec![dec!(3), dec!(1), dec!(4), dec!(1), dec!(5)]);
2691 }
2692
2693 #[test]
2696 fn test_minmax_normalized_midpoint_none_for_empty_window() {
2697 assert!(norm(3).normalized_midpoint().is_none());
2698 }
2699
2700 #[test]
2701 fn test_minmax_normalized_midpoint_half_for_uniform_range() {
2702 let mut n = norm(4);
2703 for v in [dec!(0), dec!(10), dec!(20), dec!(30)] { n.update(v); }
2704 let mid = n.normalized_midpoint().unwrap();
2706 assert!((mid - 0.5).abs() < 1e-9);
2707 }
2708
2709 #[test]
2712 fn test_minmax_is_at_min_false_for_empty_window() {
2713 assert!(!norm(3).is_at_min(dec!(5)));
2714 }
2715
2716 #[test]
2717 fn test_minmax_is_at_min_true_for_minimum_value() {
2718 let mut n = norm(4);
2719 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2720 assert!(n.is_at_min(dec!(5)));
2721 }
2722
2723 #[test]
2724 fn test_minmax_is_at_min_false_for_non_minimum() {
2725 let mut n = norm(4);
2726 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2727 assert!(!n.is_at_min(dec!(10)));
2728 }
2729
2730 #[test]
2731 fn test_minmax_is_at_max_false_for_empty_window() {
2732 assert!(!norm(3).is_at_max(dec!(5)));
2733 }
2734
2735 #[test]
2736 fn test_minmax_is_at_max_true_for_maximum_value() {
2737 let mut n = norm(4);
2738 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2739 assert!(n.is_at_max(dec!(30)));
2740 }
2741
2742 #[test]
2743 fn test_minmax_is_at_max_false_for_non_maximum() {
2744 let mut n = norm(4);
2745 for v in [dec!(10), dec!(20), dec!(5), dec!(30)] { n.update(v); }
2746 assert!(!n.is_at_max(dec!(20)));
2747 }
2748
2749 #[test]
2752 fn test_minmax_fraction_above_none_for_empty_window() {
2753 assert!(norm(3).fraction_above(dec!(5)).is_none());
2754 }
2755
2756 #[test]
2757 fn test_minmax_fraction_above_correct() {
2758 let mut n = norm(5);
2759 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2760 let frac = n.fraction_above(dec!(3)).unwrap();
2762 assert!((frac - 0.4).abs() < 1e-9);
2763 }
2764
2765 #[test]
2766 fn test_minmax_fraction_below_none_for_empty_window() {
2767 assert!(norm(3).fraction_below(dec!(5)).is_none());
2768 }
2769
2770 #[test]
2771 fn test_minmax_fraction_below_correct() {
2772 let mut n = norm(5);
2773 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2774 let frac = n.fraction_below(dec!(3)).unwrap();
2776 assert!((frac - 0.4).abs() < 1e-9);
2777 }
2778
2779 #[test]
2782 fn test_minmax_window_values_above_empty_window() {
2783 assert!(norm(3).window_values_above(dec!(5)).is_empty());
2784 }
2785
2786 #[test]
2787 fn test_minmax_window_values_above_filters_correctly() {
2788 let mut n = norm(5);
2789 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2790 let above = n.window_values_above(dec!(5));
2791 assert_eq!(above.len(), 2);
2792 assert!(above.contains(&dec!(7)));
2793 assert!(above.contains(&dec!(9)));
2794 }
2795
2796 #[test]
2797 fn test_minmax_window_values_below_empty_window() {
2798 assert!(norm(3).window_values_below(dec!(5)).is_empty());
2799 }
2800
2801 #[test]
2802 fn test_minmax_window_values_below_filters_correctly() {
2803 let mut n = norm(5);
2804 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
2805 let below = n.window_values_below(dec!(5));
2806 assert_eq!(below.len(), 2);
2807 assert!(below.contains(&dec!(1)));
2808 assert!(below.contains(&dec!(3)));
2809 }
2810
2811 #[test]
2814 fn test_minmax_percentile_rank_none_for_empty_window() {
2815 assert!(norm(3).percentile_rank(dec!(5)).is_none());
2816 }
2817
2818 #[test]
2819 fn test_minmax_percentile_rank_correct() {
2820 let mut n = norm(5);
2821 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2822 let rank = n.percentile_rank(dec!(3)).unwrap();
2824 assert!((rank - 0.6).abs() < 1e-9);
2825 }
2826
2827 #[test]
2830 fn test_minmax_count_equal_zero_for_no_match() {
2831 let mut n = norm(3);
2832 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2833 assert_eq!(n.count_equal(dec!(99)), 0);
2834 }
2835
2836 #[test]
2837 fn test_minmax_count_equal_counts_duplicates() {
2838 let mut n = norm(5);
2839 for v in [dec!(5), dec!(5), dec!(3), dec!(5), dec!(2)] { n.update(v); }
2840 assert_eq!(n.count_equal(dec!(5)), 3);
2841 }
2842
2843 #[test]
2846 fn test_minmax_rolling_range_none_for_empty() {
2847 assert!(norm(3).rolling_range().is_none());
2848 }
2849
2850 #[test]
2851 fn test_minmax_rolling_range_correct() {
2852 let mut n = norm(5);
2853 for v in [dec!(10), dec!(50), dec!(30), dec!(20), dec!(40)] { n.update(v); }
2854 assert_eq!(n.rolling_range(), Some(dec!(40)));
2855 }
2856
2857 #[test]
2860 fn test_minmax_skewness_none_for_fewer_than_3() {
2861 let mut n = norm(5);
2862 n.update(dec!(1)); n.update(dec!(2));
2863 assert!(n.skewness().is_none());
2864 }
2865
2866 #[test]
2867 fn test_minmax_skewness_near_zero_for_symmetric_data() {
2868 let mut n = norm(5);
2869 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2870 let s = n.skewness().unwrap();
2871 assert!(s.abs() < 0.5);
2872 }
2873
2874 #[test]
2877 fn test_minmax_kurtosis_none_for_fewer_than_4() {
2878 let mut n = norm(5);
2879 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
2880 assert!(n.kurtosis().is_none());
2881 }
2882
2883 #[test]
2884 fn test_minmax_kurtosis_returns_f64_for_populated_window() {
2885 let mut n = norm(5);
2886 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2887 assert!(n.kurtosis().is_some());
2888 }
2889
2890 #[test]
2893 fn test_minmax_autocorrelation_none_for_single_value() {
2894 let mut n = norm(3);
2895 n.update(dec!(1));
2896 assert!(n.autocorrelation_lag1().is_none());
2897 }
2898
2899 #[test]
2900 fn test_minmax_autocorrelation_positive_for_trending_data() {
2901 let mut n = norm(5);
2902 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2903 let ac = n.autocorrelation_lag1().unwrap();
2904 assert!(ac > 0.0);
2905 }
2906
2907 #[test]
2910 fn test_minmax_trend_consistency_none_for_single_value() {
2911 let mut n = norm(3);
2912 n.update(dec!(1));
2913 assert!(n.trend_consistency().is_none());
2914 }
2915
2916 #[test]
2917 fn test_minmax_trend_consistency_one_for_strictly_rising() {
2918 let mut n = norm(5);
2919 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2920 let tc = n.trend_consistency().unwrap();
2921 assert!((tc - 1.0).abs() < 1e-9);
2922 }
2923
2924 #[test]
2925 fn test_minmax_trend_consistency_zero_for_strictly_falling() {
2926 let mut n = norm(5);
2927 for v in [dec!(5), dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
2928 let tc = n.trend_consistency().unwrap();
2929 assert!((tc - 0.0).abs() < 1e-9);
2930 }
2931
2932 #[test]
2935 fn test_minmax_cov_none_for_single_value() {
2936 let mut n = norm(3);
2937 n.update(dec!(10));
2938 assert!(n.coefficient_of_variation().is_none());
2939 }
2940
2941 #[test]
2942 fn test_minmax_cov_positive_for_varied_data() {
2943 let mut n = norm(5);
2944 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
2945 let cov = n.coefficient_of_variation().unwrap();
2946 assert!(cov > 0.0);
2947 }
2948
2949 #[test]
2952 fn test_minmax_mean_absolute_deviation_none_for_empty() {
2953 assert!(norm(3).mean_absolute_deviation().is_none());
2954 }
2955
2956 #[test]
2957 fn test_minmax_mean_absolute_deviation_zero_for_identical_values() {
2958 let mut n = norm(3);
2959 for v in [dec!(5), dec!(5), dec!(5)] { n.update(v); }
2960 let mad = n.mean_absolute_deviation().unwrap();
2961 assert!((mad - 0.0).abs() < 1e-9);
2962 }
2963
2964 #[test]
2965 fn test_minmax_mean_absolute_deviation_positive_for_varied_data() {
2966 let mut n = norm(4);
2967 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2968 let mad = n.mean_absolute_deviation().unwrap();
2969 assert!(mad > 0.0);
2970 }
2971
2972 #[test]
2975 fn test_minmax_percentile_of_latest_none_for_empty() {
2976 assert!(norm(3).percentile_of_latest().is_none());
2977 }
2978
2979 #[test]
2980 fn test_minmax_percentile_of_latest_returns_some_after_update() {
2981 let mut n = norm(4);
2982 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
2983 assert!(n.percentile_of_latest().is_some());
2984 }
2985
2986 #[test]
2987 fn test_minmax_percentile_of_latest_max_has_high_rank() {
2988 let mut n = norm(5);
2989 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
2990 let rank = n.percentile_of_latest().unwrap();
2991 assert!(rank >= 0.9, "max value should have rank near 1.0, got {}", rank);
2992 }
2993
2994 #[test]
2997 fn test_minmax_tail_ratio_none_for_empty() {
2998 assert!(norm(4).tail_ratio().is_none());
2999 }
3000
3001 #[test]
3002 fn test_minmax_tail_ratio_one_for_identical_values() {
3003 let mut n = norm(4);
3004 for _ in 0..4 { n.update(dec!(7)); }
3005 let r = n.tail_ratio().unwrap();
3007 assert!((r - 1.0).abs() < 1e-9, "expected 1.0, got {}", r);
3008 }
3009
3010 #[test]
3011 fn test_minmax_tail_ratio_above_one_with_outlier() {
3012 let mut n = norm(5);
3013 for v in [dec!(1), dec!(1), dec!(1), dec!(1), dec!(10)] { n.update(v); }
3014 let r = n.tail_ratio().unwrap();
3015 assert!(r > 1.0, "outlier should push ratio above 1.0, got {}", r);
3016 }
3017
3018 #[test]
3021 fn test_minmax_z_score_of_min_none_for_empty() {
3022 assert!(norm(4).z_score_of_min().is_none());
3023 }
3024
3025 #[test]
3026 fn test_minmax_z_score_of_max_none_for_empty() {
3027 assert!(norm(4).z_score_of_max().is_none());
3028 }
3029
3030 #[test]
3031 fn test_minmax_z_score_of_min_negative_for_varied_window() {
3032 let mut n = norm(5);
3033 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3034 let z = n.z_score_of_min().unwrap();
3036 assert!(z < 0.0, "z-score of min should be negative, got {}", z);
3037 }
3038
3039 #[test]
3040 fn test_minmax_z_score_of_max_positive_for_varied_window() {
3041 let mut n = norm(5);
3042 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3043 let z = n.z_score_of_max().unwrap();
3045 assert!(z > 0.0, "z-score of max should be positive, got {}", z);
3046 }
3047
3048 #[test]
3051 fn test_minmax_window_entropy_none_for_empty() {
3052 assert!(norm(4).window_entropy().is_none());
3053 }
3054
3055 #[test]
3056 fn test_minmax_window_entropy_zero_for_identical_values() {
3057 let mut n = norm(3);
3058 for _ in 0..3 { n.update(dec!(5)); }
3059 let e = n.window_entropy().unwrap();
3060 assert!((e - 0.0).abs() < 1e-9, "identical values should have zero entropy, got {}", e);
3061 }
3062
3063 #[test]
3064 fn test_minmax_window_entropy_positive_for_varied_values() {
3065 let mut n = norm(4);
3066 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3067 let e = n.window_entropy().unwrap();
3068 assert!(e > 0.0, "varied values should have positive entropy, got {}", e);
3069 }
3070
3071 #[test]
3074 fn test_minmax_normalized_std_dev_none_for_single_value() {
3075 let mut n = norm(4);
3076 n.update(dec!(5));
3077 assert!(n.normalized_std_dev().is_none());
3078 }
3079
3080 #[test]
3081 fn test_minmax_normalized_std_dev_positive_for_varied_values() {
3082 let mut n = norm(4);
3083 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3084 let r = n.normalized_std_dev().unwrap();
3085 assert!(r > 0.0, "expected positive normalized std dev, got {}", r);
3086 }
3087
3088 #[test]
3091 fn test_minmax_value_above_mean_count_none_for_empty() {
3092 assert!(norm(4).value_above_mean_count().is_none());
3093 }
3094
3095 #[test]
3096 fn test_minmax_value_above_mean_count_correct() {
3097 let mut n = norm(4);
3099 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3100 assert_eq!(n.value_above_mean_count().unwrap(), 2);
3101 }
3102
3103 #[test]
3106 fn test_minmax_consecutive_above_mean_none_for_empty() {
3107 assert!(norm(4).consecutive_above_mean().is_none());
3108 }
3109
3110 #[test]
3111 fn test_minmax_consecutive_above_mean_correct() {
3112 let mut n = norm(4);
3114 for v in [dec!(1), dec!(5), dec!(6), dec!(7)] { n.update(v); }
3115 assert_eq!(n.consecutive_above_mean().unwrap(), 3);
3116 }
3117
3118 #[test]
3121 fn test_minmax_above_threshold_fraction_none_for_empty() {
3122 assert!(norm(4).above_threshold_fraction(dec!(5)).is_none());
3123 }
3124
3125 #[test]
3126 fn test_minmax_above_threshold_fraction_correct() {
3127 let mut n = norm(4);
3128 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3129 let f = n.above_threshold_fraction(dec!(2)).unwrap();
3131 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
3132 }
3133
3134 #[test]
3135 fn test_minmax_below_threshold_fraction_none_for_empty() {
3136 assert!(norm(4).below_threshold_fraction(dec!(5)).is_none());
3137 }
3138
3139 #[test]
3140 fn test_minmax_below_threshold_fraction_correct() {
3141 let mut n = norm(4);
3142 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3143 let f = n.below_threshold_fraction(dec!(3)).unwrap();
3145 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
3146 }
3147
3148 #[test]
3151 fn test_minmax_lag_k_autocorrelation_none_for_zero_k() {
3152 let mut n = norm(5);
3153 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3154 assert!(n.lag_k_autocorrelation(0).is_none());
3155 }
3156
3157 #[test]
3158 fn test_minmax_lag_k_autocorrelation_none_when_k_gte_len() {
3159 let mut n = norm(3);
3160 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3161 assert!(n.lag_k_autocorrelation(3).is_none());
3162 }
3163
3164 #[test]
3165 fn test_minmax_lag_k_autocorrelation_positive_for_trend() {
3166 let mut n = norm(6);
3168 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6)] { n.update(v); }
3169 let ac = n.lag_k_autocorrelation(1).unwrap();
3170 assert!(ac > 0.0, "trending series should have positive AC, got {}", ac);
3171 }
3172
3173 #[test]
3176 fn test_minmax_half_life_estimate_none_for_fewer_than_3() {
3177 let mut n = norm(3);
3178 n.update(dec!(1)); n.update(dec!(2));
3179 assert!(n.half_life_estimate().is_none());
3180 }
3181
3182 #[test]
3183 fn test_minmax_half_life_estimate_some_for_mean_reverting() {
3184 let mut n = norm(6);
3186 for v in [dec!(10), dec!(5), dec!(10), dec!(5), dec!(10), dec!(5)] { n.update(v); }
3187 let _ = n.half_life_estimate();
3189 }
3190
3191 #[test]
3194 fn test_minmax_geometric_mean_none_for_empty() {
3195 assert!(norm(4).geometric_mean().is_none());
3196 }
3197
3198 #[test]
3199 fn test_minmax_geometric_mean_correct_for_powers_of_2() {
3200 let mut n = norm(4);
3202 for v in [dec!(1), dec!(2), dec!(4), dec!(8)] { n.update(v); }
3203 let gm = n.geometric_mean().unwrap();
3204 assert!((gm - 64.0f64.powf(0.25)).abs() < 1e-6, "got {}", gm);
3205 }
3206
3207 #[test]
3210 fn test_minmax_harmonic_mean_none_for_empty() {
3211 assert!(norm(4).harmonic_mean().is_none());
3212 }
3213
3214 #[test]
3215 fn test_minmax_harmonic_mean_none_when_any_zero() {
3216 let mut n = norm(2);
3217 n.update(dec!(0)); n.update(dec!(5));
3218 assert!(n.harmonic_mean().is_none());
3219 }
3220
3221 #[test]
3222 fn test_minmax_harmonic_mean_positive_for_positive_values() {
3223 let mut n = norm(4);
3224 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3225 let hm = n.harmonic_mean().unwrap();
3226 assert!(hm > 0.0 && hm < 4.0, "HM should be in (0, max), got {}", hm);
3227 }
3228
3229 #[test]
3232 fn test_minmax_range_normalized_value_none_for_empty() {
3233 assert!(norm(4).range_normalized_value(dec!(5)).is_none());
3234 }
3235
3236 #[test]
3237 fn test_minmax_range_normalized_value_zero_for_min() {
3238 let mut n = norm(4);
3239 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3240 let r = n.range_normalized_value(dec!(1)).unwrap();
3241 assert!((r - 0.0).abs() < 1e-9, "min value should normalize to 0, got {}", r);
3242 }
3243
3244 #[test]
3245 fn test_minmax_range_normalized_value_one_for_max() {
3246 let mut n = norm(4);
3247 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3248 let r = n.range_normalized_value(dec!(4)).unwrap();
3249 assert!((r - 1.0).abs() < 1e-9, "max value should normalize to 1, got {}", r);
3250 }
3251
3252 #[test]
3255 fn test_minmax_distance_from_median_none_for_empty() {
3256 assert!(norm(4).distance_from_median(dec!(5)).is_none());
3257 }
3258
3259 #[test]
3260 fn test_minmax_distance_from_median_zero_at_median() {
3261 let mut n = norm(5);
3262 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3263 let d = n.distance_from_median(dec!(3)).unwrap();
3265 assert!((d - 0.0).abs() < 1e-9, "distance from median should be 0, got {}", d);
3266 }
3267
3268 #[test]
3269 fn test_minmax_distance_from_median_positive_above() {
3270 let mut n = norm(5);
3271 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3272 let d = n.distance_from_median(dec!(5)).unwrap();
3273 assert!(d > 0.0, "value above median should give positive distance, got {}", d);
3274 }
3275
3276 #[test]
3277 fn test_minmax_momentum_none_for_single_value() {
3278 let mut n = norm(5);
3279 n.update(dec!(10));
3280 assert!(n.momentum().is_none());
3281 }
3282
3283 #[test]
3284 fn test_minmax_momentum_positive_for_rising_window() {
3285 let mut n = norm(3);
3286 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3287 let m = n.momentum().unwrap();
3288 assert!(m > 0.0, "rising window → positive momentum, got {}", m);
3289 }
3290
3291 #[test]
3292 fn test_minmax_value_rank_none_for_empty() {
3293 assert!(norm(4).value_rank(dec!(5)).is_none());
3294 }
3295
3296 #[test]
3297 fn test_minmax_value_rank_extremes() {
3298 let mut n = norm(4);
3299 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3300 let low = n.value_rank(dec!(0)).unwrap();
3302 assert!((low - 0.0).abs() < 1e-9, "got {}", low);
3303 let high = n.value_rank(dec!(5)).unwrap();
3305 assert!((high - 1.0).abs() < 1e-9, "got {}", high);
3306 }
3307
3308 #[test]
3309 fn test_minmax_coeff_of_variation_none_for_single_value() {
3310 let mut n = norm(5);
3311 n.update(dec!(10));
3312 assert!(n.coeff_of_variation().is_none());
3313 }
3314
3315 #[test]
3316 fn test_minmax_coeff_of_variation_positive_for_spread() {
3317 let mut n = norm(4);
3318 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
3319 let cv = n.coeff_of_variation().unwrap();
3320 assert!(cv > 0.0, "expected positive CV, got {}", cv);
3321 }
3322
3323 #[test]
3324 fn test_minmax_quantile_range_none_for_empty() {
3325 assert!(norm(4).quantile_range().is_none());
3326 }
3327
3328 #[test]
3329 fn test_minmax_quantile_range_non_negative() {
3330 let mut n = norm(5);
3331 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3332 let iqr = n.quantile_range().unwrap();
3333 assert!(iqr >= 0.0, "IQR should be non-negative, got {}", iqr);
3334 }
3335
3336 #[test]
3339 fn test_minmax_upper_quartile_none_for_empty() {
3340 assert!(norm(4).upper_quartile().is_none());
3341 }
3342
3343 #[test]
3344 fn test_minmax_lower_quartile_none_for_empty() {
3345 assert!(norm(4).lower_quartile().is_none());
3346 }
3347
3348 #[test]
3349 fn test_minmax_upper_ge_lower_quartile() {
3350 let mut n = norm(8);
3351 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6), dec!(7), dec!(8)] {
3352 n.update(v);
3353 }
3354 let q3 = n.upper_quartile().unwrap();
3355 let q1 = n.lower_quartile().unwrap();
3356 assert!(q3 >= q1, "Q3 ({}) should be >= Q1 ({})", q3, q1);
3357 }
3358
3359 #[test]
3362 fn test_minmax_sign_change_rate_none_for_fewer_than_3() {
3363 let mut n = norm(4);
3364 n.update(dec!(1));
3365 n.update(dec!(2));
3366 assert!(n.sign_change_rate().is_none());
3367 }
3368
3369 #[test]
3370 fn test_minmax_sign_change_rate_one_for_zigzag() {
3371 let mut n = norm(5);
3372 for v in [dec!(1), dec!(3), dec!(1), dec!(3), dec!(1)] { n.update(v); }
3374 let r = n.sign_change_rate().unwrap();
3375 assert!((r - 1.0).abs() < 1e-9, "zigzag should give 1.0, got {}", r);
3376 }
3377
3378 #[test]
3379 fn test_minmax_sign_change_rate_zero_for_monotone() {
3380 let mut n = norm(5);
3381 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3382 let r = n.sign_change_rate().unwrap();
3383 assert!((r - 0.0).abs() < 1e-9, "monotone should give 0.0, got {}", r);
3384 }
3385
3386 #[test]
3391 fn test_consecutive_below_mean_none_for_single_value() {
3392 let mut n = norm(5);
3393 n.update(dec!(10));
3394 assert!(n.consecutive_below_mean().is_none());
3395 }
3396
3397 #[test]
3398 fn test_consecutive_below_mean_zero_when_latest_above_mean() {
3399 let mut n = norm(5);
3400 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(100)] { n.update(v); }
3401 let c = n.consecutive_below_mean().unwrap();
3402 assert_eq!(c, 0, "latest above mean → streak=0, got {}", c);
3403 }
3404
3405 #[test]
3406 fn test_consecutive_below_mean_counts_trailing_below() {
3407 let mut n = norm(5);
3408 for v in [dec!(100), dec!(100), dec!(1), dec!(1), dec!(1)] { n.update(v); }
3409 let c = n.consecutive_below_mean().unwrap();
3410 assert!(c >= 3, "last 3 below mean → streak>=3, got {}", c);
3411 }
3412
3413 #[test]
3416 fn test_drift_rate_none_for_single_value() {
3417 let mut n = norm(5);
3418 n.update(dec!(10));
3419 assert!(n.drift_rate().is_none());
3420 }
3421
3422 #[test]
3423 fn test_drift_rate_positive_for_rising_series() {
3424 let mut n = norm(6);
3425 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6)] { n.update(v); }
3426 let d = n.drift_rate().unwrap();
3427 assert!(d > 0.0, "rising series → positive drift, got {}", d);
3428 }
3429
3430 #[test]
3431 fn test_drift_rate_negative_for_falling_series() {
3432 let mut n = norm(6);
3433 for v in [dec!(6), dec!(5), dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
3434 let d = n.drift_rate().unwrap();
3435 assert!(d < 0.0, "falling series → negative drift, got {}", d);
3436 }
3437
3438 #[test]
3441 fn test_peak_to_trough_ratio_none_for_empty() {
3442 assert!(norm(4).peak_to_trough_ratio().is_none());
3443 }
3444
3445 #[test]
3446 fn test_peak_to_trough_ratio_one_for_constant() {
3447 let mut n = norm(4);
3448 for v in [dec!(10), dec!(10), dec!(10), dec!(10)] { n.update(v); }
3449 let r = n.peak_to_trough_ratio().unwrap();
3450 assert!((r - 1.0).abs() < 1e-9, "constant → ratio=1, got {}", r);
3451 }
3452
3453 #[test]
3454 fn test_peak_to_trough_ratio_above_one_for_spread() {
3455 let mut n = norm(4);
3456 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
3457 let r = n.peak_to_trough_ratio().unwrap();
3458 assert!(r > 1.0, "spread → ratio>1, got {}", r);
3459 }
3460
3461 #[test]
3464 fn test_normalized_deviation_none_for_empty() {
3465 assert!(norm(4).normalized_deviation().is_none());
3466 }
3467
3468 #[test]
3469 fn test_normalized_deviation_none_for_constant() {
3470 let mut n = norm(4);
3471 for v in [dec!(5), dec!(5), dec!(5), dec!(5)] { n.update(v); }
3472 assert!(n.normalized_deviation().is_none());
3473 }
3474
3475 #[test]
3476 fn test_normalized_deviation_positive_for_latest_above_mean() {
3477 let mut n = norm(5);
3478 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(10)] { n.update(v); }
3479 let d = n.normalized_deviation().unwrap();
3480 assert!(d > 0.0, "latest above mean → positive deviation, got {}", d);
3481 }
3482
3483 #[test]
3486 fn test_window_cv_pct_none_for_single_value() {
3487 let mut n = norm(5);
3488 n.update(dec!(10));
3489 assert!(n.window_cv_pct().is_none());
3490 }
3491
3492 #[test]
3493 fn test_window_cv_pct_positive_for_varied_values() {
3494 let mut n = norm(4);
3495 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
3496 let cv = n.window_cv_pct().unwrap();
3497 assert!(cv > 0.0, "expected positive CV%, got {}", cv);
3498 }
3499
3500 #[test]
3503 fn test_latest_rank_pct_none_for_single_value() {
3504 let mut n = norm(5);
3505 n.update(dec!(10));
3506 assert!(n.latest_rank_pct().is_none());
3507 }
3508
3509 #[test]
3510 fn test_latest_rank_pct_one_for_max_value() {
3511 let mut n = norm(4);
3512 for v in [dec!(1), dec!(2), dec!(3), dec!(100)] { n.update(v); }
3513 let r = n.latest_rank_pct().unwrap();
3514 assert!((r - 1.0).abs() < 1e-9, "latest is max → rank=1, got {}", r);
3515 }
3516
3517 #[test]
3518 fn test_latest_rank_pct_zero_for_min_value() {
3519 let mut n = norm(4);
3520 for v in [dec!(10), dec!(20), dec!(30), dec!(1)] { n.update(v); }
3521 let r = n.latest_rank_pct().unwrap();
3522 assert!(r.abs() < 1e-9, "latest is min → rank=0, got {}", r);
3523 }
3524
3525 #[test]
3528 fn test_minmax_trimmed_mean_none_for_empty() {
3529 assert!(norm(4).trimmed_mean(0.1).is_none());
3530 }
3531
3532 #[test]
3533 fn test_minmax_trimmed_mean_equals_mean_at_zero_trim() {
3534 let mut n = norm(4);
3535 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
3536 let tm = n.trimmed_mean(0.0).unwrap();
3537 let m = n.mean().unwrap().to_f64().unwrap();
3538 assert!((tm - m).abs() < 1e-9, "0% trim should equal mean, got tm={} m={}", tm, m);
3539 }
3540
3541 #[test]
3542 fn test_minmax_trimmed_mean_reduces_effect_of_outlier() {
3543 let mut n = norm(5);
3544 for v in [dec!(10), dec!(10), dec!(10), dec!(10), dec!(1000)] { n.update(v); }
3545 let tm = n.trimmed_mean(0.2).unwrap();
3547 let m = n.mean().unwrap().to_f64().unwrap();
3548 assert!(tm < m, "trimmed mean should be less than mean when outlier is trimmed, tm={} m={}", tm, m);
3549 }
3550
3551 #[test]
3554 fn test_minmax_linear_trend_slope_none_for_single_value() {
3555 let mut n = norm(4);
3556 n.update(dec!(10));
3557 assert!(n.linear_trend_slope().is_none());
3558 }
3559
3560 #[test]
3561 fn test_minmax_linear_trend_slope_positive_for_rising() {
3562 let mut n = norm(4);
3563 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3564 let slope = n.linear_trend_slope().unwrap();
3565 assert!(slope > 0.0, "rising window → positive slope, got {}", slope);
3566 }
3567
3568 #[test]
3569 fn test_minmax_linear_trend_slope_negative_for_falling() {
3570 let mut n = norm(4);
3571 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
3572 let slope = n.linear_trend_slope().unwrap();
3573 assert!(slope < 0.0, "falling window → negative slope, got {}", slope);
3574 }
3575
3576 #[test]
3577 fn test_minmax_linear_trend_slope_zero_for_flat() {
3578 let mut n = norm(4);
3579 for v in [dec!(5), dec!(5), dec!(5), dec!(5)] { n.update(v); }
3580 let slope = n.linear_trend_slope().unwrap();
3581 assert!(slope.abs() < 1e-9, "flat window → slope=0, got {}", slope);
3582 }
3583
3584 #[test]
3587 fn test_minmax_variance_ratio_none_for_few_values() {
3588 let mut n = norm(3);
3589 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3590 assert!(n.variance_ratio().is_none());
3591 }
3592
3593 #[test]
3594 fn test_minmax_variance_ratio_gt_one_for_decreasing_vol() {
3595 let mut n = norm(6);
3596 for v in [dec!(1), dec!(10), dec!(1), dec!(5), dec!(6), dec!(5)] { n.update(v); }
3598 let r = n.variance_ratio().unwrap();
3599 assert!(r > 1.0, "first half more volatile → ratio > 1, got {}", r);
3600 }
3601
3602 #[test]
3605 fn test_minmax_z_score_trend_slope_none_for_single_value() {
3606 let mut n = norm(4);
3607 n.update(dec!(10));
3608 assert!(n.z_score_trend_slope().is_none());
3609 }
3610
3611 #[test]
3612 fn test_minmax_z_score_trend_slope_positive_for_rising() {
3613 let mut n = norm(5);
3614 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
3615 let slope = n.z_score_trend_slope().unwrap();
3616 assert!(slope > 0.0, "rising window → positive z-score slope, got {}", slope);
3617 }
3618
3619 #[test]
3622 fn test_minmax_mean_absolute_change_none_for_single_value() {
3623 let mut n = norm(4);
3624 n.update(dec!(10));
3625 assert!(n.mean_absolute_change().is_none());
3626 }
3627
3628 #[test]
3629 fn test_minmax_mean_absolute_change_zero_for_constant() {
3630 let mut n = norm(4);
3631 for _ in 0..4 { n.update(dec!(5)); }
3632 let mac = n.mean_absolute_change().unwrap();
3633 assert!(mac.abs() < 1e-9, "constant window → MAC=0, got {}", mac);
3634 }
3635
3636 #[test]
3637 fn test_minmax_mean_absolute_change_positive_for_varying() {
3638 let mut n = norm(4);
3639 for v in [dec!(1), dec!(3), dec!(2), dec!(5)] { n.update(v); }
3640 let mac = n.mean_absolute_change().unwrap();
3641 assert!(mac > 0.0, "varying window → MAC > 0, got {}", mac);
3642 }
3643
3644 #[test]
3647 fn test_minmax_monotone_increase_fraction_none_for_single() {
3648 let mut n = norm(4);
3649 n.update(dec!(5));
3650 assert!(n.monotone_increase_fraction().is_none());
3651 }
3652
3653 #[test]
3654 fn test_minmax_monotone_increase_fraction_one_for_rising() {
3655 let mut n = norm(4);
3656 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3657 let f = n.monotone_increase_fraction().unwrap();
3658 assert!((f - 1.0).abs() < 1e-9, "all rising → fraction=1, got {}", f);
3659 }
3660
3661 #[test]
3662 fn test_minmax_abs_max_none_for_empty() {
3663 let n = norm(4);
3664 assert!(n.abs_max().is_none());
3665 }
3666
3667 #[test]
3668 fn test_minmax_abs_max_returns_max_absolute() {
3669 let mut n = norm(4);
3670 for v in [dec!(1), dec!(3), dec!(2)] { n.update(v); }
3671 assert_eq!(n.abs_max().unwrap(), dec!(3));
3672 }
3673
3674 #[test]
3675 fn test_minmax_max_count_none_for_empty() {
3676 let n = norm(4);
3677 assert!(n.max_count().is_none());
3678 }
3679
3680 #[test]
3681 fn test_minmax_max_count_correct() {
3682 let mut n = norm(4);
3683 for v in [dec!(1), dec!(5), dec!(3), dec!(5)] { n.update(v); }
3684 assert_eq!(n.max_count().unwrap(), 2);
3685 }
3686
3687 #[test]
3688 fn test_minmax_mean_ratio_none_for_single() {
3689 let mut n = norm(4);
3690 n.update(dec!(10));
3691 assert!(n.mean_ratio().is_none());
3692 }
3693
3694 #[test]
3697 fn test_minmax_exponential_weighted_mean_none_for_empty() {
3698 let n = norm(4);
3699 assert!(n.exponential_weighted_mean(0.5).is_none());
3700 }
3701
3702 #[test]
3703 fn test_minmax_exponential_weighted_mean_returns_value() {
3704 let mut n = norm(4);
3705 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3706 let ewm = n.exponential_weighted_mean(0.5).unwrap();
3707 assert!(ewm > 0.0, "EWM should be positive, got {}", ewm);
3708 }
3709
3710 #[test]
3711 fn test_minmax_peak_to_trough_none_for_empty() {
3712 let n = norm(4);
3713 assert!(n.peak_to_trough_ratio().is_none());
3714 }
3715
3716 #[test]
3717 fn test_minmax_peak_to_trough_correct() {
3718 let mut n = norm(4);
3719 for v in [dec!(2), dec!(4), dec!(1), dec!(8)] { n.update(v); }
3720 let r = n.peak_to_trough_ratio().unwrap();
3721 assert!((r - 8.0).abs() < 1e-9, "max=8, min=1 → ratio=8, got {}", r);
3722 }
3723
3724 #[test]
3725 fn test_minmax_second_moment_none_for_empty() {
3726 let n = norm(4);
3727 assert!(n.second_moment().is_none());
3728 }
3729
3730 #[test]
3731 fn test_minmax_second_moment_correct() {
3732 let mut n = norm(4);
3733 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3734 let m = n.second_moment().unwrap();
3736 assert!((m - 14.0 / 3.0).abs() < 1e-9, "second moment ≈ 4.667, got {}", m);
3737 }
3738
3739 #[test]
3740 fn test_minmax_range_over_mean_none_for_empty() {
3741 let n = norm(4);
3742 assert!(n.range_over_mean().is_none());
3743 }
3744
3745 #[test]
3746 fn test_minmax_range_over_mean_positive() {
3747 let mut n = norm(4);
3748 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3749 let r = n.range_over_mean().unwrap();
3750 assert!(r > 0.0, "range/mean should be positive, got {}", r);
3751 }
3752
3753 #[test]
3754 fn test_minmax_above_median_fraction_none_for_empty() {
3755 let n = norm(4);
3756 assert!(n.above_median_fraction().is_none());
3757 }
3758
3759 #[test]
3760 fn test_minmax_above_median_fraction_in_range() {
3761 let mut n = norm(4);
3762 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3763 let f = n.above_median_fraction().unwrap();
3764 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
3765 }
3766
3767 #[test]
3770 fn test_minmax_interquartile_mean_none_for_empty() {
3771 let n = norm(4);
3772 assert!(n.interquartile_mean().is_none());
3773 }
3774
3775 #[test]
3776 fn test_minmax_outlier_fraction_none_for_empty() {
3777 let n = norm(4);
3778 assert!(n.outlier_fraction(2.0).is_none());
3779 }
3780
3781 #[test]
3782 fn test_minmax_outlier_fraction_zero_for_constant() {
3783 let mut n = norm(4);
3784 for _ in 0..4 { n.update(dec!(5)); }
3785 let f = n.outlier_fraction(1.0).unwrap();
3786 assert!(f.abs() < 1e-9, "constant window → no outliers, got {}", f);
3787 }
3788
3789 #[test]
3790 fn test_minmax_sign_flip_count_none_for_single() {
3791 let mut n = norm(4);
3792 n.update(dec!(1));
3793 assert!(n.sign_flip_count().is_none());
3794 }
3795
3796 #[test]
3797 fn test_minmax_sign_flip_count_correct() {
3798 let mut n = norm(6);
3799 for v in [dec!(1), dec!(-1), dec!(1), dec!(-1)] { n.update(v); }
3800 let c = n.sign_flip_count().unwrap();
3801 assert_eq!(c, 3, "3 sign flips expected, got {}", c);
3802 }
3803
3804 #[test]
3805 fn test_minmax_rms_none_for_empty() {
3806 let n = norm(4);
3807 assert!(n.rms().is_none());
3808 }
3809
3810 #[test]
3811 fn test_minmax_rms_correct_for_unit_value() {
3812 let mut n = norm(4);
3813 for _ in 0..4 { n.update(dec!(1)); }
3814 let r = n.rms().unwrap();
3815 assert!((r - 1.0).abs() < 1e-9, "RMS of all-ones = 1.0, got {}", r);
3816 }
3817
3818 #[test]
3821 fn test_minmax_distinct_count_zero_for_empty() {
3822 let n = norm(4);
3823 assert_eq!(n.distinct_count(), 0);
3824 }
3825
3826 #[test]
3827 fn test_minmax_distinct_count_correct() {
3828 let mut n = norm(4);
3829 for v in [dec!(1), dec!(1), dec!(2), dec!(3)] { n.update(v); }
3830 assert_eq!(n.distinct_count(), 3);
3831 }
3832
3833 #[test]
3834 fn test_minmax_max_fraction_none_for_empty() {
3835 let n = norm(4);
3836 assert!(n.max_fraction().is_none());
3837 }
3838
3839 #[test]
3840 fn test_minmax_max_fraction_correct() {
3841 let mut n = norm(4);
3842 for v in [dec!(1), dec!(2), dec!(3), dec!(3)] { n.update(v); }
3843 let f = n.max_fraction().unwrap();
3844 assert!((f - 0.5).abs() < 1e-9, "2/4 are max → 0.5, got {}", f);
3846 }
3847
3848 #[test]
3849 fn test_minmax_latest_minus_mean_none_for_empty() {
3850 let n = norm(4);
3851 assert!(n.latest_minus_mean().is_none());
3852 }
3853
3854 #[test]
3855 fn test_minmax_latest_to_mean_ratio_none_for_empty() {
3856 let n = norm(4);
3857 assert!(n.latest_to_mean_ratio().is_none());
3858 }
3859
3860 #[test]
3861 fn test_minmax_latest_to_mean_ratio_one_for_constant() {
3862 let mut n = norm(4);
3863 for _ in 0..4 { n.update(dec!(5)); }
3864 let r = n.latest_to_mean_ratio().unwrap();
3865 assert!((r - 1.0).abs() < 1e-9, "latest=mean → ratio=1, got {}", r);
3866 }
3867
3868 #[test]
3871 fn test_minmax_below_mean_fraction_none_for_empty() {
3872 assert!(norm(4).below_mean_fraction().is_none());
3873 }
3874
3875 #[test]
3876 fn test_minmax_below_mean_fraction_symmetric_data() {
3877 let mut n = norm(4);
3878 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3879 let f = n.below_mean_fraction().unwrap();
3881 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
3882 }
3883
3884 #[test]
3885 fn test_minmax_tail_variance_none_for_small_window() {
3886 let mut n = norm(3);
3887 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3888 assert!(n.tail_variance().is_none());
3889 }
3890
3891 #[test]
3892 fn test_minmax_tail_variance_nonneg_for_varied_data() {
3893 let mut n = norm(6);
3894 for v in [dec!(1), dec!(2), dec!(5), dec!(6), dec!(9), dec!(10)] { n.update(v); }
3895 let tv = n.tail_variance().unwrap();
3896 assert!(tv >= 0.0, "tail variance should be non-negative, got {}", tv);
3897 }
3898
3899 #[test]
3902 fn test_minmax_new_max_count_zero_for_empty() {
3903 let n = norm(4);
3904 assert_eq!(n.new_max_count(), 0);
3905 }
3906
3907 #[test]
3908 fn test_minmax_new_max_count_all_rising() {
3909 let mut n = norm(4);
3910 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3911 assert_eq!(n.new_max_count(), 4, "each value is a new high");
3912 }
3913
3914 #[test]
3915 fn test_minmax_new_min_count_zero_for_empty() {
3916 let n = norm(4);
3917 assert_eq!(n.new_min_count(), 0);
3918 }
3919
3920 #[test]
3921 fn test_minmax_new_min_count_all_falling() {
3922 let mut n = norm(4);
3923 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
3924 assert_eq!(n.new_min_count(), 4, "each value is a new low");
3925 }
3926
3927 #[test]
3928 fn test_minmax_zero_fraction_none_for_empty() {
3929 let n = norm(4);
3930 assert!(n.zero_fraction().is_none());
3931 }
3932
3933 #[test]
3934 fn test_minmax_zero_fraction_zero_when_no_zeros() {
3935 let mut n = norm(4);
3936 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3937 let f = n.zero_fraction().unwrap();
3938 assert!(f.abs() < 1e-9, "no zeros → fraction=0, got {}", f);
3939 }
3940
3941 #[test]
3944 fn test_minmax_cumulative_sum_zero_for_empty() {
3945 let n = norm(4);
3946 assert_eq!(n.cumulative_sum(), rust_decimal::Decimal::ZERO);
3947 }
3948
3949 #[test]
3950 fn test_minmax_cumulative_sum_correct() {
3951 let mut n = norm(4);
3952 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
3953 assert_eq!(n.cumulative_sum(), dec!(6));
3954 }
3955
3956 #[test]
3957 fn test_minmax_max_to_min_ratio_none_for_empty() {
3958 assert!(norm(4).max_to_min_ratio().is_none());
3959 }
3960
3961 #[test]
3962 fn test_minmax_max_to_min_ratio_one_for_constant() {
3963 let mut n = norm(4);
3964 for _ in 0..4 { n.update(dec!(5)); }
3965 let r = n.max_to_min_ratio().unwrap();
3966 assert!((r - 1.0).abs() < 1e-9, "constant window → ratio=1, got {}", r);
3967 }
3968
3969 #[test]
3972 fn test_minmax_above_midpoint_fraction_none_for_empty() {
3973 assert!(norm(4).above_midpoint_fraction().is_none());
3974 }
3975
3976 #[test]
3977 fn test_minmax_above_midpoint_fraction_half_for_symmetric() {
3978 let mut n = norm(4);
3979 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
3980 let f = n.above_midpoint_fraction().unwrap();
3982 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
3983 }
3984
3985 #[test]
3986 fn test_minmax_span_utilization_none_for_empty() {
3987 assert!(norm(4).span_utilization().is_none());
3988 }
3989
3990 #[test]
3991 fn test_minmax_span_utilization_one_for_latest_at_max() {
3992 let mut n = norm(4);
3993 for v in [dec!(1), dec!(5), dec!(3), dec!(10)] { n.update(v); }
3994 let u = n.span_utilization().unwrap();
3996 assert!((u - 1.0).abs() < 1e-9, "latest=max → 1.0, got {}", u);
3997 }
3998
3999 #[test]
4000 fn test_minmax_positive_fraction_none_for_empty() {
4001 assert!(norm(4).positive_fraction().is_none());
4002 }
4003
4004 #[test]
4005 fn test_minmax_positive_fraction_half() {
4006 let mut n = norm(4);
4007 for v in [dec!(-1), dec!(0), dec!(1), dec!(2)] { n.update(v); }
4008 let f = n.positive_fraction().unwrap();
4010 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
4011 }
4012
4013 #[test]
4016 fn test_minmax_window_iqr_none_for_empty() {
4017 assert!(norm(4).window_iqr().is_none());
4018 }
4019
4020 #[test]
4021 fn test_minmax_window_iqr_zero_for_constant() {
4022 let mut n = norm(4);
4023 for _ in 0..4 { n.update(dec!(5)); }
4024 assert_eq!(n.window_iqr().unwrap(), dec!(0));
4025 }
4026
4027 #[test]
4028 fn test_minmax_mean_absolute_deviation_none_for_empty() {
4029 assert!(norm(4).mean_absolute_deviation().is_none());
4030 }
4031
4032 #[test]
4033 fn test_minmax_mean_absolute_deviation_zero_for_constant() {
4034 let mut n = norm(4);
4035 for _ in 0..4 { n.update(dec!(7)); }
4036 let mad = n.mean_absolute_deviation().unwrap();
4037 assert!(mad.abs() < 1e-9, "constant window → MAD=0, got {}", mad);
4038 }
4039
4040 #[test]
4041 fn test_minmax_run_length_mean_none_for_single_value() {
4042 let mut n = norm(4);
4043 n.update(dec!(1));
4044 assert!(n.run_length_mean().is_none());
4045 }
4046
4047 #[test]
4048 fn test_minmax_run_length_mean_all_increasing() {
4049 let mut n = norm(4);
4050 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
4051 let r = n.run_length_mean().unwrap();
4053 assert!((r - 4.0).abs() < 1e-9, "monotone up → run_len=4, got {}", r);
4054 }
4055}
4056
4057pub struct ZScoreNormalizer {
4080 window_size: usize,
4081 window: VecDeque<Decimal>,
4082 sum: Decimal,
4083 sum_sq: Decimal,
4084}
4085
4086impl ZScoreNormalizer {
4087 pub fn new(window_size: usize) -> Result<Self, StreamError> {
4093 if window_size == 0 {
4094 return Err(StreamError::ConfigError {
4095 reason: "ZScoreNormalizer window_size must be > 0".into(),
4096 });
4097 }
4098 Ok(Self {
4099 window_size,
4100 window: VecDeque::with_capacity(window_size),
4101 sum: Decimal::ZERO,
4102 sum_sq: Decimal::ZERO,
4103 })
4104 }
4105
4106 pub fn update(&mut self, value: Decimal) {
4112 if self.window.len() == self.window_size {
4113 let evicted = self.window.pop_front().unwrap_or(Decimal::ZERO);
4114 self.sum -= evicted;
4115 self.sum_sq -= evicted * evicted;
4116 }
4117 self.window.push_back(value);
4118 self.sum += value;
4119 self.sum_sq += value * value;
4120 }
4121
4122 #[must_use = "z-score is returned; ignoring it loses the normalized value"]
4134 pub fn normalize(&self, value: Decimal) -> Result<f64, StreamError> {
4135 let n = self.window.len();
4136 if n == 0 {
4137 return Err(StreamError::NormalizationError {
4138 reason: "window is empty; call update() before normalize()".into(),
4139 });
4140 }
4141 if n < 2 {
4142 return Ok(0.0);
4143 }
4144 let std_dev = self.std_dev().unwrap_or(0.0);
4145 if std_dev < f64::EPSILON {
4146 return Ok(0.0);
4147 }
4148 let mean = self.mean().ok_or_else(|| StreamError::NormalizationError {
4149 reason: "mean unavailable".into(),
4150 })?;
4151 let diff = value - mean;
4152 let diff_f64 = diff.to_f64().ok_or_else(|| StreamError::NormalizationError {
4153 reason: "Decimal-to-f64 conversion failed for diff".into(),
4154 })?;
4155 Ok(diff_f64 / std_dev)
4156 }
4157
4158 pub fn mean(&self) -> Option<Decimal> {
4160 if self.window.is_empty() {
4161 return None;
4162 }
4163 let n = Decimal::from(self.window.len() as u64);
4164 Some(self.sum / n)
4165 }
4166
4167 pub fn std_dev(&self) -> Option<f64> {
4173 let n = self.window.len();
4174 if n == 0 {
4175 return None;
4176 }
4177 if n < 2 {
4178 return Some(0.0);
4179 }
4180 self.variance_f64().map(f64::sqrt)
4181 }
4182
4183 pub fn reset(&mut self) {
4185 self.window.clear();
4186 self.sum = Decimal::ZERO;
4187 self.sum_sq = Decimal::ZERO;
4188 }
4189
4190 pub fn len(&self) -> usize {
4192 self.window.len()
4193 }
4194
4195 pub fn is_empty(&self) -> bool {
4197 self.window.is_empty()
4198 }
4199
4200 pub fn window_size(&self) -> usize {
4202 self.window_size
4203 }
4204
4205 pub fn is_full(&self) -> bool {
4210 self.window.len() == self.window_size
4211 }
4212
4213 pub fn sum(&self) -> Option<Decimal> {
4218 if self.window.is_empty() {
4219 return None;
4220 }
4221 Some(self.sum)
4222 }
4223
4224 pub fn variance(&self) -> Option<Decimal> {
4229 let n = self.window.len();
4230 if n < 2 {
4231 return None;
4232 }
4233 let n_dec = Decimal::from(n as u64);
4234 let mean = self.sum / n_dec;
4235 let v = (self.sum_sq / n_dec) - mean * mean;
4236 Some(if v < Decimal::ZERO { Decimal::ZERO } else { v })
4237 }
4238
4239 pub fn std_dev_f64(&self) -> Option<f64> {
4243 self.variance_f64().map(|v| v.sqrt())
4244 }
4245
4246 pub fn variance_f64(&self) -> Option<f64> {
4250 use rust_decimal::prelude::ToPrimitive;
4251 self.variance()?.to_f64()
4252 }
4253
4254 pub fn normalize_batch(
4264 &mut self,
4265 values: &[Decimal],
4266 ) -> Result<Vec<f64>, StreamError> {
4267 values
4268 .iter()
4269 .map(|&v| {
4270 self.update(v);
4271 self.normalize(v)
4272 })
4273 .collect()
4274 }
4275
4276 pub fn is_outlier(&self, value: Decimal, z_threshold: f64) -> bool {
4281 use rust_decimal::prelude::ToPrimitive;
4282 if self.window.len() < 2 {
4283 return false;
4284 }
4285 let sd = self.std_dev().unwrap_or(0.0);
4286 if sd == 0.0 {
4287 return false;
4288 }
4289 let Some(mean_f64) = self.mean().and_then(|m| m.to_f64()) else { return false; };
4290 let val_f64 = value.to_f64().unwrap_or(mean_f64);
4291 ((val_f64 - mean_f64) / sd).abs() > z_threshold
4292 }
4293
4294 pub fn percentile_rank(&self, value: Decimal) -> Option<f64> {
4298 if self.window.is_empty() {
4299 return None;
4300 }
4301 let count = self.window.iter().filter(|&&v| v <= value).count();
4302 Some(count as f64 / self.window.len() as f64)
4303 }
4304
4305 pub fn running_min(&self) -> Option<Decimal> {
4309 self.window.iter().copied().reduce(Decimal::min)
4310 }
4311
4312 pub fn running_max(&self) -> Option<Decimal> {
4316 self.window.iter().copied().reduce(Decimal::max)
4317 }
4318
4319 pub fn window_range(&self) -> Option<Decimal> {
4323 let min = self.running_min()?;
4324 let max = self.running_max()?;
4325 Some(max - min)
4326 }
4327
4328 pub fn coefficient_of_variation(&self) -> Option<f64> {
4333 let mean = self.mean()?;
4334 if mean.is_zero() {
4335 return None;
4336 }
4337 let std_dev = self.std_dev()?;
4338 let mean_f = mean.abs().to_f64()?;
4339 Some(std_dev / mean_f)
4340 }
4341
4342 pub fn sample_variance(&self) -> Option<f64> {
4349 let sd = self.std_dev()?;
4350 Some(sd * sd)
4351 }
4352
4353 pub fn window_mean_f64(&self) -> Option<f64> {
4358 use rust_decimal::prelude::ToPrimitive;
4359 self.mean()?.to_f64()
4360 }
4361
4362 pub fn is_near_mean(&self, value: Decimal, sigma_tolerance: f64) -> bool {
4368 if self.window.len() < 2 {
4370 return false;
4371 }
4372 let Some(std_dev) = self.std_dev() else { return false; };
4373 if std_dev == 0.0 {
4374 return true;
4375 }
4376 let Some(mean) = self.mean() else { return false; };
4377 use rust_decimal::prelude::ToPrimitive;
4378 let diff = (value - mean).abs().to_f64().unwrap_or(f64::MAX);
4379 diff / std_dev <= sigma_tolerance
4380 }
4381
4382 pub fn window_sum(&self) -> Decimal {
4386 self.sum
4387 }
4388
4389 pub fn window_sum_f64(&self) -> f64 {
4393 use rust_decimal::prelude::ToPrimitive;
4394 self.sum.to_f64().unwrap_or(0.0)
4395 }
4396
4397 pub fn window_max_f64(&self) -> Option<f64> {
4401 use rust_decimal::prelude::ToPrimitive;
4402 self.running_max()?.to_f64()
4403 }
4404
4405 pub fn window_min_f64(&self) -> Option<f64> {
4409 use rust_decimal::prelude::ToPrimitive;
4410 self.running_min()?.to_f64()
4411 }
4412
4413 pub fn window_span_f64(&self) -> Option<f64> {
4417 use rust_decimal::prelude::ToPrimitive;
4418 self.window_range()?.to_f64()
4419 }
4420
4421 pub fn kurtosis(&self) -> Option<f64> {
4427 use rust_decimal::prelude::ToPrimitive;
4428 let n = self.window.len();
4429 if n < 4 {
4430 return None;
4431 }
4432 let n_f = n as f64;
4433 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4434 if vals.len() < n {
4435 return None;
4436 }
4437 let mean = vals.iter().sum::<f64>() / n_f;
4438 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
4439 let std_dev = variance.sqrt();
4440 if std_dev == 0.0 {
4441 return None;
4442 }
4443 let kurt = vals.iter().map(|v| ((v - mean) / std_dev).powi(4)).sum::<f64>() / n_f - 3.0;
4444 Some(kurt)
4445 }
4446
4447 pub fn skewness(&self) -> Option<f64> {
4453 use rust_decimal::prelude::ToPrimitive;
4454 let n = self.window.len();
4455 if n < 3 {
4456 return None;
4457 }
4458 let n_f = n as f64;
4459 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4460 if vals.len() < n {
4461 return None;
4462 }
4463 let mean = vals.iter().sum::<f64>() / n_f;
4464 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n_f;
4465 let std_dev = variance.sqrt();
4466 if std_dev == 0.0 {
4467 return None;
4468 }
4469 let skew = vals.iter().map(|v| ((v - mean) / std_dev).powi(3)).sum::<f64>() / n_f;
4470 Some(skew)
4471 }
4472
4473 pub fn is_extreme(&self, value: Decimal, sigma: f64) -> bool {
4478 self.normalize(value).ok().map_or(false, |z| z.abs() > sigma)
4479 }
4480
4481 pub fn latest(&self) -> Option<Decimal> {
4483 self.window.back().copied()
4484 }
4485
4486 pub fn median(&self) -> Option<Decimal> {
4488 if self.window.is_empty() { return None; }
4489 let mut vals: Vec<Decimal> = self.window.iter().copied().collect();
4490 vals.sort();
4491 let mid = vals.len() / 2;
4492 if vals.len() % 2 == 0 {
4493 Some((vals[mid - 1] + vals[mid]) / Decimal::TWO)
4494 } else {
4495 Some(vals[mid])
4496 }
4497 }
4498
4499 pub fn percentile(&self, value: Decimal) -> Option<f64> {
4503 self.percentile_rank(value)
4504 }
4505
4506 pub fn interquartile_range(&self) -> Option<Decimal> {
4511 let n = self.window.len();
4512 if n < 4 {
4513 return None;
4514 }
4515 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
4516 sorted.sort();
4517 let q1_idx = n / 4;
4518 let q3_idx = 3 * n / 4;
4519 Some(sorted[q3_idx] - sorted[q1_idx])
4520 }
4521
4522 pub fn ema_z_score(value: Decimal, alpha: f64, ema_mean: &mut f64, ema_var: &mut f64) -> Option<f64> {
4529 use rust_decimal::prelude::ToPrimitive;
4530 let v = value.to_f64()?;
4531 let delta = v - *ema_mean;
4532 *ema_mean += alpha * delta;
4533 *ema_var = (1.0 - alpha) * (*ema_var + alpha * delta * delta);
4534 let std = ema_var.sqrt();
4535 if std == 0.0 { return None; }
4536 Some((v - *ema_mean) / std)
4537 }
4538
4539 pub fn z_score_of_latest(&self) -> Option<f64> {
4543 let latest = self.latest()?;
4544 self.normalize(latest).ok()
4545 }
4546
4547 pub fn ema_of_z_scores(&self, alpha: f64) -> Option<f64> {
4552 let n = self.window.len();
4553 if n < 2 {
4554 return None;
4555 }
4556 let mut ema: Option<f64> = None;
4557 for &value in &self.window {
4558 if let Ok(z) = self.normalize(value) {
4559 ema = Some(match ema {
4560 None => z,
4561 Some(prev) => alpha * z + (1.0 - alpha) * prev,
4562 });
4563 }
4564 }
4565 ema
4566 }
4567
4568 pub fn add_observation(&mut self, value: Decimal) -> &mut Self {
4570 self.update(value);
4571 self
4572 }
4573
4574 pub fn deviation_from_mean(&self, value: Decimal) -> Option<f64> {
4578 use rust_decimal::prelude::ToPrimitive;
4579 let mean = self.mean()?.to_f64()?;
4580 value.to_f64().map(|v| v - mean)
4581 }
4582
4583 pub fn trim_outliers(&self, sigma: f64) -> Vec<Decimal> {
4588 use rust_decimal::prelude::ToPrimitive;
4589 if self.window.is_empty() { return vec![]; }
4590 let Some(mean) = self.mean() else { return vec![]; };
4591 let std = match self.std_dev() {
4592 Some(s) if s > 0.0 => s,
4593 _ => return self.window.iter().copied().collect(),
4594 };
4595 let Some(mean_f64) = mean.to_f64() else { return vec![]; };
4596 self.window.iter().copied()
4597 .filter(|v| {
4598 v.to_f64().map_or(false, |vf| ((vf - mean_f64) / std).abs() <= sigma)
4599 })
4600 .collect()
4601 }
4602
4603 pub fn rolling_zscore_batch(&mut self, values: &[Decimal]) -> Vec<Option<f64>> {
4609 values.iter().map(|&v| {
4610 self.update(v);
4611 self.normalize(v).ok()
4612 }).collect()
4613 }
4614
4615 pub fn rolling_mean_change(&self) -> Option<f64> {
4621 let n = self.window.len();
4622 if n < 2 {
4623 return None;
4624 }
4625 let mid = n / 2;
4626 let first: Decimal = self.window.iter().take(mid).copied().sum::<Decimal>()
4627 / Decimal::from(mid as u64);
4628 let second: Decimal = self.window.iter().skip(mid).copied().sum::<Decimal>()
4629 / Decimal::from((n - mid) as u64);
4630 (second - first).to_f64()
4631 }
4632
4633 pub fn count_positive_z_scores(&self) -> usize {
4637 self.window
4638 .iter()
4639 .filter(|&&v| self.normalize(v).map_or(false, |z| z > 0.0))
4640 .count()
4641 }
4642
4643 pub fn is_mean_stable(&self, threshold: f64) -> bool {
4648 self.rolling_mean_change().map_or(false, |c| c.abs() < threshold)
4649 }
4650
4651 pub fn above_threshold_count(&self, z_threshold: f64) -> usize {
4655 self.window
4656 .iter()
4657 .filter(|&&v| {
4658 self.normalize(v)
4659 .map_or(false, |z| z.abs() > z_threshold)
4660 })
4661 .count()
4662 }
4663
4664 pub fn mad(&self) -> Option<Decimal> {
4669 let med = self.median()?;
4670 let mut deviations: Vec<Decimal> = self.window.iter().map(|&x| (x - med).abs()).collect();
4671 deviations.sort();
4672 let n = deviations.len();
4673 if n == 0 { return None; }
4674 let mid = n / 2;
4675 if n % 2 == 0 {
4676 Some((deviations[mid - 1] + deviations[mid]) / Decimal::TWO)
4677 } else {
4678 Some(deviations[mid])
4679 }
4680 }
4681
4682 pub fn robust_z_score(&self, value: Decimal) -> Option<f64> {
4687 use rust_decimal::prelude::ToPrimitive;
4688 let med = self.median()?;
4689 let mad = self.mad()?;
4690 if mad.is_zero() { return None; }
4691 ((value - med) / mad).to_f64()
4692 }
4693
4694 pub fn count_above(&self, threshold: Decimal) -> usize {
4696 self.window.iter().filter(|&&v| v > threshold).count()
4697 }
4698
4699 pub fn count_below(&self, threshold: Decimal) -> usize {
4701 self.window.iter().filter(|&&v| v < threshold).count()
4702 }
4703
4704 pub fn percentile_value(&self, p: f64) -> Option<Decimal> {
4709 if self.window.is_empty() {
4710 return None;
4711 }
4712 let p = p.clamp(0.0, 1.0);
4713 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
4714 sorted.sort();
4715 let n = sorted.len();
4716 if n == 1 {
4717 return Some(sorted[0]);
4718 }
4719 let idx = p * (n - 1) as f64;
4720 let lo = idx.floor() as usize;
4721 let hi = idx.ceil() as usize;
4722 if lo == hi {
4723 Some(sorted[lo])
4724 } else {
4725 let frac = Decimal::try_from(idx - lo as f64).ok()?;
4726 Some(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
4727 }
4728 }
4729
4730 pub fn ewma(&self, alpha: f64) -> Option<f64> {
4736 use rust_decimal::prelude::ToPrimitive;
4737 let alpha = alpha.clamp(1e-9, 1.0);
4738 let mut iter = self.window.iter();
4739 let first = iter.next()?.to_f64()?;
4740 let result = iter.fold(first, |acc, &v| {
4741 let vf = v.to_f64().unwrap_or(acc);
4742 alpha * vf + (1.0 - alpha) * acc
4743 });
4744 Some(result)
4745 }
4746
4747 pub fn midpoint(&self) -> Option<Decimal> {
4751 let lo = self.running_min()?;
4752 let hi = self.running_max()?;
4753 Some((lo + hi) / Decimal::from(2u64))
4754 }
4755
4756 pub fn clamp_to_window(&self, value: Decimal) -> Decimal {
4760 match (self.running_min(), self.running_max()) {
4761 (Some(lo), Some(hi)) => value.max(lo).min(hi),
4762 _ => value,
4763 }
4764 }
4765
4766 pub fn fraction_above_mid(&self) -> Option<f64> {
4771 let lo = self.running_min()?;
4772 let hi = self.running_max()?;
4773 if lo == hi {
4774 return None;
4775 }
4776 let mid = (lo + hi) / Decimal::from(2u64);
4777 let above = self.window.iter().filter(|&&v| v > mid).count();
4778 Some(above as f64 / self.window.len() as f64)
4779 }
4780
4781 pub fn normalized_range(&self) -> Option<f64> {
4786 use rust_decimal::prelude::ToPrimitive;
4787 let span = self.window_range()?;
4788 let mean = self.mean()?;
4789 if mean.is_zero() {
4790 return None;
4791 }
4792 (span / mean).to_f64()
4793 }
4794
4795 pub fn min_max(&self) -> Option<(Decimal, Decimal)> {
4799 Some((self.running_min()?, self.running_max()?))
4800 }
4801
4802 pub fn values(&self) -> Vec<Decimal> {
4804 self.window.iter().copied().collect()
4805 }
4806
4807 pub fn above_zero_fraction(&self) -> Option<f64> {
4811 if self.window.is_empty() {
4812 return None;
4813 }
4814 let above = self.window.iter().filter(|&&v| v > Decimal::ZERO).count();
4815 Some(above as f64 / self.window.len() as f64)
4816 }
4817
4818 pub fn z_score_opt(&self, value: Decimal) -> Option<f64> {
4824 self.normalize(value).ok()
4825 }
4826
4827 pub fn is_stable(&self, z_threshold: f64) -> bool {
4832 self.z_score_of_latest()
4833 .map_or(false, |z| z.abs() <= z_threshold)
4834 }
4835
4836 pub fn fraction_above(&self, threshold: Decimal) -> Option<f64> {
4840 if self.window.is_empty() {
4841 return None;
4842 }
4843 Some(self.count_above(threshold) as f64 / self.window.len() as f64)
4844 }
4845
4846 pub fn fraction_below(&self, threshold: Decimal) -> Option<f64> {
4850 if self.window.is_empty() {
4851 return None;
4852 }
4853 Some(self.count_below(threshold) as f64 / self.window.len() as f64)
4854 }
4855
4856 pub fn window_values_above(&self, threshold: Decimal) -> Vec<Decimal> {
4858 self.window.iter().copied().filter(|&v| v > threshold).collect()
4859 }
4860
4861 pub fn window_values_below(&self, threshold: Decimal) -> Vec<Decimal> {
4863 self.window.iter().copied().filter(|&v| v < threshold).collect()
4864 }
4865
4866 pub fn count_equal(&self, value: Decimal) -> usize {
4868 self.window.iter().filter(|&&v| v == value).count()
4869 }
4870
4871 pub fn rolling_range(&self) -> Option<Decimal> {
4875 let lo = self.running_min()?;
4876 let hi = self.running_max()?;
4877 Some(hi - lo)
4878 }
4879
4880 pub fn autocorrelation_lag1(&self) -> Option<f64> {
4884 use rust_decimal::prelude::ToPrimitive;
4885 let n = self.window.len();
4886 if n < 2 {
4887 return None;
4888 }
4889 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4890 if vals.len() < 2 {
4891 return None;
4892 }
4893 let mean = vals.iter().sum::<f64>() / vals.len() as f64;
4894 let var: f64 = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / vals.len() as f64;
4895 if var == 0.0 {
4896 return None;
4897 }
4898 let cov: f64 = vals.windows(2).map(|w| (w[0] - mean) * (w[1] - mean)).sum::<f64>()
4899 / (vals.len() - 1) as f64;
4900 Some(cov / var)
4901 }
4902
4903 pub fn trend_consistency(&self) -> Option<f64> {
4907 let n = self.window.len();
4908 if n < 2 {
4909 return None;
4910 }
4911 let up = self.window.iter().collect::<Vec<_>>().windows(2)
4912 .filter(|w| w[1] > w[0]).count();
4913 Some(up as f64 / (n - 1) as f64)
4914 }
4915
4916 pub fn mean_absolute_deviation(&self) -> Option<f64> {
4920 use rust_decimal::prelude::ToPrimitive;
4921 if self.window.is_empty() {
4922 return None;
4923 }
4924 let n = self.window.len();
4925 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
4926 let mean = vals.iter().sum::<f64>() / n as f64;
4927 let mad = vals.iter().map(|v| (v - mean).abs()).sum::<f64>() / n as f64;
4928 Some(mad)
4929 }
4930
4931 pub fn percentile_of_latest(&self) -> Option<f64> {
4936 let latest = self.latest()?;
4937 self.percentile(latest)
4938 }
4939
4940 pub fn tail_ratio(&self) -> Option<f64> {
4946 use rust_decimal::prelude::ToPrimitive;
4947 let max = self.running_max()?;
4948 let p75 = self.percentile_value(0.75)?;
4949 if p75.is_zero() {
4950 return None;
4951 }
4952 (max / p75).to_f64()
4953 }
4954
4955 pub fn z_score_of_min(&self) -> Option<f64> {
4959 let min = self.running_min()?;
4960 self.z_score_opt(min)
4961 }
4962
4963 pub fn z_score_of_max(&self) -> Option<f64> {
4967 let max = self.running_max()?;
4968 self.z_score_opt(max)
4969 }
4970
4971 pub fn window_entropy(&self) -> Option<f64> {
4977 if self.window.is_empty() {
4978 return None;
4979 }
4980 let n = self.window.len() as f64;
4981 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
4982 for v in &self.window {
4983 *counts.entry(v.to_string()).or_insert(0) += 1;
4984 }
4985 let entropy: f64 = counts.values().map(|&c| {
4986 let p = c as f64 / n;
4987 -p * p.ln()
4988 }).sum();
4989 Some(entropy)
4990 }
4991
4992 pub fn normalized_std_dev(&self) -> Option<f64> {
4994 self.coefficient_of_variation()
4995 }
4996
4997 pub fn value_above_mean_count(&self) -> Option<usize> {
5001 let mean = self.mean()?;
5002 Some(self.window.iter().filter(|&&v| v > mean).count())
5003 }
5004
5005 pub fn consecutive_above_mean(&self) -> Option<usize> {
5009 let mean = self.mean()?;
5010 let mut max_run = 0usize;
5011 let mut current = 0usize;
5012 for &v in &self.window {
5013 if v > mean {
5014 current += 1;
5015 if current > max_run {
5016 max_run = current;
5017 }
5018 } else {
5019 current = 0;
5020 }
5021 }
5022 Some(max_run)
5023 }
5024
5025 pub fn above_threshold_fraction(&self, threshold: Decimal) -> Option<f64> {
5029 if self.window.is_empty() {
5030 return None;
5031 }
5032 let count = self.window.iter().filter(|&&v| v > threshold).count();
5033 Some(count as f64 / self.window.len() as f64)
5034 }
5035
5036 pub fn below_threshold_fraction(&self, threshold: Decimal) -> Option<f64> {
5040 if self.window.is_empty() {
5041 return None;
5042 }
5043 let count = self.window.iter().filter(|&&v| v < threshold).count();
5044 Some(count as f64 / self.window.len() as f64)
5045 }
5046
5047 pub fn lag_k_autocorrelation(&self, k: usize) -> Option<f64> {
5051 use rust_decimal::prelude::ToPrimitive;
5052 let n = self.window.len();
5053 if k == 0 || k >= n {
5054 return None;
5055 }
5056 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5057 if vals.len() != n {
5058 return None;
5059 }
5060 let mean = vals.iter().sum::<f64>() / n as f64;
5061 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
5062 if var == 0.0 {
5063 return None;
5064 }
5065 let m = n - k;
5066 let cov: f64 = (0..m).map(|i| (vals[i] - mean) * (vals[i + k] - mean)).sum::<f64>() / m as f64;
5067 Some(cov / var)
5068 }
5069
5070 pub fn half_life_estimate(&self) -> Option<f64> {
5076 use rust_decimal::prelude::ToPrimitive;
5077 let n = self.window.len();
5078 if n < 3 {
5079 return None;
5080 }
5081 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5082 if vals.len() != n {
5083 return None;
5084 }
5085 let diffs: Vec<f64> = vals.windows(2).map(|w| w[1] - w[0]).collect();
5086 let lagged: Vec<f64> = vals[..n - 1].to_vec();
5087 let nf = diffs.len() as f64;
5088 let mean_l = lagged.iter().sum::<f64>() / nf;
5089 let mean_d = diffs.iter().sum::<f64>() / nf;
5090 let cov: f64 = lagged.iter().zip(diffs.iter()).map(|(l, d)| (l - mean_l) * (d - mean_d)).sum::<f64>();
5091 let var: f64 = lagged.iter().map(|l| (l - mean_l).powi(2)).sum::<f64>();
5092 if var == 0.0 {
5093 return None;
5094 }
5095 let beta = cov / var;
5096 if beta >= 0.0 {
5097 return None;
5098 }
5099 let lambda = (1.0 + beta).abs().ln();
5100 if lambda == 0.0 {
5101 return None;
5102 }
5103 Some(-std::f64::consts::LN_2 / lambda)
5104 }
5105
5106 pub fn geometric_mean(&self) -> Option<f64> {
5111 use rust_decimal::prelude::ToPrimitive;
5112 if self.window.is_empty() {
5113 return None;
5114 }
5115 let logs: Vec<f64> = self.window.iter()
5116 .filter_map(|v| v.to_f64())
5117 .filter_map(|f| if f > 0.0 { Some(f.ln()) } else { None })
5118 .collect();
5119 if logs.len() != self.window.len() {
5120 return None;
5121 }
5122 Some((logs.iter().sum::<f64>() / logs.len() as f64).exp())
5123 }
5124
5125 pub fn harmonic_mean(&self) -> Option<f64> {
5130 use rust_decimal::prelude::ToPrimitive;
5131 if self.window.is_empty() {
5132 return None;
5133 }
5134 let reciprocals: Vec<f64> = self.window.iter()
5135 .filter_map(|v| v.to_f64())
5136 .filter_map(|f| if f != 0.0 { Some(1.0 / f) } else { None })
5137 .collect();
5138 if reciprocals.len() != self.window.len() {
5139 return None;
5140 }
5141 let n = reciprocals.len() as f64;
5142 Some(n / reciprocals.iter().sum::<f64>())
5143 }
5144
5145 pub fn range_normalized_value(&self, value: Decimal) -> Option<f64> {
5149 use rust_decimal::prelude::ToPrimitive;
5150 let min = self.running_min()?;
5151 let max = self.running_max()?;
5152 let range = max - min;
5153 if range.is_zero() {
5154 return None;
5155 }
5156 ((value - min) / range).to_f64()
5157 }
5158
5159 pub fn distance_from_median(&self, value: Decimal) -> Option<f64> {
5163 use rust_decimal::prelude::ToPrimitive;
5164 let med = self.median()?;
5165 (value - med).to_f64()
5166 }
5167
5168 pub fn momentum(&self) -> Option<f64> {
5173 use rust_decimal::prelude::ToPrimitive;
5174 if self.window.len() < 2 {
5175 return None;
5176 }
5177 let oldest = *self.window.front()?;
5178 let latest = *self.window.back()?;
5179 (latest - oldest).to_f64()
5180 }
5181
5182 pub fn value_rank(&self, value: Decimal) -> Option<f64> {
5187 if self.window.is_empty() {
5188 return None;
5189 }
5190 let n = self.window.len();
5191 let below = self.window.iter().filter(|&&v| v < value).count();
5192 Some(below as f64 / n as f64)
5193 }
5194
5195 pub fn coeff_of_variation(&self) -> Option<f64> {
5200 use rust_decimal::prelude::ToPrimitive;
5201 let n = self.window.len();
5202 if n < 2 {
5203 return None;
5204 }
5205 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5206 if vals.len() < 2 {
5207 return None;
5208 }
5209 let nf = vals.len() as f64;
5210 let mean = vals.iter().sum::<f64>() / nf;
5211 if mean == 0.0 {
5212 return None;
5213 }
5214 let std_dev = (vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (nf - 1.0)).sqrt();
5215 Some(std_dev / mean.abs())
5216 }
5217
5218 pub fn quantile_range(&self) -> Option<f64> {
5222 use rust_decimal::prelude::ToPrimitive;
5223 let q3 = self.percentile_value(0.75)?;
5224 let q1 = self.percentile_value(0.25)?;
5225 (q3 - q1).to_f64()
5226 }
5227
5228 pub fn upper_quartile(&self) -> Option<Decimal> {
5232 self.percentile_value(0.75)
5233 }
5234
5235 pub fn lower_quartile(&self) -> Option<Decimal> {
5239 self.percentile_value(0.25)
5240 }
5241
5242 pub fn sign_change_rate(&self) -> Option<f64> {
5248 let n = self.window.len();
5249 if n < 3 {
5250 return None;
5251 }
5252 let vals: Vec<&Decimal> = self.window.iter().collect();
5253 let diffs: Vec<i32> = vals
5254 .windows(2)
5255 .map(|w| {
5256 if w[1] > w[0] { 1 } else if w[1] < w[0] { -1 } else { 0 }
5257 })
5258 .collect();
5259 let total_pairs = (diffs.len() - 1) as f64;
5260 if total_pairs == 0.0 {
5261 return None;
5262 }
5263 let changes = diffs
5264 .windows(2)
5265 .filter(|w| w[0] != 0 && w[1] != 0 && w[0] != w[1])
5266 .count();
5267 Some(changes as f64 / total_pairs)
5268 }
5269
5270 pub fn trimmed_mean(&self, p: f64) -> Option<f64> {
5278 use rust_decimal::prelude::ToPrimitive;
5279 if self.window.is_empty() {
5280 return None;
5281 }
5282 let p = p.clamp(0.0, 0.499);
5283 let mut sorted: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5284 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
5285 let n = sorted.len();
5286 let trim = (n as f64 * p).floor() as usize;
5287 let trimmed = &sorted[trim..n - trim];
5288 if trimmed.is_empty() {
5289 return None;
5290 }
5291 Some(trimmed.iter().sum::<f64>() / trimmed.len() as f64)
5292 }
5293
5294 pub fn linear_trend_slope(&self) -> Option<f64> {
5299 use rust_decimal::prelude::ToPrimitive;
5300 let n = self.window.len();
5301 if n < 2 {
5302 return None;
5303 }
5304 let n_f = n as f64;
5305 let x_mean = (n_f - 1.0) / 2.0;
5306 let y_vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5307 if y_vals.len() < 2 {
5308 return None;
5309 }
5310 let y_mean = y_vals.iter().sum::<f64>() / y_vals.len() as f64;
5311 let numerator: f64 = y_vals
5312 .iter()
5313 .enumerate()
5314 .map(|(i, &y)| (i as f64 - x_mean) * (y - y_mean))
5315 .sum();
5316 let denominator: f64 = (0..n).map(|i| (i as f64 - x_mean).powi(2)).sum();
5317 if denominator == 0.0 {
5318 return None;
5319 }
5320 Some(numerator / denominator)
5321 }
5322
5323 pub fn variance_ratio(&self) -> Option<f64> {
5331 use rust_decimal::prelude::ToPrimitive;
5332 let n = self.window.len();
5333 if n < 4 {
5334 return None;
5335 }
5336 let mid = n / 2;
5337 let first: Vec<f64> = self.window.iter().take(mid).filter_map(|v| v.to_f64()).collect();
5338 let second: Vec<f64> = self.window.iter().skip(mid).filter_map(|v| v.to_f64()).collect();
5339 let var = |vals: &[f64]| -> Option<f64> {
5340 let n_f = vals.len() as f64;
5341 if n_f < 2.0 { return None; }
5342 let mean = vals.iter().sum::<f64>() / n_f;
5343 Some(vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n_f - 1.0))
5344 };
5345 let v1 = var(&first)?;
5346 let v2 = var(&second)?;
5347 if v2 == 0.0 {
5348 return None;
5349 }
5350 Some(v1 / v2)
5351 }
5352
5353 pub fn z_score_trend_slope(&self) -> Option<f64> {
5359 use rust_decimal::prelude::ToPrimitive;
5360 let n = self.window.len();
5361 if n < 2 {
5362 return None;
5363 }
5364 let mean_dec = self.mean()?;
5365 let std_dev = self.std_dev()?;
5366 if std_dev == 0.0 {
5367 return None;
5368 }
5369 let mean_f = mean_dec.to_f64()?;
5370 let z_vals: Vec<f64> = self
5371 .window
5372 .iter()
5373 .filter_map(|v| v.to_f64())
5374 .map(|v| (v - mean_f) / std_dev)
5375 .collect();
5376 if z_vals.len() < 2 {
5377 return None;
5378 }
5379 let n_f = z_vals.len() as f64;
5380 let x_mean = (n_f - 1.0) / 2.0;
5381 let z_mean = z_vals.iter().sum::<f64>() / n_f;
5382 let num: f64 = z_vals.iter().enumerate().map(|(i, &z)| (i as f64 - x_mean) * (z - z_mean)).sum();
5383 let den: f64 = (0..z_vals.len()).map(|i| (i as f64 - x_mean).powi(2)).sum();
5384 if den == 0.0 { return None; }
5385 Some(num / den)
5386 }
5387
5388 pub fn mean_absolute_change(&self) -> Option<f64> {
5392 use rust_decimal::prelude::ToPrimitive;
5393 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5394 if vals.len() < 2 {
5395 return None;
5396 }
5397 let mac = vals.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / (vals.len() - 1) as f64;
5398 Some(mac)
5399 }
5400
5401 pub fn monotone_increase_fraction(&self) -> Option<f64> {
5405 let vals: Vec<Decimal> = self.window.iter().copied().collect();
5406 let n = vals.len();
5407 if n < 2 {
5408 return None;
5409 }
5410 let inc = vals.windows(2).filter(|w| w[1] > w[0]).count();
5411 Some(inc as f64 / (n - 1) as f64)
5412 }
5413
5414 pub fn abs_max(&self) -> Option<Decimal> {
5416 self.window.iter().map(|v| v.abs()).reduce(|a, b| a.max(b))
5417 }
5418
5419 pub fn abs_min(&self) -> Option<Decimal> {
5421 self.window.iter().map(|v| v.abs()).reduce(|a, b| a.min(b))
5422 }
5423
5424 pub fn max_count(&self) -> Option<usize> {
5426 let max = self.window.iter().copied().reduce(|a, b| a.max(b))?;
5427 Some(self.window.iter().filter(|&&v| v == max).count())
5428 }
5429
5430 pub fn min_count(&self) -> Option<usize> {
5432 let min = self.window.iter().copied().reduce(|a, b| a.min(b))?;
5433 Some(self.window.iter().filter(|&&v| v == min).count())
5434 }
5435
5436 pub fn mean_ratio(&self) -> Option<f64> {
5438 use rust_decimal::prelude::ToPrimitive;
5439 let n = self.window.len();
5440 if n < 2 {
5441 return None;
5442 }
5443 let current_mean = self.mean()?;
5444 let half = (n / 2).max(1);
5445 let early_sum: Decimal = self.window.iter().take(half).copied().sum();
5446 let early_mean = early_sum / Decimal::from(half as i64);
5447 if early_mean.is_zero() {
5448 return None;
5449 }
5450 (current_mean / early_mean).to_f64()
5451 }
5452
5453 pub fn exponential_weighted_mean(&self, alpha: f64) -> Option<f64> {
5457 use rust_decimal::prelude::ToPrimitive;
5458 if self.window.is_empty() {
5459 return None;
5460 }
5461 let alpha = alpha.clamp(1e-6, 1.0);
5462 let vals: Vec<f64> = self.window.iter().filter_map(|v| v.to_f64()).collect();
5463 if vals.is_empty() {
5464 return None;
5465 }
5466 let mut ewm = vals[0];
5467 for &v in &vals[1..] {
5468 ewm = alpha * v + (1.0 - alpha) * ewm;
5469 }
5470 Some(ewm)
5471 }
5472
5473 pub fn peak_to_trough_ratio(&self) -> Option<f64> {
5475 use rust_decimal::prelude::ToPrimitive;
5476 let max = self.window.iter().copied().reduce(|a, b| a.max(b))?;
5477 let min = self.window.iter().copied().reduce(|a, b| a.min(b))?;
5478 if min.is_zero() {
5479 return None;
5480 }
5481 (max / min).to_f64()
5482 }
5483
5484 pub fn second_moment(&self) -> Option<f64> {
5486 use rust_decimal::prelude::ToPrimitive;
5487 if self.window.is_empty() {
5488 return None;
5489 }
5490 let sum: f64 = self.window.iter().filter_map(|v| v.to_f64()).map(|v| v * v).sum();
5491 Some(sum / self.window.len() as f64)
5492 }
5493
5494 pub fn range_over_mean(&self) -> Option<f64> {
5496 use rust_decimal::prelude::ToPrimitive;
5497 let max = self.window.iter().copied().reduce(|a, b| a.max(b))?;
5498 let min = self.window.iter().copied().reduce(|a, b| a.min(b))?;
5499 let mean = self.mean()?;
5500 if mean.is_zero() {
5501 return None;
5502 }
5503 ((max - min) / mean).to_f64()
5504 }
5505
5506 pub fn above_median_fraction(&self) -> Option<f64> {
5508 if self.window.is_empty() {
5509 return None;
5510 }
5511 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
5512 sorted.sort();
5513 let mid = sorted.len() / 2;
5514 let median = if sorted.len() % 2 == 0 {
5515 (sorted[mid - 1] + sorted[mid]) / Decimal::from(2)
5516 } else {
5517 sorted[mid]
5518 };
5519 let count = self.window.iter().filter(|&&v| v > median).count();
5520 Some(count as f64 / self.window.len() as f64)
5521 }
5522
5523 pub fn interquartile_mean(&self) -> Option<f64> {
5527 use rust_decimal::prelude::ToPrimitive;
5528 if self.window.is_empty() {
5529 return None;
5530 }
5531 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
5532 sorted.sort();
5533 let n = sorted.len();
5534 let q1_idx = n / 4;
5535 let q3_idx = (3 * n) / 4;
5536 let iqr_vals: Vec<f64> = sorted[q1_idx..q3_idx]
5537 .iter()
5538 .filter_map(|v| v.to_f64())
5539 .collect();
5540 if iqr_vals.is_empty() {
5541 return None;
5542 }
5543 Some(iqr_vals.iter().sum::<f64>() / iqr_vals.len() as f64)
5544 }
5545
5546 pub fn outlier_fraction(&self, threshold: f64) -> Option<f64> {
5548 use rust_decimal::prelude::ToPrimitive;
5549 if self.window.is_empty() {
5550 return None;
5551 }
5552 let std_dev = self.std_dev()?;
5553 let mean = self.mean()?.to_f64()?;
5554 if std_dev == 0.0 {
5555 return Some(0.0);
5556 }
5557 let count = self.window
5558 .iter()
5559 .filter_map(|v| v.to_f64())
5560 .filter(|&v| ((v - mean) / std_dev).abs() > threshold)
5561 .count();
5562 Some(count as f64 / self.window.len() as f64)
5563 }
5564
5565 pub fn sign_flip_count(&self) -> Option<usize> {
5567 if self.window.len() < 2 {
5568 return None;
5569 }
5570 let count = self.window
5571 .iter()
5572 .collect::<Vec<_>>()
5573 .windows(2)
5574 .filter(|w| w[0].is_sign_negative() != w[1].is_sign_negative())
5575 .count();
5576 Some(count)
5577 }
5578
5579 pub fn rms(&self) -> Option<f64> {
5581 use rust_decimal::prelude::ToPrimitive;
5582 if self.window.is_empty() {
5583 return None;
5584 }
5585 let sum_sq: f64 = self.window.iter().filter_map(|v| v.to_f64()).map(|v| v * v).sum();
5586 Some((sum_sq / self.window.len() as f64).sqrt())
5587 }
5588
5589 pub fn distinct_count(&self) -> usize {
5593 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
5594 sorted.sort();
5595 sorted.dedup();
5596 sorted.len()
5597 }
5598
5599 pub fn max_fraction(&self) -> Option<f64> {
5601 if self.window.is_empty() {
5602 return None;
5603 }
5604 let max = self.window.iter().copied().max()?;
5605 let count = self.window.iter().filter(|&&v| v == max).count();
5606 Some(count as f64 / self.window.len() as f64)
5607 }
5608
5609 pub fn min_fraction(&self) -> Option<f64> {
5611 if self.window.is_empty() {
5612 return None;
5613 }
5614 let min = self.window.iter().copied().min()?;
5615 let count = self.window.iter().filter(|&&v| v == min).count();
5616 Some(count as f64 / self.window.len() as f64)
5617 }
5618
5619 pub fn latest_minus_mean(&self) -> Option<f64> {
5621 use rust_decimal::prelude::ToPrimitive;
5622 let latest = self.latest()?;
5623 let mean = self.mean()?;
5624 (latest - mean).to_f64()
5625 }
5626
5627 pub fn latest_to_mean_ratio(&self) -> Option<f64> {
5629 use rust_decimal::prelude::ToPrimitive;
5630 let latest = self.latest()?;
5631 let mean = self.mean()?;
5632 if mean.is_zero() {
5633 return None;
5634 }
5635 (latest / mean).to_f64()
5636 }
5637
5638 pub fn below_mean_fraction(&self) -> Option<f64> {
5642 if self.window.is_empty() {
5643 return None;
5644 }
5645 let mean = self.mean()?;
5646 let count = self.window.iter().filter(|&&v| v < mean).count();
5647 Some(count as f64 / self.window.len() as f64)
5648 }
5649
5650 pub fn tail_variance(&self) -> Option<f64> {
5653 use rust_decimal::prelude::ToPrimitive;
5654 if self.window.len() < 4 {
5655 return None;
5656 }
5657 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
5658 sorted.sort();
5659 let n = sorted.len();
5660 let q1 = sorted[n / 4];
5661 let q3 = sorted[(3 * n) / 4];
5662 let tails: Vec<f64> = sorted
5663 .iter()
5664 .filter(|&&v| v < q1 || v > q3)
5665 .filter_map(|v| v.to_f64())
5666 .collect();
5667 if tails.len() < 2 {
5668 return None;
5669 }
5670 let nt = tails.len() as f64;
5671 let mean = tails.iter().sum::<f64>() / nt;
5672 let var = tails.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (nt - 1.0);
5673 Some(var)
5674 }
5675
5676 pub fn new_max_count(&self) -> usize {
5680 if self.window.is_empty() {
5681 return 0;
5682 }
5683 let vals: Vec<Decimal> = self.window.iter().copied().collect();
5684 let mut running = vals[0];
5685 let mut count = 1usize;
5686 for &v in vals.iter().skip(1) {
5687 if v > running {
5688 running = v;
5689 count += 1;
5690 }
5691 }
5692 count
5693 }
5694
5695 pub fn new_min_count(&self) -> usize {
5697 if self.window.is_empty() {
5698 return 0;
5699 }
5700 let vals: Vec<Decimal> = self.window.iter().copied().collect();
5701 let mut running = vals[0];
5702 let mut count = 1usize;
5703 for &v in vals.iter().skip(1) {
5704 if v < running {
5705 running = v;
5706 count += 1;
5707 }
5708 }
5709 count
5710 }
5711
5712 pub fn zero_fraction(&self) -> Option<f64> {
5714 if self.window.is_empty() {
5715 return None;
5716 }
5717 let count = self.window.iter().filter(|&&v| v == Decimal::ZERO).count();
5718 Some(count as f64 / self.window.len() as f64)
5719 }
5720
5721 pub fn cumulative_sum(&self) -> Decimal {
5725 self.window.iter().copied().sum()
5726 }
5727
5728 pub fn max_to_min_ratio(&self) -> Option<f64> {
5731 use rust_decimal::prelude::ToPrimitive;
5732 let max = self.window.iter().copied().max()?;
5733 let min = self.window.iter().copied().min()?;
5734 if min.is_zero() {
5735 return None;
5736 }
5737 (max / min).to_f64()
5738 }
5739
5740 pub fn above_midpoint_fraction(&self) -> Option<f64> {
5746 if self.window.is_empty() {
5747 return None;
5748 }
5749 let min = self.window.iter().copied().min()?;
5750 let max = self.window.iter().copied().max()?;
5751 let mid = (min + max) / Decimal::TWO;
5752 let count = self.window.iter().filter(|&&v| v > mid).count();
5753 Some(count as f64 / self.window.len() as f64)
5754 }
5755
5756 pub fn positive_fraction(&self) -> Option<f64> {
5760 if self.window.is_empty() {
5761 return None;
5762 }
5763 let count = self.window.iter().filter(|&&v| v > Decimal::ZERO).count();
5764 Some(count as f64 / self.window.len() as f64)
5765 }
5766
5767 pub fn above_mean_fraction(&self) -> Option<f64> {
5771 use rust_decimal::prelude::ToPrimitive;
5772 if self.window.is_empty() {
5773 return None;
5774 }
5775 let n = self.window.len() as u32;
5776 let mean = self.window.iter().copied().sum::<Decimal>() / Decimal::from(n);
5777 let count = self.window.iter().filter(|&&v| v > mean).count();
5778 Some(count as f64 / self.window.len() as f64)
5779 }
5780
5781 pub fn window_iqr(&self) -> Option<Decimal> {
5787 if self.window.is_empty() {
5788 return None;
5789 }
5790 let mut sorted: Vec<Decimal> = self.window.iter().copied().collect();
5791 sorted.sort();
5792 let n = sorted.len();
5793 let q1 = sorted[n / 4];
5794 let q3 = sorted[(3 * n) / 4];
5795 Some(q3 - q1)
5796 }
5797
5798 pub fn run_length_mean(&self) -> Option<f64> {
5802 if self.window.len() < 2 {
5803 return None;
5804 }
5805 let vals: Vec<Decimal> = self.window.iter().copied().collect();
5806 let mut runs: Vec<usize> = Vec::new();
5807 let mut run_len = 1usize;
5808 for w in vals.windows(2) {
5809 if w[1] >= w[0] {
5810 run_len += 1;
5811 } else {
5812 runs.push(run_len);
5813 run_len = 1;
5814 }
5815 }
5816 runs.push(run_len);
5817 Some(runs.iter().sum::<usize>() as f64 / runs.len() as f64)
5818 }
5819
5820}
5821
5822#[cfg(test)]
5823mod zscore_tests {
5824 use super::*;
5825 use rust_decimal_macros::dec;
5826
5827 fn znorm(w: usize) -> ZScoreNormalizer {
5828 ZScoreNormalizer::new(w).unwrap()
5829 }
5830
5831 #[test]
5832 fn test_zscore_new_zero_window_returns_error() {
5833 assert!(matches!(
5834 ZScoreNormalizer::new(0),
5835 Err(StreamError::ConfigError { .. })
5836 ));
5837 }
5838
5839 #[test]
5840 fn test_zscore_is_full_false_before_capacity() {
5841 let mut n = znorm(3);
5842 assert!(!n.is_full());
5843 n.update(dec!(1));
5844 n.update(dec!(2));
5845 assert!(!n.is_full());
5846 n.update(dec!(3));
5847 assert!(n.is_full());
5848 }
5849
5850 #[test]
5851 fn test_zscore_is_full_stays_true_after_eviction() {
5852 let mut n = znorm(3);
5853 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
5854 n.update(v);
5855 }
5856 assert!(n.is_full());
5857 }
5858
5859 #[test]
5860 fn test_zscore_empty_window_returns_error() {
5861 let n = znorm(4);
5862 assert!(matches!(
5863 n.normalize(dec!(1)),
5864 Err(StreamError::NormalizationError { .. })
5865 ));
5866 }
5867
5868 #[test]
5869 fn test_zscore_single_value_returns_zero() {
5870 let mut n = znorm(4);
5871 n.update(dec!(50));
5872 assert_eq!(n.normalize(dec!(50)).unwrap(), 0.0);
5873 }
5874
5875 #[test]
5876 fn test_zscore_mean_is_zero() {
5877 let mut n = znorm(5);
5878 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] {
5879 n.update(v);
5880 }
5881 let z = n.normalize(dec!(30)).unwrap();
5883 assert!((z - 0.0).abs() < 1e-9, "z-score of mean should be 0, got {z}");
5884 }
5885
5886 #[test]
5887 fn test_zscore_symmetric_around_mean() {
5888 let mut n = znorm(4);
5889 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
5890 n.update(v);
5891 }
5892 let z_low = n.normalize(dec!(15)).unwrap();
5894 let z_high = n.normalize(dec!(35)).unwrap();
5895 assert!((z_low.abs() - z_high.abs()).abs() < 1e-9);
5896 assert!(z_low < 0.0, "below-mean z-score should be negative");
5897 assert!(z_high > 0.0, "above-mean z-score should be positive");
5898 }
5899
5900 #[test]
5901 fn test_zscore_all_same_returns_zero() {
5902 let mut n = znorm(4);
5903 for _ in 0..4 {
5904 n.update(dec!(100));
5905 }
5906 assert_eq!(n.normalize(dec!(100)).unwrap(), 0.0);
5907 }
5908
5909 #[test]
5910 fn test_zscore_rolling_window_eviction() {
5911 let mut n = znorm(3);
5912 n.update(dec!(1));
5913 n.update(dec!(2));
5914 n.update(dec!(3));
5915 n.update(dec!(100));
5917 let z = n.normalize(dec!(100)).unwrap();
5919 assert!(z > 0.0);
5920 }
5921
5922 #[test]
5923 fn test_zscore_reset_clears_state() {
5924 let mut n = znorm(4);
5925 for v in [dec!(10), dec!(20), dec!(30)] {
5926 n.update(v);
5927 }
5928 n.reset();
5929 assert!(n.is_empty());
5930 assert!(n.mean().is_none());
5931 assert!(matches!(
5932 n.normalize(dec!(1)),
5933 Err(StreamError::NormalizationError { .. })
5934 ));
5935 }
5936
5937 #[test]
5938 fn test_zscore_len_and_window_size() {
5939 let mut n = znorm(5);
5940 assert_eq!(n.len(), 0);
5941 assert!(n.is_empty());
5942 n.update(dec!(1));
5943 n.update(dec!(2));
5944 assert_eq!(n.len(), 2);
5945 assert_eq!(n.window_size(), 5);
5946 }
5947
5948 #[test]
5951 fn test_std_dev_none_when_empty() {
5952 let n = znorm(5);
5953 assert!(n.std_dev().is_none());
5954 }
5955
5956 #[test]
5957 fn test_std_dev_zero_with_one_observation() {
5958 let mut n = znorm(5);
5959 n.update(dec!(42));
5960 assert_eq!(n.std_dev(), Some(0.0));
5961 }
5962
5963 #[test]
5964 fn test_std_dev_zero_when_all_same() {
5965 let mut n = znorm(4);
5966 for _ in 0..4 {
5967 n.update(dec!(10));
5968 }
5969 let sd = n.std_dev().unwrap();
5970 assert!(sd < f64::EPSILON);
5971 }
5972
5973 #[test]
5974 fn test_std_dev_positive_for_varying_values() {
5975 let mut n = znorm(4);
5976 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
5977 n.update(v);
5978 }
5979 let sd = n.std_dev().unwrap();
5980 assert!((sd - 11.18).abs() < 0.01);
5982 }
5983
5984 #[test]
5987 fn test_variance_none_when_fewer_than_two_observations() {
5988 let mut n = znorm(5);
5989 assert!(n.variance().is_none());
5990 n.update(dec!(10));
5991 assert!(n.variance().is_none());
5992 }
5993
5994 #[test]
5995 fn test_variance_zero_for_identical_values() {
5996 let mut n = znorm(4);
5997 for _ in 0..4 {
5998 n.update(dec!(7));
5999 }
6000 assert_eq!(n.variance().unwrap(), dec!(0));
6001 }
6002
6003 #[test]
6004 fn test_variance_correct_for_known_values() {
6005 let mut n = znorm(4);
6006 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] {
6007 n.update(v);
6008 }
6009 let var = n.variance().unwrap();
6011 let var_f64 = f64::try_from(var).unwrap();
6012 assert!((var_f64 - 125.0).abs() < 0.01, "expected 125 got {var_f64}");
6013 }
6014
6015 #[test]
6018 fn test_normalize_batch_same_length_as_input() {
6019 let mut n = znorm(5);
6020 let vals = [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)];
6021 let out = n.normalize_batch(&vals).unwrap();
6022 assert_eq!(out.len(), vals.len());
6023 }
6024
6025 #[test]
6026 fn test_normalize_batch_last_value_matches_single_normalize() {
6027 let mut n1 = znorm(5);
6028 let vals = [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)];
6029 let batch = n1.normalize_batch(&vals).unwrap();
6030
6031 let mut n2 = znorm(5);
6032 for &v in &vals {
6033 n2.update(v);
6034 }
6035 let single = n2.normalize(dec!(50)).unwrap();
6036 assert!((batch[4] - single).abs() < 1e-9);
6037 }
6038
6039 #[test]
6040 fn test_sum_empty_returns_none() {
6041 let n = znorm(4);
6042 assert!(n.sum().is_none());
6043 }
6044
6045 #[test]
6046 fn test_sum_matches_manual() {
6047 let mut n = znorm(4);
6048 n.update(dec!(10));
6049 n.update(dec!(20));
6050 n.update(dec!(30));
6051 assert_eq!(n.sum().unwrap(), dec!(60));
6053 }
6054
6055 #[test]
6056 fn test_sum_evicts_old_values() {
6057 let mut n = znorm(2);
6058 n.update(dec!(10));
6059 n.update(dec!(20));
6060 n.update(dec!(30)); assert_eq!(n.sum().unwrap(), dec!(50));
6063 }
6064
6065 #[test]
6066 fn test_std_dev_single_observation_returns_some_zero() {
6067 let mut n = znorm(5);
6068 n.update(dec!(10));
6069 assert!(n.std_dev().is_none() || n.std_dev().unwrap() == 0.0);
6072 }
6073
6074 #[test]
6075 fn test_std_dev_constant_window_is_zero() {
6076 let mut n = znorm(4);
6077 for _ in 0..4 {
6078 n.update(dec!(5));
6079 }
6080 let sd = n.std_dev().unwrap();
6081 assert!(sd.abs() < 1e-9, "expected 0.0 got {sd}");
6082 }
6083
6084 #[test]
6085 fn test_std_dev_known_population() {
6086 let mut n = znorm(8);
6088 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
6089 n.update(v);
6090 }
6091 let sd = n.std_dev().unwrap();
6092 assert!((sd - 2.0).abs() < 1e-6, "expected ~2.0 got {sd}");
6093 }
6094
6095 #[test]
6098 fn test_window_range_none_when_empty() {
6099 let n = znorm(5);
6100 assert!(n.window_range().is_none());
6101 }
6102
6103 #[test]
6104 fn test_window_range_correct_value() {
6105 let mut n = znorm(5);
6106 n.update(dec!(10));
6107 n.update(dec!(20));
6108 n.update(dec!(15));
6109 assert_eq!(n.window_range().unwrap(), dec!(10));
6111 }
6112
6113 #[test]
6114 fn test_coefficient_of_variation_none_when_empty() {
6115 let n = znorm(5);
6116 assert!(n.coefficient_of_variation().is_none());
6117 }
6118
6119 #[test]
6120 fn test_coefficient_of_variation_none_when_mean_zero() {
6121 let mut n = znorm(5);
6122 n.update(dec!(-5));
6123 n.update(dec!(5)); assert!(n.coefficient_of_variation().is_none());
6125 }
6126
6127 #[test]
6128 fn test_coefficient_of_variation_positive_for_nonzero_mean() {
6129 let mut n = znorm(8);
6130 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
6131 n.update(v);
6132 }
6133 let cv = n.coefficient_of_variation().unwrap();
6135 assert!((cv - 0.4).abs() < 1e-5, "expected ~0.4 got {cv}");
6136 }
6137
6138 #[test]
6141 fn test_sample_variance_none_when_empty() {
6142 let n = znorm(5);
6143 assert!(n.sample_variance().is_none());
6144 }
6145
6146 #[test]
6147 fn test_sample_variance_zero_for_constant_window() {
6148 let mut n = znorm(3);
6149 n.update(dec!(7));
6150 n.update(dec!(7));
6151 n.update(dec!(7));
6152 assert!(n.sample_variance().unwrap().abs() < 1e-10);
6153 }
6154
6155 #[test]
6156 fn test_sample_variance_equals_std_dev_squared() {
6157 let mut n = znorm(8);
6158 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
6159 n.update(v);
6160 }
6161 let variance = n.sample_variance().unwrap();
6163 let sd = n.std_dev().unwrap();
6164 assert!((variance - sd * sd).abs() < 1e-10);
6165 }
6166
6167 #[test]
6170 fn test_window_mean_f64_none_when_empty() {
6171 let n = znorm(5);
6172 assert!(n.window_mean_f64().is_none());
6173 }
6174
6175 #[test]
6176 fn test_window_mean_f64_correct_value() {
6177 let mut n = znorm(4);
6178 n.update(dec!(10));
6179 n.update(dec!(20));
6180 let m = n.window_mean_f64().unwrap();
6182 assert!((m - 15.0).abs() < 1e-10);
6183 }
6184
6185 #[test]
6186 fn test_window_mean_f64_matches_decimal_mean() {
6187 let mut n = znorm(8);
6188 for v in [dec!(2), dec!(4), dec!(4), dec!(4), dec!(5), dec!(5), dec!(7), dec!(9)] {
6189 n.update(v);
6190 }
6191 use rust_decimal::prelude::ToPrimitive;
6192 let expected = n.mean().unwrap().to_f64().unwrap();
6193 assert!((n.window_mean_f64().unwrap() - expected).abs() < 1e-10);
6194 }
6195
6196 #[test]
6199 fn test_kurtosis_none_when_fewer_than_4_observations() {
6200 let mut n = znorm(5);
6201 n.update(dec!(1));
6202 n.update(dec!(2));
6203 n.update(dec!(3));
6204 assert!(n.kurtosis().is_none());
6205 }
6206
6207 #[test]
6208 fn test_kurtosis_returns_some_with_4_observations() {
6209 let mut n = znorm(4);
6210 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6211 n.update(v);
6212 }
6213 assert!(n.kurtosis().is_some());
6214 }
6215
6216 #[test]
6217 fn test_kurtosis_none_when_all_same_value() {
6218 let mut n = znorm(4);
6219 for _ in 0..4 {
6220 n.update(dec!(5));
6221 }
6222 assert!(n.kurtosis().is_none());
6224 }
6225
6226 #[test]
6227 fn test_kurtosis_uniform_distribution_is_negative() {
6228 let mut n = znorm(10);
6230 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5),
6231 dec!(6), dec!(7), dec!(8), dec!(9), dec!(10)] {
6232 n.update(v);
6233 }
6234 let k = n.kurtosis().unwrap();
6235 assert!(k < 0.0, "expected negative excess kurtosis for uniform dist, got {k}");
6237 }
6238
6239 #[test]
6241 fn test_is_near_mean_false_with_fewer_than_two_obs() {
6242 let mut n = znorm(5);
6243 n.update(dec!(10));
6244 assert!(!n.is_near_mean(dec!(10), 1.0));
6245 }
6246
6247 #[test]
6248 fn test_is_near_mean_true_within_one_sigma() {
6249 let mut n = znorm(10);
6250 for _ in 0..9 {
6252 n.update(dec!(10));
6253 }
6254 n.update(dec!(20));
6255 assert!(n.is_near_mean(dec!(11), 1.0));
6257 }
6258
6259 #[test]
6260 fn test_is_near_mean_false_when_far_from_mean() {
6261 let mut n = znorm(5);
6262 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] {
6263 n.update(v);
6264 }
6265 assert!(!n.is_near_mean(dec!(100), 2.0));
6267 }
6268
6269 #[test]
6270 fn test_is_near_mean_true_when_all_identical_any_value() {
6271 let mut n = znorm(4);
6272 for _ in 0..4 {
6273 n.update(dec!(7));
6274 }
6275 assert!(n.is_near_mean(dec!(999), 0.0));
6277 }
6278
6279 #[test]
6281 fn test_window_sum_f64_zero_on_empty() {
6282 let n = znorm(5);
6283 assert_eq!(n.window_sum_f64(), 0.0);
6284 }
6285
6286 #[test]
6287 fn test_window_sum_f64_correct_after_updates() {
6288 let mut n = znorm(5);
6289 n.update(dec!(10));
6290 n.update(dec!(20));
6291 n.update(dec!(30));
6292 assert!((n.window_sum_f64() - 60.0).abs() < 1e-10);
6293 }
6294
6295 #[test]
6296 fn test_window_sum_f64_rolls_out_old_values() {
6297 let mut n = znorm(2);
6298 n.update(dec!(100));
6299 n.update(dec!(200));
6300 n.update(dec!(300)); assert!((n.window_sum_f64() - 500.0).abs() < 1e-10);
6303 }
6304
6305 #[test]
6308 fn test_zscore_latest_none_when_empty() {
6309 let n = znorm(5);
6310 assert!(n.latest().is_none());
6311 }
6312
6313 #[test]
6314 fn test_zscore_latest_returns_most_recent() {
6315 let mut n = znorm(5);
6316 n.update(dec!(10));
6317 n.update(dec!(20));
6318 assert_eq!(n.latest(), Some(dec!(20)));
6319 }
6320
6321 #[test]
6322 fn test_zscore_latest_updates_on_roll() {
6323 let mut n = znorm(2);
6324 n.update(dec!(1));
6325 n.update(dec!(2));
6326 n.update(dec!(3)); assert_eq!(n.latest(), Some(dec!(3)));
6328 }
6329
6330 #[test]
6332 fn test_window_max_f64_none_on_empty() {
6333 let n = znorm(5);
6334 assert!(n.window_max_f64().is_none());
6335 }
6336
6337 #[test]
6338 fn test_window_max_f64_correct_value() {
6339 let mut n = znorm(5);
6340 for v in [dec!(3), dec!(7), dec!(1), dec!(5)] {
6341 n.update(v);
6342 }
6343 assert!((n.window_max_f64().unwrap() - 7.0).abs() < 1e-10);
6344 }
6345
6346 #[test]
6347 fn test_window_min_f64_none_on_empty() {
6348 let n = znorm(5);
6349 assert!(n.window_min_f64().is_none());
6350 }
6351
6352 #[test]
6353 fn test_window_min_f64_correct_value() {
6354 let mut n = znorm(5);
6355 for v in [dec!(3), dec!(7), dec!(1), dec!(5)] {
6356 n.update(v);
6357 }
6358 assert!((n.window_min_f64().unwrap() - 1.0).abs() < 1e-10);
6359 }
6360
6361 #[test]
6364 fn test_percentile_none_when_empty() {
6365 let n = znorm(5);
6366 assert!(n.percentile(dec!(10)).is_none());
6367 }
6368
6369 #[test]
6370 fn test_percentile_one_when_all_lte_value() {
6371 let mut n = znorm(4);
6372 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6373 n.update(v);
6374 }
6375 assert!((n.percentile(dec!(4)).unwrap() - 1.0).abs() < 1e-9);
6376 }
6377
6378 #[test]
6379 fn test_percentile_zero_when_all_gt_value() {
6380 let mut n = znorm(4);
6381 for v in [dec!(5), dec!(6), dec!(7), dec!(8)] {
6382 n.update(v);
6383 }
6384 assert_eq!(n.percentile(dec!(4)).unwrap(), 0.0);
6386 }
6387
6388 #[test]
6389 fn test_percentile_half_at_median() {
6390 let mut n = znorm(4);
6391 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6392 n.update(v);
6393 }
6394 assert!((n.percentile(dec!(2)).unwrap() - 0.5).abs() < 1e-9);
6396 }
6397
6398 #[test]
6401 fn test_zscore_iqr_none_fewer_than_4_observations() {
6402 let mut n = znorm(5);
6403 for v in [dec!(1), dec!(2), dec!(3)] {
6404 n.update(v);
6405 }
6406 assert!(n.interquartile_range().is_none());
6407 }
6408
6409 #[test]
6410 fn test_zscore_iqr_some_with_4_observations() {
6411 let mut n = znorm(4);
6412 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6413 n.update(v);
6414 }
6415 assert!(n.interquartile_range().is_some());
6416 }
6417
6418 #[test]
6419 fn test_zscore_iqr_zero_when_all_same() {
6420 let mut n = znorm(4);
6421 for _ in 0..4 {
6422 n.update(dec!(5));
6423 }
6424 assert_eq!(n.interquartile_range(), Some(dec!(0)));
6425 }
6426
6427 #[test]
6428 fn test_zscore_iqr_correct_for_sorted_data() {
6429 let mut n = znorm(8);
6431 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6), dec!(7), dec!(8)] {
6432 n.update(v);
6433 }
6434 assert_eq!(n.interquartile_range(), Some(dec!(4)));
6435 }
6436
6437 #[test]
6440 fn test_z_score_of_latest_none_when_empty() {
6441 let n = znorm(5);
6442 assert!(n.z_score_of_latest().is_none());
6443 }
6444
6445 #[test]
6446 fn test_z_score_of_latest_zero_when_all_same() {
6447 let mut n = znorm(4);
6448 for _ in 0..4 {
6449 n.update(dec!(5));
6450 }
6451 assert_eq!(n.z_score_of_latest(), Some(0.0));
6453 }
6454
6455 #[test]
6456 fn test_z_score_of_latest_returns_some_with_variance() {
6457 let mut n = znorm(4);
6458 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6459 n.update(v);
6460 }
6461 assert!(n.z_score_of_latest().is_some());
6463 }
6464
6465 #[test]
6466 fn test_deviation_from_mean_none_when_empty() {
6467 let n = znorm(5);
6468 assert!(n.deviation_from_mean(dec!(10)).is_none());
6469 }
6470
6471 #[test]
6472 fn test_deviation_from_mean_correct() {
6473 let mut n = znorm(4);
6474 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6475 n.update(v);
6476 }
6477 let d = n.deviation_from_mean(dec!(4)).unwrap();
6479 assert!((d - 1.5).abs() < 1e-9);
6480 }
6481
6482 #[test]
6485 fn test_add_observation_same_as_update() {
6486 let mut n1 = znorm(4);
6487 let mut n2 = znorm(4);
6488 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6489 n1.update(v);
6490 n2.add_observation(v);
6491 }
6492 assert_eq!(n1.mean(), n2.mean());
6493 }
6494
6495 #[test]
6496 fn test_add_observation_chainable() {
6497 let mut n = znorm(4);
6498 n.add_observation(dec!(1))
6499 .add_observation(dec!(2))
6500 .add_observation(dec!(3));
6501 assert_eq!(n.len(), 3);
6502 }
6503
6504 #[test]
6507 fn test_variance_f64_none_when_single_observation() {
6508 let mut n = znorm(4);
6509 n.update(dec!(5));
6510 assert!(n.variance_f64().is_none());
6511 }
6512
6513 #[test]
6514 fn test_variance_f64_zero_when_all_same() {
6515 let mut n = znorm(4);
6516 for _ in 0..4 { n.update(dec!(5)); }
6517 assert_eq!(n.variance_f64(), Some(0.0));
6518 }
6519
6520 #[test]
6521 fn test_variance_f64_positive_with_spread() {
6522 let mut n = znorm(4);
6523 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6524 assert!(n.variance_f64().unwrap() > 0.0);
6525 }
6526
6527 #[test]
6530 fn test_ema_of_z_scores_none_when_single_value() {
6531 let mut n = znorm(4);
6532 n.update(dec!(5));
6533 assert!(n.ema_of_z_scores(0.5).is_none());
6534 }
6535
6536 #[test]
6537 fn test_ema_of_z_scores_returns_some_with_variance() {
6538 let mut n = znorm(4);
6539 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] {
6540 n.update(v);
6541 }
6542 let ema = n.ema_of_z_scores(0.3);
6543 assert!(ema.is_some());
6544 }
6545
6546 #[test]
6547 fn test_ema_of_z_scores_zero_when_all_same() {
6548 let mut n = znorm(4);
6549 for _ in 0..4 { n.update(dec!(5)); }
6550 assert_eq!(n.ema_of_z_scores(0.5), Some(0.0));
6552 }
6553
6554 #[test]
6557 fn test_std_dev_f64_none_when_single_observation() {
6558 let mut n = znorm(4);
6559 n.update(dec!(5));
6560 assert!(n.std_dev_f64().is_none());
6561 }
6562
6563 #[test]
6564 fn test_std_dev_f64_zero_when_all_same() {
6565 let mut n = znorm(4);
6566 for _ in 0..4 { n.update(dec!(5)); }
6567 assert_eq!(n.std_dev_f64(), Some(0.0));
6568 }
6569
6570 #[test]
6571 fn test_std_dev_f64_equals_sqrt_of_variance() {
6572 let mut n = znorm(4);
6573 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6574 let var = n.variance_f64().unwrap();
6575 let std = n.std_dev_f64().unwrap();
6576 assert!((std - var.sqrt()).abs() < 1e-12);
6577 }
6578
6579 #[test]
6582 fn test_rolling_mean_change_none_when_one_observation() {
6583 let mut n = znorm(4);
6584 n.update(dec!(5));
6585 assert!(n.rolling_mean_change().is_none());
6586 }
6587
6588 #[test]
6589 fn test_rolling_mean_change_positive_when_rising() {
6590 let mut n = znorm(4);
6591 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6592 let change = n.rolling_mean_change().unwrap();
6594 assert!((change - 2.0).abs() < 1e-9);
6595 }
6596
6597 #[test]
6598 fn test_rolling_mean_change_negative_when_falling() {
6599 let mut n = znorm(4);
6600 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
6601 let change = n.rolling_mean_change().unwrap();
6602 assert!(change < 0.0);
6603 }
6604
6605 #[test]
6606 fn test_rolling_mean_change_zero_when_flat() {
6607 let mut n = znorm(4);
6608 for _ in 0..4 { n.update(dec!(7)); }
6609 let change = n.rolling_mean_change().unwrap();
6610 assert!(change.abs() < 1e-9);
6611 }
6612
6613 #[test]
6616 fn test_window_span_f64_none_when_empty() {
6617 let n = znorm(4);
6618 assert!(n.window_span_f64().is_none());
6619 }
6620
6621 #[test]
6622 fn test_window_span_f64_zero_when_all_same() {
6623 let mut n = znorm(4);
6624 for _ in 0..4 { n.update(dec!(5)); }
6625 assert_eq!(n.window_span_f64(), Some(0.0));
6626 }
6627
6628 #[test]
6629 fn test_window_span_f64_correct_value() {
6630 let mut n = znorm(4);
6631 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
6632 assert!((n.window_span_f64().unwrap() - 30.0).abs() < 1e-9);
6634 }
6635
6636 #[test]
6639 fn test_count_positive_z_scores_zero_when_empty() {
6640 let n = znorm(4);
6641 assert_eq!(n.count_positive_z_scores(), 0);
6642 }
6643
6644 #[test]
6645 fn test_count_positive_z_scores_zero_when_all_same() {
6646 let mut n = znorm(4);
6647 for _ in 0..4 { n.update(dec!(5)); }
6648 assert_eq!(n.count_positive_z_scores(), 0);
6649 }
6650
6651 #[test]
6652 fn test_count_positive_z_scores_half_above_mean() {
6653 let mut n = znorm(4);
6654 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6655 assert_eq!(n.count_positive_z_scores(), 2);
6657 }
6658
6659 #[test]
6662 fn test_above_threshold_count_zero_when_empty() {
6663 let n = znorm(4);
6664 assert_eq!(n.above_threshold_count(1.0), 0);
6665 }
6666
6667 #[test]
6668 fn test_above_threshold_count_zero_when_all_same() {
6669 let mut n = znorm(4);
6670 for _ in 0..4 { n.update(dec!(5)); }
6671 assert_eq!(n.above_threshold_count(0.5), 0);
6672 }
6673
6674 #[test]
6675 fn test_above_threshold_count_correct_with_extremes() {
6676 let mut n = znorm(6);
6677 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(100)] { n.update(v); }
6678 assert!(n.above_threshold_count(1.0) >= 1);
6680 }
6681}
6682
6683#[cfg(test)]
6684mod minmax_extra_tests {
6685 use super::*;
6686 use rust_decimal_macros::dec;
6687
6688 fn norm(w: usize) -> MinMaxNormalizer {
6689 MinMaxNormalizer::new(w).unwrap()
6690 }
6691
6692 #[test]
6695 fn test_fraction_above_mid_none_when_empty() {
6696 let mut n = norm(4);
6697 assert!(n.fraction_above_mid().is_none());
6698 }
6699
6700 #[test]
6701 fn test_fraction_above_mid_zero_when_all_same() {
6702 let mut n = norm(4);
6703 for _ in 0..4 { n.update(dec!(5)); }
6704 assert_eq!(n.fraction_above_mid(), Some(0.0));
6705 }
6706
6707 #[test]
6708 fn test_fraction_above_mid_half_when_symmetric() {
6709 let mut n = norm(4);
6710 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
6711 let f = n.fraction_above_mid().unwrap();
6713 assert!((f - 0.5).abs() < 1e-10);
6714 }
6715}
6716
6717#[cfg(test)]
6718mod zscore_stability_tests {
6719 use super::*;
6720 use rust_decimal_macros::dec;
6721
6722 fn znorm(w: usize) -> ZScoreNormalizer {
6723 ZScoreNormalizer::new(w).unwrap()
6724 }
6725
6726 #[test]
6729 fn test_is_mean_stable_false_when_window_too_small() {
6730 let n = znorm(4);
6731 assert!(!n.is_mean_stable(1.0));
6732 }
6733
6734 #[test]
6735 fn test_is_mean_stable_true_when_flat() {
6736 let mut n = znorm(4);
6737 for _ in 0..4 { n.update(dec!(5)); }
6738 assert!(n.is_mean_stable(0.001));
6739 }
6740
6741 #[test]
6742 fn test_is_mean_stable_false_when_trending() {
6743 let mut n = znorm(4);
6744 for v in [dec!(1), dec!(2), dec!(10), dec!(20)] { n.update(v); }
6745 assert!(!n.is_mean_stable(0.5));
6746 }
6747
6748 #[test]
6751 fn test_zscore_count_above_zero_for_empty_window() {
6752 assert_eq!(znorm(4).count_above(dec!(10)), 0);
6753 }
6754
6755 #[test]
6756 fn test_zscore_count_above_correct() {
6757 let mut n = znorm(5);
6758 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6759 assert_eq!(n.count_above(dec!(3)), 2);
6761 }
6762
6763 #[test]
6764 fn test_zscore_count_below_correct() {
6765 let mut n = znorm(5);
6766 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6767 assert_eq!(n.count_below(dec!(3)), 2);
6769 }
6770
6771 #[test]
6772 fn test_zscore_count_above_excludes_at_threshold() {
6773 let mut n = znorm(3);
6774 for v in [dec!(5), dec!(5), dec!(5)] { n.update(v); }
6775 assert_eq!(n.count_above(dec!(5)), 0);
6776 assert_eq!(n.count_below(dec!(5)), 0);
6777 }
6778
6779 #[test]
6782 fn test_zscore_skewness_none_for_fewer_than_3_obs() {
6783 let mut n = znorm(5);
6784 n.update(dec!(10));
6785 n.update(dec!(20));
6786 assert!(n.skewness().is_none());
6787 }
6788
6789 #[test]
6790 fn test_zscore_skewness_none_for_all_identical() {
6791 let mut n = znorm(4);
6792 for _ in 0..4 { n.update(dec!(5)); }
6793 assert!(n.skewness().is_none());
6794 }
6795
6796 #[test]
6797 fn test_zscore_skewness_near_zero_for_symmetric_distribution() {
6798 let mut n = znorm(5);
6799 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
6800 let skew = n.skewness().unwrap();
6801 assert!(skew.abs() < 0.01, "symmetric distribution should have ~0 skewness, got {skew}");
6802 }
6803
6804 #[test]
6807 fn test_zscore_percentile_value_none_for_empty_window() {
6808 assert!(znorm(4).percentile_value(0.5).is_none());
6809 }
6810
6811 #[test]
6812 fn test_zscore_percentile_value_min_at_zero() {
6813 let mut n = znorm(5);
6814 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
6815 assert_eq!(n.percentile_value(0.0), Some(dec!(10)));
6816 }
6817
6818 #[test]
6819 fn test_zscore_percentile_value_max_at_one() {
6820 let mut n = znorm(5);
6821 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
6822 assert_eq!(n.percentile_value(1.0), Some(dec!(50)));
6823 }
6824
6825 #[test]
6828 fn test_zscore_ewma_none_for_empty_window() {
6829 assert!(znorm(4).ewma(0.5).is_none());
6830 }
6831
6832 #[test]
6833 fn test_zscore_ewma_equals_value_for_single_obs() {
6834 let mut n = znorm(4);
6835 n.update(dec!(42));
6836 assert!((n.ewma(0.5).unwrap() - 42.0).abs() < 1e-10);
6837 }
6838
6839 #[test]
6840 fn test_zscore_ewma_weights_recent_more_with_high_alpha() {
6841 let mut n = znorm(4);
6843 for v in [dec!(10), dec!(20), dec!(30), dec!(100)] { n.update(v); }
6844 let ewma = n.ewma(1.0).unwrap();
6845 assert!((ewma - 100.0).abs() < 1e-10);
6846 }
6847
6848 #[test]
6849 fn test_zscore_fraction_above_mid_none_for_empty_window() {
6850 let n = znorm(3);
6851 assert!(n.fraction_above_mid().is_none());
6852 }
6853
6854 #[test]
6855 fn test_zscore_fraction_above_mid_none_when_all_equal() {
6856 let mut n = znorm(3);
6857 for _ in 0..3 { n.update(dec!(5)); }
6858 assert!(n.fraction_above_mid().is_none());
6859 }
6860
6861 #[test]
6862 fn test_zscore_fraction_above_mid_half_above() {
6863 let mut n = znorm(4);
6864 for v in [dec!(0), dec!(10), dec!(6), dec!(4)] { n.update(v); }
6865 let frac = n.fraction_above_mid().unwrap();
6867 assert!((frac - 0.5).abs() < 1e-9);
6868 }
6869
6870 #[test]
6871 fn test_zscore_normalized_range_none_for_empty_window() {
6872 let n = znorm(3);
6873 assert!(n.normalized_range().is_none());
6874 }
6875
6876 #[test]
6877 fn test_zscore_normalized_range_zero_for_uniform_window() {
6878 let mut n = znorm(3);
6879 for _ in 0..3 { n.update(dec!(10)); }
6880 assert_eq!(n.normalized_range(), Some(0.0));
6881 }
6882
6883 #[test]
6884 fn test_zscore_normalized_range_positive_for_varying_window() {
6885 let mut n = znorm(3);
6886 for v in [dec!(8), dec!(10), dec!(12)] { n.update(v); }
6887 let nr = n.normalized_range().unwrap();
6889 assert!((nr - 0.4).abs() < 1e-9);
6890 }
6891
6892 #[test]
6895 fn test_zscore_midpoint_none_for_empty_window() {
6896 assert!(znorm(3).midpoint().is_none());
6897 }
6898
6899 #[test]
6900 fn test_zscore_midpoint_correct_for_known_range() {
6901 let mut n = znorm(4);
6902 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
6903 assert_eq!(n.midpoint(), Some(dec!(25)));
6905 }
6906
6907 #[test]
6910 fn test_zscore_clamp_returns_value_unchanged_on_empty_window() {
6911 let n = znorm(3);
6912 assert_eq!(n.clamp_to_window(dec!(50)), dec!(50));
6913 }
6914
6915 #[test]
6916 fn test_zscore_clamp_clamps_to_min() {
6917 let mut n = znorm(3);
6918 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
6919 assert_eq!(n.clamp_to_window(dec!(-5)), dec!(10));
6920 }
6921
6922 #[test]
6923 fn test_zscore_clamp_clamps_to_max() {
6924 let mut n = znorm(3);
6925 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
6926 assert_eq!(n.clamp_to_window(dec!(100)), dec!(30));
6927 }
6928
6929 #[test]
6930 fn test_zscore_clamp_passes_through_in_range_value() {
6931 let mut n = znorm(3);
6932 for v in [dec!(10), dec!(20), dec!(30)] { n.update(v); }
6933 assert_eq!(n.clamp_to_window(dec!(15)), dec!(15));
6934 }
6935
6936 #[test]
6939 fn test_zscore_min_max_none_for_empty_window() {
6940 assert!(znorm(3).min_max().is_none());
6941 }
6942
6943 #[test]
6944 fn test_zscore_min_max_returns_correct_pair() {
6945 let mut n = znorm(4);
6946 for v in [dec!(5), dec!(15), dec!(10), dec!(20)] { n.update(v); }
6947 assert_eq!(n.min_max(), Some((dec!(5), dec!(20))));
6948 }
6949
6950 #[test]
6951 fn test_zscore_min_max_single_value() {
6952 let mut n = znorm(3);
6953 n.update(dec!(42));
6954 assert_eq!(n.min_max(), Some((dec!(42), dec!(42))));
6955 }
6956
6957 #[test]
6960 fn test_zscore_values_empty_for_empty_window() {
6961 assert!(znorm(3).values().is_empty());
6962 }
6963
6964 #[test]
6965 fn test_zscore_values_preserves_insertion_order() {
6966 let mut n = znorm(4);
6967 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
6968 assert_eq!(n.values(), vec![dec!(10), dec!(20), dec!(30), dec!(40)]);
6969 }
6970
6971 #[test]
6974 fn test_zscore_above_zero_fraction_none_for_empty_window() {
6975 assert!(znorm(3).above_zero_fraction().is_none());
6976 }
6977
6978 #[test]
6979 fn test_zscore_above_zero_fraction_zero_for_all_negative() {
6980 let mut n = znorm(3);
6981 for v in [dec!(-3), dec!(-2), dec!(-1)] { n.update(v); }
6982 assert_eq!(n.above_zero_fraction(), Some(0.0));
6983 }
6984
6985 #[test]
6986 fn test_zscore_above_zero_fraction_one_for_all_positive() {
6987 let mut n = znorm(3);
6988 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
6989 assert_eq!(n.above_zero_fraction(), Some(1.0));
6990 }
6991
6992 #[test]
6993 fn test_zscore_above_zero_fraction_half_for_mixed() {
6994 let mut n = znorm(4);
6995 for v in [dec!(-2), dec!(-1), dec!(1), dec!(2)] { n.update(v); }
6996 let frac = n.above_zero_fraction().unwrap();
6997 assert!((frac - 0.5).abs() < 1e-9);
6998 }
6999
7000 #[test]
7003 fn test_zscore_opt_none_for_empty_window() {
7004 assert!(znorm(3).z_score_opt(dec!(10)).is_none());
7005 }
7006
7007 #[test]
7008 fn test_zscore_opt_matches_normalize_for_populated_window() {
7009 let mut n = znorm(4);
7010 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
7011 let z_opt = n.z_score_opt(dec!(25)).unwrap();
7012 let z_norm = n.normalize(dec!(25)).unwrap();
7013 assert!((z_opt - z_norm).abs() < 1e-12);
7014 }
7015
7016 #[test]
7019 fn test_zscore_is_stable_false_for_empty_window() {
7020 assert!(!znorm(3).is_stable(2.0));
7021 }
7022
7023 #[test]
7024 fn test_zscore_is_stable_true_for_near_mean_value() {
7025 let mut n = znorm(5);
7026 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(30)] { n.update(v); }
7027 assert!(n.is_stable(2.0));
7029 }
7030
7031 #[test]
7032 fn test_zscore_is_stable_false_for_extreme_value() {
7033 let mut n = znorm(5);
7034 for v in [dec!(10), dec!(10), dec!(10), dec!(10), dec!(100)] { n.update(v); }
7035 assert!(!n.is_stable(1.0));
7037 }
7038
7039 #[test]
7042 fn test_zscore_window_values_above_via_znorm_empty() {
7043 assert!(znorm(3).window_values_above(dec!(5)).is_empty());
7044 }
7045
7046 #[test]
7047 fn test_zscore_window_values_above_via_znorm_filters() {
7048 let mut n = znorm(5);
7049 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
7050 let above = n.window_values_above(dec!(5));
7051 assert_eq!(above.len(), 2);
7052 assert!(above.contains(&dec!(7)));
7053 assert!(above.contains(&dec!(9)));
7054 }
7055
7056 #[test]
7057 fn test_zscore_window_values_below_via_znorm_empty() {
7058 assert!(znorm(3).window_values_below(dec!(5)).is_empty());
7059 }
7060
7061 #[test]
7062 fn test_zscore_window_values_below_via_znorm_filters() {
7063 let mut n = znorm(5);
7064 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
7065 let below = n.window_values_below(dec!(5));
7066 assert_eq!(below.len(), 2);
7067 assert!(below.contains(&dec!(1)));
7068 assert!(below.contains(&dec!(3)));
7069 }
7070
7071 #[test]
7074 fn test_zscore_fraction_above_none_for_empty_window() {
7075 assert!(znorm(3).fraction_above(dec!(5)).is_none());
7076 }
7077
7078 #[test]
7079 fn test_zscore_fraction_above_correct() {
7080 let mut n = znorm(5);
7081 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7082 let frac = n.fraction_above(dec!(3)).unwrap();
7084 assert!((frac - 0.4).abs() < 1e-9);
7085 }
7086
7087 #[test]
7088 fn test_zscore_fraction_below_none_for_empty_window() {
7089 assert!(znorm(3).fraction_below(dec!(5)).is_none());
7090 }
7091
7092 #[test]
7093 fn test_zscore_fraction_below_correct() {
7094 let mut n = znorm(5);
7095 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7096 let frac = n.fraction_below(dec!(3)).unwrap();
7098 assert!((frac - 0.4).abs() < 1e-9);
7099 }
7100
7101 #[test]
7104 fn test_zscore_window_values_above_empty_window() {
7105 assert!(znorm(3).window_values_above(dec!(0)).is_empty());
7106 }
7107
7108 #[test]
7109 fn test_zscore_window_values_above_filters_correctly() {
7110 let mut n = znorm(5);
7111 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
7112 let above = n.window_values_above(dec!(5));
7113 assert_eq!(above.len(), 2);
7114 assert!(above.contains(&dec!(7)));
7115 assert!(above.contains(&dec!(9)));
7116 }
7117
7118 #[test]
7119 fn test_zscore_window_values_below_empty_window() {
7120 assert!(znorm(3).window_values_below(dec!(0)).is_empty());
7121 }
7122
7123 #[test]
7124 fn test_zscore_window_values_below_filters_correctly() {
7125 let mut n = znorm(5);
7126 for v in [dec!(1), dec!(3), dec!(5), dec!(7), dec!(9)] { n.update(v); }
7127 let below = n.window_values_below(dec!(5));
7128 assert_eq!(below.len(), 2);
7129 assert!(below.contains(&dec!(1)));
7130 assert!(below.contains(&dec!(3)));
7131 }
7132
7133 #[test]
7136 fn test_zscore_percentile_rank_none_for_empty_window() {
7137 assert!(znorm(3).percentile_rank(dec!(5)).is_none());
7138 }
7139
7140 #[test]
7141 fn test_zscore_percentile_rank_correct() {
7142 let mut n = znorm(5);
7143 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7144 let rank = n.percentile_rank(dec!(3)).unwrap();
7146 assert!((rank - 0.6).abs() < 1e-9);
7147 }
7148
7149 #[test]
7152 fn test_zscore_count_equal_zero_for_no_match() {
7153 let mut n = znorm(3);
7154 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7155 assert_eq!(n.count_equal(dec!(99)), 0);
7156 }
7157
7158 #[test]
7159 fn test_zscore_count_equal_counts_duplicates() {
7160 let mut n = znorm(5);
7161 for v in [dec!(5), dec!(5), dec!(3), dec!(5), dec!(2)] { n.update(v); }
7162 assert_eq!(n.count_equal(dec!(5)), 3);
7163 }
7164
7165 #[test]
7168 fn test_zscore_median_none_for_empty_window() {
7169 assert!(znorm(3).median().is_none());
7170 }
7171
7172 #[test]
7173 fn test_zscore_median_correct_for_odd_count() {
7174 let mut n = znorm(5);
7175 for v in [dec!(3), dec!(1), dec!(5), dec!(4), dec!(2)] { n.update(v); }
7176 assert_eq!(n.median(), Some(dec!(3)));
7178 }
7179
7180 #[test]
7183 fn test_zscore_rolling_range_none_for_empty() {
7184 assert!(znorm(3).rolling_range().is_none());
7185 }
7186
7187 #[test]
7188 fn test_zscore_rolling_range_correct() {
7189 let mut n = znorm(5);
7190 for v in [dec!(10), dec!(50), dec!(30), dec!(20), dec!(40)] { n.update(v); }
7191 assert_eq!(n.rolling_range(), Some(dec!(40)));
7192 }
7193
7194 #[test]
7197 fn test_zscore_skewness_none_for_fewer_than_3() {
7198 let mut n = znorm(5);
7199 n.update(dec!(1)); n.update(dec!(2));
7200 assert!(n.skewness().is_none());
7201 }
7202
7203 #[test]
7204 fn test_zscore_skewness_near_zero_for_symmetric_data() {
7205 let mut n = znorm(5);
7206 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7207 let s = n.skewness().unwrap();
7208 assert!(s.abs() < 0.5);
7209 }
7210
7211 #[test]
7214 fn test_zscore_kurtosis_none_for_fewer_than_4() {
7215 let mut n = znorm(5);
7216 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7217 assert!(n.kurtosis().is_none());
7218 }
7219
7220 #[test]
7221 fn test_zscore_kurtosis_returns_f64_for_populated_window() {
7222 let mut n = znorm(5);
7223 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7224 assert!(n.kurtosis().is_some());
7225 }
7226
7227 #[test]
7230 fn test_zscore_autocorrelation_none_for_single_value() {
7231 let mut n = znorm(3);
7232 n.update(dec!(1));
7233 assert!(n.autocorrelation_lag1().is_none());
7234 }
7235
7236 #[test]
7237 fn test_zscore_autocorrelation_positive_for_trending_data() {
7238 let mut n = znorm(5);
7239 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7240 let ac = n.autocorrelation_lag1().unwrap();
7241 assert!(ac > 0.0);
7242 }
7243
7244 #[test]
7247 fn test_zscore_trend_consistency_none_for_single_value() {
7248 let mut n = znorm(3);
7249 n.update(dec!(1));
7250 assert!(n.trend_consistency().is_none());
7251 }
7252
7253 #[test]
7254 fn test_zscore_trend_consistency_one_for_strictly_rising() {
7255 let mut n = znorm(5);
7256 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7257 let tc = n.trend_consistency().unwrap();
7258 assert!((tc - 1.0).abs() < 1e-9);
7259 }
7260
7261 #[test]
7262 fn test_zscore_trend_consistency_zero_for_strictly_falling() {
7263 let mut n = znorm(5);
7264 for v in [dec!(5), dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
7265 let tc = n.trend_consistency().unwrap();
7266 assert!((tc - 0.0).abs() < 1e-9);
7267 }
7268
7269 #[test]
7272 fn test_zscore_cov_none_for_empty_window() {
7273 assert!(znorm(3).coefficient_of_variation().is_none());
7274 }
7275
7276 #[test]
7277 fn test_zscore_cov_positive_for_varied_data() {
7278 let mut n = znorm(5);
7279 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50)] { n.update(v); }
7280 let cov = n.coefficient_of_variation().unwrap();
7281 assert!(cov > 0.0);
7282 }
7283
7284 #[test]
7287 fn test_zscore_mad_none_for_empty() {
7288 assert!(znorm(3).mean_absolute_deviation().is_none());
7289 }
7290
7291 #[test]
7292 fn test_zscore_mad_zero_for_identical_values() {
7293 let mut n = znorm(3);
7294 for v in [dec!(5), dec!(5), dec!(5)] { n.update(v); }
7295 let mad = n.mean_absolute_deviation().unwrap();
7296 assert!((mad - 0.0).abs() < 1e-9);
7297 }
7298
7299 #[test]
7300 fn test_zscore_mad_positive_for_varied_data() {
7301 let mut n = znorm(4);
7302 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7303 let mad = n.mean_absolute_deviation().unwrap();
7304 assert!(mad > 0.0);
7305 }
7306
7307 #[test]
7310 fn test_zscore_percentile_of_latest_none_for_empty() {
7311 assert!(znorm(3).percentile_of_latest().is_none());
7312 }
7313
7314 #[test]
7315 fn test_zscore_percentile_of_latest_returns_some_after_update() {
7316 let mut n = znorm(4);
7317 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7318 assert!(n.percentile_of_latest().is_some());
7319 }
7320
7321 #[test]
7322 fn test_zscore_percentile_of_latest_max_has_high_rank() {
7323 let mut n = znorm(5);
7324 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7325 let rank = n.percentile_of_latest().unwrap();
7326 assert!(rank >= 0.9, "max value should have rank near 1.0, got {}", rank);
7327 }
7328
7329 #[test]
7332 fn test_zscore_tail_ratio_none_for_empty() {
7333 assert!(znorm(4).tail_ratio().is_none());
7334 }
7335
7336 #[test]
7337 fn test_zscore_tail_ratio_one_for_identical_values() {
7338 let mut n = znorm(4);
7339 for _ in 0..4 { n.update(dec!(7)); }
7340 let r = n.tail_ratio().unwrap();
7341 assert!((r - 1.0).abs() < 1e-9, "expected 1.0, got {}", r);
7342 }
7343
7344 #[test]
7345 fn test_zscore_tail_ratio_above_one_with_outlier() {
7346 let mut n = znorm(5);
7347 for v in [dec!(1), dec!(1), dec!(1), dec!(1), dec!(10)] { n.update(v); }
7348 let r = n.tail_ratio().unwrap();
7349 assert!(r > 1.0, "outlier should push ratio above 1.0, got {}", r);
7350 }
7351
7352 #[test]
7355 fn test_zscore_z_score_of_min_none_for_empty() {
7356 assert!(znorm(4).z_score_of_min().is_none());
7357 }
7358
7359 #[test]
7360 fn test_zscore_z_score_of_max_none_for_empty() {
7361 assert!(znorm(4).z_score_of_max().is_none());
7362 }
7363
7364 #[test]
7365 fn test_zscore_z_score_of_min_negative_for_varied_window() {
7366 let mut n = znorm(5);
7367 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7368 let z = n.z_score_of_min().unwrap();
7369 assert!(z < 0.0, "z-score of min should be negative, got {}", z);
7370 }
7371
7372 #[test]
7373 fn test_zscore_z_score_of_max_positive_for_varied_window() {
7374 let mut n = znorm(5);
7375 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7376 let z = n.z_score_of_max().unwrap();
7377 assert!(z > 0.0, "z-score of max should be positive, got {}", z);
7378 }
7379
7380 #[test]
7383 fn test_zscore_window_entropy_none_for_empty() {
7384 assert!(znorm(4).window_entropy().is_none());
7385 }
7386
7387 #[test]
7388 fn test_zscore_window_entropy_zero_for_identical_values() {
7389 let mut n = znorm(3);
7390 for _ in 0..3 { n.update(dec!(5)); }
7391 let e = n.window_entropy().unwrap();
7392 assert!((e - 0.0).abs() < 1e-9, "identical values should have zero entropy, got {}", e);
7393 }
7394
7395 #[test]
7396 fn test_zscore_window_entropy_positive_for_varied_values() {
7397 let mut n = znorm(4);
7398 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7399 let e = n.window_entropy().unwrap();
7400 assert!(e > 0.0, "varied values should have positive entropy, got {}", e);
7401 }
7402
7403 #[test]
7406 fn test_zscore_normalized_std_dev_none_for_empty() {
7407 assert!(znorm(4).normalized_std_dev().is_none());
7408 }
7409
7410 #[test]
7411 fn test_zscore_normalized_std_dev_positive_for_varied_values() {
7412 let mut n = znorm(4);
7413 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7414 let r = n.normalized_std_dev().unwrap();
7415 assert!(r > 0.0, "expected positive normalized std dev, got {}", r);
7416 }
7417
7418 #[test]
7421 fn test_zscore_value_above_mean_count_none_for_empty() {
7422 assert!(znorm(4).value_above_mean_count().is_none());
7423 }
7424
7425 #[test]
7426 fn test_zscore_value_above_mean_count_correct() {
7427 let mut n = znorm(4);
7428 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7429 assert_eq!(n.value_above_mean_count().unwrap(), 2);
7431 }
7432
7433 #[test]
7436 fn test_zscore_consecutive_above_mean_none_for_empty() {
7437 assert!(znorm(4).consecutive_above_mean().is_none());
7438 }
7439
7440 #[test]
7441 fn test_zscore_consecutive_above_mean_correct() {
7442 let mut n = znorm(4);
7443 for v in [dec!(1), dec!(5), dec!(6), dec!(7)] { n.update(v); }
7444 assert_eq!(n.consecutive_above_mean().unwrap(), 3);
7446 }
7447
7448 #[test]
7451 fn test_zscore_above_threshold_fraction_none_for_empty() {
7452 assert!(znorm(4).above_threshold_fraction(dec!(5)).is_none());
7453 }
7454
7455 #[test]
7456 fn test_zscore_above_threshold_fraction_correct() {
7457 let mut n = znorm(4);
7458 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7459 let f = n.above_threshold_fraction(dec!(2)).unwrap();
7460 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
7461 }
7462
7463 #[test]
7464 fn test_zscore_below_threshold_fraction_none_for_empty() {
7465 assert!(znorm(4).below_threshold_fraction(dec!(5)).is_none());
7466 }
7467
7468 #[test]
7469 fn test_zscore_below_threshold_fraction_correct() {
7470 let mut n = znorm(4);
7471 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7472 let f = n.below_threshold_fraction(dec!(3)).unwrap();
7473 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
7474 }
7475
7476 #[test]
7479 fn test_zscore_lag_k_autocorrelation_none_for_zero_k() {
7480 let mut n = znorm(5);
7481 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7482 assert!(n.lag_k_autocorrelation(0).is_none());
7483 }
7484
7485 #[test]
7486 fn test_zscore_lag_k_autocorrelation_none_when_k_gte_len() {
7487 let mut n = znorm(3);
7488 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7489 assert!(n.lag_k_autocorrelation(3).is_none());
7490 }
7491
7492 #[test]
7493 fn test_zscore_lag_k_autocorrelation_positive_for_trend() {
7494 let mut n = znorm(6);
7495 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5), dec!(6)] { n.update(v); }
7496 let ac = n.lag_k_autocorrelation(1).unwrap();
7497 assert!(ac > 0.0, "trending series should have positive AC, got {}", ac);
7498 }
7499
7500 #[test]
7503 fn test_zscore_half_life_estimate_none_for_fewer_than_3() {
7504 let mut n = znorm(3);
7505 n.update(dec!(1)); n.update(dec!(2));
7506 assert!(n.half_life_estimate().is_none());
7507 }
7508
7509 #[test]
7510 fn test_zscore_half_life_no_panic_for_alternating() {
7511 let mut n = znorm(6);
7512 for v in [dec!(10), dec!(5), dec!(10), dec!(5), dec!(10), dec!(5)] { n.update(v); }
7513 let _ = n.half_life_estimate();
7514 }
7515
7516 #[test]
7519 fn test_zscore_geometric_mean_none_for_empty() {
7520 assert!(znorm(4).geometric_mean().is_none());
7521 }
7522
7523 #[test]
7524 fn test_zscore_geometric_mean_correct_for_powers_of_2() {
7525 let mut n = znorm(4);
7526 for v in [dec!(1), dec!(2), dec!(4), dec!(8)] { n.update(v); }
7527 let gm = n.geometric_mean().unwrap();
7528 assert!((gm - 64.0f64.powf(0.25)).abs() < 1e-6, "got {}", gm);
7529 }
7530
7531 #[test]
7534 fn test_zscore_harmonic_mean_none_for_empty() {
7535 assert!(znorm(4).harmonic_mean().is_none());
7536 }
7537
7538 #[test]
7539 fn test_zscore_harmonic_mean_positive_for_positive_values() {
7540 let mut n = znorm(4);
7541 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7542 let hm = n.harmonic_mean().unwrap();
7543 assert!(hm > 0.0 && hm < 4.0, "HM should be in (0, max), got {}", hm);
7544 }
7545
7546 #[test]
7549 fn test_zscore_range_normalized_value_none_for_empty() {
7550 assert!(znorm(4).range_normalized_value(dec!(5)).is_none());
7551 }
7552
7553 #[test]
7554 fn test_zscore_range_normalized_value_in_range() {
7555 let mut n = znorm(4);
7556 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7557 let r = n.range_normalized_value(dec!(2)).unwrap();
7558 assert!(r >= 0.0 && r <= 1.0, "expected [0,1], got {}", r);
7559 }
7560
7561 #[test]
7564 fn test_zscore_distance_from_median_none_for_empty() {
7565 assert!(znorm(4).distance_from_median(dec!(5)).is_none());
7566 }
7567
7568 #[test]
7569 fn test_zscore_distance_from_median_zero_at_median() {
7570 let mut n = znorm(5);
7571 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7572 let d = n.distance_from_median(dec!(3)).unwrap();
7573 assert!((d - 0.0).abs() < 1e-9, "distance from median=3 should be 0, got {}", d);
7574 }
7575
7576 #[test]
7577 fn test_zscore_momentum_none_for_single_value() {
7578 let mut n = znorm(5);
7579 n.update(dec!(10));
7580 assert!(n.momentum().is_none());
7581 }
7582
7583 #[test]
7584 fn test_zscore_momentum_positive_for_rising_window() {
7585 let mut n = znorm(3);
7586 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7587 let m = n.momentum().unwrap();
7588 assert!(m > 0.0, "rising window → positive momentum, got {}", m);
7589 }
7590
7591 #[test]
7592 fn test_zscore_value_rank_none_for_empty() {
7593 assert!(znorm(4).value_rank(dec!(5)).is_none());
7594 }
7595
7596 #[test]
7597 fn test_zscore_value_rank_extremes() {
7598 let mut n = znorm(4);
7599 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7600 let low = n.value_rank(dec!(0)).unwrap();
7601 assert!((low - 0.0).abs() < 1e-9, "got {}", low);
7602 let high = n.value_rank(dec!(5)).unwrap();
7603 assert!((high - 1.0).abs() < 1e-9, "got {}", high);
7604 }
7605
7606 #[test]
7607 fn test_zscore_coeff_of_variation_none_for_single_value() {
7608 let mut n = znorm(5);
7609 n.update(dec!(10));
7610 assert!(n.coeff_of_variation().is_none());
7611 }
7612
7613 #[test]
7614 fn test_zscore_coeff_of_variation_positive_for_spread() {
7615 let mut n = znorm(4);
7616 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
7617 let cv = n.coeff_of_variation().unwrap();
7618 assert!(cv > 0.0, "expected positive CV, got {}", cv);
7619 }
7620
7621 #[test]
7622 fn test_zscore_quantile_range_none_for_empty() {
7623 assert!(znorm(4).quantile_range().is_none());
7624 }
7625
7626 #[test]
7627 fn test_zscore_quantile_range_non_negative() {
7628 let mut n = znorm(5);
7629 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7630 let iqr = n.quantile_range().unwrap();
7631 assert!(iqr >= 0.0, "IQR should be non-negative, got {}", iqr);
7632 }
7633
7634 #[test]
7637 fn test_zscore_upper_quartile_none_for_empty() {
7638 assert!(znorm(4).upper_quartile().is_none());
7639 }
7640
7641 #[test]
7642 fn test_zscore_lower_quartile_none_for_empty() {
7643 assert!(znorm(4).lower_quartile().is_none());
7644 }
7645
7646 #[test]
7647 fn test_zscore_upper_ge_lower_quartile() {
7648 let mut n = znorm(8);
7649 for v in [dec!(10), dec!(20), dec!(30), dec!(40), dec!(50), dec!(60), dec!(70), dec!(80)] {
7650 n.update(v);
7651 }
7652 let q3 = n.upper_quartile().unwrap();
7653 let q1 = n.lower_quartile().unwrap();
7654 assert!(q3 >= q1, "Q3 ({}) should be >= Q1 ({})", q3, q1);
7655 }
7656
7657 #[test]
7660 fn test_zscore_sign_change_rate_none_for_fewer_than_3() {
7661 let mut n = znorm(4);
7662 n.update(dec!(1));
7663 n.update(dec!(2));
7664 assert!(n.sign_change_rate().is_none());
7665 }
7666
7667 #[test]
7668 fn test_zscore_sign_change_rate_one_for_zigzag() {
7669 let mut n = znorm(5);
7670 for v in [dec!(1), dec!(3), dec!(1), dec!(3), dec!(1)] { n.update(v); }
7671 let r = n.sign_change_rate().unwrap();
7672 assert!((r - 1.0).abs() < 1e-9, "zigzag should give 1.0, got {}", r);
7673 }
7674
7675 #[test]
7676 fn test_zscore_sign_change_rate_zero_for_monotone() {
7677 let mut n = znorm(5);
7678 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7679 let r = n.sign_change_rate().unwrap();
7680 assert!((r - 0.0).abs() < 1e-9, "monotone should give 0.0, got {}", r);
7681 }
7682
7683 #[test]
7686 fn test_zscore_trimmed_mean_none_for_empty() {
7687 assert!(znorm(4).trimmed_mean(0.1).is_none());
7688 }
7689
7690 #[test]
7691 fn test_zscore_trimmed_mean_equals_mean_at_zero_trim() {
7692 let mut n = znorm(4);
7693 for v in [dec!(10), dec!(20), dec!(30), dec!(40)] { n.update(v); }
7694 let tm = n.trimmed_mean(0.0).unwrap();
7695 let m = n.mean().unwrap().to_f64().unwrap();
7696 assert!((tm - m).abs() < 1e-9, "0% trim should equal mean, got tm={} m={}", tm, m);
7697 }
7698
7699 #[test]
7700 fn test_zscore_trimmed_mean_reduces_outlier_effect() {
7701 let mut n = znorm(5);
7702 for v in [dec!(10), dec!(10), dec!(10), dec!(10), dec!(1000)] { n.update(v); }
7703 let tm = n.trimmed_mean(0.2).unwrap();
7705 let m = n.mean().unwrap().to_f64().unwrap();
7706 assert!(tm < m, "trimmed mean should be less than mean when outlier trimmed, tm={} m={}", tm, m);
7707 }
7708
7709 #[test]
7712 fn test_zscore_linear_trend_slope_none_for_single_value() {
7713 let mut n = znorm(4);
7714 n.update(dec!(10));
7715 assert!(n.linear_trend_slope().is_none());
7716 }
7717
7718 #[test]
7719 fn test_zscore_linear_trend_slope_positive_for_rising() {
7720 let mut n = znorm(4);
7721 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7722 let slope = n.linear_trend_slope().unwrap();
7723 assert!(slope > 0.0, "rising window → positive slope, got {}", slope);
7724 }
7725
7726 #[test]
7727 fn test_zscore_linear_trend_slope_negative_for_falling() {
7728 let mut n = znorm(4);
7729 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
7730 let slope = n.linear_trend_slope().unwrap();
7731 assert!(slope < 0.0, "falling window → negative slope, got {}", slope);
7732 }
7733
7734 #[test]
7735 fn test_zscore_linear_trend_slope_zero_for_flat() {
7736 let mut n = znorm(4);
7737 for v in [dec!(5), dec!(5), dec!(5), dec!(5)] { n.update(v); }
7738 let slope = n.linear_trend_slope().unwrap();
7739 assert!(slope.abs() < 1e-9, "flat window → slope=0, got {}", slope);
7740 }
7741
7742 #[test]
7745 fn test_zscore_variance_ratio_none_for_few_values() {
7746 let mut n = znorm(3);
7747 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7748 assert!(n.variance_ratio().is_none());
7749 }
7750
7751 #[test]
7752 fn test_zscore_variance_ratio_gt_one_for_decreasing_vol() {
7753 let mut n = znorm(6);
7754 for v in [dec!(1), dec!(10), dec!(1), dec!(5), dec!(6), dec!(5)] { n.update(v); }
7756 let r = n.variance_ratio().unwrap();
7757 assert!(r > 1.0, "first half more volatile → ratio > 1, got {}", r);
7758 }
7759
7760 #[test]
7763 fn test_zscore_z_score_trend_slope_none_for_single_value() {
7764 let mut n = znorm(4);
7765 n.update(dec!(10));
7766 assert!(n.z_score_trend_slope().is_none());
7767 }
7768
7769 #[test]
7770 fn test_zscore_z_score_trend_slope_positive_for_rising() {
7771 let mut n = znorm(5);
7772 for v in [dec!(1), dec!(2), dec!(3), dec!(4), dec!(5)] { n.update(v); }
7773 let slope = n.z_score_trend_slope().unwrap();
7774 assert!(slope > 0.0, "rising window → positive z-score slope, got {}", slope);
7775 }
7776
7777 #[test]
7780 fn test_zscore_mean_absolute_change_none_for_single_value() {
7781 let mut n = znorm(4);
7782 n.update(dec!(10));
7783 assert!(n.mean_absolute_change().is_none());
7784 }
7785
7786 #[test]
7787 fn test_zscore_mean_absolute_change_zero_for_constant() {
7788 let mut n = znorm(4);
7789 for _ in 0..4 { n.update(dec!(5)); }
7790 let mac = n.mean_absolute_change().unwrap();
7791 assert!(mac.abs() < 1e-9, "constant window → MAC=0, got {}", mac);
7792 }
7793
7794 #[test]
7795 fn test_zscore_mean_absolute_change_positive_for_varying() {
7796 let mut n = znorm(4);
7797 for v in [dec!(1), dec!(3), dec!(2), dec!(5)] { n.update(v); }
7798 let mac = n.mean_absolute_change().unwrap();
7799 assert!(mac > 0.0, "varying window → MAC > 0, got {}", mac);
7800 }
7801
7802 #[test]
7805 fn test_zscore_monotone_increase_fraction_none_for_single() {
7806 let mut n = znorm(4);
7807 n.update(dec!(5));
7808 assert!(n.monotone_increase_fraction().is_none());
7809 }
7810
7811 #[test]
7812 fn test_zscore_monotone_increase_fraction_one_for_rising() {
7813 let mut n = znorm(4);
7814 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7815 let f = n.monotone_increase_fraction().unwrap();
7816 assert!((f - 1.0).abs() < 1e-9, "all rising → fraction=1, got {}", f);
7817 }
7818
7819 #[test]
7820 fn test_zscore_abs_max_none_for_empty() {
7821 let n = znorm(4);
7822 assert!(n.abs_max().is_none());
7823 }
7824
7825 #[test]
7826 fn test_zscore_abs_max_returns_max_absolute() {
7827 let mut n = znorm(4);
7828 for v in [dec!(1), dec!(3), dec!(2)] { n.update(v); }
7829 assert_eq!(n.abs_max().unwrap(), dec!(3));
7830 }
7831
7832 #[test]
7833 fn test_zscore_max_count_none_for_empty() {
7834 let n = znorm(4);
7835 assert!(n.max_count().is_none());
7836 }
7837
7838 #[test]
7839 fn test_zscore_max_count_correct() {
7840 let mut n = znorm(4);
7841 for v in [dec!(1), dec!(5), dec!(3), dec!(5)] { n.update(v); }
7842 assert_eq!(n.max_count().unwrap(), 2);
7843 }
7844
7845 #[test]
7846 fn test_zscore_mean_ratio_none_for_single() {
7847 let mut n = znorm(4);
7848 n.update(dec!(10));
7849 assert!(n.mean_ratio().is_none());
7850 }
7851
7852 #[test]
7855 fn test_zscore_exponential_weighted_mean_none_for_empty() {
7856 let n = znorm(4);
7857 assert!(n.exponential_weighted_mean(0.5).is_none());
7858 }
7859
7860 #[test]
7861 fn test_zscore_exponential_weighted_mean_returns_value() {
7862 let mut n = znorm(4);
7863 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7864 let ewm = n.exponential_weighted_mean(0.5).unwrap();
7865 assert!(ewm > 0.0, "EWM should be positive, got {}", ewm);
7866 }
7867
7868 #[test]
7869 fn test_zscore_peak_to_trough_none_for_empty() {
7870 let n = znorm(4);
7871 assert!(n.peak_to_trough_ratio().is_none());
7872 }
7873
7874 #[test]
7875 fn test_zscore_peak_to_trough_correct() {
7876 let mut n = znorm(4);
7877 for v in [dec!(2), dec!(4), dec!(1), dec!(8)] { n.update(v); }
7878 let r = n.peak_to_trough_ratio().unwrap();
7879 assert!((r - 8.0).abs() < 1e-9, "max=8, min=1 → ratio=8, got {}", r);
7880 }
7881
7882 #[test]
7883 fn test_zscore_second_moment_none_for_empty() {
7884 let n = znorm(4);
7885 assert!(n.second_moment().is_none());
7886 }
7887
7888 #[test]
7889 fn test_zscore_second_moment_correct() {
7890 let mut n = znorm(4);
7891 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
7892 let m = n.second_moment().unwrap();
7893 assert!((m - 14.0 / 3.0).abs() < 1e-9, "second moment ≈ 4.667, got {}", m);
7894 }
7895
7896 #[test]
7897 fn test_zscore_range_over_mean_none_for_empty() {
7898 let n = znorm(4);
7899 assert!(n.range_over_mean().is_none());
7900 }
7901
7902 #[test]
7903 fn test_zscore_range_over_mean_positive() {
7904 let mut n = znorm(4);
7905 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7906 let r = n.range_over_mean().unwrap();
7907 assert!(r > 0.0, "range/mean should be positive, got {}", r);
7908 }
7909
7910 #[test]
7911 fn test_zscore_above_median_fraction_none_for_empty() {
7912 let n = znorm(4);
7913 assert!(n.above_median_fraction().is_none());
7914 }
7915
7916 #[test]
7917 fn test_zscore_above_median_fraction_in_range() {
7918 let mut n = znorm(4);
7919 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7920 let f = n.above_median_fraction().unwrap();
7921 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
7922 }
7923
7924 #[test]
7927 fn test_zscore_interquartile_mean_none_for_empty() {
7928 let n = znorm(4);
7929 assert!(n.interquartile_mean().is_none());
7930 }
7931
7932 #[test]
7933 fn test_zscore_outlier_fraction_none_for_empty() {
7934 let n = znorm(4);
7935 assert!(n.outlier_fraction(2.0).is_none());
7936 }
7937
7938 #[test]
7939 fn test_zscore_outlier_fraction_zero_for_constant() {
7940 let mut n = znorm(4);
7941 for _ in 0..4 { n.update(dec!(5)); }
7942 let f = n.outlier_fraction(1.0).unwrap();
7943 assert!(f.abs() < 1e-9, "constant window → no outliers, got {}", f);
7944 }
7945
7946 #[test]
7947 fn test_zscore_sign_flip_count_none_for_single() {
7948 let mut n = znorm(4);
7949 n.update(dec!(1));
7950 assert!(n.sign_flip_count().is_none());
7951 }
7952
7953 #[test]
7954 fn test_zscore_sign_flip_count_correct() {
7955 let mut n = znorm(6);
7956 for v in [dec!(1), dec!(-1), dec!(1), dec!(-1)] { n.update(v); }
7957 let c = n.sign_flip_count().unwrap();
7958 assert_eq!(c, 3, "3 sign flips expected, got {}", c);
7959 }
7960
7961 #[test]
7962 fn test_zscore_rms_none_for_empty() {
7963 let n = znorm(4);
7964 assert!(n.rms().is_none());
7965 }
7966
7967 #[test]
7968 fn test_zscore_rms_positive_for_nonzero_values() {
7969 let mut n = znorm(4);
7970 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
7971 let r = n.rms().unwrap();
7972 assert!(r > 0.0, "RMS should be positive, got {}", r);
7973 }
7974
7975 #[test]
7978 fn test_zscore_distinct_count_zero_for_empty() {
7979 let n = znorm(4);
7980 assert_eq!(n.distinct_count(), 0);
7981 }
7982
7983 #[test]
7984 fn test_zscore_distinct_count_correct() {
7985 let mut n = znorm(4);
7986 for v in [dec!(1), dec!(1), dec!(2), dec!(3)] { n.update(v); }
7987 assert_eq!(n.distinct_count(), 3);
7988 }
7989
7990 #[test]
7991 fn test_zscore_max_fraction_none_for_empty() {
7992 let n = znorm(4);
7993 assert!(n.max_fraction().is_none());
7994 }
7995
7996 #[test]
7997 fn test_zscore_max_fraction_correct() {
7998 let mut n = znorm(4);
7999 for v in [dec!(1), dec!(2), dec!(3), dec!(3)] { n.update(v); }
8000 let f = n.max_fraction().unwrap();
8001 assert!((f - 0.5).abs() < 1e-9, "2/4 are max → 0.5, got {}", f);
8002 }
8003
8004 #[test]
8005 fn test_zscore_latest_minus_mean_none_for_empty() {
8006 let n = znorm(4);
8007 assert!(n.latest_minus_mean().is_none());
8008 }
8009
8010 #[test]
8011 fn test_zscore_latest_to_mean_ratio_none_for_empty() {
8012 let n = znorm(4);
8013 assert!(n.latest_to_mean_ratio().is_none());
8014 }
8015
8016 #[test]
8017 fn test_zscore_latest_to_mean_ratio_one_for_constant() {
8018 let mut n = znorm(4);
8019 for _ in 0..4 { n.update(dec!(5)); }
8020 let r = n.latest_to_mean_ratio().unwrap();
8021 assert!((r - 1.0).abs() < 1e-9, "latest=mean → ratio=1, got {}", r);
8022 }
8023
8024 #[test]
8027 fn test_zscore_below_mean_fraction_none_for_empty() {
8028 assert!(znorm(4).below_mean_fraction().is_none());
8029 }
8030
8031 #[test]
8032 fn test_zscore_below_mean_fraction_symmetric_data() {
8033 let mut n = znorm(4);
8034 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
8035 let f = n.below_mean_fraction().unwrap();
8037 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
8038 }
8039
8040 #[test]
8041 fn test_zscore_tail_variance_none_for_small_window() {
8042 let mut n = znorm(3);
8043 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
8044 assert!(n.tail_variance().is_none());
8045 }
8046
8047 #[test]
8048 fn test_zscore_tail_variance_nonneg_for_varied_data() {
8049 let mut n = znorm(6);
8050 for v in [dec!(1), dec!(2), dec!(5), dec!(6), dec!(9), dec!(10)] { n.update(v); }
8051 let tv = n.tail_variance().unwrap();
8052 assert!(tv >= 0.0, "tail variance should be non-negative, got {}", tv);
8053 }
8054
8055 #[test]
8058 fn test_zscore_new_max_count_zero_for_empty() {
8059 let n = znorm(4);
8060 assert_eq!(n.new_max_count(), 0);
8061 }
8062
8063 #[test]
8064 fn test_zscore_new_max_count_all_rising() {
8065 let mut n = znorm(4);
8066 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
8067 assert_eq!(n.new_max_count(), 4, "each value is a new high");
8068 }
8069
8070 #[test]
8071 fn test_zscore_new_min_count_zero_for_empty() {
8072 let n = znorm(4);
8073 assert_eq!(n.new_min_count(), 0);
8074 }
8075
8076 #[test]
8077 fn test_zscore_new_min_count_all_falling() {
8078 let mut n = znorm(4);
8079 for v in [dec!(4), dec!(3), dec!(2), dec!(1)] { n.update(v); }
8080 assert_eq!(n.new_min_count(), 4, "each value is a new low");
8081 }
8082
8083 #[test]
8084 fn test_zscore_zero_fraction_none_for_empty() {
8085 let n = znorm(4);
8086 assert!(n.zero_fraction().is_none());
8087 }
8088
8089 #[test]
8090 fn test_zscore_zero_fraction_zero_when_no_zeros() {
8091 let mut n = znorm(4);
8092 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
8093 let f = n.zero_fraction().unwrap();
8094 assert!(f.abs() < 1e-9, "no zeros → fraction=0, got {}", f);
8095 }
8096
8097 #[test]
8100 fn test_zscore_cumulative_sum_zero_for_empty() {
8101 let n = znorm(4);
8102 assert_eq!(n.cumulative_sum(), rust_decimal::Decimal::ZERO);
8103 }
8104
8105 #[test]
8106 fn test_zscore_cumulative_sum_correct() {
8107 let mut n = znorm(4);
8108 for v in [dec!(1), dec!(2), dec!(3)] { n.update(v); }
8109 assert_eq!(n.cumulative_sum(), dec!(6));
8110 }
8111
8112 #[test]
8113 fn test_zscore_max_to_min_ratio_none_for_empty() {
8114 assert!(znorm(4).max_to_min_ratio().is_none());
8115 }
8116
8117 #[test]
8118 fn test_zscore_max_to_min_ratio_one_for_constant() {
8119 let mut n = znorm(4);
8120 for _ in 0..4 { n.update(dec!(5)); }
8121 let r = n.max_to_min_ratio().unwrap();
8122 assert!((r - 1.0).abs() < 1e-9, "constant window → ratio=1, got {}", r);
8123 }
8124
8125 #[test]
8128 fn test_zscore_above_midpoint_fraction_none_for_empty() {
8129 assert!(znorm(4).above_midpoint_fraction().is_none());
8130 }
8131
8132 #[test]
8133 fn test_zscore_above_midpoint_fraction_half_for_symmetric() {
8134 let mut n = znorm(4);
8135 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
8136 let f = n.above_midpoint_fraction().unwrap();
8138 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
8139 }
8140
8141 #[test]
8142 fn test_zscore_positive_fraction_none_for_empty() {
8143 assert!(znorm(4).positive_fraction().is_none());
8144 }
8145
8146 #[test]
8147 fn test_zscore_positive_fraction_zero_for_all_nonpositive() {
8148 let mut n = znorm(3);
8149 for v in [dec!(-3), dec!(-1), dec!(0)] { n.update(v); }
8150 let f = n.positive_fraction().unwrap();
8151 assert!((f - 0.0).abs() < 1e-9, "no positives → 0.0, got {}", f);
8152 }
8153
8154 #[test]
8155 fn test_zscore_above_mean_fraction_none_for_empty() {
8156 assert!(znorm(4).above_mean_fraction().is_none());
8157 }
8158
8159 #[test]
8160 fn test_zscore_above_mean_fraction_half_for_symmetric() {
8161 let mut n = znorm(4);
8162 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
8163 let f = n.above_mean_fraction().unwrap();
8165 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
8166 }
8167
8168 #[test]
8171 fn test_zscore_window_iqr_none_for_empty() {
8172 assert!(znorm(4).window_iqr().is_none());
8173 }
8174
8175 #[test]
8176 fn test_zscore_window_iqr_zero_for_constant() {
8177 let mut n = znorm(4);
8178 for _ in 0..4 { n.update(dec!(5)); }
8179 assert_eq!(n.window_iqr().unwrap(), dec!(0));
8180 }
8181
8182 #[test]
8183 fn test_zscore_mean_absolute_deviation_none_for_empty() {
8184 assert!(znorm(4).mean_absolute_deviation().is_none());
8185 }
8186
8187 #[test]
8188 fn test_zscore_mean_absolute_deviation_zero_for_constant() {
8189 let mut n = znorm(4);
8190 for _ in 0..4 { n.update(dec!(7)); }
8191 let mad = n.mean_absolute_deviation().unwrap();
8192 assert!(mad.abs() < 1e-9, "constant window → MAD=0, got {}", mad);
8193 }
8194
8195 #[test]
8196 fn test_zscore_run_length_mean_none_for_single_value() {
8197 let mut n = znorm(4);
8198 n.update(dec!(1));
8199 assert!(n.run_length_mean().is_none());
8200 }
8201
8202 #[test]
8203 fn test_zscore_run_length_mean_all_increasing() {
8204 let mut n = znorm(4);
8205 for v in [dec!(1), dec!(2), dec!(3), dec!(4)] { n.update(v); }
8206 let r = n.run_length_mean().unwrap();
8208 assert!((r - 4.0).abs() < 1e-9, "monotone up → run_len=4, got {}", r);
8209 }
8210}