Fund of Funds Portfolio Allocation Optimizer

Fund of Funds Portfolio Allocation Optimizer

Fund Information

Enter the details for each fund in your portfolio. You can add or remove funds as needed.

Correlation Matrix (values between -1.0 and 1.0)

Fund

${fundName}: ${(weight * 100).toFixed(2)}%

`; }); gmvpPortfolioReturnSpan.textContent = `${gmvpReturn.toFixed(2)}%`; gmvpPortfolioRiskSpan.textContent = `${minRisk.toFixed(2)}%`; gmvpResultsDiv.classList.remove('hidden'); gmvpCalculated = true; // Mark as calculated showTab(currentTab); // Re-evaluate PDF button visibility }); // --- Input Validation --- function validateFundInputs() { let isValid = true; if (fundData.length === 0) { alertUser('Please add at least one fund.'); return false; // Exit immediately if no funds } fundData.forEach(fund => { const nameInput = document.getElementById(`fund-name-${fund.id}`); const returnInput = document.getElementById(`fund-return-${fund.id}`); const stdDevInput = document.getElementById(`fund-stddev-${fund.id}`); if (!nameInput || !nameInput.value.trim()) { alertUser('Fund Name cannot be empty for all funds.'); if (nameInput) nameInput.focus(); isValid = false; } if (!returnInput || isNaN(parseFloat(returnInput.value)) || returnInput.value.trim() === '') { alertUser(`Expected Return for ${fund.name} must be a number.`); if (returnInput) returnInput.focus(); isValid = false; } if (!stdDevInput || isNaN(parseFloat(stdDevInput.value)) || stdDevInput.value.trim() === '') { alertUser(`Standard Deviation for ${fund.name} must be a number.`); if (stdDevInput) stdDevInput.focus(); isValid = false; } }); // Validate correlation matrix values are between -1 and 1 const numFunds = fundData.length; for (let i = 0; i < numFunds; i++) { for (let j = 0; j < numFunds; j++) { const correlationId = `corr-${fundData[i].id}-${fundData[j].id}`; const input = document.getElementById(correlationId); if (input) { const val = parseFloat(input.value); if (isNaN(val) || val < -1 || val > 1) { alertUser(`Correlation for ${fundData[i].name} and ${fundData[j].name} must be between -1.0 and 1.0.`); input.focus(); isValid = false; // No return here, allow all validations to run and collect all errors } } } } return isValid; } // Function to update fundData array from input fields on the page function updateFundDataFromInputs() { fundData.forEach(fund => { const nameInput = document.getElementById(`fund-name-${fund.id}`); const returnInput = document.getElementById(`fund-return-${fund.id}`); const stdDevInput = document.getElementById(`fund-stddev-${fund.id}`); if (nameInput) fund.name = nameInput.value.trim(); if (returnInput) fund.expectedReturn = parseFloat(returnInput.value || 0); if (stdDevInput) fund.stdDev = parseFloat(stdDevInput.value || 0); }); } // --- PDF Generation Function --- downloadPdfButton.addEventListener('click', async function() { // Ensure data is up-to-date before generating PDF updateFundDataFromInputs(); const pdfTitle = 'Fund of Funds Portfolio Allocation Report'; let pdfHtmlContent = `

${pdfTitle}

`; // 1. Fund Details Table if (fundData.length > 0) { pdfHtmlContent += `

Fund Information

`; pdfHtmlContent += ``; fundData.forEach(fund => { pdfHtmlContent += ``; }); pdfHtmlContent += `
Fund NameExpected Return (%)Standard Deviation (%)
${fund.name}${fund.expectedReturn.toFixed(2)}${fund.stdDev.toFixed(2)}
`; } // 2. Correlation Matrix Table if (fundData.length > 1) { const correlationMatrix = getCorrelationMatrix(); pdfHtmlContent += `

Correlation Matrix

`; pdfHtmlContent += ``; fundData.forEach(fund => { pdfHtmlContent += ``; }); pdfHtmlContent += ``; for (let i = 0; i < fundData.length; i++) { pdfHtmlContent += ``; for (let j = 0; j < fundData.length; j++) { pdfHtmlContent += ``; } pdfHtmlContent += ``; } pdfHtmlContent += `
Fund${fund.name}
${fundData[i].name}${correlationMatrix[i][j].toFixed(2)}
`; } // 3. Custom Portfolio Results if (customPortfolioCalculated && !customPortfolioResultsDiv.classList.contains('hidden')) { pdfHtmlContent += `

Your Custom Portfolio Metrics

`; pdfHtmlContent += `

Expected Portfolio Return: ${customPortfolioReturnSpan.textContent}

`; pdfHtmlContent += `

Portfolio Standard Deviation (Risk): ${customPortfolioRiskSpan.textContent}

`; } // 4. GMVP Results if (gmvpCalculated && !gmvpResultsDiv.classList.contains('hidden')) { pdfHtmlContent += `

Global Minimum Variance Portfolio (GMVP)

`; pdfHtmlContent += `

Optimal Weights:

`; const gmvpWeightParagraphs = gmvpWeightsDisplayDiv.querySelectorAll('p'); gmvpWeightParagraphs.forEach(p => { pdfHtmlContent += `

${p.innerHTML}

`; }); pdfHtmlContent += `

Expected Portfolio Return: ${gmvpPortfolioReturnSpan.textContent}

`; pdfHtmlContent += `

Portfolio Standard Deviation (Risk): ${gmvpPortfolioRiskSpan.textContent}

`; } // Set the HTML content to the hidden container pdfRenderContainer.innerHTML = pdfHtmlContent; try { // Create a canvas from the hidden container const canvas = await html2canvas(pdfRenderContainer, { useCORS: true, scale: 2, // Increase scale for better resolution logging: false // Disable logging for cleaner console }); const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF('p', 'mm', 'a4'); // 'p' for portrait, 'mm' for millimeters, 'a4' size const imgWidth = 210; // A4 width in mm const pageHeight = 297; // A4 height in mm const imgHeight = canvas.height * imgWidth / canvas.width; let heightLeft = imgHeight; let position = 0; // Add image with calculated height for multi-page support pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pageHeight; while (heightLeft >= 0) { position = heightLeft - imgHeight; pdf.addPage(); pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pageHeight; } pdf.save('portfolio_allocation_report.pdf'); } catch (error) { console.error('Error generating PDF:', error); alertUser('Failed to generate PDF. Please try again or check console for errors.'); } finally { // Clear the content of the hidden container pdfRenderContainer.innerHTML = ''; } }); // --- Custom Alert Function (instead of alert()) --- function alertUser(message) { // Implement a simple modal or message box instead of alert() // For now, using a simple console log for demonstration in browser console.warn("Alert (User-friendly):", message); // A basic in-page message display: let msgBox = document.getElementById('user-message-box'); if (!msgBox) { msgBox = document.createElement('div'); msgBox.id = 'user-message-box'; msgBox.className = 'fixed inset-x-0 top-0 flex items-center justify-center p-4 z-50'; msgBox.innerHTML = ` `; document.body.appendChild(msgBox); document.getElementById('close-user-message').addEventListener('click', () => { msgBox.classList.add('hidden'); }); } document.getElementById('user-message-text').textContent = message; msgBox.classList.remove('hidden'); setTimeout(() => { msgBox.classList.add('hidden'); }, 5000); // Hide after 5 seconds } // Initial rendering renderFundsInputs(); updateCorrelationMatrix(); updateCustomWeightsInputs(); });
Scroll to Top