SPB Git
13.9 KB · 211 lines python
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
Files · ae9e02e7cd803a16ec66687feb5b5de2a15d673f
Menu

Theme