Factor-Tilted Portfolio Construction Tool (Conceptual Simulator)
Portfolio Setup
$
E.g., 0.2-0.4 for diversified portfolios. Used for conceptual base portfolio risk.
Base Portfolio: Asset Classes & Assumptions
Define your base asset classes, target allocations (must sum to 100%), and their expected nominal performance.
Total Allocation: 0%
Define Factor Tilts
Select factors to tilt your portfolio towards. Define the conceptual annual return impact (premium/discount) and tracking error (active risk as % volatility) introduced by each tilt. These are applied on top of the base portfolio.
Factor-Tilted Portfolio Analysis
Portfolio comparison will appear here.
Base Asset Allocation
Return/Volatility Comparison
Comparison chart appears here.
Factor Tilt Contributions:
Note on Tilted Portfolio Volatility: The tilted portfolio's volatility is conceptually estimated by adding the variance of factor tracking errors (assumed uncorrelated with each other and the base portfolio) to the base portfolio's variance. This is a simplification.
Factor contributions table.
'; return; } const res = ftpc_model.analysisResults; summaryDiv.innerHTML = `Portfolio Comparison
| Metric | Base Portfolio | Factor-Tilted Portfolio | Impact of Tilts |
|---|---|---|---|
| Expected Return | ${ftpc_formatPercent(res.basePortfolio.expReturn)} | ${ftpc_formatPercent(res.tiltedPortfolio.expReturn)} | ${ftpc_formatPercent(res.activeReturn)} |
| Conceptual Volatility | ${ftpc_formatPercent(res.basePortfolio.volatility)} | ${ftpc_formatPercent(res.tiltedPortfolio.volatility)} | ${ftpc_formatPercent(res.tiltedPortfolio.volatility - res.basePortfolio.volatility)} |
| Conceptual Information Ratio | - | ${res.informationRatio_conceptual.toFixed(2)} | |
| Factor Tilt Applied | Return Impact (%) | Tracking Error (%) |
|---|---|---|
| ${f.name} | ${f.returnImpact.toFixed(2)} | ${f.trackingError.toFixed(2)} |
| No factor tilts enabled. | ||
No allocations.
'; return; } const boxSize = Math.min(svg.parentElement.offsetWidth, svg.parentElement.offsetHeight, 260) || 260; svg.setAttribute('viewBox', `0 0 ${boxSize} ${boxSize}`); const radius = boxSize / 2 * 0.85; const cx = boxSize / 2; const cy = boxSize / 2; let cumulativePercent = 0; filteredAC.forEach(ac => { const percent = (ac.targetAllocation || 0) / 100; const startAngle = cumulativePercent * 2 * Math.PI - Math.PI / 2; const endAngle = (cumulativePercent + percent) * 2 * Math.PI - Math.PI / 2; const x1 = cx + radius * Math.cos(startAngle); const y1 = cy + radius * Math.sin(startAngle); const x2 = cx + radius * Math.cos(endAngle); const y2 = cy + radius * Math.sin(endAngle); const largeArcFlag = percent > 0.5 ? 1 : 0; const pathData = `M ${cx},${cy} L ${x1},${y1} A ${radius},${radius} 0 ${largeArcFlag},1 ${x2},${y2} Z`; const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", pathData); path.setAttribute("fill", ac.color || '#ccc'); path.setAttribute("stroke", "#1F2937"); path.setAttribute("stroke-width", "1"); const title = document.createElementNS("http://www.w3.org/2000/svg", "title"); title.textContent = `${ac.name}: ${ac.targetAllocation.toFixed(1)}%`; path.appendChild(title); svg.appendChild(path); cumulativePercent += percent; }); } function ftpc_renderReturnVolBarChart(results, container) { container.innerHTML = ''; // Clear previous const svgWidth = Math.min(600, (ftpc_getEl('ftpcContainer_main').offsetWidth || 400) - 80); const svgHeight = 280; const m = {top: 20, right: 20, bottom: 60, left: 50}; const w = svgWidth - m.left - m.right; const h = svgHeight - m.top - m.bottom; const data = [ { name: 'Base E(R)', value: results.basePortfolio.expReturn, color: '#A78BFA' }, { name: 'Tilted E(R)', value: results.tiltedPortfolio.expReturn, color: '#818CF8' }, { name: 'Base Vol.', value: results.basePortfolio.volatility, color: '#FBBF24' }, { name: 'Tilted Vol.', value: results.tiltedPortfolio.volatility, color: '#F59E0B' } ]; const allYValues = data.map(d => d.value); const yMin = Math.min(0, ...allYValues.filter(v=>!isNaN(v))); const yMax = Math.max(...allYValues.filter(v=>!isNaN(v))); const numBars = data.length; const barWidth = w / numBars / 1.5; const groupPadding = barWidth / 2; const xScale = i => i * (barWidth + groupPadding) + groupPadding/2; const yScale = y => h - ((y - yMin) / (yMax - yMin === 0 ? 1 : yMax - yMin)) * h; let chartHTML = ``; container.innerHTML = chartHTML; } // --- PDF Generation --- function ftpc_downloadPDF() { if (!ftpc_model.analysisResults) { alert("Please analyze portfolio first."); return; } if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') { alert('Core PDF library (jsPDF) is not loaded.'); console.error('jsPDF library not found.'); return; } const { jsPDF: JSPDF } = window.jspdf; const doc = new JSPDF(); if (typeof doc.autoTable !== 'function') { alert('PDF Table plugin (jsPDF-AutoTable) not loaded correctly. Tables in PDF may be missing.'); console.error('doc.autoTable is not a function. jsPDF-AutoTable plugin error.'); } let y = 15; const m = 15; const cw = doc.internal.pageSize.getWidth() - (2 * m); const res = ftpc_model.analysisResults; const inputs = res.inputsSnapshot || ftpc_model; // Use snapshot function addLine(text, size, style = 'normal', indent = 0, spacing = 2.5) { if (y > 275) { doc.addPage(); y = m; } doc.setFontSize(size); doc.setFont(undefined, style); const lines = doc.splitTextToSize(text, cw - indent); doc.text(lines, m + indent, y); y += (lines.length * (size * 0.35)) + spacing; } addLine(`Factor-Tilted Portfolio Analysis: ${inputs.portfolioName || 'N/A'}`, 16, 'bold', 0, 5); addLine(`Report Date: ${new Date().toLocaleDateString()}`, 9, 'italic', 0, 5); addLine("Base Portfolio Assumptions:", 12, 'bold', 0, 3); addLine(`- Assumed Avg. Pairwise Correlation: ${inputs.avgCorrelation.toFixed(2)}`, 9, 'normal', 5, 2); if (typeof doc.autoTable === 'function') { const baseAssetHead = [['Asset Class', 'Allocation (%)', 'Nom. E(R) (%)', 'Volatility (%)']]; const baseAssetBody = inputs.assetClasses.map(ac => [ac.name, (ac.targetAllocation||0).toFixed(1), (ac.nominalExpReturn||0).toFixed(1), (ac.nominalExpVolatility||0).toFixed(1)]); doc.autoTable({startY: y, head: baseAssetHead, body: baseAssetBody, theme: 'striped', headStyles:{fillColor:[55,65,81]}, styles:{fontSize:8}}); y = doc.lastAutoTable.finalY + 5; } else { addLine("Base asset allocation table could not be generated.", 8, 'italic'); y+=5; } addLine("Factor Tilt Assumptions:", 12, 'bold', 0, 3); const factorHead = [['Factor Tilt', 'Enabled', 'Return Impact (%)', 'Tracking Error (%)']]; const factorBody = inputs.factors.map(f => [f.name, f.enabled ? 'Yes':'No', f.enabled?(f.returnImpact||0).toFixed(1):'N/A', f.enabled?(f.trackingError||0).toFixed(1):'N/A']); if (typeof doc.autoTable === 'function') { doc.autoTable({startY: y, head: factorHead, body: factorBody, theme: 'striped', headStyles:{fillColor:[55,65,81]}, styles:{fontSize:8}}); y = doc.lastAutoTable.finalY + 7; } else { addLine("Factor tilts table could not be generated.", 8, 'italic'); y+=5; } addLine("Portfolio Comparison:", 14, 'bold', 0, 4); const compHead = [['Metric', 'Base Portfolio', 'Factor-Tilted Portfolio', 'Impact of Tilts']]; const compBody = [ ['Expected Return', ftpc_formatPercent(res.basePortfolio.expReturn), ftpc_formatPercent(res.tiltedPortfolio.expReturn), ftpc_formatPercent(res.activeReturn)], ['Conceptual Volatility', ftpc_formatPercent(res.basePortfolio.volatility), ftpc_formatPercent(res.tiltedPortfolio.volatility), ftpc_formatPercent(res.tiltedPortfolio.volatility - res.basePortfolio.volatility)], ['Conceptual Information Ratio', '-', '-', res.informationRatio_conceptual.toFixed(2)] ]; if (typeof doc.autoTable === 'function') { doc.autoTable({startY: y, head: compHead, body: compBody, theme: 'grid', headStyles:{fillColor:[139,92,246]}, styles:{fontSize:9, halign:'center'}, columnStyles:{0:{halign:'left',fontStyle:'bold'}}}); y = doc.lastAutoTable.finalY + 7; } else { addLine("Comparison table could not be generated.", 8, 'italic'); y+=5;} addLine("Note: This is a conceptual simulator. Expected returns, volatilities, correlations, and factor impacts are assumptions. Actual results will vary. Volatility calculations are simplified.", 7, 'italic'); doc.save(`${(inputs.portfolioName || 'FactorTiltedPortfolio').replace(/\s+/g, '_')}_Analysis.pdf`); }