XIRR sur les projets

This commit is contained in:
ocroguennec committed 2026-08-22 18:49:33 +02:00
1 parent c9ca27b7be
commit e401530673
1 file changed
+68
@@ -953,6 +953,15 @@ export default function InvestissementDetail() {
let rendementReelBrut = null;
let rendementReel = null;
const rendementEstime = inv.statut !== 'rembourse';
// XIRR "attendu" (théorique) — même principe que sur la page Plateformes (cf.
// project_xirr_attendu_theorique.md) : au lieu de valoriser prudemment le capital
// restant dû "comme racheté aujourd'hui" (sans intérêts futurs), on projette les
// échéances futures prévues (simul_remboursements) jusqu'au terme du prêt — pour
// matcher le taux de rendement théorique affiché par les plateformes elles-mêmes
// (ex. "Mon taux de rendement interne" chez Bienprêter). Fallback identique au XIRR
// réel (valorisation du capital restant au jour) si aucune échéance future en base.
let rendementAttenduBrut = null;
let rendementAttendu = null;
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 }));
@@ -975,6 +984,37 @@ export default function InvestissementDetail() {
...valorisationAuj,
];
rendementReel = xirr(cashflows);
if (rendementEstime && capRestant > 0.01) {
const futureEcheances = simul.filter(s =>
s.date_prevue > todayStr && ((s.capital_prevu || 0) > 0 || (s.interets_prevus || 0) > 0)
);
if (futureEcheances.length) {
const flowsBrut = [
{ 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 })),
];
const flowsNet = [
{ amount: -inv.montant_investi, date: inv.date_souscription },
...reinvOutflows,
...remb.map(r => ({ amount: r.net_recu, date: r.date_remb })),
];
for (const s of futureEcheances) {
const rates = getRatesForYear(s.date_prevue);
const pfu = rates ? (rates.prelev_sociaux + rates.impot_revenu) / 100 : 0.3;
flowsBrut.push({ amount: (s.capital_prevu || 0) + (s.interets_prevus || 0), date: s.date_prevue });
flowsNet.push({ amount: (s.capital_prevu || 0) + (s.interets_prevus || 0) * (1 - pfu), date: s.date_prevue });
}
rendementAttenduBrut = xirr(flowsBrut);
rendementAttendu = xirr(flowsNet);
} else {
// Pas d'échéancier prévisionnel disponible : identique au XIRR réel, pour ne
// jamais sous-évaluer un prêt faute de données (cf. page Plateformes).
rendementAttenduBrut = rendementReelBrut;
rendementAttendu = rendementReel;
}
}
}
return (
@@ -1049,6 +1089,7 @@ export default function InvestissementDetail() {
</div>
{(() => {
const rendement = netMode ? rendementReel : rendementReelBrut;
const rendementAttenduAffiche = netMode ? rendementAttendu : rendementAttenduBrut;
const xirrExplication =
`XIRR (taux de rendement interne actualisé) : taux annualisé calculé à partir des flux réels datés ` +
`(versement initial, réinvestissements, remboursements ${netMode ? 'nets, après fiscalité' : 'bruts, avant fiscalité'}). ` +
@@ -1056,6 +1097,14 @@ export default function InvestissementDetail() {
(rendementEstime
? "Prêt pas encore soldé : le capital restant dû est ajouté en flux final, valorisé à la date du jour — c'est donc une estimation, pas un taux définitif."
: 'Prêt intégralement remboursé : taux définitif.');
const xirrAttenduExplication =
`XIRR attendu (théorique) : comme le taux de rendement interne affiché par les plateformes de crowdlending ` +
`(ex. "Mon taux de rendement interne" chez Bienprêter, le %/an affiché chez La Première Brique), ce calcul suppose ` +
`que toutes les échéances prévues (capital + intérêts) encore à venir seront honorées exactement à la date prévue ` +
`par l'échéancier — contrairement au XIRR ci-dessus, qui valorise prudemment le capital restant dû "comme racheté ` +
`aujourd'hui", sans compter les intérêts futurs.\n\n` +
`C'est donc une projection optimiste : elle ne tient pas compte d'un éventuel retard ou défaut de paiement. Si ce ` +
`prêt n'a pas d'échéancier prévisionnel en base, il est valorisé de la même façon que dans le XIRR réel.`;
return (
<div className="kpi">
<div className="label" style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
@@ -1078,6 +1127,25 @@ export default function InvestissementDetail() {
? `${(rendement * 100).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} %`
: remb.length === 0 ? 'Aucun remboursement' : '—'}
</div>
{rendementAttenduAffiche !== null && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 6 }}>
<span style={{
fontSize: '0.78em', fontWeight: 600, padding: '2px 8px', borderRadius: 20,
background: 'rgba(124,58,237,0.10)', color: '#7c3aed', whiteSpace: 'nowrap',
}}>
Attendu : {(rendementAttenduAffiche * 100).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} %
</span>
<span
className="cell-tooltip tooltip-down"
data-tooltip={xirrAttenduExplication}
style={{ display: 'inline-flex', color: 'var(--text-muted)' }}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
</span>
</div>
)}
</div>
);
})()}