SPB Git
13.6 KB · 303 lines python
1"""Mathématiques financières de l'évaluation immobilière (conventions déclarées).23Conventions :4- Les taux annuels sont des proportions (0.05 = 5 %).5- Hypothèques canadiennes à taux fixe : intérêt composé SEMESTRIELLEMENT, non à l'avance (Loi sur l'intérêt,6  L.R.C. 1985, c. I-15, art. 6 : le taux doit être exprimé en taux annuel calculé semestriellement ou annuellement).7  Le taux périodique mensuel équivalent est i_m = (1 + i/2)^(1/6) - 1.8- Flux en FIN de période sauf indication contraire ; t = 1 … T ; V_0 à t = 0.9"""10from __future__ import annotations11from dataclasses import dataclass12from typing import Iterable, Sequence13import math1415# ----------------------------------------------------------------------------- taux1617def taux_periodique(i_nominal: float, m_composition: int, m_paiements: int) -> float:18    """Taux périodique équivalent à un taux nominal i composé m_composition fois/an, pour m_paiements paiements/an.19    i_p = (1 + i/m_c)^(m_c/m_p) - 1."""20    return (1.0 + i_nominal / m_composition) ** (m_composition / m_paiements) - 1.02122def taux_mensuel_canadien(i_nominal: float) -> float:23    """Taux mensuel d'une hypothèque canadienne (composition semestrielle)."""24    return taux_periodique(i_nominal, 2, 12)2526def taux_effectif_annuel(i_nominal: float, m: int) -> float:27    return (1.0 + i_nominal / m) ** m - 1.02829def taux_nominal_depuis_effectif(i_eff: float, m: int) -> float:30    return m * ((1.0 + i_eff) ** (1.0 / m) - 1.0)3132# ----------------------------------------------------------------------------- facteurs3334def facteur_va(r: float, n: int) -> float:35    """Facteur de valeur actuelle d'un dollar : 1/(1+r)^n."""36    return (1.0 + r) ** (-n)3738def facteur_va_annuite(r: float, n: int) -> float:39    """Valeur actuelle d'une annuité de 1 $ en fin de période pendant n périodes."""40    if abs(r) < 1e-14:41        return float(n)42    return (1.0 - (1.0 + r) ** (-n)) / r4344def facteur_recouvrement(r: float, n: int) -> float:45    """Facteur de recouvrement du capital (annuité équivalente à 1 $ présent) = 1/facteur_va_annuite."""46    return 1.0 / facteur_va_annuite(r, n)4748def facteur_fonds_amortissement(r: float, n: int) -> float:49    """Sinking fund factor : versement périodique qui accumule 1 $ après n périodes."""50    if abs(r) < 1e-14:51        return 1.0 / n52    return r / ((1.0 + r) ** n - 1.0)5354def facteur_vf(r: float, n: int) -> float:55    return (1.0 + r) ** n5657# ----------------------------------------------------------------------------- VAN / TRI5859def van(flux: Sequence[float], r: float, t0: int = 0) -> float:60    """Valeur actuelle nette : flux[k] survient à t = t0 + k (par défaut flux[0] à t = 0)."""61    return sum(cf / (1.0 + r) ** (t0 + k) for k, cf in enumerate(flux))6263def tri(flux: Sequence[float], borne_inf: float = -0.99, borne_sup: float = 10.0, tol: float = 1e-10, max_iter: int = 500) -> float:64    """Taux de rendement interne par bissection (robuste) puis raffinement de Newton.65    Exige un changement de signe de la VAN sur [borne_inf, borne_sup]."""66    f = lambda r: van(flux, r)67    lo, hi = borne_inf, borne_sup68    flo, fhi = f(lo), f(hi)69    if flo * fhi > 0:70        raise ValueError("Pas de changement de signe de la VAN : TRI non défini sur l'intervalle")71    for _ in range(max_iter):72        mid = 0.5 * (lo + hi)73        fm = f(mid)74        if abs(fm) < tol or (hi - lo) < tol:75            break76        if flo * fm < 0:77            hi, fhi = mid, fm78        else:79            lo, flo = mid, fm80    r = 0.5 * (lo + hi)81    for _ in range(20):  # Newton82        d = sum(-k * cf / (1.0 + r) ** (k + 1) for k, cf in enumerate(flux))83        if abs(d) < 1e-14:84            break85        r_new = r - f(r) / d86        if not (borne_inf < r_new < borne_sup):87            break88        if abs(r_new - r) < 1e-14:89            r = r_new90            break91        r = r_new92    return r9394def tri_modifie(flux: Sequence[float], taux_financement: float, taux_reinvestissement: float) -> float:95    """TRI modifié (MIRR) : flux négatifs actualisés au taux de financement, positifs capitalisés au taux de réinvestissement."""96    n = len(flux) - 197    va_neg = sum(cf / (1 + taux_financement) ** k for k, cf in enumerate(flux) if cf < 0)98    vf_pos = sum(cf * (1 + taux_reinvestissement) ** (n - k) for k, cf in enumerate(flux) if cf > 0)99    if va_neg == 0 or vf_pos == 0:100        raise ValueError("MIRR exige des flux négatifs et positifs")101    return (vf_pos / -va_neg) ** (1.0 / n) - 1.0102103def delai_recuperation(flux: Sequence[float]) -> float | None:104    """Délai de récupération simple (interpolation linéaire), None si jamais récupéré."""105    cum = 0.0106    for k, cf in enumerate(flux):107        prev = cum108        cum += cf109        if k > 0 and prev < 0 <= cum:110            return (k - 1) + (-prev / cf if cf else 0.0)111    return None112113# ----------------------------------------------------------------------------- hypothèques114115@dataclass(frozen=True)116class Hypotheque:117    capital: float118    taux_nominal: float          # annuel, composé semestriellement (Canada)119    amortissement_mois: int120    composition: int = 2         # 2 = semestriel (Canada) ; 12 = mensuel (É.-U.)121    paiements_par_an: int = 12122123    @property124    def taux_periodique(self) -> float:125        return taux_periodique(self.taux_nominal, self.composition, self.paiements_par_an)126127    @property128    def paiement(self) -> float:129        """Paiement périodique constant (fin de période)."""130        return self.capital * facteur_recouvrement(self.taux_periodique, self.amortissement_mois)131132    @property133    def service_dette_annuel(self) -> float:134        return self.paiement * self.paiements_par_an135136    @property137    def constante_hypothecaire(self) -> float:138        """f = service de la dette annuel / capital (mortgage constant)."""139        return self.service_dette_annuel / self.capital140141    def solde(self, apres_paiements: int) -> float:142        """Solde du capital après k paiements (formule prospective)."""143        i, n = self.taux_periodique, self.amortissement_mois144        k = apres_paiements145        if i == 0:146            return self.capital * (1 - k / n)147        return self.capital * ((1 + i) ** n - (1 + i) ** k) / ((1 + i) ** n - 1)148149    def tableau(self, nb_paiements: int | None = None) -> list[dict]:150        """Tableau d'amortissement : période, paiement, intérêt, capital, solde."""151        n = nb_paiements or self.amortissement_mois152        i, pmt = self.taux_periodique, self.paiement153        solde = self.capital154        rows = []155        for k in range(1, n + 1):156            interet = solde * i157            cap = pmt - interet158            solde = solde - cap159            if k == self.amortissement_mois:160                cap += solde; solde = 0.0  # absorbe l'arrondi final161            rows.append({"periode": k, "paiement": pmt, "interet": interet, "capital": cap, "solde": max(solde, 0.0)})162        return rows163164    def interets_annee(self, annee: int) -> float:165        rows = self.tableau()166        return sum(r["interet"] for r in rows[(annee - 1) * 12: annee * 12])167168    def capital_annee(self, annee: int) -> float:169        rows = self.tableau()170        return sum(r["capital"] for r in rows[(annee - 1) * 12: annee * 12])171172def pret_max_par_rcd(noi: float, rcd_min: float, taux_nominal: float, amortissement_mois: int, composition: int = 2) -> float:173    """Prêt maximal tel que RCD = RNE / service de la dette >= rcd_min."""174    sd_max = noi / rcd_min175    f = Hypotheque(1.0, taux_nominal, amortissement_mois, composition).constante_hypothecaire176    return sd_max / f177178def pret_max_par_rpv(valeur: float, rpv_max: float) -> float:179    return valeur * rpv_max180181def rcd(noi: float, service_dette: float) -> float:182    """Ratio de couverture de la dette (DSCR)."""183    return noi / service_dette184185def rpv(pret: float, valeur: float) -> float:186    """Ratio prêt-valeur (LTV)."""187    return pret / valeur188189def taux_admissible_minimal(taux_contrat: float, plancher: float = 0.0525, marge: float = 0.02) -> float:190    """Taux admissible minimal (test de résistance BSIF B-20 / assuré) : max(taux + marge, plancher). Paramètres à vérifier (sources/notes-C.md)."""191    return max(taux_contrat + marge, plancher)192193# ----------------------------------------------------------------------------- revenus194195def revenu_brut_effectif(rbp: float, taux_vacance_pertes: float, autres_revenus: float = 0.0) -> float:196    """RBE = RBP × (1 − v) + autres revenus."""197    return rbp * (1.0 - taux_vacance_pertes) + autres_revenus198199def rne(rbe: float, depenses_exploitation: float, reserves: float = 0.0) -> float:200    """RNE = RBE − dépenses d'exploitation − réserves (si la convention retenue les inclut)."""201    return rbe - depenses_exploitation - reserves202203def ratio_depenses(depenses: float, rbe: float) -> float:204    return depenses / rbe205206def multiplicateur_revenu_brut(prix: float, revenu_brut: float) -> float:207    return prix / revenu_brut208209# ----------------------------------------------------------------------------- capitalisation210211def capitalisation_directe(noi_1: float, taux: float) -> float:212    """V = RNE_1 / R."""213    if taux <= 0:214        raise ValueError("taux de capitalisation doit être > 0")215    return noi_1 / taux216217def taux_global_extrait(noi_1: float, prix: float) -> float:218    return noi_1 / prix219220def taux_bande_investissement(rpv: float, constante_hyp: float, rendement_fonds_propres: float) -> float:221    """R = M·f + (1 − M)·R_E (band of investment : pondération dette/fonds propres)."""222    return rpv * constante_hyp + (1.0 - rpv) * rendement_fonds_propres223224def taux_par_rcd(rcd_cible: float, rpv: float, constante_hyp: float) -> float:225    """Formule de couverture de la dette : R = RCD × M × f."""226    return rcd_cible * rpv * constante_hyp227228def taux_gordon(r: float, g: float) -> float:229    """R = r − g (croissance constante perpétuelle du RNE, r > g)."""230    if r <= g:231        raise ValueError("r doit excéder g")232    return r - g233234def taux_ellwood(y_e: float, rpv: float, constante_hyp: float, part_remboursee: float, n: int, variation_valeur: float) -> float:235    """Taux global d'Ellwood (sans financement de classe J) :236    R = Y_E − M·(Y_E + P·SFF − f) − Δ·SFF, SFF au taux Y_E sur n ans, P = part du capital remboursée, Δ = variation relative de la valeur."""237    sff = facteur_fonds_amortissement(y_e, n)238    return y_e - rpv * (y_e + part_remboursee * sff - constante_hyp) - variation_valeur * sff239240def taux_akerson(y_e: float, rpv: float, constante_hyp: float, part_remboursee: float, n: int, variation_valeur: float) -> float:241    """Formulation d'Akerson (équivalente à Ellwood) : R = M·f + (1−M)·Y_E − M·P·SFF − Δ·SFF."""242    sff = facteur_fonds_amortissement(y_e, n)243    return rpv * constante_hyp + (1 - rpv) * y_e - rpv * part_remboursee * sff - variation_valeur * sff244245# ----------------------------------------------------------------------------- DCF246247@dataclass248class ResultatDCF:249    valeur: float250    va_flux: float251    va_terminale: float252    valeur_terminale_brute: float253    valeur_terminale_nette: float254    flux: list[float]255256def valeur_terminale(noi_suivant: float, taux_terminal: float, couts_vente: float = 0.0) -> tuple[float, float]:257    """(VT brute, VT nette) : VT = RNE_{T+1}/R_T ; nette = brute × (1 − c_v)."""258    brute = capitalisation_directe(noi_suivant, taux_terminal)259    return brute, brute * (1.0 - couts_vente)260261def dcf(flux: Sequence[float], taux_actualisation: float, noi_suivant: float | None = None, taux_terminal: float | None = None,262        couts_vente: float = 0.0, valeur_terminale_nette: float | None = None) -> ResultatDCF:263    """V_0 = Σ_{t=1}^{T} CF_t/(1+r)^t + VT_nette/(1+r)^T. flux[0] est le flux de l'année 1.264    Fournir soit (noi_suivant, taux_terminal) soit valeur_terminale_nette."""265    T = len(flux)266    va_flux = sum(cf / (1 + taux_actualisation) ** (t + 1) for t, cf in enumerate(flux))267    if valeur_terminale_nette is None:268        if noi_suivant is None or taux_terminal is None:269            raise ValueError("fournir noi_suivant et taux_terminal, ou valeur_terminale_nette")270        vt_b, vt_n = valeur_terminale(noi_suivant, taux_terminal, couts_vente)271    else:272        vt_n = valeur_terminale_nette; vt_b = vt_n / (1 - couts_vente) if couts_vente < 1 else vt_n273    va_vt = vt_n / (1 + taux_actualisation) ** T274    return ResultatDCF(va_flux + va_vt, va_flux, va_vt, vt_b, vt_n, list(flux))275276def projeter_croissance(valeur_initiale: float, g: float | Sequence[float], n: int) -> list[float]:277    """Série de n valeurs à partir de valeur_initiale (année 1) avec croissance g (scalaire ou par année)."""278    gs = [g] * (n - 1) if isinstance(g, (int, float)) else list(g)279    out = [valeur_initiale]280    for k in range(n - 1):281        out.append(out[-1] * (1 + gs[k]))282    return out283284def flux_apres_financement(noi: Sequence[float], hyp: Hypotheque) -> list[float]:285    """Flux de trésorerie avant impôt pour l'investisseur : RNE_t − service de la dette."""286    sd = hyp.service_dette_annuel287    return [x - sd for x in noi]288289def rendement_fonds_propres_levier(noi: Sequence[float], prix: float, hyp: Hypotheque, valeur_revente_nette: float) -> dict:290    """TRI des fonds propres avec levier : mise de fonds à t=0, flux après dette, produit net de revente moins solde."""291    T = len(noi)292    mise = prix - hyp.capital293    flux = [-mise] + flux_apres_financement(noi, hyp)294    flux[-1] += valeur_revente_nette - hyp.solde(T * 12)295    return {"flux": flux, "tri": tri(flux), "mise_de_fonds": mise, "solde_final": hyp.solde(T * 12)}296297def rendement_cash_on_cash(flux_annee1: float, mise_de_fonds: float) -> float:298    return flux_annee1 / mise_de_fonds299300def ratio_point_mort(depenses: float, service_dette: float, rbp: float) -> float:301    """(DE + SD) / RBP : taux d'occupation économique minimal."""302    return (depenses + service_dette) / rbp303
Files · faf80546624105e8fab6f9b3544b7f1ede4bc34a
Menu

Theme