To achieve the optimized allocation, you would need to adjust your portfolio. Detailed rebalancing would involve selling specific current assets and buying into assets that align with the recommended categories.
';
rebalancingHtml += '
This tool recommends target allocations for broad asset categories. You will need to determine how your specific holdings fit these categories and rebalance accordingly.
';
if(this.userAssets.length > 0 && this.totalCurrentValue > 0) {
rebalancingHtml += '
Consider the overall value of your current holdings when reallocating towards these target percentages for the broad asset categories.
';
}
this.optimizedResultsContainerEl.innerHTML += `
${rebalancingHtml}
`;
},
showTab: function(tabIndex) {
// Ensure tabs and tabButtons NodeLists are populated and have length
if (!this.tabs || this.tabs.length === 0 || !this.tabButtons || this.tabButtons.length === 0) {
console.error("DAA Optimizer Error: Tabs or tabButtons not initialized correctly in showTab.");
return;
}
if (tabIndex < 0 || tabIndex >= this.tabs.length) {
console.warn("DAA Optimizer Warning: Invalid tabIndex in showTab:", tabIndex);
return;
}
// Validations before moving tabs
if (this.currentTab === 1 && tabIndex > 1) {
if (this.userAssets.length === 0 || this.userAssets.every(asset => (asset.value || 0) === 0 && !asset.name.trim())) {
alert("Please add at least one asset with a name or current value before proceeding from 'Current Portfolio' tab.");
return;
}
if (this.userAssets.some(asset => !asset.name.trim() && (asset.value || 0) > 0)) {
alert("Please ensure all asset classes with values have a name.");
return;
}
}
this.tabs.forEach(tab => tab.classList.remove('active'));
this.tabButtons.forEach(button => button.classList.remove('active'));
if (this.tabs[tabIndex]) this.tabs[tabIndex].classList.add('active');
if (this.tabButtons[tabIndex]) this.tabButtons[tabIndex].classList.add('active');
this.currentTab = tabIndex;
this.updateNavButtons();
if (tabIndex === 3) {
if (this.optimizedResultsContainerEl) this.renderOptimizedResults(); // Check element
if (this.pdfButtonContainerEl) this.pdfButtonContainerEl.style.display = 'block';
} else {
if (this.pdfButtonContainerEl) this.pdfButtonContainerEl.style.display = 'none';
if (this.optimizationRationaleEl) this.optimizationRationaleEl.style.display = 'none';
}
},
navigateTabs: function(direction) {
const newTabIndex = this.currentTab + direction;
this.showTab(newTabIndex);
},
updateNavButtons: function() {
if (this.prevButtonEl) {
this.prevButtonEl.style.display = (this.currentTab === 0) ? 'none' : 'inline-block';
}
if (this.nextButtonEl) {
this.nextButtonEl.style.display = (this.currentTab === this.tabs.length - 1 || this.tabs.length === 0) ? 'none' : 'inline-block';
}
},
formatCurrency: function(value) {
if (isNaN(value) || value === null) value = 0; // Ensure value is a number
return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
},
generatePdf: function() {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('The jsPDF library (jspdf.jsPDF) is not loaded correctly.'); console.error('Error: window.jspdf or window.jspdf.jsPDF is not defined.'); return;
}
const JsPDFConstructor = window.jspdf.jsPDF;
let doc;
try { doc = new JsPDFConstructor(); } catch (e) {
alert('Failed to create a PDF document instance.'); console.error('Error instantiating jsPDF:', e); return;
}
if (typeof doc.autoTable !== 'function') {
alert('The jsPDF-AutoTable plugin is not loaded correctly.'); console.error('Error: doc.autoTable is not a function.'); return;
}
if (!this.riskToleranceEl || !this.investmentHorizonEl || !this.optimizationRationaleEl) {
alert("Cannot generate PDF due to missing critical elements for profile or rationale."); return;
}
doc.setFontSize(18); doc.text("Dynamic Asset Allocation Optimization Report", 14, 22);
doc.setFontSize(11); doc.setTextColor(100);
doc.text(`Report Date: ${new Date().toLocaleDateString()}`, 14, 30);
let yPos = 40;
doc.setFontSize(14); doc.text("Investment Profile", 14, yPos); yPos += 7;
doc.setFontSize(10);
doc.text(`Risk Tolerance: ${this.riskToleranceEl.options[this.riskToleranceEl.selectedIndex].text}`, 14, yPos); yPos += 6;
doc.text(`Investment Horizon: ${this.investmentHorizonEl.options[this.investmentHorizonEl.selectedIndex].text}`, 14, yPos); yPos += 10;
doc.setFontSize(14); doc.text("Market Outlooks", 14, yPos); yPos += 2;
const outlooksData = this.assetCategories.map(cat => [cat.name, cat.outlook.replace(/_/g,' ').replace(/\b\w/g, l => l.toUpperCase())]);
doc.autoTable({ startY: yPos, head: [['Asset Category', 'Outlook']], body: outlooksData, theme: 'striped', headStyles: { fillColor: this.getCssVariableValue('--primary-color-daa') || [0, 86, 179] } });
yPos = doc.lastAutoTable.finalY + 10;
if (this.userAssets.length > 0 && this.totalCurrentValue > 0) {
doc.setFontSize(14); doc.text("Current Portfolio Summary", 14, yPos); yPos += 2;
const currentPortfolioData = this.userAssets
.filter(asset => asset.name.trim() || (asset.value || 0) > 0) // Filter out empty assets
.map(asset => [
asset.name || 'N/A',
this.formatCurrency(asset.value || 0),
`${(asset.currentPercent || 0).toFixed(2)}%`
]);
if (currentPortfolioData.length > 0) {
doc.autoTable({ startY: yPos, head: [['Asset', 'Current Value', 'Current %']], body: currentPortfolioData });
yPos = doc.lastAutoTable.finalY + 6;
}
doc.setFontSize(10); doc.text(`Total Current Portfolio Value: ${this.formatCurrency(this.totalCurrentValue)}`, 14, yPos); yPos += 10;
}
doc.setFontSize(14); doc.text("Recommended Dynamic Allocation", 14, yPos); yPos += 7;
const optimizedAllocations = this.getOptimizedAllocation();
const optimizedData = optimizedAllocations.map(cat => [
cat.name,
`${((cat.targetPercent || 0) * 100).toFixed(2)}%`,
this.formatCurrency(this.totalCurrentValue * (cat.targetPercent || 0))
]);
doc.autoTable({ startY: yPos, head: [['Asset Category', 'Recommended %', 'Recommended Value ($)']], body: optimizedData, headStyles: { fillColor: this.getCssVariableValue('--primary-color-daa') || [0, 86, 179] } });
yPos = doc.lastAutoTable.finalY + 10;
doc.setFontSize(11); doc.text("Optimization Rationale:", 14, yPos); yPos +=6;
doc.setFontSize(9);
const rationaleLines = doc.splitTextToSize(this.optimizationRationaleEl.textContent || "No rationale generated.", 180);
doc.text(rationaleLines, 14, yPos);
yPos += rationaleLines.length * 4 + 5;
doc.setFontSize(9); doc.setTextColor(150);
const disclaimer = "This report is based on user-provided data and simplified optimization models. Consult a financial advisor for personalized advice.";
const disclaimerLines = doc.splitTextToSize(disclaimer, 180);
if (yPos > 260) { doc.addPage(); yPos = 20; } // Check if enough space for disclaimer
doc.text(disclaimerLines, 14, yPos);
doc.save(`Dynamic_Asset_Allocation_Optimizer_${new Date().toISOString().slice(0,10)}.pdf`);
},
getCssVariableValue(variableName) { // Helper to get CSS var for PDF colors
try {
const colorStr = getComputedStyle(document.documentElement).getPropertyValue(variableName).trim();
if (colorStr.startsWith('#')) { // Convert hex to RGB array
let hex = colorStr.substring(1);
if (hex.length === 3) hex = hex.split('').map(char => char + char).join('');
const bigint = parseInt(hex, 16);
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
} else if (colorStr.startsWith('rgb')) { // Parse rgb(r, g, b)
return colorStr.match(/\d+/g).map(Number);
}
} catch (e) { console.warn("Could not parse CSS variable for PDF:", variableName, e); }
return null; // Fallback color
}
};
daaApp.init();
window.daaApp = daaApp;
});