Smart Beta Portfolio Builder
Define Your Profile
Select Smart Beta Factors
Choose the smart beta factors you'd like to include in your portfolio. Hover over factor names for a brief description (if tooltips were implemented via JS/CSS - here, descriptions are inline).
Allocate to Selected Factors
Total allocation must be 100%.
Your Smart Beta Portfolio Summary
Error: Tool UI elements missing. Please ensure the code was copied completely.
'; return; } this.tabButtons.forEach((button, index) => { button.addEventListener('click', () => this.showTab(index)); }); if(this.investmentAmountEl) this.investmentAmountEl.addEventListener('change', (e) => { this.investmentAmount = parseFloat(e.target.value) || 0; this.renderAllocationTable(); // Re-render if amount changes values if(this.currentTab === 2) this.renderPortfolioSummary(); // Update summary if on that tab }); if(this.suggestAllocationButtonEl) this.suggestAllocationButtonEl.addEventListener('click', () => this.suggestAllocation()); if(this.prevButtonEl) this.prevButtonEl.addEventListener('click', () => this.navigateTabs(-1)); if(this.nextButtonEl) this.nextButtonEl.addEventListener('click', () => this.navigateTabs(1)); const downloadPdfBtn = document.getElementById('sbpbDownloadPdfButton'); if(downloadPdfBtn) downloadPdfBtn.addEventListener('click', () => this.generatePdf()); else console.error("SBPB Error: PDF Download button not found."); this.renderFactorSelection(); this.investmentAmount = parseFloat(this.investmentAmountEl.value) || 10000; // Initial amount this.showTab(0); }, renderFactorSelection: function() { if (!this.factorSelectionContainerEl) return; this.factorSelectionContainerEl.innerHTML = ''; this.smartBetaFactors.forEach(factor => { const itemDiv = document.createElement('div'); itemDiv.className = 'sbpb-factor-item'; const label = document.createElement('label'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `factor_cb_${factor.id}`; checkbox.value = factor.id; checkbox.checked = factor.selected; checkbox.addEventListener('change', (e) => { factor.selected = e.target.checked; this.toggleAllocationSectionVisibility(); this.renderAllocationTable(); // Re-render allocation table on selection change }); label.appendChild(checkbox); label.appendChild(document.createTextNode(` ${factor.name}`)); const description = document.createElement('p'); description.className = 'factor-description'; description.textContent = factor.description; itemDiv.appendChild(label); itemDiv.appendChild(description); this.factorSelectionContainerEl.appendChild(itemDiv); }); }, toggleAllocationSectionVisibility: function() { if (!this.allocationSectionEl) return; const anyFactorSelected = this.smartBetaFactors.some(f => f.selected); this.allocationSectionEl.style.display = anyFactorSelected ? 'block' : 'none'; }, renderAllocationTable: function() { if (!this.allocationTableContainerEl) return; this.allocationTableContainerEl.innerHTML = ''; const selectedFactors = this.smartBetaFactors.filter(f => f.selected); if (selectedFactors.length === 0) { this.validateAndDisplayTotalAllocation(); return; } const table = document.createElement('table'); table.className = 'sbpb-table'; const thead = table.createTHead(); const headerRow = thead.insertRow(); ['Factor', 'Allocation (%)', 'Value ($)'].forEach(text => { const th = document.createElement('th'); th.textContent = text; headerRow.appendChild(th); }); const tbody = table.createTBody(); selectedFactors.forEach(factor => { const row = tbody.insertRow(); row.insertCell().textContent = factor.name; const allocCell = row.insertCell(); const allocInput = document.createElement('input'); allocInput.type = 'number'; allocInput.min = '0'; allocInput.max = '100'; allocInput.value = factor.allocation.toFixed(2); // Ensure it shows 2 decimal places allocInput.addEventListener('change', (e) => { // Use change for committed value factor.allocation = parseFloat(e.target.value) || 0; this.renderAllocationTable(); // Re-render to update values and total }); allocCell.appendChild(allocInput); allocCell.setAttribute('data-label', 'Allocation (%)'); const valueCell = row.insertCell(); valueCell.className = 'allocation-value'; valueCell.textContent = this.formatCurrency(this.investmentAmount * (factor.allocation / 100)); valueCell.setAttribute('data-label', 'Value ($)'); }); // Total Row const totalRow = tbody.insertRow(); totalRow.className = 'total-allocation-row'; totalRow.insertCell().textContent = 'Total'; const totalAllocPercentCell = totalRow.insertCell(); totalAllocPercentCell.setAttribute('data-label', 'Total Allocation (%)'); const totalAllocValueCell = totalRow.insertCell(); totalAllocValueCell.className = 'allocation-value'; totalAllocValueCell.setAttribute('data-label', 'Total Value ($)'); this.allocationTableContainerEl.appendChild(table); this.validateAndDisplayTotalAllocation(); // Call this to update totals in the new row }, validateAndDisplayTotalAllocation: function() { const selectedFactors = this.smartBetaFactors.filter(f => f.selected); let totalAllocationPercent = selectedFactors.reduce((sum, f) => sum + f.allocation, 0); // Update total cells if they exist (they are created in renderAllocationTable) const totalRow = this.allocationTableContainerEl.querySelector('.total-allocation-row'); if (totalRow) { totalRow.cells[1].textContent = `${totalAllocationPercent.toFixed(2)}%`; totalRow.cells[2].textContent = this.formatCurrency(this.investmentAmount * (totalAllocationPercent / 100)); } if (this.allocationErrorEl) { if (selectedFactors.length > 0 && Math.abs(totalAllocationPercent - 100) > 0.01) { this.allocationErrorEl.style.display = 'block'; return false; } this.allocationErrorEl.style.display = 'none'; } return true; }, suggestAllocation: function() { const selectedFactors = this.smartBetaFactors.filter(f => f.selected); if (selectedFactors.length === 0) { alert("Please select at least one smart beta factor to suggest an allocation."); return; } const riskProfile = this.riskToleranceEl ? this.riskToleranceEl.value : 'moderate'; let totalBaseWeight = 0; selectedFactors.forEach(factor => { let baseKey = 'baseModerate'; if (riskProfile === 'conservative') baseKey = 'baseConservative'; else if (riskProfile === 'aggressive') baseKey = 'baseAggressive'; factor.allocation = factor[baseKey] * 100; // Using percentages directly totalBaseWeight += factor.allocation; }); // Normalize if totalBaseWeight is not 0 and not 100 (or if only one factor, it gets 100) if (selectedFactors.length === 1) { selectedFactors[0].allocation = 100; } else if (totalBaseWeight > 0 && Math.abs(totalBaseWeight - 100) > 0.01) { selectedFactors.forEach(factor => { factor.allocation = (factor.allocation / totalBaseWeight) * 100; }); } else if (totalBaseWeight === 0 && selectedFactors.length > 0) { // Equal weight if all base weights are 0 const equalWeight = 100 / selectedFactors.length; selectedFactors.forEach(factor => factor.allocation = equalWeight); } // Round to 2 decimal places and adjust last to sum to 100 if needed due to rounding let currentSum = 0; selectedFactors.forEach((factor, index) => { factor.allocation = parseFloat(factor.allocation.toFixed(2)); if (index < selectedFactors.length - 1) { currentSum += factor.allocation; } }); if (selectedFactors.length > 0) { const lastFactor = selectedFactors[selectedFactors.length - 1]; lastFactor.allocation = parseFloat((100 - currentSum).toFixed(2)); if (lastFactor.allocation < 0) { // Safety, re-distribute if last becomes negative lastFactor.allocation = 0; this.suggestAllocation(); // Re-run suggestion if rounding caused issues (rare) return; } } this.renderAllocationTable(); }, renderPortfolioSummary: function() { if (!this.portfolioSummaryContainerEl) return; this.portfolioSummaryContainerEl.innerHTML = ''; const selectedFactorsWithAllocation = this.smartBetaFactors.filter(f => f.selected && f.allocation > 0); if (selectedFactorsWithAllocation.length === 0) { this.portfolioSummaryContainerEl.innerHTML = 'No factors selected or allocated. Please go to Tab 2 to build your portfolio.
'; return; } if (!this.validateAndDisplayTotalAllocation() && this.currentTab === 2) { // Check allocation sum from tab 2 logic this.portfolioSummaryContainerEl.innerHTML = 'Total allocation in Tab 2 is not 100%. Please correct it.
'; return; } let summaryHtml = `Portfolio Overview
`; summaryHtml += `Risk Tolerance: ${this.riskToleranceEl ? this.riskToleranceEl.options[this.riskToleranceEl.selectedIndex].text : 'N/A'}
`; summaryHtml += `Investment Horizon: ${this.investmentHorizonEl ? this.investmentHorizonEl.options[this.investmentHorizonEl.selectedIndex].text : 'N/A'}
`; summaryHtml += `Total Investment Amount: ${this.formatCurrency(this.investmentAmount)}
Factor Allocations:
`; summaryHtml += '| Smart Beta Factor | Allocation (%) | Allocated Value ($) |
|---|---|---|
| ${factor.name} | ${factor.allocation.toFixed(2)}% | ${this.formatCurrency(this.investmentAmount * (factor.allocation / 100))} |
Factor Descriptions:
- `;
selectedFactorsWithAllocation.forEach(factor => {
summaryHtml += `
- ${factor.name}: ${factor.description} `; }); summaryHtml += '