Source code for skfolio.descriptor._volatility._ew_residual_volatility

"""Exponentially weighted CAPM residual volatility descriptors."""

# Copyright (c) 2023-2026
# Author: Hugo Delatte <hugo.delatte@skfoliolabs.com>
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

from skfolio.descriptor._volatility._base import _BaseEWResidualVolatility


[docs] class EWResidualVolatility(_BaseEWResidualVolatility): r"""Exponentially weighted CAPM residual volatility descriptor. Computes volatility of CAPM residuals with an EWMA variance estimate: .. math:: :nowrap: \[ \begin{aligned} \epsilon_i(t) &= r_i(t) - \hat\beta_i(t) \cdot r_m(t) \\[0.75em] S_{\epsilon,i}(t) &= \lambda_v \cdot S_{\epsilon,i}(t-1) + (1 - \lambda_v) \cdot \epsilon_i(t)^2 \\[0.75em] \text{output}_i(t) &= \sqrt{\frac{S_{\epsilon,i}(t)} {1 - \lambda_v^{n_i(t)}}} \end{aligned} \] where :math:`\hat\beta_i(t)` is the EWMA beta estimated with decay :math:`\lambda_\beta = \exp(-\ln(2)/\text{beta\_half\_life})` and the residual variance uses decay :math:`\lambda_v = \exp(-\ln(2)/\text{half\_life})`. The zero-initialized residual variance accumulator is bias-corrected at output time using each asset's valid observation count :math:`n_i(t)`. The market return :math:`r_m(t)` is computed as the cap-weighted average of returns in the estimation universe. Residual volatility isolates the part of return variation not explained by the market. This can be useful when market beta is already modeled separately and the intended signal is stock-specific risk after removing market exposure [1]_. Parameters ---------- half_life : float, default=40.0 EWMA half-life in observations for the residual variance estimator. beta_half_life : float, default=60.0 EWMA half-life in observations for the beta estimator. min_periods : int, optional Minimum number of valid returns required for each asset. Until an asset reaches this count, its output is NaN. This warm-up period avoids exposing early EWMA values before the residual volatility estimate has sufficiently converged from its zero initialization. If `None`, defaults to :math:`\lceil\max(\text{half\_life}, \text{beta\_half\_life})\rceil`, with a minimum of 1. eps : float, default=1e-12 Small constant for numerical stability in :math:`1 / \text{Var}(r_m)` when computing beta. Attributes ---------- n_assets_ : int Number of assets seen during fitting. asset_names_ : ndarray of shape (n_assets,) Asset names seen during fitting. residual_volatility_ : ndarray of shape (n_assets,) Last computed EWMA residual volatility. Contains NaN for inactive assets and assets that have not reached `min_periods` valid returns. Notes ----- NaNs are treated as missing observations. Active assets with missing returns keep their previous asset-specific EWMA state; inactive assets output NaN and restart their warm-up period when they become active again. Non-missing returns must be finite. Market returns are computed from the estimation universe (`estimation_mask` of :class:`~skfolio.containers.AssetPanel`). If no estimable asset has both finite returns and finite `market_cap` at an observation, the market return is undefined and a `ValueError` is raised. References ---------- .. [1] "The cross-section of volatility and expected returns" The Journal of Finance. Ang, A., Hodrick, R. J., Xing, Y., & Zhang, X. (2006). See Also -------- EWResidualDownsideVolatility : Downside variant using semi-deviation of residuals. Examples -------- >>> from skfolio.datasets import make_synthetic_characteristics >>> from skfolio.descriptor import EWResidualVolatility >>> >>> X = make_synthetic_characteristics() >>> >>> descriptor = EWResidualVolatility() >>> residual_volatility = descriptor.fit_transform(X) """ def __init__( self, half_life: float = 40.0, beta_half_life: float = 60.0, min_periods: int | None = None, eps: float = 1e-12, ): super().__init__( half_life=half_life, beta_half_life=beta_half_life, min_acceptable_return=None, min_periods=min_periods, eps=eps, )