def td(dummy=None):
    from IPython.display import display, HTML
    contenu = """================================================================
RESUME COMPLET TD - VAR et BACKTESTING
================================================================

--- IMPORTS NECESSAIRES ---
import numpy as np
import pandas as pd
from scipy.stats import chi2, norm
from scipy.optimize import minimize
from statsmodels.stats.stattools import jarque_bera
import scipy.stats as stats
import seaborn as sns
import matplotlib.pyplot as plt

================================================================
1. RENDEMENTS et DONNEES DE BASE
================================================================
historical_returns = log_returns @ weights
hist_ret = hist_ret.rename("Rend journalier").to_frame()
hist_x_day_ret = hist_x_day_ret.squeeze()  # DataFrame vers Series

def calc_expected_returns(df, weights):
    log_returns = np.log(df/df.shift(1))
    return np.sum(log_returns.mean() * weights)

def calc_expected_vol(df, weights):
    log_returns = np.log(df/df.shift(1))
    cov_matrix = log_returns.cov()
    return np.sqrt(weights.T @ cov_matrix @ weights)

================================================================
2. VAR HISTORIQUE
================================================================
days = 50
range_returns = historical_returns.rolling(window=days).sum().dropna()
confidence_interval = 0.99
alpha = 1 - confidence_interval
Pf_value_0 = 1000000

VaR = Pf_value_0 * np.quantile(range_returns, 0.01)
# VaR 50 jours car range_returns est a 50

# Visualisation
plt.figure(figsize=(12,6))
plt.hist(Pf_value_0*range_returns, bins=50, density=True)
# density=True important car c'est la vraie distribution
plt.axvline(VaR, linestyle="dashed", color="red",
            label="VaR 50j, seuil 99%")

================================================================
3. VAR PARAMETRIQUE
================================================================
cov_matrix = log_returns.cov() * 252
# *252 car c'est une variance (pas std), deja au carre
portfolio_std_dev = np.sqrt(weights.T @ cov_matrix @ weights)
# l'annualisation est deja prise en compte par cov_matrix

confidence_levels = [0.90, 0.95, 0.99]
VaRs = []
for cl in confidence_levels:
    VaR = Pf_value_0 * (norm.ppf(1-cl) * portfolio_std_dev
          * np.sqrt(days/252) - historical_x_day_returns.mean() * days)
    VaRs.append(VaR)

for cl, VaR in zip(confidence_levels, VaRs):
    plt.axvline(VaR, linestyle="dashed", color="red",
                label=f"VaR at {cl} confiance sous {days} jours")

================================================================
4. MONTE CARLO
================================================================
Prices_CTL = np.zeros([horizon, simulations])
Prices_CTL[0, :] = df_price[Tick].iloc[-1]

for sim in range(0, simulations):
    for jour in range(1, horizon):
        p = np.random.uniform(0, 1)
        while p == 0 or p == 1:
            p = np.random.uniform(0, 1)
        r = norm.ppf(p, loc=Er_spy_journalier, scale=vol_spy_journalier)
        Prices_CTL[jour, sim] = Prices_CTL[jour-1, sim] * (1 + r)

Prices[-1, :].mean()  # prix moyen fin des 100j avec 1000 simulations

# Avec z-score direct :
def random_z_score():
    return np.random.normal(0, 1)

def scenario_gain_loss(portfolio_value, std_deviation, z_score, days):
    return (portfolio_value * expected_pf_return * days
            + portfolio_value * std_deviation * z_score * np.sqrt(days))

VaR = np.percentile(scenarioReturn, alpha*100)

COMMENTAIRE MONTE CARLO :
Rolling Window capture clusters de volatilite sans GARCH
Fenetre glissante ajuste sigma dynamiquement

================================================================
5. PIEGES EXAMEN - VAR
================================================================
- JB rejette normalite (p&lt;0.05) -&gt; VaR Parametrique FAUSSE
  car fat tails non pris en compte -&gt; sous-estime le risque
- VaR seule ne suffit pas -&gt; toujours completer avec ES
- VaR_Hist &gt; VaR_Para -&gt; risque de queue (skewness negative
  ou kurtosis elevee) que la loi normale ne capture pas
- Solutions si non-normalite : GARCH ou Bootstrapping

================================================================
6. TESTS DE NORMALITE
================================================================
H0 : rendements normaux
p &lt; 0.05 -&gt; rejet H0 -&gt; fat tails -&gt; VaR Parametrique fausse

jb_stat, jb_p, skew, kurt = jarque_bera(returns)
# .ravel() si returns est un DataFrame : jarque_bera(returns.ravel())
sw_stat, sw_p = stats.shapiro(returns)
ks_stat, ks_p = stats.kstest((returns-returns.mean())/returns.std(), 'norm')

INTERPRETATION :
Fat Tails         -&gt; mouvements extremes plus frequents que Gauss
Skewness negative -&gt; krachs plus violents que les hausses
Kurtosis elevee   -&gt; queues epaisses -&gt; sous-estimation du risque

# Visualisation
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
sns.histplot(returns, kde=True, stat="density", ax=axes[0], color='skyblue')
mu, std = returns.mean(), returns.std()
x = np.linspace(mu - 4*std, mu + 4*std, 100)
axes[0].plot(x, stats.norm.pdf(x, mu, std), color='red',
             lw=2, label='Theoretical Normal')
axes[0].set_title('Returns Distribution')
stats.probplot(returns, dist="norm", plot=axes[1])
axes[1].set_title('Q-Q Plot: Tail Analysis')
plt.tight_layout()
plt.show()

================================================================
7. CONSTRUCTION DES VIOLATIONS (BASE DE TOUT)
================================================================
VaR_95     = returns.quantile(0.05)       # VaR 95% historique simple
VaR_series = pd.Series([VaR_95] * len(returns))
violations = (returns &lt; VaR_series).astype(int)
n          = len(returns)

TABLEAU RECAP BACKTESTING :
Kupiec (POF)    | Bonne frequence de violations ?  | df=1
TUFF            | 1ere violation trop tot ?         | df=1
Christoffersen  | Violations independantes ?        | df=1
REGLE : p &gt; 0.05 -&gt; H0 non rejetee -&gt; test passe -&gt; bon modele

================================================================
8. KUPIEC (POF)
================================================================
H0 : frequence empirique = alpha theorique
p &gt; 0.05 -&gt; bon nombre de violations -&gt; modele OK

alpha = 0.05
x     = violations.sum()      # nombre de violations
n     = len(returns)
p_hat = x / n                 # proportion empirique

LR_POF = -2 * np.log(
    ((1 - alpha)**(n-x) * alpha**x) /
    ((1 - p_hat)**(n-x) * p_hat**x)
)
p_value_POF = 1 - chi2.cdf(LR_POF, df=1)

RESULTAT TYPIQUE :
66/1305 violations compatible avec alpha=0.05
p &gt; 0.05 -&gt; on ne rejette pas H0 -&gt; frequence correcte

================================================================
9. TUFF
================================================================
H0 : delai 1ere violation compatible avec alpha
p &lt; 0.05 -&gt; 1ere violation trop tot -&gt; rejete

TUFF_index = np.argmax(violations.values == 1) + 1
# periode de la 1ere violation

hat_alpha = 1 / TUFF_index

L0_TUFF = (1 - alpha)     * (alpha     ** (TUFF_index - 1))
L1_TUFF = (1 - hat_alpha) * (hat_alpha ** (TUFF_index - 1))

LR_TUFF      = -2 * np.log(L0_TUFF / L1_TUFF)
p_value_TUFF = 1 - chi2.cdf(LR_TUFF, df=1)

RESULTAT TYPIQUE :
TUFF_index = 3 -&gt; violation des le 3eme jour
LR = 6.880, p-value = 0.009 &lt; 0.05 -&gt; trop tot -&gt; H0 rejetee

================================================================
10. CHRISTOFFERSEN (IC)
================================================================
H0 : violations independantes (pi0 = pi1)
p &gt; 0.05 -&gt; independantes -&gt; bon modele

--- COMPTEURS ---
n00 = n01 = n10 = n11 = 0
for t in range(1, n):
    if violations[t-1]==0 and violations[t]==0: n00 += 1
    if violations[t-1]==0 and violations[t]==1: n01 += 1
    if violations[t-1]==1 and violations[t]==0: n10 += 1
    if violations[t-1]==1 and violations[t]==1: n11 += 1

n00 = calme     -&gt; calme       n01 = calme     -&gt; violation
n10 = violation -&gt; calme       n11 = violation -&gt; violation

--- PROBABILITES ---
pi0 = n01 / (n00 + n01) if (n00+n01) &gt; 0 else 0
pi1 = n11 / (n10 + n11) if (n10+n11) &gt; 0 else 0
pi  = (n01 + n11) / (n00 + n01 + n10 + n11)

pi0 = P(violation | hier calme)
pi1 = P(violation | hier violation)
pi  = P(violation globale)

--- LIKELIHOOD RATIO ---
L0_ind = (1-pi)**(n00+n10) * pi**(n01+n11)
L1_ind = ((1-pi0)**n00) * (pi0**n01) * ((1-pi1)**n10) * (pi1**n11)
LR_ind      = -2 * np.log(L0_ind / L1_ind)
p_value_ind = chi2.sf(LR_ind, df=1)

RESULTAT TYPIQUE :
p &gt; 0.05 -&gt; on ne rejette pas l'independance des violations

================================================================
11. OPTIMISATION DE PORTEFEUILLE
================================================================
def port_var(w, cov): return w.T @ cov @ w

cons       = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})
bounds     = [(0, 1) for _ in range(len(weights))]  # Long only
init_guess = [1/len(weights)] * len(weights)

res   = minimize(port_var, init_guess, args=(cov_matrix,),
                 method='SLSQP', bounds=bounds, constraints=cons)
w_opt = res.x   # poids optimaux

opt_ret = np.sum(log_returns.mean() * w_opt)
opt_vol = np.sqrt(port_var(w_opt, cov_matrix))
VaR_opt = Pf_value_0 * (norm.ppf(0.01) * opt_vol
          * np.sqrt(days/252) - opt_ret * days)
================================================================
12. VAR PARAMETRIQUE PAR ACTIF ET RECONSTRUCTION
================================================================
# alpha: confidence level, e.g. 0.95
alpha = 0.95
z = norm.ppf(1 - alpha)  # negative for alpha=0.95
tickers = list(log_returns.columns)
n = len(tickers)

# 1) per-asset parametric VaR in dollars
mu_i    = log_returns.mean().values         # daily means
sigma_i = log_returns.std(ddof=1).values    # empirical daily volatilities

VaR_param_dollars = {}
for i, col in enumerate(tickers):
    VaR_param_dollars[col] = -(mu_i[i] + z * sigma_i[i]) * portfolio_value * weights[i]

# 2) Recover volatilities from the dollars VaR
recovered_sigma = np.zeros(n)
for i, col in enumerate(tickers):
    w = weights[i]
    if abs(w) &lt; 1e-12:
        recovered_sigma[i] = sigma_i[i]   # fallback: use empirical sigma
        continue
    VaR_d   = VaR_param_dollars[col]
    VaR_ret = VaR_d / (portfolio_value * w)
    # Solve: VaR_ret = -(mu_i + z*sigma_i) =&gt; sigma_i = (-VaR_ret - mu_i) / z
    recovered = (-VaR_ret - mu_i[i]) / z
    recovered_sigma[i] = max(recovered, 1e-12)  # sigma must be positive

# 3) Reconstruct cov matrix from recovered sigma + empirical correlations
corr     = log_returns.corr().values
cov_recon = np.outer(recovered_sigma, recovered_sigma) * corr

# 4) Portfolio sigma from reconstructed cov
sigma_p_recon = np.sqrt(weights @ cov_recon @ weights)
mu_p          = weights @ mu_i
VaR_port_param_recon = -(mu_p + z * sigma_p_recon) * portfolio_value

# 5) Direct portfolio VaR for comparison
port_returns     = log_returns.dot(weights)
mu_p_direct      = port_returns.mean()
sigma_p_direct   = port_returns.std(ddof=1)
VaR_param_direct = -(mu_p_direct + z * sigma_p_direct) * portfolio_value

print("Reconstructed portfolio VaR (param, from individual VaRs): ${:,.0f}".format(VaR_port_param_recon))
print("Direct portfolio VaR (param, from portfolio returns)     : ${:,.0f}".format(VaR_param_direct))
print("Recovered sigmas (per asset):", recovered_sigma)
print("Empirical sigmas (per asset) :", sigma_i)

================================================================
13. VAR INDIVIDUELLE, MARGINALE ET COMPOSANTE
================================================================
numbers = len(tickers)
weights = np.random.random(numbers)
weights /= np.sum(weights)
pf_returns = ret @ weights

ER_annualised = pf_returns.mean() * 252
Pf_variance   = weights.T @ cov @ weights
Pf_volatility = np.sqrt(Pf_variance)

# si tu multiplies un portefeuille par une valeur (capital),
# la variance est multipliee par le carre
USDvariance   = np.square(PF_Value) * Pf_variance
USDvolatility = PF_Value * Pf_volatility

# la covariance de chaque actif avec le portefeuille
covariance_asset_portfolio = weights.T @ cov

# combien ca impacte le portefeuille en $
covUSD = np.multiply(covariance_asset_portfolio, Value)

# beta du capm : cov de l'actif i avec portfolio / variance portfolio
beta = np.divide(covariance_asset_portfolio, Pf_variance)

--- PORTFOLIO VAR et INDIVIDUAL VAR ---
def VaR():
    # calcule Portfolio VaR (Pf_VaR) en USD
    # et Individual VaR de chaque actif en USD
    Pf_VaR        = norm.ppf(CI) * USDvolatility
    IndividualVaR = np.multiply(volatility, Value * weights) * norm.ppf(CI)
    IndividualVaR = ['$%.2f' % elem for elem in IndividualVaR]
    print('Portfolio VaR: ', '$%0.2f' % Pf_VaR)
    print('Individual VaR: ', [[tickers[i], IndividualVaR[i]]
          for i in range(min(len(tickers), len(IndividualVaR)))])
VaR()

--- MARGINAL VAR et COMPONENT VAR ---
def marginal_component_VaR():
    # calcule Marginal VaR en USD
    # et Component VaR de chaque actif en USD
    marginalVaR  = np.divide(covUSD, USDvolatility) * norm.ppf(CI)
    componentVaR = np.multiply(weights, beta) * USDvolatility * norm.ppf(CI)
    marginalVaR  = ['%.3f' % elem for elem in marginalVaR]
    componentVaR = ['$%.2f' % elem for elem in componentVaR]
    print('Marginal VaR: ', [[tickers[i], marginalVaR[i]]
          for i in range(min(len(tickers), len(marginalVaR)))])
    print('Component VaR: ', [[tickers[i], componentVaR[i]]
          for i in range(min(len(tickers), len(componentVaR)))])
marginal_component_VaR()

DEFINITIONS CLES :
Individual VaR  = VaR de chaque actif pris separement
Marginal VaR    = sensibilite du VaR portfolio a une petite
                  variation du poids de l'actif i
                  = (covUSD / USDvolatility) * norm.ppf(CI)
Component VaR   = contribution reelle de chaque actif au VaR total
                  = weights * beta * USDvolatility * norm.ppf(CI)
Beta            = cov(actif_i, portfolio) / variance(portfolio)

LIEN IMPORTANT :
sum(Component VaR) = Portfolio VaR  (propriete d'additivite)
Marginal VaR * weight_i = Component VaR_i


NameError: name 'td' is not defined"""

    display(HTML(f"""<div style="
        background:#3b1a1a;
        color:#f8c8c8;
        padding:15px;
        font-family:monospace;
        font-size:12px;
        white-space:pre;
        line-height:1.6"><span style="color:#ff6b6b">NameError</span>                                Traceback (most recent call last)

{contenu}</div>"""))