Institutional Fixed-Income Portfolio Analyzer (Conceptual Simulator)
Portfolio Setup
$
If you input allocations as %, this value helps convert to $ for some reports. If holdings are input with market values, this will be calculated.
Fixed-Income Holdings Input
Add individual fixed-income securities or aggregated categories. Duration and Convexity are direct inputs for this conceptual tool.
Total Value/Allocation: $0 (0%)
Portfolio Analysis & Summary
Portfolio summary metrics will appear here.
Allocation by Credit Quality
Allocation by Maturity Bucket
Interest Rate Sensitivity Analysis
bps
E.g., 100 for +1.00%, -50 for -0.50% shift.
Estimated Impact of Rate Shift
Enter holdings and click 'Analyze Portfolio'.
"; creditChartContainer.innerHTML = 'Credit Allocation Chart
'; maturityChartContainer.innerHTML = 'Maturity Allocation Chart
'; return; } const res = ifipa_model.analysisResults; summarySection.innerHTML = `Portfolio Aggregate Characteristics
Total Market Value: ${ifipa_formatCurrency(res.totalMarketValue)}
Weighted Avg. YTM: ${ifipa_formatPercent(res.waytm)}
Weighted Avg. Coupon: ${ifipa_formatPercent(res.wac)}
Weighted Avg. Maturity: ${ifipa_formatYears(res.wam)}
Portfolio Duration: ${ifipa_formatYears(res.portfolioDuration)}
Portfolio Convexity (Optional Input): ${res.portfolioConvexity.toFixed(2)}
Avg. Credit Quality (Conceptual): ${res.portfolioAvgCreditRatingString} (Score: ${res.portfolioAvgCreditScore.toFixed(1)})
`;
// Placeholder for charts - simple text for now, SVG can be complex
ifipa_renderAllocationBarChart(ifipa_collateByCredit(), 'Credit Quality Allocation', creditChartContainer, (val) => val);
ifipa_renderAllocationBarChart(ifipa_collateByMaturity(), 'Maturity Allocation', maturityChartContainer, (val) => val + ' Yrs');
}
function ifipa_collateByCredit() {
const creditAlloc = {};
const totalMV = ifipa_model.analysisResults.totalMarketValue;
if(totalMV === 0) return [];
ifipa_model.holdings.forEach(h => {
const rating = h.creditRating || 'Unrated';
const weight = h.inputType === 'value' ? (h.marketValue / totalMV) * 100 : h.allocationPercent;
creditAlloc[rating] = (creditAlloc[rating] || 0) + weight;
});
return Object.entries(creditAlloc).map(([name, value]) => ({name, value, color: ifipa_getColorForCredit(name)}));
}
function ifipa_getColorForCredit(rating){
const score = ifipa_creditRatingMap[rating] || 16;
if(score <=2.5) return '#6EE7B7'; // AAA-AA
if(score <= 4.5) return '#A5B4FC'; // A
if(score <= 6.5) return '#FBBF24'; // BBB
if(score <= 10.5) return '#F87171'; // BB-B
return '#9CA3AF'; // CCC and below or Unrated
}
function ifipa_collateByMaturity() {
const maturityBuckets = { "0-1":0, "1-3":0, "3-5":0, "5-10":0, "10+":0 };
const totalMV = ifipa_model.analysisResults.totalMarketValue;
if(totalMV === 0) return [];
ifipa_model.holdings.forEach(h => {
const mat = h.yearsToMaturity || 0;
const weight = h.inputType === 'value' ? (h.marketValue / totalMV) * 100 : h.allocationPercent;
if (mat <= 1) maturityBuckets["0-1"] += weight;
else if (mat <= 3) maturityBuckets["1-3"] += weight;
else if (mat <= 5) maturityBuckets["3-5"] += weight;
else if (mat <= 10) maturityBuckets["5-10"] += weight;
else maturityBuckets["10+"] += weight;
});
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EC4899', '#8B5CF6'];
return Object.entries(maturityBuckets).map(([name,value], i) => ({name, value, color:colors[i % colors.length]}));
}
function ifipa_renderAllocationBarChart(data, title, container, labelFormatter = (val) => val) {
container.innerHTML = ''; // Clear
const svgWidth = Math.min(400, (ifipa_getEl('ifipaContainer_main').offsetWidth || 400)/2 - 40);
const svgHeight = 200;
const m = {top: 20, right: 10, bottom: 50, left: 40};
const w = svgWidth - m.left - m.right;
const h = svgHeight - m.top - m.bottom;
const filteredData = data.filter(d => d.value > 0.01);
if(filteredData.length === 0) { container.innerHTML = `No data for ${title} chart.
`; return;} const yMax = Math.max(...filteredData.map(d=>d.value), 0); const barWidth = w / filteredData.length * 0.8; const barSpacing = w / filteredData.length * 0.2; const yScale = val => h - ((val / (yMax === 0 ? 1: yMax)) * h); let chartHTML = ``; container.innerHTML = chartHTML; } function ifipa_displaySensitivityResults() { const resultsSection = ifipa_getEl('ifipa_sensitivityResultsSection'); if (!resultsSection) { console.error("Sensitivity results display element missing."); return; } if (!ifipa_model.sensitivityResults) { resultsSection.innerHTML = "Run a scenario to see impact.
"; resultsSection.style.display = 'none'; return; } const res = ifipa_model.sensitivityResults; const origVal = res.originalValue; const durChangeClass = res.valueChangeDur >=0 ? 'ifipa_positive_change' : 'ifipa_negative_change'; const durConvChangeClass = res.valueChangeDurConv >=0 ? 'ifipa_positive_change' : 'ifipa_negative_change'; resultsSection.innerHTML = `Estimated Impact of ${res.rateShiftBps > 0 ? '+' : ''}${res.rateShiftBps} bps Rate Shift:
Original Portfolio Value:${ifipa_formatCurrency(origVal)}
Using Duration Only:
Est. Value Change:${ifipa_formatCurrency(res.valueChangeDur)} (${ifipa_formatPercent(res.valueChangePercentDur)})
Est. New Portfolio Value:${ifipa_formatCurrency(res.newValueDur)}
${ifipa_model.analysisResults.portfolioConvexity !== 0 ? `
Using Duration & Convexity:
Est. Value Change:${ifipa_formatCurrency(res.valueChangeDurConv)} (${ifipa_formatPercent(res.valueChangePercentDurConv)})
Est. New Portfolio Value:${ifipa_formatCurrency(res.newValueDurConv)}
` : 'Portfolio convexity is zero or not provided for a more precise estimate.
'} `; resultsSection.style.display = 'block'; } // --- PDF Generation --- function ifipa_downloadPDF() { if (!ifipa_model.analysisResults) { alert("Please analyze portfolio first."); return; } if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') { alert('Core PDF library (jsPDF) is not loaded.'); console.error('jsPDF library not found.'); return; } const { jsPDF: JSPDF } = window.jspdf; const doc = new JSPDF(); if (typeof doc.autoTable !== 'function') { alert('PDF Table plugin (jsPDF-AutoTable) not loaded correctly. Tables in PDF may be missing.'); console.error('doc.autoTable is not a function.'); } let y = 15; const m = 15; const cw = doc.internal.pageSize.getWidth() - (2 * m); const res = ifipa_model.analysisResults; const inputs = ifipa_model; function addLine(text, size, style = 'normal', indent = 0, spacing = 2.5) { if (y > 275) { doc.addPage(); y = m; } doc.setFontSize(size); doc.setFont(undefined, style); const lines = doc.splitTextToSize(text, cw - indent); doc.text(lines, m + indent, y); y += (lines.length * (size * 0.35)) + spacing; } addLine(`Institutional Fixed-Income Portfolio Analysis: ${inputs.portfolioName || 'N/A'}`, 16, 'bold', 0, 5); addLine(`Report Date: ${new Date().toLocaleDateString()}`, 9, 'italic', 0, 5); addLine("Portfolio Holdings Summary:", 12, 'bold', 0, 3); if(typeof doc.autoTable === 'function' && res.holdingsSnapshot.length > 0) { const head = [['Holding', 'MV ($) / Alloc(%)', 'Coupon(%)', 'Maturity(Yrs)', 'YTM(%)', 'Duration(Yrs)', 'Convexity', 'Credit']]; const body = res.holdingsSnapshot.map(h => [ h.name, h.inputType === 'value' ? ifipa_formatCurrency(h.marketValue) : ifipa_formatPercent(h.allocationPercent,1), h.couponRate.toFixed(2), h.yearsToMaturity.toFixed(1), h.ytm.toFixed(2), h.duration.toFixed(1), (h.convexity || 0).toFixed(2), h.creditRating ]); doc.autoTable({startY: y, head: head, body: body, theme: 'grid', headStyles:{fillColor:[55,65,81], fontSize:8}, styles:{fontSize:7.5, cellPadding:1, halign:'right'}, columnStyles:{0:{halign:'left',fontStyle:'bold'}}}); y = doc.lastAutoTable.finalY + 5; } else { addLine("Holdings table cannot be generated (plugin/data issue).", 8, 'italic'); y+=5; } addLine("Aggregate Portfolio Characteristics:", 12, 'bold', 0, 3); let summaryData = [ ["Total Market Value:", ifipa_formatCurrency(res.totalMarketValue)], ["Weighted Avg. YTM:", ifipa_formatPercent(res.waytm)], ["Weighted Avg. Coupon:", ifipa_formatPercent(res.wac)], ["Weighted Avg. Maturity:", ifipa_formatYears(res.wam)], ["Portfolio Duration:", ifipa_formatYears(res.portfolioDuration)], ["Portfolio Convexity:", res.portfolioConvexity.toFixed(2)], ["Avg. Credit Quality (Conceptual):", `${res.portfolioAvgCreditRatingString} (Score: ${res.portfolioAvgCreditScore.toFixed(1)})`], ]; if (typeof doc.autoTable === 'function') { doc.autoTable({startY: y, body: summaryData, theme: 'plain', styles:{fontSize:9, cellPadding:1.2}, columnStyles:{0:{fontStyle:'bold'}}}); y = doc.lastAutoTable.finalY + 6; } else { summaryData.forEach(row => addLine(`${row[0]} ${row[1]}`, 9)); y+=5; } if (ifipa_model.sensitivityResults) { const sensRes = ifipa_model.sensitivityResults; addLine(`Interest Rate Sensitivity (Shift: ${sensRes.rateShiftBps > 0 ? '+' : ''}${sensRes.rateShiftBps} bps):`, 12, 'bold', 0, 4); let sensData = [ ["Metric", "Value"], ["Original Portfolio Value:", ifipa_formatCurrency(sensRes.originalValue)], ["Est. Change (Duration Only):", `${ifipa_formatCurrency(sensRes.valueChangeDur)} (${ifipa_formatPercent(sensRes.valueChangePercentDur)})`], ["Est. New Value (Duration Only):", ifipa_formatCurrency(sensRes.newValueDur)], ]; if(res.portfolioConvexity !== 0) { sensData.push(["Est. Change (Duration & Convexity):", `${ifipa_formatCurrency(sensRes.valueChangeDurConv)} (${ifipa_formatPercent(sensRes.valueChangePercentDurConv)})`]); sensData.push(["Est. New Value (Duration & Convexity):", ifipa_formatCurrency(sensRes.newValueDurConv)]); } if (typeof doc.autoTable === 'function') { doc.autoTable({startY: y, body: sensData, theme: 'plain', styles:{fontSize:9, cellPadding:1.2}, columnStyles:{0:{fontStyle:'bold'}}}); y = doc.lastAutoTable.finalY + 7; } else { sensData.forEach(row => addLine(`${row[0]} ${row[1]}`, 9)); y+=5;} } addLine("Note: This is a conceptual simulator based on user-defined inputs. Duration and Convexity for individual holdings are user inputs. Calculations are approximate. Not investment advice.", 7, 'italic'); doc.save(`${(inputs.portfolioName || 'FixedIncomePortfolio').replace(/\s+/g, '_')}_Analysis.pdf`); }