Simpl-Resultat/docs/adr/0008-modified-dietz-pour-rendement.md
le king fu 098e15bb5c docs(adr): add ADRs 0008-0010 (Modified Dietz, proxy price-fetching, FK RESTRICT)
- 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>
2026-04-25 17:06:40 -04:00

7 KiB
Raw Blame History

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

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 RESTRICT sur balance_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é 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-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
  • GIPS standards (Global Investment Performance Standards) — Modified Dietz est listé comme méthode acceptable d'approximation pour des périodes < 1 an.