# ADR 0008 — Modified Dietz pour le calcul du rendement par compte - Status: Accepted - Date: 2026-04-25 - Milestone: `overnight-2026-04-26-bilan` (Issues #138 → #145) ## Context La feature Bilan introduit une vue patrimoniale (snapshots datés) avec calcul du rendement par compte. Le rendement réel d'un compte d'investissement n'est PAS `(V_fin − V_début) / V_début` : cette formule confond les **gains réels** avec les **apports/retraits**. > Exemple : compte CELI à 10 000 $, on dépose 5 000 $, le compte vaut 16 000 $ à la fin. La formule naïve donne 60 % (16/10), mais la moitié du gain est juste l'apport. Le vrai rendement est 6 % : `(16 000 − 5 000 − 10 000) / 10 000`. C'est exactement la raison pour laquelle l'utilisateur tague des transferts (table `balance_account_transfers`) : pour les exclure du calcul. Quatre formules candidates ont été comparées dans le spike (`~/claude-code/.spikes/bilan/code/rendement.md`) : | Méthode | Pondère le timing des flux ? | Nécessite des valeurs intermédiaires ? | Standard d'industrie ? | |---------|---|---|---| | ROI ajusté simple | ❌ | ❌ | ❌ | | **Modified Dietz** | ✅ (approximation linéaire) | ❌ | ✅ (GIPS-compliant en première approximation) | | Time-Weighted Return (TWR) | ✅ (exact) | ✅ (à chaque flux) | ✅ | | Money-Weighted Return / IRR | ✅ (exact, itératif) | ❌ | ✅ | Contraintes du contexte Simpl'Résultat : - Les snapshots sont saisis librement (mensuels, trimestriels, ad-hoc) — il n'y a **pas** de valeur du compte aux dates de flux. - Pas de solveur numérique embarqué côté client (pas de Newton-Raphson en Rust pour l'IRR). - L'utilisateur doit pouvoir comprendre le résultat sans formation financière. ## Decision **Adopter Modified Dietz** comme méthode unique de calcul du rendement par compte au MVP, implémentée côté Rust dans le module privé `src-tauri/src/commands/return_calculator.rs`. ``` R = (V_fin − V_début − C_net) / (V_début + Σ(C_i × W_i)) ``` où : - `C_i` = chaque flux (signé : + apport, − retrait) - `W_i = (T − t_i) / T` = poids temporel (1 si début de période, 0 si fin) - `T` = durée totale de la période en jours, `t_i` = position du flux ### Architecture - **Logique pure** : `commands/return_calculator.rs` (module privé, pas exposé comme commande). `pub(crate) fn modified_dietz(...) -> AccountReturn`. - **Commande Tauri** : `commands/balance_commands.rs::compute_account_return(account_id, period_start, period_end, db_filename)` ouvre une connexion `rusqlite` courte sur la DB du profil actif, lit le snapshot ≤ start, le snapshot ≥ end et les cash flows liés, puis délègue le calcul. - **Dépendance Cargo** : `chrono = "0.4"` ajoutée pour l'arithmétique de dates (poids temporels en jours). - **Tests TDD co-localisés** : `#[cfg(test)] mod tests` dans le même fichier — 7 cas (nominal, pas de snapshot début, partial-end, compte créé en cours, compte vidé, aucun transfert, annualisation). ### Output ```rust struct AccountReturn { value_start: Option, value_end: f64, net_contributions: f64, return_pct: Option, // None si dénominateur ≈ 0 annualized_pct: Option, // (1 + R)^(365/days) - 1, si days > 30 is_partial: bool, // true si snapshot manquant après fin has_no_transfers_warning: bool, // true si aucun transfert lié } ``` ### Affichage côté UI (`BalanceAccountsTable`) - 3 colonnes Modified Dietz : 3M / 1A / depuis création - 1 colonne **rendement non-ajusté** (`(V_fin − V_début) / V_début`) côte-à-côte — pédagogique : montre l'effet des apports vs gains réels - Warnings visibles (`is_partial`, `has_no_transfers_warning`) avec tooltip i18n ## Consequences ### Positive - **Pas besoin de valeurs intermédiaires** : le calcul ne nécessite que les snapshots existants + les transferts taggés. C'est exactement ce que l'utilisateur saisit déjà. - **Standard d'industrie** : Modified Dietz est GIPS-compliant en première approximation. Le résultat est défendable. - **Pédagogique** : afficher le rendement non-ajusté à côté du Modified Dietz éduque l'utilisateur sur la différence entre "valeur du compte" et "vraie performance". - **Implémentation simple** : ~50 lignes de logique pure en Rust + 7 tests. Pas de solveur numérique. - **Reproductibilité** : combinée avec la FK `ON DELETE RESTRICT` sur `balance_account_transfers.transaction_id` (voir [ADR 0010](0010-fk-restrict-balance-transfers.md)), une période déjà calculée ne peut pas changer rétroactivement. ### Negative / trade-offs - **Approximation** : Modified Dietz suppose une distribution linéaire des flux dans le temps. Si plusieurs flux concentrés tombent juste avant un mouvement de marché significatif, l'erreur s'accumule. Acceptable pour un usage personnel ; un investisseur professionnel utiliserait TWR exact. - **Cas dégénéré "compte vidé puis rechargé"** : le dénominateur `V_début + Σ(C_i × W_i)` peut tendre vers zéro et faire exploser le ratio. Mitigé par un warning UI "Performance non significative" basé sur `has_no_transfers_warning` ou un seuil sur le dénominateur. - **Pas de TWR au MVP** : si l'utilisateur veut la vraie performance gestionnaire (indépendante du timing des flux), il devra attendre une v2 qui demandera de saisir des valeurs intermédiaires aux dates de flux. - **Pas de Money-Weighted Return / IRR** : formule plus précise mais nécessite Newton-Raphson. Coût/bénéfice défavorable au MVP. ## Alternatives considered - **ROI ajusté simple** (`(V_fin − V_début − C_net) / V_début`). Rejeté : ignore *quand* l'apport est arrivé. Un dépôt de 10 000 $ le 1er janvier vs le 31 décembre donne le même résultat — incorrect. - **TWR (Time-Weighted Return)**. Rejeté pour le MVP : nécessite des valeurs du compte aux dates de flux, qu'on ne stocke pas. Possible v2 si l'utilisateur accepte de saisir des valeurs intermédiaires. - **IRR (Money-Weighted Return)**. Rejeté : nécessite un solveur Newton-Raphson, complexité disproportionnée pour un usage personnel. - **Calcul côté TypeScript (sans commande Rust)**. Rejeté : l'arithmétique de dates en JavaScript (`Date.UTC(...) / 86400000`) est correcte mais le pattern projet (logique financière côté Rust avec tests `cargo`) est plus robuste. Cohérent avec `aes-gcm`, `argon2`, etc. ## References - Spec : [`spec-decisions-bilan.md`](../../spec-decisions-bilan.md), [`spec-plan-bilan.md`](../../spec-plan-bilan.md) - Spike : `~/claude-code/.spikes/bilan/code/rendement.md` (comparaison ROI / Modified Dietz / TWR / IRR) - Implémentation : `src-tauri/src/commands/return_calculator.rs`, `src-tauri/src/commands/balance_commands.rs` - Tests TDD : `#[cfg(test)] mod tests` dans `return_calculator.rs` (7 cas) - ADR liée : [0010 — FK RESTRICT sur `balance_account_transfers.transaction_id`](0010-fk-restrict-balance-transfers.md) - GIPS standards (Global Investment Performance Standards) — Modified Dietz est listé comme méthode acceptable d'approximation pour des périodes < 1 an.