quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
use super::*;
use quant_primitives::Candle;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;

fn make_candles(count: usize, step: Decimal) -> Vec<Candle> {
    let ts = chrono::Utc::now();
    (0..count)
        .map(|i| {
            let price = Decimal::from(100) + step * Decimal::from(i);
            Candle::new(
                price,
                price + dec!(1),
                price - dec!(1),
                price,
                dec!(1000),
                ts,
            )
            .expect("valid candle")
        })
        .collect()
}

// --- compute_log_returns ---

#[test]
fn log_returns_empty_input() {
    assert!(compute_log_returns(&[]).is_empty());
}

#[test]
fn log_returns_single_price() {
    assert!(compute_log_returns(&[dec!(100)]).is_empty());
}

#[test]
fn log_returns_basic() {
    let closes = vec![dec!(100), dec!(110), dec!(121)];
    let returns = compute_log_returns(&closes);
    assert_eq!(returns.len(), 2);
    assert_eq!(returns[0], dec!(0.1)); // (110-100)/100
    assert_eq!(returns[1], dec!(0.1)); // (121-110)/110
}

#[test]
fn log_returns_skips_zero_denominator() {
    let closes = vec![dec!(0), dec!(100), dec!(110)];
    let returns = compute_log_returns(&closes);
    assert_eq!(returns.len(), 1); // first pair skipped
    assert_eq!(returns[0], dec!(0.1));
}

// --- rescaled_range_for_n ---

#[test]
fn rescaled_range_none_when_n_exceeds_data() {
    let returns = vec![dec!(0.01); 10];
    assert!(rescaled_range_for_n(&returns, 20).is_none());
}

#[test]
fn rescaled_range_none_for_zero_n() {
    let returns = vec![dec!(0.01); 10];
    assert!(rescaled_range_for_n(&returns, 0).is_none());
}

#[test]
fn rescaled_range_produces_positive_value() {
    // 32 alternating returns — should produce a valid R/S
    let returns: Vec<Decimal> = (0..32)
        .map(|i| if i % 2 == 0 { dec!(0.02) } else { dec!(-0.01) })
        .collect();
    let rs = rescaled_range_for_n(&returns, 16);
    assert!(rs.is_some());
    assert!(rs.expect("rs") > Decimal::ZERO);
}

#[test]
fn rescaled_range_none_for_constant_returns() {
    // All identical returns → variance = 0 → no valid subseries
    let returns = vec![dec!(0.05); 32];
    assert!(rescaled_range_for_n(&returns, 16).is_none());
}

// --- estimate_slope ---

#[test]
fn estimate_slope_none_with_single_point() {
    assert!(estimate_slope(&[16], &[dec!(2)]).is_none());
}

#[test]
fn estimate_slope_none_with_empty() {
    assert!(estimate_slope(&[], &[]).is_none());
}

#[test]
fn estimate_slope_returns_value_in_0_1() {
    // Two points: if RS doubles when n quadruples, H = log2(2)/log2(4) = 0.5
    let h = estimate_slope(&[16, 64], &[dec!(2), dec!(4)]);
    assert!(h.is_some());
    let val = h.expect("slope");
    assert!(
        val >= Decimal::ZERO && val <= Decimal::ONE,
        "H={val} outside [0,1]"
    );
}

#[test]
fn estimate_slope_clamps_to_unit_interval() {
    // RS ratio >> n ratio would give H > 1 — should clamp
    let h = estimate_slope(&[16, 32], &[dec!(1), dec!(100)]);
    assert!(h.is_some());
    let val = h.expect("slope");
    assert!(val <= Decimal::ONE, "H={val} should be clamped to 1");
}

// --- HurstExponent (integration) ---

#[test]
fn hurst_new_rejects_small_window() {
    assert!(HurstExponent::new(32).is_err());
}

#[test]
fn hurst_returns_none_for_short_series() {
    let hurst = HurstExponent::new(128).expect("valid");
    let candles = make_candles(50, dec!(0.5));
    assert!(hurst.compute_from_candles(&candles).is_none());
}

#[test]
fn hurst_in_valid_range() {
    let hurst = HurstExponent::new(128).expect("valid");
    let candles = make_candles(200, dec!(0.5));
    let result = hurst.compute_from_candles(&candles);
    assert!(result.is_some(), "200 candles > 128 window → Some");
    let h = result.expect("hurst");
    assert!(
        h >= Decimal::ZERO && h <= Decimal::ONE,
        "Hurst {h} should be in [0, 1]"
    );
}