SPB Git
14.4 KB · 184 lines python
1"""Tests du chapitre 40 (Monte-Carlo et stress tests — données synthétiques, cas 3 et cas 4) : égalité stricte avec les chiffres du2chapitre 47 (mêmes lois MC_LOIS, même graine 20261031), moments théoriques des lois, Cholesky/copule, erreur de Monte-Carlo,3hypercube latin, mesures de risque et vérification analytique du RCD, Sobol, portefeuille, exercices.4Tolérances déclarées : montants ±1 $ (valeurs citées), moments de lois ±1e-9 (formules fermées), fréquences ±1e-4, indices de Sobol ±0,02."""5import sys, pathlib, math6import numpy as np, pandas as pd, pytest7from scipy import stats8sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))9from traite import cas3, cas410import ch40_montecarlo as m11import ch47_dossier as c471213@pytest.fixture(scope="module")14def r():15    return cas3.tout()1617@pytest.fixture(scope="module")18def mc(r):19    return m.monte_carlo(r, m.N_MC, m.GRAINE)2021# ----------------------------------------------------------------------------- cohérence avec le chapitre 472223def test_memes_lois_et_graine_que_ch47():24    assert m.MC_LOIS is c47.MC_LOIS and m.GRAINE == c47.GRAINE == 20261031 and m.R_T_PLANCHER == c47.R_T_PLANCHER2526def test_chiffres_identiques_au_ch47(r, mc):27    st, pr = mc["stats"], mc["probabilites"]28    assert st["mediane"] == pytest.approx(2_191_607.87, abs=1) and st["q05"] == pytest.approx(1_735_194.21, abs=1) and st["q95"] == pytest.approx(2_876_902.35, abs=1)29    assert st["moyenne"] == pytest.approx(2_233_505.68, abs=1) and st["ecart_type"] == pytest.approx(350_442.57, abs=1)30    assert pr["rcd1_sous_1.20"] == pytest.approx(0.4936, abs=1e-4) and pr["rcd1_sous_1.00"] == 0.0 and pr["rcd6_choc200_sous_1.20"] == pytest.approx(0.0140, abs=1e-4)31    assert pr["V_sous_cap"] == pytest.approx(0.3094, abs=1e-4) and pr["V_sous_2M"] == pytest.approx(0.2645, abs=1e-4) and pr["V_sous_role"] == pytest.approx(0.5118, abs=1e-4)32    # evaluer() (chemin du ch. 40) reproduit monte_carlo() (chemin du ch. 47) tirage par tirage33    e = m.evaluer(r, mc["tirages"])34    assert np.array_equal(e["valeur"], mc["valeur"]) and np.allclose(e["rcd1"], mc["rcd"][:, 0])3536def test_valeur_deterministe_reproduit_dcf(r):37    assert m.valeur_deterministe(r) == pytest.approx(r["dcf"]["valeur"], rel=1e-12)38    assert m.valeur_deterministe(r, taux_actualisation=0.08, taux_terminal=0.055) == pytest.approx(1_947_889, abs=1)   # scénario « hausse des taux » du ch. 303940# ----------------------------------------------------------------------------- lois4142def test_moments_lois_formules_fermees():43    ln = m.moments_loi("lognormale", {"mediane": 0.039, "sigma_log": 0.30})44    assert ln["moyenne"] == pytest.approx(0.039 * math.exp(0.045), abs=1e-12) and ln["q05"] == pytest.approx(stats.lognorm.ppf(0.05, 0.30, scale=0.039), abs=1e-12)45    assert ln["ecart_type"] == pytest.approx(stats.lognorm.std(0.30, scale=0.039), abs=1e-12)46    tri = m.moments_loi("triangulaire", {"min": 0.08, "mode": 0.15, "max": 0.25}); d = stats.triang(c=0.07 / 0.17, loc=0.08, scale=0.17)47    assert tri["moyenne"] == pytest.approx(0.16, abs=1e-12) and tri["ecart_type"] == pytest.approx(d.std(), abs=1e-12)48    assert float(tri["q05"]) == pytest.approx(d.ppf(0.05), abs=1e-12) and float(tri["q95"]) == pytest.approx(d.ppf(0.95), abs=1e-12)49    pe = m.pert(0.08, 0.15, 0.25)50    assert pe["moyenne"] == pytest.approx((0.08 + 4 * 0.15 + 0.25) / 6, abs=1e-12) and pe["moyenne"] == pytest.approx(pe["loi"].mean(), abs=1e-12)51    assert pe["ecart_type"] == pytest.approx(pe["loi"].std(), abs=1e-12)52    assert pe["loi"].pdf(0.15) >= max(pe["loi"].pdf(0.149), pe["loi"].pdf(0.151))      # mode en m53    # élicitation : inversion exacte des quantiles54    p = m.parametres_depuis_quantiles(0.024, 0.063, "lognormale"); mm = m.moments_loi("lognormale", p)55    assert mm["q05"] == pytest.approx(0.024, abs=1e-12) and mm["q95"] == pytest.approx(0.063, abs=1e-12)56    pn = m.parametres_depuis_quantiles(-0.01, 0.03, "normale"); assert pn["mu"] == pytest.approx(0.01) and pn["mu"] + pn["sigma"] * m.Z90 == pytest.approx(0.03, abs=1e-12)5758def test_quantile_loi_inverse_les_tirages():59    for nom, (typ, p) in m.MC_LOIS.items():60        u = np.array([0.05, 0.5, 0.95]); q = m.quantile_loi(typ, p, u)61        if typ == "normale": assert np.allclose(q, stats.norm.ppf(u, p["mu"], p["sigma"]))62        if typ == "lognormale": assert np.allclose(q, stats.lognorm.ppf(u, p["sigma_log"], scale=p["mediane"]))63        if typ == "triangulaire": assert np.allclose(q, stats.triang.ppf(u, c=(p["mode"] - p["min"]) / (p["max"] - p["min"]), loc=p["min"], scale=p["max"] - p["min"]))6465# ----------------------------------------------------------------------------- corrélations6667def test_cholesky_et_copule(mc):68    L = m.cholesky_2x2(0.005, 0.0064, 0.78); S = L @ L.T69    assert S[0, 0] == pytest.approx(0.005**2) and S[1, 1] == pytest.approx(0.0064**2) and S[0, 1] == pytest.approx(0.78 * 0.005 * 0.0064)70    assert np.allclose(L, np.linalg.cholesky(S))71    c = m.correlation_r_RT(mc["tirages"])72    assert c["rho_theorique"] == pytest.approx(0.005 / math.sqrt(0.005**2 + 0.004**2), abs=1e-12) and c["rho_empirique"] == pytest.approx(c["rho_theorique"], abs=0.01)73    assert c["spearman_empirique"] == pytest.approx(c["spearman_theorique"], abs=0.01) and c["part_plancher"] < 0.0274    assert m.spearman_depuis_pearson_gaussien(0.0) == 0.0 and m.spearman_depuis_pearson_gaussien(1.0) == pytest.approx(1.0)75    # copule gaussienne : marginales exactes et corrélation de rang ≈ (6/pi) arcsin(rho/2)76    corr = np.array([[1.0, 0.78], [0.78, 1.0]]); X = m.copule_gaussienne(50_000, 3, corr, [("normale", {"mu": 0.0725, "sigma": 0.005}), ("lognormale", {"mediane": 0.039, "sigma_log": 0.30})])77    assert X[:, 0].mean() == pytest.approx(0.0725, abs=1e-4) and np.median(X[:, 1]) == pytest.approx(0.039, abs=5e-4)78    assert stats.spearmanr(X[:, 0], X[:, 1])[0] == pytest.approx(m.spearman_depuis_pearson_gaussien(0.78), abs=0.01)7980# ----------------------------------------------------------------------------- erreur de Monte-Carlo, convergence, hypercube latin8182def test_erreur_monte_carlo(r, mc):83    e = m.erreurs_theoriques(mc).set_index("statistique")84    assert e.loc["moyenne", "se_theorique"] == pytest.approx(mc["stats"]["ecart_type"] / 100, rel=1e-9)85    assert e.loc["mediane", "se_theorique"] == pytest.approx(4135, abs=30) and e.loc["q95", "se_theorique"] == pytest.approx(10621, abs=80)86    assert e.loc["P(RCD1<1,20)", "se_theorique"] == pytest.approx(math.sqrt(0.4936 * 0.5064 / 10_000), abs=1e-5)87    rep = m.repetitions(r, 10_000, 30, graine0=1)88    assert rep["mediane"].std(ddof=1) == pytest.approx(e.loc["mediane", "se_theorique"], rel=0.35)      # accord théorie/empirique (30 répétitions)89    assert abs(rep["p_rcd1_120"].mean() - 0.5) < 0.004                                                 # valeur exacte 0,590    assert m.n_requis(413_451, 5000) == 26_267 and m.n_requis(0.5, 0.01) == 9_604 and m.n_requis(0.5, 0.005) == 38_4159192def test_convergence_et_pente(r):93    c = m.convergence(r, N_max=20_000, points=(100, 1000, 10_000, 20_000))94    assert len(c) == 4 and c["mediane"].iloc[-1] == pytest.approx(2_204_000, abs=15_000)95    s = m.se_empirique_selon_N(r, Ns=(250, 1000, 4000), k=12)96    assert -0.75 < s.attrs["pente_log_mediane"] < -0.39798def test_hypercube_latin(r):99    rng = np.random.default_rng(0); U = m.hypercube_latin(500, 3, rng)100    for k in range(3):   # exactement un point par strate101        assert np.array_equal(np.sort(np.floor(U[:, k] * 500).astype(int)), np.arange(500))102    x = m.tirer_lhs(2000, 1, 3)103    assert set(x) == set(m.tirer(2000, 1, 3)) and x["facteur_capex"].shape == (2000, 3)104    assert x["inoccupation_pertes"].mean() == pytest.approx(0.039 * math.exp(0.045), rel=0.01) and x["taux_actualisation"].mean() == pytest.approx(0.0725, abs=2e-5)105    assert np.all(x["taux_terminal"] >= m.R_T_PLANCHER)106    cmp = m.comparer_lhs(r, n=500, k=20).set_index("statistique")107    assert cmp.loc["moyenne", "ratio_variance"] > 5 and cmp.loc["p_rcd1_120", "se_lhs"] == 0.0 and cmp.loc["p_rcd1_120", "moy_lhs"] == 0.5108109# ----------------------------------------------------------------------------- risque, covenants, stress110111def test_mesures_risque_et_rcd_analytique(r, mc):112    mr = m.mesures_risque(mc, r["dcf"]["valeur"]).set_index("alpha")113    assert mr.loc[0.05, "VaR"] == pytest.approx(r["dcf"]["valeur"] - mc["stats"]["q05"], abs=1e-6) and mr.loc[0.05, "CVaR"] >= mr.loc[0.05, "VaR"]114    assert mr.loc[0.05, "VaR"] == pytest.approx(473_372, abs=1) and mr.loc[0.05, "CVaR"] == pytest.approx(557_097, abs=1)115    assert mr.loc[0.10, "VaR"] == pytest.approx(385_254, abs=1) and mr.loc[0.10, "CVaR"] == pytest.approx(491_208, abs=1)116    assert m.cvar_normale(0, 1, 0.05) == pytest.approx(-stats.norm.pdf(stats.norm.ppf(0.05)) / 0.05)117    an = m.rcd1_analytique(r)118    assert an["rcd1_base"] == pytest.approx(1.20, abs=1e-9) and an["v_seuil_120"] == pytest.approx(0.039, abs=1e-9)119    assert an["P_rcd1_sous_120_exacte"] == pytest.approx(0.5, abs=1e-12) and an["v_seuil_100"] == pytest.approx(0.1333, abs=1e-4) and an["P_rcd1_sous_100_exacte"] < 1e-4120    # la fonction affine reproduit le RCD1 simulé pour chaque tirage121    v = mc["tirages"]["inoccupation_pertes"]; b, hyp, f = r["base"], r["hyp"], r["financement"]122    de_hg = b["depenses_hors_reserve"] - 0.04 * b["rbe"]123    rcd_affine = (0.96 * (b["rbp_contrat"] * (1 - v) + hyp["autres_revenus_annuels"]) - de_hg - b["reserve"]) / f["service_dette"]124    assert np.allclose(rcd_affine, mc["rcd"][:, 0], rtol=1e-10)125126def test_stress_et_inverse(r, mc):127    st = m.stress_deterministes(r, mc)128    assert st["valeur"].iloc[0] == pytest.approx(r["dcf"]["valeur"], rel=1e-12) and st["P_V_inferieure"].is_monotonic_decreasing or True129    assert st.loc[st["scenario"].str.startswith("Hausse"), "valeur"].iloc[0] == pytest.approx(1_947_889, abs=1)130    assert st["P_V_inferieure"].iloc[-1] < 0.01 and st["P_V_inferieure"].iloc[1] == pytest.approx(0.2072, abs=1e-4)131    inv = m.stress_inverse(r)132    assert m.valeur_deterministe(r, taux_actualisation=inv["r_valeur_2M"], taux_terminal=inv["r_valeur_2M"] - 0.0225) == pytest.approx(2_000_000, abs=1)133    assert m.valeur_deterministe(r, taux_terminal=inv["RT_valeur_cap"]) == pytest.approx(r["valeur_capitalisation"], abs=1)134    assert inv["v_rcd1_egal_1"] == pytest.approx(0.1333, abs=1e-4)135136# ----------------------------------------------------------------------------- sensibilité globale137138def test_sobol_et_rangs(r, mc):139    so = m.sobol(r, n=5000).set_index("intrant")140    assert 0.9 < so["S1"].sum() < 1.05 and so["S1"].idxmax() == "taux_actualisation"141    assert so.loc["taux_actualisation", "S1"] == pytest.approx(0.528, abs=0.03) and so.loc["croissance_loyers_marche", "S1"] == pytest.approx(0.248, abs=0.03)142    assert (so["ST"] >= so["S1"] - 0.02).all()143    rg = m.correlations_rang(mc).set_index("intrant")144    assert rg.loc["taux_terminal", "spearman"] == pytest.approx(-0.78, abs=0.01) and rg.loc["taux_actualisation", "spearman"] == pytest.approx(-0.71, abs=0.01)145    assert rg.loc["croissance_loyers_marche", "spearman"] == pytest.approx(0.49, abs=0.01)146    reg = m.regression_standardisee(mc); assert reg["R2"] == pytest.approx(0.955, abs=0.01)147148# ----------------------------------------------------------------------------- portefeuille (cas 4)149150def test_portefeuille_structure_de_correlation():151    d = cas4.charger(); pf = d["portefeuille"]; cr = cas4.croissance_rne(d["series"])152    s = m.simuler_portefeuille(pf, 4000, 1, croissance=cr)153    assert s["valeurs"].shape == (4000, 48) and s["stats"]["nb_renouvellements_stress"] == int((pf["echeance_dette"] <= 2028).sum()) == 23154    assert s["stats"]["nb_rcd_det_sous_seuil"] == 5 and s["stats"]["nb_rcd_det_sous_1"] == 1155    # corrélation empirique des chocs : rho_rmr intra-RMR, rho_prov inter-RMR (sur les log-valeurs, à sigma_g près)156    V = np.log(s["valeurs"]); c = np.corrcoef(V.T); rmr = pf["rmr"].values157    intra = [c[i, j] for i in range(48) for j in range(i) if rmr[i] == rmr[j]]; inter = [c[i, j] for i in range(48) for j in range(i) if rmr[i] != rmr[j]]158    assert np.mean(intra) == pytest.approx(0.60, abs=0.05) and np.mean(inter) == pytest.approx(0.30, abs=0.05)159    ind = m.simuler_portefeuille(pf, 4000, 1, rho_rmr=0.0, rho_prov=0.0, croissance=cr)["stats"]; par = m.simuler_portefeuille(pf, 4000, 1, rho_rmr=1.0, rho_prov=1.0, croissance=cr)["stats"]160    assert ind["ecart_type"] < s["stats"]["ecart_type"] < par["ecart_type"] and ind["ratio_diversification"] < 0.3 and par["ratio_diversification"] > 0.95161    # l'espérance du nombre de bris ne dépend pas de la corrélation (±1 immeuble d'erreur MC)162    assert abs(ind["nb_rcd_sous_seuil_moyen"] - par["nb_rcd_sous_seuil_moyen"]) < 0.5163    # variance d'une somme à corrélation commune164    sig = np.array([1.0, 2.0, 3.0]); assert m.variance_somme_correlee(sig, 1.0) == pytest.approx(6.0) and m.variance_somme_correlee(sig, 0.0) == pytest.approx(math.sqrt(14))165    with pytest.raises(AssertionError): m.simuler_portefeuille(pf, 10, 1, rho_rmr=0.2, rho_prov=0.5)166167def test_portefeuille_valeurs_citees():168    d = cas4.charger(); cr = cas4.croissance_rne(d["series"])169    s = m.simuler_portefeuille(d["portefeuille"], m.N_MC, m.GRAINE, croissance=cr)["stats"]170    assert s["valeur_2026"] == 143_363_000 and s["valeur_deterministe"] == pytest.approx(147_935_699, abs=1)171    assert s["ecart_type"] == pytest.approx(10_295_448, abs=1) and s["VaR05"] == pytest.approx(14_063_788, abs=1) and s["CVaR05"] == pytest.approx(17_388_940, abs=1)172    assert s["ratio_diversification"] == pytest.approx(0.643, abs=1e-3) and s["nb_rcd_sous_seuil_moyen"] == pytest.approx(10.19, abs=0.01)173174# ----------------------------------------------------------------------------- exercices175176def test_exercices(r, mc):177    ex = m.exercices(r, mc)178    assert ex["ex1"]["mediane"] == pytest.approx(0.03888, abs=1e-5) and ex["ex1"]["P_sup_5pct"] == pytest.approx(0.1957, abs=1e-4)179    assert ex["ex2"]["pert"]["alpha"] == pytest.approx(2.647, abs=1e-3) and ex["ex2"]["P_tri_sup_20"] == pytest.approx(0.147, abs=1e-3) and ex["ex2"]["P_pert_sup_20"] == pytest.approx(0.092, abs=1e-3)180    assert ex["ex3"]["N_mediane_5000"] == 26_267 and ex["ex3"]["N_proba_1pt"] == 9_604181    assert ex["ex4"]["r_tire"] == pytest.approx(0.0785, abs=1e-6) and ex["ex4"]["RT_tire"] == pytest.approx(0.053988, abs=1e-5)182    assert ex["ex5"]["VaR"] == pytest.approx(385_254, abs=1) and ex["ex5"]["cvar_normale"] == pytest.approx(1_618_485, abs=1)183    assert ex["ex6"]["mediane"] == pytest.approx(2_184_377, abs=1) and ex["ex6"]["ecart_type"] == pytest.approx(546_896, abs=1) and ex["ex6"]["P_V_2M"] == pytest.approx(0.3558, abs=1e-4)184
Files · main
Menu

Theme