quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
use super::*;
use chrono::{TimeZone, Utc};
use rust_decimal_macros::dec;

fn make_candle_ohlc(
    open: Decimal,
    high: Decimal,
    low: Decimal,
    close: Decimal,
    idx: i64,
) -> Candle {
    let ts = Utc
        .with_ymd_and_hms(2024, 1, 1, idx as u32, 0, 0)
        .single()
        .expect("valid datetime");
    Candle::new(open, high, low, close, dec!(1000), ts).expect("valid candle")
}

#[test]
fn true_range_basic() {
    // High-Low range is largest
    let candle = make_candle_ohlc(dec!(100), dec!(110), dec!(95), dec!(105), 0);
    let tr = true_range(&candle, dec!(100));
    assert_eq!(tr, dec!(15)); // 110 - 95 = 15

    // Gap up: high - prev_close is largest
    let candle = make_candle_ohlc(dec!(120), dec!(125), dec!(118), dec!(122), 0);
    let tr = true_range(&candle, dec!(100));
    assert_eq!(tr, dec!(25)); // 125 - 100 = 25

    // Gap down: prev_close - low is largest
    let candle = make_candle_ohlc(dec!(80), dec!(85), dec!(75), dec!(82), 0);
    let tr = true_range(&candle, dec!(100));
    assert_eq!(tr, dec!(25)); // |75 - 100| = 25
}

#[test]
fn atr_measures_volatility() {
    // High volatility candles
    let high_vol: Vec<Candle> = (0..10)
        .map(|i| {
            let base = Decimal::from(100);
            make_candle_ohlc(base, base + dec!(20), base - dec!(20), base, i)
        })
        .collect();

    // Low volatility candles
    let low_vol: Vec<Candle> = (0..10)
        .map(|i| {
            let base = Decimal::from(100);
            make_candle_ohlc(base, base + dec!(2), base - dec!(2), base, i)
        })
        .collect();

    let atr = Atr::new(5).expect("valid ATR period");

    let high_vol_atr = atr.compute(&high_vol).expect("sufficient high-vol data");
    let low_vol_atr = atr.compute(&low_vol).expect("sufficient low-vol data");

    let high_last = high_vol_atr.last().expect("non-empty high-vol series").1;
    let low_last = low_vol_atr.last().expect("non-empty low-vol series").1;

    assert!(
        high_last > low_last * dec!(5),
        "High vol ATR {} should be much larger than low vol ATR {}",
        high_last,
        low_last
    );
}

#[test]
fn atr_always_positive() {
    let candles: Vec<Candle> = (0..20)
        .map(|i| {
            let base = Decimal::from(100 + i % 10);
            make_candle_ohlc(base, base + dec!(5), base - dec!(5), base + dec!(2), i)
        })
        .collect();

    let atr = Atr::new(14).expect("valid ATR period");
    let series = atr.compute(&candles).expect("sufficient data for ATR");

    for (_, value) in series.values() {
        assert!(*value > Decimal::ZERO, "ATR {} should be positive", value);
    }
}

#[test]
fn atr_insufficient_data() {
    let candles: Vec<Candle> = (0..5)
        .map(|i| make_candle_ohlc(dec!(100), dec!(105), dec!(95), dec!(102), i))
        .collect();

    let atr = Atr::new(14).expect("valid ATR period");
    let result = atr.compute(&candles);

    assert!(matches!(
        result,
        Err(IndicatorError::InsufficientData {
            required: 15,
            actual: 5
        })
    ));
}

#[test]
fn atr_period_zero() {
    let result = Atr::new(0);
    assert!(matches!(
        result,
        Err(IndicatorError::InvalidParameter { .. })
    ));
}

#[test]
fn atr_name() {
    let atr = Atr::new(14).expect("valid ATR period");
    assert_eq!(atr.name(), "ATR(14)");
}

#[test]
fn atr_warmup_period() {
    let atr = Atr::new(14).expect("valid ATR period");
    assert_eq!(atr.warmup_period(), 15);
}