Add at least two assets to define correlations.
';
return;
}
const table = document.createElement('table');
table.className = 'min-w-full text-sm';
let headerHtml = '
Asset ';
assets.forEach(asset => {
headerHtml += `${asset.name} `;
});
headerHtml += '';
table.innerHTML = headerHtml;
const tbody = document.createElement('tbody');
assets.forEach((asset1, i) => {
const row = document.createElement('tr');
let rowHtml = `
${asset1.name} `;
assets.forEach((asset2, j) => {
if (i === j) {
rowHtml += `
`;
} else if (j < i) {
rowHtml += `
`;
} else {
// Default correlations for initial setup
let defaultValue = (i === 0 && j === 1) ? -0.2 : (i === 0 && j === 2) ? 0.6 : 0.1;
rowHtml += `
`;
}
});
rowHtml += '';
row.innerHTML = rowHtml;
tbody.appendChild(row);
});
table.appendChild(tbody);
correlationMatrixContainer.appendChild(table);
};
const addAsset = () => {
assets.push({ id: nextAssetId++, name: 'New Asset', return: 5.0, risk: 10.0 });
renderAssets();
};
const removeAsset = (id) => {
assets = assets.filter(asset => asset.id !== id);
renderAssets();
};
const updateAssetData = (e) => {
if (e.target.classList.contains('asset-input')) {
const id = parseInt(e.target.dataset.id);
const prop = e.target.dataset.prop;
const value = (prop === 'name') ? e.target.value : parseFloat(e.target.value);
const asset = assets.find(a => a.id === id);
if (asset) {
asset[prop] = value;
// If name changes, re-render matrix to update headers
if(prop === 'name') renderCorrelationMatrix();
}
}
};
const handleOptimizer = () => {
// 1. Get user inputs
const riskFreeRate = parseFloat(document.getElementById('riskFreeRate').value) / 100;
const numSimulations = parseInt(document.getElementById('simulations').value);
const returns = assets.map(a => a.return / 100);
const risks = assets.map(a => a.risk / 100);
const n = assets.length;
// Build correlation matrix
const correlations = Array(n).fill(0).map(() => Array(n).fill(0));
const inputs = correlationMatrixContainer.querySelectorAll('.correlation-input');
inputs.forEach(input => {
const i = parseInt(input.dataset.i);
const j = parseInt(input.dataset.j);
const value = parseFloat(input.value);
correlations[i][j] = value;
correlations[j][i] = value; // Symmetric matrix
});
for (let i = 0; i < n; i++) correlations[i][i] = 1.0;
// 2. Run Monte Carlo Simulation
portfolioSimulations = [];
for (let i = 0; i < numSimulations; i++) {
// Generate random weights that sum to 1
let weights = Array(n).fill(0).map(() => Math.random());
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
weights = weights.map(w => w / totalWeight);
// Calculate portfolio return
const portfolioReturn = weights.reduce((sum, w, j) => sum + w * returns[j], 0);
// Calculate portfolio variance
let portfolioVariance = 0;
for (let j = 0; j < n; j++) {
for (let k = 0; k < n; k++) {
portfolioVariance += weights[j] * weights[k] * risks[j] * risks[k] * correlations[j][k];
}
}
const portfolioRisk = Math.sqrt(portfolioVariance);
// Calculate Sharpe Ratio
const sharpeRatio = (portfolioReturn - riskFreeRate) / portfolioRisk;
portfolioSimulations.push({
return: portfolioReturn,
risk: portfolioRisk,
sharpe: sharpeRatio,
weights: weights,
});
}
// 3. Find the optimal portfolio (max Sharpe ratio)
const optimalPortfolio = portfolioSimulations.reduce((best, current) =>
(current.sharpe > best.sharpe) ? current : best, { sharpe: -Infinity }
);
// 4. Update UI
displayResults(optimalPortfolio, 'optimized');
updateChart(portfolioSimulations, optimalPortfolio);
updateRiskSliderPortfolio(); // Initial update for slider
// 5. Switch to dashboard and enable PDF button
switchTab('dashboard');
downloadPdfBtn.disabled = false;
};
const updateChart = (portfolios, optimalPortfolio) => {
const ctx = document.getElementById('efficientFrontierChart').getContext('2d');
if (chartInstance) {
chartInstance.destroy();
}
const dataPoints = portfolios.map(p => ({ x: p.risk * 100, y: p.return * 100 }));
chartInstance = new Chart(ctx, {
type: 'scatter',
data: {
datasets: [{
label: 'Simulated Portfolios',
data: dataPoints,
backgroundColor: 'rgba(59, 130, 246, 0.2)',
borderColor: 'rgba(59, 130, 246, 0.4)',
pointRadius: 2,
}, {
label: 'Optimal Portfolio (Max Sharpe)',
data: [{ x: optimalPortfolio.risk * 100, y: optimalPortfolio.return * 100 }],
backgroundColor: 'rgba(239, 68, 68, 1)',
pointRadius: 6,
pointStyle: 'star',
},{
label: 'Your Selected Portfolio',
data: [], // Will be updated by slider
backgroundColor: 'rgba(22, 163, 74, 1)',
pointRadius: 6,
pointStyle: 'triangle',
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
x: {
title: {
display: true,
text: 'Risk (Standard Deviation %)',
font: { size: 14 }
},
ticks: {
callback: value => value.toFixed(1) + '%'
}
},
y: {
title: {
display: true,
text: 'Expected Return %',
font: { size: 14 }
},
ticks: {
callback: value => value.toFixed(1) + '%'
}
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) label += ': ';
label += `Return: ${context.parsed.y.toFixed(2)}%, Risk: ${context.parsed.x.toFixed(2)}%`;
return label;
}
}
}
}
}
});
};
const displayResults = (portfolio, type) => {
if (!portfolio) return;
const containerId = type === 'optimized' ? 'optimized-portfolio-summary' : 'risk-selected-portfolio-summary';
const container = document.getElementById(containerId);
let weightsHtml = portfolio.weights.map((weight, i) => {
return `
${assets[i].name}
${(weight * 100).toFixed(1)}%
`;
}).join('');
container.innerHTML = `
Expected Return:
${(portfolio.return * 100).toFixed(2)}%
Portfolio Risk:
${(portfolio.risk * 100).toFixed(2)}%
Sharpe Ratio:
${portfolio.sharpe.toFixed(3)}
Asset Allocation:
${weightsHtml}
`;
};
const updateRiskSliderPortfolio = () => {
if(portfolioSimulations.length === 0) return;
const tolerance = parseInt(riskToleranceSlider.value);
riskToleranceValue.textContent = tolerance;
// Find min/max risk to map the slider value
const minRisk = Math.min(...portfolioSimulations.map(p => p.risk));
const maxRisk = Math.max(...portfolioSimulations.map(p => p.risk));
const targetRisk = minRisk + (tolerance / 100) * (maxRisk - minRisk);
// Find the portfolio on the efficient frontier closest to the target risk
// First filter for portfolios on the upper edge (the frontier)
const frontierPortfolios = portfolioSimulations.reduce((acc, p) => {
const existing = acc.find(item => Math.abs(item.risk - p.risk) < 0.001);
if (!existing || p.return > existing.return) {
return [...acc.filter(item => Math.abs(item.risk - p.risk) >= 0.001), p];
}
return acc;
}, []);
// Now find the one closest to our target risk from the frontier
const selectedPortfolio = frontierPortfolios.reduce((closest, current) => {
const closestDiff = Math.abs(closest.risk - targetRisk);
const currentDiff = Math.abs(current.risk - targetRisk);
return currentDiff < closestDiff ? current : closest;
});
displayResults(selectedPortfolio, 'risk-selected');
if (chartInstance) {
chartInstance.data.datasets[2].data = [{ x: selectedPortfolio.risk * 100, y: selectedPortfolio.return * 100 }];
chartInstance.update();
}
};
const handlePdfDownload = () => {
const pdfContent = document.getElementById('pdf-content');
const downloadButton = document.getElementById('download-pdf-btn');
// Hide button during capture
downloadButton.style.visibility = 'hidden';
html2canvas(pdfContent, { scale: 2, backgroundColor: '#ffffff' }).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const { jsPDF } = window.jspdf;
// Use page dimensions of A4 paper
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'pt',
format: 'a4'
});
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const ratio = canvasWidth / canvasHeight;
const imgWidth = pdfWidth - 40; // with some margin
const imgHeight = imgWidth / ratio;
pdf.setFontSize(18);
pdf.text("Risk-Return Profile Optimization Report", 20, 30);
pdf.addImage(imgData, 'PNG', 20, 50, imgWidth, imgHeight);
pdf.save('Risk-Return-Optimizer-Report.pdf');
// Show button again
downloadButton.style.visibility = 'visible';
});
};
// --- EVENT LISTENERS ---
window.switchTab = switchTab; // Make it globally accessible for onclick
window.navigateTabs = navigateTabs;
addAssetBtn.addEventListener('click', addAsset);
assetTableBody.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-asset-btn')) {
removeAsset(parseInt(e.target.dataset.id));
}
});
assetTableBody.addEventListener('input', updateAssetData);
optimizeBtn.addEventListener('click', handleOptimizer);
downloadPdfBtn.addEventListener('click', handlePdfDownload);
riskToleranceSlider.addEventListener('input', updateRiskSliderPortfolio);
// --- INITIALIZATION ---
renderAssets();
updateNavButtons();
switchTab('dashboard'); // Start on the dashboard
});