Merge pull request 'docs(balance): ADR 0015 + guide + architecture + CHANGELOG (#218)' (#227) from issue-218-docs into main

This commit is contained in:
maximus 2026-06-10 01:09:06 +00:00
commit ae0f8150ff
8 changed files with 212 additions and 21 deletions

View file

@ -5,6 +5,8 @@
### Ajouté ### Ajouté
- Bilan : **saisie de snapshot par titre**. Un compte *détaillé* se saisit désormais titre par titre — chaque position a sa propre ligne avec un sélecteur de titre (autocomplétion sur vos titres existants, avec création inline d'un nouveau symbole), quantité, cours (avec la récupération automatique de prix optionnelle), coût d'acquisition, et un gain latent calculé en direct. La valeur du compte est la somme affichée de ses positions. Les comptes simples sont inchangés. Le sélecteur de titre accepte n'importe quel symbole normalisé (MAJUSCULES/sans espaces) — pas de validation en direct du symbole, puisque la récupération du prix est une étape séparée et au mieux ; vous choisissez la classe d'actif (Action / Crypto) à la création d'un nouveau symbole (#214). - Bilan : **saisie de snapshot par titre**. Un compte *détaillé* se saisit désormais titre par titre — chaque position a sa propre ligne avec un sélecteur de titre (autocomplétion sur vos titres existants, avec création inline d'un nouveau symbole), quantité, cours (avec la récupération automatique de prix optionnelle), coût d'acquisition, et un gain latent calculé en direct. La valeur du compte est la somme affichée de ses positions. Les comptes simples sont inchangés. Le sélecteur de titre accepte n'importe quel symbole normalisé (MAJUSCULES/sans espaces) — pas de validation en direct du symbole, puisque la récupération du prix est une étape séparée et au mieux ; vous choisissez la classe d'actif (Action / Crypto) à la création d'un nouveau symbole (#214).
- Bilan : **détailler un compte agrégé en titres** (assistant). Un compte *simple* peut être basculé en *détaillé* via l'action « Détailler en titres » dans le tableau des comptes. La bascule est à sens unique : elle passe le compte en détaillé et fixe la date du jour comme date de bascule (pivot, `detailed_since`) — l'historique agrégé passé reste figé en lecture seule, et les titres se saisissent au prochain snapshot normal (l'assistant ne capture pas de portefeuille initial). Une fois un titre saisi, le compte ne peut plus revenir en saisie simple (verrouillé à la fois dans l'interface et dans le service) (#215).
- Bilan : **drill-down par titre et gain latent dans le tableau des comptes et l'aperçu**. Un compte détaillé peut être déplié pour afficher ses titres (valeur + gain latent %), et le gain latent (valeur coût d'acquisition) est agrégé par compte, par classe d'actif et par enveloppe fiscale dans le tableau des comptes et la carte d'aperçu du bilan. Les positions sans coût d'acquisition sont signalées « N/A » et exclues du pourcentage (pas de division par zéro) ; le rendement Modified Dietz par compte est inchangé (#216).
- Bilan : la date d'un snapshot existant peut maintenant être déplacée. Le champ date devient modifiable en mode édition — changez-la puis enregistrez, et le snapshot (avec toutes ses lignes) est déplacé à la nouvelle date dans une seule transaction atomique. Si un autre snapshot occupe déjà la date cible, le déplacement est refusé avec un message clair et rien n'est modifié (#200). - Bilan : la date d'un snapshot existant peut maintenant être déplacée. Le champ date devient modifiable en mode édition — changez-la puis enregistrez, et le snapshot (avec toutes ses lignes) est déplacé à la nouvelle date dans une seule transaction atomique. Si un autre snapshot occupe déjà la date cible, le déplacement est refusé avec un message clair et rien n'est modifié (#200).
- Bilan : **enveloppe fiscale sur les comptes**. Un compte peut désormais porter une enveloppe fiscale optionnelle (Non-enregistré, CELI, REER, FERR, CELIAPP, REEE — ou aucune), choisie via un menu déroulant dans le formulaire de compte. C'est un axe distinct du type du compte (la classe d'actif) : un compte d'actions logé dans un CELI est enfin exprimable — type *Actions* + enveloppe *CELI*. La migration v12 ajoute la colonne nullable `balance_accounts.vehicle_type` avec un CHECK sur l'enum et backfille les anciens comptes CELI/REER (#202, #203). - Bilan : **enveloppe fiscale sur les comptes**. Un compte peut désormais porter une enveloppe fiscale optionnelle (Non-enregistré, CELI, REER, FERR, CELIAPP, REEE — ou aucune), choisie via un menu déroulant dans le formulaire de compte. C'est un axe distinct du type du compte (la classe d'actif) : un compte d'actions logé dans un CELI est enfin exprimable — type *Actions* + enveloppe *CELI*. La migration v12 ajoute la colonne nullable `balance_accounts.vehicle_type` avec un CHECK sur l'enum et backfille les anciens comptes CELI/REER (#202, #203).
- Bilan : **axe enveloppe du graphique empilé**. Le graphique d'évolution empilé gagne un sous-choix d'axe — **Par classe d'actif** (défaut, comportement inchangé) ou **Par enveloppe** (regroupe par enveloppe fiscale, avec un bucket « Aucune » pour les comptes sans enveloppe). Lisez votre patrimoine par ce que vous détenez *ou* par l'abri fiscal où il se trouve (#204). - Bilan : **axe enveloppe du graphique empilé**. Le graphique d'évolution empilé gagne un sous-choix d'axe — **Par classe d'actif** (défaut, comportement inchangé) ou **Par enveloppe** (regroupe par enveloppe fiscale, avec un bucket « Aucune » pour les comptes sans enveloppe). Lisez votre patrimoine par ce que vous détenez *ou* par l'abri fiscal où il se trouve (#204).
@ -16,6 +18,7 @@
- Bilan : **un type est désormais une pure classe d'actif**. Les types standard sont les cinq classes d'actif — Liquidités, Fonds / FNB, Actions, Crypto, Autres. Les enveloppes fiscales qui étaient des types (CELI, REER) ne sont plus des types : elles ont migré vers le nouvel attribut d'enveloppe fiscale, porté par le compte. À la migration, les comptes CELI/REER existants sont reclassés dans la classe d'actif **Autres** tout en conservant leur enveloppe (`tfsa`/`rrsp`) ; les types seedés `tfsa`/`rrsp` sont désactivés et disparaissent des menus de types (migrations v12 + v13). Les nouveaux profils seedent cinq classes d'actif ; les comptes de départ CELI/REER passent sous *Autres* avec leur enveloppe renseignée. Vos montants et votre historique ne changent pas — seul le regroupement change (#202). - Bilan : **un type est désormais une pure classe d'actif**. Les types standard sont les cinq classes d'actif — Liquidités, Fonds / FNB, Actions, Crypto, Autres. Les enveloppes fiscales qui étaient des types (CELI, REER) ne sont plus des types : elles ont migré vers le nouvel attribut d'enveloppe fiscale, porté par le compte. À la migration, les comptes CELI/REER existants sont reclassés dans la classe d'actif **Autres** tout en conservant leur enveloppe (`tfsa`/`rrsp`) ; les types seedés `tfsa`/`rrsp` sont désactivés et disparaissent des menus de types (migrations v12 + v13). Les nouveaux profils seedent cinq classes d'actif ; les comptes de départ CELI/REER passent sous *Autres* avec leur enveloppe renseignée. Vos montants et votre historique ne changent pas — seul le regroupement change (#202).
- Bilan : **note sur les bilans historiques**. L'axe « par classe d'actif » du graphique est recalculé sur le type *actuel* de chaque compte ; un snapshot saisi *avant* cette migration pour un ancien compte CELI/REER apparaît donc désormais sous **Autres** sur cet axe (il ne s'affiche plus sous « CELI »/« REER »). C'est attendu — le nouvel axe **par enveloppe** retrouve bien ces CELI/REER, et aucune valeur ni aucun historique n'est modifié (#204). - Bilan : **note sur les bilans historiques**. L'axe « par classe d'actif » du graphique est recalculé sur le type *actuel* de chaque compte ; un snapshot saisi *avant* cette migration pour un ancien compte CELI/REER apparaît donc désormais sous **Autres** sur cet axe (il ne s'affiche plus sous « CELI »/« REER »). C'est attendu — le nouvel axe **par enveloppe** retrouve bien ces CELI/REER, et aucune valeur ni aucun historique n'est modifié (#204).
- Bilan : **renommer un type ne casse plus la traduction** (correction de bug). Renommer un type écrasait auparavant sa clé i18n avec le texte libre, cassant la traduction FR/EN. Le nom personnalisé est désormais stocké à part dans `balance_categories.custom_label`, et la clé de traduction d'origine n'est jamais touchée (et réapparaît si vous videz le nom personnalisé). La migration v12 récupère aussi, en défense, tout type seedé dont la clé de traduction aurait déjà été écrasée par un renommage antérieur (#202, #203). - Bilan : **renommer un type ne casse plus la traduction** (correction de bug). Renommer un type écrasait auparavant sa clé i18n avec le texte libre, cassant la traduction FR/EN. Le nom personnalisé est désormais stocké à part dans `balance_categories.custom_label`, et la clé de traduction d'origine n'est jamais touchée (et réapparaît si vous videz le nom personnalisé). La migration v12 récupère aussi, en défense, tout type seedé dont la clé de traduction aurait déjà été écrasée par un renommage antérieur (#202, #203).
- Bilan : **les comptes cotés existants sont convertis automatiquement en comptes détaillés**. À la migration, chaque compte coté existant (doté d'une classe d'actif et d'un symbole) devient un compte *détaillé* à une seule position — ses lignes historiques quantité/cours sont reflétées en positions par titre, sans perte de valeur ni d'historique, et la ligne agrégée conserve le total. Les comptes cotés sans classe d'actif ou sans symbole restent intacts. La valeur du compte et toutes les agrégations sont inchangées ; vous pouvez désormais ajouter d'autres titres au même compte (migrations v14/v15/v16) (#211).
### Corrigé ### Corrigé

View file

@ -5,6 +5,8 @@
### Added ### Added
- Balance: **per-security snapshot entry**. A *detailed* account is now entered title by title — each holding has its own row with a security picker (autocomplete over your existing securities, with inline creation of a new ticker), quantity, price (with the optional automatic price fetch), cost basis, and a live unrealized-gain figure. The account's value is the displayed sum of its positions. Simple accounts are unchanged. The security picker accepts any normalized symbol (UPPER/TRIM) — there is no live ticker validation, since the price fetch is a separate, best-effort step; you choose the asset class (Stock / Crypto) when creating a new symbol (#214). - Balance: **per-security snapshot entry**. A *detailed* account is now entered title by title — each holding has its own row with a security picker (autocomplete over your existing securities, with inline creation of a new ticker), quantity, price (with the optional automatic price fetch), cost basis, and a live unrealized-gain figure. The account's value is the displayed sum of its positions. Simple accounts are unchanged. The security picker accepts any normalized symbol (UPPER/TRIM) — there is no live ticker validation, since the price fetch is a separate, best-effort step; you choose the asset class (Stock / Crypto) when creating a new symbol (#214).
- Balance: **detail an aggregated account into securities** (wizard). A *simple* account can be switched to *detailed* via a "Detail into securities" action in the accounts table. The switch is one-way: it sets the account to detailed and stamps today as the pivot date (`detailed_since`) — past aggregated history stays frozen and read-only, and the individual securities are entered at your next normal snapshot (the wizard does not capture an initial portfolio). Once any holding is entered, the account can no longer revert to simple (enforced both in the UI and in the service) (#215).
- Balance: **per-security drill-down and unrealized gain in the accounts table and overview**. A detailed account can be expanded to show its securities (value + unrealized gain %), and the unrealized gain (value cost basis) is aggregated by account, by asset class, and by fiscal envelope in the accounts table and the balance overview card. Positions without a cost basis are flagged "N/A" and excluded from the percentage (no division by zero); the per-account Modified Dietz return is unchanged (#216).
- Balance: an existing snapshot's date can now be moved. The date field is editable in edit mode — change it and save, and the snapshot (with all its lines) is moved to the new date inside a single atomic transaction. If another snapshot already occupies the target date, the move is rejected with a clear message and nothing changes (#200). - Balance: an existing snapshot's date can now be moved. The date field is editable in edit mode — change it and save, and the snapshot (with all its lines) is moved to the new date inside a single atomic transaction. If another snapshot already occupies the target date, the move is rejected with a clear message and nothing changes (#200).
- Balance: **fiscal envelope on accounts**. An account can now carry an optional fiscal envelope (Non-registered, TFSA, RRSP, RRIF, FHSA, RESP — or none), set via a dropdown in the account form. This is a separate axis from the account's type (asset class), so an account holding stocks inside a TFSA is finally expressible — type *Stocks* + envelope *TFSA*. Migration v12 adds the nullable `balance_accounts.vehicle_type` column with a CHECK on the enum and backfills the former TFSA/RRSP accounts (#202, #203). - Balance: **fiscal envelope on accounts**. An account can now carry an optional fiscal envelope (Non-registered, TFSA, RRSP, RRIF, FHSA, RESP — or none), set via a dropdown in the account form. This is a separate axis from the account's type (asset class), so an account holding stocks inside a TFSA is finally expressible — type *Stocks* + envelope *TFSA*. Migration v12 adds the nullable `balance_accounts.vehicle_type` column with a CHECK on the enum and backfills the former TFSA/RRSP accounts (#202, #203).
- Balance: **stacked-chart envelope axis**. The stacked evolution chart gains an axis sub-toggle — **By asset class** (default, unchanged behaviour) or **By envelope** (groups by fiscal envelope, with a "None" bucket for accounts without one). Read your net worth by what you hold *or* by where it's sheltered (#204). - Balance: **stacked-chart envelope axis**. The stacked evolution chart gains an axis sub-toggle — **By asset class** (default, unchanged behaviour) or **By envelope** (groups by fiscal envelope, with a "None" bucket for accounts without one). Read your net worth by what you hold *or* by where it's sheltered (#204).
@ -16,6 +18,7 @@
- Balance: **a type is now a pure asset class**. The standard types are the five asset classes — Cash, Funds / ETF, Stocks, Crypto, Other. The fiscal envelopes that used to be types (TFSA, RRSP) are no longer types: they moved to the new per-account fiscal-envelope attribute. On migration, existing TFSA/RRSP accounts are reclassed to the **Other** asset class while carrying their envelope (`tfsa`/`rrsp`); the `tfsa`/`rrsp` seed types are deactivated and disappear from the type dropdowns (migrations v12 + v13). New profiles seed five asset classes; the TFSA/RRSP starter accounts now sit under *Other* with their envelope set. Your amounts and history are unchanged — only the grouping changes (#202). - Balance: **a type is now a pure asset class**. The standard types are the five asset classes — Cash, Funds / ETF, Stocks, Crypto, Other. The fiscal envelopes that used to be types (TFSA, RRSP) are no longer types: they moved to the new per-account fiscal-envelope attribute. On migration, existing TFSA/RRSP accounts are reclassed to the **Other** asset class while carrying their envelope (`tfsa`/`rrsp`); the `tfsa`/`rrsp` seed types are deactivated and disappear from the type dropdowns (migrations v12 + v13). New profiles seed five asset classes; the TFSA/RRSP starter accounts now sit under *Other* with their envelope set. Your amounts and history are unchanged — only the grouping changes (#202).
- Balance: **historical reclass note**. The "by asset class" chart axis is recomputed on each account's *current* type, so a snapshot taken *before* this migration for a former TFSA/RRSP account now appears under **Other** on that axis (it no longer shows under "TFSA"/"RRSP"). This is expected — the new **by envelope** axis still surfaces those CELI/REER, and no value or history is altered (#204). - Balance: **historical reclass note**. The "by asset class" chart axis is recomputed on each account's *current* type, so a snapshot taken *before* this migration for a former TFSA/RRSP account now appears under **Other** on that axis (it no longer shows under "TFSA"/"RRSP"). This is expected — the new **by envelope** axis still surfaces those CELI/REER, and no value or history is altered (#204).
- Balance: **renaming a type no longer breaks the translation** (bug fix). Renaming a type used to overwrite its i18n key with the free text, clobbering the FR/EN translation. The custom name is now stored separately in `balance_categories.custom_label`, and the original translation key is never touched (and reappears if you clear the custom name). Migration v12 also defensively recovers any seed type whose translation key had already been clobbered by an earlier rename (#202, #203). - Balance: **renaming a type no longer breaks the translation** (bug fix). Renaming a type used to overwrite its i18n key with the free text, clobbering the FR/EN translation. The custom name is now stored separately in `balance_categories.custom_label`, and the original translation key is never touched (and reappears if you clear the custom name). Migration v12 also defensively recovers any seed type whose translation key had already been clobbered by an earlier rename (#202, #203).
- Balance: **existing priced accounts auto-converted to detailed**. On migration, every existing priced account (one with an asset class and a symbol) becomes a *detailed* account with a single position — its historical quantity/price lines are mirrored into per-security holdings with no value or history loss, and the aggregated line keeps the total. Priced accounts without an asset class or without a symbol are left untouched. The account's value and all aggregations are unchanged; you can now add further securities to the same account (migrations v14/v15/v16) (#211).
### Fixed ### Fixed

View file

@ -118,8 +118,8 @@ src-tauri/
## Base de données ## Base de données
- **13 tables** SQLite, **15 index** (voir `docs/architecture.md` pour le détail) - **20 tables** SQLite, **24 index** (voir `docs/architecture.md` pour le détail). Le module Bilan en représente 7 tables (`balance_categories`, `balance_accounts`, `balance_snapshots`, `balance_snapshot_lines`, `balance_account_transfers`, puis `balance_securities` + `balance_snapshot_holdings` ajoutées en Étape 2 — détail par titre) et 9 index
- **7 migrations inline** dans `lib.rs` (via `tauri_plugin_sql::Migration`) - **16 migrations inline** dans `lib.rs` (v1→v16, via `tauri_plugin_sql::Migration`). Étape 2 (détail par titre) : v14 (`balance_securities` + `balance_snapshot_holdings` + 2 index), v15 (`balance_accounts.kind` + `detailed_since` + backfill), v16 (conversion des comptes cotés existants en détaillés 1-position). Voir [ADR 0015](docs/adr/0015-balance-detail-par-titre.md)
- **Schéma consolidé** (`consolidated_schema.sql`) pour l'initialisation des nouveaux profils - **Schéma consolidé** (`consolidated_schema.sql`) pour l'initialisation des nouveaux profils
- Les migrations appliquées sont protégées par checksum — ne jamais modifier une migration existante, toujours en créer une nouvelle - Les migrations appliquées sont protégées par checksum — ne jamais modifier une migration existante, toujours en créer une nouvelle

View file

@ -0,0 +1,122 @@
# ADR 0015 — Bilan : détail par titre (holdings par snapshot)
- Status: **Accepted**
- Date: 2026-06-06
- Issues: #210 (schéma/migrations v14/v15 + types), #211 (conversion v16), #212 (service securities + save détaillé), #213 (refonte reducer + dispatch `account.kind`), #214 (UI saisie multi-titres), #215 (assistant « détailler »), #216 (drill-down + gain latent), #217 (tests), #218 (cet ADR + doc)
- Spec: [`spec-decisions-bilan-detail-titres.md`](../../spec-decisions-bilan-detail-titres.md), [`spec-plan-bilan-detail-titres.md`](../../spec-plan-bilan-detail-titres.md)
- Audit source: [docs/audit-bilan-2026-05.md](../audit-bilan-2026-05.md)
- Lève le gating « détail par titre reporté » posé par [ADR 0012](0012-balance-two-level-model.md) (Rejected) et le « Hors scope Étape 2 » de [ADR 0014](0014-balance-vehicule-attribut.md)
## Contexte
L'audit critique de la page Bilan (`docs/audit-bilan-2026-05.md`, revue CPA + UX) a défini une **trajectoire additive en trois temps**. L'Étape 0 (quick wins, #201) et l'Étape 1 (axe véhicule, [ADR 0014](0014-balance-vehicule-attribut.md) Accepted) sont livrées. L'**Étape 2 — détail par titre** — était reportée et explicitement hors scope de l'ADR 0014 (« l'Étape 2 fera l'objet d'un ADR distinct quand le besoin sera confirmé »). Ce besoin est confirmé : l'audit le qualifie d'« unique levier qui débloque le progressif pour les deux personas », et l'Étape 1 laisse une limitation connue — un compte ex-CELI/REER contenant de vraies actions reste un **montant agrégé** sous « Autres », sans détail de portefeuille.
État technique de départ : le modèle **coté** (`priced`) existe déjà, mais à raison d'**un seul titre par compte** — `balance_accounts.symbol` + `balance_snapshot_lines(quantity, unit_price, value, price_source)`, alimenté par le price-fetching maximus-api (ADR 0009/0011/0013). L'Étape 2 ne réinvente pas le `quantité × cours` : elle le **généralise de 1 titre/compte à N titres/compte**, avec un coût d'acquisition (`book_cost`) par position pour distinguer l'apport du gain latent.
### Pourquoi un ADR distinct (et non une extension de 0014)
L'ADR 0014 a posé l'axe véhicule en gardant le **grain « classe d'actif »** ; il a explicitement laissé ouverte la question du grain **« titre »**. Le détail par titre touche un autre plan du modèle (positions sous une ligne de snapshot, gain latent, bascule agrégé→détaillé, immortalité des titres référencés) qui mérite sa propre décision tracée — d'où ce 0015.
## Décision
**Le détail par titre est stocké par snapshot, sous la ligne agrégée existante, qui reste la source de vérité unique.**
### 1. Modèle holdings-par-snapshot (additif)
Trois migrations additives **post-v13**`v14`, `v15`, `v16` — sans aucune modification des migrations `v1``v13` (checksum SHA-384 sqlx). `consolidated_schema.sql` (nouveaux profils) et les seeds sont synchronisés en parallèle.
- **`v14`** — deux tables :
- `balance_securities` : table normalisée et **partagée** des titres. `symbol TEXT NOT NULL UNIQUE COLLATE NOCASE` (forme canonique upper/trim), `currency TEXT NOT NULL DEFAULT 'CAD'` (préparé multi-devise, sans CHECK restrictif), `asset_type TEXT NOT NULL CHECK (asset_type IN ('stock','crypto'))`, `name?`. Un même titre est partagé entre comptes.
- `balance_snapshot_holdings` : le détail par titre d'un compte détaillé, rattaché à **sa ligne de snapshot agrégée** (`snapshot_line_id`, `security_id`, `quantity`, `unit_price`, `value`, `book_cost?`, `price_source?`, `price_fetched_at?`), `UNIQUE (snapshot_line_id, security_id)`. Deux index (`_line`, `_security`).
- **`v15`** — `balance_accounts.kind` (`'simple' | 'detailed'`, défaut `'simple'`, CHECK) + `balance_accounts.detailed_since` (DATE nullable). Backfill : un compte sous catégorie `kind='priced'` devient `'detailed'`. `balance_categories.kind` est **conservé** comme **défaut suggéré** pour les nouveaux comptes (additif, non supprimé).
- **`v16`** — convertit les comptes cotés **existants** (1 titre via `account.symbol`) en comptes `detailed` à **1 position** : création/liaison d'un `balance_securities` depuis le symbole normalisé, miroir de chaque ligne `priced` en holding, puis neutralisation de `quantity`/`unit_price` sur la ligne agrégée — **uniquement** si un holding a effectivement été créé (garde anti-perte de données pour les comptes priced à `asset_type` NULL ou sans symbole, laissés intacts). Idempotente façon v13.
### 2. Invariant central — la ligne agrégée reste la source de vérité
À chaque snapshot, **un compte = une ligne `balance_snapshot_lines`** (`UNIQUE(snapshot_id, account_id)` inchangé) :
- compte `simple` : la ligne porte `value` (saisie libre), `quantity`/`unit_price` NULL, aucun holding ;
- compte `detailed` : la ligne porte la **valeur totale** du compte — `value = SUM(holdings.value)`, chaque holding arrondi au cent puis sommé (**comparaison exacte au cent, pas de tolérance flottante**) — `quantity`/`unit_price` NULL ; le détail par titre vit dans `balance_snapshot_holdings`.
**C'est l'invariant qui rend l'Étape 2 non-breaking.** Tous les agrégateurs existants (`getSnapshotTotalsByDate` / `…ByCategoryAndDate` / `…ByVehicleAndDate`) et le **rendement Modified Dietz par compte** ([ADR 0008](0008-modified-dietz-pour-rendement.md), `compute_account_return`) continuent de lire **exactement** `balance_snapshot_lines.value` — zéro changement de chemin de lecture, zéro rebuild des lignes existantes. Les golden values des agrégations sont figées avant/après (tests de régression, #217). Le détail par titre est une **couche d'enrichissement** sous la valeur agrégée, jamais un remplacement.
### 3. `detailed_since` — pivot faisant autorité
La frontière agrégé/détaillé est portée par `balance_accounts.detailed_since` (DATE), **faisant autorité** plutôt qu'inférée du comptage de holdings : une ligne **à ou après** `detailed_since` **doit** porter des holdings ; **avant**, l'agrégé figé est toléré (historique pré-bascule en lecture seule). Un compte `detailed` peut donc cumuler des snapshots passés agrégés et des snapshots récents détaillés, mais la frontière est **explicite et requêtable**, conforme à la convention d'état du projet (`archived_at`, `is_active`). La validation est une passe séparée `validateDetailedSnapshot(account.kind, line, holdings)` ; le CHECK SQL par ligne reste inchangé ; la ligne agrégée s'écrit via le chemin « simple ».
### 4. Dispatch sur `account.kind` (et non `category_kind`)
Tout le dispatch UI/service bascule de `category_kind` vers **`account.kind`** (`BalanceAccountWithCategory.kind` propagé). `category.kind` (`simple`/`priced`) ne survit que comme **défaut suggéré** de l'`AccountForm` à la création. Un compte `detailed` sous une catégorie `simple` s'affiche donc correctement.
### 5. Gain latent vs Modified Dietz par titre
Le **gain/perte latent** d'une position = `value book_cost`, en valeur et en pourcentage (`/ book_cost`), agrégeable par compte / classe d'actif / enveloppe, avec drill-down par titre dans le tableau des comptes (`computeUnrealizedGain`, garde-fou `book_cost = 0` ou NULL → « N/A »). `book_cost` est **saisi directement par position** (pré-rempli depuis le snapshot précédent) et historisé sur `balance_snapshot_holdings` — il n'est **pas** dérivé de transactions.
Le **rendement Modified Dietz reste au niveau compte** (inchangé). Un Modified Dietz **par titre** est **hors scope** : il exigerait des flux datés par lot et par titre (achats/ventes/RoC) — c'est-à-dire un suivi par lots, que l'app ne tient pas (modèle snapshot, pas journal de transactions). Le gain latent depuis l'acquisition est exact **sans** ces flux et répond directement au besoin (apport vs plus-value latente).
### 6. Assistant « détailler un compte agrégé » (date pivot)
Sur un compte `simple`, une action **« Détailler en titres »** (point d'entrée dans `BalanceAccountsTable`) bascule `kind='detailed'` et fixe `detailed_since = aujourd'hui`. L'historique agrégé passé reste **figé en lecture seule** ; les titres se saisissent au **prochain snapshot normal** (l'assistant ne capture pas de positions initiales). Le retour `detailed → simple` est **interdit dès qu'un holding existe** — bloqué côté UI **et** côté service (`updateBalanceAccount`, erreur typée), pour éviter les holdings orphelins.
### 7. Politique des titres : immortels une fois référencés
`balance_snapshot_holdings.security_id``balance_securities(id)` **`ON DELETE RESTRICT`** (miroir de `balance_account_transfers.transaction_id`, [ADR 0010](0010-fk-restrict-balance-transfers.md)) : un titre référencé par au moins un holding **ne peut pas être supprimé** — l'historique reste reproductible. La suppression de titre est donc **masquée dans l'UI** (immortalité assumée). `balance_snapshot_holdings.snapshot_line_id``balance_snapshot_lines(id)` **`ON DELETE CASCADE`** : supprimer une ligne (ou son snapshot) emporte ses holdings.
### Hors scope (explicitement exclu)
- **Suivi des lots d'achat / transactions** (PBR/ACB calculé, gain en capital *réalisé*, remboursement de capital, perte superficielle 61 j). `book_cost` est **saisi**, pas dérivé. Un Modified Dietz par titre en relève aussi.
- **Conversion multi-devise réelle** (taux datés, devise de référence). Seule la colonne `currency` est préparée ; affichage et agrégation restent **CAD**. L'affichage natif ≠ CAD est dé-scopé (chemin non testable tant qu'aucun titre non-CAD ne peut être créé).
- **Import CSV de cours local** (mode privacy/hors-ligne) : déféré, le price-fetching maximus-api couvre déjà l'acquisition des cours.
- **Agrégation du rendement par titre × véhicule fiscal** (`GROUP BY vehicle_type, security`).
## Alternatives considérées
### A. Modèle transaction-based (valeur dérivée) — **rejeté**
L'alternative établie dans l'industrie (cf. **[Portfolio Performance PR #779](https://github.com/portfolio-performance/portfolio/pull/779)** — « historical quotes from transactions ») : ne pas snapshoter de valeur, mais la **dériver** d'un journal de transactions (achats/ventes datés) × cours historiques. **Rejeté consciemment.**
- L'app est **snapshot-based par design** (saisie périodique manuelle d'un relevé daté), pas un journal de transactions. Tous les chemins Bilan lisent déjà des snapshots.
- Le **rendement Modified Dietz tourne déjà par compte** ([ADR 0008](0008-modified-dietz-pour-rendement.md)) à partir des snapshots + transferts taggés — sans valeurs intermédiaires. Passer au transaction-based forcerait à dériver la valeur à chaque date, soit une **refonte complète** du modèle de lecture, pour un gain (PBR exact, gain réalisé) qui est précisément le **suivi par lots hors scope**.
- Le **`book_cost` total saisi par position** colle à la référence courtier ([Wealthsimple stocke une *book value* totale, pas des lots](https://help.wealthsimple.com/hc/en-ca/articles/4409775037083-What-is-adjusted-cost-base-ACB)) ; le snapshot par titre est l'extension **additive** naturelle (cf. [Portfolio Performance — Securities/Transactions/Quotes séparés](https://help.portfolio-performance.info/en/reference/view/securities/all-securities/)).
### B. Table de positions courantes (hors snapshot) — rejeté
Maintenir une table « portefeuille courant » distincte des snapshots. Rejeté : incohérent avec le modèle snapshot (le portefeuille courant = le dernier snapshot), et la valeur agrégée ne vivrait plus sur la ligne → agrégations à dédoubler. Stocker les holdings **par snapshot** garde les agrégations existantes intactes et la migration additive sans rebuild.
### C. Tagging multi-axes / sous-comptes (`parent_id`) — déjà rejetés en 0012/0014
Ne donnent pas le grain titre et imposent un invariant somme parent = somme enfants. Voir [ADR 0012](0012-balance-two-level-model.md) (alternatives A/B) et [ADR 0014](0014-balance-vehicule-attribut.md).
## Consequences
### Positives
- **Détail de portefeuille enfin exprimable** : N titres (quantité, cours, valeur, `book_cost`) dans un même compte ; un ex-CELI/REER avec de vraies actions n'est plus un montant agrégé opaque — il se détaille via l'assistant.
- **Non-breaking par construction** : la ligne agrégée reste la source de vérité ; agrégations date/classe/enveloppe et Modified Dietz par compte **inchangés** (golden values figées, #217). Migration purement additive, idempotente, atomique (rollback testé), `v1``v13` intactes.
- **Gain latent exact sans suivi par lots** : `value book_cost` répond aux findings D/E de l'audit sans le coût d'un journal de transactions.
- **Frontière agrégé/détaillé explicite** : `detailed_since` faisant autorité, pas d'état implicite ; les comptes cotés existants sont auto-convertis sans perte d'historique.
- **Reproductibilité préservée** : titres immortels une fois référencés (RESTRICT), holdings emportés par CASCADE avec leur ligne/snapshot.
### Négatives / risques actés
- **`book_cost` pré-rempli ≠ ajusté automatiquement** : la copie depuis le snapshot précédent est correcte tant qu'aucun apport/retrait n'a eu lieu sur le titre ; un achat/vente doit être ajusté manuellement, sinon gain latent (`book_cost`) et rendement (Dietz, via les transferts) peuvent diverger silencieusement. Documenté au guide.
- **Pas de gain en capital réalisé / PBR fiscal** : le `book_cost` est une *book value* estimée, pas un ACB fiscalement opposable (pas de RoC, pas de perte superficielle). Hors scope assumé.
- **Titres orphelins non nettoyables** : un titre référencé puis « abandonné » (qty 0) reste indélébile (RESTRICT). Acceptable (mêmes propriétés que `balance_account_transfers`) ; un nettoyage gardé « si aucun holding » pourra être ajouté plus tard.
- **`book_cost` NULL sur l'historique converti** : les lignes converties par v16 n'ont pas de coût rétroactif → gain latent « N/A » avant la première saisie. Attendu.
- **Pas de migration Down** : v14/v15/v16 idempotentes et conditionnelles ; toute correction passe par une `v17+` (jamais d'édition rétroactive).
### Neutre
- La colonne `currency` est prête mais inerte (CAD partout) tant qu'aucun titre non-CAD ne peut être créé — l'UX d'affichage natif s'activera avec le multi-devise.
## Liens
- [Audit Bilan 2026-05](../audit-bilan-2026-05.md) — trajectoire additive en trois temps ; Étape 2 = levier central
- [ADR 0008](0008-modified-dietz-pour-rendement.md) — Modified Dietz **par compte** (préservé tel quel ; pas de Dietz par titre)
- [ADR 0010](0010-fk-restrict-balance-transfers.md) — FK `RESTRICT` (modèle d'immortalité réutilisé pour `security_id`)
- [ADR 0012](0012-balance-two-level-model.md) — Rejected ; levait le gating « détail par titre reporté »
- [ADR 0014](0014-balance-vehicule-attribut.md) — axe véhicule (Étape 1) ; détail par titre y était explicitement hors scope
- Spec : [`spec-decisions-bilan-detail-titres.md`](../../spec-decisions-bilan-detail-titres.md) · [`spec-plan-bilan-detail-titres.md`](../../spec-plan-bilan-detail-titres.md)
- [Portfolio Performance PR #779](https://github.com/portfolio-performance/portfolio/pull/779) — modèle transaction-based **rejeté** au profit du snapshot
- [Wealthsimple — Adjusted cost base](https://help.wealthsimple.com/hc/en-ca/articles/4409775037083-What-is-adjusted-cost-base-ACB) · [AdjustedCostBase.ca](https://www.adjustedcostbase.ca/blog/how-to-calculate-adjusted-cost-base-acb-and-capital-gains/) — `book_cost` total comme modèle légitime
- Issues #210#218 — implémentation Étape 2

View file

@ -73,7 +73,7 @@ simpl-resultat/
## Base de données ## Base de données
### Tables (18) ### Tables (20)
| Table | Description | | Table | Description |
|-------|-------------| |-------|-------------|
@ -90,17 +90,19 @@ simpl-resultat/
| `budget_template_entries` | Catégories et montants dans les modèles | | `budget_template_entries` | Catégories et montants dans les modèles |
| `import_config_templates` | Modèles prédéfinis de config d'import | | `import_config_templates` | Modèles prédéfinis de config d'import |
| `user_preferences` | Préférences applicatives (clé-valeur) | | `user_preferences` | Préférences applicatives (clé-valeur) |
| `balance_categories` | Taxonomie des types d'actifs (cash, TFSA, RRSP, fund, stock, crypto, other) — `kind ∈ {simple, priced}`, 7 seedées (`is_seed = 1`) | | `balance_categories` | Taxonomie des **classes d'actif** (Liquidités, Fonds/FNB, Actions, Crypto, Autres) — `kind ∈ {simple, priced}` (défaut suggéré pour les nouveaux comptes), `custom_label` pour le renommage bilingue-safe (v12). Les ex-types véhicules (TFSA/RRSP) ont migré vers `balance_accounts.vehicle_type` (Étape 1, v12/v13, [ADR 0014](adr/0014-balance-vehicule-attribut.md)) |
| `balance_accounts` | Comptes de bilan (rattachés à une catégorie). `currency` hardcodée à `CAD` au MVP via CHECK. `archived_at` pour soft-delete. **Issue #179** : 4 comptes de départ (Compte chèque, CELI, REER, Compte non-enregistré) seedés pour les nouveaux profils via `consolidated_schema.sql`, et proposés aux profils existants via `StarterAccountsModal` (one-shot, pref `balance_starter_proposed`). Le futur passage à un modèle véhicule × composition est décrit dans [ADR 0012](adr/0012-balance-two-level-model.md) (Proposed) | | `balance_accounts` | Comptes de bilan (rattachés à une catégorie). `currency` hardcodée à `CAD` au MVP via CHECK. `archived_at` pour soft-delete. `vehicle_type` (enveloppe fiscale nullable, v12, [ADR 0014](adr/0014-balance-vehicule-attribut.md)). `kind ∈ {simple, detailed}` + `detailed_since` (pivot faisant autorité, v15, [ADR 0015](adr/0015-balance-detail-par-titre.md)) — porte désormais l'axe simple/détaillé (auparavant dérivé de `category.kind`). **Issue #179** : 4 comptes de départ seedés (`consolidated_schema.sql`) + proposés aux profils existants via `StarterAccountsModal` |
| `balance_snapshots` | Snapshots datés (`snapshot_date` UNIQUE) — éditer = mettre à jour les lignes, pas dupliquer | | `balance_snapshots` | Snapshots datés (`snapshot_date` UNIQUE) — éditer = mettre à jour les lignes, pas dupliquer |
| `balance_snapshot_lines` | Une ligne par `(snapshot, compte)`. Stockage denormalisé : pour `simple` `value` seul, pour `priced` `quantity + unit_price + value`. CHECK kind invariants côté SQL | | `balance_snapshot_lines` | Une ligne par `(snapshot, compte)`**source de vérité agrégée**. `simple` : `value` seul. `priced`/`detailed` : la ligne porte la valeur **totale** (`value = SUM(holdings.value)`, `quantity`/`unit_price` NULL pour un compte détaillé), le détail vit dans `balance_snapshot_holdings`. Les agrégateurs et Modified Dietz lisent uniquement cette `value` ([ADR 0015](adr/0015-balance-detail-par-titre.md)) |
| `balance_account_transfers` | Liaison `transactions ↔ balance_accounts` avec `direction ∈ {in, out}`. Utilisée par le calcul Modified Dietz pour séparer apports et gains | | `balance_account_transfers` | Liaison `transactions ↔ balance_accounts` avec `direction ∈ {in, out}`. Utilisée par le calcul Modified Dietz pour séparer apports et gains |
| `balance_securities` | Table normalisée et **partagée** des titres (v14, [ADR 0015](adr/0015-balance-detail-par-titre.md)) : `symbol` UNIQUE `COLLATE NOCASE` (canonique upper/trim), `currency` (DEFAULT `CAD`, préparé multi-devise), `asset_type ∈ {stock, crypto}`, `name?`. Référencée en `ON DELETE RESTRICT` → un titre référencé est immortel (suppression masquée UI) |
| `balance_snapshot_holdings` | Détail par titre d'un compte détaillé, rattaché à sa **ligne de snapshot agrégée** (v14, [ADR 0015](adr/0015-balance-detail-par-titre.md)) : `snapshot_line_id` (FK CASCADE), `security_id` (FK RESTRICT), `quantity`, `unit_price`, `value` (= qty × prix, arrondi cent), `book_cost?` (gain latent = `value book_cost`), `price_source?`, `price_fetched_at?`. `UNIQUE(snapshot_line_id, security_id)` |
### Index (16) ### Index (24)
Index existants (9) : `transactions` (date, category, supplier, source, file, parent), `categories` (parent, type), `suppliers` (category, normalized_name), `keywords` (category, keyword), `budget_entries` (year, month), `adjustment_entries` (adjustment_id), `imported_files` (source). Index existants (15) : `transactions` (date, category, supplier, source, file, parent), `categories` (parent, type), `suppliers` (category, normalized_name), `keywords` (category, keyword), `budget_entries` (year, month), `adjustment_entries` (adjustment_id), `imported_files` (source).
Index Bilan (7, ajoutés en migration v9) : Index Bilan (9) — 7 ajoutés en v9 :
- `idx_balance_accounts_category` (FK lookup catégorie → comptes) - `idx_balance_accounts_category` (FK lookup catégorie → comptes)
- `idx_balance_accounts_active` partiel `WHERE is_active = 1` (filtre liste active) - `idx_balance_accounts_active` partiel `WHERE is_active = 1` (filtre liste active)
- `idx_balance_snapshot_lines_snapshot` (chargement d'un snapshot) - `idx_balance_snapshot_lines_snapshot` (chargement d'un snapshot)
@ -109,15 +111,25 @@ Index Bilan (7, ajoutés en migration v9) :
- `idx_balance_account_transfers_transaction` (lookup icône d'attribution dans `TransactionTable`) - `idx_balance_account_transfers_transaction` (lookup icône d'attribution dans `TransactionTable`)
- `idx_balance_snapshots_date` (sélecteur de période + agrégation chronologique) - `idx_balance_snapshots_date` (sélecteur de période + agrégation chronologique)
2 ajoutés en v14 (Étape 2 — détail par titre) :
- `idx_balance_snapshot_holdings_line` (chargement des positions d'une ligne de snapshot)
- `idx_balance_snapshot_holdings_security` (FK lookup titre → positions, garde `ON DELETE RESTRICT`)
### Invariants Bilan (CHECK + FK) ### Invariants Bilan (CHECK + FK)
- `balance_categories.kind``('simple','priced')` - `balance_categories.kind``('simple','priced')` (défaut suggéré pour les nouveaux comptes ; l'axe agrégé/détaillé est désormais porté par `balance_accounts.kind`)
- `balance_accounts.currency = 'CAD'` (verrou MVP — v2 lèvera ce CHECK avec table de taux) - `balance_accounts.currency = 'CAD'` (verrou MVP — v2 lèvera ce CHECK avec table de taux)
- `balance_snapshot_lines` : `(quantity, unit_price)` doivent être tous deux NULL (kind simple) OU tous deux NOT NULL (kind priced) - `balance_accounts.vehicle_type``('unregistered','tfsa','rrsp','rrif','fhsa','resp')` ou NULL (enveloppe fiscale, v12, [ADR 0014](adr/0014-balance-vehicule-attribut.md))
- `balance_accounts.kind``('simple','detailed')` (v15, [ADR 0015](adr/0015-balance-detail-par-titre.md)) — un compte `detailed` à/après `detailed_since` doit porter des holdings (validation TS `validateDetailedSnapshot`, pivot faisant autorité)
- `balance_snapshot_lines` : `(quantity, unit_price)` doivent être tous deux NULL (kind simple) OU tous deux NOT NULL (kind priced) ; pour un compte `detailed`, la ligne agrégée porte `value = SUM(holdings.value)` (comparaison exacte au cent), `quantity`/`unit_price` NULL
- `balance_securities.symbol` UNIQUE `COLLATE NOCASE` ; `asset_type``('stock','crypto')`
- `balance_snapshot_holdings` : `UNIQUE (snapshot_line_id, security_id)` (un titre une seule fois par ligne)
- `balance_account_transfers.direction``('in','out')` ; UNIQUE `(transaction_id, account_id)` (une transaction ne peut pas être liée deux fois au même compte) - `balance_account_transfers.direction``('in','out')` ; UNIQUE `(transaction_id, account_id)` (une transaction ne peut pas être liée deux fois au même compte)
- FK `balance_accounts.balance_category_id``balance_categories(id)` `ON DELETE RESTRICT` (empêche suppression de catégorie avec comptes liés) - FK `balance_accounts.balance_category_id``balance_categories(id)` `ON DELETE RESTRICT` (empêche suppression de catégorie avec comptes liés)
- FK `balance_snapshot_lines.snapshot_id``balance_snapshots(id)` `ON DELETE CASCADE` (supprimer un snapshot supprime ses lignes) - FK `balance_snapshot_lines.snapshot_id``balance_snapshots(id)` `ON DELETE CASCADE` (supprimer un snapshot supprime ses lignes)
- FK `balance_snapshot_lines.account_id``balance_accounts(id)` `ON DELETE RESTRICT` (préserve l'historique) - FK `balance_snapshot_lines.account_id``balance_accounts(id)` `ON DELETE RESTRICT` (préserve l'historique)
- FK `balance_snapshot_holdings.snapshot_line_id``balance_snapshot_lines(id)` `ON DELETE CASCADE` (supprimer une ligne/snapshot emporte ses holdings)
- FK `balance_snapshot_holdings.security_id``balance_securities(id)` `ON DELETE RESTRICT` — un titre référencé est immortel (préserve l'historique, miroir de la règle transferts), voir [ADR 0015](adr/0015-balance-detail-par-titre.md)
- FK `balance_account_transfers.account_id``balance_accounts(id)` `ON DELETE CASCADE` - FK `balance_account_transfers.account_id``balance_accounts(id)` `ON DELETE CASCADE`
- FK `balance_account_transfers.transaction_id``transactions(id)` `ON DELETE RESTRICT` — décision structurante pour la reproductibilité Modified Dietz, voir [ADR 0010](adr/0010-fk-restrict-balance-transfers.md) - FK `balance_account_transfers.transaction_id``transactions(id)` `ON DELETE RESTRICT` — décision structurante pour la reproductibilité Modified Dietz, voir [ADR 0010](adr/0010-fk-restrict-balance-transfers.md)
@ -136,6 +148,13 @@ Les migrations sont définies inline dans `src-tauri/src/lib.rs` via `tauri_plug
| 7 | v7 | Ajout sous-catégories d'assurance (niveau 3) | | 7 | v7 | Ajout sous-catégories d'assurance (niveau 3) |
| 8 | v8 | Migration de catégories (cf. release 0.8.x) | | 8 | v8 | Migration de catégories (cf. release 0.8.x) |
| 9 | v9 | Schéma Bilan : 5 tables + 7 index + seed des 7 catégories standard (cash, TFSA, RRSP, fund, stock, crypto, other) | | 9 | v9 | Schéma Bilan : 5 tables + 7 index + seed des 7 catégories standard (cash, TFSA, RRSP, fund, stock, crypto, other) |
| 10 | v10 | Ajout `asset_type` sur `balance_categories` (stock/crypto) + backfill des 2 catégories cotées |
| 11 | v11 | Nettoyage des snapshots Bilan orphelins |
| 12 | v12 | Étape 1 : `balance_accounts.vehicle_type` (+ CHECK, backfill ex-CELI/REER) + `balance_categories.custom_label` (+ backfill défensif du bug i18n) — [ADR 0014](adr/0014-balance-vehicule-attribut.md) |
| 13 | v13 | Étape 1 : reclasse les comptes ex-tfsa/rrsp vers « Autres », désactive les seeds enveloppes (idempotente) — [ADR 0014](adr/0014-balance-vehicule-attribut.md) |
| 14 | v14 | Étape 2 : `balance_securities` + `balance_snapshot_holdings` + 2 index (additive) — [ADR 0015](adr/0015-balance-detail-par-titre.md) |
| 15 | v15 | Étape 2 : `balance_accounts.kind` (`simple`/`detailed`) + `detailed_since` + backfill depuis `category.kind` (`priced` → `detailed`) — [ADR 0015](adr/0015-balance-detail-par-titre.md) |
| 16 | v16 | Étape 2 : conversion des comptes cotés existants en détaillés 1-position (security + holding miroir, gardée anti-perte, idempotente) — [ADR 0015](adr/0015-balance-detail-par-titre.md) |
Pour les **nouveaux profils**, le fichier `consolidated_schema.sql` contient le schéma complet avec toutes les migrations pré-appliquées (pas besoin de rejouer les migrations). Pour les **nouveaux profils**, le fichier `consolidated_schema.sql` contient le schéma complet avec toutes les migrations pré-appliquées (pas besoin de rejouer les migrations).
@ -166,8 +185,8 @@ Pour les **nouveaux profils**, le fichier `consolidated_schema.sql` contient le
Un seul service par convention projet (1 service par domaine, splitter seulement > ~400 lignes). Quatre sections logiques distinctes : Un seul service par convention projet (1 service par domaine, splitter seulement > ~400 lignes). Quatre sections logiques distinctes :
1. **CRUD catégories + comptes** — `listBalanceCategories`, `createBalanceCategory`, `updateBalanceCategory`, `archiveBalanceCategory` (refus si comptes liés via FK RESTRICT, refus si `is_seed = 1`), `listBalanceAccounts`, `createBalanceAccount`, `updateBalanceAccount`, `archiveBalanceAccount`. Le service garde une `BalanceServiceError` typée (`BalanceErrorCode`) pour permettre à la UI d'afficher des messages i18n distincts (`currency_unsupported`, `category_seed_protected`, `category_has_accounts`, etc.). 1. **CRUD catégories + comptes + titres** — `listBalanceCategories`, `createBalanceCategory`, `updateBalanceCategory`, `archiveBalanceCategory` (refus si comptes liés via FK RESTRICT, refus si `is_seed = 1`), `listBalanceAccounts`, `createBalanceAccount`, `updateBalanceAccount` (garde `detailed → simple` refusée si des holdings existent, erreur typée), `archiveBalanceAccount`. **Securities (Étape 2)** : `listSecurities`, `getSecurity`, `findOrCreateSecurity` (UPSERT sur symbol normalisé upper/trim, `asset_type` requis), `updateSecurity`. Le service garde une `BalanceServiceError` typée (`BalanceErrorCode`) pour des messages i18n distincts (`currency_unsupported`, `category_seed_protected`, `category_has_accounts`, `account_kind_detailed_has_holdings`, etc.).
2. **Snapshots + lines** — `listBalanceSnapshots`, `getBalanceSnapshotByDate`, `upsertSnapshot` (création + édition par date), `upsertSnapshotLines` (rewrite-all : DELETE WHERE snapshot_id puis INSERT par ligne — choix simple pour < 20 comptes/snapshot), `deleteSnapshot`, helper `validateLineKindInvariants` exporté pour les tests (kind invariants TS en complément du CHECK SQL ; tolérance `PRICED_VALUE_TOLERANCE = 0.01` pour le match `value ≈ quantity × unit_price`). 2. **Snapshots + lines + holdings** — `listBalanceSnapshots`, `getBalanceSnapshotByDate`, `upsertSnapshot` (création + édition par date), `upsertSnapshotLines` (rewrite-all : DELETE WHERE snapshot_id puis INSERT par ligne). **Save détaillé (Étape 2)** : pour un compte `detailed`, la ligne agrégée (value = somme des holdings) **et** ses holdings sont écrits dans la **même transaction** (`BEGIN/COMMIT`), `value` recalculée = `SUM(holdings.value)` (chaque holding arrondi au cent, comparaison exacte). `validateLineKindInvariants` (simple, inchangé, tolérance `PRICED_VALUE_TOLERANCE = 0.01`) + nouvelle passe `validateDetailedSnapshot(account.kind, line, holdings)` (detailed + holdings ⇒ ligne agrégée ET `value = SUM` ; detailed pré-pivot ⇒ agrégé toléré). `getHoldingsForLatestSnapshot` (pré-remplissage : titres + qty + book_cost reportés, qty-0 exclus), `listHoldingsBySnapshotLine` (drill-down), `computeUnrealizedGain` (gain latent `value book_cost` en valeur + %, garde-fou `book_cost = 0`/NULL → « N/A », agrégeable par classe/enveloppe). `deleteSnapshot`.
3. **Returns + transfers**`linkTransfer`, `unlinkTransfer`, `listAccountTransfers`, `listAllLinkedTransfersForTooltip` (un coup pour la `Map.has(txId)` consommée par l'icône d'attribution dans `TransactionTable`), `computeAccountReturn` (wrapper sur la commande Tauri `compute_account_return` qui lit `db_filename` du profil actif via `loadProfiles()`). 3. **Returns + transfers**`linkTransfer`, `unlinkTransfer`, `listAccountTransfers`, `listAllLinkedTransfersForTooltip` (un coup pour la `Map.has(txId)` consommée par l'icône d'attribution dans `TransactionTable`), `computeAccountReturn` (wrapper sur la commande Tauri `compute_account_return` qui lit `db_filename` du profil actif via `loadProfiles()`).
4. **Prices***(Phase 5, livraison reportée à l'Issue #143)*. La forme prévue : `fetchPrice(symbol, date)` invoquant `fetch_price` (Tauri), avec rate-limit client (1/2s), backoff exponentiel et dedup in-flight. Voir [ADR 0009](adr/0009-proxy-price-fetching-via-maximus-api.md) pour l'architecture proxy. 4. **Prices***(Phase 5, livraison reportée à l'Issue #143)*. La forme prévue : `fetchPrice(symbol, date)` invoquant `fetch_price` (Tauri), avec rate-limit client (1/2s), backoff exponentiel et dedup in-flight. Voir [ADR 0009](adr/0009-proxy-price-fetching-via-maximus-api.md) pour l'architecture proxy.
@ -406,4 +425,7 @@ Les ADRs documentent les décisions techniques structurantes. Ils vivent dans `d
| [0009](adr/0009-proxy-price-fetching-via-maximus-api.md) | Proxy price-fetching via maximus-api | 2025-01-01 | Accepted | | [0009](adr/0009-proxy-price-fetching-via-maximus-api.md) | Proxy price-fetching via maximus-api | 2025-01-01 | Accepted |
| [0010](adr/0010-fk-restrict-balance-transfers.md) | FK RESTRICT sur balance_account_transfers | 2025-01-01 | Accepted | | [0010](adr/0010-fk-restrict-balance-transfers.md) | FK RESTRICT sur balance_account_transfers | 2025-01-01 | Accepted |
| [0011](adr/0011-providers-best-effort-yahoo.md) | Providers best-effort Yahoo | 2026-04-26 | Accepted | | [0011](adr/0011-providers-best-effort-yahoo.md) | Providers best-effort Yahoo | 2026-04-26 | Accepted |
| [0012](adr/0012-balance-two-level-model.md) | Modèle à deux niveaux pour le Bilan (véhicules × compositions) | 2026-05-01 | Proposed | | [0012](adr/0012-balance-two-level-model.md) | Modèle à deux niveaux pour le Bilan (véhicules × compositions) | 2026-05-01 | Rejected |
| [0013](adr/0013-stocks-provider-evaluation.md) | Évaluation provider stocks : Alpha Vantage retenu comme cible | 2026-05-09 | Accepted |
| [0014](adr/0014-balance-vehicule-attribut.md) | Bilan : le véhicule fiscal est un attribut du compte (Étape 1) | 2026-06-01 | Accepted |
| [0015](adr/0015-balance-detail-par-titre.md) | Bilan : détail par titre (holdings par snapshot, Étape 2) | 2026-06-06 | Accepted |

View file

@ -365,6 +365,10 @@ Deux notions distinctes structurent le Bilan :
Ce sont deux axes indépendants : un même CELI peut contenir des actions, des fonds et des liquidités. La classe d'actif est portée par le type du compte ; l'enveloppe fiscale est un attribut optionnel du compte. Vous pouvez ainsi lire votre patrimoine *par classe d'actif* OU *par enveloppe fiscale* (voir le graphique d'évolution). Ce sont deux axes indépendants : un même CELI peut contenir des actions, des fonds et des liquidités. La classe d'actif est portée par le type du compte ; l'enveloppe fiscale est un attribut optionnel du compte. Vous pouvez ainsi lire votre patrimoine *par classe d'actif* OU *par enveloppe fiscale* (voir le graphique d'évolution).
Un compte peut être saisi de deux façons :
- **en montant unique** (compte *simple*) — vous entrez directement la valeur du compte à chaque snapshot ;
- **par titre** (compte *détaillé*) — vous entrez chaque valeur mobilière du compte (quantité × cours, avec coût d'acquisition) ; la valeur du compte est la **somme** de ses positions. Voir [Détail par titre](#détail-par-titre) plus bas.
Trois pages composent le module Bilan : Trois pages composent le module Bilan :
- `/balance` — vue d'ensemble (graphique + tableau des comptes) - `/balance` — vue d'ensemble (graphique + tableau des comptes)
- `/balance/snapshot` — saisie / édition d'un snapshot daté - `/balance/snapshot` — saisie / édition d'un snapshot daté
@ -380,7 +384,10 @@ L'entrée **Bilan** dans la barre latérale (icône portefeuille) donne accès
- Renommage d'un type sans casser le bilingue : le nom personnalisé est stocké à part, la traduction FR/EN d'origine reste intacte (et réapparaît si vous videz le nom personnalisé) - Renommage d'un type sans casser le bilingue : le nom personnalisé est stocké à part, la traduction FR/EN d'origine reste intacte (et réapparaît si vous videz le nom personnalisé)
- Snapshots datés avec contrainte UNIQUE par date — éditer = revenir sur la même date, jamais dupliquer ; la date d'un snapshot existant peut être déplacée (ses lignes sont conservées), tant qu'aucun autre snapshot n'occupe déjà la date cible - Snapshots datés avec contrainte UNIQUE par date — éditer = revenir sur la même date, jamais dupliquer ; la date d'un snapshot existant peut être déplacée (ses lignes sont conservées), tant qu'aucun autre snapshot n'occupe déjà la date cible
- Saisie groupée par type ; pour les types `priced`, le `value` est calculé automatiquement (`quantity × unit_price`) - Saisie groupée par type ; pour les types `priced`, le `value` est calculé automatiquement (`quantity × unit_price`)
- Bouton **Pré-remplir depuis le snapshot précédent** : copie les valeurs simples + les quantités priced (vous remplissez juste les nouveaux prix) - **Comptes détaillés (par titre)** : un compte peut contenir plusieurs valeurs mobilières — chaque titre a sa ligne (symbole, quantité, cours, valeur, coût d'acquisition, gain latent). La valeur du compte est la somme de ses titres. Le sélecteur de titre auto-complète sur vos titres existants et permet d'en créer un (symbole + classe d'actif Action/Crypto)
- **Gain latent** par titre et agrégé (par compte, par classe d'actif, par enveloppe) : `valeur coût d'acquisition`, en dollars et en %. Drill-down par titre dans le tableau des comptes. Une position sans coût d'acquisition saisi affiche « N/A » et est exclue du calcul du %
- **Assistant « Détailler en titres »** : bascule un compte simple en compte détaillé à partir d'une date de bascule (pivot) ; l'historique agrégé passé reste figé en lecture seule
- Bouton **Pré-remplir depuis le snapshot précédent** : copie les valeurs simples + les quantités priced + les titres, quantités et coûts d'acquisition des comptes détaillés (vous remplissez juste les nouveaux cours ; un titre à quantité 0 est ignoré)
- Liaison de transactions existantes à un compte de bilan (modal avec filtres par période / catégorie / recherche, sens auto-proposé selon le signe) - Liaison de transactions existantes à un compte de bilan (modal avec filtres par période / catégorie / recherche, sens auto-proposé selon le signe)
- Icône d'attribution dans la page Transactions pour les transactions liées à un transfert - Icône d'attribution dans la page Transactions pour les transactions liées à un transfert
- Graphique d'évolution du bilan : mode courbe simple, ou aire empilée avec un sous-choix d'axe — **Par classe d'actif** (défaut) ou **Par enveloppe** (fiscale). Marqueurs verticaux pour les transferts taggés (vert = in, rouge = out) - Graphique d'évolution du bilan : mode courbe simple, ou aire empilée avec un sous-choix d'axe — **Par classe d'actif** (défaut) ou **Par enveloppe** (fiscale). Marqueurs verticaux pour les transferts taggés (vert = in, rouge = out)
@ -399,8 +406,9 @@ L'entrée **Bilan** dans la barre latérale (icône portefeuille) donne accès
5. Enregistrez. Le graphique sur `/balance` s'actualise immédiatement 5. Enregistrez. Le graphique sur `/balance` s'actualise immédiatement
6. Pour calculer le rendement réel d'un compte d'investissement, ouvrez le menu actions du compte → **Lier transferts** → cochez les transactions qui correspondent à des apports / retraits (un dépôt CELI, un achat d'actions, etc.). Le sens (in/out) est proposé automatiquement selon le signe de la transaction 6. Pour calculer le rendement réel d'un compte d'investissement, ouvrez le menu actions du compte → **Lier transferts** → cochez les transactions qui correspondent à des apports / retraits (un dépôt CELI, un achat d'actions, etc.). Le sens (in/out) est proposé automatiquement selon le signe de la transaction
7. Le tableau des comptes affiche maintenant les rendements Modified Dietz sur 3M / 1A / depuis création. Le rendement non-ajusté à droite vous permet de comparer "valeur du compte" et "vraie performance" 7. Le tableau des comptes affiche maintenant les rendements Modified Dietz sur 3M / 1A / depuis création. Le rendement non-ajusté à droite vous permet de comparer "valeur du compte" et "vraie performance"
8. Pour éditer un snapshot existant, cliquez sur son point dans le graphique ou utilisez le sélecteur de date — la page s'ouvre en mode édition. Vous pouvez aussi y corriger la date : changez-la puis enregistrez, le snapshot est déplacé avec ses lignes (un message s'affiche si la date cible est déjà prise par un autre snapshot) 8. Pour suivre un compte **titre par titre**, soit créez-le détaillé, soit ouvrez son menu d'actions → **Détailler en titres** (assistant) ; aux snapshots suivants, ajoutez chaque valeur mobilière avec sa quantité, son cours et son coût d'acquisition. Voir [Détail par titre](#détail-par-titre)
9. Pour supprimer un snapshot, cliquez **Supprimer** dans son éditeur et re-saisissez la date pour confirmer 9. Pour éditer un snapshot existant, cliquez sur son point dans le graphique ou utilisez le sélecteur de date — la page s'ouvre en mode édition. Vous pouvez aussi y corriger la date : changez-la puis enregistrez, le snapshot est déplacé avec ses lignes (un message s'affiche si la date cible est déjà prise par un autre snapshot)
10. Pour supprimer un snapshot, cliquez **Supprimer** dans son éditeur et re-saisissez la date pour confirmer
### Lecture des rendements multi-horizons ### Lecture des rendements multi-horizons
@ -414,6 +422,28 @@ Avertissements affichés :
- *Aucun transfert lié* — le rendement est calculé sans apports identifiés (équivaut au non-ajusté) - *Aucun transfert lié* — le rendement est calculé sans apports identifiés (équivaut au non-ajusté)
- *Performance non significative* — le compte a été vidé puis rechargé, le calcul Modified Dietz produit un résultat instable - *Performance non significative* — le compte a été vidé puis rechargé, le calcul Modified Dietz produit un résultat instable
### Détail par titre
Un compte d'investissement peut être suivi **titre par titre** plutôt qu'en montant unique. Chaque valeur mobilière (action, FNB, crypto) devient une ligne avec sa quantité, son cours, sa valeur (`quantité × cours`) et son **coût d'acquisition** (le prix payé). La valeur du compte est la **somme** de ses titres.
**Saisir un compte détaillé, titre par titre.** Dans l'éditeur de snapshot, un compte détaillé se déplie en sous-lignes :
1. Cliquez **Ajouter un titre**, puis tapez un symbole (ex. `AAPL`, `BTC`) dans le sélecteur. S'il existe déjà, choisissez-le ; sinon, créez-le en choisissant sa classe d'actif (Action ou Crypto)
2. Saisissez la **quantité** et le **cours** (ou utilisez le bouton de récupération automatique des prix si vous y avez droit) — la valeur de la ligne se calcule
3. Saisissez le **coût d'acquisition** de la position (le total payé) — il alimente le gain latent ; laissez-le vide si vous ne le suivez pas (le gain s'affichera « N/A »)
4. Répétez pour chaque titre. La valeur du compte affichée en bas = la somme des positions
5. Enregistrez. Au prochain snapshot, **Pré-remplir** rapporte vos titres, quantités et coûts d'acquisition — vous n'avez qu'à rafraîchir les cours
**Détailler un compte agrégé existant (l'assistant).** Si un compte était suivi en montant unique et que vous voulez désormais le suivre par titre, ouvrez son menu d'actions dans le tableau des comptes → **Détailler en titres**. L'assistant :
- passe le compte en **détaillé** et fixe la **date de bascule (pivot)** à aujourd'hui ;
- **fige l'historique agrégé passé** en lecture seule : vos anciens snapshots gardent leur montant unique, rien n'est réécrit ni recalculé ;
- ne vous demande **pas** de saisir les titres tout de suite — vous les saisirez à votre **prochain snapshot** normal.
⚠️ C'est une action **à sens unique** : une fois qu'au moins un titre est saisi, le compte ne peut plus revenir en saisie agrégée (sinon le détail serait perdu).
**Lire le gain latent.** Dans le tableau des comptes, un compte détaillé peut être déplié pour voir chaque titre avec sa valeur et son gain latent (`valeur coût d'acquisition`, en $ et en %). Le gain latent est aussi agrégé **par classe d'actif** et **par enveloppe**. Le gain latent répond à « combien ai-je gagné depuis l'achat ? » — c'est distinct du **rendement Modified Dietz** (3M / 1A / depuis création), qui mesure la performance du compte en tenant compte du *timing* de vos apports. Les deux coexistent : le rendement reste au niveau du compte, inchangé.
> Le coût d'acquisition est **saisi**, pas déduit de vos transactions. Pré-rempli d'un snapshot à l'autre, il est correct tant qu'aucun achat ni vente n'a eu lieu sur le titre — après un achat/vente, ajustez-le manuellement pour que le gain latent reste juste.
### Que faire si je supprime une transaction liée ? ### Que faire si je supprime une transaction liée ?
C'est intentionnellement bloqué : si vous tentez de supprimer une transaction qui est liée à un compte de bilan, vous voyez le message **"Cette transaction est liée au compte de bilan _<nom>_"** avec un lien direct vers le compte. Ouvrez le compte → Lier transferts → décochez la transaction → revenez la supprimer. Cette friction préserve la reproductibilité de vos rendements passés (un rendement calculé hier ne peut pas changer aujourd'hui à cause d'une suppression silencieuse). C'est intentionnellement bloqué : si vous tentez de supprimer une transaction qui est liée à un compte de bilan, vous voyez le message **"Cette transaction est liée au compte de bilan _<nom>_"** avec un lien direct vers le compte. Ouvrez le compte → Lier transferts → décochez la transaction → revenez la supprimer. Cette friction préserve la reproductibilité de vos rendements passés (un rendement calculé hier ne peut pas changer aujourd'hui à cause d'une suppression silencieuse).
@ -426,6 +456,7 @@ C'est intentionnellement bloqué : si vous tentez de supprimer une transaction q
- **Note pour les bilans historiques.** Depuis la version qui sépare classe d'actif et enveloppe fiscale, les anciens types « CELI » et « REER » sont devenus des **enveloppes**, et les comptes concernés ont été reclassés en classe d'actif **« Autres »** (en conservant leur enveloppe). L'axe « par classe d'actif » étant recalculé sur la classe **actuelle** du compte, un snapshot saisi *avant* cette migration apparaît désormais sous « Autres » sur cet axe (et non plus sous « CELI »/« REER »). C'est attendu : l'axe **« par enveloppe »**, lui, retrouve bien vos CELI / REER. Vos montants et votre historique ne changent pas — seul le regroupement d'affichage évolue - **Note pour les bilans historiques.** Depuis la version qui sépare classe d'actif et enveloppe fiscale, les anciens types « CELI » et « REER » sont devenus des **enveloppes**, et les comptes concernés ont été reclassés en classe d'actif **« Autres »** (en conservant leur enveloppe). L'axe « par classe d'actif » étant recalculé sur la classe **actuelle** du compte, un snapshot saisi *avant* cette migration apparaît désormais sous « Autres » sur cet axe (et non plus sous « CELI »/« REER »). C'est attendu : l'axe **« par enveloppe »**, lui, retrouve bien vos CELI / REER. Vos montants et votre historique ne changent pas — seul le regroupement d'affichage évolue
- Les marqueurs verticaux du graphique (transferts taggés) aident à lire les sauts de valeur — un saut suivi d'un marqueur vert n'est pas une "performance", c'est juste un dépôt - Les marqueurs verticaux du graphique (transferts taggés) aident à lire les sauts de valeur — un saut suivi d'un marqueur vert n'est pas une "performance", c'est juste un dépôt
- L'avertissement "bilan pas à jour" apparaît si votre dernier snapshot remonte à plus de 60 jours — c'est le signe qu'il est temps d'en saisir un nouveau - L'avertissement "bilan pas à jour" apparaît si votre dernier snapshot remonte à plus de 60 jours — c'est le signe qu'il est temps d'en saisir un nouveau
- **Gain latent ≠ rendement.** Le gain latent (`valeur coût d'acquisition`) répond à « combien vaut ma plus-value depuis l'achat ? » ; le rendement Modified Dietz répond à « quelle performance, en tenant compte de *quand* j'ai investi ? ». Les deux s'affichent côte à côte sur un compte détaillé. Pensez à ajuster le coût d'acquisition après un achat ou une vente, sinon le gain latent dérive.
- (À venir Phase 5) **Récupération automatique des prix** pour les comptes Actions / Crypto via un proxy privé (premium-only). Le service interroge un serveur Maximus dédié qui anonymise votre requête (votre IP n'est jamais exposée à Yahoo / CoinGecko). La saisie manuelle reste toujours disponible. - (À venir Phase 5) **Récupération automatique des prix** pour les comptes Actions / Crypto via un proxy privé (premium-only). Le service interroge un serveur Maximus dédié qui anonymise votre requête (votre IP n'est jamais exposée à Yahoo / CoinGecko). La saisie manuelle reste toujours disponible.
--- ---

View file

@ -945,14 +945,17 @@
}, },
"balance": { "balance": {
"title": "Balance Sheet", "title": "Balance Sheet",
"overview": "The Balance Sheet is a net-worth view: you periodically enter a dated snapshot of all your accounts, track their evolution over time, and compute the true return of each investment account by linking transfers (deposits/withdrawals) to the matching accounts. Two independent axes: the asset class (the account's type: Cash, Funds / ETF, Stocks, Crypto, Other) and the fiscal envelope (an optional account attribute: TFSA, RRSP, RRIF, FHSA, RESP, or none).", "overview": "The Balance Sheet is a net-worth view: you periodically enter a dated snapshot of all your accounts, track their evolution over time, and compute the true return of each investment account by linking transfers (deposits/withdrawals) to the matching accounts. Two independent axes: the asset class (the account's type: Cash, Funds / ETF, Stocks, Crypto, Other) and the fiscal envelope (an optional account attribute: TFSA, RRSP, RRIF, FHSA, RESP, or none). An account is entered either as a single amount (simple account) or security by security (detailed account: each holding with quantity × price and a cost basis, the account value being the sum of its positions).",
"features": [ "features": [
"5 standard types pre-installed, which are asset classes (Cash, Funds / ETF, Stocks, Crypto, Other) — renameable, non-deletable; a type groups accounts of the same nature (distinct from transaction categories). Fiscal envelopes are no longer types", "5 standard types pre-installed, which are asset classes (Cash, Funds / ETF, Stocks, Crypto, Other) — renameable, non-deletable; a type groups accounts of the same nature (distinct from transaction categories). Fiscal envelopes are no longer types",
"Custom type creation with simple (direct amount) or priced (quantity × unit price) entry mode", "Custom type creation with simple (direct amount) or priced (quantity × unit price) entry mode",
"Detailed accounts (per security): an account can hold several securities — each security has its own row (symbol, quantity, price, value, cost basis, unrealized gain), the account value being the sum of its securities. Security picker with autocomplete and inline creation (symbol + asset class Stock/Crypto)",
"Unrealized gain per security and aggregated (by account, by asset class, by envelope): value cost basis, in $ and %; per-security drill-down in the accounts table; a position without a cost basis shows \"N/A\" and is excluded from the %",
"\"Detail into securities\" wizard: switches a simple account to detailed from a pivot date; past aggregated history stays frozen and read-only",
"Accounts per type: name, optional fiscal envelope (Non-registered, TFSA, RRSP, RRIF, FHSA, RESP, or none by default), optional symbol (even for priced types — it only drives automatic price fetching), currency (CAD at MVP), notes", "Accounts per type: name, optional fiscal envelope (Non-registered, TFSA, RRSP, RRIF, FHSA, RESP, or none by default), optional symbol (even for priced types — it only drives automatic price fetching), currency (CAD at MVP), notes",
"Renaming a type no longer breaks bilingual support: the custom name is stored separately, the original FR/EN translation is preserved", "Renaming a type no longer breaks bilingual support: the custom name is stored separately, the original FR/EN translation is preserved",
"Dated snapshots with a UNIQUE constraint per date — editing means revisiting the same date, never duplicating; an existing snapshot's date can be moved (lines preserved) when the target date is free", "Dated snapshots with a UNIQUE constraint per date — editing means revisiting the same date, never duplicating; an existing snapshot's date can be moved (lines preserved) when the target date is free",
"\"Prefill from previous snapshot\" button: copies simple values + priced quantities", "\"Prefill from previous snapshot\" button: copies simple values, priced quantities, and the securities/quantities/cost bases of detailed accounts (a security at quantity 0 is skipped)",
"Linking existing transactions to a balance account (modal with filters and auto-suggested direction)", "Linking existing transactions to a balance account (modal with filters and auto-suggested direction)",
"Attribution icon in the Transactions page for transactions linked to a transfer", "Attribution icon in the Transactions page for transactions linked to a transfer",
"Evolution chart with line or stacked-area mode, with an axis sub-toggle: by asset class (default) or by envelope + vertical markers for tagged transfers (green = in, red = out)", "Evolution chart with line or stacked-area mode, with an axis sub-toggle: by asset class (default) or by envelope + vertical markers for tagged transfers (green = in, red = out)",
@ -970,6 +973,7 @@
"Save. The chart on /balance refreshes immediately", "Save. The chart on /balance refreshes immediately",
"To compute the real return of an investment account, open the actions menu → \"Link transfers\" → check the transactions matching deposits/withdrawals — direction (in/out) is auto-proposed", "To compute the real return of an investment account, open the actions menu → \"Link transfers\" → check the transactions matching deposits/withdrawals — direction (in/out) is auto-proposed",
"The accounts table now shows Modified Dietz returns over 3M / 1Y / since inception, side-by-side with the unadjusted return", "The accounts table now shows Modified Dietz returns over 3M / 1Y / since inception, side-by-side with the unadjusted return",
"To track an account security by security, create it detailed or open its actions menu → \"Detail into securities\"; at later snapshots, add each holding with its quantity, price and cost basis (the account value = the sum of its securities)",
"To edit an existing snapshot, click its point on the chart or use the date picker — the page opens in edit mode. You can also fix the date: change it then save, and the snapshot is moved with its lines (a warning shows if the target date is already taken)", "To edit an existing snapshot, click its point on the chart or use the date picker — the page opens in edit mode. You can also fix the date: change it then save, and the snapshot is moved with its lines (a warning shows if the target date is already taken)",
"To delete a snapshot, click \"Delete\" in its editor and retype the date to confirm" "To delete a snapshot, click \"Delete\" in its editor and retype the date to confirm"
], ],
@ -979,6 +983,7 @@
"Vertical chart markers help you read value jumps: a jump followed by a green marker isn't \"performance\", it's a deposit", "Vertical chart markers help you read value jumps: a jump followed by a green marker isn't \"performance\", it's a deposit",
"If you try to delete a transaction linked to a balance account, the app asks you to unlink it first — this friction preserves the reproducibility of past returns", "If you try to delete a transaction linked to a balance account, the app asks you to unlink it first — this friction preserves the reproducibility of past returns",
"The \"balance out of date\" warning appears if your latest snapshot is more than 60 days old", "The \"balance out of date\" warning appears if your latest snapshot is more than 60 days old",
"Unrealized gain ≠ return: the unrealized gain (value cost basis) measures your gain since purchase, while the Modified Dietz return measures performance accounting for the timing of your contributions — both show on a detailed account. Adjust the cost basis after a buy/sell, otherwise the unrealized gain drifts",
"(Coming in Phase 5) Automatic price fetching for Stocks/Crypto via a private proxy (premium-only) that anonymizes your request — manual entry remains always available", "(Coming in Phase 5) Automatic price fetching for Stocks/Crypto via a private proxy (premium-only) that anonymizes your request — manual entry remains always available",
"Toggle the stacked chart axis between \"by asset class\" and \"by envelope\" to read both sides of your net worth. Note: a snapshot taken before the class/envelope split shows under \"Other\" on the by-asset-class axis (the former TFSA/RRSP types became envelopes) — this is expected; the by-envelope axis still surfaces your TFSA/RRSP, and your amounts are unchanged" "Toggle the stacked chart axis between \"by asset class\" and \"by envelope\" to read both sides of your net worth. Note: a snapshot taken before the class/envelope split shows under \"Other\" on the by-asset-class axis (the former TFSA/RRSP types became envelopes) — this is expected; the by-envelope axis still surfaces your TFSA/RRSP, and your amounts are unchanged"
] ]

View file

@ -945,14 +945,17 @@
}, },
"balance": { "balance": {
"title": "Bilan", "title": "Bilan",
"overview": "Le Bilan est une vue patrimoniale : vous saisissez périodiquement un snapshot (relevé daté de votre patrimoine) de l'ensemble de vos comptes, suivez leur évolution dans le temps et calculez le vrai rendement de chaque compte d'investissement en liant les transferts (apports/retraits) aux comptes correspondants. Deux axes indépendants : la classe d'actif (le type du compte : Liquidités, Fonds / FNB, Actions, Crypto, Autres) et l'enveloppe fiscale (un attribut optionnel du compte : CELI, REER, FERR, CELIAPP, REEE, ou aucune).", "overview": "Le Bilan est une vue patrimoniale : vous saisissez périodiquement un snapshot (relevé daté de votre patrimoine) de l'ensemble de vos comptes, suivez leur évolution dans le temps et calculez le vrai rendement de chaque compte d'investissement en liant les transferts (apports/retraits) aux comptes correspondants. Deux axes indépendants : la classe d'actif (le type du compte : Liquidités, Fonds / FNB, Actions, Crypto, Autres) et l'enveloppe fiscale (un attribut optionnel du compte : CELI, REER, FERR, CELIAPP, REEE, ou aucune). Un compte se saisit en montant unique (compte simple) ou titre par titre (compte détaillé : chaque valeur mobilière avec quantité × cours et coût d'acquisition, la valeur du compte étant la somme de ses positions).",
"features": [ "features": [
"5 types standard pré-installés, qui sont des classes d'actif (Liquidités, Fonds / FNB, Actions, Crypto, Autres) — renommables, non-supprimables ; un type regroupe des comptes de même nature (distinct des catégories de transactions). Les enveloppes fiscales ne sont plus des types", "5 types standard pré-installés, qui sont des classes d'actif (Liquidités, Fonds / FNB, Actions, Crypto, Autres) — renommables, non-supprimables ; un type regroupe des comptes de même nature (distinct des catégories de transactions). Les enveloppes fiscales ne sont plus des types",
"Création de types personnalisés avec choix simple (montant direct) ou priced (quantité × prix unitaire)", "Création de types personnalisés avec choix simple (montant direct) ou priced (quantité × prix unitaire)",
"Comptes détaillés (par titre) : un compte peut contenir plusieurs valeurs mobilières — chaque titre a sa ligne (symbole, quantité, cours, valeur, coût d'acquisition, gain latent), la valeur du compte étant la somme de ses titres. Sélecteur de titre avec autocomplétion et création inline (symbole + classe d'actif Action/Crypto)",
"Gain latent par titre et agrégé (par compte, par classe d'actif, par enveloppe) : valeur coût d'acquisition, en $ et en % ; drill-down par titre dans le tableau des comptes ; une position sans coût d'acquisition affiche « N/A » et est exclue du %",
"Assistant « Détailler en titres » : bascule un compte simple en compte détaillé à partir d'une date de bascule (pivot) ; l'historique agrégé passé reste figé en lecture seule",
"Comptes par type : nom, enveloppe fiscale optionnelle (Non-enregistré, CELI, REER, FERR, CELIAPP, REEE, ou aucune par défaut), symbole optionnel (même pour les types cotés, il ne sert qu'à la récupération automatique des prix), devise (CAD au MVP), notes", "Comptes par type : nom, enveloppe fiscale optionnelle (Non-enregistré, CELI, REER, FERR, CELIAPP, REEE, ou aucune par défaut), symbole optionnel (même pour les types cotés, il ne sert qu'à la récupération automatique des prix), devise (CAD au MVP), notes",
"Renommer un type ne casse plus le bilingue : le nom personnalisé est stocké à part, la traduction FR/EN d'origine est préservée", "Renommer un type ne casse plus le bilingue : le nom personnalisé est stocké à part, la traduction FR/EN d'origine est préservée",
"Snapshots datés avec contrainte UNIQUE par date — éditer = revenir sur la même date, jamais dupliquer ; la date d'un snapshot existant peut être déplacée (lignes conservées) si la date cible est libre", "Snapshots datés avec contrainte UNIQUE par date — éditer = revenir sur la même date, jamais dupliquer ; la date d'un snapshot existant peut être déplacée (lignes conservées) si la date cible est libre",
"Bouton « Pré-remplir depuis le snapshot précédent » : copie les valeurs simples + les quantités priced", "Bouton « Pré-remplir depuis le snapshot précédent » : copie les valeurs simples, les quantités priced, et les titres/quantités/coûts d'acquisition des comptes détaillés (un titre à quantité 0 est ignoré)",
"Liaison de transactions existantes à un compte de bilan (modal avec filtres et sens auto-proposé)", "Liaison de transactions existantes à un compte de bilan (modal avec filtres et sens auto-proposé)",
"Icône d'attribution dans la page Transactions pour les transactions liées à un transfert", "Icône d'attribution dans la page Transactions pour les transactions liées à un transfert",
"Graphique d'évolution avec mode courbe ou aire empilée, avec un sous-choix d'axe : par classe d'actif (défaut) ou par enveloppe + marqueurs verticaux pour les transferts (vert = in, rouge = out)", "Graphique d'évolution avec mode courbe ou aire empilée, avec un sous-choix d'axe : par classe d'actif (défaut) ou par enveloppe + marqueurs verticaux pour les transferts (vert = in, rouge = out)",
@ -970,6 +973,7 @@
"Enregistrez. Le graphique sur /balance s'actualise immédiatement", "Enregistrez. Le graphique sur /balance s'actualise immédiatement",
"Pour calculer le rendement réel d'un compte d'investissement, ouvrez le menu actions → « Lier transferts » → cochez les transactions qui correspondent à des apports/retraits — le sens (in/out) est proposé automatiquement", "Pour calculer le rendement réel d'un compte d'investissement, ouvrez le menu actions → « Lier transferts » → cochez les transactions qui correspondent à des apports/retraits — le sens (in/out) est proposé automatiquement",
"Le tableau des comptes affiche maintenant les rendements Modified Dietz sur 3M / 1A / depuis création, avec le rendement non-ajusté côte-à-côte", "Le tableau des comptes affiche maintenant les rendements Modified Dietz sur 3M / 1A / depuis création, avec le rendement non-ajusté côte-à-côte",
"Pour suivre un compte titre par titre, créez-le détaillé ou ouvrez son menu d'actions → « Détailler en titres » ; aux snapshots suivants, ajoutez chaque valeur mobilière avec sa quantité, son cours et son coût d'acquisition (la valeur du compte = la somme des titres)",
"Pour éditer un snapshot existant, cliquez sur son point dans le graphique ou utilisez le sélecteur de date — la page s'ouvre en mode édition. Vous pouvez aussi corriger la date : changez-la puis enregistrez, le snapshot est déplacé avec ses lignes (message d'alerte si la date cible est déjà prise)", "Pour éditer un snapshot existant, cliquez sur son point dans le graphique ou utilisez le sélecteur de date — la page s'ouvre en mode édition. Vous pouvez aussi corriger la date : changez-la puis enregistrez, le snapshot est déplacé avec ses lignes (message d'alerte si la date cible est déjà prise)",
"Pour supprimer un snapshot, cliquez « Supprimer » dans son éditeur et re-saisissez la date pour confirmer" "Pour supprimer un snapshot, cliquez « Supprimer » dans son éditeur et re-saisissez la date pour confirmer"
], ],
@ -979,6 +983,7 @@
"Les marqueurs verticaux du graphique aident à lire les sauts de valeur : un saut suivi d'un marqueur vert n'est pas une « performance », c'est un dépôt", "Les marqueurs verticaux du graphique aident à lire les sauts de valeur : un saut suivi d'un marqueur vert n'est pas une « performance », c'est un dépôt",
"Si vous tentez de supprimer une transaction liée à un compte de bilan, l'app vous demande de la délier d'abord — cette friction préserve la reproductibilité de vos rendements passés", "Si vous tentez de supprimer une transaction liée à un compte de bilan, l'app vous demande de la délier d'abord — cette friction préserve la reproductibilité de vos rendements passés",
"L'avertissement « bilan pas à jour » apparaît si votre dernier snapshot remonte à plus de 60 jours", "L'avertissement « bilan pas à jour » apparaît si votre dernier snapshot remonte à plus de 60 jours",
"Gain latent ≠ rendement : le gain latent (valeur coût d'acquisition) mesure votre plus-value depuis l'achat, le rendement Modified Dietz mesure la performance en tenant compte du timing de vos apports — les deux s'affichent sur un compte détaillé. Ajustez le coût d'acquisition après un achat/vente, sinon le gain latent dérive",
"(À venir Phase 5) Récupération automatique des prix pour Actions/Crypto via un proxy privé (premium-only) qui anonymise votre requête — la saisie manuelle reste toujours disponible", "(À venir Phase 5) Récupération automatique des prix pour Actions/Crypto via un proxy privé (premium-only) qui anonymise votre requête — la saisie manuelle reste toujours disponible",
"Basculez l'axe du graphique empilé entre « par classe d'actif » et « par enveloppe » pour lire les deux faces du patrimoine. Note : un snapshot saisi avant la séparation classe/enveloppe apparaît sous « Autres » sur l'axe par classe d'actif (les ex-types CELI/REER sont devenus des enveloppes) — c'est attendu, l'axe par enveloppe retrouve bien vos CELI/REER, vos montants ne changent pas" "Basculez l'axe du graphique empilé entre « par classe d'actif » et « par enveloppe » pour lire les deux faces du patrimoine. Note : un snapshot saisi avant la séparation classe/enveloppe apparaît sous « Autres » sur l'axe par classe d'actif (les ex-types CELI/REER sont devenus des enveloppes) — c'est attendu, l'axe par enveloppe retrouve bien vos CELI/REER, vos montants ne changent pas"
] ]