SPB Git
19.0 KB · 252 lines python
1"""Tests du chapitre 41 — PHMU, terrains et développement (DONNÉES SYNTHÉTIQUES du cas 1 ; zonage FICTIF ; hypothèses déclarées).2Tolérances : 1 $ sur les montants cités arrondis au dollar ; 0,05 point sur les pourcentages à une décimale ; 1e-9 sur les identités.3Les valeurs « citées » sont celles de manuscript/chapters/ch41.tex et de solutions/ch41.tex."""4import sys, pathlib, math5import numpy as np, pytest6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7import ch41_phmu as cas8import ch08_zonage as c089from traite import finance as fin1011TOL = 1.0121314@pytest.fixture(scope="module")15def R():16    return cas.exemple_complet()171819class TestIntrants:20    def test_couts_ch21(self, R):21        cu = R["couts"]22        assert cu["direct"] == pytest.approx(298826.17, abs=0.01) and cu["indirects"] == pytest.approx(62598.68, abs=0.01)23        assert cu["cout_neuf"] == pytest.approx(397567.33, abs=0.01)24        assert cu["di_m2_maison"] == pytest.approx(2190.45, abs=0.01)25        assert cu["ratio_ipcb_plex"] == pytest.approx(118.5 / 116.5, rel=1e-12) and round(cu["ratio_ipcb_plex"], 4) == 1.017226        assert cu["di_m2_plex"] == pytest.approx(2228.06, abs=0.01)2728    def test_prix_sortie(self, R):29        u = R["usages"]30        assert u["S0"]["vente"]["prix_isolee_equiv"] == pytest.approx(637953, abs=TOL)       # prédiction hédonique du sujet (ch. 18)31        assert u["U"]["vente"]["gdv"] == pytest.approx(720727, abs=TOL)32        assert u["D"]["vente"]["prix_isolee_equiv"] == pytest.approx(550726, abs=TOL) and u["D"]["vente"]["prix_unite"] == pytest.approx(495654, abs=TOL)33        assert u["D"]["vente"]["gdv"] == pytest.approx(991307, abs=TOL)34        assert u["T"]["vente"]["prix_isolee_equiv"] == pytest.approx(480573, abs=TOL) and u["T"]["vente"]["prix_unite"] == pytest.approx(408487, abs=TOL)35        assert u["T"]["vente"]["gdv"] == pytest.approx(1225460, abs=TOL)36        assert u["Q"]["vente"]["gdv"] == pytest.approx(1371384, abs=TOL)37        assert u["D"]["vente"]["prix_unite"] == pytest.approx(0.90 * u["D"]["vente"]["prix_isolee_equiv"], rel=1e-12)38        assert u["T"]["vente"]["prix_unite"] == pytest.approx(0.85 * u["T"]["vente"]["prix_isolee_equiv"], rel=1e-12)3940    def test_en_bloc_cas2(self, R):41        tb = R["usages"]["T_bloc"]42        assert tb["prix_par_logement_triplex"] == pytest.approx(281000, abs=TOL) and tb["gdv"] == pytest.approx(843000, abs=TOL)43        assert tb["promoteur"]["valeur_residuelle"] == pytest.approx(-28102, abs=TOL) and tb["promoteur"]["reste"] < 0444546class TestQuatreTests:47    def test_legal(self, R):48        u = R["usages"]49        assert u["U"]["legal"]["logements_admissibles"] == 3 and u["U"]["legal"]["aire_max_m2"] == pytest.approx(372.0)50        assert u["D"]["legal"]["aire_brute_m2"] == pytest.approx(240 / 0.85) and u["D"]["legal"]["permis"]51        assert u["T"]["legal"]["aire_brute_m2"] == pytest.approx(300 / 0.85) and u["T"]["legal"]["permis"]52        assert not u["Q"]["legal"]["permis"] and not u["Q"]["legal"]["ok_cos"] and not u["Q"]["legal"]["ok_logements"]53        assert c08.densite_admissible(620, 150, 3, 0.60, 450)["logements_par_densite"] == 4   # la densité seule admettrait 4 ; le plafond de la grille (3) l'emporte5455    def test_physique(self, R):56        for k in ("U", "D", "T"):57            tp = R["usages"][k]["physique"]58            assert tp["possible"] and tp["etages_requis"] <= 3 and tp["empreinte_max_m2"] == pytest.approx(16 * 16)596061class TestResiduel:62    def test_valeurs_citees_promoteur(self, R):63        u = R["usages"]64        attendu = {"U": (158849, 185550, 6814, 108109, 28829), "D": (190541, 225936, 12290, 148696, 39652), "T": (229265, 275967, 18239, 183819, 49018), "Q": (244935, 299288, 23994, 205708, 54855)}65        for k, (vl, reste, F, marge, frais) in attendu.items():66            r = u[k]["promoteur"]67            assert r["valeur_residuelle"] == pytest.approx(vl, abs=TOL) and r["reste"] == pytest.approx(reste, abs=TOL)68            assert r["financement"] == pytest.approx(F, abs=TOL) and r["marge"] == pytest.approx(marge, abs=TOL) and r["frais_vente"] == pytest.approx(frais, abs=TOL)69        assert u["T"]["promoteur"]["couts_construction"] == pytest.approx(668417, abs=TOL) and u["T"]["promoteur"]["contribution_parcs"] == pytest.approx(22927, abs=TOL)70        assert u["T"]["promoteur"]["v_l_statique"] == pytest.approx(250879, abs=TOL) and u["T"]["promoteur"]["facteur_temps"] == pytest.approx(1.0619 ** 1.5, rel=1e-12)71        assert u["D"]["promoteur"]["couts_construction"] == pytest.approx(534734, abs=TOL) and u["U"]["promoteur"]["couts_construction"] == pytest.approx(361425, abs=TOL)7273    def test_valeurs_citees_occupant(self, R):74        u = R["usages"]75        assert u["U"]["occupant"]["valeur_residuelle"] == pytest.approx(266187, abs=TOL) and u["U"]["occupant"]["marge"] == pytest.approx(39824, abs=TOL)76        assert u["D"]["occupant"]["valeur_residuelle"] == pytest.approx(330792, abs=TOL) and u["T"]["occupant"]["valeur_residuelle"] == pytest.approx(399479, abs=TOL)77        # marge sur le coût = 10 % de (C + D + F)78        r = u["U"]["occupant"]; assert r["marge"] == pytest.approx(0.10 * (r["couts_construction"] + r["demolition"] + r["financement"]), rel=1e-12)7980    def test_identites(self, R):81        r = R["usages"]["T"]["promoteur"]; h = cas.HYP82        # reste = GDV − frais − C − D − F − marge ; V_L(1+τ)(1+r)^T = reste ; parcs = τ V_L83        assert r["reste"] == pytest.approx(r["gdv"] - r["frais_vente"] - r["couts_construction"] - r["demolition"] - r["financement"] - r["marge"], rel=1e-12)84        assert r["valeur_residuelle"] * (1 + h["taux_parcs"]) * r["facteur_temps"] == pytest.approx(r["reste"], rel=1e-12)85        assert r["contribution_parcs"] == pytest.approx(h["taux_parcs"] * r["valeur_residuelle"], rel=1e-12)86        # intérêts intérimaires : I = L i T_eff / 2 avec T_eff = T(1 − part mise de fonds)87        L = h["part_pret"] * (r["couts_construction"] + r["demolition"]); T_eff = 1.5 * (1 - (1 - h["part_pret"]))88        assert r["financement"] == pytest.approx(L * h["taux_pret"] * T_eff / 2, rel=1e-12)89        # forme linéaire V_L = a·GDV − b′ et seuil90        a, b_prime = cas._a_b(r, h)91        assert a * r["gdv"] - b_prime == pytest.approx(r["valeur_residuelle"], rel=1e-10)92        s = cas.seuil_gdv(r, R["v_act"]); r2 = cas.residuel_developpement(s, 300.0, R["couts"]["di_m2_plex"], 1.5, h)93        assert r2["valeur_residuelle"] == pytest.approx(R["v_act"], rel=1e-9)9495    def test_version_statique_egale_ch08(self, R):96        """Avec r = 0, marge et financement en % du GDV et frais de vente nuls, la fonction retrouve valeur_residuelle_redeveloppement du ch. 8."""97        gdv, s_tot, di = 1_000_000.0, 300.0, 2000.098        h = {**cas.HYP, "frais_vente": 0.0, "r_portage": 0.0}99        r = cas.residuel_developpement(gdv, s_tot, di, 1.0, h)100        ref = c08.valeur_residuelle_redeveloppement(3, 100.0, gdv / 300.0, di, 0.0, h["demolition"], h["marge"], r["financement"] / gdv, h["taux_parcs"])101        assert r["valeur_residuelle"] == pytest.approx(ref["valeur_residuelle"], rel=1e-10)102103    def test_seuils_et_conclusion(self, R):104        assert R["seuil_gdv_T"] == pytest.approx(1776390, abs=TOL) and R["hausse_requise_T"] * 100 == pytest.approx(45.0, abs=0.05)105        assert R["seuil_gdv_T_marche"] == pytest.approx(1296396, abs=TOL) and R["hausse_requise_T_marche"] * 100 == pytest.approx(5.8, abs=0.05)106        assert R["phmu_vacant"] == "U" and R["phmu_vacant_promoteur"] == "T" and R["phmu_ameliore"] == "S0"107        assert R["contribution_batiment"] == pytest.approx(323000) and all(p == 0 for p in R["primes"].values())108109    def test_sensibilite_citee(self, R):110        s = R["sensibilite"].set_index("parametre")111        assert s.loc["Prix des unités", "bas"] == pytest.approx(105569, abs=TOL) and s.loc["Prix des unités", "haut"] == pytest.approx(352962, abs=TOL)112        assert s.loc["Coût de construction", "bas"] == pytest.approx(286246, abs=TOL) and s.loc["Coût de construction", "haut"] == pytest.approx(172285, abs=TOL)113        assert s.loc["Marge du promoteur", "bas"] == pytest.approx(280169, abs=TOL) and s.loc["Marge du promoteur", "haut"] == pytest.approx(178362, abs=TOL)114        assert s.loc["Durée totale", "bas"] == pytest.approx(241459, abs=TOL) and s.loc["Durée totale", "haut"] == pytest.approx(217582, abs=TOL)115        assert s.loc["Taux (prêt et portage)", "bas"] == pytest.approx(236137, abs=TOL) and s.loc["Taux (prêt et portage)", "haut"] == pytest.approx(221939, abs=TOL)116        assert s.loc["Contribution pour fins de parcs", "bas"] == pytest.approx(252192, abs=TOL)117        assert s.loc["Frais de vente", "bas"] == pytest.approx(249627, abs=TOL) and s.loc["Frais de vente", "haut"] == pytest.approx(208904, abs=TOL)118        # la hausse de prix de 15 % fait passer le triplex (promoteur) au-dessus de la maison neuve (occupant) : bascule de la PHMU du terrain vacant119        assert s.loc["Prix des unités", "haut"] > R["usages"]["U"]["occupant"]["valeur_residuelle"] > s.loc["Prix des unités", "base"]120121122class TestOption:123    def test_parametres(self, R):124        o = R["option"]125        assert o["u"] == pytest.approx(math.exp(0.10 * math.sqrt(5)), rel=1e-12) and o["d"] == pytest.approx(1 / o["u"], rel=1e-12)126        assert round(o["u"], 4) == 1.2506 and round(o["d"], 4) == 0.7996 and round(o["F"], 4) == 0.8860 and round(o["q"], 4) == 0.1915 and round(o["disc"], 4) == 0.9057127        assert 0 < o["q"] < 1 and o["F"] == pytest.approx(((1.02) / 1.045) ** 5, rel=1e-12)128        assert round(R["a"], 4) == 0.6729 and R["b"] == pytest.approx(1195378, abs=TOL)129130    def test_noeuds_cites(self, R):131        n = R["option"]["noeuds"]132        assert n[(1, 1)]["gdv"] == pytest.approx(1532535, abs=TOL) and n[(1, 0)]["gdv"] == pytest.approx(979914, abs=TOL)133        assert n[(2, 2)]["gdv"] == pytest.approx(1916556, abs=TOL) and n[(2, 1)]["gdv"] == pytest.approx(1225460, abs=TOL) and n[(2, 0)]["gdv"] == pytest.approx(783568, abs=TOL)134        assert n[(2, 2)]["exercice"] == pytest.approx(94322, abs=TOL) and n[(2, 1)]["exercice"] == 0 and n[(2, 0)]["exercice"] == 0135        assert n[(1, 1)]["continuation"] == pytest.approx(16357, abs=TOL) and n[(1, 1)]["exercice"] == 0 and not n[(1, 1)]["exercer"]136        assert n[(0, 0)]["valeur"] == pytest.approx(2837, abs=TOL)137138    def test_prime_et_identites(self, R):139        o = R["option"]140        assert o["prime_americaine"] == pytest.approx(2837, abs=TOL) and o["prime_europeenne"] == pytest.approx(o["prime_americaine"], rel=1e-12)141        assert o["van_immediate"] == 0.0 and o["prime_americaine"] >= o["van_immediate"]142        # valeur actualisée risque-neutre du seul nœud dans la monnaie : q² × 94 322 × disc²143        assert o["prime_americaine"] == pytest.approx(o["q"] ** 2 * o["noeuds"][(2, 2)]["exercice"] * o["disc"] ** 2, rel=1e-10)144        assert o["gdv_seuil_van"] == pytest.approx(R["seuil_gdv_T"], rel=1e-10)145        assert R["frontiere_t1"] == pytest.approx(1874959, abs=TOL) and R["frontiere_t1"] > o["gdv_seuil_van"]146        assert o["prime_americaine"] / R["v_act"] * 100 == pytest.approx(0.47, abs=0.005)147148    def test_sensibilite_option(self, R):149        s = R["sens_option"]150        assert s[600000.0][0.05] == 0.0 and s[600000.0][0.10] == pytest.approx(2837, abs=TOL) and s[600000.0][0.15] == pytest.approx(21420, abs=TOL)151        assert s[600000.0][0.20] == pytest.approx(47939, abs=TOL) and s[600000.0][0.25] == pytest.approx(77556, abs=TOL)152        assert s[450000.0][0.10] == pytest.approx(7348, abs=TOL) and s[300000.0][0.10] == pytest.approx(23568, abs=TOL) and s[300000.0][0.25] == pytest.approx(132208, abs=TOL)153        for v in s:   # la prime croît avec σ et décroît avec la valeur en usage154            vals = list(s[v].values()); assert all(b >= a for a, b in zip(vals, vals[1:]))155        assert s[300000.0][0.15] > s[450000.0][0.15] > s[600000.0][0.15]156157    def test_americaine_superieure_europeenne_quand_exercice_anticipe(self):158        """Avec un rendement de détention élevé et une forte volatilité, l'exercice anticipé a de la valeur."""159        o = cas.option_redeveloppement(1_000_000, 0.7, 500_000, 0.30, 5.0, 0.02, 0.08, 2)160        assert o["prime_americaine"] >= o["prime_europeenne"] and o["prime_americaine"] >= o["van_immediate"]161        assert any(nd.get("exercer") for nd in o["noeuds"].values())162163    def test_frontiere_coherente(self):164        a, b = 0.7, 1_000_000.0165        f = cas.frontiere_exercice_t1(a, b, 0.20, 5.0, 0.02, 0.045)166        p = cas.parametres_binomiaux(0.20, 5.0, 0.02, 0.045)167        cont = p["disc"] * (p["q"] * max(a * f * p["u"] - b, 0) + (1 - p["q"]) * max(a * f * p["d"] - b, 0))168        assert a * f - b == pytest.approx(cont, rel=1e-6) and f >= b / a169170171class TestCapozzaHelsley:172    def test_valeurs_citees(self, R):173        ch = R["capozza"]; cr = ch["resultat"]174        assert ch["R_A"] == pytest.approx(27000, abs=TOL) and ch["R_T"] == pytest.approx(55146, abs=TOL) and ch["C"] == pytest.approx(716656, abs=TOL)175        assert cr["R_certitude"] == pytest.approx(59250, abs=TOL) and cr["prime_irreversibilite"] == pytest.approx(13249, abs=TOL) and cr["R_seuil"] == pytest.approx(72498, abs=TOL)176        assert ch["hausse_requise_certitude"] * 100 == pytest.approx(7.4, abs=0.05) and ch["hausse_requise_incertitude"] * 100 == pytest.approx(31.5, abs=0.05)177178    def test_limite_certitude_et_racine(self):179        r, g, s = 0.045, 551.0, 5515.0180        out = cas.seuil_capozza_helsley(27000, 716656, r, g, s); al = out["alpha"]181        assert 0.5 * s * s * al * al + g * al - r == pytest.approx(0, abs=1e-9)          # α racine positive de ½σ²α² + gα − r = 0182        assert cas.seuil_capozza_helsley(27000, 716656, r, g, 0.0)["R_seuil"] == pytest.approx(27000 + r * 716656, rel=1e-12)183        assert cas.seuil_capozza_helsley(27000, 716656, r, g, 1e-3)["prime_irreversibilite"] == pytest.approx(0.0, abs=1e-6)184        assert out["prime_irreversibilite"] > 0 and cas.seuil_capozza_helsley(27000, 716656, r, g, 2 * s)["prime_irreversibilite"] > out["prime_irreversibilite"]185186187class TestExercices:188    """Chiffres des solutions (solutions/ch41.tex)."""189    def test_ex2_residuel_statique(self):190        # 6 logements de 95 m² à 4 900 $/m² ; coûts 2 300 $/m² ; indirects 15 % ; démolition 45 000 $ ; marge 15 % ; financement 3 % ; parcs 10 %191        r = c08.valeur_residuelle_redeveloppement(6, 95.0, 4900.0, 2300.0, 0.15, 45000.0, 0.15, 0.03, 0.10)192        assert r["gdv"] == pytest.approx(2793000) and r["couts_directs"] == pytest.approx(1311000) and r["couts_indirects"] == pytest.approx(196650)193        assert r["reste_avant_parcs"] == pytest.approx(737610) and r["valeur_residuelle"] == pytest.approx(670554.5, abs=0.1)194        # (b) sans parcs ni financement : 737 610 + 83 790 = 821 400195        r2 = c08.valeur_residuelle_redeveloppement(6, 95.0, 4900.0, 2300.0, 0.15, 45000.0, 0.15, 0.0, 0.0)196        assert r2["valeur_residuelle"] == pytest.approx(821400)197198    def test_ex3_calendrier(self):199        h = {**cas.HYP, "demolition": 0.0}200        r = cas.residuel_developpement(1_800_000, 400.0, 2300.0, 2.0, h)201        assert r["couts_construction"] == pytest.approx(920000) and r["pret"] == pytest.approx(690000)202        assert r["financement"] == pytest.approx(690000 * 0.0619 * 1.5 / 2, rel=1e-12) and r["financement"] == pytest.approx(32033.25, abs=0.01)203        assert r["reste"] == pytest.approx(1800000 - 72000 - 920000 - 32033.25 - 270000, abs=0.01)204        assert r["valeur_residuelle"] == pytest.approx(505966.75 / (1.1 * 1.0619 ** 2), rel=1e-9) and r["valeur_residuelle"] == pytest.approx(407908, abs=TOL)205        r0 = cas.residuel_developpement(1_800_000, 400.0, 2300.0, 2.0, {**h, "r_portage": 0.0})206        assert r0["valeur_residuelle"] == pytest.approx(459970, abs=TOL)207208    def test_ex5_binomial(self):209        o = cas.option_redeveloppement(1_000_000, 0.70, 800_000, 0.15, 5.0, 0.02, 0.04, 2)210        assert round(o["u"], 4) == 1.3985 and round(o["d"], 4) == 0.7150 and round(o["q"], 4) == 0.2815 and round(o["F"], 4) == 0.9075211        assert o["noeuds"][(2, 2)]["exercice"] == pytest.approx(0.70 * 1_000_000 * o["u"] ** 2 - 800_000, rel=1e-12) and o["noeuds"][(2, 2)]["exercice"] == pytest.approx(569089, abs=TOL)212        assert o["prime_americaine"] == pytest.approx(45636, abs=TOL) and o["prime_europeenne"] == pytest.approx(37007, abs=TOL) and o["van_immediate"] == 0.0213        assert o["noeuds"][(1, 1)]["exercice"] == pytest.approx(178960, abs=TOL) and o["noeuds"][(1, 1)]["continuation"] == pytest.approx(145121, abs=TOL)214        assert o["noeuds"][(1, 1)]["exercer"]   # exercer vaut plus qu'attendre au nœud haut : option américaine > européenne215        assert o["gdv_seuil_van"] == pytest.approx(800000 / 0.70, rel=1e-12)216217    def test_ex6_capozza(self):218        out = cas.seuil_capozza_helsley(18000, 600000, 0.05, 400, 4000)219        assert out["R_certitude"] == pytest.approx(48000) and out["prime_irreversibilite"] == pytest.approx(9266, abs=TOL) and out["R_seuil"] == pytest.approx(57266, abs=TOL)220        assert out["R_seuil"] / 42000 - 1 == pytest.approx(0.3635, abs=5e-4)   # rente courante de 42 000 $ : hausse requise de 36,3 %221222    def test_ex4_teardown(self):223        # prix 455 000 $ ; démolition 28 000 $ ; valeur du terrain extraite = 483 000 $ ; contribution du bâtiment −28 000 $224        assert 455000 + 28000 == 483000225        # ratio V_L / P = 1,06 > 1 ; part du bâtiment négative226        assert (455000 - 483000) / 455000 == pytest.approx(-0.0615, abs=1e-4)227228    def test_ex1_grille(self):229        g = c08.densite_admissible(900, 200, 4, 0.70, 450)230        assert g["logements_admissibles"] == 4 and g["aire_plancher_max_m2"] == pytest.approx(630.0)231        assert 6 * 80 / 0.85 == pytest.approx(564.7, abs=0.05) and 4 * 110 / 0.85 == pytest.approx(517.6, abs=0.05)232233    def test_ex7_variantes(self, R):234        h7 = {**cas.HYP, "marge": 0.12, "frais_vente": 0.03}235        attendu = {"U": 183529, "D": 223981, "T": 269989}236        for k, v in attendu.items():237            u = R["usages"][k]238            assert cas.residuel_developpement(u["vente"]["gdv"], u["vente"]["s_total"], u["di_m2"], u["programme"]["duree_ans"], h7)["valeur_residuelle"] == pytest.approx(v, abs=TOL)239        rT = R["usages"]["T"]["promoteur"]; s = cas.seuil_gdv(rT, R["usages"]["U"]["occupant"]["valeur_residuelle"])240        assert s == pytest.approx(1280327, abs=TOL) and (s / rT["gdv"] - 1) * 100 == pytest.approx(4.5, abs=0.05)241        a_, b_ = cas.coefficients_option(rT, 350_000.0)242        assert b_ - 350_000 == pytest.approx(595378, abs=TOL) and b_ / a_ == pytest.approx(1404877, abs=TOL)243        assert cas.option_redeveloppement(rT["gdv"], a_, b_, 0.10, 5.0, 0.02, 0.045, 2)["prime_americaine"] == pytest.approx(14897, abs=TOL)244        assert cas.option_redeveloppement(rT["gdv"], a_, b_, 0.15, 5.0, 0.02, 0.045, 2)["prime_americaine"] == pytest.approx(47091, abs=TOL)245246    def test_ex6_sensibilite_sigma(self):247        assert cas.seuil_capozza_helsley(18000, 600000, 0.05, 400, 2000)["prime_irreversibilite"] == pytest.approx(3483, abs=TOL)248        assert cas.seuil_capozza_helsley(18000, 600000, 0.05, 400, 8000)["prime_irreversibilite"] == pytest.approx(21612, abs=TOL)249250    def test_marges(self):251        assert fin.marge_sur_prix(0.10) == pytest.approx(1 / 11) and fin.marge_sur_cout(0.15) == pytest.approx(0.15 / 0.85)252
Files · 81b1896e4502a619cf4f142fb8809253a8ff0bf3
Menu

Theme