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 sharpe_positive_returns() {
    // Consistently positive returns should give positive Sharpe
    let equity = vec![
        dec!(100),
        dec!(101),
        dec!(102),
        dec!(103),
        dec!(104),
        dec!(105),
    ];
    let sharpe = sharpe_ratio(&equity, dec!(0.02), 252).expect("sharpe with positive returns");
    assert!(sharpe > dec!(0));
}

#[test]
fn sharpe_volatile_returns() {
    // More volatile returns = lower Sharpe
    let stable = vec![dec!(100), dec!(101), dec!(102), dec!(103), dec!(104)];
    let volatile = vec![dec!(100), dec!(105), dec!(98), dec!(108), dec!(104)];

    let sharpe_stable = sharpe_ratio(&stable, dec!(0.02), 252).expect("sharpe for stable returns");
    let sharpe_volatile =
        sharpe_ratio(&volatile, dec!(0.02), 252).expect("sharpe for volatile returns");

    assert!(sharpe_stable > sharpe_volatile);
}

#[test]
fn sharpe_insufficient_data() {
    let equity = vec![dec!(100), dec!(101)];
    assert_eq!(
        sharpe_ratio(&equity, dec!(0.02), 252),
        Err(MetricsError::InsufficientData {
            required: 3,
            actual: 2
        })
    );
}

#[test]
fn sortino_better_than_sharpe_for_upside_vol() {
    // Returns with high upside volatility but low downside
    // Sortino should be higher than Sharpe
    let equity = vec![
        dec!(100),
        dec!(102), // +2%
        dec!(101), // -1% (small downside)
        dec!(108), // +7% (high upside)
        dec!(107), // -1% (small downside)
        dec!(115), // +7% (high upside)
    ];

    let sharpe = sharpe_ratio(&equity, dec!(0.02), 252).expect("sharpe for upside-vol equity");
    let sortino = sortino_ratio(&equity, dec!(0.02), 252).expect("sortino for upside-vol equity");

    // Sortino ignores upside vol, so should be higher
    assert!(sortino > sharpe);
}

#[test]
fn calmar_positive() {
    // Positive returns with some drawdown
    let equity = vec![dec!(100), dec!(110), dec!(95), dec!(120)];
    let calmar = calmar_ratio(&equity, 252).expect("calmar with drawdown");
    assert!(calmar > dec!(0));
}

#[test]
fn calmar_no_drawdown() {
    let equity = vec![dec!(100), dec!(110), dec!(120)];
    assert_eq!(
        calmar_ratio(&equity, 252),
        Err(MetricsError::DivisionByZero {
            context: "zero max drawdown"
        })
    );
}

#[test]
fn period_returns_calculation() {
    let equity = vec![dec!(100), dec!(110), dec!(99)];
    let returns = period_returns(&equity);

    assert_eq!(returns.len(), 2);
    assert_eq!(returns[0], dec!(0.10)); // 10%
    assert_eq!(returns[1], dec!(-0.10)); // -10%
}

#[test]
fn information_ratio_outperformance() {
    // Portfolio outperforms benchmark consistently
    let portfolio = vec![dec!(100), dec!(110), dec!(121), dec!(133)];
    let benchmark = vec![dec!(100), dec!(105), dec!(110), dec!(115)];

    let ir =
        information_ratio(&portfolio, &benchmark, 252).expect("IR for outperforming portfolio");
    assert!(ir > dec!(0)); // Positive IR = outperformance
}

#[test]
fn information_ratio_underperformance() {
    // Portfolio underperforms benchmark
    let portfolio = vec![dec!(100), dec!(105), dec!(110), dec!(115)];
    let benchmark = vec![dec!(100), dec!(110), dec!(121), dec!(133)];

    let ir =
        information_ratio(&portfolio, &benchmark, 252).expect("IR for underperforming portfolio");
    assert!(ir < dec!(0)); // Negative IR = underperformance
}

#[test]
fn information_ratio_length_mismatch() {
    let portfolio = vec![dec!(100), dec!(110), dec!(121)];
    let benchmark = vec![dec!(100), dec!(105)];

    assert!(matches!(
        information_ratio(&portfolio, &benchmark, 252),
        Err(MetricsError::InvalidParameter(_))
    ));
}