quant-metrics 0.7.0

Pure performance statistics library for trading — Sharpe, Sortino, drawdown, VaR, portfolio composition
Documentation
//! Unit tests for Kelly criterion computation.

use rust_decimal::Decimal;
use rust_decimal_macros::dec;

use super::*;

#[test]
fn full_kelly_standard_case() {
    // W=0.6, R=2.0 → f* = 0.6 - 0.4/2.0 = 0.4
    let frac = compute_kelly_fraction(dec!(0.6), dec!(2.0), KellyMode::Full);
    assert_eq!(frac.as_decimal(), dec!(0.4));
}

#[test]
fn half_kelly_halves_fraction() {
    let frac = compute_kelly_fraction(dec!(0.6), dec!(2.0), KellyMode::Half);
    assert_eq!(frac.as_decimal(), dec!(0.2));
}

#[test]
fn quarter_kelly_quarters_fraction() {
    let frac = compute_kelly_fraction(dec!(0.6), dec!(2.0), KellyMode::Quarter);
    assert_eq!(frac.as_decimal(), dec!(0.1));
}

#[test]
fn no_edge_returns_zero() {
    // W=0.3, R=1.0 → f* = 0.3 - 0.7/1.0 = -0.4 → clamped to 0
    let frac = compute_kelly_fraction(dec!(0.3), dec!(1.0), KellyMode::Full);
    assert!(frac.is_zero());
    assert_eq!(frac.as_decimal(), Decimal::ZERO);
}

#[test]
fn perfect_system_capped_at_one() {
    // W=1.0, R=5.0 → f* = 1.0 - 0.0/5.0 = 1.0
    let frac = compute_kelly_fraction(dec!(1.0), dec!(5.0), KellyMode::Full);
    assert_eq!(frac.as_decimal(), Decimal::ONE);
}

#[test]
fn zero_win_rate_returns_zero() {
    // W=0.0, R=1.0 → f* = 0.0 - 1.0/1.0 = -1.0 → clamped to 0
    let frac = compute_kelly_fraction(dec!(0.0), dec!(1.0), KellyMode::Full);
    assert!(frac.is_zero());
}

#[test]
fn zero_ratio_returns_zero() {
    let frac = compute_kelly_fraction(dec!(0.6), Decimal::ZERO, KellyMode::Full);
    assert!(frac.is_zero());
}

#[test]
fn negative_ratio_returns_zero() {
    let frac = compute_kelly_fraction(dec!(0.6), dec!(-1.0), KellyMode::Full);
    assert!(frac.is_zero());
}

#[test]
fn breakeven_system_returns_zero() {
    // W=0.5, R=1.0 → f* = 0.5 - 0.5/1.0 = 0.0
    let frac = compute_kelly_fraction(dec!(0.5), dec!(1.0), KellyMode::Full);
    assert!(frac.is_zero());
}

#[test]
fn kelly_mode_display() {
    assert_eq!(KellyMode::Full.to_string(), "Full");
    assert_eq!(KellyMode::Half.to_string(), "Half");
    assert_eq!(KellyMode::Quarter.to_string(), "Quarter");
}

#[test]
fn kelly_fraction_display() {
    let frac = compute_kelly_fraction(dec!(0.6), dec!(2.0), KellyMode::Full);
    let display = format!("{frac}");
    assert!(
        display.contains("0.4"),
        "Display should show 0.4, got: {display}"
    );
}

#[test]
fn inputs_from_pnls() {
    // 3 wins (100, 75, 80), 2 losses (-50, -25)
    let pnls = [dec!(100), dec!(-50), dec!(75), dec!(-25), dec!(80)];
    let (wr, ratio, count) = compute_kelly_inputs(&pnls).expect("should succeed");

    // win_rate from trading.rs returns 60 (percentage), we expect fraction 0.6
    assert_eq!(wr, dec!(0.6), "win_rate should be fraction, not percentage");
    assert_eq!(count, 5);

    // avg_win = (100+75+80)/3 = 85, avg_loss = (-50+-25)/2 = -37.5, abs = 37.5
    // ratio = 85 / 37.5 = 2.2666...
    let expected_ratio = dec!(85) / dec!(37.5);
    assert_eq!(ratio, expected_ratio);
}

#[test]
fn inputs_empty_pnls_errors() {
    let result = compute_kelly_inputs(&[]);
    assert_eq!(
        result,
        Err(crate::MetricsError::InsufficientData {
            required: 1,
            actual: 0,
        })
    );
}

#[test]
fn inputs_all_wins_errors() {
    // No losses → can't compute ratio → DivisionByZero
    let result = compute_kelly_inputs(&[dec!(100), dec!(200)]);
    assert!(
        matches!(result, Err(crate::MetricsError::DivisionByZero { .. })),
        "expected DivisionByZero, got: {result:?}"
    );
}

#[test]
fn inputs_all_losses_errors() {
    // No wins → avg_win errors → InsufficientData
    let result = compute_kelly_inputs(&[dec!(-100), dec!(-200)]);
    assert!(
        matches!(result, Err(crate::MetricsError::InsufficientData { .. })),
        "expected InsufficientData, got: {result:?}"
    );
}