Skip to content

benchmarks

Functions for comparing portfolio performance against a benchmark. These metrics measure active return, market sensitivity, and relative performance.

benchmarks

Benchmark-relative metrics: alpha, correlation, R-squared, capture ratios, batting average.

alpha

alpha(
    returns: Series,
    benchmark: Series,
    rf: float = DEFAULT_RISK_FREE_RATE,
    periods_per_year: int = TRADING_DAYS_PER_YEAR,
    as_result: bool = False,
) -> float | MetricResult

Jensen's alpha: the portfolio's annualized excess return unexplained by its beta exposure to the benchmark.

Parameters:

Name Type Description Default
returns Series

Periodic portfolio returns.

required
benchmark Series

Periodic benchmark returns, aligned to returns.

required
rf float

Annual risk-free rate used to compute excess returns.

DEFAULT_RISK_FREE_RATE
periods_per_year int

Number of return periods per year, used for annualization.

TRADING_DAYS_PER_YEAR
as_result bool

If True, return a MetricResult instead of a plain float.

False

Returns:

Type Description
float | MetricResult

Annualized alpha, or a MetricResult wrapping it.

Source code in src/portpy/metrics/benchmarks.py
def alpha(
    returns: pd.Series,
    benchmark: pd.Series,
    rf: float = DEFAULT_RISK_FREE_RATE,
    periods_per_year: int = TRADING_DAYS_PER_YEAR,
    as_result: bool = False,
) -> float | MetricResult:
    """
    Jensen's alpha: the portfolio's annualized excess return unexplained by
    its beta exposure to the benchmark.

    Args:
        returns: Periodic portfolio returns.
        benchmark: Periodic benchmark returns, aligned to `returns`.
        rf: Annual risk-free rate used to compute excess returns.
        periods_per_year: Number of return periods per year, used for
            annualization.
        as_result: If True, return a `MetricResult` instead of a plain float.

    Returns:
        Annualized alpha, or a `MetricResult` wrapping it.
    """
    r, b = align_pair(returns, benchmark)
    ensure_min_observations(r, 2, "returns")
    rf_period = periodic_rate_from_annual(rf, periods_per_year)
    beta_value = _beta(r, b)
    alpha_period = float((r - rf_period).mean() - beta_value * (b - rf_period).mean())
    value = (1.0 + alpha_period) ** periods_per_year - 1.0
    return MetricResult(value, "alpha", unit="%") if as_result else value

correlation

correlation(
    returns: Series,
    benchmark: Series,
    as_result: bool = False,
) -> float | MetricResult

Pearson correlation coefficient between returns and benchmark.

Parameters:

Name Type Description Default
returns Series

Periodic portfolio returns.

required
benchmark Series

Periodic benchmark returns, aligned to returns.

required
as_result bool

If True, return a MetricResult instead of a plain float.

False

Returns:

Type Description
float | MetricResult

The correlation coefficient, or a MetricResult wrapping it.

Source code in src/portpy/metrics/benchmarks.py
def correlation(returns: pd.Series, benchmark: pd.Series, as_result: bool = False) -> float | MetricResult:
    """
    Pearson correlation coefficient between returns and benchmark.

    Args:
        returns: Periodic portfolio returns.
        benchmark: Periodic benchmark returns, aligned to `returns`.
        as_result: If True, return a `MetricResult` instead of a plain float.

    Returns:
        The correlation coefficient, or a `MetricResult` wrapping it.
    """
    r, b = align_pair(returns, benchmark)
    ensure_min_observations(r, 2, "returns")
    value = float(r.corr(b))
    return MetricResult(value, "correlation") if as_result else value

r_squared

r_squared(
    returns: Series,
    benchmark: Series,
    as_result: bool = False,
) -> float | MetricResult

Fraction of the portfolio's return variance explained by the benchmark.

Parameters:

Name Type Description Default
returns Series

Periodic portfolio returns.

required
benchmark Series

Periodic benchmark returns, aligned to returns.

required
as_result bool

If True, return a MetricResult instead of a plain float.

False

Returns:

Type Description
float | MetricResult

R-squared (correlation squared), or a MetricResult wrapping it.

Source code in src/portpy/metrics/benchmarks.py
def r_squared(returns: pd.Series, benchmark: pd.Series, as_result: bool = False) -> float | MetricResult:
    """
    Fraction of the portfolio's return variance explained by the benchmark.

    Args:
        returns: Periodic portfolio returns.
        benchmark: Periodic benchmark returns, aligned to `returns`.
        as_result: If True, return a `MetricResult` instead of a plain float.

    Returns:
        R-squared (correlation squared), or a `MetricResult` wrapping it.
    """
    value = float(correlation(returns, benchmark) ** 2)
    return MetricResult(value, "r_squared") if as_result else value

up_capture_ratio

up_capture_ratio(
    returns: Series,
    benchmark: Series,
    periods_per_year: int = TRADING_DAYS_PER_YEAR,
    as_result: bool = False,
) -> float | MetricResult

Ratio of compounded returns during periods when the benchmark was positive.

Parameters:

Name Type Description Default
returns Series

Periodic portfolio returns.

required
benchmark Series

Periodic benchmark returns, aligned to returns.

required
periods_per_year int

Unused; accepted only for signature consistency with down_capture_ratio/capture_ratio.

TRADING_DAYS_PER_YEAR
as_result bool

If True, return a MetricResult instead of a plain float.

False

Returns:

Type Description
float | MetricResult

The up-capture ratio, or a MetricResult wrapping it. NaN if fewer

float | MetricResult

than 2 periods have a positive benchmark return.

