- 0008 — Modified Dietz: justifies the choice over ROI / TWR / IRR; references commands/return_calculator.rs and the 7 TDD cases. - 0009 — Proxy price-fetching via maximus-api: documents the privacy proxy architecture (header stripping, no log correlation, fixed simpl-resultat UA), the Yahoo + CoinGecko adapter abstraction, the Bearer activation_token auth strategy, the rate limiting (client + server), and the dual-side premium gating. Implementation stays BLOCKED in #143; this ADR documents the agreed-upon design. - 0010 — FK ON DELETE RESTRICT on balance_account_transfers .transaction_id: justifies the integrity-over-friction trade-off for Modified Dietz reproducibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 KiB
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 connexionrusqlitecourte 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 testsdans le même fichier — 7 cas (nominal, pas de snapshot début, partial-end, compte créé en cours, compte vidé, aucun transfert, annualisation).
Output
struct AccountReturn {
value_start: Option<f64>,
value_end: f64,
net_contributions: f64,
return_pct: Option<f64>, // None si dénominateur ≈ 0
annualized_pct: Option<f64>, // (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 RESTRICTsurbalance_account_transfers.transaction_id(voir ADR 0010), 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é surhas_no_transfers_warningou 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 testscargo) est plus robuste. Cohérent avecaes-gcm,argon2, etc.
References
- Spec :
spec-decisions-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 testsdansreturn_calculator.rs(7 cas) - ADR liée : 0010 — FK RESTRICT sur
balance_account_transfers.transaction_id - GIPS standards (Global Investment Performance Standards) — Modified Dietz est listé comme méthode acceptable d'approximation pour des périodes < 1 an.