quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
use super::*;
use crate::sma::Sma;
use crate::test_helpers::helpers::{make_candle, ts};
use rust_decimal_macros::dec;

// ============ Diff tests ============

#[test]
fn diff_basic() {
    // Uptrend: prices [100, 102, 104, 106, 108, 110, 112, 114, 116, 118]
    // SMA(3) moves faster than SMA(5) in uptrend
    // At each aligned point, SMA(3) - SMA(5) = 2
    let candles: Vec<Candle> = (0..10)
        .map(|i| make_candle(Decimal::from(100 + i * 2), ts(i)))
        .collect();

    let sma3 = Sma::new(3).expect("period 3 is valid for SMA");
    let sma5 = Sma::new(5).expect("period 5 is valid for SMA");
    let diff = Diff::new(sma3, sma5);

    let series = diff
        .compute(&candles)
        .expect("compute Diff on valid candles");

    // Series aligned from end: 6 values where diff = 2
    assert_eq!(series.len(), 6);
    for (_, v) in series.values() {
        assert_eq!(*v, dec!(2), "Diff should be exactly 2 in linear uptrend");
    }
}

#[test]
fn diff_flat_zero() {
    let candles: Vec<Candle> = (0..10).map(|i| make_candle(dec!(100), ts(i))).collect();

    let sma3 = Sma::new(3).expect("period 3 is valid for SMA");
    let sma5 = Sma::new(5).expect("period 5 is valid for SMA");
    let diff = Diff::new(sma3, sma5);

    let series = diff
        .compute(&candles)
        .expect("compute Diff on flat candles");

    for (_, v) in series.values() {
        assert_eq!(*v, Decimal::ZERO);
    }
}

#[test]
fn diff_name() {
    let sma3 = Sma::new(3).expect("period 3 is valid for SMA");
    let sma5 = Sma::new(5).expect("period 5 is valid for SMA");
    let diff = Diff::new(sma3, sma5);
    assert_eq!(diff.name(), "Diff(SMA(3),SMA(5))");
}

#[test]
fn diff_warmup() {
    let sma3 = Sma::new(3).expect("period 3 is valid for SMA");
    let sma5 = Sma::new(5).expect("period 5 is valid for SMA");
    let diff = Diff::new(sma3, sma5);
    assert_eq!(diff.warmup_period(), 5); // max of 3 and 5
}

// ============ Ratio tests ============

#[test]
fn ratio_equal_indicators() {
    let candles: Vec<Candle> = (0..5).map(|i| make_candle(dec!(100), ts(i))).collect();

    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let sma3 = Sma::new(3).expect("period 3 is valid for SMA");
    let ratio = Ratio::new(sma2, sma3);

    let series = ratio
        .compute(&candles)
        .expect("compute Ratio on flat candles");

    // Both SMAs equal 100, so ratio should be 1
    for (_, v) in series.values() {
        assert_eq!(*v, Decimal::ONE, "Expected 1, got {}", v);
    }
}

/// Test indicator that always returns zero values (for testing div-by-zero)
#[derive(Debug, Clone)]
struct ZeroIndicator;

impl Indicator for ZeroIndicator {
    fn name(&self) -> &str {
        "Zero"
    }
    fn warmup_period(&self) -> usize {
        1
    }
    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
        let values: Vec<_> = candles
            .iter()
            .map(|c| (c.timestamp(), Decimal::ZERO))
            .collect();
        Ok(Series::new(values))
    }
}

#[test]
fn ratio_division_by_zero() {
    // Use ZeroIndicator as denominator to actually test div-by-zero handling
    let candles: Vec<Candle> = (0..5).map(|i| make_candle(dec!(100), ts(i))).collect();

    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let ratio = Ratio::new(sma2, ZeroIndicator);

    let series = ratio
        .compute(&candles)
        .expect("compute Ratio with zero denominator");

    // Division by zero should return 0, not panic
    for (_, v) in series.values() {
        assert_eq!(*v, Decimal::ZERO, "Division by zero should return 0");
    }
}