Source code in src/portpy/metrics/benchmarks.py
def up_capture_ratio(
    returns: pd.Series,
    benchmark: pd.Series,
    periods_per_year: int = TRADING_DAYS_PER_YEAR,
    as_result: bool = False,
) -> float | MetricResult:
    """
    Ratio of compounded returns during periods when the benchmark was positive.

    Args:
        returns: Periodic portfolio returns.
        benchmark: Periodic benchmark returns, aligned to `returns`.
        periods_per_year: Unused; accepted only for signature consistency
            with `down_capture_ratio`/`capture_ratio`.
        as_result: If True, return a `MetricResult` instead of a plain float.

    Returns:
        The up-capture ratio, or a `MetricResult` wrapping it. NaN if fewer
        than 2 periods have a positive benchmark return.
    """
    r, b = align_pair(returns, benchmark)
    mask = b > 0
    value = _capture_uncontiguous(r[mask], b[mask]) if mask.sum() >= 2 else np.nan
    return MetricResult(value, "up_capture_ratio") if as_result else value

down_capture_ratio

down_capture_ratio(
    returns: Series,
    benchmark: Series,
    periods_per_year: int = TRADING_DAYS_PER_YEAR,
    as_result: bool = False,
) -> float | MetricResult

Ratio of compounded returns during periods when the benchmark was negative.

Parameters:

Name Type Description Default
returns Series

Periodic portfolio returns.

required
benchmark Series

Periodic benchmark returns, aligned to returns.

required
periods_per_year int

Unused; accepted only for signature consistency with up_capture_ratio/capture_ratio.

TRADING_DAYS_PER_YEAR
as_result bool

If True, return a MetricResult instead of a plain float.

False

Returns:

Type Description
float | MetricResult

The down-capture ratio, or a MetricResult wrapping it. NaN if fewer

float | MetricResult

than 2 periods have a negative benchmark return.

Source code in src/portpy/metrics/benchmarks.py
def down_capture_ratio(
    returns: pd.Series,
    benchmark: pd.Series,
    periods_per_year: int = TRADING_DAYS_PER_YEAR,
    as_result: bool = False,
) -> float | MetricResult:
    """
    Ratio of compounded returns during periods when the benchmark was negative.

    Args:
        returns: Periodic portfolio returns.
        benchmark: Periodic benchmark returns, aligned to `returns`.
        periods_per_year: Unused; accepted only for signature consistency
            with `up_capture_ratio`/`capture_ratio`.
        as_result: If True, return a `MetricResult` instead of a plain float.

    Returns:
        The down-capture ratio, or a `MetricResult` wrapping it. NaN if fewer
        than 2 periods have a negative benchmark return.
    """
    r, b = align_pair(returns, benchmark)
    mask = b < 0
    value = _capture_uncontiguous(r[mask], b[mask]) if mask.sum() >= 2 else np.nan
    return MetricResult(value, "down_capture_ratio") if as_result else value

capture_ratio

capture_ratio(
    returns: Series,
    benchmark: Series,
    periods_per_year: int = TRADING_DAYS_PER_YEAR,
    as_result: bool = False,
) -> float | MetricResult

Ratio of the portfolio's annualized return to the benchmark's annualized return, over the full contiguous series (unconditional on the benchmark's sign).

Parameters:

Name Type Description Default
returns Series

Periodic portfolio returns.

required
benchmark Series

Periodic benchmark returns, aligned to returns.

required
periods_per_year int

Number of return periods per year, used for annualization.

TRADING_DAYS_PER_YEAR
as_result bool

If True, return a MetricResult instead of a plain float.

False

Returns:

Type Description
float | MetricResult

The capture ratio, or a MetricResult wrapping it.

Source code in src/portpy/metrics/benchmarks.py
def capture_ratio(
    returns: pd.Series,
    benchmark: pd.Series,
    periods_per_year: int = TRADING_DAYS_PER_YEAR,
    as_result: bool = False,
) -> float | MetricResult:
    """
    Ratio of the portfolio's annualized return to the benchmark's annualized
    return, over the full contiguous series (unconditional on the
    benchmark's sign).

    Args:
        returns: Periodic portfolio returns.
        benchmark: Periodic benchmark returns, aligned to `returns`.
        periods_per_year: Number of return periods per year, used for
            annualization.
        as_result: If True, return a `MetricResult` instead of a plain float.

    Returns:
        The capture ratio, or a `MetricResult` wrapping it.
    """
    r, b = align_pair(returns, benchmark)
    ensure_min_observations(r, 2, "returns")
    value = _capture(r, b, periods_per_year)
    return MetricResult(value, "capture_ratio") if as_result else value

batting_average

batting_average(
    returns: Series,
    benchmark: Series,
    as_result: bool = False,
) -> float | MetricResult

Fraction of periods in which the portfolio's return strictly exceeded the benchmark's return.

Parameters:

Name Type Description Default
returns Series

Periodic portfolio returns.

required
benchmark Series

Periodic benchmark returns, aligned to returns.

required
as_result bool

If True, return a MetricResult instead of a plain float.

False

Returns:

Type Description
float | MetricResult

The batting average, or a MetricResult wrapping it.

Source code in src/portpy/metrics/benchmarks.py
def batting_average(returns: pd.Series, benchmark: pd.Series, as_result: bool = False) -> float | MetricResult:
    """
    Fraction of periods in which the portfolio's return strictly exceeded
    the benchmark's return.

    Args:
        returns: Periodic portfolio returns.
        benchmark: Periodic benchmark returns, aligned to `returns`.
        as_result: If True, return a `MetricResult` instead of a plain float.

    Returns:
        The batting average, or a `MetricResult` wrapping it.
    """
    r, b = align_pair(returns, benchmark)
    ensure_min_observations(r, 1, "returns")
    value = float((r > b).mean())
    return MetricResult(value, "batting_average", unit="%") if as_result else value