Python 75.9%
TeX 24%
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