#[test]
fn ratio_name() {
    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let sma3 = Sma::new(3).expect("period 3 is valid for SMA");
    let ratio = Ratio::new(sma2, sma3);
    assert_eq!(ratio.name(), "Ratio(SMA(2),SMA(3))");
}

// ============ Lag tests ============

#[test]
fn lag_basic() {
    // SMA(2) of [100, 110, 120, 130, 140] = [105, 115, 125, 135]
    // Lag(1) should give [105, 115, 125] with timestamps shifted
    let candles: Vec<Candle> = vec![
        make_candle(dec!(100), ts(0)),
        make_candle(dec!(110), ts(1)),
        make_candle(dec!(120), ts(2)),
        make_candle(dec!(130), ts(3)),
        make_candle(dec!(140), ts(4)),
    ];

    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let lag = Lag::new(sma2, 1).expect("lag period 1 is valid");

    let series = lag.compute(&candles).expect("compute Lag on valid candles");
    let values = series.decimal_values();

    assert_eq!(values.len(), 3);
    assert_eq!(values[0], dec!(105)); // First lagged value
}

#[test]
fn lag_period_zero() {
    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let result = Lag::new(sma2, 0);
    assert!(matches!(
        result,
        Err(IndicatorError::InvalidParameter { .. })
    ));
}

#[test]
fn lag_name() {
    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let lag = Lag::new(sma2, 3).expect("lag period 3 is valid");
    assert_eq!(lag.name(), "Lag(SMA(2),3)");
}

#[test]
fn lag_warmup() {
    let sma5 = Sma::new(5).expect("period 5 is valid for SMA");
    let lag = Lag::new(sma5, 3).expect("lag period 3 is valid");
    assert_eq!(lag.warmup_period(), 8); // 5 + 3
}

// ============ Scale tests ============

#[test]
fn scale_basic() {
    let candles: Vec<Candle> = (0..5).map(|i| make_candle(dec!(100), ts(i))).collect();

    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let scale = Scale::new(sma2, dec!(2));

    let series = scale
        .compute(&candles)
        .expect("compute Scale on valid candles");

    // SMA is 100, scaled by 2 = 200
    for (_, v) in series.values() {
        assert_eq!(*v, dec!(200));
    }
}

#[test]
fn scale_fractional() {
    let candles: Vec<Candle> = (0..5).map(|i| make_candle(dec!(100), ts(i))).collect();

    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let scale = Scale::new(sma2, dec!(0.5));

    let series = scale
        .compute(&candles)
        .expect("compute Scale with fractional factor");

    for (_, v) in series.values() {
        assert_eq!(*v, dec!(50));
    }
}

#[test]
fn scale_name() {
    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let scale = Scale::new(sma2, dec!(2.5));
    assert_eq!(scale.name(), "Scale(SMA(2),2.5)");
}

#[test]
fn scale_warmup() {
    let sma5 = Sma::new(5).expect("period 5 is valid for SMA");
    let scale = Scale::new(sma5, dec!(2));
    assert_eq!(scale.warmup_period(), 5); // Same as inner
}

// ============ Composition tests ============

#[test]
fn scale_of_diff() {
    let candles: Vec<Candle> = (0..10).map(|i| make_candle(dec!(100), ts(i))).collect();

    let sma2 = Sma::new(2).expect("period 2 is valid for SMA");
    let sma3 = Sma::new(3).expect("period 3 is valid for SMA");
    let diff = Diff::new(sma2, sma3);
    let scaled = Scale::new(diff, dec!(0.5));

    let series = scaled
        .compute(&candles)
        .expect("compute Scale(Diff) on flat candles");

    // Flat prices => diff is 0, scaled is 0
    for (_, v) in series.values() {
        assert_eq!(*v, Decimal::ZERO);
    }
}