SPB Git

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%
11.4 KB · 291 lines typescript
Raw Blame History
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