spb/traite-evaluation-qc
Public
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