quant-metrics 0.7.0

Pure performance statistics library for trading — Sharpe, Sortino, drawdown, VaR, portfolio composition
Documentation
//! Kelly criterion position sizing.
//!
//! Pure computation — `f* = W - (1-W)/R` where:
//! - W = win rate (fraction of winning trades)
//! - R = average win / average loss ratio
//!
//! The Kelly fraction is the theoretically optimal fraction of capital to
//! risk on each trade. Half-Kelly and quarter-Kelly are conservative
//! variants widely used in practice.

use std::fmt;

use rust_decimal::Decimal;

use crate::MetricsError;

/// Kelly sizing mode — controls what fraction of the theoretical optimum to use.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KellyMode {
    /// Full Kelly — theoretical optimum. Aggressive.
    Full,
    /// Half Kelly — recommended for live trading. Halves variance.
    Half,
    /// Quarter Kelly — conservative.
    Quarter,
}

impl KellyMode {
    /// Scaling factor for this mode.
    pub fn scale(&self) -> Decimal {
        match self {
            Self::Full => Decimal::ONE,
            Self::Half => Decimal::new(5, 1),     // 0.5
            Self::Quarter => Decimal::new(25, 2), // 0.25
        }
    }
}

impl fmt::Display for KellyMode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Full => write!(f, "Full"),
            Self::Half => write!(f, "Half"),
            Self::Quarter => write!(f, "Quarter"),
        }
    }
}

/// A validated Kelly fraction in the range [0.0, 1.0].
///
/// Represents the fraction of available capital to allocate to a trade.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct KellyFraction(Decimal);

impl KellyFraction {
    /// The zero fraction — no allocation.
    pub fn zero() -> Self {
        Self(Decimal::ZERO)
    }

    /// The raw fraction value in [0.0, 1.0].
    pub fn as_decimal(&self) -> Decimal {
        self.0
    }

    /// Whether this fraction is zero (system has no edge).
    pub fn is_zero(&self) -> bool {
        self.0 == Decimal::ZERO
    }
}

impl fmt::Display for KellyFraction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.4}", self.0)
    }
}

/// Compute the Kelly fraction for given trade statistics.
///
/// Formula: `f* = W - (1-W)/R`, then scaled by mode, clamped to [0, 1].
///
/// # Arguments
/// * `win_rate` — fraction of winning trades (0.0 to 1.0)
/// * `avg_win_loss_ratio` — average win / average loss (the R in Kelly)
/// * `mode` — Full, Half, or Quarter Kelly
///
/// # Returns
/// A `KellyFraction` clamped to [0.0, 1.0]. If the system has no edge
/// (negative raw Kelly), returns `KellyFraction::zero()`.
pub fn compute_kelly_fraction(
    win_rate: Decimal,
    avg_win_loss_ratio: Decimal,
    mode: KellyMode,
) -> KellyFraction {
    // Guard: if R is zero or negative, no meaningful Kelly computation
    if avg_win_loss_ratio <= Decimal::ZERO {
        return KellyFraction::zero();
    }

    // f* = W - (1 - W) / R
    let loss_rate = Decimal::ONE - win_rate;
    let raw_fraction = win_rate - loss_rate / avg_win_loss_ratio;

    // If negative (no edge), clamp to zero
    if raw_fraction <= Decimal::ZERO {
        return KellyFraction::zero();
    }

    // Scale by mode
    let scaled = raw_fraction * mode.scale();

    // Clamp to [0, 1]
    let clamped = if scaled > Decimal::ONE {
        Decimal::ONE
    } else {
        scaled
    };

    KellyFraction(clamped)
}

/// Compute Kelly inputs (win rate as fraction, reward ratio, trade count) from PnL values.
///
/// Delegates to `win_rate()`, `avg_win()`, `avg_loss()` from `crate::trading`.
///
/// Returns `(win_rate_fraction, avg_win_loss_ratio, trade_count)`.
pub fn compute_kelly_inputs(
    trade_pnls: &[Decimal],
) -> Result<(Decimal, Decimal, usize), MetricsError> {
    if trade_pnls.is_empty() {
        return Err(MetricsError::InsufficientData {
            required: 1,
            actual: 0,
        });
    }

    // Delegate to existing trading functions — NO reimplementation
    let wr_pct = crate::win_rate(trade_pnls)?;
    let wr_fraction = wr_pct / Decimal::from(100);

    let avg_w = crate::avg_win(trade_pnls)?;

    let avg_l = match crate::avg_loss(trade_pnls) {
        Ok(v) => v.abs(),
        Err(MetricsError::InsufficientData { .. }) => {
            // No losing trades → ratio is undefined
            return Err(MetricsError::DivisionByZero {
                context: "no losing trades — cannot compute win/loss ratio",
            });
        }
        Err(e) => return Err(e),
    };

    let ratio = avg_w / avg_l;

    Ok((wr_fraction, ratio, trade_pnls.len()))
}

#[cfg(test)]
#[path = "kelly_tests.rs"]
mod tests;