"""Chapitre 2 — Droits immobiliers et bases de valeur : identités (additivité, Gordon, annuité) et valeurs citées dans le texte, les tableaux et les solutions (tolérances déclarées). Toutes les grandeurs du cas 3 proviennent de traite.cas3 (module partagé).""" import sys, pathlib, math import numpy as np, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) import ch02_droits as c2 from traite import cas3, finance as fin @pytest.fixture(scope="module") def res(): return c2.main(verbose=False) # ----------------------------------------------------------------------------- identités def test_additivite_droit_temporaire_et_reversion(): """V_pleine = V_droit temporaire + V_réversion lorsque la réversion est la capitalisation du revenu suivant (éq. 2.6, Gordon).""" for n in (1, 5, 15, 40): u = c2.usufruit_nue_propriete(30000.0, 0.01, 0.06, n) assert u["somme"] == pytest.approx(u["V_pleine"], rel=1e-12) assert 0 < u["part_usufruit"] < 1 # croissance nulle : usufruit = annuité, réversion = V/(1+r)^n d = c2.valeur_droit_temporaire_et_reversion(30000.0, 15, 0.06, 500000.0) assert d["droit_temporaire"] == pytest.approx(30000 * fin.facteur_va_annuite(0.06, 15)) assert d["total"] == pytest.approx(500000.0) # 30 000/0,06 = 500 000 : la somme redonne la perpétuité def test_formule_fermee_rotation(): """Σ Δ[(1−ρ)(1+g)]^{t−1}/(1+r)^t = Δ/(r − g + ρ(1+g)) ; sans rotation = Δ/(r−g) ; cohérence somme finie / infinie.""" D, r, g = 19697.0, 0.0725, 0.035 for rho in (0.0, 0.05, 0.15, 0.5): brute = sum(D * ((1 - rho) * (1 + g)) ** (t - 1) / (1 + r) ** t for t in range(1, 5000)) assert c2.va_ecart_rotation(D, r, g, rho) == pytest.approx(brute, rel=1e-9) assert c2.va_ecart_rotation(D, r, g, 0.0) == pytest.approx(D / (r - g)) assert c2.va_ecart_rotation(D, r, g, 0.15, T=10) < c2.va_ecart_rotation(D, r, g, 0.15) with pytest.raises(ValueError): c2.va_ecart_rotation(D, 0.02, 0.035, 0.0) def test_valeur_bail_locataire_annuite(): """éq. 2.4 : annuité ; croissante en n ; tend vers Δ/r.""" v5 = c2.valeur_bail_locataire(3828.0, 5, 0.0725); v10 = c2.valeur_bail_locataire(3828.0, 10, 0.0725) assert v5 == pytest.approx(sum(3828 / 1.0725 ** t for t in range(1, 6)), rel=1e-12) assert v5 < v10 < 3828 / 0.0725 assert c2.valeur_bail_locataire(3828.0, 10000, 0.0725) == pytest.approx(3828 / 0.0725, rel=1e-6) # ----------------------------------------------------------------------------- valeurs citées (cas 3) def test_ecart_contractuel(res): r = res["revenus"]; e = res["ecarts"] assert r["rbp_contrat"] == pytest.approx(165540) and r["rbp_marche"] == pytest.approx(186036) and r["ecart_marche"] == pytest.approx(20496) assert res["ferme"]["ecart_net_annee1"] == pytest.approx(20496 * (1 - 0.039), abs=0.5) assert round(res["ferme"]["ecart_net_annee1"]) == 19697 assert r["ecart_marche"] / r["rbp_marche"] == pytest.approx(0.110, abs=0.0005) l103 = e.loc[e["logement"] == 103].iloc[0] assert l103["ecart_mensuel"] == 319 and l103["ecart_annuel"] == 3828 and l103["anciennete"] == 18 assert l103["ratio_contrat_marche"] == pytest.approx(0.695, abs=0.0005) assert e["ratio_contrat_marche"].mean() == pytest.approx(0.884, abs=0.0005) assert e["ratio_contrat_marche"].max() == pytest.approx(0.980, abs=0.0005) assert e["ecart_annuel"].sum() == 20496 and len(e) == 12 # tableau 2.4, colonne écart annuel assert list(e["ecart_annuel"].astype(int)) == [1176, 2592, 3828, 264, 2412, 588, 2460, 2244, 336, 2640, 1152, 804] def test_decomposition_dcf(res): b = res["base"] # cohérence avec le module partagé (valeurs des ch. 29/30/47) t = cas3.tout() assert b["V_locateur"] == pytest.approx(t["dcf"]["valeur"], rel=1e-12) assert b["V_locateur"] == pytest.approx(2208566, abs=1) assert b["V_pleine"] == pytest.approx(2235712, abs=1) assert b["V_locataires"] == pytest.approx(27146, abs=1) assert b["V_locataires"] == pytest.approx(b["V_pleine"] - b["V_locateur"], rel=1e-12) assert b["part_locataires"] == pytest.approx(0.0121, abs=0.00005) assert b["rne1_contrat"] == pytest.approx(89956, abs=1) and b["rne1_marche"] == pytest.approx(108865, abs=1) assert b["rbe_contrat"] == pytest.approx(168444, abs=1) and b["rbe_marche"] == pytest.approx(188141, abs=1) assert b["va_flux_contrat"] == pytest.approx(662410, abs=1) and b["va_flux_marche"] == pytest.approx(699966, abs=1) assert b["va_vt_contrat"] == pytest.approx(1546156, abs=1) and b["va_vt_marche"] == pytest.approx(1535746, abs=1) pc, pm = b["proj_contrat"], b["proj_marche"] ecart_rne = (pm["rne"] - pc["rne"]).round(0).astype(int).tolist() assert ecart_rne[:6] == [18909, 13166, 8821, 3692, 1241, -394] assert ecart_rne[10] == -1075 ecart_rbp = (pm["rbp"] - pc["rbp"]).round(0).astype(int).tolist() assert ecart_rbp[:3] == [20496, 14545, 10048] assert abs(b["va_vt_marche"] / b["va_vt_contrat"] - 1) < 0.01 # valeurs terminales à moins de 1 % def test_capitalisation_naive_et_formules(res): c = res["cap"]; f = res["ferme"] assert c["Vcap_contrat"] == pytest.approx(2037503, abs=1) and c["Vcap_marche"] == pytest.approx(2465788, abs=1) assert c["ecart_capitalise"] == pytest.approx(428285, abs=1) assert c["ecart_capitalise"] / res["base"]["V_locataires"] > 15 assert f["perpetuite_sans_rotation"] == pytest.approx(525244, abs=1) assert f["perpetuite_rotation"] == pytest.approx(102188, abs=1) assert 0.0375 + 0.15 * 1.035 == pytest.approx(0.19275) assert f["horizon10_rotation"] == pytest.approx(88094, abs=1) def test_sensibilite(res): s = res["sensibilite"] assert len(s) == 15 def v(rot, r): return float(s[(np.isclose(s["rotation"], rot)) & (np.isclose(s["taux"], r))]["V_locataires"].iloc[0]) assert v(0.0, 0.0725) == pytest.approx(397186, abs=1) assert v(1 / 12, 0.0725) == pytest.approx(59858, abs=1) assert v(2 / 12, 0.0725) == pytest.approx(27146, abs=1) assert v(3 / 12, 0.0725) == pytest.approx(17360, abs=1) assert v(6 / 12, 0.0725) == pytest.approx(5195, abs=1) assert v(0.0, 0.0725) / v(6 / 12, 0.0725) == pytest.approx(76, abs=1) assert v(2 / 12, 0.0625) == pytest.approx(26673, abs=1) and v(2 / 12, 0.0825) == pytest.approx(27527, abs=1) part = s.set_index(["rotation", "taux"])["part"] assert part.max() == pytest.approx(0.1850, abs=0.0001) and part.min() == pytest.approx(0.0017, abs=0.0001) # monotonie : l'écart décroît avec la rotation à taux donné for r in c2.TAUX: col = s[np.isclose(s["taux"], r)].sort_values("rotation")["V_locataires"].tolist() assert all(a > b for a, b in zip(col, col[1:])) # V_locateur très sensible au taux (≈ 15 %), écart peu sensible assert float(s[(np.isclose(s["rotation"], 2 / 12)) & np.isclose(s["taux"], 0.0625)]["V_locateur"].iloc[0]) == pytest.approx(2396754, abs=1) assert float(s[(np.isclose(s["rotation"], 2 / 12)) & np.isclose(s["taux"], 0.0825)]["V_locateur"].iloc[0]) == pytest.approx(2037842, abs=1) # ----------------------------------------------------------------------------- exercices et encadré class TestExercices: def test_ex3_bail_locataire(self, res): e = res["exercices"]["ex3"] assert e["ecart_annuel"] == 3828 and e["va_5ans"] == pytest.approx(15591, abs=1) and e["va_10ans"] == pytest.approx(26578, abs=1) assert e["va_perpetuite"] == pytest.approx(52800, abs=1) assert fin.facteur_va_annuite(0.0725, 5) == pytest.approx(4.0728, abs=0.0001) and fin.facteur_va_annuite(0.0725, 10) == pytest.approx(6.9431, abs=0.0001) def test_ex4_usufruit(self, res): e = res["exercices"]["ex4"] assert e["V_pleine"] == pytest.approx(600000) and e["usufruit"] == pytest.approx(309341, abs=1) and e["nue_propriete"] == pytest.approx(290659, abs=1) assert e["V_n"] == pytest.approx(696581, abs=1) and e["part_usufruit"] == pytest.approx(0.516, abs=0.001) assert (1.01 / 1.06) ** 15 == pytest.approx(0.48443, abs=1e-5) and 1.06 ** -15 == pytest.approx(0.41727, abs=1e-5) v8 = c2.usufruit_nue_propriete(30000.0, 0.01, 0.06, 8) assert v8["usufruit"] == pytest.approx(192361, abs=1) and v8["nue_propriete"] == pytest.approx(407639, abs=1) assert v8["part_usufruit"] == pytest.approx(0.321, abs=0.001) and (1.01 / 1.06) ** 8 == pytest.approx(0.67940, abs=1e-5) def test_ex5_date(self, res): e = res["exercices"]["ex5"] assert e["indice"] == pytest.approx(1.0985, abs=0.0001) and e["valeur_indexee"] == pytest.approx(514084, abs=1) def test_ex6_formule_fermee(self): D, r, g = 19697.0, 0.0725, 0.035 assert c2.va_ecart_rotation(D, r, g, 1 / 12) == pytest.approx(159168, abs=1) assert c2.va_ecart_rotation(D, r, g, 2 / 12) == pytest.approx(93795, abs=1) assert 0.0375 + (1 / 12) * 1.035 == pytest.approx(0.12375, abs=1e-5) assert 1 - (10 / 12) ** 2 == pytest.approx(0.306, abs=0.001) def test_ex7_et_encadre(self, res): e = res["exercices"]["ex7"] assert e["V_sans_hyp"] == pytest.approx(1955638, abs=1) and e["incidence"] == pytest.approx(252928, abs=1) assert e["incidence"] / e["V_hyp"] == pytest.approx(0.1145, abs=0.0005) # ex. 2.7 : CAPEX 2027 de 85 000 $ actualisé un an assert 85000 / 1.0725 == pytest.approx(79254, abs=1) assert (2208566 + 85000 / 1.0725) == pytest.approx(2287820, abs=1) assert (85000 / 1.0725) / 2208566 == pytest.approx(0.036, abs=0.0005)