use super::*;
use chrono::{TimeZone, Utc};
use rust_decimal_macros::dec;
fn make_vol_candle(volume: Decimal, idx: i64) -> Candle {
let ts = Utc
.with_ymd_and_hms(2024, 1, 1, idx as u32 % 24, 0, 0)
.single()
.expect("valid datetime");
Candle::new(dec!(100), dec!(101), dec!(99), dec!(100), volume, ts).expect("valid candle")
}
fn default_config(lookback: usize) -> VolSignalConfig {
VolSignalConfig {
lookback,
elevated_threshold: dec!(2),
}
}
#[test]
fn returns_none_before_lookback() {
let candles: Vec<Candle> = (0..5).map(|i| make_vol_candle(dec!(1000), i)).collect();
let indicator =
VolumeSignalIndicator::new(default_config(20)).expect("valid volume signal config");
let results = indicator
.compute(&candles)
.expect("sufficient data for volume signal");
assert_eq!(results.len(), 5);
assert!(results.iter().all(|r| r.is_none()));
}
#[test]
fn elevated_volume_classified_correctly() {
let mut candles: Vec<Candle> = (0..20).map(|i| make_vol_candle(dec!(1000), i)).collect();
candles.push(make_vol_candle(dec!(3000), 20));
let indicator =
VolumeSignalIndicator::new(default_config(20)).expect("valid volume signal config");
let results = indicator
.compute(&candles)
.expect("sufficient data for volume signal");
assert!(results[..20].iter().all(|r| r.is_none()));
let sig = results[20].as_ref().expect("signal present at index");
assert_eq!(sig.anomaly, VolumeAnomaly::Elevated);
}
#[test]
fn subdued_volume_classified_correctly() {
let mut candles: Vec<Candle> = (0..20).map(|i| make_vol_candle(dec!(1000), i)).collect();
candles.push(make_vol_candle(dec!(400), 20));
let indicator =
VolumeSignalIndicator::new(default_config(20)).expect("valid volume signal config");
let results = indicator
.compute(&candles)
.expect("sufficient data for volume signal");
let sig = results[20].as_ref().expect("signal present at index");
assert_eq!(sig.anomaly, VolumeAnomaly::Subdued);
}
#[test]
fn normal_volume_classified_correctly() {
let candles: Vec<Candle> = (0..21).map(|i| make_vol_candle(dec!(1000), i)).collect();
let indicator =
VolumeSignalIndicator::new(default_config(20)).expect("valid volume signal config");
let results = indicator
.compute(&candles)
.expect("sufficient data for volume signal");
let sig = results[20].as_ref().expect("signal present at index");
assert_eq!(sig.anomaly, VolumeAnomaly::Normal);
assert_eq!(sig.ratio, dec!(1));
}
#[test]
fn invalid_lookback_zero_returns_error() {
let config = VolSignalConfig {
lookback: 0,
elevated_threshold: dec!(2),
};
let result = VolumeSignalIndicator::new(config);
assert!(matches!(
result,
Err(IndicatorError::InvalidParameter { .. })
));
}
#[test]
fn result_count_equals_candle_count() {
let candles: Vec<Candle> = (0..30).map(|i| make_vol_candle(dec!(1000), i)).collect();
let indicator =
VolumeSignalIndicator::new(default_config(10)).expect("valid volume signal config");
let results = indicator
.compute(&candles)
.expect("sufficient data for volume signal");
assert_eq!(results.len(), 30);
}