spb/traite-evaluation-qc
Public
1"""Tests du chapitre 47 (cas 3, 12 logements à Montréal — données synthétiques) : identités, valeurs citées dans le texte,2reproductibilité du Monte-Carlo. Tolérances déclarées : montants ±1 $ (valeurs citées au dollar), taux ±1e-6, MC ±1 $ (même graine)."""3import sys, pathlib4import numpy as np, pytest5sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))6from traite import cas3, finance as fin7import ch47_dossier as ch89@pytest.fixture(scope="module")10def D():11 return ch.dossier(n_mc=10_000, graine=ch.GRAINE)1213# ----------------------------------------------------------------------------- identités1415def test_identites_revenus_depenses(D):16 b = D["base"]17 assert b["rbp_contrat"] == pytest.approx(D["etat"]["loyer_mensuel_contrat"].sum() * 12)18 assert b["rbe"] == pytest.approx(b["rbp_contrat"] * (1 - 0.039) + 9360)19 assert b["rne"] == pytest.approx(b["rbe"] - b["depenses_hors_reserve"] - b["reserve"])20 det = b["detail"]21 assert det.loc[det["poste"] == "Taxes municipales", "normalise"].iloc[0] == pytest.approx(2_200_000 * 0.004631)22 assert det.loc[det["poste"].str.startswith("Gestion"), "normalise"].iloc[0] == pytest.approx(0.04 * b["rbe"])2324def test_capitalisation_et_dcf(D):25 b, tx, d, h = D["base"], D["taux"], D["dcf"], D["hyp"]26 assert tx["n"] == 4027 assert D["valeur_capitalisation"] == pytest.approx(b["rne"] / tx["mediane"])28 p = D["projection"]29 flux = p.loc[p["annee"] <= 10, "flux_avant_dette"].tolist()30 va = sum(cf / (1 + h["taux_actualisation"]) ** (t + 1) for t, cf in enumerate(flux)) + d["vt_nette"] / (1 + h["taux_actualisation"]) ** 1031 assert d["valeur"] == pytest.approx(va)32 assert d["vt_brute"] == pytest.approx(d["rne_T1"] / h["taux_terminal"]) and d["vt_nette"] == pytest.approx(d["vt_brute"] * 0.975)33 # taux implicite de première année de la valeur DCF et TRI non levier = taux d'actualisation34 assert D["financement_dcf"]["tri_sans_levier"] == pytest.approx(h["taux_actualisation"], abs=1e-8)3536def test_financement_identites(D):37 f = D["financement"]; h = f["hypotheque"]38 assert f["contrainte"] == "RCD" and f["pret"] == pytest.approx(f["pret_rcd"])39 assert f["rcd"] == pytest.approx(1.20) and f["rpv"] <= 0.7540 assert h.taux_periodique == pytest.approx((1 + 0.0412 / 2) ** (1 / 6) - 1)41 assert h.service_dette_annuel == pytest.approx(12 * h.paiement)42 assert h.solde(480) == pytest.approx(0.0, abs=1e-6)43 ff = D["financement_final"]44 assert fin.van(ff["flux_fonds_propres"], ff["tri_fonds_propres"]) == pytest.approx(0.0, abs=1e-3) # VAN nulle au TRI4546def test_projection_vectorisee_reproduit_cas3(D):47 """La projection vectorisée (Monte-Carlo) doit coïncider exactement avec cas3.projection_dcf aux paramètres de base."""48 h, b, e = D["hyp"], D["base"], D["etat"]49 one = lambda x: np.array([x])50 proj = ch.projeter_vectorise(e, h, b, one(h["taux_inoccupation_pertes"]), one(h["croissance_loyers_marche"]), one(h["rotation_annuelle"]), one(h["croissance_depenses"]), np.ones((1, 3)))51 p = D["projection"]52 assert np.allclose(proj["rne"][0], p["rne"].values, rtol=1e-12)53 assert np.allclose(proj["flux"][0], p.loc[p["annee"] <= 10, "flux_avant_dette"].values, rtol=1e-12)54 v = ch.valeur_vectorisee(proj, one(h["taux_actualisation"]), one(h["taux_terminal"]), h["couts_vente"])55 assert v["valeur"][0] == pytest.approx(D["dcf"]["valeur"], rel=1e-12)56 assert D["tornade"]["V_base"].iloc[0] == pytest.approx(D["dcf"]["valeur"], rel=1e-9)5758def test_droits_mutation_montreal():59 assert ch.droits_mutation_montreal(2_060_000) == pytest.approx(41_325.50)60 assert ch.droits_mutation_montreal(62_900) == pytest.approx(314.50)61 assert ch.droits_mutation_montreal(0) == 0.06263# ----------------------------------------------------------------------------- valeurs citées dans ch47.tex (tolérance ±1 $ sauf indication)6465def test_valeurs_citees(D):66 b, tx, d, f, rec, cu = D["base"], D["taux"], D["dcf"], D["financement"], D["reconciliation"], D["comparaison_unite"]67 assert b["rbp_contrat"] == pytest.approx(165_540, abs=1) and b["rbp_marche"] == pytest.approx(186_036, abs=1)68 assert b["rbe"] == pytest.approx(168_443.94, abs=0.01)69 assert b["depenses_hors_reserve"] == pytest.approx(70_385.19, abs=0.01) and b["reserve"] == pytest.approx(8_103, abs=0.01)70 assert b["rne"] == pytest.approx(89_955.75, abs=0.01)71 assert tx["mediane"] == pytest.approx(0.04415, abs=1e-6) and tx["moyenne"] == pytest.approx(0.0440375, abs=1e-6) and tx["ponderee_rne"] == pytest.approx(0.043319, abs=1e-6)72 assert tx["q1"] == pytest.approx(0.041525, abs=1e-6) and tx["q3"] == pytest.approx(0.046375, abs=1e-6)73 assert D["valeur_capitalisation"] == pytest.approx(2_037_502.89, abs=0.01)74 assert d["valeur"] == pytest.approx(2_208_565.91, abs=0.01) and d["va_flux"] == pytest.approx(662_409.91, abs=0.01) and d["va_terminale"] == pytest.approx(1_546_156.00, abs=0.01)75 assert d["vt_brute"] == pytest.approx(3_193_167.52, abs=0.01) and d["rne_T1"] == pytest.approx(159_658.38, abs=0.01) and d["part_terminale"] == pytest.approx(0.7001, abs=1e-4)76 assert f["pret_rcd"] == pytest.approx(1_475_959.45, abs=0.01) and f["pret_rpv"] == pytest.approx(1_656_424.43, abs=0.01)77 assert f["service_dette"] == pytest.approx(74_963.13, abs=0.01) and f["constante"] == pytest.approx(0.050789, abs=1e-6) and f["hypotheque"].paiement == pytest.approx(6_246.93, abs=0.01)78 assert f["hypotheque"].solde(60) == pytest.approx(1_394_739.19, abs=0.01)79 assert D["financement_dcf"]["tri_fonds_propres"] == pytest.approx(0.1122, abs=1e-4) and D["financement_dcf"]["point_mort"] == pytest.approx(0.927, abs=1e-3)80 assert cu["indication"] == pytest.approx(1_870_902.67, abs=0.01) and cu["prix_par_logement_retenu"] == pytest.approx(155_908.56, abs=0.01)81 assert cu["mrb_mediane"] == pytest.approx(11.94, abs=1e-9) and cu["indication_mrb"] == pytest.approx(1_976_547.60, abs=0.01)82 assert cu["ols"]["coef"]["ln_loyer"] == pytest.approx(1.1707, abs=1e-4) and cu["ols"]["coef"]["taille"] == pytest.approx(0.0079, abs=1e-4)83 assert rec["valeur_ponderee"] == pytest.approx(2_064_054.90, abs=0.01) and rec["valeur_finale"] == 2_060_00084 assert rec["ratio_valeur_role"] == pytest.approx(0.9364, abs=1e-4) and rec["mrb_contrat"] == pytest.approx(12.44, abs=0.01) and rec["taux_implicite"] == pytest.approx(0.04367, abs=1e-5)85 ff = D["financement_final"]86 assert ff["mise_de_fonds"] == pytest.approx(584_040.55, abs=0.01) and ff["rpv"] == pytest.approx(0.7165, abs=1e-4) and ff["tri_fonds_propres"] == pytest.approx(0.1383, abs=1e-4)87 assert D["libre_de_baux"]["rne"] == pytest.approx(108_864.54, abs=0.01) and D["libre_de_baux"]["valeur"] == pytest.approx(2_465_788.04, abs=0.01)88 sc = D["scenarios"]89 assert sc.loc[sc["hausse_pb"] == 200, "service_dette"].iloc[0] == pytest.approx(95_920.20, abs=0.01) and sc.loc[sc["hausse_pb"] == 300, "rcd_6"].iloc[0] == pytest.approx(1.222, abs=1e-3)90 aph = D["aph"]91 assert aph.loc[aph["amortissement_ans"] == 40, "pret"].iloc[0] == pytest.approx(1_610_137.58, abs=0.01)9293def test_monte_carlo_citations_et_reproductibilite(D):94 mc = D["mc"]; st = mc["stats"]; pr = mc["probabilites"]95 assert mc["n"] == 10_000 and len(mc["valeur"]) == 10_00096 assert st["q05"] < st["q25"] < st["mediane"] < st["q75"] < st["q95"]97 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)98 assert st["moyenne"] == pytest.approx(2_233_505.68, abs=1) and st["ecart_type"] == pytest.approx(350_442.57, abs=1)99 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)100 assert pr["V_sous_cap"] == pytest.approx(0.3094, abs=1e-4) and pr["V_sous_role"] == pytest.approx(0.5118, abs=1e-4)101 # même graine → résultats identiques ; graine différente → distribution voisine (±1,5 % sur la médiane)102 mc2 = ch.monte_carlo({k: D[k] for k in ["etat", "hyp", "base", "financement", "valeur_capitalisation"]}, 10_000, ch.GRAINE)103 assert np.array_equal(mc2["valeur"], mc["valeur"])104 mc3 = ch.monte_carlo({k: D[k] for k in ["etat", "hyp", "base", "financement", "valeur_capitalisation"]}, 10_000, 7)105 assert mc3["stats"]["mediane"] == pytest.approx(st["mediane"], rel=0.015)106 # la médiane du RCD de première année est au seuil 1,20 (prêt calibré au seuil)107 assert float(np.median(mc["rcd"][:, 0])) == pytest.approx(1.20, abs=0.02)108109def test_exercices(D):110 ex = ch.exercices(D, D["comparaison_unite"], D["reconciliation"])111 assert ex["ex1"]["rne"] == pytest.approx(88_207.65, abs=0.01) and ex["ex1"]["valeur"] == pytest.approx(1_997_908.27, abs=0.01)112 assert ex["ex2"]["rne"] == pytest.approx(88_949.51, abs=0.01)113 assert ex["ex3"]["V_q1"] == pytest.approx(2_166_303.49, abs=0.01) and ex["ex3"]["V_q3"] == pytest.approx(1_939_746.68, abs=0.01)114 assert ex["ex4_rot10"]["valeur"] == pytest.approx(2_140_916.43, abs=0.01) and ex["ex4_rot25"]["valeur"] == pytest.approx(2_231_654.72, abs=0.01)115 assert ex["ex5"]["pret"] == pytest.approx(1_283_814.77, abs=0.01) and ex["ex5"]["contrainte"] == "RCD"116 assert ex["ex6"]["pret"] == pytest.approx(1_610_137.58, abs=0.01) and ex["ex6"]["prime_pct"] == pytest.approx(0.051, abs=1e-6)117 assert ex["ex7"]["p_rcd1_sous_1.20"] == pytest.approx(0.9262, abs=1e-4) and ex["ex7"]["mediane"] == pytest.approx(2_108_706.66, abs=1)118