quant-metrics 0.7.0

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

/// Generate a random walk (non-stationary) series.
fn random_walk(n: usize, start: f64, drift: f64, noise: f64, seed: u64) -> Vec<f64> {
    let mut prices = Vec::with_capacity(n);
    let mut price = start;
    let mut state = seed;
    for _ in 0..n {
        // Simple LCG for deterministic "random" noise
        state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
        let u = (state >> 33) as f64 / (1u64 << 31) as f64; // [0, 1)
        let normal_approx = (u - 0.5) * 2.0 * noise;
        price += drift + normal_approx;
        prices.push(price);
    }
    prices
}

/// Generate a cointegrated pair: y = alpha + beta * x + mean-reverting spread.
fn cointegrated_pair(x: &[f64], beta: f64, alpha: f64, spread_noise: f64, seed: u64) -> Vec<f64> {
    let mut y = Vec::with_capacity(x.len());
    let mut spread = 0.0;
    let mut state = seed;
    let mean_reversion_speed = 0.1; // fast mean reversion

    for xi in x {
        state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
        let u = (state >> 33) as f64 / (1u64 << 31) as f64;
        let shock = (u - 0.5) * 2.0 * spread_noise;
        spread = spread * (1.0 - mean_reversion_speed) + shock;
        y.push(alpha + beta * xi + spread);
    }
    y
}

#[test]
fn cointegrated_pair_has_low_p_value() {
    let x = random_walk(500, 100.0, 0.01, 1.0, 42);
    let y = cointegrated_pair(&x, 1.5, 10.0, 0.5, 99);

    let result = engle_granger(&x, &y).expect("should succeed");

    assert!(
        result.p_value < 0.05,
        "cointegrated pair should have p < 0.05, got {}",
        result.p_value
    );
    assert!(
        result.half_life.is_some(),
        "cointegrated pair should have a finite half-life"
    );
    let hl = result
        .half_life
        .expect("cointegrated pair must have a finite half-life");
    assert!(
        hl > 0.0 && hl < 100.0,
        "half-life should be reasonable, got {}",
        hl
    );
    assert!(
        result.correlation.abs() > 0.5,
        "cointegrated pair should have high correlation, got {}",
        result.correlation
    );
}

#[test]
fn independent_series_have_high_p_value() {
    let x = random_walk(500, 100.0, 0.02, 1.0, 1);
    let y = random_walk(500, 200.0, -0.01, 2.0, 99999);

    let result = engle_granger(&x, &y).expect("should succeed");

    assert!(
        result.p_value > 0.10,
        "independent series should have p > 0.10, got {}",
        result.p_value
    );
}

#[test]
fn insufficient_data_returns_error() {
    let x = vec![1.0, 2.0, 3.0];
    let y = vec![2.0, 4.0, 6.0];

    let result = engle_granger(&x, &y);
    assert!(result.is_err());
    assert!(matches!(
        result.unwrap_err(),
        CointegrationMathError::InsufficientData { .. }
    ));
}

#[test]
fn length_mismatch_returns_error() {
    let x = vec![1.0; 50];
    let y = vec![1.0; 40];

    let result = engle_granger(&x, &y);
    assert!(result.is_err());
    assert!(matches!(
        result.unwrap_err(),
        CointegrationMathError::LengthMismatch { .. }
    ));
}

#[test]
fn ols_regression_recovers_known_relationship() {
    // y = 5.0 + 2.0 * x
    let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
    let y: Vec<f64> = x.iter().map(|xi| 5.0 + 2.0 * xi).collect();

    let (alpha, beta) = ols_regression(&x, &y).expect("should succeed");

    assert!(
        (alpha - 5.0).abs() < 0.01,
        "alpha should be ~5.0, got {}",
        alpha
    );
    assert!(
        (beta - 2.0).abs() < 0.01,
        "beta should be ~2.0, got {}",
        beta
    );
}

#[test]
fn perfect_correlation_returns_one() {
    let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
    let y: Vec<f64> = x.iter().map(|xi| 3.0 * xi + 10.0).collect();

    let corr = pearson_correlation(&x, &y).expect("should succeed");
    assert!(
        (corr - 1.0).abs() < 0.001,
        "perfect linear relationship should have corr ≈ 1.0, got {}",
        corr
    );
}

#[test]
fn spread_stats_to_decimal_converts() {
    let (m, s) = spread_stats_to_decimal(1.5, 0.25);
    assert!(m > Decimal::ZERO);
    assert!(s > Decimal::ZERO);
}