Achieved Portfolio Return: ${(currentPortfolioReturn * 100).toFixed(2)}%
Estimated Portfolio Volatility: ${(currentPortfolioVolatility * 100).toFixed(2)}%
(Note: This is a heuristic optimization. For precise financial modeling, a dedicated optimization engine is recommended.)
`;
optimizedAllocationsTableBody.innerHTML = '';
assets.forEach((asset, index) => {
const optimizedWeightPercent = (optimizedWeights[index] * 100).toFixed(2);
const row = optimizedAllocationsTableBody.insertRow();
row.innerHTML = `
${asset.name} |
${optimizedWeightPercent}% |
`;
});
}
optimizePortfolioButton.addEventListener('click', optimizePortfolio);
// --- PDF Download Functionality ---
downloadPdfButton.addEventListener('click', function() {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
doc.setFontSize(22);
doc.text(toolTitle.textContent, doc.internal.pageSize.getWidth() / 2, 20, { align: 'center' });
// --- Input Portfolio Data Section ---
doc.setFontSize(16);
doc.text("1. Input Portfolio Data", 15, 40);
let yPos = 50;
const assetInputHeaders = ['Asset Name', 'Current Value ($)', 'Expected Return (%)', 'Volatility (%)'];
const assetInputRows = [];
const assetGroups = assetInputsContainer.querySelectorAll('.asset-input-group');
assetGroups.forEach(group => {
const id = group.dataset.assetId;
const name = document.getElementById(`assetName${id}`).value.trim();
const value = parseFloat(document.getElementById(`assetValue${id}`).value);
const expectedReturn = parseFloat(document.getElementById(`expectedReturn${id}`).value);
const volatility = parseFloat(document.getElementById(`volatility${id}`).value);
assetInputRows.push([
name,
isNaN(value) ? '' : `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
isNaN(expectedReturn) ? '' : `${expectedReturn.toFixed(2)}%`,
isNaN(volatility) ? '' : `${volatility.toFixed(2)}%`
]);
});
doc.autoTable({
startY: yPos,
head: [assetInputHeaders],
body: assetInputRows,
theme: 'grid',
styles: { fontSize: 9, cellPadding: 2, overflow: 'linebreak' },
headStyles: { fillColor: [52, 152, 219], textColor: [255, 255, 255] },
alternateRowStyles: { fillColor: [236, 240, 241] },
margin: { top: 5, right: 15, bottom: 5, left: 15 }
});
yPos = doc.autoTable.previous.finalY + 15;
// --- Portfolio Analysis Section ---
doc.setFontSize(16);
doc.text("2. Portfolio Analysis", 15, yPos);
yPos += 10;
const totalValue = totalPortfolioValueSpan.textContent;
const portfolioReturn = calculatedPortfolioReturnSpan.textContent;
const portfolioVolatility = calculatedPortfolioVolatilitySpan.textContent;
doc.setFontSize(12);
doc.text(`Total Portfolio Value: ${totalValue}`, 15, yPos);
doc.text(`Calculated Portfolio Return: ${portfolioReturn}`, 15, yPos + 7);
doc.text(`Calculated Portfolio Volatility (Risk): ${portfolioVolatility}`, 15, yPos + 14);
yPos += 25;
doc.setFontSize(14);
doc.text("Asset Contributions:", 15, yPos);
yPos += 7;
const analysisHeaders = ['Asset Name', 'Current Value ($)', 'Weight (%)', 'Expected Return (%)', 'Volatility (%)'];
const analysisRows = [];
const tableRows = assetContributionsTableBody.querySelectorAll('tr');
tableRows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 1) { // Check if it's a data row, not "No assets entered"
analysisRows.push(Array.from(cells).map(cell => cell.textContent.trim()));
}
});
if (analysisRows.length === 0) {
doc.text("No asset data available for analysis.", 15, yPos + 5);
yPos += 15;
} else {
doc.autoTable({
startY: yPos,
head: [analysisHeaders],
body: analysisRows,
theme: 'grid',
styles: { fontSize: 9, cellPadding: 2, overflow: 'linebreak' },
headStyles: { fillColor: [52, 152, 219], textColor: [255, 255, 255] },
alternateRowStyles: { fillColor: [236, 240, 241] },
margin: { top: 5, right: 15, bottom: 5, left: 15 }
});
yPos = doc.autoTable.previous.finalY + 15;
}
// --- Portfolio Optimization Section ---
doc.setFontSize(16);
doc.text("3. Portfolio Optimization", 15, yPos);
yPos += 10;
const targetRet = targetReturnInput.value;
doc.setFontSize(12);
doc.text(`Target Annual Return: ${targetRet}%`, 15, yPos);
yPos += 10;
const optResultsText = optimizationResultsDiv.style.display === 'block' ? optimizationResultsDiv.innerText : 'Optimization not performed or no results.';
doc.setFontSize(10);
doc.text(optResultsText, 15, yPos, { maxWidth: doc.internal.pageSize.getWidth() - 30 });
yPos += doc.getTextDimensions(optResultsText, { fontSize: 10, maxWidth: doc.internal.pageSize.getWidth() - 30 }).h + 10;
doc.setFontSize(14);
doc.text("Optimized Asset Allocations:", 15, yPos);
yPos += 7;
const optimizedHeaders = ['Asset Name', 'Optimized Weight (%)'];
const optimizedRows = [];
const optimizedTableRows = optimizedAllocationsTableBody.querySelectorAll('tr');
optimizedTableRows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 1) {
optimizedRows.push(Array.from(cells).map(cell => cell.textContent.trim()));
}
});
if (optimizedRows.length === 0) {
doc.text("No optimized allocation data available.", 15, yPos + 5);
} else {
doc.autoTable({
startY: yPos,
head: [optimizedHeaders],
body: optimizedRows,
theme: 'grid',
styles: { fontSize: 9, cellPadding: 2, overflow: 'linebreak' },
headStyles: { fillColor: [52, 152, 219], textColor: [255, 255, 255] },
alternateRowStyles: { fillColor: [236, 240, 241] },
margin: { top: 5, right: 15, bottom: 5, left: 15 }
});
}
// Add page numbers
let totalPages = doc.internal.getNumberOfPages();
for (let i = 1; i <= totalPages; i++) {
doc.setPage(i);
doc.setFontSize(10);
doc.text("Page " + i + " of " + totalPages, doc.internal.pageSize.getWidth() / 2, doc.internal.pageSize.height - 10, { align: 'center' });
}
doc.save('Portfolio_Management_Optimization.pdf');
});
// Initial calculation on load for default values
calculatePortfolioMetrics();
});