use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use crate::error::IndicatorError;
use crate::rolling_zscore::RollingZScore;
#[test]
fn window_not_filled_returns_none() {
let mut zs = RollingZScore::new(5).expect("valid window");
assert!(zs.value().is_none(), "empty window");
zs.update(dec!(1));
assert!(zs.value().is_none(), "1 of 5");
zs.update(dec!(2));
assert!(zs.value().is_none(), "2 of 5");
zs.update(dec!(3));
assert!(zs.value().is_none(), "3 of 5");
zs.update(dec!(4));
assert!(zs.value().is_none(), "4 of 5");
}
#[test]
fn all_same_values_returns_none() {
let mut zs = RollingZScore::new(5).expect("valid window");
for _ in 0..5 {
zs.update(dec!(7));
}
assert!(
zs.value().is_none(),
"stddev=0 when all values identical → None"
);
}
#[test]
fn known_zscore_1_2_3_4_5_window5() {
let mut zs = RollingZScore::new(5).expect("valid window");
for i in 1..=5 {
zs.update(Decimal::from(i));
}
let z = zs.value().expect("window filled, stddev > 0");
let expected = dec!(1.4142135624);
let diff = (z - expected).abs();
assert!(
diff < dec!(0.0001),
"expected z ≈ {expected}, got {z}, diff = {diff}"
);
}
#[test]
fn negative_and_mixed_values() {
let mut zs = RollingZScore::new(5).expect("valid window");
for v in [-2, -1, 0, 1, 2] {
zs.update(Decimal::from(v));
}
let z = zs.value().expect("window filled");
let expected = dec!(1.4142135624);
let diff = (z - expected).abs();
assert!(
diff < dec!(0.0001),
"expected z ≈ {expected}, got {z}, diff = {diff}"
);
zs.update(dec!(-3));
let z = zs.value().expect("window filled");
assert!(z < Decimal::ZERO, "z-score should be negative, got {z}");
let expected_neg = dec!(-1.6274);
let diff = (z - expected_neg).abs();
assert!(
diff < dec!(0.001),
"expected z ≈ {expected_neg}, got {z}, diff = {diff}"
);
}
#[test]
fn window_1_returns_error() {
let err = RollingZScore::new(1).expect_err("window=1 should fail");
match err {
IndicatorError::InvalidParameter { message } => {
assert!(
message.contains("must be > 1"),
"error should mention 'must be > 1', got: {message}"
);
}
other => panic!("expected InvalidParameter, got: {other:?}"),
}
}
#[test]
fn window_0_returns_error() {
let err = RollingZScore::new(0).expect_err("window=0 should fail");
match err {
IndicatorError::InvalidParameter { message } => {
assert!(
message.contains("must be > 1"),
"error should mention 'must be > 1', got: {message}"
);
}
other => panic!("expected InvalidParameter, got: {other:?}"),
}
}
#[test]
fn rolling_window_slides_correctly() {
let mut zs = RollingZScore::new(5).expect("valid window");
let data = [
dec!(10),
dec!(12),
dec!(11),
dec!(13),
dec!(15),
dec!(14),
dec!(16),
dec!(18),
dec!(17),
dec!(19),
];
for v in &data[..5] {
zs.update(*v);
}
let z1 = zs.value().expect("window filled after 5");
assert!(z1 > Decimal::ZERO, "15 > mean → positive z, got {z1}");
zs.update(data[5]); let z2 = zs.value().expect("window still filled");
assert!(z2 > Decimal::ZERO, "14 > mean → positive z, got {z2}");
assert!(z1 != z2, "z-score should change as window slides");
for v in &data[6..] {
zs.update(*v);
}
let z_final = zs.value().expect("window filled");
assert!(
z_final > Decimal::ZERO,
"19 > mean(16.8) → positive z, got {z_final}"
);
}
#[test]
fn new_with_valid_window_succeeds() {
let zs = RollingZScore::new(2);
assert!(zs.is_ok(), "window=2 should succeed");
let zs = RollingZScore::new(100);
assert!(zs.is_ok(), "window=100 should succeed");
}