"""Tests du chapitre 31 — financement, levier, RCD et RPV (cas 3, données synthétiques ; repères BdC réels). Identités : taux périodique canadien ; paiement × facteur VA = capital ; solde prospectif = solde du tableau ; solde final nul ; Σ intérêts + capital = Σ paiements ; constante = 12·PMT/M_0 ; prêt max par RCD ⇒ RCD = seuil ; R_E = (R − Mf)/(1 − M) ; levier positif ssi y > k ; renouvellement exact : solde au terme reporté, TRI exact ≥ approximation ; barème APH ; mise de fonds ; valeurs citées dans manuscript/chapters/ch31.tex (tolérances déclarées).""" import math, pytest import numpy as np from traite import finance as fin from cases import ch31_financement as c @pytest.fixture(scope="module") def R(): return c.base() @pytest.fixture(scope="module") def FI(R): return c.financement(R) # ------------------------------------------------------------------ hypothèque canadienne def test_taux_canadien_et_effectif(): im = fin.taux_mensuel_canadien(0.0412) assert im == pytest.approx((1 + 0.0412 / 2) ** (1 / 6) - 1, rel=1e-14) assert im == pytest.approx(0.0034042, abs=5e-8) assert fin.taux_effectif_annuel(0.0412, 2) == pytest.approx(0.041624, abs=1e-6) assert (1 + im) ** 12 - 1 == pytest.approx(fin.taux_effectif_annuel(0.0412, 2), rel=1e-12) assert 0.0412 / 12 == pytest.approx(0.0034333, abs=5e-8) def test_identites_hypotheque(FI): h = FI["hypotheque"]; n = h.amortissement_mois; i = h.taux_periodique assert h.paiement * fin.facteur_va_annuite(i, n) == pytest.approx(h.capital, rel=1e-12) tab = h.tableau() assert tab[-1]["solde"] == pytest.approx(0.0, abs=1e-6) for k in (1, 12, 60, 120, 240, 479): assert tab[k - 1]["solde"] == pytest.approx(h.solde(k), rel=1e-9) assert sum(r["interet"] + r["capital"] for r in tab) == pytest.approx(h.paiement * n, rel=1e-9) assert sum(r["capital"] for r in tab) == pytest.approx(h.capital, rel=1e-9) assert h.constante_hypothecaire == pytest.approx(12 * h.paiement / h.capital, rel=1e-14) # capital remboursé croît géométriquement assert tab[1]["capital"] / tab[0]["capital"] == pytest.approx(1 + i, rel=1e-9) def test_valeurs_citees_financement(FI): assert FI["valeur"] == pytest.approx(2_208_566, abs=1) assert FI["rne_1"] == pytest.approx(89_956, abs=1) assert FI["pret_rpv"] == pytest.approx(1_656_424, abs=1) assert FI["pret_rcd"] == pytest.approx(1_475_959, abs=1) assert FI["pret"] == FI["pret_rcd"] and FI["contrainte"] == "RCD" assert FI["rpv"] == pytest.approx(0.6683, abs=5e-5) assert FI["mise_de_fonds"] == pytest.approx(732_606, abs=1) assert FI["paiement"] == pytest.approx(6246.93, abs=0.01) assert FI["service_dette"] == pytest.approx(74_963, abs=1) assert FI["constante"] == pytest.approx(0.050789, abs=1e-6) assert FI["interets_an1"] == pytest.approx(60_016, abs=1) and FI["capital_an1"] == pytest.approx(14_947, abs=1) assert FI["solde_60"] == pytest.approx(1_394_739, abs=1) and FI["solde_120"] == pytest.approx(1_295_148, abs=1) assert FI["rcd"] == pytest.approx(1.20, abs=1e-9) assert FI["flux_apres_dette_1"] == pytest.approx(14_993, abs=1) assert FI["cash_on_cash_1"] == pytest.approx(0.0205, abs=5e-5) assert FI["point_mort"] == pytest.approx(0.927, abs=5e-4) and FI["point_mort_hors_reserve"] == pytest.approx(0.878, abs=5e-4) assert FI["tri_fp"] == pytest.approx(0.1122, abs=5e-5) assert FI["k_annuel"] == pytest.approx(0.0406, abs=5e-5) and FI["k_annuel"] < FI["taux_effectif"] assert FI["rpv_max_par_rcd_sur_Vcap"] == pytest.approx(0.724, abs=5e-4) def test_conventions(FI): d = c.conventions(FI["pret"]).set_index("convention") ca, us = d.loc["Canada (semestrielle)"], d.loc["États-Unis (mensuelle)"] assert ca["paiement"] < us["paiement"] and ca["taux_effectif"] < us["taux_effectif"] assert us["paiement"] == pytest.approx(6279.21, abs=0.01) assert us["service_dette"] - ca["service_dette"] == pytest.approx(387, abs=1) assert us["constante"] == pytest.approx(0.051052, abs=1e-6) assert us["solde_60"] == pytest.approx(1_395_377, abs=1) def test_tableau_extraits(FI): t = c.tableau_extraits(FI["hypotheque"]).set_index("periode") assert t.loc[1, "interet"] == pytest.approx(5024.50, abs=0.01) and t.loc[1, "capital"] == pytest.approx(1222.42, abs=0.01) assert t.loc[60, "solde"] == pytest.approx(1_394_739.19, abs=0.01) assert t.loc[480, "solde"] == pytest.approx(0.0, abs=1e-6) assert t.loc[1, "part_interet"] == pytest.approx(0.804, abs=1e-3) dec = c.decomposition_annuelle(FI["hypotheque"]) assert dec["interet"].sum() == pytest.approx(1_522_566, abs=1) assert (dec["interet"] + dec["capital"] - FI["service_dette"]).abs().max() < 1e-6 # ------------------------------------------------------------------ ratios, prêt max, règles def test_pret_max_par_rcd_donne_rcd_seuil(R): noi = R["base"]["rne"] for i, n, q in ((0.0412, 480, 1.2), (0.0612, 300, 1.1), (0.0312, 360, 1.3)): p = fin.pret_max_par_rcd(noi, q, i, n) assert fin.rcd(noi, fin.Hypotheque(p, i, n).service_dette_annuel) == pytest.approx(q, rel=1e-12) g = c.grille_pret_max(R) assert (g.groupby(["amortissement_mois", "rcd_min"])["pret_max"].diff().dropna() < 0).all() # décroissant en taux row = g[(g["taux"] == 0.0412) & (g["amortissement_mois"] == 480) & (g["rcd_min"] == 1.2)].iloc[0] assert row["pret_max"] == pytest.approx(1_475_959, abs=1) r300 = g[(g["taux"] == 0.0412) & (g["amortissement_mois"] == 300) & (g["rcd_min"] == 1.2)].iloc[0]["pret_max"] assert row["pret_max"] / r300 == pytest.approx(1.258, abs=2e-3) def test_seuil_rpv_max_par_rcd(FI): # M_max/V = R/(RCD·f) ; seuil de liaison R = RCD · f · LTVmax = 4,571 % assert FI["rpv_max_par_rcd_sur_V"] == pytest.approx(FI["rpv"], rel=1e-9) assert 1.2 * FI["constante"] * 0.75 == pytest.approx(0.04571, abs=5e-5) def test_regles_residentielles(): assert fin.taux_admissible_minimal(0.0458) == pytest.approx(0.0658) and fin.taux_admissible_minimal(0.03) == pytest.approx(0.0525) assert c.mise_de_fonds_minimale(400_000) == 20_000 and c.mise_de_fonds_minimale(650_000) == 40_000 assert c.mise_de_fonds_minimale(1_000_000) == 75_000 and c.mise_de_fonds_minimale(1_500_000) == 125_000 assert c.mise_de_fonds_minimale(1_600_000) == 320_000 rr = c.regles_residentielles() assert rr["capacite"]["pret_au_contrat"] == pytest.approx(594_162, abs=1) and rr["capacite"]["pret_au_mqr"] == pytest.approx(491_344, abs=1) assert rr["capacite"]["reduction_mqr"] == pytest.approx(0.173, abs=5e-4) def test_prime_aph_bareme(): assert c.prime_aph(0.60, 300) == pytest.approx(0.0260) and c.prime_aph(0.65, 300) == pytest.approx(0.0260) 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) 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) assert c.prime_aph(0.95, 600) == pytest.approx(0.0615 + 0.0125) # 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 %) 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) def test_aph_select(R): a = c.aph_select(R).set_index("amortissement_mois") assert a.loc[480, "pret"].iloc[-1] if hasattr(a.loc[480, "pret"], "iloc") else True aph = c.aph_select(R).iloc[1:] assert list(aph["pret"].round(0)) == [1_610_138, 1_682_397, 1_741_328] assert (aph["pret_rcd"] < aph["pret_rpv"]).all() # RCD liant assert (aph["rcd_1"] - 1.10).abs().max() < 1e-9 assert aph["service_dette"].iloc[0] == pytest.approx(81_778, abs=1) assert list(aph["prime_pct"].round(4)) == [0.0410, 0.0535, 0.0560] assert aph["prime"].iloc[0] == pytest.approx(66_016, abs=1) and aph["mise_de_fonds"].iloc[-1] == pytest.approx(467_238, abs=1) def test_stress_rcd(R): s = c.stress_rcd(R).set_index("marge") assert s.loc[0.0, "rcd_1"] == pytest.approx(1.2, abs=1e-9) 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) assert s.loc[0.02, "pret_max_qualifie"] == pytest.approx(1_129_149, abs=1) assert (s["rcd_1"].diff().dropna() < 0).all() # ------------------------------------------------------------------ levier def test_levier_courant_formule(R): l = c.levier_courant(R) assert l["RE_formule"] == pytest.approx(l["RE_direct"], rel=1e-12) and l["RE_alt"] == pytest.approx(l["RE_direct"], rel=1e-12) assert l["RE_direct"] == pytest.approx(0.0205, abs=5e-5) and l["R"] < l["f"] and l["levier_courant"] == "négatif" assert l["RE_cap"] == pytest.approx(0.0267, abs=5e-5) def test_levier_par_rpv_monotone(R, FI): t = c.levier_par_rpv(R) assert t["tri_fp"].iloc[0] == pytest.approx(0.0725, abs=1e-6) # sans levier : TRI = r assert (t["tri_fp"].diff().dropna() > 0).all() # y > k ⇒ TRI FP croît avec M assert (t["cash_on_cash_1"].diff().dropna() < 0).all() # R < f ⇒ rendement courant décroît ret = t[t["retenu"]].iloc[0] assert ret["tri_fp"] == pytest.approx(FI["tri_fp"], rel=1e-9) and ret["rcd_1"] == pytest.approx(1.2, abs=1e-9) t85 = t[np.isclose(t["rpv"], 0.85)].iloc[0] assert t85["tri_fp"] == pytest.approx(0.1524, abs=5e-5) and t85["cash_on_cash_1"] < 0 and t85["rcd_1"] < 1 t75 = t[np.isclose(t["rpv"], 0.75)].iloc[0] assert t75["rcd_1"] == pytest.approx(1.07, abs=5e-3) and t75["flux_min"] == pytest.approx(-69_617, abs=1) def test_proposition_levier_positif(R, FI): """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.""" assert FI["tri_sans_levier"] > FI["k_annuel"] and FI["tri_fp"] > FI["tri_sans_levier"] d = R["dcf"]; V = d["valeur"] cher = fin.Hypotheque(0.5 * V, 0.09, 480) # dette à 9 % > y = 7,25 % res = fin.rendement_fonds_propres_levier(d["flux"], V, cher, d["vt_nette"]) assert res["tri"] < 0.0725 neutre_k = 0.0725 # dette dont le TRI annuel = y exactement (taux effectif y, paiements annuels) ⇒ y_E = y sd = 0.5 * V * fin.facteur_recouvrement(neutre_k, 10) flux = [-(0.5 * V)] + [cf - sd for cf in d["flux"]]; flux[-1] += d["vt_nette"] assert fin.tri(flux) == pytest.approx(0.0725, abs=1e-9) def test_risque_levier(R): rk = c.risque_levier(R) assert rk["sigma_fp"] == pytest.approx(0.08 / (1 - rk["M"])) and rk["multiplicateur"] == pytest.approx(3.01, abs=5e-3) # ------------------------------------------------------------------ renouvellement def test_renouvellement_exact(R, FI): ren = c.renouvellement(R); t = ren["tableau"].set_index("delta") assert ren["solde_terme"] == pytest.approx(FI["solde_60"], rel=1e-12) and ren["n_residuel"] == 420 assert t.loc[0.0, "paiement_2"] == pytest.approx(FI["paiement"], rel=1e-9) # taux inchangé ⇒ même paiement 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) assert list(t["service_dette_2"].round(0)) == [74_963, 85_177, 95_920, 107_120] # identiques au ch. 47 assert list(t["rcd_an_terme_plus_1"].round(3)) == [1.746, 1.537, 1.365, 1.222] assert list(t["tri_fp"].round(4)) == [0.1122, 0.1078, 0.1032, 0.0986] assert list(t["tri_fp_approx_ch30"].round(4)) == [0.1122, 0.1003, 0.0881, 0.0757] assert (t["tri_fp"] >= t["tri_fp_approx_ch30"] - 1e-12).all() and (t["sd_approx_ch30"] >= t["service_dette_2"] - 1e-6).all() assert t.loc[0.02, "solde_T"] == pytest.approx(1_327_042, abs=1) # le solde renouvelé suit la formule prospective avec le nouveau taux h2 = ren["detail"][0.02]["hypotheque_2"] assert h2.capital == pytest.approx(ren["solde_terme"]) and h2.solde(60) == pytest.approx(t.loc[0.02, "solde_T"], rel=1e-12) assert ren["delta_rcd_120"] == pytest.approx(0.0317, abs=1e-4) and ren["delta_rcd_100"] == pytest.approx(0.0502, abs=1e-4) def test_sensibilite_service_dette(R, FI): s = c.sensibilite_service_dette(R, taux=np.array([0.0412, 0.055, 0.0612])) b = s[(s["amortissement_mois"] == 480) & np.isclose(s["taux"], 0.0412)].iloc[0] assert b["service_dette"] == pytest.approx(FI["service_dette"], rel=1e-9) and b["rcd_1"] == pytest.approx(1.2, abs=1e-9) assert s[(s["amortissement_mois"] == 480) & np.isclose(s["taux"], 0.055)].iloc[0]["rcd_1"] < 1.0 assert s[(s["amortissement_mois"] == 300) & np.isclose(s["taux"], 0.0412)].iloc[0]["rcd_1"] == pytest.approx(0.954, abs=5e-4) # ------------------------------------------------------------------ exercices def test_exercices_coherence(): e = c.exercices() ca, us = e["ex1"]["Canada (semestrielle)"], e["ex1"]["États-Unis (mensuelle)"] assert ca["paiement"] == pytest.approx(4742.40, abs=0.01) and us["paiement"] == pytest.approx(4763.26, abs=0.01) assert e["ex2"]["solde_60"] == pytest.approx(944_971, abs=1) and e["ex2"]["part_remboursee_60"] == pytest.approx(0.0550, abs=5e-5) assert e["ex2"]["interets_totaux"] == pytest.approx(1_031_577, abs=1) and e["ex2"]["interets_totaux_25"] == pytest.approx(597_637, abs=1) 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) 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) 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) assert e["ex6"]["service_dette_2"] == pytest.approx(90_487, abs=1) and e["ex6"]["tri_fp"] == pytest.approx(0.1055, abs=5e-5) 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)