use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use super::*;
use crate::IndicatorError;
fn make_candle(close: Decimal, idx: i64) -> Candle {
use quant_primitives::Candle;
Candle::new(
close,
close + dec!(1),
close - dec!(1),
close,
dec!(1000),
chrono::DateTime::from_timestamp(1_700_000_000 + idx * 86400, 0).expect("valid timestamp"),
)
.expect("valid candle")
}
#[test]
fn new_rejects_lag_zero() {
let err = VarianceRatio::new(0).unwrap_err();
assert!(matches!(err, IndicatorError::InvalidParameter { .. }));
}
#[test]
fn new_rejects_lag_one() {
let err = VarianceRatio::new(1).unwrap_err();
assert!(matches!(err, IndicatorError::InvalidParameter { .. }));
}
#[test]
fn new_accepts_lag_two() {
assert!(VarianceRatio::new(2).is_ok());
}
#[test]
fn compute_ratio_insufficient_data() {
let vr = VarianceRatio::new(20).expect("valid VR lag");
let candles: Vec<Candle> = (0..10).map(|i| make_candle(dec!(100), i)).collect();
let err = vr.compute_ratio(&candles).unwrap_err();
assert!(matches!(
err,
IndicatorError::InsufficientData {
required: 22,
actual: 10
}
));
}
#[test]
fn compute_ratio_constant_prices_returns_error() {
let vr = VarianceRatio::new(2).expect("valid VR lag");
let candles: Vec<Candle> = (0..50).map(|i| make_candle(dec!(100), i)).collect();
let err = vr.compute_ratio(&candles).unwrap_err();
assert!(matches!(err, IndicatorError::InsufficientData { .. }));
}
#[test]
fn trending_series_produces_vr_above_one() {
let vr = VarianceRatio::new(5).expect("valid VR lag");
let candles: Vec<Candle> = (0..50)
.map(|i| make_candle(dec!(100) + Decimal::from(i) * dec!(2), i))
.collect();
let ratio = vr
.compute_ratio(&candles)
.expect("sufficient data for VR ratio");
assert!(
ratio > Decimal::ONE,
"Trending VR should be > 1, got {}",
ratio
);
}
#[test]
fn mean_reverting_series_produces_vr_below_one() {
let vr = VarianceRatio::new(2).expect("valid VR lag");
let candles: Vec<Candle> = (0..50)
.map(|i| {
let price = if i % 2 == 0 { dec!(103) } else { dec!(97) };
make_candle(price, i)
})
.collect();
let ratio = vr
.compute_ratio(&candles)
.expect("sufficient data for VR ratio");
assert!(
ratio < Decimal::ONE,
"Mean-reverting VR should be < 1, got {}",
ratio
);
}
#[test]
fn vr_is_always_non_negative() {
let vr = VarianceRatio::new(3).expect("valid VR lag");
let candles: Vec<Candle> = (0..30)
.map(|i| {
let price = if i % 2 == 0 { dec!(105) } else { dec!(95) };
make_candle(price, i)
})
.collect();
let ratio = vr
.compute_ratio(&candles)
.expect("sufficient data for VR ratio");
assert!(
!ratio.is_sign_negative(),
"VR must be non-negative, got {}",
ratio
);
}
#[test]
fn rolling_produces_correct_count() {
let vr = VarianceRatio::new(3).expect("valid VR lag");
let candles: Vec<Candle> = (0..30)
.map(|i| make_candle(dec!(100) + Decimal::from(i) * dec!(1), i))
.collect();
let series = vr
.rolling(&candles, 10)
.expect("sufficient data for VR rolling");
assert_eq!(
series.len(),
21,
"Expected 21 rolling windows, got {}",
series.len()
);
}
#[test]
fn rolling_insufficient_candles() {
let vr = VarianceRatio::new(5).expect("valid VR lag");
let candles: Vec<Candle> = (0..8)
.map(|i| make_candle(dec!(100) + Decimal::from(i), i))
.collect();
let err = vr.rolling(&candles, 20).unwrap_err();
assert!(matches!(err, IndicatorError::InsufficientData { .. }));
}
#[test]
fn rolling_indices_are_monotonically_increasing() {
let vr = VarianceRatio::new(3).expect("valid VR lag");
let candles: Vec<Candle> = (0..40)
.map(|i| make_candle(dec!(100) + Decimal::from(i) * dec!(2), i))
.collect();
let series = vr
.rolling(&candles, 10)
.expect("sufficient data for VR rolling");
for window in series.windows(2) {
assert!(
window[1].0 > window[0].0,
"Indices must be monotonically increasing"
);
}
}
#[test]
fn variance_of_empty_is_zero() {
assert_eq!(variance(&[]), Decimal::ZERO);
}
#[test]
fn variance_of_constant_is_zero() {
let data = vec![dec!(5), dec!(5), dec!(5), dec!(5)];
assert_eq!(variance(&data), Decimal::ZERO);
}
#[test]
fn variance_known_values() {
let data: Vec<Decimal> = (1..=5).map(Decimal::from).collect();
assert_eq!(variance(&data), dec!(2));
}
#[test]
fn decimal_ln_of_one_is_zero() {
assert_eq!(decimal_ln(Decimal::ONE), Decimal::ZERO);
}
#[test]
fn decimal_ln_of_zero_is_zero() {
assert_eq!(decimal_ln(Decimal::ZERO), Decimal::ZERO);
}
#[test]
fn decimal_ln_of_negative_is_zero() {
assert_eq!(decimal_ln(dec!(-5)), Decimal::ZERO);
}
#[test]
fn decimal_ln_positive_value_reasonable() {
let result = decimal_ln(dec!(100));
let diff = (result - dec!(4.6052)).abs();
assert!(
diff < dec!(0.01),
"ln(100) should be ~4.6052, got {}",
result
);
}