SPB Git

Repository Public Research preview

verification_calculs.py 46.2 KB 553 lines python

1#!/usr/bin/env python32# -*- coding: utf-8 -*-3"""4verification_calculs.py — moteur de calcul et de vérification des trois séances refondues5(IMM1003 S08, IMM1003 S09, IMM1033 S08), UQO, automne 2026.67Rôle : SOURCE UNIQUE des chiffres utilisés dans les diapositives, les scripts, les exercices et les corrigés.8  1. Recalcule les cas pédagogiques (bungalow de Gatineau, atelier à cinq comparables, six logements de Gatineau).9  2. Vérifie les calculs des decks originaux (sommes, pourcentages, bases, arrondis, net/brut, intérêts, profit…).10  3. Produit `resultats.json` (chiffres) et `RESULTATS_CALCULS.md` (tableaux lisibles), plus la liste des corrections.1112Conventions d'arrondi : montants au dollar (ROUND_HALF_UP) à chaque ligne affichée ; pourcentages à 0,1 %.13Usage : python3 verification_calculs.py [--json chemin] [--md chemin]14"""15from __future__ import annotations16import json, sys, math, datetime as dt17from decimal import Decimal, ROUND_HALF_UP18from pathlib import Path1920HERE = Path(__file__).resolve().parent2122def r0(x: float) -> int:23    return int(Decimal(str(x)).quantize(Decimal("1"), rounding=ROUND_HALF_UP))2425def r1(x: float) -> float:26    return float(Decimal(str(x)).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP))2728def r2(x: float) -> float:29    return float(Decimal(str(x)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))3031def arrondi(x: float, pas: int = 5000) -> int:32    return int(pas * Decimal(str(x / pas)).quantize(Decimal("1"), rounding=ROUND_HALF_UP))3334def mois_ecoules(vente: dt.date, evaluation: dt.date) -> int:35    return (evaluation.year - vente.year) * 12 + (evaluation.month - vente.month)3637CHECKS: list[dict] = []   # vérifications des decks originaux38def check(doc: str, libelle: str, attendu, obtenu, tol: float = 0.5, note: str = ""):39    ok = abs(float(attendu) - float(obtenu)) <= tol40    CHECKS.append({"document": doc, "element": libelle, "valeur_deck": attendu, "valeur_recalculee": obtenu,41                   "exact": ok, "note": note})42    return ok4344# =============================================================================45# 1. CATALOGUE D'AJUSTEMENTS DU COURS (IMM1003, séances 8 et 9) — convention pédagogique46#    Aligné sur les conventions de la firme du TP2 officiel (temps 0,17 %/mois ; aire 150 $/pi² ; lot 6 $/pi² au-delà47#    de 1 000 pi² d'écart ; garage 30 000 $ ; sous-sol aménagé 20 000 $ ; salle de bains 12 000 $ ; piscine 25 000 $).48# =============================================================================49CATALOGUE = {50    "temps_pct_par_mois": 0.17,        # +0,17 %/mois ≈ +2 %/an (APCIQ, unifamiliale RMR de Gatineau, 2026) — prorata simple51    "aire_par_pi2": 150,               # valeur contributive marginale de l'aire habitable ($/pi² d'écart)52    "lot_par_pi2": 6,                  # $/pi² d'écart de terrain53    "lot_seuil_pi2": 1000,             # aucun ajustement si |écart| < 1 000 pi²54    "garage_simple_vs_aucun": 30000,55    "garage_double_vs_simple": 15000,56    "soussol_fini_vs_non_fini": 20000,57    "soussol_fini_vs_partiel": 10000,  # donc partiel vs non fini = 10 000 $58    "sdb_complete": 12000,59    "salle_eau": 6000,60    "chambre": 10000,61    "age_par_an": 2000,62    "etat": {"très bon": -8000, "bon": 0, "moyen": 12000},   # ajustement si le COMPARABLE est dans cet état et le sujet « bon »63    "localisation_pct": 5.0,           # ± 5 % du prix ajusté transactionnel (analyse par paires)64    "piscine_creusee": 25000,65}66GARAGE_RANG = {"aucun": 0, "simple": 1, "double": 2}67SOUSSOL_RANG = {"non fini": 0, "partiel": 1, "fini": 2}6869def valeur_garage(niveau_sujet: str, niveau_comp: str) -> int:70    """Ajustement au comparable pour le garage (comparable → sujet)."""71    def v(n):  # valeur cumulée d'un niveau par rapport à « aucun »72        return {0: 0, 1: CATALOGUE["garage_simple_vs_aucun"],73                2: CATALOGUE["garage_simple_vs_aucun"] + CATALOGUE["garage_double_vs_simple"]}[GARAGE_RANG[n]]74    return v(niveau_sujet) - v(niveau_comp)7576def valeur_soussol(sujet: str, comp: str) -> int:77    def v(n):78        return {0: 0, 1: CATALOGUE["soussol_fini_vs_non_fini"] - CATALOGUE["soussol_fini_vs_partiel"],79                2: CATALOGUE["soussol_fini_vs_non_fini"]}[SOUSSOL_RANG[n]]80    return v(sujet) - v(comp)8182def valeur_sdb(sujet: tuple[int, int], comp: tuple[int, int]) -> int:83    """(complètes, salles d'eau)"""84    return (sujet[0] - comp[0]) * CATALOGUE["sdb_complete"] + (sujet[1] - comp[1]) * CATALOGUE["salle_eau"]8586def grille(sujet: dict, comp: dict, date_eval: dt.date) -> dict:87    """Applique la séquence du cours : 5 ajustements transactionnels en séquence, puis 3 ajustements de propriété additifs.88    Signe : on ajuste le COMPARABLE vers le SUJET (comparable supérieur → négatif)."""89    rows = []90    prix = comp["prix"]91    cour = prix92    # --- transactionnels (séquence) ---93    droits = comp.get("droits", 0); cour += droits; rows.append(("1. Droits de propriété", droits, cour))94    fin = -comp.get("financement_avantage", 0); cour += fin; rows.append(("2. Conditions de financement", fin, cour))95    cond = comp.get("conditions_vente_pct", 0.0); m_cond = r0(cour * cond / 100); cour += m_cond96    rows.append((f"3. Conditions de vente ({cond:+.1f} %)" if cond else "3. Conditions de vente", m_cond, cour))97    dep = comp.get("depenses_apres_achat", 0); cour += dep; rows.append(("4. Dépenses après achat", dep, cour))98    mois = mois_ecoules(comp["date"], date_eval)99    t = r0(cour * CATALOGUE["temps_pct_par_mois"] / 100 * mois); cour += t100    rows.append((f"5. Temps ({mois} mois × 0,17 %)", t, cour))101    prix_trans = cour102    # --- propriété (additifs sur le prix ajusté transactionnel) ---103    prop = []104    loc = comp.get("localisation", "similaire")105    loc_pct = {"similaire": 0.0, "inférieure": CATALOGUE["localisation_pct"], "supérieure": -CATALOGUE["localisation_pct"]}[loc]106    prop.append((f"6. Localisation ({loc}, {loc_pct:+.0f} %)" if loc_pct else "6. Localisation (similaire)", r0(prix_trans * loc_pct / 100)))107    d_aire = comp["aire"] - sujet["aire"]108    prop.append((f"7a. Aire habitable ({d_aire:+d} pi² × 150 $)", -d_aire * CATALOGUE["aire_par_pi2"]))109    d_lot = comp["lot"] - sujet["lot"]110    lot_adj = -d_lot * CATALOGUE["lot_par_pi2"] if abs(d_lot) >= CATALOGUE["lot_seuil_pi2"] else 0111    prop.append((f"7b. Terrain ({d_lot:+d} pi²{'' if abs(d_lot) >= CATALOGUE['lot_seuil_pi2'] else ', < 1 000'})", lot_adj))112    d_age = comp["annee"] - sujet["annee"]   # >0 : comparable plus récent → négatif113    prop.append((f"7c. Âge ({d_age:+d} an{'s' if abs(d_age) > 1 else ''} × 2 000 $)", -d_age * CATALOGUE["age_par_an"]))114    prop.append((f"7d. État ({comp['etat']})", CATALOGUE["etat"][comp["etat"]] - CATALOGUE["etat"][sujet["etat"]]))115    d_ch = comp["chambres"] - sujet["chambres"]116    prop.append((f"7e. Chambres ({d_ch:+d})", -d_ch * CATALOGUE["chambre"]))117    prop.append((f"7f. Salles de bains ({comp['sdb'][0]} c. + {comp['sdb'][1]} s.e. vs {sujet['sdb'][0]} + {sujet['sdb'][1]})", valeur_sdb(sujet["sdb"], comp["sdb"])))118    prop.append((f"7g. Sous-sol ({comp['soussol']} vs {sujet['soussol']})", valeur_soussol(sujet["soussol"], comp["soussol"])))119    prop.append((f"7h. Garage ({comp['garage']} vs {sujet['garage']})", valeur_garage(sujet["garage"], comp["garage"])))120    pisc = (int(sujet.get("piscine", False)) - int(comp.get("piscine", False))) * CATALOGUE["piscine_creusee"]121    if sujet.get("piscine") or comp.get("piscine"):122        prop.append(("7i. Piscine creusée", pisc))123    nonimm = -comp.get("non_immobilier", 0)124    prop.append(("8. Composantes non immobilières", nonimm))125    total_prop = sum(v for _, v in prop)126    indication = prix_trans + total_prop127    ajustements = [v for _, v, _ in rows] + [v for _, v in prop]128    net = sum(ajustements); brut = sum(abs(v) for v in ajustements)129    return {130        "nom": comp["nom"], "prix": prix, "mois": mois, "transactionnels": rows, "prix_ajuste_transactionnel": prix_trans,131        "propriete": prop, "total_propriete": total_prop, "indication": indication,132        "net": net, "net_pct": r1(net / prix * 100), "brut": brut, "brut_pct": r1(brut / prix * 100),133        "seuil_net_ok": abs(net) / prix <= 0.15, "seuil_brut_ok": brut / prix <= 0.25,134        "prix_pi2": r0(prix / comp["aire"]), "indication_pi2": r0(indication / sujet["aire"]),135    }136137def reconcilier(grilles: list[dict], poids: dict[str, float]) -> dict:138    assert abs(sum(poids.values()) - 100) < 1e-9, "les poids doivent totaliser 100"139    vp = sum(g["indication"] * poids[g["nom"]] / 100 for g in grilles)140    vals = [g["indication"] for g in grilles]141    moy = sum(vals) / len(vals)142    med = sorted(vals)[len(vals) // 2] if len(vals) % 2 else (sorted(vals)[len(vals)//2 - 1] + sorted(vals)[len(vals)//2]) / 2143    return {"poids": poids, "valeur_ponderee": r0(vp), "valeur_conclue": arrondi(vp), "moyenne_simple": r0(moy),144            "moyenne_arrondie": arrondi(moy), "mediane": r0(med), "min": min(vals), "max": max(vals),145            "etendue_pct": r1((max(vals) - min(vals)) / vp * 100)}146147# =============================================================================148# 2. IMM1003 S08 — ÉTUDE DE CAS : bungalow, Gatineau (secteur Hull), date d'évaluation 1er novembre 2026149# =============================================================================150DATE_EVAL = dt.date(2026, 11, 1)151SUJET_S8 = {"nom": "Sujet", "type": "bungalow", "annee": 2008, "aire": 1200, "lot": 6000, "chambres": 3, "sdb": (1, 1),152            "soussol": "partiel", "garage": "simple", "etat": "bon", "localisation": "Hull"}153COMPS_S8 = [154    {"nom": "C1", "prix": 435000, "date": dt.date(2026, 7, 1), "localisation": "similaire", "annee": 2007, "aire": 1250,155     "lot": 6200, "chambres": 3, "sdb": (1, 1), "soussol": "fini", "garage": "simple", "etat": "bon"},156    {"nom": "C2", "prix": 415000, "date": dt.date(2026, 5, 1), "localisation": "similaire", "annee": 2010, "aire": 1150,157     "lot": 5500, "chambres": 3, "sdb": (1, 0), "soussol": "non fini", "garage": "aucun", "etat": "bon", "non_immobilier": 4000},158    {"nom": "C3", "prix": 460000, "date": dt.date(2026, 8, 1), "localisation": "supérieure", "annee": 2005, "aire": 1350,159     "lot": 7000, "chambres": 4, "sdb": (2, 0), "soussol": "fini", "garage": "double", "etat": "très bon"},160    {"nom": "C4", "prix": 425000, "date": dt.date(2026, 3, 1), "localisation": "similaire", "annee": 2008, "aire": 1200,161     "lot": 6000, "chambres": 3, "sdb": (1, 1), "soussol": "partiel", "garage": "simple", "etat": "moyen"},162]163POIDS_S8 = {"C1": 40, "C4": 35, "C2": 20, "C3": 5}164165# Exercice « 4 parmi 8 » (bloc 2) : candidates à filtrer — scores de similarité166CANDIDATS_S8 = [167    # nom, secteur, type, date, prix, aire, remarque, décision attendue168    ("A", "Hull", "bungalow", "juil. 2026", 435000, 1250, "vente libre, vérifiée", "retenir (= C1)"),169    ("B", "Hull", "bungalow", "mai 2026", 415000, 1150, "inclut électroménagers 4 000 $", "retenir avec ajustement (= C2)"),170    ("C", "Hull", "bungalow", "août 2026", 460000, 1350, "secteur supérieur, 4 ch., garage double", "retenir, poids faible (= C3)"),171    ("D", "Hull", "bungalow", "mars 2026", 425000, 1200, "état moyen", "retenir (= C4)"),172    ("E", "Hull", "bungalow", "sept. 2026", 380000, 1180, "vente entre parents (lien de dépendance)", "écarter : non quantifiable"),173    ("F", "Aylmer", "bungalow", "juin 2026", 505000, 1210, "quartier différent, prix supérieur", "écarter : localisation non comparable (ou poids très faible)"),174    ("G", "Hull", "2 étages", "juil. 2026", 489000, 1700, "type différent", "écarter : type et taille"),175    ("H", "Hull", "bungalow", "oct. 2024", 398000, 1220, "vente il y a 25 mois", "écarter : trop ancienne (ajustement de temps dominant)"),176]177178# =============================================================================179# 3. IMM1003 S09 — ATELIER : unifamiliale 2 étages, Gatineau (Hull), cinq comparables, 1er novembre 2026180# =============================================================================181SUJET_S9 = {"nom": "Sujet", "type": "2 étages", "annee": 2005, "aire": 1500, "lot": 5500, "chambres": 3, "sdb": (2, 0),182            "soussol": "fini", "garage": "double", "etat": "bon", "localisation": "Hull", "role_2025": 385000,183            "role_terrain": 95000, "role_batiment": 290000}184185def equivalence_comptant(solde: float, taux_contrat: float, taux_marche: float, annees: int) -> dict:186    """Solde de prix de vente à intérêts seulement, capital remboursé à l'échéance : avantage = solde − VA(paiements au taux du marché)."""187    i_c, i_m = taux_contrat / 100, taux_marche / 100188    interets = solde * i_c189    facteur_annuite = (1 - (1 + i_m) ** -annees) / i_m190    va = interets * facteur_annuite + solde / (1 + i_m) ** annees191    return {"solde": solde, "taux_contrat": taux_contrat, "taux_marche": taux_marche, "annees": annees,192            "interets_annuels": r0(interets), "facteur_annuite": r2(r2(facteur_annuite)), "va_capital": r0(solde / (1 + i_m) ** annees),193            "va_interets": r0(interets * facteur_annuite), "valeur_actuelle": r0(va), "avantage": r0(solde - va)}194195FIN_C1 = equivalence_comptant(100000, 2.0, 5.0, 3)   # avantage ≈ 8 170 $196197COMPS_S9 = [198    {"nom": "C1", "prix": 410000, "date": dt.date(2026, 2, 1), "localisation": "similaire", "annee": 2003, "aire": 1580,199     "lot": 5000, "chambres": 3, "sdb": (2, 0), "soussol": "non fini", "garage": "simple", "etat": "bon",200     "financement_avantage": FIN_C1["avantage"], "particularite": "solde de prix de vente 100 000 $ à 2 % (3 ans, intérêts seulement) ; marché 5 %"},201    {"nom": "C2", "prix": 445000, "date": dt.date(2026, 7, 1), "localisation": "similaire", "annee": 2006, "aire": 1480,202     "lot": 5200, "chambres": 3, "sdb": (1, 0), "soussol": "fini", "garage": "double", "etat": "très bon"},203    {"nom": "C3", "prix": 425000, "date": dt.date(2026, 4, 1), "localisation": "similaire", "annee": 2005, "aire": 1520,204     "lot": 6000, "chambres": 3, "sdb": (2, 0), "soussol": "fini", "garage": "double", "etat": "bon",205     "depenses_apres_achat": 15000, "particularite": "toiture à refaire connue de l'acheteur : 15 000 $"},206    {"nom": "C4", "prix": 460000, "date": dt.date(2026, 8, 1), "localisation": "supérieure", "annee": 2008, "aire": 1650,207     "lot": 7000, "chambres": 4, "sdb": (2, 0), "soussol": "fini", "garage": "double", "etat": "très bon",208     "particularite": "secteur Aylmer (paire de ventes : +5 % vs Hull)"},209    {"nom": "C5", "prix": 438000, "date": dt.date(2026, 5, 1), "localisation": "similaire", "annee": 2004, "aire": 1500,210     "lot": 5500, "chambres": 3, "sdb": (2, 0), "soussol": "partiel", "garage": "simple", "etat": "moyen",211     "non_immobilier": 6000, "particularite": "inclut électroménagers et spa : 6 000 $"},212]213POIDS_S9 = {"C2": 30, "C3": 30, "C1": 20, "C5": 15, "C4": 5}214POIDS_S9_SANS_C4 = {"C2": 32, "C3": 32, "C1": 21, "C5": 15, "C4": 0}215216# Démonstrations S9 ----------------------------------------------------------217PAIRES = {218    "garage_double_vs_simple": {"A (double)": 452000, "B (simple)": 437000},219    "localisation_aylmer_vs_hull": {"Aylmer": 472000, "Hull": 449000},220    "soussol_amenage": {"aménagé": 441000, "non aménagé": 421000},221}222REVENTE = {"prix_2024": 410000, "date_2024": "1er nov. 2024", "prix_2026": 426600, "date_2026": "1er nov. 2026", "mois": 24}223CAPITALISATION = {"loyer_mensuel_garage": 150, "taux": 0.07}224# Régression (démonstration) : 12 ventes de bungalows à Hull, prix vs aire habitable (données pédagogiques fixes)225REG_AIRES = [1050, 1100, 1120, 1150, 1180, 1200, 1220, 1250, 1280, 1300, 1350, 1400]226REG_RESID = [-9000, 6500, -4000, 11000, -7500, 2000, 9500, -11000, 4000, -3000, 7000, -5500]227REG_PRIX = [r0(250000 + 150 * a + e) for a, e in zip(REG_AIRES, REG_RESID)]228229def ols(x: list[float], y: list[float]) -> dict:230    n = len(x); mx = sum(x) / n; my = sum(y) / n231    sxx = sum((xi - mx) ** 2 for xi in x); sxy = sum((xi - mx) * (yi - my) for xi, yi in zip(x, y))232    b1 = sxy / sxx; b0 = my - b1 * mx233    yhat = [b0 + b1 * xi for xi in x]234    sse = sum((yi - yh) ** 2 for yi, yh in zip(y, yhat)); sst = sum((yi - my) ** 2 for yi in y)235    se_b1 = math.sqrt(sse / (n - 2) / sxx)236    return {"n": n, "beta0": r0(b0), "beta1": r2(b1), "r2": r2(1 - sse / sst), "se_beta1": r2(se_b1),237            "t_beta1": r2(b1 / se_b1), "ecart_type_residus": r0(math.sqrt(sse / (n - 2)))}238239# Exemple A (fiabilité, V1 à V5) conservé de la version originale (exact)240EXEMPLE_A = [("V1", 456000, 5, 7.9, 11.8, "8 mois", "Moyen"), ("V2", 448000, 3, 4.2, 6.5, "3 mois", "Fort"),241             ("V3", 462000, 6, 12.1, 18.4, "10 mois", "Faible"), ("V4", 455000, 4, 5.8, 9.1, "5 mois", "Fort"),242             ("V5", 470000, 7, 16.3, 22.7, "14 mois", "Écarté")]243244# =============================================================================245# 4. IMM1033 S08 — CHAÎNE DU COÛT NEUF (convention du cours = TP2) et six logements de Gatineau (Hull)246# =============================================================================247PARAM_1033 = {248    "cg_pct": 8.0, "fg_pct": 7.0, "profit_eg_pct": 8.0, "contingences_pct": 5.0,249    "permis_par_1000": 4.70, "permis_min_par_logement": 335,          # Gatineau, règl. 501-2005, art. 104 (TP2)250    "gcr_par_unite": 2656.68, "gcr_max_logements_vises": 5,            # B-1.1, r. 8, art. 2 : unifamiliale, duplex à quintuplex, copropriété ≤ 4 parties privatives superposées (vérifié 2026-11-02)251    "taux_directeur": 2.50, "taux_preferentiel": 4.70, "prime_construction": 2.05,   # BdC 28 oct. 2026 (vérifié) ; préférentiel 4,70 % depuis le 29 oct. 2026 (vérifié) ; prime = approximation (aucune source bancaire publique)252    "honoraires_pct_directs_cours": 4.0, "assurance_pct_dc": 0.8, "profit_entrepreneurial_pct": 11.0,253    "tps": 5.0, "tvq": 9.975,254}255SIX_LOGEMENTS = {256    "nom": "Immeuble de six logements, secteur Hull (Gatineau)", "logements": 6, "programme": "3 × 4½ et 3 × 5½",257    "aire_hors_sol_pi2": 5400, "aire_sous_sol_pi2": 1800, "stationnements": 8, "duree_mois": 14, "juridique_forfait": 12000,258    "terrain_valeur": 210000,259    "directs_nus": [  # divisions (prix nus composites RSMeans 2026 T4, localisés Gatineau — hypothèse pédagogique calibrée sur le TP2)260        ("Fondations et structure (béton, ossature bois)", 170000), ("Enveloppe (brique 70 %, revêtement, fenêtres, portes)", 145000),261        ("Toiture plate à membrane", 42000), ("Plomberie, chauffage (plinthes), VRC", 120000), ("Électricité (6 entrées)", 65000),262        ("Finitions intérieures (cuisines, salles de bains, planchers, gypse, peinture)", 175000),263        ("Équipements (buanderie commune, rangements)", 20000), ("Site : excavation, branchements, 8 stationnements, aménagement", 48000),264    ],265}266# Marché (hypothèse pédagogique) : ventes de six logements neufs à Gatineau pour l'extraction du profit267MARCHE_6LOG = [("M1", 1560000, 215000), ("M2", 1510000, 205000), ("M3", 1595000, 225000), ("M4", 1465000, 200000)]  # prix, terrain268269def chaine_cout(cas: dict, p: dict, gcr_applicable: bool | None = None, duree: int | None = None, i_pct: float | None = None) -> dict:270    nus = sum(v for _, v in cas["directs_nus"])271    cg = r0(nus * p["cg_pct"] / 100); cum1 = nus + cg272    fg = r0(cum1 * p["fg_pct"] / 100); cum2 = cum1 + fg273    peg = r0(cum2 * p["profit_eg_pct"] / 100); directs_cours = cum2 + peg274    conting = r0(nus * p["contingences_pct"] / 100); dc = directs_cours + conting275    permis_prop = r0(dc / 1000 * p["permis_par_1000"]); permis_min = p["permis_min_par_logement"] * cas["logements"]276    permis = max(permis_prop, permis_min)277    if gcr_applicable is None:278        gcr_applicable = cas["logements"] <= p["gcr_max_logements_vises"]279    gcr = r0(p["gcr_par_unite"] * cas["logements"]) if gcr_applicable else 0280    T = duree if duree is not None else cas["duree_mois"]281    i = (i_pct if i_pct is not None else p["taux_preferentiel"] + p["prime_construction"])282    interets = r0(dc * i / 100 * T / 12 * 0.5)283    honoraires = r0(directs_cours * p["honoraires_pct_directs_cours"] / 100)284    assurance = r0(dc * p["assurance_pct_dc"] / 100)285    juridique = cas["juridique_forfait"]286    indirects = permis + gcr + interets + honoraires + assurance + juridique287    assiette = dc + indirects288    profit = r0(assiette * p["profit_entrepreneurial_pct"] / 100)289    total = assiette + profit290    return {291        "directs_nus": nus, "cg": cg, "apres_cg": cum1, "fg": fg, "apres_fg": cum2, "profit_eg": peg, "directs_cours": directs_cours,292        "contingences": conting, "directs_plus_contingences": dc, "majoration_cascade_pct": r2((directs_cours / nus - 1) * 100),293        "permis_proportionnel": permis_prop, "permis_minimum": permis_min, "permis": permis,294        "gcr_applicable": gcr_applicable, "gcr": gcr, "taux_pret": i, "duree_mois": T, "interets_intercalaires": interets,295        "honoraires": honoraires, "assurance": assurance, "juridique": juridique, "indirects": indirects,296        "ratio_indirects_directs_cours_pct": r1(indirects / directs_cours * 100), "assiette_profit": assiette,297        "profit_entrepreneurial": profit, "cout_total": total,298        "cout_pi2_hors_sol": r2(total / cas["aire_hors_sol_pi2"]), "cout_pi2_brut": r2(total / (cas["aire_hors_sol_pi2"] + cas["aire_sous_sol_pi2"])),299        "cout_par_logement": r0(total / cas["logements"]), "redevances_developpement": 0,  # Gatineau règl. 961-2024 : 3 986,39 $/logement en zone 1 (Aylmer) seulement ; Hull = 0300        "profit_sur_cout_pct": p["profit_entrepreneurial_pct"], "marge_sur_prix_pct": r2(p["profit_entrepreneurial_pct"] / (100 + p["profit_entrepreneurial_pct"]) * 100),301        "part_indirects_du_total_pct": r1(indirects / total * 100), "part_profit_du_total_pct": r1(profit / total * 100),302    }303304def interets_par_periode(montant: float, i_pct: float, T: int, profil: str = "lineaire") -> dict:305    """Méthode par période : I = Σ B_t · i · Δt, décaissements mensuels selon un profil (linéaire ou courbe en S 3t²−2t³),306    intérêts du mois calculés sur le solde en début de mois (décaissement en fin de mois), capitalisés au solde."""307    i_m = i_pct / 100 / 12308    cum = lambda t: t / T if profil == "lineaire" else 3 * (t / T) ** 2 - 2 * (t / T) ** 3309    solde = 0.0; total_int = 0.0; lignes = []310    for t in range(1, T + 1):311        debours = montant * (cum(t) - cum(t - 1))312        interet = solde * i_m313        solde += debours + interet314        total_int += interet315        lignes.append({"mois": t, "decaissement": r0(debours), "cumul_decaisse": r0(montant * cum(t)), "interet": r0(interet), "solde_fin": r0(solde)})316    return {"profil": profil, "montant": montant, "taux": i_pct, "duree": T, "interets_total": r0(total_int), "lignes": lignes,317            "solde_moyen": r0(sum(l["solde_fin"] for l in lignes) / T)}318319def taxes_habitation_neuve(prix: float, p: dict, premier_acheteur: bool = False, tvq_rembours: bool = True) -> dict:320    """TPS/TVQ pour une habitation neuve achetée par un occupant (règles générales 2026 ; remboursement TPS 36 % ≤ 350 000 $,321    dégressif jusqu'à 450 000 $ ; remboursement TVQ 50 % max 9 975 $ ≤ 200 000 $, dégressif jusqu'à 300 000 $).322    Le remboursement « premiers acheteurs » (jusqu'à 100 % de la TPS ≤ 1 M$, dégressif à 1,5 M$) est calculé à part : statut à confirmer."""323    tps = prix * p["tps"] / 100; tvq = prix * p["tvq"] / 100324    if prix <= 350000: remb_tps = min(tps * 0.36, 6300)325    elif prix < 450000: remb_tps = 6300 * (450000 - prix) / 100000326    else: remb_tps = 0.0327    if prix <= 200000: remb_tvq = min(tvq * 0.50, 9975)328    elif prix < 300000: remb_tvq = 9975 * (300000 - prix) / 100000329    else: remb_tvq = 0.0330    if not tvq_rembours: remb_tvq = 0.0331    remb_tps_pa = 0.0332    if premier_acheteur:333        remb_tps_pa = tps if prix <= 1000000 else (tps * (1500000 - prix) / 500000 if prix < 1500000 else 0.0)334    remb_tps_eff = max(remb_tps, remb_tps_pa)335    net = tps + tvq - remb_tps_eff - remb_tvq336    return {"prix": prix, "tps": r2(tps), "tvq": r2(tvq), "taxes_brutes": r2(tps + tvq), "remb_tps_general": r2(remb_tps),337            "remb_tps_premier_acheteur": r2(remb_tps_pa), "remb_tps_retenu": r2(remb_tps_eff), "remb_tvq": r2(remb_tvq),338            "taxes_nettes": r2(net), "taxes_nettes_pct": r2(net / prix * 100)}339340# =============================================================================341# 5. VÉRIFICATION DES DECKS ORIGINAUX342# =============================================================================343def verifier_originaux():344    d = "IMM1003 S08 (original)"345    # grille originale à 5 %/an, prorata mensuel, 15 sept. 2026346    for nom, prix, mois, att in [("C1", 435000, 2, 3600), ("C2", 415000, 4, 6900), ("C3", 460000, 1, 1900), ("C4", 425000, 6, 10600)]:347        check(d, f"temps {nom} (5 %/an)", att, r0(prix * 0.05 * mois / 12 / 100) * 100, tol=100, note=f"exact à 5 %/an, arrondi à la centaine dans le deck (valeur exacte {r0(prix * 0.05 * mois / 12)} $) ; taux non sourcé (APCIQ 2026 ≈ 2 %/an)")348    check(d, "net propriété C1", -14000, sum([0, -5000, -2000, -2000, -5000, 0, 0, 0]))349    check(d, "net propriété C2", 60000, sum([10000, 5000, 5000, 3000, 12000, 20000, 5000, 0]))350    check(d, "net propriété C3", -92000, sum([-15000, -15000, -10000, -8000, -10000, -18000, -8000, -8000]))351    check(d, "net propriété C4", 13000, sum([0, 0, 0, 10000, 3000, 0, 0, 0]))352    check(d, "prix ajusté final C2", 481900, 415000 + 6900 + 60000)353    check(d, "moyenne des 4 indications", 431250, (424600 + 481900 + 369900 + 448600) / 4)354    check(d, "ampleur C3 brut %", 20.4, r1((1900 + 92000) / 460000 * 100), tol=0.05)355    check(d, "ampleur C1 net %", -2.4, r1((3600 - 14000) / 435000 * 100), tol=0.05)356    check(d, "financement : reprise 100 000 $ à 2 % vs 5 % (3 ans) — deck ≈ 10 000 $", 10000, FIN_C1["avantage"], tol=500,357          note="montant posé sans calcul ; équivalence au comptant : 8 170 $ (3 ans) ou 12 990 $ (5 ans)")358    check(d, "financement (5 ans)", 10000, equivalence_comptant(100000, 2, 5, 5)["avantage"], tol=500, note="idem, 5 ans")359    d = "IMM1003 S09 (original)"360    check(d, "temps C1 : 410 000 × 5 % × 8/12", 13667, 410000 * 0.05 * 8 / 12, tol=1)361    check(d, "indication C1", 456167, 410000 + 13667 + 32500)362    check(d, "net C1 % (sans le temps)", 7.9, r1(32500 / 410000 * 100), tol=0.05, note="convention différente de S08 (qui inclut le temps)")363    check(d, "brut C1 % (sans le temps)", 11.8, r1(48500 / 410000 * 100), tol=0.05)364    check(d, "temps C2 : 445 000 × 5 % × 3/12", 5563, 445000 * 0.05 * 3 / 12, tol=1)365    check(d, "indication C2", 454663, 445000 + 5562.5 + 4100, tol=1)366    check(d, "temps C3", 10625, 425000 * 0.05 * 6 / 12, tol=1); check(d, "indication C3", 430125, 435625 - 5500)367    check(d, "temps C4", 3833, 460000 * 0.05 * 2 / 12, tol=1); check(d, "localisation C4 −5 %", -23192, -0.05 * 463833, tol=1)368    check(d, "indication C4", 389141, 463833 - 74692); check(d, "net C4 %", 16.2, r1(74692 / 460000 * 100), tol=0.05)369    check(d, "temps C5", 9125, 438000 * 0.05 * 5 / 12, tol=1); check(d, "indication C5", 484125, 447125 + 37000)370    check(d, "quiz net 350 000 $", 3.7, r1(13000 / 350000 * 100), tol=0.05); check(d, "quiz brut", 12.3, r1(43000 / 350000 * 100), tol=0.05)371    check(d, "capitalisation garage 150 × 12 / 0,07", 25700, 150 * 12 / 0.07, tol=20)372    check(d, "financement : hypothèque 400 000 $ assumable 2 % vs 5 % — deck 12 000 $", 12000, equivalence_comptant(400000, 2, 5, 5)["avantage"], tol=1000,373          note="NON ÉTAYÉ : montant posé sans base (solde, durée) ; sur 400 000 $ et 5 ans l'équivalence au comptant donne ≈ 51 954 $ ; S08 dit ≈ 10 000 $ pour 100 000 $")374    d = "IMM1033 S08 (original)"375    check(d, "exemple rapide : total 660 000 $", 660000, 500000 * 1.2 * 1.1); check(d, "honoraires 8 logements", 225500, 96000 + 24000 + 36000 + 18000 + 3500 + 48000)376    check(d, "honoraires / directs 8 logements %", 18.8, r1(225500 / 1200000 * 100), tol=0.05, note="exact mais invraisemblable (convention du cours : 4 %)")377    check(d, "intercalaires 800 000 × 7 % × 10/12 × ½", 23333, 800000 * 0.07 * 10 / 12 * 0.5, tol=1)378    check(d, "TPS 350 000 $", 17500, 350000 * 0.05); check(d, "TVQ 350 000 $", 34913, 350000 * 0.09975, tol=1)379    tx = taxes_habitation_neuve(350000, PARAM_1033)380    check(d, "remboursement TVQ à 350 000 $ (deck : 9 975 $)", 9975, tx["remb_tvq"], tol=1, note="ERREUR : nul au-delà de 300 000 $")381    check(d, "taxes nettes 350 000 $ (deck : 36 138 $)", 36138, tx["taxes_nettes"], tol=1, note="corrigé : 46 112,50 $ (13,2 %)")382    check(d, "assurances/légal 6 logements : 35 500 $", 35500, 9000 + 3000 + 4500 + 4000 + 15000, note="dont GCR 15 000 $ pour un 6 logements : plan non applicable (à confirmer)")383    check(d, "extraction du profit 15,7 %", 15.7, r1(170000 / 1080000 * 100), tol=0.05)384    ind = [19425, 5550, 6937.5, 2500, 6475, 2081.25, 2775, 2220, 2500, 2000, 4162.5]385    check(d, "exemple résidentiel : indirects 56 627 $", 56627, sum(ind), tol=1); check(d, "coût total 367 540 $", 367540, (277500 + sum(ind)) * 1.10, tol=2, note="exact (arrondi des lignes)")386    check(d, "coût total $/pi² 245", 245, 367540 / 1500, tol=0.5); check(d, "permis exemple résidentiel (10 $/1 000 $) 2 775 $", 2775, 277500 / 1000 * 10,387          note="hypothèse 10 $/1 000 $ ; tarif de Gatineau : 4,70 $/1 000 $, min. 335 $/logement → 1 304 $")388    check(d, "commercial : indirects 205 375 $", 205375, 775000 * 0.265, tol=1); check(d, "commercial : total 1 098 020 $", 1098020, 980375 * 1.12, tol=1)389    check(d, "6 logements : directs 692 200 $", 692200, (28 + 22 + 24 + 12 + 32) * 5400 + 35000 + 20000)390    check(d, "6 logements : 128 $/pi²", 128, 692200 / 5400, tol=0.5)391    check(d, "taux directeur 2,25 % (15 juillet 2026)", 2.25, PARAM_1033["taux_directeur"], tol=0.001, note="périmé : 2,50 % depuis le 28 oct. 2026 (vérifié, Banque du Canada) ; préférentiel 4,70 %")392393# =============================================================================394# 6. EXÉCUTION395# =============================================================================396def main(argv):397    out_json = HERE / "resultats.json"; out_md = HERE / "RESULTATS_CALCULS.md"398    if "--json" in argv: out_json = Path(argv[argv.index("--json") + 1])399    if "--md" in argv: out_md = Path(argv[argv.index("--md") + 1])400    verifier_originaux()401    R: dict = {"date_generation": dt.date.today().isoformat(), "catalogue": CATALOGUE, "parametres_1033": PARAM_1033}402    # ---- IMM1003 S08403    g8 = [grille(SUJET_S8, c, DATE_EVAL) for c in COMPS_S8]404    rec8 = reconcilier(g8, POIDS_S8)405    R["imm1003_s08"] = {"date_evaluation": DATE_EVAL.isoformat(), "sujet": SUJET_S8, "comparables": COMPS_S8, "grilles": g8, "reconciliation": rec8,406                        "candidats_4_parmi_8": CANDIDATS_S8,407                        "exemples_criteres": {408                            "financement": FIN_C1, "financement_5_ans": equivalence_comptant(100000, 2, 5, 5),409                            "depenses_apres_achat": {"prix": 410000, "toiture": 15000, "equivalent": 425000},410                            "temps": {"prix": 400000, "mois": 12, "taux_mois": 0.17, "ajustement": r0(400000 * 0.0017 * 12), "compose_2pct": r0(400000 * 0.02)},411                            "non_immobilier": {"prix": 440000, "electros": 8000, "mobilier": 2000, "net": 430000},412                            "terrain": {"sujet": 5500, "comp": 7000, "ecart": 1500, "ajust": -1500 * 6},413                            "servitude": {"prix": 430000, "perte_pct": 3, "ajust": r0(430000 * 0.03)},414                            "conditions_vente_succession": {"prix": 405000, "ecart_pct": 4, "ajust": r0(405000 * 0.04)},415                        }}416    for c in COMPS_S8: c["date"] = c["date"].isoformat()417    # ---- IMM1003 S09418    g9 = [grille(SUJET_S9, c, DATE_EVAL) for c in COMPS_S9]419    rec9 = reconcilier(g9, POIDS_S9); rec9_sans_c4 = reconcilier(g9, POIDS_S9_SANS_C4)420    # sensibilités421    sens = {}422    base_g = CATALOGUE["garage_double_vs_simple"]423    for gv in (10000, 15000, 20000):424        CATALOGUE["garage_double_vs_simple"] = gv425        gg = [grille(SUJET_S9, {**c, "date": c["date"]}, DATE_EVAL) for c in COMPS_S9]426        sens[f"garage_double_vs_simple_{gv}"] = reconcilier(gg, POIDS_S9)["valeur_ponderee"]427    CATALOGUE["garage_double_vs_simple"] = base_g428    base_t = CATALOGUE["temps_pct_par_mois"]429    for tv in (-0.25, 0.0, 0.17, 0.34):430        CATALOGUE["temps_pct_par_mois"] = tv431        gg = [grille(SUJET_S9, c, DATE_EVAL) for c in COMPS_S9]432        sens[f"temps_{tv}"] = reconcilier(gg, POIDS_S9)["valeur_ponderee"]433    CATALOGUE["temps_pct_par_mois"] = base_t434    base_l = CATALOGUE["localisation_pct"]435    for lv in (3.0, 5.0, 8.0):436        CATALOGUE["localisation_pct"] = lv437        gg = [grille(SUJET_S9, c, DATE_EVAL) for c in COMPS_S9]438        sens[f"localisation_{lv}"] = reconcilier(gg, POIDS_S9)["valeur_ponderee"]439    CATALOGUE["localisation_pct"] = base_l440    reg = ols(REG_AIRES, REG_PRIX)441    paires = {k: {"ecart": max(v.values()) - min(v.values()), "ecart_pct": r1((max(v.values()) - min(v.values())) / min(v.values()) * 100), **v} for k, v in PAIRES.items()}442    rev = {**REVENTE, "variation_pct": r2((REVENTE["prix_2026"] / REVENTE["prix_2024"] - 1) * 100),443           "taux_annuel_compose_pct": r2(((REVENTE["prix_2026"] / REVENTE["prix_2024"]) ** (12 / REVENTE["mois"]) - 1) * 100),444           "taux_mensuel_simple_pct": r2((REVENTE["prix_2026"] / REVENTE["prix_2024"] - 1) * 100 / REVENTE["mois"])}445    aberrant = {"indications": [g["indication"] for g in g9], "mediane": rec9["mediane"],446                "ecarts_pct": {g["nom"]: r1((g["indication"] - rec9["mediane"]) / rec9["mediane"] * 100) for g in g9}, "seuil_pct": 10}447    R["imm1003_s09"] = {"date_evaluation": DATE_EVAL.isoformat(), "sujet": SUJET_S9, "comparables": COMPS_S9, "grilles": g9,448                        "reconciliation": rec9, "reconciliation_sans_C4": rec9_sans_c4, "sensibilite": sens, "regression": {"aires": REG_AIRES, "prix": REG_PRIX, **reg},449                        "paires": paires, "revente": rev, "capitalisation": {**CAPITALISATION, "valeur": r0(150 * 12 / 0.07)},450                        "exemple_A": EXEMPLE_A, "valeurs_aberrantes": aberrant, "equivalence_comptant_C1": FIN_C1,451                        "exemple_pct_vs_dollar": {"prix": 420000, "ajust_pct": 3, "ajust_dollar": r0(420000 * 0.03)},452                        "exemple_ordre": {"prix": 400000, "financement": -10000, "temps_pct": 2, "sequence": r0((400000 - 10000) * 1.02),453                                          "additif": r0(400000 * 1.02 - 10000), "ecart": r0((400000 - 10000) * 1.02 - (400000 * 1.02 - 10000))}}454    for c in COMPS_S9: c["date"] = c["date"].isoformat()455    # ---- IMM1033 S08456    ch = chaine_cout(SIX_LOGEMENTS, PARAM_1033)457    ch_gcr = chaine_cout(SIX_LOGEMENTS, PARAM_1033, gcr_applicable=True)458    dc = ch["directs_plus_contingences"]; i = ch["taux_pret"]; T = ch["duree_mois"]459    per_lin = interets_par_periode(dc, i, T, "lineaire"); per_s = interets_par_periode(dc, i, T, "courbe_en_S")460    sens_i = {f"{iv}": chaine_cout(SIX_LOGEMENTS, PARAM_1033, i_pct=iv) for iv in (5.75, 6.75, 7.75, 8.75)}461    sens_T = {f"{tv}": chaine_cout(SIX_LOGEMENTS, PARAM_1033, duree=tv) for tv in (10, 14, 18, 20, 24)}462    retard = chaine_cout(SIX_LOGEMENTS, PARAM_1033, duree=T + 6)463    # unités et marché464    assiette = ch["assiette_profit"]465    marche = []466    for nom, prix, terr in MARCHE_6LOG:467        bat = prix - terr468        marche.append({"vente": nom, "prix": prix, "terrain": terr, "batiment_implicite": bat, "profit_extrait_pct": r1((bat - assiette) / assiette * 100),469                       "prix_par_logement": r0(prix / 6)})470    bats = sorted(m["batiment_implicite"] for m in marche); med_bat = (bats[1] + bats[2]) / 2471    extraction = {"ventes": marche, "batiment_implicite_median": r0(med_bat), "assiette": assiette,472                  "profit_extrait_median_pct": r1((med_bat - assiette) / assiette * 100), "ecart_cout_total_vs_marche_pct": r1((ch["cout_total"] - med_bat) / med_bat * 100)}473    taxes = {"occupant_350000": taxes_habitation_neuve(350000, PARAM_1033), "occupant_280000": taxes_habitation_neuve(280000, PARAM_1033),474             "occupant_premier_acheteur_650000": taxes_habitation_neuve(650000, PARAM_1033, premier_acheteur=True),475             "occupant_premier_acheteur_350000": taxes_habitation_neuve(350000, PARAM_1033, premier_acheteur=True),476             "locatif_6_logements": {"base_jvm_estimee": ch["cout_total"] + SIX_LOGEMENTS["terrain_valeur"], "tps": r2((ch["cout_total"] + SIX_LOGEMENTS["terrain_valeur"]) * 0.05),477                                     "tvq": r2((ch["cout_total"] + SIX_LOGEMENTS["terrain_valeur"]) * 0.09975),478                                     "remb_tps_pct": 100, "tps_nette": 0.0, "remb_tvq": 0.0,479                                     "note": "autocotisation à l'achèvement (JVM) ; remboursement bonifié de 100 % de la TPS (C-56 : locatif ≥ 4 logements, chantier commencé du 14 sept. 2023 au 31 déc. 2030) ; Québec non harmonisé : remboursement TVQ locatif 36 % max 7 182 $ par logement, nul si JVM du logement ≥ 225 000 $ → 0 $ ici (base estimée ÷ 6 ≈ 242 800 $/logement)"}}480    honoraires_methodes = {"pourcentage": {"base": ch["directs_cours"], "taux": 4.0, "montant": ch["honoraires"]},481                           "detail_convention": {"architecte_pct": 2.5, "ingenieurs_pct": 1.5, "architecte": r0(ch["directs_cours"] * 0.025), "ingenieurs": r0(ch["directs_cours"] * 0.015)},482                           "horaire": {"heures": 260, "taux_horaire": 145, "montant": 260 * 145},483                           "forfait": {"montant": 38500}}484    profit_marge = {"cout": assiette, "profit_11_sur_cout": ch["profit_entrepreneurial"], "prix_equivalent": ch["cout_total"],485                    "marge_sur_prix_pct": ch["marge_sur_prix_pct"], "profit_pour_marge_11_sur_prix": r0(assiette * 0.11 / 0.89),486                    "taux_sur_cout_equivalent_marge_11_pct": r2(0.11 / 0.89 * 100)}487    R["imm1033_s08"] = {"cas": SIX_LOGEMENTS, "chaine": ch, "chaine_si_gcr_applicable": ch_gcr, "interets_methode_simplifiee": ch["interets_intercalaires"],488                        "interets_par_periode_lineaire": per_lin, "interets_par_periode_courbe_S": per_s,489                        "sensibilite_taux": {k: {"taux": v["taux_pret"], "interets": v["interets_intercalaires"], "cout_total": v["cout_total"]} for k, v in sens_i.items()},490                        "sensibilite_duree": {k: {"duree": v["duree_mois"], "interets": v["interets_intercalaires"], "cout_total": v["cout_total"]} for k, v in sens_T.items()},491                        "retard_6_mois": {"interets": retard["interets_intercalaires"], "surcout_interets": retard["interets_intercalaires"] - ch["interets_intercalaires"],492                                          "cout_total": retard["cout_total"], "surcout_total": retard["cout_total"] - ch["cout_total"]},493                        "extraction_marche": extraction, "taxes": taxes, "honoraires_methodes": honoraires_methodes, "profit_vs_marge": profit_marge,494                        "exemples_simples": {"intercalaires_800000": {"C": 800000, "i": 7.0, "T_mois": 10, "I": r0(800000 * 0.07 * 10 / 12 * 0.5)},495                                             "cascade_vs_somme": {"nus": 100000, "cascade": r0(100000 * 1.08 * 1.07 * 1.08), "somme_23pct": 123000},496                                             "permis_exemples": {"travaux_277500": max(r0(277500 / 1000 * 4.70), 335), "travaux_800000_4_log": max(r0(800000 / 1000 * 4.70), 4 * 335),497                                                                 "petits_travaux_40000_1_log": max(r0(40000 / 1000 * 4.70), 335)},498                                             "extraction_originale": {"prix": 1450000, "terrain": 200000, "directs": 900000, "indirects": 180000, "profit": 170000, "pct": r1(170000 / 1080000 * 100)}}}499    R["verifications_originaux"] = CHECKS500    out_json.write_text(json.dumps(R, ensure_ascii=False, indent=1, default=str), encoding="utf-8")501    out_md.write_text(rapport_md(R), encoding="utf-8")502    nb_err = sum(1 for c in CHECKS if not c["exact"])503    print(f"OK — {out_json.name}, {out_md.name} ; vérifications des decks originaux : {len(CHECKS)} contrôles, {nb_err} écart(s)/erreur(s) documenté(s)")504    print(f"S08 bungalow : {[ (g['nom'], g['indication']) for g in g8 ]} → pondérée {rec8['valeur_ponderee']} → {rec8['valeur_conclue']} $ (moyenne {rec8['moyenne_simple']})")505    print(f"S09 atelier  : {[ (g['nom'], g['indication']) for g in g9 ]} → pondérée {rec9['valeur_ponderee']} → {rec9['valeur_conclue']} $ (moyenne {rec9['moyenne_simple']})")506    print(f"IMM1033 six logements : directs nus {ch['directs_nus']} → directs du cours {ch['directs_cours']} → D+C {dc} → indirects {ch['indirects']} → total {ch['cout_total']} $ ({ch['cout_pi2_hors_sol']} $/pi²) ; I½ = {ch['interets_intercalaires']} vs période lin. {per_lin['interets_total']} / S {per_s['interets_total']}")507508def fmt(x) -> str:509    if isinstance(x, bool): return "oui" if x else "non"510    if isinstance(x, int): return f"{x:,}".replace(",", " ")511    if isinstance(x, float): return f"{x:,.2f}".replace(",", " ").replace(".", ",")512    return str(x)513514def rapport_md(R: dict) -> str:515    L = ["# Résultats de calcul — séances refondues (généré par verification_calculs.py)", "", f"Généré le {R['date_generation']}. Toute valeur citée dans les diapositives, scripts, exercices et corrigés doit provenir de ce fichier.", ""]516    L += ["## Catalogue d'ajustements du cours (IMM1003 S8–S9)", "", "| Élément | Valeur |", "|---|---|"]517    for k, v in R["catalogue"].items(): L.append(f"| {k} | {fmt(v) if not isinstance(v, dict) else v} |")518    for key, titre in (("imm1003_s08", "IMM1003 S08 — Étude de cas : bungalow à Hull (évaluation au 1er novembre 2026)"), ("imm1003_s09", "IMM1003 S09 — Atelier : unifamiliale 2 étages à Hull, cinq comparables (1er novembre 2026)")):519        S = R[key]; L += ["", f"## {titre}", "", f"Sujet : {S['sujet']}", ""]520        for g in S["grilles"]:521            L += [f"### {g['nom']} — prix de vente {fmt(g['prix'])} $ ({g['mois']} mois)", "", "| Étape | Ajustement ($) | Prix courant ($) |", "|---|---:|---:|"]522            for lib, v, cur in g["transactionnels"]: L.append(f"| {lib} | {f'{v:+,}'.replace(',', ' ')} | {fmt(cur)} |")523            L.append(f"| **Prix ajusté transactionnel** | | **{fmt(g['prix_ajuste_transactionnel'])}** |")524            for lib, v in g["propriete"]: L.append(f"| {lib} | {f'{v:+,}'.replace(',', ' ')} | |")525            L += [f"| **Total propriété** | **{f'{g['total_propriete']:+,}'.replace(',', ' ')}** | |", f"| **Indication de valeur** | | **{fmt(g['indication'])}** |",526                  "", f"Net : {f'{g['net']:+,}'.replace(',', ' ')} $ ({g['net_pct']} %) — Brut : {fmt(g['brut'])} $ ({g['brut_pct']} %) — seuils net ≤ 15 % : {fmt(g['seuil_net_ok'])}, brut ≤ 25 % : {fmt(g['seuil_brut_ok'])}", ""]527        rec = S["reconciliation"]528        L += ["### Réconciliation", "", f"Poids : {rec['poids']} → valeur pondérée {fmt(rec['valeur_ponderee'])} $ → **valeur conclue {fmt(rec['valeur_conclue'])} $** (arrondi 5 000 $). Moyenne simple {fmt(rec['moyenne_simple'])} $ (→ {fmt(rec['moyenne_arrondie'])}), médiane {fmt(rec['mediane'])} $, étendue {fmt(rec['min'])}–{fmt(rec['max'])} $ ({rec['etendue_pct']} % de la valeur pondérée).", ""]529        if key == "imm1003_s09":530            L += [f"Sans C4 (poids {S['reconciliation_sans_C4']['poids']}) : {fmt(S['reconciliation_sans_C4']['valeur_ponderee'])} $ → {fmt(S['reconciliation_sans_C4']['valeur_conclue'])} $.", "",531                  "Sensibilité (valeur pondérée) : " + ", ".join(f"{k} → {fmt(v)} $" for k, v in S["sensibilite"].items()), "",532                  f"Régression prix–aire (n = {S['regression']['n']}) : β0 = {fmt(S['regression']['beta0'])}, β1 = {S['regression']['beta1']} $/pi², R² = {S['regression']['r2']}, t = {S['regression']['t_beta1']}, σ résidus = {fmt(S['regression']['ecart_type_residus'])} $.",533                  f"Paires : {S['paires']}", f"Revente : {S['revente']}", f"Équivalence au comptant C1 : {S['equivalence_comptant_C1']}", f"Valeurs aberrantes : {S['valeurs_aberrantes']}", f"Ordre séquentiel vs additif : {S['exemple_ordre']}", ""]534        else:535            L += [f"Exemples des critères : {S['exemples_criteres']}", ""]536    S = R["imm1033_s08"]; ch = S["chaine"]537    L += ["", "## IMM1033 S08 — Six logements, secteur Hull (chaîne du cours = TP2)", "", "| Poste | Montant ($) |", "|---|---:|"]538    for k in ("directs_nus", "cg", "apres_cg", "fg", "apres_fg", "profit_eg", "directs_cours", "contingences", "directs_plus_contingences", "permis_proportionnel", "permis_minimum", "permis", "gcr", "interets_intercalaires", "honoraires", "assurance", "juridique", "indirects", "assiette_profit", "profit_entrepreneurial", "cout_total"):539        L.append(f"| {k} | {fmt(ch[k])} |")540    L += ["", f"Taux du prêt {ch['taux_pret']} % ; durée {ch['duree_mois']} mois ; majoration en cascade {ch['majoration_cascade_pct']} % ; ratio indirects/directs du cours {ch['ratio_indirects_directs_cours_pct']} % ; {ch['cout_pi2_hors_sol']} $/pi² hors sol ; {ch['cout_pi2_brut']} $/pi² brut (avec sous-sol) ; {fmt(ch['cout_par_logement'])} $/logement ; marge sur prix {ch['marge_sur_prix_pct']} % ; GCR applicable : {fmt(ch['gcr_applicable'])} (si applicable : total {fmt(S['chaine_si_gcr_applicable']['cout_total'])} $).", "",541          f"Intérêts : simplifiée ½ = {fmt(S['interets_methode_simplifiee'])} $ ; par période linéaire = {fmt(S['interets_par_periode_lineaire']['interets_total'])} $ ; courbe en S = {fmt(S['interets_par_periode_courbe_S']['interets_total'])} $.", "",542          "Sensibilité taux : " + "; ".join(f"{v['taux']} % → I {fmt(v['interets'])} $, total {fmt(v['cout_total'])} $" for v in S["sensibilite_taux"].values()), "",543          "Sensibilité durée : " + "; ".join(f"{v['duree']} mois → I {fmt(v['interets'])} $, total {fmt(v['cout_total'])} $" for v in S["sensibilite_duree"].values()), "",544          f"Retard de 6 mois : {S['retard_6_mois']}", "", f"Extraction marché : {S['extraction_marche']}", "", f"Taxes : {S['taxes']}", "", f"Honoraires : {S['honoraires_methodes']}", "", f"Profit vs marge : {S['profit_vs_marge']}", "", f"Exemples simples : {S['exemples_simples']}", ""]545    L += ["", "## Calendrier mensuel (courbe en S) — six logements", "", "| Mois | Décaissement | Cumul | Intérêt | Solde fin |", "|---:|---:|---:|---:|---:|"]546    for l in S["interets_par_periode_courbe_S"]["lignes"]: L.append(f"| {l['mois']} | {fmt(l['decaissement'])} | {fmt(l['cumul_decaisse'])} | {fmt(l['interet'])} | {fmt(l['solde_fin'])} |")547    L += ["", "## Vérification des decks originaux", "", "| Document | Élément | Deck | Recalculé | Exact | Note |", "|---|---|---:|---:|:-:|---|"]548    for c in R["verifications_originaux"]: L.append(f"| {c['document']} | {c['element']} | {fmt(c['valeur_deck'])} | {fmt(c['valeur_recalculee'])} | {'✓' if c['exact'] else '✗'} | {c['note']} |")549    return "\n".join(L) + "\n"550551if __name__ == "__main__":552    main(sys.argv[1:])553
Files main
Menu

Theme