// Auteur : Simon-Pierre Boucher — contact@spboucher.ai /** * Pro forma d'investisseur ValoPlex — approche revenu inversée. * * Partant de la valeur estimée V du plex, on répond à la question de * l'investisseur : « quelle structure de revenus et de dépenses ce prix * suppose-t-il, et que donne le financement au taux d'aujourd'hui ? » * * 1. TGA (taux global d'actualisation) de référence par gabarit ; * 2. RNE implicite = V × TGA ; * 3. loyer moyen implicite par porte : forme fermée * RB = (V·TGA + F) / (1 − vacance − v) où F = dépenses fixes, * v = part des dépenses proportionnelles aux revenus ; * 4. dépenses détaillées « tout le kit » : taxes municipales et scolaires * (dérivées de la valeur au rôle), assurances, entretien, gestion, * déneigement/pelouse, réserve de remplacement ; * 5. financement : hypothèque canadienne (composition semestrielle), * PMT mensuel, cashflow, DSCR, cash-on-cash ; * 6. frais d'acquisition : droits de mutation (barème QC / barème bonifié * de Montréal), notaire, inspection. * * Toutes les hypothèses sont paramétrables et affichées — zéro boîte noire. */ export interface ProformaParams { tauxHypoPct: number; // taux hypothécaire annuel (5 ans fixe), ex. 4.75 amortAns: number; // amortissement, ex. 25 miseDeFondsPct: number; // ex. 25 vacancePct: number; // provision vacance + mauvaises créances, ex. 3 gestionPct: number; // % des revenus bruts, ex. 4 (0 si autogéré) entretienPct: number; // % des revenus bruts, ex. 5 appreciationPct: number; // appréciation annuelle supposée de la valeur, ex. 2.5 tgaPct?: number; // TGA imposé ; sinon référence par gabarit } export const DEFAULT_PARAMS: ProformaParams = { tauxHypoPct: 4.75, amortAns: 25, miseDeFondsPct: 25, vacancePct: 3, gestionPct: 4, entretienPct: 5, appreciationPct: 2.5, }; /** TGA de référence par gabarit (hypothèse de marché, ajustable). */ export function tgaReference(doors: number): number { if (doors <= 3) return 4.75; if (doors <= 5) return 5.0; if (doors <= 11) return 5.25; return 5.5; } /** Taux hypothécaire canadien : composition semestrielle → taux mensuel effectif. */ export function tauxMensuelCanadien(tauxAnnuelPct: number): number { const r = tauxAnnuelPct / 100; return Math.pow(1 + r / 2, 2 / 12) - 1; } /** Paiement mensuel d'une hypothèque canadienne. */ export function paiementMensuel(principal: number, tauxAnnuelPct: number, amortAns: number): number { if (principal <= 0) return 0; const i = tauxMensuelCanadien(tauxAnnuelPct); const n = amortAns * 12; if (i <= 0) return principal / n; return (principal * i) / (1 - Math.pow(1 + i, -n)); } /** Solde d'une hypothèque canadienne après n mois. */ export function soldeApresMois( principal: number, tauxAnnuelPct: number, amortAns: number, mois: number ): number { const i = tauxMensuelCanadien(tauxAnnuelPct); const pmt = paiementMensuel(principal, tauxAnnuelPct, amortAns); if (i <= 0) return Math.max(0, principal - pmt * mois); return Math.max(0, principal * Math.pow(1 + i, mois) - pmt * (Math.pow(1 + i, mois) - 1) / i); } /** Droits de mutation (« taxe de Bienvenue ») — barèmes 2026 approximatifs. */ export function droitsMutation(prix: number, montreal: boolean): number { // tranches provinciales indexées (approx. 2026) const t1 = 61_500; const t2 = 307_800; let d = Math.min(prix, t1) * 0.005; if (prix > t1) d += (Math.min(prix, t2) - t1) * 0.01; if (prix > t2) { if (!montreal) { d += (prix - t2) * 0.015; } else { // barème bonifié de Montréal (approx. 2026) const m1 = 615_700, m2 = 1_231_400, m3 = 2_366_200, m4 = 3_113_000; d += (Math.min(prix, m1) - t2) * 0.015; if (prix > m1) d += (Math.min(prix, m2) - m1) * 0.02; if (prix > m2) d += (Math.min(prix, m3) - m2) * 0.025; if (prix > m3) d += (Math.min(prix, m4) - m3) * 0.035; if (prix > m4) d += (prix - m4) * 0.04; } } return Math.round(d); } export interface ProformaLine { key: string; amount: number; // annuel, $ (négatif = dépense) } export interface Proforma { // hypothèses effectives params: Required; tgaRefPct: number; // revenus loyerMoyenMensuel: number; // par porte revenusBruts: number; // annuel vacance: number; // annuel (négatif) revenusEffectifs: number; // dépenses détaillées (annuelles, négatives) depenses: ProformaLine[]; totalDepenses: number; ratioDepensesPct: number; // dépenses / revenus effectifs // exploitation rne: number; // revenu net d'exploitation (NOI) tgaImplicitePct: number; // RNE / valeur mrb: number; // multiplicateur de revenus bruts (prix / RB) // financement miseDeFonds: number; hypotheque: number; paiementMensuelHypo: number; serviceDetteAnnuel: number; cashflowAnnuel: number; cashflowMensuelParPorte: number; dscr: number; // RNE / service de la dette // acquisition « tout le kit » droitsMutation: number; fraisNotaire: number; fraisInspection: number; liquiditesRequises: number; // mise de fonds + frais cashOnCashPct: number; // cashflow / liquidités requises // remboursement de capital et projection 5 ans capitalAn1: number; // capital remboursé la 1re année interetsAn1: number; rendementTotalAn1Pct: number; // (cashflow + capital an 1) / liquidités soldeHypo5Ans: number; valeur5Ans: number; // valeur projetée à l'appréciation supposée equite5Ans: number; // valeur 5 ans − solde gainTotal5Ans: number; // cashflows + capital remboursé + plus-value multipleLiquidites5Ans: number; // équité gagnée + cashflows / liquidités // marges de sécurité loyerPointMort: number; // loyer/porte/mois où cashflow = 0 margeSecuriteLoyerPct: number; // (loyer implicite − point mort) / implicite tauxPointMortPct: number | null; // taux hypo où cashflow = 0 (même amort/mise) sensibiliteTaux: { tauxPct: number; cashflowAnnuel: number; dscr: number }[]; } export function buildProforma( valeur: number, doors: number, valeurRole: number | null, municipalite: string | null, partial?: Partial ): Proforma { const p: Required = { ...DEFAULT_PARAMS, tgaPct: partial?.tgaPct ?? tgaReference(doors), ...Object.fromEntries( Object.entries(partial ?? {}).filter(([, v]) => v !== undefined) ), } as Required; const vr = valeurRole && valeurRole > 0 ? valeurRole : valeur; const tga = p.tgaPct / 100; const vac = p.vacancePct / 100; const vGestion = p.gestionPct / 100; const vEntretien = p.entretienPct / 100; // dépenses fixes (indépendantes des revenus) const taxesMunicipales = vr * 0.011; // ~1,1 % de la valeur au rôle const taxeScolaire = vr * 0.001; // ~0,10 % const assurances = 900 + doors * 550; const deneigementPelouse = 1_100 + doors * 140; const energieCommuns = doors * 260; // éclairage/chauffage des communs const reserveRemplacement = doors * 500; const F = taxesMunicipales + taxeScolaire + assurances + deneigementPelouse + energieCommuns + reserveRemplacement; // loyer implicite : RNE = V·TGA = RB(1−vac) − F − (vGestion+vEntretien)·RB const denom = 1 - vac - vGestion - vEntretien; const revenusBruts = Math.max(0, (valeur * tga + F) / Math.max(0.2, denom)); const loyerMoyenMensuel = revenusBruts / 12 / doors; const vacance = -revenusBruts * vac; const revenusEffectifs = revenusBruts + vacance; const gestion = -revenusBruts * vGestion; const entretien = -revenusBruts * vEntretien; const depenses: ProformaLine[] = [ { key: "taxesMunicipales", amount: -taxesMunicipales }, { key: "taxeScolaire", amount: -taxeScolaire }, { key: "assurances", amount: -assurances }, { key: "entretien", amount: entretien }, { key: "gestion", amount: gestion }, { key: "deneigementPelouse", amount: -deneigementPelouse }, { key: "energieCommuns", amount: -energieCommuns }, { key: "reserveRemplacement", amount: -reserveRemplacement }, ]; const totalDepenses = depenses.reduce((a, l) => a + l.amount, 0); const rne = revenusEffectifs + totalDepenses; const miseDeFonds = valeur * (p.miseDeFondsPct / 100); const hypotheque = valeur - miseDeFonds; const pmt = paiementMensuel(hypotheque, p.tauxHypoPct, p.amortAns); const serviceDette = pmt * 12; const cashflow = rne - serviceDette; // capital vs intérêts, année 1 const solde12 = soldeApresMois(hypotheque, p.tauxHypoPct, p.amortAns, 12); const capitalAn1 = hypotheque - solde12; const interetsAn1 = serviceDette - capitalAn1; // projection 5 ans const app = p.appreciationPct / 100; const solde60 = soldeApresMois(hypotheque, p.tauxHypoPct, p.amortAns, 60); const valeur5 = valeur * Math.pow(1 + app, 5); const equite5 = valeur5 - solde60; const gainTotal5 = cashflow * 5 + (hypotheque - solde60) + (valeur5 - valeur); // point mort de loyer : RB' tel que RNE = service de dette const rbPointMort = (serviceDette + F) / Math.max(0.2, denom); const loyerPointMort = rbPointMort / 12 / doors; // taux de point mort (bissection sur le taux, mêmes mise/amortissement) let tauxPM: number | null = null; { let lo_ = 0.1, hi_ = 25; const cf = (tx: number) => rne - 12 * paiementMensuel(hypotheque, tx, p.amortAns); if (cf(lo_) > 0 && cf(hi_) < 0) { for (let k = 0; k < 60; k++) { const mid = (lo_ + hi_) / 2; if (cf(mid) > 0) lo_ = mid; else hi_ = mid; } tauxPM = (lo_ + hi_) / 2; } } const sensibiliteTaux = [-1, 0, 1].map((d) => { const tx = p.tauxHypoPct + d; const sd = 12 * paiementMensuel(hypotheque, tx, p.amortAns); return { tauxPct: tx, cashflowAnnuel: rne - sd, dscr: sd > 0 ? rne / sd : Infinity }; }); const dm = droitsMutation(valeur, (municipalite ?? "").toLowerCase().includes("montréal")); const notaire = 1_800; const inspection = 750 + doors * 120; const liquidites = miseDeFonds + dm + notaire + inspection; return { params: p, tgaRefPct: tgaReference(doors), loyerMoyenMensuel, revenusBruts, vacance, revenusEffectifs, depenses, totalDepenses, ratioDepensesPct: revenusEffectifs > 0 ? (-totalDepenses / revenusEffectifs) * 100 : 0, rne, tgaImplicitePct: valeur > 0 ? (rne / valeur) * 100 : 0, mrb: revenusBruts > 0 ? valeur / revenusBruts : 0, miseDeFonds, hypotheque, paiementMensuelHypo: pmt, serviceDetteAnnuel: serviceDette, cashflowAnnuel: cashflow, cashflowMensuelParPorte: cashflow / 12 / doors, dscr: serviceDette > 0 ? rne / serviceDette : Infinity, droitsMutation: dm, fraisNotaire: notaire, fraisInspection: inspection, liquiditesRequises: liquidites, cashOnCashPct: liquidites > 0 ? (cashflow / liquidites) * 100 : 0, capitalAn1, interetsAn1, rendementTotalAn1Pct: liquidites > 0 ? ((cashflow + capitalAn1) / liquidites) * 100 : 0, soldeHypo5Ans: solde60, valeur5Ans: valeur5, equite5Ans: equite5, gainTotal5Ans: gainTotal5, multipleLiquidites5Ans: liquidites > 0 ? (liquidites + gainTotal5) / liquidites : 0, loyerPointMort, margeSecuriteLoyerPct: loyerMoyenMensuel > 0 ? ((loyerMoyenMensuel - loyerPointMort) / loyerMoyenMensuel) * 100 : 0, tauxPointMortPct: tauxPM, sensibiliteTaux, }; }