Quantitative Multi-Factor Model Creator

Define Model Factors and Their Weights

Total Weight: 0%

Input Data for Each Asset

Factor Values:

Added Assets

No assets added yet.

Quantitative Multi-Factor Model Results

No results to display. Please define factors, add assets, and then click "Calculate Model".

No assets added yet.

'; return; } let tableHTML = ''; const namedFactors = factors.filter(f => f.name && f.name.trim() !== ''); namedFactors.forEach(f => tableHTML += ``); tableHTML += ''; assets.forEach(asset => { tableHTML += ``; namedFactors.forEach(f => { const value = asset.factorValues[f.id]; tableHTML += ``; }); tableHTML += ``; }); tableHTML += '
Asset Name${escapeHtml(f.name)}Action
${escapeHtml(asset.name)}${value !== undefined && !isNaN(value) ? value : 'N/A'}
'; assetsTableContainer.innerHTML = tableHTML; } window.removeAsset = function(assetId) { assets = assets.filter(a => a.id !== assetId); renderAssetsTable(); } // --- Tab 3: Model Results --- const calculateModelButton = document.getElementById('qmfCalculateModelButton'); const resultsTableContainer = document.getElementById('qmfResultsTableContainer'); const downloadPdfButtonResults = document.getElementById('qmfDownloadPdfButton'); const resultsFactorsSummaryContainer = document.getElementById('qmfResultsFactorsSummary'); function renderFactorsSummaryForResults() { if (!resultsFactorsSummaryContainer) return; if (factors.length === 0) { resultsFactorsSummaryContainer.innerHTML = '

Factors & Weights:

No factors defined.

'; return; } let summaryHTML = '

Factors & Weights Used:

    '; factors.forEach(f => { if (f.name && f.name.trim()) { summaryHTML += `
  • ${escapeHtml(f.name)}: ${f.weight}% (${f.direction === 'high' ? 'Higher is better' : 'Lower is better'})
  • `; } }); summaryHTML += '
'; resultsFactorsSummaryContainer.innerHTML = summaryHTML; } calculateModelButton?.addEventListener('click', () => { if (factors.length === 0 || assets.length === 0) { alert('Please define factors and add asset data first.'); resultsTableContainer.innerHTML = '

Please define factors on Tab 1 and add asset data on Tab 2.

'; downloadPdfButtonResults.style.display = 'none'; return; } if (!validateFactorWeights()){ alert('Factor weights issue. Please correct on Tab 1.'); switchTab('factorsTab'); downloadPdfButtonResults.style.display = 'none'; return; } const results = []; const namedFactors = factors.filter(f => f.name && f.name.trim() !== ''); const factorRawValues = {}; namedFactors.forEach(f => factorRawValues[f.id] = []); assets.forEach(asset => { namedFactors.forEach(f => { const val = asset.factorValues[f.id]; if (val !== undefined && !isNaN(val)) { factorRawValues[f.id].push(val); } }); }); const factorStats = {}; namedFactors.forEach(f => { const currentFactorValues = factorRawValues[f.id]; // Renamed for clarity within this scope if (currentFactorValues && currentFactorValues.length > 0) { factorStats[f.id] = { min: Math.min(...currentFactorValues), max: Math.max(...currentFactorValues) }; } else { factorStats[f.id] = { min: 0, max: 0 }; } }); assets.forEach(asset => { let compositeScore = 0; const assetResult = { id: asset.id, name: asset.name, factorValues: {}, normalizedScores: {}, weightedScores: {}, compositeScore: 0, rank: 0 }; namedFactors.forEach(f => { const rawValue = asset.factorValues[f.id]; assetResult.factorValues[f.id] = (rawValue !== undefined && !isNaN(rawValue)) ? rawValue : 'N/A'; let normalizedScore = 0; if (rawValue !== undefined && !isNaN(rawValue)) { const { min, max } = factorStats[f.id]; if (max > min) { if (f.direction === 'high') { normalizedScore = (rawValue - min) / (max - min); } else { normalizedScore = (max - rawValue) / (max - min); } } else if (max === min && factorRawValues[f.id] && factorRawValues[f.id].length > 0) { // *** FIXED LINE HERE *** normalizedScore = 0; } } normalizedScore = Math.max(0, Math.min(1, normalizedScore)); assetResult.normalizedScores[f.id] = normalizedScore; const weightedScore = normalizedScore * (f.weight / 100); assetResult.weightedScores[f.id] = weightedScore; compositeScore += weightedScore; }); assetResult.compositeScore = compositeScore; results.push(assetResult); }); results.sort((a, b) => b.compositeScore - a.compositeScore); results.forEach((res, index) => res.rank = index + 1); renderResultsTable(results, namedFactors); downloadPdfButtonResults.style.display = 'block'; }); function renderResultsTable(results, displayFactors) { if (!resultsTableContainer) return; if (results.length === 0) { resultsTableContainer.innerHTML = '

No results calculated or no assets to display.

'; return; } let tableHTML = ``; displayFactors.forEach(f => tableHTML += ``); displayFactors.forEach(f => tableHTML += ``); tableHTML += ``; results.forEach(res => { tableHTML += ``; displayFactors.forEach(f => { const rawVal = res.factorValues[f.id]; tableHTML += ``; }); displayFactors.forEach(f => tableHTML += ``); tableHTML += ``; }); tableHTML += `
RankAsset Name${escapeHtml(f.name)} (Raw)${escapeHtml(f.name)} (Wgt. Score)Composite Score
${res.rank} ${escapeHtml(res.name)}${typeof rawVal === 'number' ? rawVal.toFixed(2) : rawVal}${res.weightedScores[f.id].toFixed(4)}${res.compositeScore.toFixed(4)}
`; resultsTableContainer.innerHTML = tableHTML; } downloadPdfButtonResults?.addEventListener('click', function () { const { jsPDF } = window.jspdf; const pdfOutputArea = document.getElementById('qmfPdfOutputArea'); if (!pdfOutputArea || assets.length === 0) { alert('No results to download or PDF area not found.'); return; } if (resultsTableContainer.querySelector('p')) { alert('Please calculate the model first before downloading PDF.'); return; } html2canvas(pdfOutputArea, { scale: 2, useCORS: true, backgroundColor: '#ffffff' }) .then(canvas => { const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a3' }); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const margin = 30; const contentWidth = pdfWidth - 2 * margin; const canvasWidth = canvas.width; const canvasHeight = canvas.height; const ratio = canvasWidth / canvasHeight; let imgWidth = contentWidth; let imgHeight = imgWidth / ratio; if (imgHeight > pdfHeight - 2*margin) { imgHeight = pdfHeight - 2*margin; imgWidth = imgHeight * ratio; } pdf.addImage(imgData, 'PNG', margin, margin, imgWidth, imgHeight); pdf.save('Quantitative_MultiFactor_Model.pdf'); }) .catch(err => { console.error("Error generating PDF:", err); alert("Error generating PDF. See console for details."); }); }); function escapeHtml(unsafe) { if (typeof unsafe !== 'string') return unsafe === undefined || unsafe === null ? '' : String(unsafe); return unsafe.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } generateFactorInputsButton.click(); switchTab('factorsTab'); });
Scroll to Top