spb/valoplex Public
ValoPlex — moteur d'évaluation spécialisé pour les plex au Québec, petit frère de Vrai-Prix.
TypeScript 90.3%
Python 7.1%
CSS 2.5%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * Pro forma d'investisseur ValoPlex — approche revenu inversée.4 *5 * Partant de la valeur estimée V du plex, on répond à la question de6 * l'investisseur : « quelle structure de revenus et de dépenses ce prix7 * suppose-t-il, et que donne le financement au taux d'aujourd'hui ? »8 *9 * 1. TGA (taux global d'actualisation) de référence par gabarit ;10 * 2. RNE implicite = V × TGA ;11 * 3. loyer moyen implicite par porte : forme fermée12 * RB = (V·TGA + F) / (1 − vacance − v) où F = dépenses fixes,13 * v = part des dépenses proportionnelles aux revenus ;14 * 4. dépenses détaillées « tout le kit » : taxes municipales et scolaires15 * (dérivées de la valeur au rôle), assurances, entretien, gestion,16 * déneigement/pelouse, réserve de remplacement ;17 * 5. financement : hypothèque canadienne (composition semestrielle),18 * PMT mensuel, cashflow, DSCR, cash-on-cash ;19 * 6. frais d'acquisition : droits de mutation (barème QC / barème bonifié20 * de Montréal), notaire, inspection.21 *22 * Toutes les hypothèses sont paramétrables et affichées — zéro boîte noire.23 */2425export interface ProformaParams {26 tauxHypoPct: number; // taux hypothécaire annuel (5 ans fixe), ex. 4.7527 amortAns: number; // amortissement, ex. 2528 miseDeFondsPct: number; // ex. 2529 vacancePct: number; // provision vacance + mauvaises créances, ex. 330 gestionPct: number; // % des revenus bruts, ex. 4 (0 si autogéré)31 entretienPct: number; // % des revenus bruts, ex. 532 appreciationPct: number; // appréciation annuelle supposée de la valeur, ex. 2.533 tgaPct?: number; // TGA imposé ; sinon référence par gabarit34}3536export const DEFAULT_PARAMS: ProformaParams = {37 tauxHypoPct: 4.75,38 amortAns: 25,39 miseDeFondsPct: 25,40 vacancePct: 3,41 gestionPct: 4,42 entretienPct: 5,43 appreciationPct: 2.5,44};4546/** TGA de référence par gabarit (hypothèse de marché, ajustable). */47export function tgaReference(doors: number): number {48 if (doors <= 3) return 4.75;49 if (doors <= 5) return 5.0;50 if (doors <= 11) return 5.25;51 return 5.5;52}5354/** Taux hypothécaire canadien : composition semestrielle → taux mensuel effectif. */55export function tauxMensuelCanadien(tauxAnnuelPct: number): number {56 const r = tauxAnnuelPct / 100;57 return Math.pow(1 + r / 2, 2 / 12) - 1;58}5960/** Paiement mensuel d'une hypothèque canadienne. */61export function paiementMensuel(principal: number, tauxAnnuelPct: number, amortAns: number): number {62 if (principal <= 0) return 0;63 const i = tauxMensuelCanadien(tauxAnnuelPct);64 const n = amortAns * 12;65 if (i <= 0) return principal / n;66 return (principal * i) / (1 - Math.pow(1 + i, -n));67}6869/** Solde d'une hypothèque canadienne après n mois. */70export function soldeApresMois(71 principal: number, tauxAnnuelPct: number, amortAns: number, mois: number72): number {73 const i = tauxMensuelCanadien(tauxAnnuelPct);74 const pmt = paiementMensuel(principal, tauxAnnuelPct, amortAns);75 if (i <= 0) return Math.max(0, principal - pmt * mois);76 return Math.max(0, principal * Math.pow(1 + i, mois) - pmt * (Math.pow(1 + i, mois) - 1) / i);77}7879/** Droits de mutation (« taxe de Bienvenue ») — barèmes 2026 approximatifs. */80export function droitsMutation(prix: number, montreal: boolean): number {81 // tranches provinciales indexées (approx. 2026)82 const t1 = 61_500;83 const t2 = 307_800;84 let d = Math.min(prix, t1) * 0.005;85 if (prix > t1) d += (Math.min(prix, t2) - t1) * 0.01;86 if (prix > t2) {87 if (!montreal) {88 d += (prix - t2) * 0.015;89 } else {90 // barème bonifié de Montréal (approx. 2026)91 const m1 = 615_700, m2 = 1_231_400, m3 = 2_366_200, m4 = 3_113_000;92 d += (Math.min(prix, m1) - t2) * 0.015;93 if (prix > m1) d += (Math.min(prix, m2) - m1) * 0.02;94 if (prix > m2) d += (Math.min(prix, m3) - m2) * 0.025;95 if (prix > m3) d += (Math.min(prix, m4) - m3) * 0.035;96 if (prix > m4) d += (prix - m4) * 0.04;97 }98 }99 return Math.round(d);100}101102export interface ProformaLine {103 key: string;104 amount: number; // annuel, $ (négatif = dépense)105}106107export interface Proforma {108 // hypothèses effectives109 params: Required<ProformaParams>;110 tgaRefPct: number;111 // revenus112 loyerMoyenMensuel: number; // par porte113 revenusBruts: number; // annuel114 vacance: number; // annuel (négatif)115 revenusEffectifs: number;116 // dépenses détaillées (annuelles, négatives)117 depenses: ProformaLine[];118 totalDepenses: number;119 ratioDepensesPct: number; // dépenses / revenus effectifs120 // exploitation121 rne: number; // revenu net d'exploitation (NOI)122 tgaImplicitePct: number; // RNE / valeur123 mrb: number; // multiplicateur de revenus bruts (prix / RB)124 // financement125 miseDeFonds: number;126 hypotheque: number;127 paiementMensuelHypo: number;128 serviceDetteAnnuel: number;129 cashflowAnnuel: number;130 cashflowMensuelParPorte: number;131 dscr: number; // RNE / service de la dette132 // acquisition « tout le kit »133 droitsMutation: number;134 fraisNotaire: number;135 fraisInspection: number;136 liquiditesRequises: number; // mise de fonds + frais137 cashOnCashPct: number; // cashflow / liquidités requises138 // remboursement de capital et projection 5 ans139 capitalAn1: number; // capital remboursé la 1re année140 interetsAn1: number;141 rendementTotalAn1Pct: number; // (cashflow + capital an 1) / liquidités142 soldeHypo5Ans: number;143 valeur5Ans: number; // valeur projetée à l'appréciation supposée144 equite5Ans: number; // valeur 5 ans − solde145 gainTotal5Ans: number; // cashflows + capital remboursé + plus-value146 multipleLiquidites5Ans: number; // équité gagnée + cashflows / liquidités147 // marges de sécurité148 loyerPointMort: number; // loyer/porte/mois où cashflow = 0149 margeSecuriteLoyerPct: number; // (loyer implicite − point mort) / implicite150 tauxPointMortPct: number | null; // taux hypo où cashflow = 0 (même amort/mise)151 sensibiliteTaux: { tauxPct: number; cashflowAnnuel: number; dscr: number }[];152}153154export function buildProforma(155 valeur: number,156 doors: number,157 valeurRole: number | null,158 municipalite: string | null,159 partial?: Partial<ProformaParams>160): Proforma {161 const p: Required<ProformaParams> = {162 ...DEFAULT_PARAMS,163 tgaPct: partial?.tgaPct ?? tgaReference(doors),164 ...Object.fromEntries(165 Object.entries(partial ?? {}).filter(([, v]) => v !== undefined)166 ),167 } as Required<ProformaParams>;168169 const vr = valeurRole && valeurRole > 0 ? valeurRole : valeur;170 const tga = p.tgaPct / 100;171 const vac = p.vacancePct / 100;172 const vGestion = p.gestionPct / 100;173 const vEntretien = p.entretienPct / 100;174175 // dépenses fixes (indépendantes des revenus)176 const taxesMunicipales = vr * 0.011; // ~1,1 % de la valeur au rôle177 const taxeScolaire = vr * 0.001; // ~0,10 %178 const assurances = 900 + doors * 550;179 const deneigementPelouse = 1_100 + doors * 140;180 const energieCommuns = doors * 260; // éclairage/chauffage des communs181 const reserveRemplacement = doors * 500;182 const F =183 taxesMunicipales + taxeScolaire + assurances +184 deneigementPelouse + energieCommuns + reserveRemplacement;185186 // loyer implicite : RNE = V·TGA = RB(1−vac) − F − (vGestion+vEntretien)·RB187 const denom = 1 - vac - vGestion - vEntretien;188 const revenusBruts = Math.max(0, (valeur * tga + F) / Math.max(0.2, denom));189 const loyerMoyenMensuel = revenusBruts / 12 / doors;190 const vacance = -revenusBruts * vac;191 const revenusEffectifs = revenusBruts + vacance;192 const gestion = -revenusBruts * vGestion;193 const entretien = -revenusBruts * vEntretien;194195 const depenses: ProformaLine[] = [196 { key: "taxesMunicipales", amount: -taxesMunicipales },197 { key: "taxeScolaire", amount: -taxeScolaire },198 { key: "assurances", amount: -assurances },199 { key: "entretien", amount: entretien },200 { key: "gestion", amount: gestion },201 { key: "deneigementPelouse", amount: -deneigementPelouse },202 { key: "energieCommuns", amount: -energieCommuns },203 { key: "reserveRemplacement", amount: -reserveRemplacement },204 ];205 const totalDepenses = depenses.reduce((a, l) => a + l.amount, 0);206 const rne = revenusEffectifs + totalDepenses;207208 const miseDeFonds = valeur * (p.miseDeFondsPct / 100);209 const hypotheque = valeur - miseDeFonds;210 const pmt = paiementMensuel(hypotheque, p.tauxHypoPct, p.amortAns);211 const serviceDette = pmt * 12;212 const cashflow = rne - serviceDette;213214 // capital vs intérêts, année 1215 const solde12 = soldeApresMois(hypotheque, p.tauxHypoPct, p.amortAns, 12);216 const capitalAn1 = hypotheque - solde12;217 const interetsAn1 = serviceDette - capitalAn1;218 // projection 5 ans219 const app = p.appreciationPct / 100;220 const solde60 = soldeApresMois(hypotheque, p.tauxHypoPct, p.amortAns, 60);221 const valeur5 = valeur * Math.pow(1 + app, 5);222 const equite5 = valeur5 - solde60;223 const gainTotal5 = cashflow * 5 + (hypotheque - solde60) + (valeur5 - valeur);224 // point mort de loyer : RB' tel que RNE = service de dette225 const rbPointMort = (serviceDette + F) / Math.max(0.2, denom);226 const loyerPointMort = rbPointMort / 12 / doors;227 // taux de point mort (bissection sur le taux, mêmes mise/amortissement)228 let tauxPM: number | null = null;229 {230 let lo_ = 0.1, hi_ = 25;231 const cf = (tx: number) => rne - 12 * paiementMensuel(hypotheque, tx, p.amortAns);232 if (cf(lo_) > 0 && cf(hi_) < 0) {233 for (let k = 0; k < 60; k++) {234 const mid = (lo_ + hi_) / 2;235 if (cf(mid) > 0) lo_ = mid; else hi_ = mid;236 }237 tauxPM = (lo_ + hi_) / 2;238 }239 }240 const sensibiliteTaux = [-1, 0, 1].map((d) => {241 const tx = p.tauxHypoPct + d;242 const sd = 12 * paiementMensuel(hypotheque, tx, p.amortAns);243 return { tauxPct: tx, cashflowAnnuel: rne - sd, dscr: sd > 0 ? rne / sd : Infinity };244 });245246 const dm = droitsMutation(valeur, (municipalite ?? "").toLowerCase().includes("montréal"));247 const notaire = 1_800;248 const inspection = 750 + doors * 120;249 const liquidites = miseDeFonds + dm + notaire + inspection;250251 return {252 params: p,253 tgaRefPct: tgaReference(doors),254 loyerMoyenMensuel,255 revenusBruts,256 vacance,257 revenusEffectifs,258 depenses,259 totalDepenses,260 ratioDepensesPct: revenusEffectifs > 0 ? (-totalDepenses / revenusEffectifs) * 100 : 0,261 rne,262 tgaImplicitePct: valeur > 0 ? (rne / valeur) * 100 : 0,263 mrb: revenusBruts > 0 ? valeur / revenusBruts : 0,264 miseDeFonds,265 hypotheque,266 paiementMensuelHypo: pmt,267 serviceDetteAnnuel: serviceDette,268 cashflowAnnuel: cashflow,269 cashflowMensuelParPorte: cashflow / 12 / doors,270 dscr: serviceDette > 0 ? rne / serviceDette : Infinity,271 droitsMutation: dm,272 fraisNotaire: notaire,273 fraisInspection: inspection,274 liquiditesRequises: liquidites,275 cashOnCashPct: liquidites > 0 ? (cashflow / liquidites) * 100 : 0,276 capitalAn1,277 interetsAn1,278 rendementTotalAn1Pct: liquidites > 0 ? ((cashflow + capitalAn1) / liquidites) * 100 : 0,279 soldeHypo5Ans: solde60,280 valeur5Ans: valeur5,281 equite5Ans: equite5,282 gainTotal5Ans: gainTotal5,283 multipleLiquidites5Ans: liquidites > 0 ? (liquidites + gainTotal5) / liquidites : 0,284 loyerPointMort,285 margeSecuriteLoyerPct: loyerMoyenMensuel > 0286 ? ((loyerMoyenMensuel - loyerPointMort) / loyerMoyenMensuel) * 100 : 0,287 tauxPointMortPct: tauxPM,288 sensibiliteTaux,289 };290}291