The portfolio's expected annual return is ${formatPercent(metrics.expectedReturn)}, with a calculated volatility (risk) of ${formatPercent(metrics.volatility)}. This results in a Sharpe Ratio of ${formatNumber(metrics.sharpeRatio)}, which measures risk-adjusted return. A higher Sharpe Ratio (typically > 1.0) is considered good.
Key Observations
- Highest Return Asset: ${highReturnAsset.name} (${formatPercent(highReturnAsset.expectedReturn)})
- Highest Risk Asset: ${highRiskAsset.name} (${formatPercent(highRiskAsset.volatility)})
- The overall portfolio risk (${formatPercent(metrics.volatility)}) is lower than that of its riskiest component due to diversification.
`;
};
// CALCULATION LOGIC
const calculateMetrics = () => {
if (portfolioData.length === 0) return null;
const totalValue = portfolioData.reduce((sum, a) => sum + a.value, 0);
if (totalValue === 0) return null;
const assets = portfolioData.map(a => ({...a, weight: a.value / totalValue }));
const expectedReturn = assets.reduce((sum, a) => sum + a.weight * a.expectedReturn, 0);
// Portfolio Volatility Calculation
let variance = 0;
for (let i = 0; i < assets.length; i++) {
// Add variance part
variance += Math.pow(assets[i].weight, 2) * Math.pow(assets[i].volatility / 100, 2);
for (let j = 0; j < assets.length; j++) {
if (i !== j) {
// Add covariance part
variance += assets[i].weight * assets[j].weight * (assets[i].volatility / 100) * (assets[j].volatility / 100) * ASSET_CORRELATION;
}
}
}
const volatility = Math.sqrt(variance) * 100;
const sharpeRatio = (expectedReturn - RISK_FREE_RATE) / volatility;
return { totalValue, expectedReturn, volatility, sharpeRatio, assets };
};
// EVENT HANDLERS
const handleRecalculate = () => {
const metrics = calculateMetrics();
if(metrics) {
renderKPIs(metrics);
renderCharts(metrics);
renderAnalysis(metrics);
switchTab('dashboard');
} else {
alert("No data available to calculate. Please add asset data with a total value greater than zero.");
}
};
const handleAddRow = () => {
const newId = portfolioData.length > 0 ? Math.max(...portfolioData.map(p => p.id)) + 1 : 1;
portfolioData.push({ id: newId, name: 'New Asset', value: 1000, expectedReturn: 5.0, volatility: 10.0 });
renderDataTable();
};
dataTableBody.addEventListener('change', (e) => {
if(e.target.matches('.data-input')) {
const id = parseInt(e.target.dataset.id);
const key = e.target.dataset.key;
const value = e.target.type === 'number' ? parseFloat(e.target.value) : e.target.value;
const assetIndex = portfolioData.findIndex(p => p.id === id);
if (assetIndex !== -1) {
portfolioData[assetIndex][key] = value;
}
}
});
dataTableBody.addEventListener('click', (e) => {
if (e.target.matches('button')) {
const id = parseInt(e.target.dataset.id);
portfolioData = portfolioData.filter(p => p.id !== id);
renderDataTable();
}
});
const generatePDF = () => {
const metrics = calculateMetrics();
if (!metrics) {
alert("No data to generate PDF.");
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const pageWidth = doc.internal.pageSize.width;
const margin = 15;
let y = margin;
doc.setFont('helvetica', 'bold');
doc.setFontSize(22);
doc.text('Investment Risk Analytics Report', pageWidth / 2, y, { align: 'center' });
y += 10;
doc.setFontSize(12);
doc.setFont('helvetica', 'normal');
doc.text(`Report Generated: ${new Date('2025-09-19T14:40:00Z').toLocaleDateString('en-US')}`, pageWidth / 2, y, { align: 'center' });
y += 15;
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('Portfolio Summary', margin, y);
y += 8;
const kpiText = [
`Total Portfolio Value: ${formatCurrency(metrics.totalValue)}`,
`Expected Return: ${formatPercent(metrics.expectedReturn)}`,
`Portfolio Volatility (Risk): ${formatPercent(metrics.volatility)}`,
`Sharpe Ratio (Risk-Adjusted Return): ${formatNumber(metrics.sharpeRatio)}`
];
doc.setFontSize(12);
doc.setFont('helvetica', 'normal');
kpiText.forEach(text => {
doc.text(text, margin, y);
y+= 7;
});
y += 10;
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('Portfolio Visualizations', margin, y);
y+= 5;
try {
const compositionImg = charts.composition.toBase64Image();
const volatilityImg = charts.volatility.toBase64Image();
doc.addImage(compositionImg, 'PNG', margin, y, 80, 80);
doc.addImage(volatilityImg, 'PNG', margin + 90, y, 95, 80);
} catch(e) {
console.error("Error adding charts to PDF:", e);
doc.text("Could not render charts.", margin, y);
}
doc.save('Investment_Risk_Report.pdf');
};
// APP NAVIGATION
const switchTab = (tabName) => {
if (!tabs.includes(tabName)) return;
currentTab = tabName;
Object.values(tabContent).forEach(content => content.classList.add('hidden'));
tabContent[tabName].classList.remove('hidden');
Object.values(tabButtons).forEach(button => button.classList.remove('active'));
tabButtons[tabName].classList.add('active');
updateNavButtons();
};
const updateNavButtons = () => {
const currentIndex = tabs.indexOf(currentTab);
prevBtn.disabled = currentIndex === 0;
nextBtn.disabled = currentIndex === tabs.length - 1;
};
const navigateTabs = (direction) => {
const currentIndex = tabs.indexOf(currentTab);
let newIndex = direction === 'next'
? Math.min(currentIndex + 1, tabs.length - 1)
: Math.max(currentIndex - 1, 0);
switchTab(tabs[newIndex]);
};
// INITIALIZATION
document.getElementById('recalculate-btn').addEventListener('click', handleRecalculate);
document.getElementById('add-row-btn').addEventListener('click', handleAddRow);
document.getElementById('download-pdf-btn-dashboard').addEventListener('click', generatePDF);
window.app = {
switchTab,
navigateTabs,
};
renderDataTable();
handleRecalculate();
updateNavButtons();
});