SPB Git forge

spb/house-ka

Public
18commits 1branches 0releases
1.9 MBsize
maindefault branch
19 days agolast push
Python 67% TypeScript 18.2% CSS 14.4%
5.0 KB · 124 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# mortgage/cmhc.py : assurance prêt hypothécaire (SCHL / Sagen / Canada5#   Guaranty). Règles et barèmes isolés ici pour être mis à jour facilement.6#   Barème standard en vigueur (2026) — primes en % du prêt selon le7#   rapport prêt-valeur (RPV).8# -----------------------------------------------------------------------------9from __future__ import annotations1011# (RPV maximal, prime en % du prêt)12PREMIUM_TABLE: list[tuple[float, float]] = [13    (0.65, 0.0060),14    (0.75, 0.0170),15    (0.80, 0.0240),16    (0.85, 0.0280),17    (0.90, 0.0310),18    (0.95, 0.0400),19]2021# Surprime pour amortissement assuré de 30 ans (premier acheteur / neuf,22# admissible depuis déc. 2024).23SURCHARGE_30Y = 0.00202425# Prix maximal admissible à l'assurance (porté de 1 M$ à 1,5 M$ en déc. 2024).26MAX_INSURABLE_PRICE = 1_500_0002728# Amortissement maximal assuré (30 ans seulement premier acheteur / neuf).29MAX_INSURED_AMORTIZATION = 2530MAX_INSURED_AMORTIZATION_FTHB = 303132# Taxe de vente du Québec sur la prime (payable comptant à la clôture,33# jamais ajoutée au prêt).34QC_TAX_ON_PREMIUM = 0.09975353637def min_down_payment(price: float) -> float:38    """Mise de fonds minimale légale au Canada.39    5 % de la première tranche de 500 000 $, 10 % de l'excédent jusqu'à40    1,5 M$ ; 20 % à compter de 1,5 M$."""41    if price <= 0:42        return 0.043    if price >= MAX_INSURABLE_PRICE:44        return round(0.20 * price, 2)45    return round(0.05 * min(price, 500_000) + 0.10 * max(0.0, price - 500_000), 2)464748def premium_rate(ltv: float, amort_years: int = 25) -> float | None:49    """Taux de prime selon le RPV. None si RPV > 95 % (non assurable)."""50    if ltv <= 0:51        return 0.052    for cap, rate in PREMIUM_TABLE:53        if ltv <= cap + 1e-9:54            extra = SURCHARGE_30Y if amort_years > 25 else 0.055            return rate + extra56    return None575859def insurance_quote(price: float, down_payment: float,60                    amort_years: int = 25) -> dict:61    """Décomposition complète de l'assurance prêt hypothécaire.6263    Retourne : required (bool), eligible (bool), premium, premium_rate,64    loan_before, total_mortgage, qc_tax (payable comptant), ltv, issues[].65    Ne devine rien : si le scénario est inadmissible, le dit explicitement.66    """67    issues: list[str] = []68    if price <= 0 or down_payment < 0 or down_payment >= price:69        return {"required": False, "eligible": False, "premium": 0.0,70                "premium_rate": 0.0, "loan_before": max(price - down_payment, 0.0),71                "total_mortgage": max(price - down_payment, 0.0),72                "qc_tax": 0.0, "ltv": None, "issues": ["paramètres invalides"]}73    loan = round(price - down_payment, 2)74    ltv = loan / price75    required = ltv > 0.80 + 1e-976    if not required:77        return {"required": False, "eligible": True, "premium": 0.0,78                "premium_rate": 0.0, "loan_before": loan,79                "total_mortgage": loan, "qc_tax": 0.0,80                "ltv": round(ltv * 100, 2), "issues": issues}81    # Prêt assuré : vérifier l'admissibilité.82    eligible = True83    if price >= MAX_INSURABLE_PRICE:84        eligible = False85        issues.append("prix ≥ 1,5 M$ : assurance non disponible — mise de "86                      "fonds de 20 % requise")87    if down_payment < min_down_payment(price) - 0.01:88        eligible = False89        issues.append("mise de fonds sous le minimum légal "90                      f"({min_down_payment(price):,.0f} $)".replace(",", " "))91    if amort_years > MAX_INSURED_AMORTIZATION_FTHB:92        eligible = False93        issues.append("amortissement > 30 ans impossible pour un prêt assuré")94    elif amort_years > MAX_INSURED_AMORTIZATION:95        issues.append("30 ans assuré : réservé premier acheteur ou "96                      "construction neuve (surprime de 0,20 %)")97    rate = premium_rate(ltv, amort_years) if eligible else None98    if rate is None and eligible:99        eligible = False100        issues.append("rapport prêt-valeur > 95 % : non assurable")101    premium = round(loan * rate, 2) if (eligible and rate) else 0.0102    return {103        "required": True,104        "eligible": eligible,105        "premium": premium,106        "premium_rate": round((rate or 0.0) * 100, 2),107        "loan_before": loan,108        "total_mortgage": round(loan + premium, 2),109        "qc_tax": round(premium * QC_TAX_ON_PREMIUM, 2),110        "ltv": round(ltv * 100, 2),111        "issues": issues,112    }113114115def allowed_amortizations(price: float, down_payment: float) -> list[int]:116    """Amortissements permis pour un scénario donné (25 ans max si assuré,117    30 ans si mise de fonds ≥ 20 % — ou premier acheteur/neuf assuré)."""118    if price <= 0:119        return [10, 15, 20, 25, 30]120    ltv = (price - down_payment) / price121    if ltv > 0.80:122        return [10, 15, 20, 25, 30]  # 30 = cas particulier (signalé par issues)123    return [10, 15, 20, 25, 30]124