SPB Git forge

spb/wp12_uqo

Public
5commits 1branches 0releases
1.2 MBsize
maindefault branch
1 mo agolast push
Python 75.9% TeX 24%
6.8 KB · 211 lines python
Raw Blame History
1"""2================================================================3Auteur  : Simon-Pierre Boucher4Contact : contact@spboucher.ai5Projet  : Prévision de volatilité réalisée multi-actifs6          (HAR-RV vs GARCH vs Machine Learning)7Fichier : backtest.py8Description : Valeur économique des prévisions — stratégie de9              volatility timing (cible de vol, cap de levier,10              coûts de transaction), gains d'utilité mean-11              variance, et backtesting de la VaR (tests de12              Kupiec et Christoffersen).13================================================================14"""1516from __future__ import annotations1718import logging1920import numpy as np21import pandas as pd22from scipy import stats2324logger = logging.getLogger(__name__)2526ANN = 252272829def volatility_timing(30    ret: pd.Series,31    pred_var: pd.Series,32    vol_target_ann: float = 0.10,33    leverage_cap: float = 3.0,34    tc_bps: float = 5.0,35) -> pd.DataFrame:36    """Volatility-timing strategy driven by one-day-ahead variance forecasts.3738    The weight on the risky asset is39    ``w_t = min(cap, sigma_target / sigma_hat_{t+1})`` where40    ``sigma_hat`` is the annualized forecast volatility; the position is41    financed at zero rate and transaction costs are charged on turnover.4243    Parameters44    ----------45    ret : pandas.Series46        Daily close-to-close log returns of the asset.47    pred_var : pandas.Series48        Daily variance forecasts indexed by forecast *origin* (the49        position for day t+1 uses the forecast dated t).50    vol_target_ann : float51        Annualized volatility target.52    leverage_cap : float53        Maximum absolute weight.54    tc_bps : float55        One-way transaction cost in basis points of traded notional.5657    Returns58    -------59    pandas.DataFrame60        Daily strategy ledger: ``weight``, ``gross``, ``net`` returns.61    """62    sigma_ann = np.sqrt(pred_var.clip(lower=1e-12) * ANN)63    w = (vol_target_ann / sigma_ann).clip(upper=leverage_cap)64    # weight decided at origin t applies to return of t+165    w_applied = w.shift(1)66    aligned = pd.concat([ret.rename("ret"), w_applied.rename("w")], axis=1).dropna()67    gross = aligned["w"] * aligned["ret"]68    turnover = aligned["w"].diff().abs().fillna(aligned["w"].abs())69    net = gross - turnover * tc_bps / 1e470    return pd.DataFrame({"weight": aligned["w"], "gross": gross, "net": net})717273def perf_stats(returns: pd.Series) -> dict[str, float]:74    """Annualized performance statistics of a daily return series."""75    r = returns.dropna()76    if len(r) < 60:77        return {k: np.nan for k in ("ann_ret", "ann_vol", "sharpe", "max_dd")}78    mu = r.mean() * ANN79    sig = r.std() * np.sqrt(ANN)80    cum = r.cumsum()81    dd = float((cum - cum.cummax()).min())82    return {83        "ann_ret": float(mu),84        "ann_vol": float(sig),85        "sharpe": float(mu / sig) if sig > 0 else np.nan,86        "max_dd": dd,87    }888990def utility_gain(91    net_a: pd.Series, net_b: pd.Series, risk_aversion: float = 5.092) -> float:93    """Annualized management fee an investor would pay to switch B -> A.9495    Mean-variance utility (Fleming, Kirby & Ostdiek 2001):96    ``Delta`` solves ``U(r_A - Delta) = U(r_B)`` with97    ``U(r) = E[r] - (gamma/2) Var[r]``.9899    Parameters100    ----------101    net_a, net_b : pandas.Series102        Daily net strategy returns (A = candidate, B = benchmark).103    risk_aversion : float104        Relative risk-aversion coefficient gamma.105106    Returns107    -------108    float109        Annualized utility gain (fee), in return units.110    """111    a, b = net_a.dropna(), net_b.dropna()112    common = a.index.intersection(b.index)113    a, b = a[common], b[common]114    ua = a.mean() - 0.5 * risk_aversion * a.var()115    ub = b.mean() - 0.5 * risk_aversion * b.var()116    return float((ua - ub) * ANN)117118119# ---------------------------------------------------------------- VaR backtest120def var_forecast(121    pred_var: pd.Series, level: float = 0.01122) -> pd.Series:123    """Gaussian Value-at-Risk from a variance forecast (positive number)."""124    z = stats.norm.ppf(level)125    return -z * np.sqrt(pred_var.clip(lower=1e-12))126127128def kupiec_test(violations: np.ndarray, level: float) -> tuple[float, float]:129    """Kupiec (1995) proportion-of-failures LR test.130131    Returns132    -------133    tuple134        ``(LR statistic, p-value)`` under chi2(1).135    """136    n = len(violations)137    x = int(violations.sum())138    if n == 0:139        return np.nan, np.nan140    pi_hat = x / n141    if x in (0, n):142        lr = -2 * n * (np.log(1 - level) if x == 0 else np.log(level))143    else:144        ll0 = (n - x) * np.log(1 - level) + x * np.log(level)145        ll1 = (n - x) * np.log(1 - pi_hat) + x * np.log(pi_hat)146        lr = -2 * (ll0 - ll1)147    return float(lr), float(stats.chi2.sf(lr, df=1))148149150def christoffersen_test(violations: np.ndarray, level: float) -> tuple[float, float]:151    """Christoffersen (1998) conditional-coverage LR test (chi2(2)).152153    Combines unconditional coverage with first-order independence of the154    violation indicator sequence.155    """156    v = violations.astype(int)157    n = len(v)158    if n < 2:159        return np.nan, np.nan160    pairs = np.column_stack([v[:-1], v[1:]])161    n00 = int(((pairs[:, 0] == 0) & (pairs[:, 1] == 0)).sum())162    n01 = int(((pairs[:, 0] == 0) & (pairs[:, 1] == 1)).sum())163    n10 = int(((pairs[:, 0] == 1) & (pairs[:, 1] == 0)).sum())164    n11 = int(((pairs[:, 0] == 1) & (pairs[:, 1] == 1)).sum())165    pi01 = n01 / max(n00 + n01, 1)166    pi11 = n11 / max(n10 + n11, 1)167    pi = (n01 + n11) / max(n00 + n01 + n10 + n11, 1)168169    def _safe_log(x: float) -> float:170        return np.log(x) if x > 0 else 0.0171172    ll_ind = (n00 * _safe_log(1 - pi01) + n01 * _safe_log(pi01)173              + n10 * _safe_log(1 - pi11) + n11 * _safe_log(pi11))174    ll_null = (n00 + n10) * _safe_log(1 - pi) + (n01 + n11) * _safe_log(pi)175    lr_ind = -2 * (ll_null - ll_ind)176    lr_uc, _ = kupiec_test(violations, level)177    lr_cc = lr_uc + lr_ind178    return float(lr_cc), float(stats.chi2.sf(lr_cc, df=2))179180181def var_backtest(182    ret: pd.Series, pred_var: pd.Series, level: float183) -> dict[str, float]:184    """Full VaR backtest for one model / asset / level.185186    Parameters187    ----------188    ret : pandas.Series189        Daily returns.190    pred_var : pandas.Series191        One-day variance forecasts indexed by origin (applied to t+1).192    level : float193        VaR tail level (0.01 or 0.05).194195    Returns196    -------197    dict198        Violation rate and Kupiec / Christoffersen p-values.199    """200    var = var_forecast(pred_var, level).shift(1)201    aligned = pd.concat([ret.rename("ret"), var.rename("var")], axis=1).dropna()202    viol = (aligned["ret"] < -aligned["var"]).to_numpy()203    lr_uc, p_uc = kupiec_test(viol, level)204    lr_cc, p_cc = christoffersen_test(viol, level)205    return {206        "n": float(len(viol)),207        "viol_rate": float(viol.mean()) if len(viol) else np.nan,208        "kupiec_p": p_uc,209        "christoffersen_p": p_cc,210    }211