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 bollinger_constant_prices_zero_width() {
    let candles: Vec<Candle> = (0..10).map(|i| make_candle(dec!(100), ts(i))).collect();

    let bb = BollingerBands::new(5, dec!(2)).expect("valid BB params");
    let result = bb.compute(&candles).expect("sufficient data for BB");

    assert_eq!(result.len(), 6);

    // All bands should equal 100 when prices are constant
    for (_, v) in result.middle.values() {
        assert_eq!(*v, dec!(100));
    }
    for (_, v) in result.upper.values() {
        assert_eq!(*v, dec!(100));
    }
    for (_, v) in result.lower.values() {
        assert_eq!(*v, dec!(100));
    }
}

#[test]
fn bollinger_basic() {
    let candles: Vec<Candle> = vec![
        make_candle(dec!(20), ts(0)),
        make_candle(dec!(22), ts(1)),
        make_candle(dec!(21), ts(2)),
        make_candle(dec!(23), ts(3)),
        make_candle(dec!(22), ts(4)),
    ];

    let bb = BollingerBands::new(3, dec!(2)).expect("valid BB params");
    let result = bb.compute(&candles).expect("sufficient data for BB");

    assert_eq!(result.len(), 3);

    // Middle should be SMA
    // Upper should be > middle
    // Lower should be < middle
    for i in 0..result.len() {
        let upper = result.upper.get(i).expect("upper band value exists").1;
        let middle = result.middle.get(i).expect("middle band value exists").1;
        let lower = result.lower.get(i).expect("lower band value exists").1;

        assert!(upper >= middle, "upper {} >= middle {}", upper, middle);
        assert!(lower <= middle, "lower {} <= middle {}", lower, middle);
    }
}

#[test]
fn bollinger_symmetric_bands() {
    // With non-zero volatility, bands should be symmetric around middle
    let candles: Vec<Candle> = vec![
        make_candle(dec!(10), ts(0)),
        make_candle(dec!(12), ts(1)),
        make_candle(dec!(14), ts(2)),
        make_candle(dec!(11), ts(3)),
        make_candle(dec!(13), ts(4)),
    ];

    let bb = BollingerBands::new(3, dec!(2)).expect("valid BB params");
    let result = bb.compute(&candles).expect("sufficient data for BB");

    for i in 0..result.len() {
        let upper = result.upper.get(i).expect("upper band value exists").1;
        let middle = result.middle.get(i).expect("middle band value exists").1;
        let lower = result.lower.get(i).expect("lower band value exists").1;

        let upper_diff = upper - middle;
        let lower_diff = middle - lower;

        assert_eq!(
            upper_diff, lower_diff,
            "Bands should be symmetric: upper_diff {} != lower_diff {}",
            upper_diff, lower_diff
        );
    }
}

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

    let bb = BollingerBands::new(5, dec!(2)).expect("valid BB params");
    let result = bb.compute(&candles);

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

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

#[test]
fn bollinger_negative_multiplier() {
    let result = BollingerBands::new(20, dec!(-1));
    assert!(matches!(
        result,
        Err(IndicatorError::InvalidParameter { .. })
    ));
}

#[test]
fn bollinger_name() {
    let bb = BollingerBands::new(20, dec!(2)).expect("valid BB params");
    assert_eq!(bb.name(), "BB(20,2)");
}

#[test]
fn bollinger_warmup() {
    let bb = BollingerBands::new(20, dec!(2)).expect("valid BB params");
    assert_eq!(bb.warmup_period(), 20);
}

#[test]
fn bollinger_standard() {
    let bb = BollingerBands::standard().expect("valid BB standard params");
    assert_eq!(bb.warmup_period(), 20);
}

#[test]
fn bollinger_zero_multiplier() {
    // With multiplier 0, all bands equal middle
    let candles: Vec<Candle> = vec![
        make_candle(dec!(10), ts(0)),
        make_candle(dec!(20), ts(1)),
        make_candle(dec!(15), ts(2)),
    ];

    let bb = BollingerBands::new(3, dec!(0)).expect("valid BB params");
    let result = bb.compute(&candles).expect("sufficient data for BB");

    for i in 0..result.len() {
        let upper = result.upper.get(i).expect("upper band value exists").1;
        let middle = result.middle.get(i).expect("middle band value exists").1;
        let lower = result.lower.get(i).expect("lower band value exists").1;

        assert_eq!(upper, middle);
        assert_eq!(lower, middle);
    }
}