use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use super::*;
use crate::IndicatorError;
fn make_candle(close: Decimal, idx: i64) -> 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_lookback_zero() {
let err = EfficiencyRatio::new(0).unwrap_err();
assert!(matches!(err, IndicatorError::InvalidParameter { .. }));
}
#[test]
fn new_accepts_lookback_one() {
assert!(EfficiencyRatio::new(1).is_ok());
}
#[test]
fn name_includes_lookback() {
let er = EfficiencyRatio::new(10).expect("valid ER lookback");
assert_eq!(er.name(), "ER(10)");
}
#[test]
fn warmup_period_is_lookback_plus_one() {
let er = EfficiencyRatio::new(10).expect("valid ER lookback");
assert_eq!(er.warmup_period(), 11);
}
#[test]
fn compute_ratio_insufficient_data() {
let er = EfficiencyRatio::new(10).expect("valid ER lookback");
let candles: Vec<Candle> = (0..5).map(|i| make_candle(dec!(100), i)).collect();
let err = er.compute_ratio(&candles).unwrap_err();
assert!(matches!(
err,
IndicatorError::InsufficientData {
required: 11,
actual: 5
}
));
}
#[test]
fn perfectly_trending_returns_one() {
let candles: Vec<Candle> = (0..6)
.map(|i| make_candle(dec!(100) + Decimal::from(i) * dec!(2), i))
.collect();
let er = EfficiencyRatio::new(5).expect("valid ER lookback");
let ratio = er
.compute_ratio(&candles)
.expect("sufficient data for ER ratio");
assert_eq!(ratio, Decimal::ONE);
}
#[test]
fn perfectly_trending_down_returns_one() {
let candles: Vec<Candle> = (0..6)
.map(|i| make_candle(dec!(110) - Decimal::from(i) * dec!(2), i))
.collect();
let er = EfficiencyRatio::new(5).expect("valid ER lookback");
let ratio = er
.compute_ratio(&candles)
.expect("sufficient data for ER ratio");
assert_eq!(ratio, Decimal::ONE);
}
#[test]
fn constant_price_returns_zero() {
let candles: Vec<Candle> = (0..10).map(|i| make_candle(dec!(100), i)).collect();
let er = EfficiencyRatio::new(5).expect("valid ER lookback");
let ratio = er
.compute_ratio(&candles)
.expect("sufficient data for ER ratio");
assert_eq!(ratio, Decimal::ZERO);
}
#[test]
fn oscillating_returns_low_er() {
let prices = [
dec!(100),
dec!(105),
dec!(100),
dec!(105),
dec!(100),
dec!(105),
];
let candles: Vec<Candle> = prices
.iter()
.enumerate()
.map(|(i, &p)| make_candle(p, i as i64))
.collect();
let er = EfficiencyRatio::new(5).expect("valid ER lookback");
let ratio = er
.compute_ratio(&candles)
.expect("sufficient data for ER ratio");
assert_eq!(ratio, dec!(0.2));
}
#[test]
fn er_bounded_zero_to_one() {
let patterns: Vec<Vec<Decimal>> = vec![
vec![
dec!(100),
dec!(90),
dec!(110),
dec!(95),
dec!(105),
dec!(100),
],
vec![dec!(50), dec!(51), dec!(49), dec!(52), dec!(48), dec!(53)],
vec![
dec!(200),
dec!(200),
dec!(200),
dec!(200),
dec!(200),
dec!(200),
],
];
let er = EfficiencyRatio::new(5).expect("valid ER lookback");
for prices in &patterns {
let candles: Vec<Candle> = prices
.iter()
.enumerate()
.map(|(i, &p)| make_candle(p, i as i64))
.collect();
let ratio = er
.compute_ratio(&candles)
.expect("sufficient data for ER ratio");
assert!(
ratio >= Decimal::ZERO && ratio <= Decimal::ONE,
"ER {} outside [0, 1] for prices {:?}",
ratio,
prices
);
}
}
#[test]
fn compute_as_indicator_returns_series() {
let candles: Vec<Candle> = (0..20)
.map(|i| make_candle(dec!(100) + Decimal::from(i), i))
.collect();
let er = EfficiencyRatio::new(5).expect("valid ER lookback");
let series = er.compute(&candles).expect("sufficient data for ER");
assert_eq!(series.len(), 15);
for val in series.decimal_values() {
assert!(val >= Decimal::ZERO && val <= Decimal::ONE);
}
}
#[test]
fn compute_as_indicator_insufficient_data() {
let candles: Vec<Candle> = (0..3).map(|i| make_candle(dec!(100), i)).collect();
let er = EfficiencyRatio::new(5).expect("valid ER lookback");
let err = er.compute(&candles).unwrap_err();
assert!(matches!(err, IndicatorError::InsufficientData { .. }));
}
#[test]
fn lookback_one_two_candles() {
let candles = vec![make_candle(dec!(100), 0), make_candle(dec!(105), 1)];
let er = EfficiencyRatio::new(1).expect("valid ER lookback");
let ratio = er
.compute_ratio(&candles)
.expect("sufficient data for ER ratio");
assert_eq!(ratio, Decimal::ONE);
}
#[test]
fn exact_boundary_candle_count() {
let candles: Vec<Candle> = (0..6)
.map(|i| make_candle(dec!(100) + Decimal::from(i), i))
.collect();
let er = EfficiencyRatio::new(5).expect("valid ER lookback");
assert!(er.compute_ratio(&candles).is_ok());
let candles: Vec<Candle> = (0..5)
.map(|i| make_candle(dec!(100) + Decimal::from(i), i))
.collect();
assert!(er.compute_ratio(&candles).is_err());
}