SPB Git
16.9 KB · 361 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# ----------------------------------------------------------------------------- financement intérimaire (construction)194195def interets_intermediaires(pret: float, taux: float, duree_ans: float, mode: str = "continu", capitaliser: bool = False,196                            mise_de_fonds: float = 0.0, cout_total: float | None = None) -> dict:197    """Intérêts intérimaires d'un prêt de construction tiré de façon UNIFORME sur la durée du chantier.198199    Convention : le prêt `pret` (L) est déboursé linéairement entre t0 et T (années) ; le taux `taux` (i) est un taux200    annuel simple appliqué au solde. Si `mise_de_fonds` (Eq) > 0 et `cout_total` (C) est fourni, la mise de fonds201    finance les premiers déboursés et le prêt ne commence qu'à t0 = T × Eq / C (sinon t0 = 0).202      mode = 'continu'  : I = L · i · (T − t0) / 2                       (solde moyen = L/2)203      mode = 'mensuel'  : n = 12 (T − t0) mois, tirage de L/n en début de mois, intérêt (i/12) sur le solde ;204                          sans capitalisation I = L · i · (T − t0)/2 · (n + 1)/n ; avec `capitaliser=True`,205                          les intérêts mensuels sont ajoutés au solde (prêt à intérêts capitalisés).206    Retourne un dict : interets, duree_effective_ans, t0, solde_moyen, n_mois, solde_final."""207    if pret < 0 or taux < 0 or duree_ans <= 0:208        raise ValueError("pret >= 0, taux >= 0, duree_ans > 0")209    t0 = 0.0210    if mise_de_fonds > 0:211        if cout_total is None or cout_total <= 0:212            raise ValueError("cout_total requis si mise_de_fonds > 0")213        t0 = duree_ans * min(mise_de_fonds / cout_total, 1.0)214    T = duree_ans - t0215    if T <= 0:216        return {"interets": 0.0, "duree_effective_ans": 0.0, "t0": t0, "solde_moyen": 0.0, "n_mois": 0, "solde_final": pret}217    if mode == "continu":218        interets = pret * taux * T / 2.0219        return {"interets": interets, "duree_effective_ans": T, "t0": t0, "solde_moyen": pret / 2.0, "n_mois": None, "solde_final": pret}220    if mode != "mensuel":221        raise ValueError("mode ∈ {'continu', 'mensuel'}")222    n = max(int(round(12 * T)), 1)223    tirage, im = pret / n, taux / 12.0224    solde, interets, cumul = 0.0, 0.0, 0.0225    for _ in range(n):226        solde += tirage227        it = solde * im228        interets += it229        cumul += solde230        if capitaliser:231            solde += it232    return {"interets": interets, "duree_effective_ans": T, "t0": t0, "solde_moyen": cumul / n, "n_mois": n, "solde_final": solde}233234def taux_duree_implicites(interets: float, pret: float) -> float:235    """Produit i × T (taux annuel × durée en années) impliqué par un montant d'intérêts intérimaires sous tirage uniforme :236    I = L i T / 2  ⇒  i T = 2 I / L."""237    if pret <= 0:238        raise ValueError("pret > 0")239    return 2.0 * interets / pret240241def marge_sur_prix(marge_sur_cout: float) -> float:242    """Convertit une marge exprimée sur le coût (m_c = E / C) en marge sur le prix (m_p = E / (C + E)) : m_p = m_c / (1 + m_c)."""243    return marge_sur_cout / (1.0 + marge_sur_cout)244245def marge_sur_cout(marge_sur_prix_: float) -> float:246    """Inverse : m_c = m_p / (1 − m_p)."""247    if marge_sur_prix_ >= 1:248        raise ValueError("marge sur prix < 1")249    return marge_sur_prix_ / (1.0 - marge_sur_prix_)250251# ----------------------------------------------------------------------------- revenus252253def revenu_brut_effectif(rbp: float, taux_vacance_pertes: float, autres_revenus: float = 0.0) -> float:254    """RBE = RBP × (1 − v) + autres revenus."""255    return rbp * (1.0 - taux_vacance_pertes) + autres_revenus256257def rne(rbe: float, depenses_exploitation: float, reserves: float = 0.0) -> float:258    """RNE = RBE − dépenses d'exploitation − réserves (si la convention retenue les inclut)."""259    return rbe - depenses_exploitation - reserves260261def ratio_depenses(depenses: float, rbe: float) -> float:262    return depenses / rbe263264def multiplicateur_revenu_brut(prix: float, revenu_brut: float) -> float:265    return prix / revenu_brut266267# ----------------------------------------------------------------------------- capitalisation268269def capitalisation_directe(noi_1: float, taux: float) -> float:270    """V = RNE_1 / R."""271    if taux <= 0:272        raise ValueError("taux de capitalisation doit être > 0")273    return noi_1 / taux274275def taux_global_extrait(noi_1: float, prix: float) -> float:276    return noi_1 / prix277278def taux_bande_investissement(rpv: float, constante_hyp: float, rendement_fonds_propres: float) -> float:279    """R = M·f + (1 − M)·R_E (band of investment : pondération dette/fonds propres)."""280    return rpv * constante_hyp + (1.0 - rpv) * rendement_fonds_propres281282def taux_par_rcd(rcd_cible: float, rpv: float, constante_hyp: float) -> float:283    """Formule de couverture de la dette : R = RCD × M × f."""284    return rcd_cible * rpv * constante_hyp285286def taux_gordon(r: float, g: float) -> float:287    """R = r − g (croissance constante perpétuelle du RNE, r > g)."""288    if r <= g:289        raise ValueError("r doit excéder g")290    return r - g291292def taux_ellwood(y_e: float, rpv: float, constante_hyp: float, part_remboursee: float, n: int, variation_valeur: float) -> float:293    """Taux global d'Ellwood (sans financement de classe J) :294    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."""295    sff = facteur_fonds_amortissement(y_e, n)296    return y_e - rpv * (y_e + part_remboursee * sff - constante_hyp) - variation_valeur * sff297298def taux_akerson(y_e: float, rpv: float, constante_hyp: float, part_remboursee: float, n: int, variation_valeur: float) -> float:299    """Formulation d'Akerson (équivalente à Ellwood) : R = M·f + (1−M)·Y_E − M·P·SFF − Δ·SFF."""300    sff = facteur_fonds_amortissement(y_e, n)301    return rpv * constante_hyp + (1 - rpv) * y_e - rpv * part_remboursee * sff - variation_valeur * sff302303# ----------------------------------------------------------------------------- DCF304305@dataclass306class ResultatDCF:307    valeur: float308    va_flux: float309    va_terminale: float310    valeur_terminale_brute: float311    valeur_terminale_nette: float312    flux: list[float]313314def valeur_terminale(noi_suivant: float, taux_terminal: float, couts_vente: float = 0.0) -> tuple[float, float]:315    """(VT brute, VT nette) : VT = RNE_{T+1}/R_T ; nette = brute × (1 − c_v)."""316    brute = capitalisation_directe(noi_suivant, taux_terminal)317    return brute, brute * (1.0 - couts_vente)318319def dcf(flux: Sequence[float], taux_actualisation: float, noi_suivant: float | None = None, taux_terminal: float | None = None,320        couts_vente: float = 0.0, valeur_terminale_nette: float | None = None) -> ResultatDCF:321    """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.322    Fournir soit (noi_suivant, taux_terminal) soit valeur_terminale_nette."""323    T = len(flux)324    va_flux = sum(cf / (1 + taux_actualisation) ** (t + 1) for t, cf in enumerate(flux))325    if valeur_terminale_nette is None:326        if noi_suivant is None or taux_terminal is None:327            raise ValueError("fournir noi_suivant et taux_terminal, ou valeur_terminale_nette")328        vt_b, vt_n = valeur_terminale(noi_suivant, taux_terminal, couts_vente)329    else:330        vt_n = valeur_terminale_nette; vt_b = vt_n / (1 - couts_vente) if couts_vente < 1 else vt_n331    va_vt = vt_n / (1 + taux_actualisation) ** T332    return ResultatDCF(va_flux + va_vt, va_flux, va_vt, vt_b, vt_n, list(flux))333334def projeter_croissance(valeur_initiale: float, g: float | Sequence[float], n: int) -> list[float]:335    """Série de n valeurs à partir de valeur_initiale (année 1) avec croissance g (scalaire ou par année)."""336    gs = [g] * (n - 1) if isinstance(g, (int, float)) else list(g)337    out = [valeur_initiale]338    for k in range(n - 1):339        out.append(out[-1] * (1 + gs[k]))340    return out341342def flux_apres_financement(noi: Sequence[float], hyp: Hypotheque) -> list[float]:343    """Flux de trésorerie avant impôt pour l'investisseur : RNE_t − service de la dette."""344    sd = hyp.service_dette_annuel345    return [x - sd for x in noi]346347def rendement_fonds_propres_levier(noi: Sequence[float], prix: float, hyp: Hypotheque, valeur_revente_nette: float) -> dict:348    """TRI des fonds propres avec levier : mise de fonds à t=0, flux après dette, produit net de revente moins solde."""349    T = len(noi)350    mise = prix - hyp.capital351    flux = [-mise] + flux_apres_financement(noi, hyp)352    flux[-1] += valeur_revente_nette - hyp.solde(T * 12)353    return {"flux": flux, "tri": tri(flux), "mise_de_fonds": mise, "solde_final": hyp.solde(T * 12)}354355def rendement_cash_on_cash(flux_annee1: float, mise_de_fonds: float) -> float:356    return flux_annee1 / mise_de_fonds357358def ratio_point_mort(depenses: float, service_dette: float, rbp: float) -> float:359    """(DE + SD) / RBP : taux d'occupation économique minimal."""360    return (depenses + service_dette) / rbp361
Files · 185d04a62ff66f3d004407340590d377c512efce
Menu

Theme