#!/usr/bin/env python3 """Chapitre 42 — Copropriétés et conversions : exemples chiffrés (DONNÉES SYNTHÉTIQUES, graine 20261031). Réutilise l'immeuble fictif de huit fractions du chapitre 7 (analysis/python/cases/ch07_servitudes_copro.py : quotes-parts, répartition des charges, cotisation de seuil, déficit de capitalisation) et y ajoute quatre blocs, tous générés ici (aucun fichier externe ; aucune statistique de marché n'est reproduite) : A. Évaluation de la fraction F4 par comparaison : deux ventes dans l'immeuble, trois ventes dans d'autres immeubles fictifs ; ajustements de date, d'étage, d'orientation, de stationnement, de superficie, de charges courantes (capitalisation) et de situation financière du syndicat (déficit du fonds de prévoyance, cotisation spéciale) ; réconciliation (traite.comparables). B. Effet d'une cotisation spéciale égale au déficit de capitalisation, répartie selon les quotes-parts (figure). C. Copropriété locative : revenu net d'exploitation et taux implicite d'une fraction louée (traite.finance). D. Conversion d'un immeuble locatif de huit logements en copropriété divise : valeur en bloc (capitalisation directe) contre somme des fractions nette des coûts, des délais et des locataires (rotation, préemption, vente occupée) ; sensibilités. Toutes les fonctions financières viennent de analysis/python/traite/ (finance.py, comparables.py) ; rien n'est réécrit. Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch42_copro.py """ from __future__ import annotations import sys, math, pathlib import numpy as np HERE = pathlib.Path(__file__).resolve() sys.path.insert(0, str(HERE.parents[1])); sys.path.insert(0, str(HERE.parent)) from traite import comparables as cp from traite import finance as fin import ch07_servitudes_copro as c7 SEED = 20261031 DATE_EVAL = "2026-09-01" # ----------------------------------------------------------------------------- marché fictif : paramètres déclarés # Prix implicites « du marché fictif » (hypothèses pédagogiques ; l'évaluateur les tient d'une analyse de ventes appariées / hédonique). PRIX_M2_REF = 3_650.0 # $/m² pour une fraction de 98 m², 2e étage, orientation est/ouest, sans stationnement, immeuble bien capitalisé ELASTICITE_S = 0.60 # élasticité prix-superficie (< 1) ; prix marginal au point de référence = 0,60 × 3 650 = 2 190 $/m² S_REF = 98.0 COEF_ETAGE_MARCHE = {"RDC": -0.04, "2e": 0.00, "3e": 0.02} # proportion du prix (immeubles sans ascenseur) COEF_ORIENT_MARCHE = {"nord": -0.03, "est": 0.00, "ouest": 0.00, "sud": 0.03} STATIONNEMENT = 28_000.0 # $ ; case intérieure à usage restreint G_MARCHE = 0.02 # croissance annuelle des prix des copropriétés sur le marché fictif (ajustement de conditions du marché) R_CHARGES = 0.08 # taux de capitalisation des écarts de charges courantes (hypothèse pédagogique ; à extraire du marché) KAPPA = 1.0 # coefficient de capitalisation du passif du syndicat (1 = dollar pour dollar, convention du ch. 7) CHARGES_REF_M2 = 40_000.0 / 710.0 # 56,34 $/m²/an : charges courantes de l'immeuble fictif (hors fonds), référence du marché SIGMA_BRUIT = 0.02 # bruit de transaction (log) dans la génération des prix des comparables # Situation financière des syndicats fictifs : déficit de capitalisation (ch. 7) ou cotisation spéciale votée non payée QP = c7.quotes_parts() INC = c7.incidence_valeur(QP) # déficit de l'immeuble fictif du ch. 7 : 150 908 $ DEFICIT_IMMEUBLE = float(INC["deficit"]) Q_F4 = float(QP["q"][3]); Q_F2 = float(QP["q"][1]); Q_F7 = float(QP["q"][6]) SUJET = dict(nom="F4 (sujet)", immeuble="A (fictif, 8 fractions)", S=98.0, etage="2e", orient="sud", stat=True, charges_m2=CHARGES_REF_M2, passif=Q_F4 * DEFICIT_IMMEUBLE, mois=0.0) COMPARABLES = [ # (nom, immeuble, S, étage, orientation, stationnement, charges courantes $/m²/an, passif de la fraction ($), ancienneté (mois), date) 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"), 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"), 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"), 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"), 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"), ] def valeur_modele(u: dict) -> float: """Valeur « vraie » d'une fraction dans le marché fictif à la date d'évaluation (hypothèse de génération). V = base(S) × (1 + c_étage + c_orient) + stationnement − κ × passif − (c_m² − c_ref) × S / R_c.""" base = PRIX_M2_REF * S_REF * (u["S"] / S_REF) ** ELASTICITE_S v = base * (1.0 + COEF_ETAGE_MARCHE[u["etage"]] + COEF_ORIENT_MARCHE[u["orient"]]) v += STATIONNEMENT if u["stat"] else 0.0 v -= KAPPA * u["passif"] v -= (u["charges_m2"] - CHARGES_REF_M2) * u["S"] / R_CHARGES return v def generer_prix(comparables=COMPARABLES, seed: int = SEED) -> list[dict]: """Prix observés = valeur du modèle déflatée à la date de vente (croissance g) × bruit, arrondis à 500 $.""" rng = np.random.default_rng(seed) out = [] for c in comparables: v = valeur_modele(c) p = v * (1.0 + G_MARCHE) ** (-c["mois"] / 12.0) * math.exp(rng.normal(0.0, SIGMA_BRUIT)) d = dict(c); d["prix"] = float(np.round(p / 500.0) * 500.0); d["valeur_modele"] = v out.append(d) return out def prix_marginal_m2(S: float = S_REF) -> float: """Prix implicite marginal d'un m² au voisinage de la superficie du sujet : dV/dS = ε × base(S) / S.""" base = PRIX_M2_REF * S_REF * (S / S_REF) ** ELASTICITE_S return ELASTICITE_S * base / S def ajuster(comp: dict, sujet: dict = SUJET, r_charges: float = R_CHARGES, kappa: float = KAPPA, g: float = G_MARCHE) -> cp.ComparableAjuste: """Grille : ajustement de conditions du marché (%, séquentiel) puis ajustements de propriété en dollars, signe du point de vue du sujet.""" p_t = comp["prix"] * (1.0 + g) ** (comp["mois"] / 12.0) # prix ramené à la date d'évaluation A = {"marché": p_t - comp["prix"]} A["étage"] = p_t * (COEF_ETAGE_MARCHE[sujet["etage"]] - COEF_ETAGE_MARCHE[comp["etage"]]) A["orientation"] = p_t * (COEF_ORIENT_MARCHE[sujet["orient"]] - COEF_ORIENT_MARCHE[comp["orient"]]) A["stationnement"] = STATIONNEMENT * (int(sujet["stat"]) - int(comp["stat"])) A["superficie"] = prix_marginal_m2() * (sujet["S"] - comp["S"]) A["charges"] = (comp["charges_m2"] - sujet["charges_m2"]) * sujet["S"] / r_charges A["syndicat"] = kappa * (comp["passif"] - sujet["passif"]) return cp.ComparableAjuste(comp["nom"], comp["prix"], A) def grille_fraction(r_charges: float = R_CHARGES, kappa: float = KAPPA, g: float = G_MARCHE, poids: str = "inverse_brut") -> dict: comps = generer_prix() ajustes = [ajuster(c, SUJET, r_charges, kappa, g) for c in comps] w = cp.poids_inverse_brut(ajustes) if poids == "inverse_brut" else None rec = cp.reconciliation(ajustes, w) return {"comparables": comps, "ajustes": ajustes, "reconciliation": rec, "poids": rec["poids"], "valeur_modele_sujet": valeur_modele(SUJET), "prix_marginal_m2": prix_marginal_m2()} def 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: grille = {k: [grille_fraction(r, k)["reconciliation"]["valeur_ponderee"] for r in r_charges_list] for k in kappas} return {"r_charges": list(r_charges_list), "kappas": list(kappas), "grille": grille} # ----------------------------------------------------------------------------- B. cotisation spéciale def cotisation_speciale(montant: float | None = None, qp: dict = QP) -> dict: """Répartition d'une cotisation spéciale (par défaut égale au déficit de capitalisation du ch. 7) selon les quotes-parts ; comparaison avec les charges annuelles normalisées (cotisation de seuil) de chaque fraction.""" montant = DEFICIT_IMMEUBLE if montant is None else montant A_star = c7.cotisation_seuil() rep = c7.repartition_charges(A_star, qp) part = qp["q"] * montant return {"montant": montant, "par_fraction": part, "charges_annuelles": rep["par_fraction_total"], "ratio_annees": part / rep["par_fraction_total"], "A_star": A_star, "rep": rep} # ----------------------------------------------------------------------------- C. copropriété locative (F4 louée) LOYER_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. INOCC_COPRO = 0.021 # SCHL, RMR de Montréal, copropriétés offertes en location, octobre 2025 (tableau 4.1.1) TAXES_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) ASSURANCE_COPRO = 600.0 # $/an ; hypothèse (BAC 2022 : 544 $ pour un copropriétaire, partiellement vérifié) GESTION = 0.05 # part du revenu brut effectif (forfait usuel) TAUX_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 %) def copro_locative(valeur: float, loyer: float = LOYER_F4, inocc: float = INOCC_COPRO, taxes: float = TAXES_F4, assurance: float = ASSURANCE_COPRO, gestion: float = GESTION, frais_copro: float | None = None) -> dict: """RNE d'une fraction louée (convention de l'ouvrage : la cotisation au fonds de prévoyance normalisée tient lieu de réserve).""" if frais_copro is None: frais_copro = float(c7.repartition_charges(c7.cotisation_seuil(), QP)["par_fraction_total"][3]) # F4 : 10 218 $ rbp = loyer * 12.0 rbe = fin.revenu_brut_effectif(rbp, inocc) de = taxes + assurance + frais_copro + gestion * rbe noi = fin.rne(rbe, de) fonds = float(c7.repartition_charges(c7.cotisation_seuil(), QP)["par_fraction_fonds"][3]) # part « fonds de prévoyance » des frais (3 082 $) return {"rbp": rbp, "rbe": rbe, "depenses": de, "frais_copro": frais_copro, "noi": noi, "noi_sans_reserve": noi + fonds, "fonds": fonds, "taux_implicite_sans_reserve": fin.taux_global_extrait(noi + fonds, valeur), "taux_implicite": fin.taux_global_extrait(noi, valeur), "ratio_depenses": fin.ratio_depenses(de, rbe), "valeur_investissement_5_25": fin.capitalisation_directe(noi, TAUX_PLEX), "mrb": fin.multiplicateur_revenu_brut(valeur, rbp)} # ----------------------------------------------------------------------------- D. conversion locatif → copropriété divise # L'immeuble fictif de huit fractions, vu AVANT conversion comme un immeuble locatif de 8 logements (hypothèses déclarées). LOYERS_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 contractuels INOCC_LOCATIF = 0.03 # inoccupation + mauvaises créances (hypothèse ; SCHL RMR 2025 : 2,4 % Québec, 2,9 % Montréal, 3,8 % Gatineau) RATIO_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) LOCATAIRES = {"F1": True, "F2": False, "F3": True, "F4": False, "F5": True, "F6": True, "F7": True, "F8": False} # occupé par un locataire (bail en cours) ? PREEMPTION = {"F6"} # locataire(s) qui acceptent l'offre de vente (art. 54.7) à la valeur libre ROTATION = 0.12 # part des logements occupés libérés chaque année (départ volontaire ; aucune éviction : moratoire jusqu'au 6 juin 2027) DECOTE_OCCUPE = 0.20 # décote d'une fraction vendue occupée (bail maintenu, art. 1937 C.c.Q.) à un investisseur — hypothèse R_PROMOTEUR = 0.10 # taux d'actualisation exigé par le convertisseur FRAIS_VENTE = 0.04 # courtage et frais de vente (proportion du prix) — hypothèse COUTS_CONVERSION = { # dollars de 2026, hypothèses pédagogiques "Cadastre vertical et certificats de localisation (arpenteur-géomètre)": 28_000.0, "Déclaration de copropriété, publication et honoraires juridiques": 22_000.0, "Rapport d'expert et circulaire d'information (art. 54.6 Loi TAL)": 9_000.0, "Frais de demande (TAL, municipalité), budget prévisionnel, valeur relative": 6_000.0, "Individualisation des services et mise aux normes (8 logements)": 120_000.0, "Rafraîchissement des parties communes": 35_000.0, "Dotation initiale du fonds de prévoyance (solde d'ouverture requis, ch. 7)": 85_000.0, } HORIZON_CONV = 6 # années de projection des ventes PORTAGE_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èse def unites_fictives() -> list[dict]: """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).""" out = [] for i, (n, S, e, o, st) in enumerate(c7.FRACTIONS): 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], locataire=LOCATAIRES[n], preemption=n in PREEMPTION, q=float(QP["q"][i]))) return out def valeur_bloc(taux: float = TAUX_PLEX, inocc: float = INOCC_LOCATIF, ratio_dep: float = RATIO_DEPENSES_BLOC) -> dict: u = unites_fictives() rbp = 12.0 * sum(x["loyer"] for x in u) rbe = fin.revenu_brut_effectif(rbp, inocc) de = ratio_dep * rbe noi = fin.rne(rbe, de) return {"rbp": rbp, "rbe": rbe, "depenses": de, "noi": noi, "taux": taux, "valeur": fin.capitalisation_directe(noi, taux), "mrb": fin.multiplicateur_revenu_brut(fin.capitalisation_directe(noi, taux), rbp), "n": len(u)} def calendrier_ventes(rotation: float = ROTATION, decote_occ: float = DECOTE_OCCUPE, vendre_occupe: bool = False, horizon: int = HORIZON_CONV, unites=None) -> dict: """Produit brut attendu par année (fin d'année), avant frais de vente. An 1 : fractions libres et locataires qui exercent la préemption, à la valeur libre. Fractions occupées : si `vendre_occupe`, vendues à l'an 1 avec décote ; sinon vendues libres l'année où le locataire part (probabilité ρ par an), le reliquat à l'horizon étant vendu occupé avec décote. Valeurs libres = modèle du marché fictif (passif nul après dotation du fonds).""" u = unites or unites_fictives() produit = np.zeros(horizon + 1); invendu = np.zeros(horizon + 1) valeurs = {x["nom"]: valeur_modele(x) for x in u} libres_an1 = [x for x in u if (not x["locataire"]) or x["preemption"]] occupes = [x for x in u if x["locataire"] and not x["preemption"]] produit[1] += sum(valeurs[x["nom"]] for x in libres_an1) V_occ = sum(valeurs[x["nom"]] for x in occupes) if vendre_occupe: produit[1] += V_occ * (1.0 - decote_occ) else: reste = 1.0 for t in range(1, horizon + 1): if t < horizon: produit[t] += V_occ * reste * rotation # fractions libérées pendant l'année t, vendues libres en fin d'année reste *= (1.0 - rotation) else: produit[t] += V_occ * reste * (1.0 - decote_occ) # reliquat vendu occupé à l'horizon reste = 0.0 invendu[t] = V_occ * reste # valeur libre des fractions invendues au début de chaque année (assiette des coûts de portage) base_portage = np.zeros(horizon + 1) total = sum(valeurs.values()) base_portage[1] = total if not vendre_occupe: reste = 1.0 for t in range(2, horizon + 1): reste *= (1.0 - rotation) base_portage[t] = V_occ * reste return {"produit": produit, "valeurs": valeurs, "somme_libre": total, "V_occ": V_occ, "n_libres_an1": len(libres_an1), "n_occupes": len(occupes), "base_portage": base_portage} def faisabilite(rotation: float = ROTATION, decote_occ: float = DECOTE_OCCUPE, r: float = R_PROMOTEUR, vendre_occupe: bool = False, frais_vente: float = FRAIS_VENTE, couts=COUTS_CONVERSION, portage: float = PORTAGE_PCT, horizon: int = HORIZON_CONV) -> dict: """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.""" cal = calendrier_ventes(rotation, decote_occ, vendre_occupe, horizon) flux = np.zeros(horizon + 1) flux[0] = -sum(couts.values()) for t in range(1, horizon + 1): flux[t] = cal["produit"][t] * (1.0 - frais_vente) - portage * cal["base_portage"][t] v_conv = fin.van(list(flux), r) bloc = valeur_bloc() return {"flux": flux, "v_conv": v_conv, "v_bloc": bloc["valeur"], "prime": v_conv - bloc["valeur"], "somme_libre": cal["somme_libre"], "couts": sum(couts.values()), "frais_vente_total": float(sum(cal["produit"]) * frais_vente), "portage_total": float(sum(portage * cal["base_portage"][1:])), "produit": cal["produit"], "calendrier": cal, "bloc": bloc, "marge_sur_bloc": (v_conv - bloc["valeur"]) / bloc["valeur"], "somme_nette_non_actualisee": float(sum(cal["produit"]) * (1 - frais_vente) - sum(couts.values()) - sum(portage * cal["base_portage"][1:]))} def 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: grille = {d: [faisabilite(rho, d, r)["v_conv"] for rho in rotations] for d in decotes} occ = {d: faisabilite(ROTATION, d, r, vendre_occupe=True)["v_conv"] for d in decotes} return {"rotations": list(rotations), "decotes": list(decotes), "grille": grille, "vente_occupee_an1": occ, "v_bloc": valeur_bloc()["valeur"]} def seuil_prix(r: float = R_PROMOTEUR) -> float: """Facteur multiplicatif uniforme des valeurs libres pour lequel V_conv = V_bloc (point mort de la conversion).""" global PRIX_M2_REF p0 = PRIX_M2_REF lo, hi = 0.3 * p0, 1.5 * p0 for _ in range(100): mid = 0.5 * (lo + hi); PRIX_M2_REF = mid if faisabilite(r=r)["prime"] > 0: hi = mid else: lo = mid PRIX_M2_REF = p0 return hi / p0 # ----------------------------------------------------------------------------- impression def fmt(x): return f"{x:,.0f}".replace(",", " ") def main() -> dict: out = {} g = grille_fraction(); out["grille"] = g print(f"== A. Fraction F4 (sujet) — marché fictif, graine {SEED}, date d'évaluation {DATE_EVAL}") 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²") print(f" valeur du modèle pour F4 (contrôle de génération) : {fmt(g['valeur_modele_sujet'])} $") for c, a, w in zip(g["comparables"], g["ajustes"], g["poids"]): 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} $ " f"{c['mois']:4.0f} mois prix {fmt(c['prix']):>8} $ (modèle {fmt(c['valeur_modele'])})") print(" ajustements : " + ", ".join(f"{k} {v:+,.0f}" for k, v in a.ajustements.items()).replace(",", " ").replace(" $", "$")) print(f" prix ajusté {fmt(a.prix_ajuste)} $ ; brut {a.brut_pct:.1%} ; net {a.net_pct:+.1%} ; poids {w:.3f}") rec = g["reconciliation"] 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%}") sf = sensibilite_fraction(); out["sens_fraction"] = sf print(" sensibilité (valeur pondérée) : lignes κ, colonnes R_c " + " ".join(f"{r:.0%}" for r in sf["r_charges"])) for k, row in sf["grille"].items(): print(f" κ={k:.2f} " + " ".join(f"{v:9,.0f}" for v in row).replace(",", " ")) cs = cotisation_speciale(); out["cotisation"] = cs 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'])} $)") for n, p, ch, r in zip(QP["noms"], cs["par_fraction"], cs["charges_annuelles"], cs["ratio_annees"]): print(f" {n} : cotisation {fmt(p):>7} $ ; charges {fmt(ch):>6} $/an ; = {r:.2f} années de charges") v_f4 = rec["valeur_ponderee"] cl = copro_locative(v_f4); out["locative"] = cl 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'])} $") print(f" RNE hors cotisation au fonds ({fmt(cl['fonds'])} $) = {fmt(cl['noi_sans_reserve'])} $ → taux {cl['taux_implicite_sans_reserve']:.2%}") 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'])} $") fb = faisabilite(); out["faisabilite"] = fb b = fb["bloc"] 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})") cal = fb["calendrier"] print(" valeurs libres des fractions : " + ", ".join(f"{k} {fmt(v)}" for k, v in cal["valeurs"].items()) + f" ; somme {fmt(cal['somme_libre'])} $") 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%}") print(" produit brut par année : " + ", ".join(f"an {t} {fmt(p)}" for t, p in enumerate(fb["produit"]) if t > 0)) print(" flux : " + ", ".join(f"t{t} {f:,.0f}".replace(",", " ") for t, f in enumerate(fb["flux"]))) 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'])}") print(f" V_conv = {fmt(fb['v_conv'])} $ ; V_bloc = {fmt(fb['v_bloc'])} $ ; prime {fmt(fb['prime'])} $ ({fb['marge_sur_bloc']:+.1%})") fo = faisabilite(vendre_occupe=True); out["faisabilite_occ"] = fo print(f" variante : tout vendre à l'an 1 (occupées avec décote) → V_conv {fmt(fo['v_conv'])} $ ; prime {fmt(fo['prime'])} $") sc = sensibilite_conversion(); out["sens_conv"] = sc print(" sensibilité V_conv : lignes décote occupée, colonnes ρ " + " ".join(f"{r:.0%}" for r in sc["rotations"]) + " | vente occupée an 1") for d, row in sc["grille"].items(): print(f" δ={d:.0%} " + " ".join(f"{v:10,.0f}" for v in row).replace(",", " ") + f" | {sc['vente_occupee_an1'][d]:10,.0f}".replace(",", " ")) sp = seuil_prix(); out["seuil_prix"] = sp print(f" point mort : les valeurs libres peuvent baisser jusqu'à {sp:.1%} de leur niveau (−{1-sp:.1%}) avant que V_conv < V_bloc") for r in (0.08, 0.12): print(f" r = {r:.0%} : V_conv {fmt(faisabilite(r=r)['v_conv'])} $") return out if __name__ == "__main__": main()