- 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>
100 lines
7 KiB
Markdown
100 lines
7 KiB
Markdown
# 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.
|