quant-metrics 0.7.0

Pure performance statistics library for trading — Sharpe, Sortino, drawdown, VaR, portfolio composition
Documentation
use super::*;
use rust_decimal_macros::dec;

#[test]
fn max_drawdown_simple() {
    // Peak at 110, trough at 90
    // DD = (90 - 110) / 110 * 100 = -18.18%
    let equity = vec![dec!(100), dec!(110), dec!(90), dec!(105)];
    let dd = max_drawdown(&equity).expect("max_drawdown should succeed with valid equity curve");

    // -18.18... %
    assert!(dd < dec!(-18));
    assert!(dd > dec!(-19));
}

#[test]
fn max_drawdown_no_drawdown() {
    // Monotonically increasing
    let equity = vec![dec!(100), dec!(110), dec!(120), dec!(130)];
    let dd = max_drawdown(&equity)
        .expect("max_drawdown should succeed for monotonically increasing equity");
    assert_eq!(dd, dec!(0));
}

#[test]
fn max_drawdown_multiple_drawdowns() {
    // First DD: 100 -> 90 = -10%
    // Second DD: 120 -> 80 = -33.33%
    let equity = vec![dec!(100), dec!(90), dec!(120), dec!(80), dec!(100)];
    let dd = max_drawdown(&equity).expect("max_drawdown should succeed with multiple drawdowns");

    // Should be approximately -33.33%
    assert!(dd < dec!(-33));
    assert!(dd > dec!(-34));
}

#[test]
fn max_drawdown_insufficient_data() {
    let equity = vec![dec!(100)];
    assert_eq!(
        max_drawdown(&equity),
        Err(MetricsError::InsufficientData {
            required: 2,
            actual: 1
        })
    );
}

#[test]
fn drawdown_series_tracks_peak() {
    let equity = vec![dec!(100), dec!(110), dec!(100), dec!(120), dec!(110)];
    let dd =
        drawdown_series(&equity).expect("drawdown_series should succeed with valid equity curve");

    assert_eq!(dd[0], dec!(0)); // At first peak
    assert_eq!(dd[1], dec!(0)); // New peak
    assert!(dd[2] < dec!(0)); // Below peak of 110
    assert_eq!(dd[3], dec!(0)); // New peak at 120
    assert!(dd[4] < dec!(0)); // Below peak of 120
}

#[test]
fn max_drawdown_duration_simple() {
    // 0: peak 100
    // 1: DD starts (90)
    // 2: still in DD (95)
    // 3: recovery (105) - duration was 2
    let equity = vec![dec!(100), dec!(90), dec!(95), dec!(105)];
    let duration = max_drawdown_duration(&equity)
        .expect("max_drawdown_duration should succeed with simple drawdown");
    assert_eq!(duration, 2);
}

#[test]
fn max_drawdown_duration_not_recovered() {
    // 0: peak 100
    // 1-4: in drawdown, never recovers
    let equity = vec![dec!(100), dec!(90), dec!(85), dec!(88), dec!(87)];
    let duration = max_drawdown_duration(&equity)
        .expect("max_drawdown_duration should succeed for unrecovered drawdown");
    assert_eq!(duration, 4);
}

#[test]
fn max_drawdown_duration_multiple() {
    // First DD: indices 1-2 (duration 2)
    // Second DD: indices 4-7 (duration 4) - peak at 3, still in DD at 7
    let equity = vec![
        dec!(100),
        dec!(90),
        dec!(95),
        dec!(110), // recovery, new peak at index 3
        dec!(100), // index 4, DD starts
        dec!(95),  // index 5
        dec!(98),  // index 6
        dec!(105), // index 7, still below 110 peak
    ];
    let duration = max_drawdown_duration(&equity)
        .expect("max_drawdown_duration should succeed with multiple drawdown periods");
    assert_eq!(duration, 4); // indices 4,5,6,7 - 4 periods in DD
}

#[test]
fn max_drawdown_duration_no_drawdown() {
    let equity = vec![dec!(100), dec!(110), dec!(120)];
    let duration = max_drawdown_duration(&equity)
        .expect("max_drawdown_duration should succeed with no drawdown");
    assert_eq!(duration, 0);
}

#[test]
fn recovery_time_recovered() {
    // DD at index 2 (90), recovery at index 4 (110)
    let equity = vec![dec!(100), dec!(110), dec!(90), dec!(100), dec!(115)];
    let recovery =
        recovery_time(&equity).expect("recovery_time should succeed for recovered drawdown");
    assert_eq!(recovery, Some(2)); // 2 periods from trough to recovery
}

#[test]
fn recovery_time_not_recovered() {
    // Peak 110, never recovers
    let equity = vec![dec!(100), dec!(110), dec!(90), dec!(95), dec!(100)];
    let recovery =
        recovery_time(&equity).expect("recovery_time should succeed for unrecovered drawdown");
    assert_eq!(recovery, None);
}

#[test]
fn recovery_time_no_drawdown() {
    let equity = vec![dec!(100), dec!(110), dec!(120)];
    let recovery = recovery_time(&equity).expect("recovery_time should succeed with no drawdown");
    assert_eq!(recovery, Some(0));
}