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).

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)}

`; summaryHtml += `

Factor Allocations:

`; summaryHtml += ''; selectedFactorsWithAllocation.forEach(factor => { summaryHtml += ``; }); summaryHtml += '
Smart Beta FactorAllocation (%)Allocated Value ($)
${factor.name} ${factor.allocation.toFixed(2)}% ${this.formatCurrency(this.investmentAmount * (factor.allocation / 100))}
'; summaryHtml += `

Factor Descriptions:

    `; selectedFactorsWithAllocation.forEach(factor => { summaryHtml += `
  • ${factor.name}: ${factor.description}
  • `; }); summaryHtml += '
'; this.portfolioSummaryContainerEl.innerHTML = summaryHtml; }, showTab: function(tabIndex) { if (!this.tabs || this.tabs.length === 0 || !this.tabButtons || this.tabButtons.length === 0) return; if (tabIndex < 0 || tabIndex >= this.tabs.length) return; // Validation before leaving Tab 2 (Factor Selection & Allocation) if (this.currentTab === 1 && tabIndex > 1) { // Moving from Tab 2 to Tab 3 const selectedFactors = this.smartBetaFactors.filter(f => f.selected); if (selectedFactors.length > 0 && !this.validateAndDisplayTotalAllocation()) { alert("Total factor allocation must sum to 100%. Please adjust allocations."); return; // Stay on Tab 2 } if (selectedFactors.length === 0 && this.investmentAmount > 0) { if (!confirm("You haven't selected or allocated to any smart beta factors. Do you want to proceed to the summary?")) { 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 === 1) { // Entering Factor Selection / Allocation Tab this.toggleAllocationSectionVisibility(); // Ensure visibility based on current selections this.renderAllocationTable(); // Render table if factors already selected } else if (tabIndex === 2) { // Entering Summary Tab this.renderPortfolioSummary(); if(this.pdfButtonContainerEl) this.pdfButtonContainerEl.style.display = 'block'; } else { if(this.pdfButtonContainerEl) this.pdfButtonContainerEl.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; return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); }, getCssVariableValue(variableName) { try { const colorStr = getComputedStyle(document.documentElement).getPropertyValue(variableName).trim(); if (colorStr.startsWith('#')) { 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')) { return colorStr.match(/\d+/g).map(Number); } } catch (e) { console.warn("Could not parse CSS var for PDF:", variableName, e); } return [44, 62, 80]; // Fallback to --primary-color-sbpb if parsing fails }, generatePdf: function() { if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') { alert('jsPDF library not loaded.'); console.error('window.jspdf or window.jspdf.jsPDF not defined.'); return; } const JsPDFConstructor = window.jspdf.jsPDF; let doc; try { doc = new JsPDFConstructor(); } catch (e) { alert('Failed to create PDF instance.'); console.error('Error instantiating jsPDF:', e); return; } if (typeof doc.autoTable !== 'function') { alert('jsPDF-AutoTable plugin not loaded.'); console.error('doc.autoTable not a function.'); return; } const riskEl = this.riskToleranceEl; const horizonEl = this.investmentHorizonEl; if (!riskEl || !horizonEl) { alert("Profile elements missing."); return; } doc.setFontSize(18); doc.text("Smart Beta Portfolio 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: ${riskEl.options[riskEl.selectedIndex].text}`, 14, yPos); yPos += 6; doc.text(`Investment Horizon: ${horizonEl.options[horizonEl.selectedIndex].text}`, 14, yPos); yPos += 6; doc.text(`Total Investment Amount: ${this.formatCurrency(this.investmentAmount)}`, 14, yPos); yPos += 10; const allocatedFactors = this.smartBetaFactors.filter(f => f.selected && f.allocation > 0); if (allocatedFactors.length > 0) { doc.setFontSize(14); doc.text("Factor Allocations", 14, yPos); yPos += 2; const portfolioData = allocatedFactors.map(f => [ f.name, `${f.allocation.toFixed(2)}%`, this.formatCurrency(this.investmentAmount * (f.allocation / 100)) ]); doc.autoTable({ startY: yPos, head: [['Smart Beta Factor', 'Allocation (%)', 'Allocated Value ($)']], body: portfolioData, theme: 'striped', headStyles: { fillColor: this.getCssVariableValue('--primary-color-sbpb') } }); yPos = doc.lastAutoTable.finalY + 10; doc.setFontSize(14); doc.text("Factor Descriptions", 14, yPos); yPos += 7; doc.setFontSize(9); allocatedFactors.forEach(factor => { if (yPos > 260) { doc.addPage(); yPos = 20; } // Add new page if content overflows doc.setFont(undefined, 'bold'); doc.text(factor.name + ":", 14, yPos); doc.setFont(undefined, 'normal'); const descLines = doc.splitTextToSize(factor.description, 180); doc.text(descLines, 14 + (doc.getStringUnitWidth(factor.name + ": ") * doc.getFontSize() / doc.internal.scaleFactor) + 2 , yPos); // Attempt to align after bold title yPos += (descLines.length * 4) + 3; // Adjust spacing }); } else { doc.setFontSize(10); doc.text("No smart beta factors were allocated in this portfolio.", 14, yPos); yPos += 10; } doc.setFontSize(9); doc.setTextColor(150); const disclaimer = "This report is based on user-provided data and illustrative models. It is not financial advice. Consult a qualified financial advisor for investment decisions."; if (yPos > 265) { doc.addPage(); yPos = 20; } const disclaimerLines = doc.splitTextToSize(disclaimer, 180); doc.text(disclaimerLines, 14, yPos); doc.save(`Smart_Beta_Portfolio_${new Date().toISOString().slice(0,10)}.pdf`); } }; sbpbApp.init(); window.sbpbApp = sbpbApp; // For debugging });
Scroll to Top