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
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)}
`;
}).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);
}
});
${Math.round(lpPct)}% / ${Math.round(gpPct)}%