Source code for finance_risk.characteristics

from __future__ import annotations

from typing import Any

import numpy as np
from pydantic import BaseModel, ConfigDict

__all__ = [
    "CharacteristicFactorModel",
    "clean_characteristics",
    "fit_characteristic_factor_model",
]


[docs] class CharacteristicFactorModel(BaseModel): model_config = ConfigDict(frozen=True) factor_returns: tuple[float, ...] residual_variance: tuple[float, ...]
[docs] def clean_characteristics(values: Any, *, winsor_z: float = 3.0) -> np.ndarray: """Winsorize and z-score characteristic matrix column-wise.""" arr = np.asarray(values, dtype=float) if arr.ndim != 2: raise ValueError("values must be a 2D array") mean = np.nanmean(arr, axis=0) std = np.nanstd(arr, axis=0) std = np.where(std <= 1e-12, 1.0, std) z = (arr - mean) / std z = np.clip(z, -winsor_z, winsor_z) z_mean = np.nanmean(z, axis=0) z_std = np.nanstd(z, axis=0) z_std = np.where(z_std <= 1e-12, 1.0, z_std) return (z - z_mean) / z_std
[docs] def fit_characteristic_factor_model( returns: Any, characteristics: Any, *, ridge: float = 1e-6, ) -> CharacteristicFactorModel: """Fit cross-sectional returns on cleaned characteristics.""" r = np.asarray(returns, dtype=float).reshape(-1) x = clean_characteristics(characteristics) if x.shape[0] != r.size: raise ValueError("characteristics rows must match returns length") xtx = x.T @ x + ridge * np.eye(x.shape[1]) xty = x.T @ r beta = np.linalg.solve(xtx, xty) residuals = r - x @ beta return CharacteristicFactorModel( factor_returns=tuple(float(v) for v in beta), residual_variance=tuple(float(v) for v in residuals**2), )