quant-metrics 0.7.0

Pure performance statistics library for trading — Sharpe, Sortino, drawdown, VaR, portfolio composition
Documentation
//! Return calculations.

use rust_decimal::Decimal;

use crate::MetricsError;

/// Calculate total return as a percentage.
///
/// Formula: (end - start) / start * 100
///
/// # Arguments
/// * `equity` - Equity curve (NAV or portfolio value over time)
///
/// # Returns
/// Total percentage return, or error if insufficient data.
///
/// # Example
/// ```
/// use quant_metrics::total_return;
/// use rust_decimal_macros::dec;
///
/// let equity = vec![dec!(10000), dec!(10500)];
/// assert_eq!(total_return(&equity).unwrap(), dec!(5)); // 5%
/// ```
pub fn total_return(equity: &[Decimal]) -> Result<Decimal, MetricsError> {
    if equity.len() < 2 {
        return Err(MetricsError::InsufficientData {
            required: 2,
            actual: equity.len(),
        });
    }

    let start = equity[0];
    let end = equity[equity.len() - 1];

    if start == Decimal::ZERO {
        return Err(MetricsError::DivisionByZero {
            context: "starting equity is zero",
        });
    }

    Ok(((end - start) / start) * Decimal::from(100))
}

/// Calculate Compound Annual Growth Rate (CAGR).
///
/// Formula: ((end / start) ^ (1/years) - 1) * 100
///
/// # Arguments
/// * `equity` - Equity curve
/// * `years` - Number of years in the period
///
/// # Note
/// Uses Newton-Raphson approximation for nth root since Decimal
/// doesn't have native power functions.
pub fn cagr(equity: &[Decimal], years: Decimal) -> Result<Decimal, MetricsError> {
    if equity.len() < 2 {
        return Err(MetricsError::InsufficientData {
            required: 2,
            actual: equity.len(),
        });
    }

    if years <= Decimal::ZERO {
        return Err(MetricsError::InvalidParameter(
            "years must be positive".into(),
        ));
    }

    let start = equity[0];
    let end = equity[equity.len() - 1];

    if start == Decimal::ZERO {
        return Err(MetricsError::DivisionByZero {
            context: "starting equity is zero",
        });
    }

    let ratio = end / start;

    // For CAGR we need: ratio^(1/years) - 1
    // Using natural log approximation: e^(ln(ratio)/years) - 1
    // Approximated via Taylor series for small growth rates
    let ln_ratio = ln_approx(ratio);
    let exponent = ln_ratio / years;
    let growth_factor = exp_approx(exponent);

    Ok((growth_factor - Decimal::ONE) * Decimal::from(100))
}

/// Calculate annualized return from period return.
///
/// Formula: ((1 + period_return/100) ^ (periods_per_year / periods) - 1) * 100
///
/// # Arguments
/// * `equity` - Equity curve
/// * `periods_per_year` - Number of periods in a year (252 for daily, 12 for monthly)
pub fn annualized_return(
    equity: &[Decimal],
    periods_per_year: u32,
) -> Result<Decimal, MetricsError> {
    if equity.len() < 2 {
        return Err(MetricsError::InsufficientData {
            required: 2,
            actual: equity.len(),
        });
    }

    let periods = Decimal::from(equity.len() - 1);
    let years = periods / Decimal::from(periods_per_year);

    cagr(equity, years)
}

/// Natural logarithm approximation using Taylor series.
/// Works best for values close to 1.
fn ln_approx(x: Decimal) -> Decimal {
    if x <= Decimal::ZERO {
        return Decimal::ZERO;
    }

    // For x close to 1: ln(x) ≈ (x-1) - (x-1)^2/2 + (x-1)^3/3 - ...
    // For larger x, we use: ln(x) = ln(x/e^n) + n for some n
    let one = Decimal::ONE;
    let y = x - one;

    if y.abs() < Decimal::from(1) {
        // Taylor series around 1 (10 terms for better accuracy)
        let y2 = y * y;
        let y3 = y2 * y;
        let y4 = y3 * y;
        let y5 = y4 * y;
        let y6 = y5 * y;
        let y7 = y6 * y;
        let y8 = y7 * y;
        let y9 = y8 * y;
        let y10 = y9 * y;

        y - y2 / Decimal::from(2) + y3 / Decimal::from(3) - y4 / Decimal::from(4)
            + y5 / Decimal::from(5)
            - y6 / Decimal::from(6)
            + y7 / Decimal::from(7)
            - y8 / Decimal::from(8)
            + y9 / Decimal::from(9)
            - y10 / Decimal::from(10)
    } else {
        // For larger values, use iterative reduction
        // ln(x) = 2 * ln(sqrt(x)) if x > 2
        let mut val = x;
        let mut multiplier = Decimal::ONE;

        while val > Decimal::from(2) {
            val = decimal_sqrt(val);
            multiplier *= Decimal::from(2);
        }

        multiplier * ln_approx(val)
    }
}

/// Exponential approximation using Taylor series (12 terms).
fn exp_approx(x: Decimal) -> Decimal {
    // e^x ≈ 1 + x + x^2/2! + x^3/3! + ...
    // 12 terms gives <0.01% error for |x| < 3
    let x2 = x * x;
    let x3 = x2 * x;
    let x4 = x3 * x;
    let x5 = x4 * x;
    let x6 = x5 * x;
    let x7 = x6 * x;
    let x8 = x7 * x;
    let x9 = x8 * x;
    let x10 = x9 * x;
    let x11 = x10 * x;
    let x12 = x11 * x;

    Decimal::ONE
        + x
        + x2 / Decimal::from(2)
        + x3 / Decimal::from(6)
        + x4 / Decimal::from(24)
        + x5 / Decimal::from(120)
        + x6 / Decimal::from(720)
        + x7 / Decimal::from(5040)
        + x8 / Decimal::from(40320)
        + x9 / Decimal::from(362880)
        + x10 / Decimal::from(3628800)
        + x11 / Decimal::from(39916800)
        + x12 / Decimal::from(479001600)
}

use crate::math::decimal_sqrt;

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