Deal Parameters
Investment Amount ?
$
Preferred Return / Hurdle Rate ?
%
Hold Period ?
5 yrs
Projected Total Distributions ?
1.80x
GP Promote / Carried Interest ?
%
LP / GP Equity Split ?
80/20
Scenario Adjustments
-20%
0%
+25%
Projected Returns (Base Case)
Projected IRR
Internal rate of return
Equity Multiple
Total return on capital
Annualized Return
Simple annual return
Total Distributions
Gross cash returned
LP Return
After promote
GP Promote
Above hurdle rate
Scenario Analysis
Bear Case
IRR
Multiple
LP Return
Base Case
IRR
Multiple
LP Return
Bull Case
IRR
Multiple
LP Return
Year-by-Year Distributions
Deal Structure
LP Capital Invested ?
$
Total Deal Profit ?
$
Preferred Return ?
%
Hold Period (years) ?
5 yrs
GP Catch-Up % ?
100%
Promote Tiers
Tier 1 — Up to IRR Hurdle 1
IRR Hurdle ?
%
GP Share ?
%
Tier 2 — Up to IRR Hurdle 2
IRR Hurdle ?
%
GP Share
%
Tier 3 — Above IRR Hurdle 2
IRR Hurdle
%
GP Share
%
Distribution Waterfall
Tranche Tranche Amount To LP To GP LP / GP
Enter deal parameters to see waterfall
LP vs GP Split
LP: —
0 && remaining > 0 && profitsPaid > 0) { const targetGPShare = t1GP; if (catchupPct >= 1) { // 100% catch-up: GP gets everything until GP has t1GP % of all profit const catchupTarget = targetGPShare * profitsPaid / (1 - targetGPShare); catchupTotal = Math.min(catchupTarget, remaining); catchupGP = catchupTotal; remaining -= catchupTotal; } else { // partial catch-up const catchupTarget = targetGPShare * profitsPaid / (1 - targetGPShare) / catchupPct; catchupTotal = Math.min(catchupTarget, remaining); catchupGP = catchupTotal * catchupPct; catchupLP = catchupTotal - catchupGP; remaining -= catchupTotal; } } if (catchupTotal > 0) { tranches.push({ name: 'GP Catch-Up (' + el('wCatchup').value + '%)', color: TIER_COLORS[2], total: catchupTotal, lp: catchupLP, gp: catchupGP }); lpTotal += catchupLP; gpTotal += catchupGP; } // For tier allocation, determine how much profit corresponds to each IRR band // Use simplified approach: allocate remaining profit by IRR-implied capital amounts const totalReturn = lpCapital + totalProfit; const irr1Cap = lpCapital * Math.pow(1 + t1Hurdle/100, hold); const irr2Cap = lpCapital * Math.pow(1 + t2Hurdle/100, hold); // Profit that falls within each tier const profitAtT1 = Math.max(0, irr1Cap - lpCapital - prefAmount); const profitAtT2 = Math.max(0, irr2Cap - irr1Cap); // Tier 1: up to t1 hurdle if (remaining > 0) { const t1Avail = profitAtT1 > 0 ? Math.min(remaining, profitAtT1) : remaining * 0.5; const t1Actual = Math.min(t1Avail, remaining); if (t1Actual > 0) { const gpAmt = t1Actual * t1GP; const lpAmt = t1Actual - gpAmt; tranches.push({ name: `Residual Split — Tier 1 (LP ${fmt.pct((1-t1GP)*100)} / GP ${fmt.pct(t1GP*100)})`, color: TIER_COLORS[3], total: t1Actual, lp: lpAmt, gp: gpAmt }); lpTotal += lpAmt; gpTotal += gpAmt; remaining -= t1Actual; } } // Tier 2 if (remaining > 0) { const t2Actual = Math.min(profitAtT2 || remaining * 0.5, remaining); if (t2Actual > 0) { const gpAmt = t2Actual * t2GP; const lpAmt = t2Actual - gpAmt; tranches.push({ name: `Residual Split — Tier 2 (LP ${fmt.pct((1-t2GP)*100)} / GP ${fmt.pct(t2GP*100)})`, color: TIER_COLORS[4], total: t2Actual, lp: lpAmt, gp: gpAmt }); lpTotal += lpAmt; gpTotal += gpAmt; remaining -= t2Actual; } } // Tier 3: remainder if (remaining > 0.01) { const gpAmt = remaining * t3GP; const lpAmt = remaining - gpAmt; tranches.push({ name: `Residual Split — Tier 3 (LP ${fmt.pct((1-t3GP)*100)} / GP ${fmt.pct(t3GP*100)})`, color: TIER_COLORS[5], total: remaining, lp: lpAmt, gp: gpAmt }); lpTotal += lpAmt; gpTotal += gpAmt; remaining = 0; } renderWaterfallTable(tranches, lpTotal, gpTotal, lpCapital); drawPieChart(lpTotal - lpCapital, gpTotal, lpCapital); // Metrics el('wLPTotal').textContent = fmt.$(lpTotal); el('wGPTotal').textContent = fmt.$(gpTotal); el('wLPMultiple').textContent = fmt.x(lpTotal / lpCapital); const gpEquity = lpCapital * 0.2; // standard 20% co-invest proxy el('wGPMultiple').textContent = gpTotal > 0 ? fmt.x((gpTotal + gpEquity) / gpEquity) : '—'; // Plain English const lpProfitShare = totalProfit > 0 ? ((lpTotal - lpCapital) / totalProfit * 100) : 0; const gpProfitShare = totalProfit > 0 ? (gpTotal / totalProfit * 100) : 0; el('plainEnglish').innerHTML = ` On a ${fmt.$(lpCapital)} equity investment generating ${fmt.$(totalProfit)} in total profit, LPs receive their ${fmt.$(lpCapital)} capital back in full, then earn an ${fmt.pct(prefRate*100)} preferred return (${fmt.$(prefPaid)}) before the GP participates. After the ${el('wCatchup').value}% catch-up, remaining profits are split across ${t1Hurdle}%, ${t2Hurdle}%, and above IRR tiers. In total, LPs take ${fmt.pct(lpProfitShare)} of profits (${fmt.$(lpTotal - lpCapital)} net of capital) while the ${t.name} ${fmt.$(t.total)} ${fmt.$(t.lp)} ${fmt.$(t.gp)}
${Math.round(lpPct)}% / ${Math.round(gpPct)}%
`; }).join('') + ` Total ${fmt.$(totalDist)} ${fmt.$(lpTotal)} ${fmt.$(gpTotal)} `; // legend const profitOnly = totalDist - lpCapital; const lpProfit = lpTotal - lpCapital; el('lpLegend').textContent = `LP: ${fmt.$(lpTotal)} (${profitOnly > 0 ? Math.round(lpProfit/profitOnly*100) : 0}% of profits)`; el('gpLegend').textContent = `GP: ${fmt.$(gpTotal)} (${profitOnly > 0 ? Math.round(gpTotal/profitOnly*100) : 0}% of profits)`; } let pieChartInstance = null; function drawPieChart(lpProfit, gpProfit, lpCapital) { const canvas = el('pieChart'); const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const size = 180; canvas.width = size * dpr; canvas.height = size * dpr; canvas.style.width = size + 'px'; canvas.style.height = size + 'px'; ctx.scale(dpr, dpr); const cx = size / 2, cy = size / 2, r = size / 2 - 10; const total = lpCapital + lpProfit + gpProfit; if (total <= 0) return; const slices = [ { val: lpCapital, color: '#1e3868', label: 'LP Capital' }, { val: lpProfit, color: '#3b82f6', label: 'LP Profit' }, { val: gpProfit, color: '#1A9E8F', label: 'GP Promote' }, ].filter(s => s.val > 0); ctx.clearRect(0, 0, size, size); let startAngle = -Math.PI / 2; slices.forEach(s => { const sweep = (s.val / total) * Math.PI * 2; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, r, startAngle, startAngle + sweep); ctx.closePath(); ctx.fillStyle = s.color; ctx.fill(); ctx.strokeStyle = '#0A1628'; ctx.lineWidth = 2; ctx.stroke(); startAngle += sweep; }); // donut hole ctx.beginPath(); ctx.arc(cx, cy, r * 0.5, 0, Math.PI * 2); ctx.fillStyle = '#0f1e38'; ctx.fill(); // center text ctx.fillStyle = '#f0f4ff'; ctx.font = `700 16px Inter`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(fmt.$(total), cx, cy - 6); ctx.fillStyle = '#5a7299'; ctx.font = `400 10px Inter`; ctx.fillText('total return', cx, cy + 10); } // ── INIT ───────────────────────────────────────────────────────────────────── syncHoldDisplay(); syncMultipleDisplay(); syncLPDisplay(); syncBearDisplay(); syncBullDisplay(); syncWHoldDisplay(); syncCatchupDisplay(); calcIRR(); calcWaterfall(); window.addEventListener('resize', () => { const active = document.querySelector('.tab-panel.active').id; if (active === 'tab-irr') { const invest = +el('investAmount').value || 0; const multiple= +el('totalMultiple').value || 1; const hold = +el('holdPeriod').value || 1; drawDistChart(invest, multiple, hold); } });