quant-indicators 0.7.0

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

#[test]
fn ema_multiplier() {
    let ema = Ema::new(9).expect("valid EMA period");
    // multiplier = 2 / (9 + 1) = 0.2
    assert_eq!(ema.multiplier(), dec!(0.2));

    let ema = Ema::new(19).expect("valid EMA period");
    // multiplier = 2 / (19 + 1) = 0.1
    assert_eq!(ema.multiplier(), dec!(0.1));
}

#[test]
fn ema_first_value_is_sma() {
    let candles: Vec<Candle> = vec![
        make_candle(dec!(100), ts(0)),
        make_candle(dec!(102), ts(1)),
        make_candle(dec!(104), ts(2)),
    ];

    let ema = Ema::new(3).expect("valid EMA period");
    let series = ema.compute(&candles).expect("sufficient data for EMA");

    // First EMA should be SMA(3) = (100+102+104)/3 = 102
    let values = series.decimal_values();
    assert_eq!(values[0], dec!(102));
}

#[test]
fn ema_reacts_to_changes() {
    let candles: Vec<Candle> = vec![
        make_candle(dec!(100), ts(0)),
        make_candle(dec!(100), ts(1)),
        make_candle(dec!(100), ts(2)),
        make_candle(dec!(120), ts(3)), // Price jump
    ];

    let ema = Ema::new(3).expect("valid EMA period");
    let series = ema.compute(&candles).expect("sufficient data for EMA");
    let values = series.decimal_values();

    // First value: SMA = 100
    assert_eq!(values[0], dec!(100));

    // Second value after price jumps to 120:
    // multiplier = 2/4 = 0.5
    // EMA = (120 - 100) * 0.5 + 100 = 110
    assert_eq!(values[1], dec!(110));
}

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

    let ema = Ema::new(5).expect("valid EMA period");
    let series = ema.compute(&candles).expect("sufficient data for EMA");

    // All values should be 100 (or very close) for constant input
    for (_, value) in series.values() {
        assert_eq!(*value, dec!(100));
    }
}

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

    let ema = Ema::new(3).expect("valid EMA period");
    let series = ema.compute(&candles).expect("sufficient data for EMA");

    // Should produce 10 - 3 + 1 = 8 values
    assert_eq!(series.len(), 8);
}

#[test]
fn ema_insufficient_data() {
    let candles: Vec<Candle> = vec![make_candle(dec!(100), ts(0)), make_candle(dec!(102), ts(1))];

    let ema = Ema::new(3).expect("valid EMA period");
    let result = ema.compute(&candles);

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

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

#[test]
fn ema_name() {
    let ema = Ema::new(20).expect("valid EMA period");
    assert_eq!(ema.name(), "EMA(20)");
}

#[test]
fn ema_warmup_period() {
    let ema = Ema::new(14).expect("valid EMA period");
    assert_eq!(ema.warmup_period(), 14);
}

#[test]
fn ema_faster_than_sma() {
    use crate::sma::Sma;

    // Price: 100, 100, 100, 120, 120, 120
    let candles: Vec<Candle> = vec![
        make_candle(dec!(100), ts(0)),
        make_candle(dec!(100), ts(1)),
        make_candle(dec!(100), ts(2)),
        make_candle(dec!(120), ts(3)),
        make_candle(dec!(120), ts(4)),
        make_candle(dec!(120), ts(5)),
    ];

    let ema = Ema::new(3).expect("valid EMA period");
    let sma = Sma::new(3).expect("valid SMA period");

    let ema_series = ema.compute(&candles).expect("sufficient data for EMA");
    let sma_series = sma.compute(&candles).expect("sufficient data for SMA");

    let ema_values = ema_series.decimal_values();
    let sma_values = sma_series.decimal_values();

    // At index 1 (second output), after price jump:
    // EMA should be closer to 120 than SMA
    let target = dec!(120);
    let ema_dist = (target - ema_values[1]).abs();
    let sma_dist = (target - sma_values[1]).abs();

    assert!(
        ema_dist < sma_dist,
        "EMA ({}) should be closer to 120 than SMA ({})",
        ema_values[1],
        sma_values[1]
    );
}