# Moteur hypothécaire Immo-Ka (Mortgage Intelligence Engine) Moteur natif de collecte, d'historisation et de calcul des taux hypothécaires canadiens, intégré au backend FastAPI d'Immo-Ka. **Aucun taux n'est jamais inventé, codé en dur ni estimé** : tout taux affiché provient d'une page officielle d'une institution financière, avec provenance (URL source) et fraîcheur (horodatage de collecte). ## Architecture ``` immoka/mortgage/ ├── providers/ # 1 connecteur indépendant par institution (12) │ ├── base.py # RateProvider : fetch() → produits normalisés │ └── README.md # comment ajouter une banque ├── validate.py # garde-fous anti-aberration (419 % ≠ 4,19 %…) ├── store.py # SQLite annexe data/mortgage.db — historisation ├── scheduler.py # orchestration : retries, backoff, santé, isolation ├── calc.py # mathématiques hypothécaires canadiennes ├── cmhc.py # assurance prêt (SCHL) + taxe de vente QC └── api.py # routes /api/mortgage/* (montées dans web.py) ``` Le flux : `provider.fetch()` → `validate_batch()` → `store.record_observations()`. Les providers ne touchent jamais la base ; le calculateur ne touche jamais les scrapers — seule l'API interne les relie. ## Institutions couvertes (12 connecteurs) | slug | Institution | Type de source | |---|---|---| | `bank_of_canada` | Banque du Canada | Valet API (JSON officiel) — taux de référence | | `bmo` | BMO | JSON embarqué | | `cibc` | CIBC | JSON | | `desjardins` | Desjardins | HTML | | `eq_bank` | Banque EQ | HTML | | `first_national` | First National | HTML | | `mcap` | MCAP | HTML (taux préférentiel) | | `national_bank` | Banque Nationale | HTML | | `rbc` | RBC | JSON | | `scotiabank` | Banque Scotia | JSON (posted + promos) | | `tangerine` | Tangerine | JSON | | `td` | TD Canada Trust | JSON | Chaque produit est normalisé par `RateProvider.make_product()` : `provider, institution, product_name, rate_type (fixed|variable|other), term_months, kind (posted|special), rate, apr, insured_status (insured|insurable|uninsured|unknown), purpose (purchase|renewal|refinance|unknown), amortization_max_years, conditions, source_url, confidence, raw`. Les taux **préférentiels/prime** sont stockés en `rate_type="other"` + `purpose="unknown"` : ils ne peuvent jamais contaminer un classement « meilleur taux d'achat ». ## Validation (validate.py) Rejette avant enregistrement : - taux hors bornes plausibles (0,5 %–24 %) — attrape `4.19 → 419` ; - champs requis manquants, enums invalides, termes hors 3–120 mois ; - APR incohérent (APR < taux − 0,02 pt) ou aberrant ; - doublons exacts dans un même lot (silencieusement dédupliqués). Un lot partiellement invalide n'est pas jeté : les produits sains sont enregistrés, les problèmes journalisés. ## Historisation (store.py — data/mortgage.db, WAL) - `rate_observations` : périodes de validité (`valid_from`/`valid_to`, `is_current`). Taux inchangé → simple mise à jour de `last_checked` ; taux changé → clôture de la période + nouvelle ligne. Un saut > 2,5 pts en < 48 h est rejeté **sans écraser** la donnée existante (garde anti-aberration au niveau BD). - `provider_runs` : journal de chaque collecte (statut, durée, produits, changements, rejets) → santé OK / WARNING (> 24 h) / ERROR. - `product_key` : sha1 tronqué de `provider|rate_type|term|kind|insured|purpose|name` — identité stable d'un produit à travers le temps. En cas de panne d'une source, **les derniers taux valides restent servis**, avec leur âge affiché (mention « stale » au-delà de 24 h). ## Collecte (scheduler.py) - `run_provider(slug)` : retries (défaut 3) avec backoff exponentiel ; une exception d'un provider n'affecte jamais les autres. - `run()` : séquentiel et poli (`request_delay` par provider — jamais de martèlement des sites bancaires). - `watch(min)` : boucle autonome ; `maybe_run()` est appelé depuis la boucle d'ingestion existante (**process PM2 `immo-ka-sync`**) et ne collecte que si la dernière passe date de plus de `IMMOKA_MORTGAGE_INTERVAL_MIN` minutes (défaut 180). CLI : ```bash python run.py mortgage-sync [slug…] # collecte (toutes ou certaines banques) python run.py mortgage-watch [min] # boucle autonome python run.py mortgage-status # santé des providers ``` Variables d'environnement (voir `.env.example`) : `IMMOKA_MORTGAGE_INTERVAL_MIN`, `IMMOKA_MORTGAGE_RETRIES`, `IMMOKA_MORTGAGE_BACKOFF`, `SCRAPFLY_KEY` (anti-bot, dernier recours). ## Calculateur canadien (calc.py + cmhc.py) - **Composition semestrielle** pour les taux fixes (norme légale canadienne) : taux périodique = `(1 + r/2)^(2/f) − 1`. Valeur étalon vérifiée par test : 100 000 $ à 6 % sur 25 ans = **639,81 $/mois** (≠ 644,30 $ en composition mensuelle américaine — testé aussi, pour prouver qu'on n'utilise pas la mauvaise formule). Taux variables : composition mensuelle. - 6 fréquences : mensuelle, bimensuelle, aux 2 semaines, hebdomadaire, accélérée aux 2 semaines (mensualité ÷ 2), accélérée hebdo (÷ 4). - **Test de résistance** fédéral : qualification à `max(taux + 2, 5,25 %)`. - **SCHL** (cmhc.py) : mise de fonds légale minimale (5 % / 10 % / 20 %), primes par tranche RPV (0,60 % → 4,00 %), surprime +0,20 % amortissement 30 ans (premier acheteur), plafond assurable 1,5 M$, **TVQ 9,975 % sur la prime payable comptant** (spécificité québécoise) — la prime s'ajoute au prêt, la taxe non. - Tableau d'amortissement, résumé de terme (solde au renouvellement), scénarios de renouvellement (+0/+1/+2/+3 pts), ratios ABD/ATD informatifs, inverses (prêt max pour un versement, taux requis). ## API interne (`/api/mortgage/*`) | Route | Rôle | |---|---| | `GET /rates` | taux courants filtrables (type, terme, kind, provider…) | | `GET /rates/best` | meilleur taux comparable + classement par institution | | `GET /rates/history` | périodes de validité (historique réel, jamais extrapolé) | | `GET /providers` | santé des sources (OK/WARNING/ERROR, âge, produits) | | `GET /market` | vue marché (meilleur/médiane/variations 7-30 j) | | `GET /intelligence` | market + taux préférentiels (page /taux-hypothecaires) | | `POST /calculate` | calcul complet (SCHL, stress, terme, renouvellement…) | | `POST /affordability` | capacité d'emprunt (ABD/ATD + stress test) | Règle absolue : **jamais de comparaison de produits incomparables** — affiché vs offre spéciale, assuré vs non assuré — sans l'indiquer. Le comparateur ne garde qu'un produit comparable par institution (l'offre spéciale prime). ## Frontend - **Fiche propriété** (`Financement.tsx`, section « Financer cette propriété », ventes seulement) : prix prérempli, mise de fonds $/% synchronisée, versement + taux utilisé avec provenance/fraîcheur, SCHL détaillée, coût réel mensuel (+ taxes municipales/scolaires de la fiche), stress test, renouvellement, comparateur banques, historique SVG, amortissement. - **Page `/taux-hypothecaires`** (`Taux.tsx`) : vue marché cliquable, comparateur par institution (nature + fraîcheur + source officielle), historique, santé des sources. Référencée (seo.py + sitemap). ## Tests ```bash PYTHONPATH=. .venv/bin/python -P -m unittest discover -s tests ``` 61 tests : `test_mortgage_calc.py` (valeurs étalons, fréquences accélérées, inverses, stress), `test_mortgage_cmhc.py` (primes, TVQ, éligibilité), `test_mortgage_validate.py` (anti-aberration), `test_mortgage_store.py` (historisation, garde 2,5 pts, meilleur taux), `test_mortgage_providers.py` (chaque parseur sur fixtures HTML/JSON committées dans `tests/fixtures/mortgage/` — aucun réseau).