Amélioration du calcul XIRR

This commit is contained in:
2026-07-13 07:11:42 +02:00
parent 316a86c95a
commit 7ace2c8f2e
2 changed files with 54 additions and 10 deletions
+21 -9
View File
@@ -872,24 +872,34 @@ export default function InvestissementDetail() {
URL.revokeObjectURL(url);
};
// Rendement réel annualisé (XIRR) — calculé uniquement si l'opération est remboursée
// Rendement annualisé (XIRR) — calcul exact une fois le prêt intégralement remboursé.
// Sinon (en cours / en retard / procédure), estimation basée sur les remboursements déjà
// versés + une valorisation du capital restant dû à la date du jour (comme si ce capital
// était "racheté" aujourd'hui) — donne un taux annualisé qui tient compte du rythme réel
// des versements, contrairement à un simple ratio intérêts perçus / capital / durée.
let rendementReelBrut = null;
let rendementReel = null;
if (inv.statut === 'rembourse' && remb.length > 0 && inv.date_souscription) {
const rendementEstime = inv.statut !== 'rembourse';
if (remb.length > 0 && inv.date_souscription) {
// Flux sortants supplémentaires (réinvestissements = nouveaux décaissements)
const reinvOutflows = reinvs.map(r => ({ amount: -r.montant, date: r.date_reinvestissement }));
// Valorisation à date du capital pas encore remboursé (uniquement pour l'estimation en cours)
const valorisationAuj = rendementEstime && capRestant > 0 ? [{ amount: capRestant, date: todayStr }] : [];
// Brut : capital + cashback + intérêts bruts (avant fiscalité)
const cashflowsBrut = [
{ amount: -inv.montant_investi, date: inv.date_souscription },
...reinvOutflows,
...remb.map(r => ({ amount: (r.capital || 0) + (r.cashback || 0) + (r.interets_bruts || 0), date: r.date_remb })),
...valorisationAuj,
];
rendementReelBrut = xirr(cashflowsBrut);
// Net : net_recu (après prélèvements sociaux + impôt sur le revenu)
// Net : net_recu (après prélèvements sociaux + impôt sur le revenu) — le capital restant
// valorisé n'est, lui, pas soumis à fiscalité (ce n'est pas un gain, juste le principal)
const cashflows = [
{ amount: -inv.montant_investi, date: inv.date_souscription },
...reinvOutflows,
...remb.map(r => ({ amount: r.net_recu, date: r.date_remb })),
...valorisationAuj,
];
rendementReel = xirr(cashflows);
}
@@ -961,18 +971,20 @@ export default function InvestissementDetail() {
</div>
{(() => {
const rendement = netMode ? rendementReel : rendementReelBrut;
const baseTip = netMode ? 'XIRR sur flux nets réels (après fiscalité)' : 'XIRR sur flux bruts (avant fiscalité)';
const titleTip = rendement !== null
? (netMode ? 'XIRR sur flux nets réels (après fiscalité)' : 'XIRR sur flux bruts (avant fiscalité)')
: inv.statut !== 'rembourse'
? 'Disponible uniquement sur les investissements remboursés'
: 'Données insuffisantes';
? baseTip + (rendementEstime ? ' — estimation : capital restant dû valorisé à la date du jour' : '')
: 'Nécessite au moins un remboursement enregistré';
return (
<div className="kpi" title={titleTip}>
<div className="label">Rendement annualisé {netMode ? 'Net' : 'Brut'}</div>
<div className="label">
Rendement annualisé {netMode ? 'Net' : 'Brut'}
{rendementEstime && rendement !== null ? <span className="text-muted"> (estimé)</span> : ''}
</div>
<div className="value" style={{ color: rendement !== null ? (rendement >= 0 ? 'var(--success)' : 'var(--danger)') : 'var(--text-muted)' }}>
{rendement !== null
? `${(rendement * 100).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} %`
: inv.statut === 'rembourse' ? '—' : 'En cours…'}
: remb.length === 0 ? 'Aucun remboursement' : '—'}
</div>
</div>
);