"""Tests du chapitre 47 (cas 3, 12 logements à Montréal — données synthétiques) : identités, valeurs citées dans le texte, reproductibilité du Monte-Carlo. Tolérances déclarées : montants ±1 $ (valeurs citées au dollar), taux ±1e-6, MC ±1 $ (même graine).""" import sys, pathlib import numpy as np, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) from traite import cas3, finance as fin import ch47_dossier as ch @pytest.fixture(scope="module") def D(): return ch.dossier(n_mc=10_000, graine=ch.GRAINE) # ----------------------------------------------------------------------------- identités def test_identites_revenus_depenses(D): b = D["base"] assert b["rbp_contrat"] == pytest.approx(D["etat"]["loyer_mensuel_contrat"].sum() * 12) assert b["rbe"] == pytest.approx(b["rbp_contrat"] * (1 - 0.039) + 9360) assert b["rne"] == pytest.approx(b["rbe"] - b["depenses_hors_reserve"] - b["reserve"]) det = b["detail"] assert det.loc[det["poste"] == "Taxes municipales", "normalise"].iloc[0] == pytest.approx(2_200_000 * 0.004631) assert det.loc[det["poste"].str.startswith("Gestion"), "normalise"].iloc[0] == pytest.approx(0.04 * b["rbe"]) def test_capitalisation_et_dcf(D): b, tx, d, h = D["base"], D["taux"], D["dcf"], D["hyp"] assert tx["n"] == 40 assert D["valeur_capitalisation"] == pytest.approx(b["rne"] / tx["mediane"]) p = D["projection"] flux = p.loc[p["annee"] <= 10, "flux_avant_dette"].tolist() va = sum(cf / (1 + h["taux_actualisation"]) ** (t + 1) for t, cf in enumerate(flux)) + d["vt_nette"] / (1 + h["taux_actualisation"]) ** 10 assert d["valeur"] == pytest.approx(va) assert d["vt_brute"] == pytest.approx(d["rne_T1"] / h["taux_terminal"]) and d["vt_nette"] == pytest.approx(d["vt_brute"] * 0.975) # taux implicite de première année de la valeur DCF et TRI non levier = taux d'actualisation assert D["financement_dcf"]["tri_sans_levier"] == pytest.approx(h["taux_actualisation"], abs=1e-8) def test_financement_identites(D): f = D["financement"]; h = f["hypotheque"] assert f["contrainte"] == "RCD" and f["pret"] == pytest.approx(f["pret_rcd"]) assert f["rcd"] == pytest.approx(1.20) and f["rpv"] <= 0.75 assert h.taux_periodique == pytest.approx((1 + 0.0412 / 2) ** (1 / 6) - 1) assert h.service_dette_annuel == pytest.approx(12 * h.paiement) assert h.solde(480) == pytest.approx(0.0, abs=1e-6) ff = D["financement_final"] assert fin.van(ff["flux_fonds_propres"], ff["tri_fonds_propres"]) == pytest.approx(0.0, abs=1e-3) # VAN nulle au TRI def test_projection_vectorisee_reproduit_cas3(D): """La projection vectorisée (Monte-Carlo) doit coïncider exactement avec cas3.projection_dcf aux paramètres de base.""" h, b, e = D["hyp"], D["base"], D["etat"] one = lambda x: np.array([x]) 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))) p = D["projection"] assert np.allclose(proj["rne"][0], p["rne"].values, rtol=1e-12) assert np.allclose(proj["flux"][0], p.loc[p["annee"] <= 10, "flux_avant_dette"].values, rtol=1e-12) v = ch.valeur_vectorisee(proj, one(h["taux_actualisation"]), one(h["taux_terminal"]), h["couts_vente"]) assert v["valeur"][0] == pytest.approx(D["dcf"]["valeur"], rel=1e-12) assert D["tornade"]["V_base"].iloc[0] == pytest.approx(D["dcf"]["valeur"], rel=1e-9) def test_droits_mutation_montreal(): assert ch.droits_mutation_montreal(2_060_000) == pytest.approx(41_325.50) assert ch.droits_mutation_montreal(62_900) == pytest.approx(314.50) assert ch.droits_mutation_montreal(0) == 0.0 # ----------------------------------------------------------------------------- valeurs citées dans ch47.tex (tolérance ±1 $ sauf indication) def test_valeurs_citees(D): b, tx, d, f, rec, cu = D["base"], D["taux"], D["dcf"], D["financement"], D["reconciliation"], D["comparaison_unite"] assert b["rbp_contrat"] == pytest.approx(165_540, abs=1) and b["rbp_marche"] == pytest.approx(186_036, abs=1) assert b["rbe"] == pytest.approx(168_443.94, abs=0.01) assert b["depenses_hors_reserve"] == pytest.approx(70_385.19, abs=0.01) and b["reserve"] == pytest.approx(8_103, abs=0.01) assert b["rne"] == pytest.approx(89_955.75, abs=0.01) 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) assert tx["q1"] == pytest.approx(0.041525, abs=1e-6) and tx["q3"] == pytest.approx(0.046375, abs=1e-6) assert D["valeur_capitalisation"] == pytest.approx(2_037_502.89, abs=0.01) 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) 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) 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) 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) assert f["hypotheque"].solde(60) == pytest.approx(1_394_739.19, abs=0.01) 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) 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) assert cu["mrb_mediane"] == pytest.approx(11.94, abs=1e-9) and cu["indication_mrb"] == pytest.approx(1_976_547.60, abs=0.01) 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) assert rec["valeur_ponderee"] == pytest.approx(2_064_054.90, abs=0.01) and rec["valeur_finale"] == 2_060_000 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) ff = D["financement_final"] 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) 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) sc = D["scenarios"] 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) aph = D["aph"] assert aph.loc[aph["amortissement_ans"] == 40, "pret"].iloc[0] == pytest.approx(1_610_137.58, abs=0.01) def test_monte_carlo_citations_et_reproductibilite(D): mc = D["mc"]; st = mc["stats"]; pr = mc["probabilites"] assert mc["n"] == 10_000 and len(mc["valeur"]) == 10_000 assert st["q05"] < st["q25"] < st["mediane"] < st["q75"] < st["q95"] 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) assert st["moyenne"] == pytest.approx(2_233_505.68, abs=1) and st["ecart_type"] == pytest.approx(350_442.57, abs=1) 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) assert pr["V_sous_cap"] == pytest.approx(0.3094, abs=1e-4) and pr["V_sous_role"] == pytest.approx(0.5118, abs=1e-4) # même graine → résultats identiques ; graine différente → distribution voisine (±1,5 % sur la médiane) mc2 = ch.monte_carlo({k: D[k] for k in ["etat", "hyp", "base", "financement", "valeur_capitalisation"]}, 10_000, ch.GRAINE) assert np.array_equal(mc2["valeur"], mc["valeur"]) mc3 = ch.monte_carlo({k: D[k] for k in ["etat", "hyp", "base", "financement", "valeur_capitalisation"]}, 10_000, 7) assert mc3["stats"]["mediane"] == pytest.approx(st["mediane"], rel=0.015) # la médiane du RCD de première année est au seuil 1,20 (prêt calibré au seuil) assert float(np.median(mc["rcd"][:, 0])) == pytest.approx(1.20, abs=0.02) def test_exercices(D): ex = ch.exercices(D, D["comparaison_unite"], D["reconciliation"]) 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) assert ex["ex2"]["rne"] == pytest.approx(88_949.51, abs=0.01) 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) 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) assert ex["ex5"]["pret"] == pytest.approx(1_283_814.77, abs=0.01) and ex["ex5"]["contrainte"] == "RCD" 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) 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)