SPB Git
26.1 KB · 353 lines python
1#!/usr/bin/env python32"""Chapitre 42 — Copropriétés et conversions : exemples chiffrés (DONNÉES SYNTHÉTIQUES, graine 20261031).34Réutilise l'immeuble fictif de huit fractions du chapitre 7 (analysis/python/cases/ch07_servitudes_copro.py : quotes-parts,5répartition des charges, cotisation de seuil, déficit de capitalisation) et y ajoute quatre blocs, tous générés ici6(aucun fichier externe ; aucune statistique de marché n'est reproduite) :7  A. Évaluation de la fraction F4 par comparaison : deux ventes dans l'immeuble, trois ventes dans d'autres immeubles fictifs ;8     ajustements de date, d'étage, d'orientation, de stationnement, de superficie, de charges courantes (capitalisation) et de9     situation financière du syndicat (déficit du fonds de prévoyance, cotisation spéciale) ; réconciliation (traite.comparables).10  B. Effet d'une cotisation spéciale égale au déficit de capitalisation, répartie selon les quotes-parts (figure).11  C. Copropriété locative : revenu net d'exploitation et taux implicite d'une fraction louée (traite.finance).12  D. Conversion d'un immeuble locatif de huit logements en copropriété divise : valeur en bloc (capitalisation directe) contre13     somme des fractions nette des coûts, des délais et des locataires (rotation, préemption, vente occupée) ; sensibilités.14Toutes les fonctions financières viennent de analysis/python/traite/ (finance.py, comparables.py) ; rien n'est réécrit.15Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch42_copro.py16"""17from __future__ import annotations18import sys, math, pathlib19import numpy as np20HERE = pathlib.Path(__file__).resolve()21sys.path.insert(0, str(HERE.parents[1])); sys.path.insert(0, str(HERE.parent))22from traite import comparables as cp23from traite import finance as fin24import ch07_servitudes_copro as c72526SEED = 2026103127DATE_EVAL = "2026-09-01"2829# ----------------------------------------------------------------------------- marché fictif : paramètres déclarés30# Prix implicites « du marché fictif » (hypothèses pédagogiques ; l'évaluateur les tient d'une analyse de ventes appariées / hédonique).31PRIX_M2_REF = 3_650.0            # $/m² pour une fraction de 98 m², 2e étage, orientation est/ouest, sans stationnement, immeuble bien capitalisé32ELASTICITE_S = 0.60              # élasticité prix-superficie (< 1) ; prix marginal au point de référence = 0,60 × 3 650 = 2 190 $/m²33S_REF = 98.034COEF_ETAGE_MARCHE = {"RDC": -0.04, "2e": 0.00, "3e": 0.02}     # proportion du prix (immeubles sans ascenseur)35COEF_ORIENT_MARCHE = {"nord": -0.03, "est": 0.00, "ouest": 0.00, "sud": 0.03}36STATIONNEMENT = 28_000.0         # $ ; case intérieure à usage restreint37G_MARCHE = 0.02                  # croissance annuelle des prix des copropriétés sur le marché fictif (ajustement de conditions du marché)38R_CHARGES = 0.08                 # taux de capitalisation des écarts de charges courantes (hypothèse pédagogique ; à extraire du marché)39KAPPA = 1.0                      # coefficient de capitalisation du passif du syndicat (1 = dollar pour dollar, convention du ch. 7)40CHARGES_REF_M2 = 40_000.0 / 710.0   # 56,34 $/m²/an : charges courantes de l'immeuble fictif (hors fonds), référence du marché41SIGMA_BRUIT = 0.02               # bruit de transaction (log) dans la génération des prix des comparables4243# Situation financière des syndicats fictifs : déficit de capitalisation (ch. 7) ou cotisation spéciale votée non payée44QP = c7.quotes_parts()45INC = c7.incidence_valeur(QP)                         # déficit de l'immeuble fictif du ch. 7 : 150 908 $46DEFICIT_IMMEUBLE = float(INC["deficit"])47Q_F4 = float(QP["q"][3]); Q_F2 = float(QP["q"][1]); Q_F7 = float(QP["q"][6])4849SUJET = dict(nom="F4 (sujet)", immeuble="A (fictif, 8 fractions)", S=98.0, etage="2e", orient="sud", stat=True,50             charges_m2=CHARGES_REF_M2, passif=Q_F4 * DEFICIT_IMMEUBLE, mois=0.0)5152COMPARABLES = [  # (nom, immeuble, S, étage, orientation, stationnement, charges courantes $/m²/an, passif de la fraction ($), ancienneté (mois), date)53    dict(nom="C1 = F2", immeuble="A (même immeuble)", S=98.0, etage="RDC", orient="sud", stat=True, charges_m2=CHARGES_REF_M2, passif=Q_F2 * DEFICIT_IMMEUBLE, mois=6.0, date="2026-03-02"),54    dict(nom="C2 = F7", immeuble="A (même immeuble)", S=85.0, etage="2e", orient="est", stat=True, charges_m2=CHARGES_REF_M2, passif=Q_F7 * DEFICIT_IMMEUBLE, mois=10.0, date="2025-11-03"),55    dict(nom="C3", immeuble="B (24 fractions, ascenseur)", S=96.0, etage="2e", orient="sud", stat=True, charges_m2=78.0, passif=0.0, mois=3.0, date="2026-06-01"),56    dict(nom="C4", immeuble="C (12 fractions)", S=100.0, etage="3e", orient="ouest", stat=False, charges_m2=52.0, passif=0.09 * 60_000.0, mois=4.0, date="2026-05-04"),57    dict(nom="C5", immeuble="D (8 fractions)", S=92.0, etage="RDC", orient="sud", stat=True, charges_m2=58.0, passif=0.13 * 240_000.0, mois=8.0, date="2026-01-05"),58]596061def valeur_modele(u: dict) -> float:62    """Valeur « vraie » d'une fraction dans le marché fictif à la date d'évaluation (hypothèse de génération).63    V = base(S) × (1 + c_étage + c_orient) + stationnement − κ × passif − (c_m² − c_ref) × S / R_c."""64    base = PRIX_M2_REF * S_REF * (u["S"] / S_REF) ** ELASTICITE_S65    v = base * (1.0 + COEF_ETAGE_MARCHE[u["etage"]] + COEF_ORIENT_MARCHE[u["orient"]])66    v += STATIONNEMENT if u["stat"] else 0.067    v -= KAPPA * u["passif"]68    v -= (u["charges_m2"] - CHARGES_REF_M2) * u["S"] / R_CHARGES69    return v707172def generer_prix(comparables=COMPARABLES, seed: int = SEED) -> list[dict]:73    """Prix observés = valeur du modèle déflatée à la date de vente (croissance g) × bruit, arrondis à 500 $."""74    rng = np.random.default_rng(seed)75    out = []76    for c in comparables:77        v = valeur_modele(c)78        p = v * (1.0 + G_MARCHE) ** (-c["mois"] / 12.0) * math.exp(rng.normal(0.0, SIGMA_BRUIT))79        d = dict(c); d["prix"] = float(np.round(p / 500.0) * 500.0); d["valeur_modele"] = v80        out.append(d)81    return out828384def prix_marginal_m2(S: float = S_REF) -> float:85    """Prix implicite marginal d'un m² au voisinage de la superficie du sujet : dV/dS = ε × base(S) / S."""86    base = PRIX_M2_REF * S_REF * (S / S_REF) ** ELASTICITE_S87    return ELASTICITE_S * base / S888990def ajuster(comp: dict, sujet: dict = SUJET, r_charges: float = R_CHARGES, kappa: float = KAPPA, g: float = G_MARCHE) -> cp.ComparableAjuste:91    """Grille : ajustement de conditions du marché (%, séquentiel) puis ajustements de propriété en dollars, signe du point de vue du sujet."""92    p_t = comp["prix"] * (1.0 + g) ** (comp["mois"] / 12.0)          # prix ramené à la date d'évaluation93    A = {"marché": p_t - comp["prix"]}94    A["étage"] = p_t * (COEF_ETAGE_MARCHE[sujet["etage"]] - COEF_ETAGE_MARCHE[comp["etage"]])95    A["orientation"] = p_t * (COEF_ORIENT_MARCHE[sujet["orient"]] - COEF_ORIENT_MARCHE[comp["orient"]])96    A["stationnement"] = STATIONNEMENT * (int(sujet["stat"]) - int(comp["stat"]))97    A["superficie"] = prix_marginal_m2() * (sujet["S"] - comp["S"])98    A["charges"] = (comp["charges_m2"] - sujet["charges_m2"]) * sujet["S"] / r_charges99    A["syndicat"] = kappa * (comp["passif"] - sujet["passif"])100    return cp.ComparableAjuste(comp["nom"], comp["prix"], A)101102103def grille_fraction(r_charges: float = R_CHARGES, kappa: float = KAPPA, g: float = G_MARCHE, poids: str = "inverse_brut") -> dict:104    comps = generer_prix()105    ajustes = [ajuster(c, SUJET, r_charges, kappa, g) for c in comps]106    w = cp.poids_inverse_brut(ajustes) if poids == "inverse_brut" else None107    rec = cp.reconciliation(ajustes, w)108    return {"comparables": comps, "ajustes": ajustes, "reconciliation": rec, "poids": rec["poids"],109            "valeur_modele_sujet": valeur_modele(SUJET), "prix_marginal_m2": prix_marginal_m2()}110111112def sensibilite_fraction(r_charges_list=(0.05, 0.06, 0.08, 0.10, 0.12), kappas=(0.0, 0.5, 0.75, 1.0)) -> dict:113    grille = {k: [grille_fraction(r, k)["reconciliation"]["valeur_ponderee"] for r in r_charges_list] for k in kappas}114    return {"r_charges": list(r_charges_list), "kappas": list(kappas), "grille": grille}115116117# ----------------------------------------------------------------------------- B. cotisation spéciale118def cotisation_speciale(montant: float | None = None, qp: dict = QP) -> dict:119    """Répartition d'une cotisation spéciale (par défaut égale au déficit de capitalisation du ch. 7) selon les quotes-parts ;120    comparaison avec les charges annuelles normalisées (cotisation de seuil) de chaque fraction."""121    montant = DEFICIT_IMMEUBLE if montant is None else montant122    A_star = c7.cotisation_seuil()123    rep = c7.repartition_charges(A_star, qp)124    part = qp["q"] * montant125    return {"montant": montant, "par_fraction": part, "charges_annuelles": rep["par_fraction_total"],126            "ratio_annees": part / rep["par_fraction_total"], "A_star": A_star, "rep": rep}127128129# ----------------------------------------------------------------------------- C. copropriété locative (F4 louée)130LOYER_F4 = 1_700.0        # $/mois ; hypothèse pédagogique dans l'ordre de grandeur des loyers SCHL (oct. 2025) des copropriétés offertes en location, 2 ch.131INOCC_COPRO = 0.021       # SCHL, RMR de Montréal, copropriétés offertes en location, octobre 2025 (tableau 4.1.1)132TAXES_F4 = 3_300.0        # $/an ; ordre de grandeur du compte de taxes moyen d'une copropriété à Montréal en 2026 (La Presse, 2026-10-19, source secondaire)133ASSURANCE_COPRO = 600.0   # $/an ; hypothèse (BAC 2022 : 544 $ pour un copropriétaire, partiellement vérifié)134GESTION = 0.05            # part du revenu brut effectif (forfait usuel)135TAUX_PLEX = 0.0525        # taux global de référence des petits immeubles locatifs du marché fictif (dans la fourchette CBRE Québec Low Rise B T3 2026 : 5,00-6,00 %)136137138def copro_locative(valeur: float, loyer: float = LOYER_F4, inocc: float = INOCC_COPRO, taxes: float = TAXES_F4,139                   assurance: float = ASSURANCE_COPRO, gestion: float = GESTION, frais_copro: float | None = None) -> dict:140    """RNE d'une fraction louée (convention de l'ouvrage : la cotisation au fonds de prévoyance normalisée tient lieu de réserve)."""141    if frais_copro is None:142        frais_copro = float(c7.repartition_charges(c7.cotisation_seuil(), QP)["par_fraction_total"][3])   # F4 : 10 218 $143    rbp = loyer * 12.0144    rbe = fin.revenu_brut_effectif(rbp, inocc)145    de = taxes + assurance + frais_copro + gestion * rbe146    noi = fin.rne(rbe, de)147    fonds = float(c7.repartition_charges(c7.cotisation_seuil(), QP)["par_fraction_fonds"][3])   # part « fonds de prévoyance » des frais (3 082 $)148    return {"rbp": rbp, "rbe": rbe, "depenses": de, "frais_copro": frais_copro, "noi": noi, "noi_sans_reserve": noi + fonds, "fonds": fonds,149            "taux_implicite_sans_reserve": fin.taux_global_extrait(noi + fonds, valeur),150            "taux_implicite": fin.taux_global_extrait(noi, valeur), "ratio_depenses": fin.ratio_depenses(de, rbe),151            "valeur_investissement_5_25": fin.capitalisation_directe(noi, TAUX_PLEX),152            "mrb": fin.multiplicateur_revenu_brut(valeur, rbp)}153154155# ----------------------------------------------------------------------------- C'. financement de l'acheteur (fraction divise vs part indivise)156TAUX_ASSURE = 0.0412        # BdC, fonds avancés, prêts assurés à taux fixe ≥ 5 ans, août 2026 (sources/notes-E.md, ch. 31)157AMORT_MOIS = 300158MISE_DIVISE = 0.05          # mise de fonds minimale assurée (prix ≤ 500 000 $ ; Finances Canada, 15 décembre 2024)159MISE_INDIVISE = 0.20        # pratique des prêteurs pour une part indivise (NON VÉRIFIÉE dans une source normative ; hypothèse déclarée)160161162def financement_acheteur(prix: float, mise: float = MISE_DIVISE, taux: float = TAUX_ASSURE, amort: int = AMORT_MOIS,163                         charges_mensuelles: float | None = None, taxes_annuelles: float = TAXES_F4) -> dict:164    """Mise de fonds, prêt (hors prime d'assurance), paiement mensuel (composition semestrielle), coût mensuel total et paiement au taux165    admissible minimal (max(taux + 2 %, 5,25 %), BSIF)."""166    if charges_mensuelles is None:167        charges_mensuelles = float(c7.repartition_charges(c7.cotisation_seuil(), QP)["par_fraction_total"][3]) / 12.0168    mdf = mise * prix; pret = prix - mdf169    h = fin.Hypotheque(pret, taux, amort)170    tam = fin.taux_admissible_minimal(taux)171    h_tam = fin.Hypotheque(pret, tam, amort)172    return {"mise_de_fonds": mdf, "pret": pret, "paiement": h.paiement, "charges_mensuelles": charges_mensuelles,173            "taxes_mensuelles": taxes_annuelles / 12.0, "cout_mensuel": h.paiement + charges_mensuelles + taxes_annuelles / 12.0,174            "tam": tam, "paiement_tam": h_tam.paiement}175176177# ----------------------------------------------------------------------------- D. conversion locatif → copropriété divise178# L'immeuble fictif de huit fractions, vu AVANT conversion comme un immeuble locatif de 8 logements (hypothèses déclarées).179LOYERS_LOG = {"F1": 1_250.0, "F2": 1_650.0, "F3": 1_300.0, "F4": 1_700.0, "F5": 1_450.0, "F6": 1_850.0, "F7": 1_500.0, "F8": 1_525.0}   # $/mois, loyers contractuels180INOCC_LOCATIF = 0.03          # inoccupation + mauvaises créances (hypothèse ; SCHL RMR 2025 : 2,4 % Québec, 2,9 % Montréal, 3,8 % Gatineau)181RATIO_DEPENSES_BLOC = 0.42    # dépenses d'exploitation + réserve / RBE (hypothèse pédagogique pour un immeuble de 8 logements des années 1990)182LOCATAIRES = {"F1": True, "F2": False, "F3": True, "F4": False, "F5": True, "F6": True, "F7": True, "F8": False}  # occupé par un locataire (bail en cours) ?183PREEMPTION = {"F6"}           # locataire(s) qui acceptent l'offre de vente (art. 54.7) à la valeur libre184ROTATION = 0.12               # part des logements occupés libérés chaque année (départ volontaire ; aucune éviction : moratoire jusqu'au 6 juin 2027)185DECOTE_OCCUPE = 0.20          # décote d'une fraction vendue occupée (bail maintenu, art. 1937 C.c.Q.) à un investisseur — hypothèse186R_PROMOTEUR = 0.10            # taux d'actualisation exigé par le convertisseur187FRAIS_VENTE = 0.04            # courtage et frais de vente (proportion du prix) — hypothèse188COUTS_CONVERSION = {          # dollars de 2026, hypothèses pédagogiques189    "Cadastre vertical et certificats de localisation (arpenteur-géomètre)": 28_000.0,190    "Déclaration de copropriété, publication et honoraires juridiques": 22_000.0,191    "Rapport d'expert et circulaire d'information (art. 54.6 Loi TAL)": 9_000.0,192    "Frais de demande (TAL, municipalité), budget prévisionnel, valeur relative": 6_000.0,193    "Individualisation des services et mise aux normes (8 logements)": 120_000.0,194    "Rafraîchissement des parties communes": 35_000.0,195    "Dotation initiale du fonds de prévoyance (solde d'ouverture requis, ch. 7)": 85_000.0,196}197HORIZON_CONV = 6              # années de projection des ventes198PORTAGE_PCT = 0.015           # coûts de portage annuels (taxes, assurance, intérêts nets des loyers résiduels) en proportion de la valeur des fractions invendues — hypothèse199200201def unites_fictives() -> list[dict]:202    """Les huit fractions de l'immeuble du ch. 7 comme unités du marché fictif (charges de référence, passif nul après dotation du fonds)."""203    out = []204    for i, (n, S, e, o, st) in enumerate(c7.FRACTIONS):205        out.append(dict(nom=n, S=S, etage=e, orient=o, stat=st, charges_m2=CHARGES_REF_M2, passif=0.0, loyer=LOYERS_LOG[n],206                        locataire=LOCATAIRES[n], preemption=n in PREEMPTION, q=float(QP["q"][i])))207    return out208209210def valeur_bloc(taux: float = TAUX_PLEX, inocc: float = INOCC_LOCATIF, ratio_dep: float = RATIO_DEPENSES_BLOC) -> dict:211    u = unites_fictives()212    rbp = 12.0 * sum(x["loyer"] for x in u)213    rbe = fin.revenu_brut_effectif(rbp, inocc)214    de = ratio_dep * rbe215    noi = fin.rne(rbe, de)216    return {"rbp": rbp, "rbe": rbe, "depenses": de, "noi": noi, "taux": taux, "valeur": fin.capitalisation_directe(noi, taux),217            "mrb": fin.multiplicateur_revenu_brut(fin.capitalisation_directe(noi, taux), rbp), "n": len(u)}218219220def calendrier_ventes(rotation: float = ROTATION, decote_occ: float = DECOTE_OCCUPE, vendre_occupe: bool = False,221                      horizon: int = HORIZON_CONV, unites=None) -> dict:222    """Produit brut attendu par année (fin d'année), avant frais de vente.223    An 1 : fractions libres et locataires qui exercent la préemption, à la valeur libre. Fractions occupées : si `vendre_occupe`,224    vendues à l'an 1 avec décote ; sinon vendues libres l'année où le locataire part (probabilité ρ par an), le reliquat à l'horizon225    étant vendu occupé avec décote. Valeurs libres = modèle du marché fictif (passif nul après dotation du fonds)."""226    u = unites or unites_fictives()227    produit = np.zeros(horizon + 1); invendu = np.zeros(horizon + 1)228    valeurs = {x["nom"]: valeur_modele(x) for x in u}229    libres_an1 = [x for x in u if (not x["locataire"]) or x["preemption"]]230    occupes = [x for x in u if x["locataire"] and not x["preemption"]]231    produit[1] += sum(valeurs[x["nom"]] for x in libres_an1)232    V_occ = sum(valeurs[x["nom"]] for x in occupes)233    if vendre_occupe:234        produit[1] += V_occ * (1.0 - decote_occ)235    else:236        reste = 1.0237        for t in range(1, horizon + 1):238            if t < horizon:239                produit[t] += V_occ * reste * rotation      # fractions libérées pendant l'année t, vendues libres en fin d'année240                reste *= (1.0 - rotation)241            else:242                produit[t] += V_occ * reste * (1.0 - decote_occ)   # reliquat vendu occupé à l'horizon243                reste = 0.0244            invendu[t] = V_occ * reste245    # valeur libre des fractions invendues au début de chaque année (assiette des coûts de portage)246    base_portage = np.zeros(horizon + 1)247    total = sum(valeurs.values())248    base_portage[1] = total249    if not vendre_occupe:250        reste = 1.0251        for t in range(2, horizon + 1):252            reste *= (1.0 - rotation)253            base_portage[t] = V_occ * reste254    return {"produit": produit, "valeurs": valeurs, "somme_libre": total, "V_occ": V_occ, "n_libres_an1": len(libres_an1),255            "n_occupes": len(occupes), "base_portage": base_portage}256257258def faisabilite(rotation: float = ROTATION, decote_occ: float = DECOTE_OCCUPE, r: float = R_PROMOTEUR, vendre_occupe: bool = False,259                frais_vente: float = FRAIS_VENTE, couts=COUTS_CONVERSION, portage: float = PORTAGE_PCT, horizon: int = HORIZON_CONV) -> dict:260    """Valeur de conversion V_conv = Σ_t [produit_t (1 − f) − portage_t] / (1+r)^t − coûts de conversion (t = 0) ; prime = V_conv − V_bloc."""261    cal = calendrier_ventes(rotation, decote_occ, vendre_occupe, horizon)262    flux = np.zeros(horizon + 1)263    flux[0] = -sum(couts.values())264    for t in range(1, horizon + 1):265        flux[t] = cal["produit"][t] * (1.0 - frais_vente) - portage * cal["base_portage"][t]266    v_conv = fin.van(list(flux), r)267    bloc = valeur_bloc()268    return {"flux": flux, "v_conv": v_conv, "v_bloc": bloc["valeur"], "prime": v_conv - bloc["valeur"],269            "somme_libre": cal["somme_libre"], "couts": sum(couts.values()), "frais_vente_total": float(sum(cal["produit"]) * frais_vente),270            "portage_total": float(sum(portage * cal["base_portage"][1:])), "produit": cal["produit"], "calendrier": cal, "bloc": bloc,271            "marge_sur_bloc": (v_conv - bloc["valeur"]) / bloc["valeur"],272            "somme_nette_non_actualisee": float(sum(cal["produit"]) * (1 - frais_vente) - sum(couts.values()) - sum(portage * cal["base_portage"][1:]))}273274275def sensibilite_conversion(rotations=(0.05, 0.08, 0.12, 0.20, 0.30), decotes=(0.10, 0.20, 0.30), r: float = R_PROMOTEUR) -> dict:276    grille = {d: [faisabilite(rho, d, r)["v_conv"] for rho in rotations] for d in decotes}277    occ = {d: faisabilite(ROTATION, d, r, vendre_occupe=True)["v_conv"] for d in decotes}278    return {"rotations": list(rotations), "decotes": list(decotes), "grille": grille, "vente_occupee_an1": occ, "v_bloc": valeur_bloc()["valeur"]}279280281def seuil_prix(r: float = R_PROMOTEUR) -> float:282    """Facteur multiplicatif uniforme des valeurs libres pour lequel V_conv = V_bloc (point mort de la conversion)."""283    global PRIX_M2_REF284    p0 = PRIX_M2_REF285    lo, hi = 0.3 * p0, 1.5 * p0286    for _ in range(100):287        mid = 0.5 * (lo + hi); PRIX_M2_REF = mid288        if faisabilite(r=r)["prime"] > 0: hi = mid289        else: lo = mid290    PRIX_M2_REF = p0291    return hi / p0292293294# ----------------------------------------------------------------------------- impression295def fmt(x): return f"{x:,.0f}".replace(",", " ")296297298def main() -> dict:299    out = {}300    g = grille_fraction(); out["grille"] = g301    print(f"== A. Fraction F4 (sujet) — marché fictif, graine {SEED}, date d'évaluation {DATE_EVAL}")302    print(f"  passif du syndicat A (déficit {fmt(DEFICIT_IMMEUBLE)} $) : part de F4 = q4 {Q_F4:.4f} × déficit = {fmt(SUJET['passif'])} $ ; prix marginal {g['prix_marginal_m2']:,.0f} $/m²")303    print(f"  valeur du modèle pour F4 (contrôle de génération) : {fmt(g['valeur_modele_sujet'])} $")304    for c, a, w in zip(g["comparables"], g["ajustes"], g["poids"]):305        print(f"  {c['nom']:8s} {c['immeuble']:28s} S {c['S']:5.0f} {c['etage']:3s} {c['orient']:5s} stat {int(c['stat'])} charges {c['charges_m2']:5.1f} $/m² passif {fmt(c['passif']):>7} $ "306              f"{c['mois']:4.0f} mois prix {fmt(c['prix']):>8} $ (modèle {fmt(c['valeur_modele'])})")307        print("           ajustements : " + ", ".join(f"{k} {v:+,.0f}" for k, v in a.ajustements.items()).replace(",", " ").replace(" $", "$"))308        print(f"           prix ajusté {fmt(a.prix_ajuste)} $ ; brut {a.brut_pct:.1%} ; net {a.net_pct:+.1%} ; poids {w:.3f}")309    rec = g["reconciliation"]310    print(f"  réconciliation : pondérée {fmt(rec['valeur_ponderee'])} $ ; moyenne {fmt(rec['moyenne'])} ; médiane {fmt(rec['mediane'])} ; min {fmt(rec['min'])} ; max {fmt(rec['max'])} ; étendue {rec['etendue_pct']:.1%}")311    sf = sensibilite_fraction(); out["sens_fraction"] = sf312    print("  sensibilité (valeur pondérée) : lignes κ, colonnes R_c " + "  ".join(f"{r:.0%}" for r in sf["r_charges"]))313    for k, row in sf["grille"].items():314        print(f"    κ={k:.2f} " + "  ".join(f"{v:9,.0f}" for v in row).replace(",", " "))315    cs = cotisation_speciale(); out["cotisation"] = cs316    print(f"\n== B. Cotisation spéciale de {fmt(cs['montant'])} $ (déficit du ch. 7) répartie selon q_i ; charges annuelles normalisées (A* = {fmt(cs['A_star'])} $)")317    for n, p, ch, r in zip(QP["noms"], cs["par_fraction"], cs["charges_annuelles"], cs["ratio_annees"]):318        print(f"  {n} : cotisation {fmt(p):>7} $ ; charges {fmt(ch):>6} $/an ; = {r:.2f} années de charges")319    v_f4 = rec["valeur_ponderee"]320    cl = copro_locative(v_f4); out["locative"] = cl321    print(f"\n== C. F4 louée {fmt(LOYER_F4)} $/mois : RBP {fmt(cl['rbp'])} ; RBE {fmt(cl['rbe'])} ; dépenses {fmt(cl['depenses'])} (frais copro {fmt(cl['frais_copro'])}) ; RNE {fmt(cl['noi'])} $")322    print(f"  RNE hors cotisation au fonds ({fmt(cl['fonds'])} $) = {fmt(cl['noi_sans_reserve'])} $ → taux {cl['taux_implicite_sans_reserve']:.2%}")323    print(f"  taux implicite sur {fmt(v_f4)} $ = {cl['taux_implicite']:.2%} ; ratio de dépenses {cl['ratio_depenses']:.1%} ; MRB {cl['mrb']:.1f} ; valeur d'investissement à {TAUX_PLEX:.2%} = {fmt(cl['valeur_investissement_5_25'])} $")324    fa = financement_acheteur(380_000.0); fi = financement_acheteur(380_000.0, MISE_INDIVISE); out["fin_divise"] = fa; out["fin_indivise"] = fi325    print(f"\n== C'. Financement d'un acheteur à 380 000 $ : divise (mise {MISE_DIVISE:.0%}) → mise {fmt(fa['mise_de_fonds'])}, prêt {fmt(fa['pret'])}, paiement {fa['paiement']:,.0f} $/mois à {TAUX_ASSURE:.2%} ; "326          f"charges {fa['charges_mensuelles']:,.0f} + taxes {fa['taxes_mensuelles']:,.0f} → coût mensuel {fa['cout_mensuel']:,.0f} $ ; TAM {fa['tam']:.2%} → paiement {fa['paiement_tam']:,.0f} $")327    print(f"   part indivise (mise {MISE_INDIVISE:.0%}, hypothèse) → mise {fmt(fi['mise_de_fonds'])}, prêt {fmt(fi['pret'])}, paiement {fi['paiement']:,.0f} $/mois ; écart de mise de fonds {fmt(fi['mise_de_fonds'] - fa['mise_de_fonds'])} $")328    fb = faisabilite(); out["faisabilite"] = fb329    b = fb["bloc"]330    print(f"\n== D. Conversion — immeuble locatif de 8 logements : RBP {fmt(b['rbp'])} ; RBE {fmt(b['rbe'])} ; DE {fmt(b['depenses'])} ; RNE {fmt(b['noi'])} ; R {b['taux']:.2%} → V_bloc {fmt(b['valeur'])} $ ({fmt(b['valeur']/8)} $/log., MRB {b['mrb']:.1f})")331    cal = fb["calendrier"]332    print("  valeurs libres des fractions : " + ", ".join(f"{k} {fmt(v)}" for k, v in cal["valeurs"].items()) + f" ; somme {fmt(cal['somme_libre'])} $")333    print(f"  libres ou préemption à l'an 1 : {cal['n_libres_an1']} fractions ; occupées : {cal['n_occupes']} (valeur libre {fmt(cal['V_occ'])} $) ; ρ = {ROTATION:.0%}, décote occupée {DECOTE_OCCUPE:.0%}, r = {R_PROMOTEUR:.0%}")334    print("  produit brut par année : " + ", ".join(f"an {t} {fmt(p)}" for t, p in enumerate(fb["produit"]) if t > 0))335    print("  flux : " + ", ".join(f"t{t} {f:,.0f}".replace(",", " ") for t, f in enumerate(fb["flux"])))336    print(f"  coûts de conversion {fmt(fb['couts'])} ; frais de vente {fmt(fb['frais_vente_total'])} ; portage {fmt(fb['portage_total'])} ; somme nette non actualisée {fmt(fb['somme_nette_non_actualisee'])}")337    print(f"  V_conv = {fmt(fb['v_conv'])} $ ; V_bloc = {fmt(fb['v_bloc'])} $ ; prime {fmt(fb['prime'])} $ ({fb['marge_sur_bloc']:+.1%})")338    fo = faisabilite(vendre_occupe=True); out["faisabilite_occ"] = fo339    print(f"  variante : tout vendre à l'an 1 (occupées avec décote) → V_conv {fmt(fo['v_conv'])} $ ; prime {fmt(fo['prime'])} $")340    sc = sensibilite_conversion(); out["sens_conv"] = sc341    print("  sensibilité V_conv : lignes décote occupée, colonnes ρ " + "  ".join(f"{r:.0%}" for r in sc["rotations"]) + " | vente occupée an 1")342    for d, row in sc["grille"].items():343        print(f"    δ={d:.0%} " + "  ".join(f"{v:10,.0f}" for v in row).replace(",", " ") + f" | {sc['vente_occupee_an1'][d]:10,.0f}".replace(",", " "))344    sp = seuil_prix(); out["seuil_prix"] = sp345    print(f"  point mort : les valeurs libres peuvent baisser jusqu'à {sp:.1%} de leur niveau (−{1-sp:.1%}) avant que V_conv < V_bloc")346    for r in (0.08, 0.12):347        print(f"  r = {r:.0%} : V_conv {fmt(faisabilite(r=r)['v_conv'])} $")348    return out349350351if __name__ == "__main__":352    main()353
Files · 960e587174484ddf8cb3579ab544c4d47de3ba3c
Menu

Theme