spb/traite-evaluation-qc
Public
1"""Tests du chapitre 31 — financement, levier, RCD et RPV (cas 3, données synthétiques ; repères BdC réels).2Identités : taux périodique canadien ; paiement × facteur VA = capital ; solde prospectif = solde du tableau ; solde final nul ;3Σ intérêts + capital = Σ paiements ; constante = 12·PMT/M_0 ; prêt max par RCD ⇒ RCD = seuil ; R_E = (R − Mf)/(1 − M) ;4levier positif ssi y > k ; renouvellement exact : solde au terme reporté, TRI exact ≥ approximation ; barème APH ; mise de fonds ;5valeurs citées dans manuscript/chapters/ch31.tex (tolérances déclarées)."""6import math, pytest7import numpy as np8from traite import finance as fin9from cases import ch31_financement as c1011@pytest.fixture(scope="module")12def R():13 return c.base()1415@pytest.fixture(scope="module")16def FI(R):17 return c.financement(R)1819# ------------------------------------------------------------------ hypothèque canadienne2021def test_taux_canadien_et_effectif():22 im = fin.taux_mensuel_canadien(0.0412)23 assert im == pytest.approx((1 + 0.0412 / 2) ** (1 / 6) - 1, rel=1e-14)24 assert im == pytest.approx(0.0034042, abs=5e-8)25 assert fin.taux_effectif_annuel(0.0412, 2) == pytest.approx(0.041624, abs=1e-6)26 assert (1 + im) ** 12 - 1 == pytest.approx(fin.taux_effectif_annuel(0.0412, 2), rel=1e-12)27 assert 0.0412 / 12 == pytest.approx(0.0034333, abs=5e-8)2829def test_identites_hypotheque(FI):30 h = FI["hypotheque"]; n = h.amortissement_mois; i = h.taux_periodique31 assert h.paiement * fin.facteur_va_annuite(i, n) == pytest.approx(h.capital, rel=1e-12)32 tab = h.tableau()33 assert tab[-1]["solde"] == pytest.approx(0.0, abs=1e-6)34 for k in (1, 12, 60, 120, 240, 479):35 assert tab[k - 1]["solde"] == pytest.approx(h.solde(k), rel=1e-9)36 assert sum(r["interet"] + r["capital"] for r in tab) == pytest.approx(h.paiement * n, rel=1e-9)37 assert sum(r["capital"] for r in tab) == pytest.approx(h.capital, rel=1e-9)38 assert h.constante_hypothecaire == pytest.approx(12 * h.paiement / h.capital, rel=1e-14)39 # capital remboursé croît géométriquement40 assert tab[1]["capital"] / tab[0]["capital"] == pytest.approx(1 + i, rel=1e-9)4142def test_valeurs_citees_financement(FI):43 assert FI["valeur"] == pytest.approx(2_208_566, abs=1)44 assert FI["rne_1"] == pytest.approx(89_956, abs=1)45 assert FI["pret_rpv"] == pytest.approx(1_656_424, abs=1)46 assert FI["pret_rcd"] == pytest.approx(1_475_959, abs=1)47 assert FI["pret"] == FI["pret_rcd"] and FI["contrainte"] == "RCD"48 assert FI["rpv"] == pytest.approx(0.6683, abs=5e-5)49 assert FI["mise_de_fonds"] == pytest.approx(732_606, abs=1)50 assert FI["paiement"] == pytest.approx(6246.93, abs=0.01)51 assert FI["service_dette"] == pytest.approx(74_963, abs=1)52 assert FI["constante"] == pytest.approx(0.050789, abs=1e-6)53 assert FI["interets_an1"] == pytest.approx(60_016, abs=1) and FI["capital_an1"] == pytest.approx(14_947, abs=1)54 assert FI["solde_60"] == pytest.approx(1_394_739, abs=1) and FI["solde_120"] == pytest.approx(1_295_148, abs=1)55 assert FI["rcd"] == pytest.approx(1.20, abs=1e-9)56 assert FI["flux_apres_dette_1"] == pytest.approx(14_993, abs=1)57 assert FI["cash_on_cash_1"] == pytest.approx(0.0205, abs=5e-5)58 assert FI["point_mort"] == pytest.approx(0.927, abs=5e-4) and FI["point_mort_hors_reserve"] == pytest.approx(0.878, abs=5e-4)59 assert FI["tri_fp"] == pytest.approx(0.1122, abs=5e-5)60 assert FI["k_annuel"] == pytest.approx(0.0406, abs=5e-5) and FI["k_annuel"] < FI["taux_effectif"]61 assert FI["rpv_max_par_rcd_sur_Vcap"] == pytest.approx(0.724, abs=5e-4)6263def test_conventions(FI):64 d = c.conventions(FI["pret"]).set_index("convention")65 ca, us = d.loc["Canada (semestrielle)"], d.loc["États-Unis (mensuelle)"]66 assert ca["paiement"] < us["paiement"] and ca["taux_effectif"] < us["taux_effectif"]67 assert us["paiement"] == pytest.approx(6279.21, abs=0.01)68 assert us["service_dette"] - ca["service_dette"] == pytest.approx(387, abs=1)69 assert us["constante"] == pytest.approx(0.051052, abs=1e-6)70 assert us["solde_60"] == pytest.approx(1_395_377, abs=1)7172def test_tableau_extraits(FI):73 t = c.tableau_extraits(FI["hypotheque"]).set_index("periode")74 assert t.loc[1, "interet"] == pytest.approx(5024.50, abs=0.01) and t.loc[1, "capital"] == pytest.approx(1222.42, abs=0.01)75 assert t.loc[60, "solde"] == pytest.approx(1_394_739.19, abs=0.01)76 assert t.loc[480, "solde"] == pytest.approx(0.0, abs=1e-6)77 assert t.loc[1, "part_interet"] == pytest.approx(0.804, abs=1e-3)78 dec = c.decomposition_annuelle(FI["hypotheque"])79 assert dec["interet"].sum() == pytest.approx(1_522_566, abs=1)80 assert (dec["interet"] + dec["capital"] - FI["service_dette"]).abs().max() < 1e-68182# ------------------------------------------------------------------ ratios, prêt max, règles8384def test_pret_max_par_rcd_donne_rcd_seuil(R):85 noi = R["base"]["rne"]86 for i, n, q in ((0.0412, 480, 1.2), (0.0612, 300, 1.1), (0.0312, 360, 1.3)):87 p = fin.pret_max_par_rcd(noi, q, i, n)88 assert fin.rcd(noi, fin.Hypotheque(p, i, n).service_dette_annuel) == pytest.approx(q, rel=1e-12)89 g = c.grille_pret_max(R)90 assert (g.groupby(["amortissement_mois", "rcd_min"])["pret_max"].diff().dropna() < 0).all() # décroissant en taux91 row = g[(g["taux"] == 0.0412) & (g["amortissement_mois"] == 480) & (g["rcd_min"] == 1.2)].iloc[0]92 assert row["pret_max"] == pytest.approx(1_475_959, abs=1)93 r300 = g[(g["taux"] == 0.0412) & (g["amortissement_mois"] == 300) & (g["rcd_min"] == 1.2)].iloc[0]["pret_max"]94 assert row["pret_max"] / r300 == pytest.approx(1.258, abs=2e-3)9596def test_seuil_rpv_max_par_rcd(FI):97 # M_max/V = R/(RCD·f) ; seuil de liaison R = RCD · f · LTVmax = 4,571 %98 assert FI["rpv_max_par_rcd_sur_V"] == pytest.approx(FI["rpv"], rel=1e-9)99 assert 1.2 * FI["constante"] * 0.75 == pytest.approx(0.04571, abs=5e-5)100101def test_regles_residentielles():102 assert fin.taux_admissible_minimal(0.0458) == pytest.approx(0.0658) and fin.taux_admissible_minimal(0.03) == pytest.approx(0.0525)103 assert c.mise_de_fonds_minimale(400_000) == 20_000 and c.mise_de_fonds_minimale(650_000) == 40_000104 assert c.mise_de_fonds_minimale(1_000_000) == 75_000 and c.mise_de_fonds_minimale(1_500_000) == 125_000105 assert c.mise_de_fonds_minimale(1_600_000) == 320_000106 rr = c.regles_residentielles()107 assert rr["capacite"]["pret_au_contrat"] == pytest.approx(594_162, abs=1) and rr["capacite"]["pret_au_mqr"] == pytest.approx(491_344, abs=1)108 assert rr["capacite"]["reduction_mqr"] == pytest.approx(0.173, abs=5e-4)109110def test_prime_aph_bareme():111 assert c.prime_aph(0.60, 300) == pytest.approx(0.0260) and c.prime_aph(0.65, 300) == pytest.approx(0.0260)112 assert c.prime_aph(0.72, 480) == pytest.approx(0.0335 + 0.0075) and c.prime_aph(0.78, 480) == pytest.approx(0.0435 + 0.0075)113 assert c.prime_aph(0.817, 540) == pytest.approx(0.0535 + 0.0100) and c.prime_aph(0.845, 600) == pytest.approx(0.0535 + 0.0125)114 assert c.prime_aph(0.95, 600) == pytest.approx(0.0615 + 0.0125)115 # cohérence avec les valeurs du ch. 47 (RPV 78,2 / 81,7 / 84,5 % sur 40/45/50 ans → 5,10 / 6,35 / 6,60 %)116 assert c.prime_aph(0.782, 480) == pytest.approx(0.0510) and c.prime_aph(0.817, 540) == pytest.approx(0.0635) and c.prime_aph(0.845, 600) == pytest.approx(0.0660)117118def test_aph_select(R):119 a = c.aph_select(R).set_index("amortissement_mois")120 assert a.loc[480, "pret"].iloc[-1] if hasattr(a.loc[480, "pret"], "iloc") else True121 aph = c.aph_select(R).iloc[1:]122 assert list(aph["pret"].round(0)) == [1_610_138, 1_682_397, 1_741_328]123 assert (aph["pret_rcd"] < aph["pret_rpv"]).all() # RCD liant124 assert (aph["rcd_1"] - 1.10).abs().max() < 1e-9125 assert aph["service_dette"].iloc[0] == pytest.approx(81_778, abs=1)126 assert list(aph["prime_pct"].round(4)) == [0.0410, 0.0535, 0.0560]127 assert aph["prime"].iloc[0] == pytest.approx(66_016, abs=1) and aph["mise_de_fonds"].iloc[-1] == pytest.approx(467_238, abs=1)128129def test_stress_rcd(R):130 s = c.stress_rcd(R).set_index("marge")131 assert s.loc[0.0, "rcd_1"] == pytest.approx(1.2, abs=1e-9)132 assert s.loc[0.02, "service_dette"] == pytest.approx(97_988, abs=1) and s.loc[0.02, "rcd_1"] == pytest.approx(0.918, abs=5e-4)133 assert s.loc[0.02, "pret_max_qualifie"] == pytest.approx(1_129_149, abs=1)134 assert (s["rcd_1"].diff().dropna() < 0).all()135136# ------------------------------------------------------------------ levier137138def test_levier_courant_formule(R):139 l = c.levier_courant(R)140 assert l["RE_formule"] == pytest.approx(l["RE_direct"], rel=1e-12) and l["RE_alt"] == pytest.approx(l["RE_direct"], rel=1e-12)141 assert l["RE_direct"] == pytest.approx(0.0205, abs=5e-5) and l["R"] < l["f"] and l["levier_courant"] == "négatif"142 assert l["RE_cap"] == pytest.approx(0.0267, abs=5e-5)143144def test_levier_par_rpv_monotone(R, FI):145 t = c.levier_par_rpv(R)146 assert t["tri_fp"].iloc[0] == pytest.approx(0.0725, abs=1e-6) # sans levier : TRI = r147 assert (t["tri_fp"].diff().dropna() > 0).all() # y > k ⇒ TRI FP croît avec M148 assert (t["cash_on_cash_1"].diff().dropna() < 0).all() # R < f ⇒ rendement courant décroît149 ret = t[t["retenu"]].iloc[0]150 assert ret["tri_fp"] == pytest.approx(FI["tri_fp"], rel=1e-9) and ret["rcd_1"] == pytest.approx(1.2, abs=1e-9)151 t85 = t[np.isclose(t["rpv"], 0.85)].iloc[0]152 assert t85["tri_fp"] == pytest.approx(0.1524, abs=5e-5) and t85["cash_on_cash_1"] < 0 and t85["rcd_1"] < 1153 t75 = t[np.isclose(t["rpv"], 0.75)].iloc[0]154 assert t75["rcd_1"] == pytest.approx(1.07, abs=5e-3) and t75["flux_min"] == pytest.approx(-69_617, abs=1)155156def test_proposition_levier_positif(R, FI):157 """y_E > y ⇔ y > k : vérifié sur le cas 3 (y = 7,25 %, k ≈ 4,06 %) et par un contre-exemple où la dette coûte plus que l'actif."""158 assert FI["tri_sans_levier"] > FI["k_annuel"] and FI["tri_fp"] > FI["tri_sans_levier"]159 d = R["dcf"]; V = d["valeur"]160 cher = fin.Hypotheque(0.5 * V, 0.09, 480) # dette à 9 % > y = 7,25 %161 res = fin.rendement_fonds_propres_levier(d["flux"], V, cher, d["vt_nette"])162 assert res["tri"] < 0.0725163 neutre_k = 0.0725164 # dette dont le TRI annuel = y exactement (taux effectif y, paiements annuels) ⇒ y_E = y165 sd = 0.5 * V * fin.facteur_recouvrement(neutre_k, 10)166 flux = [-(0.5 * V)] + [cf - sd for cf in d["flux"]]; flux[-1] += d["vt_nette"]167 assert fin.tri(flux) == pytest.approx(0.0725, abs=1e-9)168169def test_risque_levier(R):170 rk = c.risque_levier(R)171 assert rk["sigma_fp"] == pytest.approx(0.08 / (1 - rk["M"])) and rk["multiplicateur"] == pytest.approx(3.01, abs=5e-3)172173# ------------------------------------------------------------------ renouvellement174175def test_renouvellement_exact(R, FI):176 ren = c.renouvellement(R); t = ren["tableau"].set_index("delta")177 assert ren["solde_terme"] == pytest.approx(FI["solde_60"], rel=1e-12) and ren["n_residuel"] == 420178 assert t.loc[0.0, "paiement_2"] == pytest.approx(FI["paiement"], rel=1e-9) # taux inchangé ⇒ même paiement179 assert t.loc[0.0, "tri_fp"] == pytest.approx(FI["tri_fp"], rel=1e-9) and t.loc[0.0, "solde_T"] == pytest.approx(FI["solde_120"], rel=1e-9)180 assert list(t["service_dette_2"].round(0)) == [74_963, 85_177, 95_920, 107_120] # identiques au ch. 47181 assert list(t["rcd_an_terme_plus_1"].round(3)) == [1.746, 1.537, 1.365, 1.222]182 assert list(t["tri_fp"].round(4)) == [0.1122, 0.1078, 0.1032, 0.0986]183 assert list(t["tri_fp_approx_ch30"].round(4)) == [0.1122, 0.1003, 0.0881, 0.0757]184 assert (t["tri_fp"] >= t["tri_fp_approx_ch30"] - 1e-12).all() and (t["sd_approx_ch30"] >= t["service_dette_2"] - 1e-6).all()185 assert t.loc[0.02, "solde_T"] == pytest.approx(1_327_042, abs=1)186 # le solde renouvelé suit la formule prospective avec le nouveau taux187 h2 = ren["detail"][0.02]["hypotheque_2"]188 assert h2.capital == pytest.approx(ren["solde_terme"]) and h2.solde(60) == pytest.approx(t.loc[0.02, "solde_T"], rel=1e-12)189 assert ren["delta_rcd_120"] == pytest.approx(0.0317, abs=1e-4) and ren["delta_rcd_100"] == pytest.approx(0.0502, abs=1e-4)190191def test_sensibilite_service_dette(R, FI):192 s = c.sensibilite_service_dette(R, taux=np.array([0.0412, 0.055, 0.0612]))193 b = s[(s["amortissement_mois"] == 480) & np.isclose(s["taux"], 0.0412)].iloc[0]194 assert b["service_dette"] == pytest.approx(FI["service_dette"], rel=1e-9) and b["rcd_1"] == pytest.approx(1.2, abs=1e-9)195 assert s[(s["amortissement_mois"] == 480) & np.isclose(s["taux"], 0.055)].iloc[0]["rcd_1"] < 1.0196 assert s[(s["amortissement_mois"] == 300) & np.isclose(s["taux"], 0.0412)].iloc[0]["rcd_1"] == pytest.approx(0.954, abs=5e-4)197198# ------------------------------------------------------------------ exercices199200def test_exercices_coherence():201 e = c.exercices()202 ca, us = e["ex1"]["Canada (semestrielle)"], e["ex1"]["États-Unis (mensuelle)"]203 assert ca["paiement"] == pytest.approx(4742.40, abs=0.01) and us["paiement"] == pytest.approx(4763.26, abs=0.01)204 assert e["ex2"]["solde_60"] == pytest.approx(944_971, abs=1) and e["ex2"]["part_remboursee_60"] == pytest.approx(0.0550, abs=5e-5)205 assert e["ex2"]["interets_totaux"] == pytest.approx(1_031_577, abs=1) and e["ex2"]["interets_totaux_25"] == pytest.approx(597_637, abs=1)206 assert e["ex3"]["pret"] == pytest.approx(1_572_079, abs=1) and e["ex3"]["pret_aph"] == pytest.approx(2_147_906, abs=1) and e["ex3"]["prime_aph"] == pytest.approx(0.0610)207 assert e["ex4"][0.7][0.06] == pytest.approx(0.03333, abs=1e-5) and e["ex4"][0.7][0.045] == pytest.approx(0.06833, abs=1e-5)208 assert e["ex5"]["mdf"] == 47_000 and e["ex5"]["paiement_mqr"] == pytest.approx(4540.60, abs=0.01) and e["ex5"]["lti_revenu_requis"] == pytest.approx(149_556, abs=1)209 assert e["ex6"]["service_dette_2"] == pytest.approx(90_487, abs=1) and e["ex6"]["tri_fp"] == pytest.approx(0.1055, abs=5e-5)210 assert e["ex7"][0]["tri_fp"] == pytest.approx(0.0949, abs=5e-5) and e["ex7"][1]["tri_fp"] == pytest.approx(0.1524, abs=5e-5)211