AI-Driven Housing Market Bubble Detection System (Conceptual Simulator)
Market Profile
Indicator Configuration (Enable, Set Thresholds & Weights)
Define how each indicator contributes to bubble risk. For thresholds, 'Low Risk Max' is the upper limit for low risk. 'High Risk Min' is the lower limit for high risk. Values between are medium risk.
Input Current Market Data for Enabled Indicators
Bubble Risk Assessment
Assessment results will appear here after processing data.
No indicators enabled. Please enable and configure indicators in Tab 2.
'; } } function aihmbds_updateIndicatorInputValue(indicatorId, inputKey, value) { const indicator = aihmbds_model.indicators.find(ind => ind.id === indicatorId); if (indicator) { if (!indicator.inputs) indicator.inputs = {}; // Ensure inputs object exists indicator.inputs[inputKey] = value; } } // --- Tab 4: Bubble Risk Assessment --- function aihmbds_assessBubbleRisk() { aihmbds_model.marketName = aihmbds_getInputValue('aihmbds_marketName', 'N/A Market'); aihmbds_model.assessmentDate = aihmbds_getInputValue('aihmbds_assessmentDate', 'Current'); let totalWeightedRiskPoints = 0; let totalWeightSum = 0; let indicatorDetails = []; let calculationError = false; aihmbds_model.indicators.filter(ind => ind.enabled).forEach(ind => { let indicatorValue = null; if (ind.type === 'ratio') { if (!ind.inputs) { calculationError = true; console.warn(`Inputs missing for ratio indicator ${ind.name}`); return; } const val1 = ind.inputs[ind.requires[0]]; const val2 = ind.inputs[ind.requires[1]]; if (val1 === undefined || val2 === undefined || val2 === 0 || isNaN(val1) || isNaN(val2)) { calculationError = true; console.warn(`Invalid inputs for ratio ${ind.name}: ${val1}, ${val2}`); indicatorValue = null; } else { indicatorValue = val1 / val2; } ind.calculatedValue = indicatorValue; } else { indicatorValue = ind.currentValue; if (indicatorValue === null || isNaN(indicatorValue)) { calculationError = true; console.warn(`Invalid current value for ${ind.name}: ${indicatorValue}`); } ind.calculatedValue = indicatorValue; } let riskLevel = 'Medium'; let riskPoints = 1; if (indicatorValue !== null && !isNaN(indicatorValue)) { // Ensure thresholds are numbers const lowMax = parseFloat(ind.low_risk_max); const highMin = parseFloat(ind.high_risk_min); if(isNaN(lowMax) || isNaN(highMin)) { calculationError = true; console.warn(`Invalid thresholds for ${ind.name}`); riskLevel = 'ErrorInThresholds'; riskPoints = 1; // default to medium if thresholds bad } else { if (ind.value_is_higher_risk) { // Higher indicator value implies higher risk if (indicatorValue >= highMin) { riskLevel = 'High'; riskPoints = 2; } else if (indicatorValue <= lowMax) { riskLevel = 'Low'; riskPoints = 0; } } else { // Lower indicator value implies higher risk if (indicatorValue <= lowMax) { riskLevel = 'High'; riskPoints = 2; } else if (indicatorValue >= highMin) { riskLevel = 'Low'; riskPoints = 0; } } } } else { riskLevel = 'N/A (No Data)'; riskPoints = 1; // Treat missing data as medium } const weight = parseInt(ind.weight) || 0; // Ensure weight is a number const weightedContribution = riskPoints * weight; totalWeightedRiskPoints += weightedContribution; totalWeightSum += weight; indicatorDetails.push({ name: ind.name, value: indicatorValue !== null && !isNaN(indicatorValue) ? (ind.unit === '%' ? `${indicatorValue.toFixed(1)}${ind.unit}` : indicatorValue.toFixed(2) + (ind.unit && ind.unit !== 'ratio' ? ` ${ind.unit}`: '')) : 'N/A', riskLevel: riskLevel, weight: weight, contribution: weightedContribution }); }); if (calculationError) { alert("Error in input data or indicator configuration. Please check values and thresholds. See console (F12) for details."); } let overallScore = 0; if (totalWeightSum > 0) { overallScore = (totalWeightedRiskPoints / (2 * totalWeightSum)) * 100; // Max possible risk points is 2 overallScore = Math.max(0, Math.min(100, overallScore)); } let qualitativeLevel = 'Indeterminate'; let qualitativeClass = ''; if (totalWeightSum === 0 && aihmbds_model.indicators.filter(ind => ind.enabled).length > 0) { qualitativeLevel = 'No weights assigned to enabled indicators'; overallScore = 0; // Or N/A } else if (aihmbds_model.indicators.filter(ind => ind.enabled).length === 0) { qualitativeLevel = 'No indicators enabled for assessment'; overallScore = 0; } else if (overallScore <= 33) { qualitativeLevel = 'Low Bubble Risk'; qualitativeClass = 'aihmbds_risk_low';} else if (overallScore <= 66) { qualitativeLevel = 'Moderate Bubble Risk'; qualitativeClass = 'aihmbds_risk_medium';} else { qualitativeLevel = 'High Bubble Risk'; qualitativeClass = 'aihmbds_risk_high';} aihmbds_model.assessment = { marketName: aihmbds_model.marketName, assessmentDate: aihmbds_model.assessmentDate, overallScore: overallScore, qualitativeLevel: qualitativeLevel, qualitativeClass: qualitativeClass, indicatorDetails: indicatorDetails, inputsForPdf: JSON.parse(JSON.stringify(aihmbds_model.indicators.filter(i=>i.enabled))) // Store a snapshot of inputs }; aihmbds_displayAssessmentResults(); const pdfButton = document.getElementById('aihmbds_downloadPdfButton'); if (pdfButton) pdfButton.style.display = 'block'; } function aihmbds_displayAssessmentResults() { const resultsSection = aihmbds_getEl('aihmbds_assessmentResultsSection'); if (!resultsSection || !aihmbds_model.assessment) { if(resultsSection) resultsSection.innerHTML = 'No assessment data. Input current data and click "Assess Bubble Risk".
'; return; } const assessment = aihmbds_model.assessment; let detailsHTML = assessment.indicatorDetails.map(d => `
${d.name}
${d.value}
${d.riskLevel}
${d.contribution.toFixed(1)} pts
`).join('');
resultsSection.innerHTML = `
Assessment for: ${assessment.marketName} (${assessment.assessmentDate})
${assessment.overallScore.toFixed(1)} / 100
${assessment.qualitativeLevel}
Indicator Breakdown (Risk Level * Weight):
${detailsHTML}
`;
}
// --- PDF Generation ---
function aihmbds_downloadPDF() {
if (!aihmbds_model.assessment) { alert("Please assess risk first."); return; }
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('Core PDF library (jsPDF) is not loaded. Cannot generate PDF.');
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) seems not to be loaded correctly. Tables in PDF might be missing or PDF generation may fail.\nPlease ensure your internet connection is active and no browser extensions are blocking scripts from cdnjs.cloudflare.com.');
console.error('doc.autoTable is not a function. jsPDF-AutoTable plugin error.');
return;
}
let y = 15; const m = 15; const cw = doc.internal.pageSize.getWidth() - (2 * m);
const assessment = aihmbds_model.assessment;
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(`Housing Market Bubble Risk Assessment`, 18, 'bold', 0, 5);
addLine(`Market: ${assessment.marketName}`, 12);
addLine(`Assessment Date: ${assessment.assessmentDate}`, 12);
y += 5;
addLine(`Overall Bubble Risk Score: ${assessment.overallScore.toFixed(1)} / 100`, 14, 'bold');
addLine(`Qualitative Level: ${assessment.qualitativeLevel}`, 12, 'bold', 0, 6);
addLine("Indicator Details & Contributions:", 12, 'bold', 0, 4);
const head = [['Indicator', 'Current Value', 'Configured Thresholds (Low Max / High Min)', 'Interpretation', 'Risk Level', 'Weight', 'Weighted Contrib.']];
const body = assessment.inputsForPdf.map(ind => { // Use inputsForPdf which has full indicator details
const currentIndAssessment = assessment.indicatorDetails.find(d => d.name === ind.name);
let displayedValue = 'N/A';
if (ind.type === 'ratio') {
displayedValue = ind.calculatedValue !== null ? ind.calculatedValue.toFixed(2) + (ind.unit || '') : 'N/A';
} else {
displayedValue = ind.currentValue !== null && ind.currentValue !== undefined ? ind.currentValue.toString() + (ind.unit || '') : 'N/A';
}
return [
ind.name,
displayedValue,
`${ind.low_risk_max} / ${ind.high_risk_min}`,
ind.value_is_higher_risk ? 'Higher=MoreRisk' : 'Lower=MoreRisk',
currentIndAssessment ? currentIndAssessment.riskLevel : 'N/A',
ind.weight.toString(),
currentIndAssessment ? currentIndAssessment.contribution.toFixed(1) : 'N/A'
];
});
if (y > 180 && body.length > 10) { doc.addPage(); y = m;} // crude check for page break
doc.autoTable({
startY: y, head: head, body: body, theme: 'grid',
headStyles: { fillColor: [55, 65, 81], textColor: 230, fontSize: 8 },
styles: { fontSize: 7, cellPadding: 1.2, overflow: 'linebreak' },
columnStyles: {
0: { cellWidth: 40}, 1: {cellWidth: 25, halign: 'center'}, 2: {cellWidth: 35, halign: 'center'},
3: {cellWidth: 25, halign: 'center'}, 4: {cellWidth: 20, halign: 'center'},
5: {cellWidth: 15, halign: 'center'}, 6: {cellWidth: 25, halign: 'right'}
}
});
y = doc.lastAutoTable.finalY + 10;
addLine("Note: This is a conceptual simulation based on user-defined parameters and does not constitute financial advice or a definitive market analysis.", 8, 'italic');
doc.save(`BubbleRisk_${assessment.marketName.replace(/\s+/g, '_')}.pdf`);
}