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