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

100 lines
7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<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](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.