quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
//! Unit tests for DetrendedOscillator.

use rust_decimal_macros::dec;

use crate::detrended::DetrendedOscillator;
use crate::indicator::Indicator;

fn make_candle(close: rust_decimal::Decimal, idx: i64) -> quant_primitives::Candle {
    use chrono::{DateTime, Duration, Utc};
    let base: DateTime<Utc> = "2024-01-01T00:00:00Z"
        .parse()
        .expect("valid datetime literal");
    let ts = base + Duration::days(idx);
    quant_primitives::Candle::new(close, close, close, close, dec!(1000), ts).expect("valid candle")
}

fn flat_candles(price: rust_decimal::Decimal, n: usize) -> Vec<quant_primitives::Candle> {
    (0..n).map(|i| make_candle(price, i as i64)).collect()
}

#[test]
fn new_rejects_invalid_hma_period() {
    let err = DetrendedOscillator::new(0, 14);
    assert!(err.is_err(), "period=0 should be rejected");
}

#[test]
fn new_rejects_invalid_atr_period() {
    let err = DetrendedOscillator::new(21, 0);
    assert!(err.is_err(), "atr_period=0 should be rejected");
}

#[test]
fn insufficient_data_returns_error() {
    let osc = DetrendedOscillator::new(21, 14).expect("valid oscillator params");
    let candles = flat_candles(dec!(100), 10); // far fewer than warmup
    let result = osc.compute(&candles);
    assert!(
        matches!(
            result,
            Err(crate::error::IndicatorError::InsufficientData { .. })
        ),
        "Expected InsufficientData, got {:?}",
        result
    );
}

#[test]
fn flat_series_produces_zero_oscillator() {
    // On a perfectly flat series, close == HMA and ATR == 0 → oscillator = 0
    let osc = DetrendedOscillator::new(21, 14).expect("valid oscillator params");
    let candles = flat_candles(dec!(100), 80);
    let series = osc.compute(&candles).expect("Should compute on 80 candles");
    assert!(!series.is_empty(), "Series should not be empty");
    for (_, v) in series.values() {
        assert!(
            v.abs() < dec!(0.001),
            "Expected ~0 on flat series, got {}",
            v
        );
    }
}

#[test]
fn warmup_period_is_positive() {
    let osc = DetrendedOscillator::new(21, 14).expect("valid oscillator params");
    assert!(osc.warmup_period() > 0);
}

#[test]
fn warmup_period_at_least_hma_warmup() {
    use crate::hull::HullMa;
    let osc = DetrendedOscillator::new(21, 14).expect("valid oscillator params");
    let hma = HullMa::new(21).expect("valid HullMA period");
    assert!(osc.warmup_period() >= hma.warmup_period());
}

#[test]
fn warmup_period_at_least_atr_warmup() {
    use crate::atr::Atr;
    let osc = DetrendedOscillator::new(21, 14).expect("valid oscillator params");
    let atr = Atr::new(14).expect("valid ATR period");
    assert!(osc.warmup_period() >= atr.warmup_period());
}

#[test]
fn name_includes_both_periods() {
    let osc = DetrendedOscillator::new(21, 14).expect("valid oscillator params");
    assert!(osc.name().contains("21"), "Name should mention HMA period");
    assert!(osc.name().contains("14"), "Name should mention ATR period");
}

#[test]
fn series_length_consistent_with_candles() {
    let osc = DetrendedOscillator::new(10, 5).expect("valid oscillator params");
    let candles = flat_candles(dec!(100), 80);
    let series = osc
        .compute(&candles)
        .expect("sufficient data for oscillator");
    // Series length should be positive and bounded by candle count
    assert!(!series.is_empty());
    assert!(series.len() <= candles.len());
}