Skip to main content
Glama
sigaihealth

RealVest Real Estate MCP Server

sensitivity-analysis.js23.5 kB
export class SensitivityAnalysisCalculator { constructor() { this.name = 'Sensitivity Analysis Calculator'; this.description = 'Perform multi-variable sensitivity analysis on real estate investments'; } getSchema() { return { type: 'object', properties: { base_scenario: { type: 'object', description: 'Base investment scenario', properties: { purchase_price: { type: 'number', description: 'Property purchase price', minimum: 0 }, down_payment_percent: { type: 'number', description: 'Down payment percentage', minimum: 0, maximum: 100, default: 20 }, annual_rental_income: { type: 'number', description: 'Annual gross rental income', minimum: 0 }, annual_expenses: { type: 'number', description: 'Annual operating expenses', minimum: 0 }, vacancy_rate: { type: 'number', description: 'Expected vacancy rate (%)', minimum: 0, maximum: 50, default: 5 }, interest_rate: { type: 'number', description: 'Mortgage interest rate (%)', minimum: 0, maximum: 20, default: 7 }, loan_term_years: { type: 'number', description: 'Loan term in years', minimum: 1, maximum: 40, default: 30 }, appreciation_rate: { type: 'number', description: 'Annual appreciation rate (%)', minimum: -10, maximum: 20, default: 3 }, holding_period_years: { type: 'number', description: 'Investment holding period in years', minimum: 1, maximum: 30, default: 5 } }, required: ['purchase_price', 'annual_rental_income', 'annual_expenses'] }, sensitivity_variables: { type: 'array', description: 'Variables to analyze', items: { type: 'object', properties: { variable: { type: 'string', description: 'Variable name to analyze', enum: ['purchase_price', 'rental_income', 'expenses', 'vacancy_rate', 'interest_rate', 'appreciation_rate'] }, variations: { type: 'array', description: 'Percentage variations from base (-20, -10, 0, 10, 20)', items: { type: 'number' }, default: [-20, -10, 0, 10, 20] } }, required: ['variable'] } }, analysis_metrics: { type: 'array', description: 'Metrics to calculate', items: { type: 'string', enum: ['irr', 'npv', 'cash_on_cash', 'total_return', 'monthly_cash_flow'] }, default: ['irr', 'cash_on_cash', 'total_return'] }, discount_rate: { type: 'number', description: 'Discount rate for NPV calculation (%)', minimum: 0, maximum: 30, default: 10 } }, required: ['base_scenario'] }; } calculate(params) { const { base_scenario, sensitivity_variables = [ { variable: 'purchase_price', variations: [-20, -10, 0, 10, 20] }, { variable: 'rental_income', variations: [-20, -10, 0, 10, 20] }, { variable: 'interest_rate', variations: [-20, -10, 0, 10, 20] } ], analysis_metrics = ['irr', 'cash_on_cash', 'total_return'], discount_rate = 10 } = params; // Calculate base case metrics const baseMetrics = this.calculateScenarioMetrics(base_scenario, discount_rate / 100); // Perform sensitivity analysis const sensitivityResults = []; sensitivity_variables.forEach(({ variable, variations = [-20, -10, 0, 10, 20] }) => { const variableResults = { variable: this.formatVariableName(variable), base_value: this.getBaseValue(base_scenario, variable), scenarios: [] }; variations.forEach(variation => { const scenario = this.createScenario(base_scenario, variable, variation); const metrics = this.calculateScenarioMetrics(scenario, discount_rate / 100); variableResults.scenarios.push({ variation_percent: variation, value: this.getVariableValue(scenario, variable), metrics: this.formatMetrics(metrics, analysis_metrics), impact: this.calculateImpact(baseMetrics, metrics, analysis_metrics) }); }); // Calculate sensitivity metrics variableResults.sensitivity_metrics = this.calculateSensitivityMetrics(variableResults.scenarios); sensitivityResults.push(variableResults); }); // Two-way sensitivity analysis (if at least 2 variables) let twoWayAnalysis = null; if (sensitivity_variables.length >= 2) { twoWayAnalysis = this.performTwoWayAnalysis( base_scenario, sensitivity_variables[0], sensitivity_variables[1], analysis_metrics[0], discount_rate / 100 ); } // Tornado diagram data const tornadoDiagram = this.createTornadoDiagram(sensitivityResults, analysis_metrics[0]); // Critical values analysis const criticalValues = this.findCriticalValues(base_scenario, sensitivity_variables, discount_rate / 100); // Risk assessment const riskAssessment = this.assessRisk(sensitivityResults, baseMetrics); return { base_case: { scenario: base_scenario, metrics: this.formatMetrics(baseMetrics, analysis_metrics) }, sensitivity_analysis: sensitivityResults, two_way_analysis: twoWayAnalysis, tornado_diagram: tornadoDiagram, critical_values: criticalValues, risk_assessment: riskAssessment, recommendations: this.generateRecommendations( sensitivityResults, riskAssessment, criticalValues ) }; } calculateScenarioMetrics(scenario, discountRate) { const { purchase_price, down_payment_percent = 20, annual_rental_income, annual_expenses, vacancy_rate = 5, interest_rate = 7, loan_term_years = 30, appreciation_rate = 3, holding_period_years = 5 } = scenario; const down_payment = purchase_price * (down_payment_percent / 100); const loan_amount = purchase_price - down_payment; // Calculate annual debt service const monthly_rate = interest_rate / 100 / 12; const num_payments = loan_term_years * 12; let monthly_payment = 0; if (loan_amount > 0 && monthly_rate > 0) { monthly_payment = loan_amount * (monthly_rate * Math.pow(1 + monthly_rate, num_payments)) / (Math.pow(1 + monthly_rate, num_payments) - 1); } else if (loan_amount > 0) { monthly_payment = loan_amount / num_payments; } const annual_debt_service = monthly_payment * 12; // Calculate annual NOI const effective_rental_income = annual_rental_income * (1 - vacancy_rate / 100); const noi = effective_rental_income - annual_expenses; // Calculate annual cash flow const annual_cash_flow = noi - annual_debt_service; const monthly_cash_flow = annual_cash_flow / 12; // Build cash flows for IRR/NPV const cashFlows = [-down_payment]; // Initial investment // Annual cash flows for (let year = 1; year <= holding_period_years; year++) { if (year < holding_period_years) { cashFlows.push(annual_cash_flow); } else { // Final year includes sale proceeds const future_value = purchase_price * Math.pow(1 + appreciation_rate / 100, holding_period_years); const remaining_balance = this.calculateRemainingBalance( loan_amount, monthly_rate, loan_term_years * 12, holding_period_years * 12 ); const sale_proceeds = future_value - remaining_balance; cashFlows.push(annual_cash_flow + sale_proceeds); } } // Calculate metrics const irr = this.calculateIRR(cashFlows); const npv = this.calculateNPV(cashFlows, discountRate); const cash_on_cash = down_payment > 0 ? (annual_cash_flow / down_payment) * 100 : 0; // Total return calculation const total_cash_received = cashFlows.slice(1).reduce((sum, cf) => sum + cf, 0); const total_return = down_payment > 0 ? ((total_cash_received - down_payment) / down_payment) * 100 : 0; return { irr: irr * 100, npv: npv, cash_on_cash: cash_on_cash, total_return: total_return, monthly_cash_flow: monthly_cash_flow, annual_cash_flow: annual_cash_flow, total_investment: down_payment, final_equity: cashFlows[cashFlows.length - 1] - annual_cash_flow }; } calculateRemainingBalance(principal, monthlyRate, totalPayments, paymentsMade) { if (monthlyRate === 0) { return principal * (1 - paymentsMade / totalPayments); } const monthlyPayment = principal * (monthlyRate * Math.pow(1 + monthlyRate, totalPayments)) / (Math.pow(1 + monthlyRate, totalPayments) - 1); const remainingBalance = principal * Math.pow(1 + monthlyRate, paymentsMade) - monthlyPayment * (Math.pow(1 + monthlyRate, paymentsMade) - 1) / monthlyRate; return Math.max(0, remainingBalance); } calculateIRR(cashFlows) { let rate = 0.1; const maxIterations = 100; const tolerance = 0.00001; for (let i = 0; i < maxIterations; i++) { let npv = 0; let dnpv = 0; for (let j = 0; j < cashFlows.length; j++) { npv += cashFlows[j] / Math.pow(1 + rate, j); if (j > 0) { dnpv -= j * cashFlows[j] / Math.pow(1 + rate, j + 1); } } const newRate = rate - npv / dnpv; if (Math.abs(newRate - rate) < tolerance) { return newRate; } rate = newRate; } return rate; } calculateNPV(cashFlows, discountRate) { return cashFlows.reduce((npv, cf, i) => npv + cf / Math.pow(1 + discountRate, i), 0); } formatVariableName(variable) { const names = { purchase_price: 'Purchase Price', rental_income: 'Rental Income', expenses: 'Operating Expenses', vacancy_rate: 'Vacancy Rate', interest_rate: 'Interest Rate', appreciation_rate: 'Appreciation Rate' }; return names[variable] || variable; } getBaseValue(scenario, variable) { const mapping = { purchase_price: scenario.purchase_price, rental_income: scenario.annual_rental_income, expenses: scenario.annual_expenses, vacancy_rate: scenario.vacancy_rate || 5, interest_rate: scenario.interest_rate || 7, appreciation_rate: scenario.appreciation_rate || 3 }; return mapping[variable]; } getVariableValue(scenario, variable) { return this.getBaseValue(scenario, variable); } createScenario(baseScenario, variable, variationPercent) { const scenario = { ...baseScenario }; const baseValue = this.getBaseValue(baseScenario, variable); const newValue = baseValue * (1 + variationPercent / 100); switch (variable) { case 'purchase_price': scenario.purchase_price = newValue; break; case 'rental_income': scenario.annual_rental_income = newValue; break; case 'expenses': scenario.annual_expenses = newValue; break; case 'vacancy_rate': scenario.vacancy_rate = baseValue + variationPercent; // Direct percentage change break; case 'interest_rate': scenario.interest_rate = baseValue + variationPercent * baseValue / 100; break; case 'appreciation_rate': scenario.appreciation_rate = baseValue + variationPercent * baseValue / 100; break; } return scenario; } formatMetrics(metrics, selectedMetrics) { const formatted = {}; selectedMetrics.forEach(metric => { formatted[metric] = parseFloat(metrics[metric].toFixed(2)); }); return formatted; } calculateImpact(baseMetrics, scenarioMetrics, selectedMetrics) { const impact = {}; selectedMetrics.forEach(metric => { const baseValue = baseMetrics[metric]; const scenarioValue = scenarioMetrics[metric]; if (metric === 'monthly_cash_flow' || metric === 'npv') { // Absolute change for dollar amounts impact[metric] = parseFloat((scenarioValue - baseValue).toFixed(2)); } else { // Percentage change for rates impact[metric] = baseValue !== 0 ? parseFloat(((scenarioValue - baseValue) / Math.abs(baseValue) * 100).toFixed(2)) : 0; } }); return impact; } calculateSensitivityMetrics(scenarios) { const metrics = {}; const metricNames = Object.keys(scenarios[0].metrics); metricNames.forEach(metric => { const values = scenarios.map(s => s.metrics[metric]); const variations = scenarios.map(s => s.variation_percent); // Calculate range const range = Math.max(...values) - Math.min(...values); // Calculate elasticity (average % change in output per % change in input) let totalElasticity = 0; let count = 0; for (let i = 0; i < scenarios.length - 1; i++) { if (variations[i + 1] !== variations[i] && values[i] !== 0) { const inputChange = variations[i + 1] - variations[i]; const outputChange = ((values[i + 1] - values[i]) / Math.abs(values[i])) * 100; totalElasticity += Math.abs(outputChange / inputChange); count++; } } metrics[metric] = { range: parseFloat(range.toFixed(2)), elasticity: count > 0 ? parseFloat((totalElasticity / count).toFixed(3)) : 0, min_value: parseFloat(Math.min(...values).toFixed(2)), max_value: parseFloat(Math.max(...values).toFixed(2)) }; }); return metrics; } performTwoWayAnalysis(baseScenario, var1, var2, metric, discountRate) { const variations = [-20, -10, 0, 10, 20]; const results = []; variations.forEach(var1Change => { const row = { var1_change: var1Change, values: [] }; variations.forEach(var2Change => { let scenario = this.createScenario(baseScenario, var1.variable, var1Change); scenario = this.createScenario(scenario, var2.variable, var2Change); const metrics = this.calculateScenarioMetrics(scenario, discountRate); row.values.push(parseFloat(metrics[metric].toFixed(2))); }); results.push(row); }); return { variable1: this.formatVariableName(var1.variable), variable2: this.formatVariableName(var2.variable), metric: metric, var2_changes: variations, data: results }; } createTornadoDiagram(sensitivityResults, primaryMetric) { const data = []; sensitivityResults.forEach(result => { const sensitivity = result.sensitivity_metrics[primaryMetric]; if (sensitivity) { data.push({ variable: result.variable, min_impact: sensitivity.min_value, max_impact: sensitivity.max_value, range: sensitivity.range, elasticity: sensitivity.elasticity }); } }); // Sort by range (highest impact first) data.sort((a, b) => b.range - a.range); return { metric: primaryMetric, variables: data }; } findCriticalValues(baseScenario, variables, discountRate) { const criticalValues = []; variables.forEach(({ variable }) => { // Find break-even point (where IRR = discount rate or NPV = 0) const breakEven = this.findBreakEvenPoint( baseScenario, variable, discountRate ); if (breakEven !== null) { criticalValues.push({ variable: this.formatVariableName(variable), base_value: this.getBaseValue(baseScenario, variable), break_even_value: breakEven.value, break_even_change_percent: breakEven.changePercent, margin_of_safety: parseFloat((100 - Math.abs(breakEven.changePercent)).toFixed(2)) }); } }); return criticalValues; } findBreakEvenPoint(baseScenario, variable, targetRate) { let low = -90; let high = 200; const tolerance = 0.1; const maxIterations = 50; for (let i = 0; i < maxIterations; i++) { const mid = (low + high) / 2; const scenario = this.createScenario(baseScenario, variable, mid); const metrics = this.calculateScenarioMetrics(scenario, targetRate); // Look for NPV = 0 if (Math.abs(metrics.npv) < 100) { return { value: this.getVariableValue(scenario, variable), changePercent: mid }; } if (metrics.npv > 0) { // Need to make investment worse if (variable === 'expenses' || variable === 'purchase_price' || variable === 'interest_rate') { low = mid; } else { high = mid; } } else { // Need to make investment better if (variable === 'expenses' || variable === 'purchase_price' || variable === 'interest_rate') { high = mid; } else { low = mid; } } if (Math.abs(high - low) < tolerance) { break; } } return null; } assessRisk(sensitivityResults, baseMetrics) { let totalElasticity = 0; let maxDownsideRisk = 0; const highSensitivityVars = []; const criticalVariables = []; sensitivityResults.forEach(result => { const primaryMetric = Object.keys(result.sensitivity_metrics)[0]; const sensitivity = result.sensitivity_metrics[primaryMetric]; totalElasticity += sensitivity.elasticity; // Check for high sensitivity (elasticity > 1) if (sensitivity.elasticity > 1) { highSensitivityVars.push({ variable: result.variable, elasticity: sensitivity.elasticity }); } // Calculate downside risk const downsideScenarios = result.scenarios.filter(s => s.variation_percent < 0); const worstCase = Math.min(...downsideScenarios.map(s => s.metrics[primaryMetric])); const downsideRisk = ((baseMetrics[primaryMetric] - worstCase) / Math.abs(baseMetrics[primaryMetric])) * 100; if (downsideRisk > maxDownsideRisk) { maxDownsideRisk = downsideRisk; } // Identify critical variables (where small changes have big impacts) if (sensitivity.elasticity > 1.5 || downsideRisk > 30) { criticalVariables.push(result.variable); } }); const avgElasticity = totalElasticity / sensitivityResults.length; // Determine overall risk level let riskLevel; if (avgElasticity < 0.5 && maxDownsideRisk < 20) { riskLevel = 'Low'; } else if (avgElasticity < 1 && maxDownsideRisk < 40) { riskLevel = 'Medium'; } else { riskLevel = 'High'; } return { overall_risk_level: riskLevel, average_elasticity: parseFloat(avgElasticity.toFixed(3)), max_downside_risk: parseFloat(maxDownsideRisk.toFixed(2)), high_sensitivity_variables: highSensitivityVars, critical_variables: criticalVariables, risk_factors: this.identifyRiskFactors(sensitivityResults, criticalVariables) }; } identifyRiskFactors(sensitivityResults, criticalVariables) { const factors = []; if (criticalVariables.includes('Interest Rate')) { factors.push({ factor: 'Interest Rate Risk', description: 'Investment highly sensitive to rate changes', mitigation: 'Consider fixed-rate financing or rate locks' }); } if (criticalVariables.includes('Rental Income')) { factors.push({ factor: 'Income Risk', description: 'Returns heavily dependent on rental income', mitigation: 'Diversify tenant base, consider long-term leases' }); } if (criticalVariables.includes('Purchase Price')) { factors.push({ factor: 'Valuation Risk', description: 'Returns sensitive to purchase price', mitigation: 'Thorough due diligence and conservative valuations' }); } if (criticalVariables.includes('Vacancy Rate')) { factors.push({ factor: 'Occupancy Risk', description: 'Performance vulnerable to vacancy', mitigation: 'Focus on high-demand locations and tenant retention' }); } return factors; } generateRecommendations(sensitivityResults, riskAssessment, criticalValues) { const recommendations = []; // Risk level recommendations if (riskAssessment.overall_risk_level === 'High') { recommendations.push({ type: 'Risk Management', priority: 'High', message: 'High sensitivity to multiple variables', action: 'Implement hedging strategies and maintain larger reserves' }); } // Critical variable recommendations riskAssessment.critical_variables.forEach(variable => { const critical = criticalValues.find(cv => cv.variable === variable); if (critical && critical.margin_of_safety < 20) { recommendations.push({ type: 'Critical Risk', priority: 'High', message: `Low margin of safety for ${variable} (${critical.margin_of_safety}%)`, action: `Monitor ${variable} closely and develop contingency plans` }); } }); // Elasticity recommendations if (riskAssessment.average_elasticity > 1.5) { recommendations.push({ type: 'Volatility', priority: 'Medium', message: 'High overall sensitivity to input changes', action: 'Consider more stable investment alternatives or risk reduction strategies' }); } // Specific variable recommendations const tornadoData = this.createTornadoDiagram(sensitivityResults, Object.keys(sensitivityResults[0].sensitivity_metrics)[0]); const topRiskVariable = tornadoData.variables[0]; if (topRiskVariable) { recommendations.push({ type: 'Focus Area', priority: 'High', message: `${topRiskVariable.variable} has the highest impact on returns`, action: `Prioritize managing ${topRiskVariable.variable} risk through contracts or hedging` }); } // Positive recommendations const lowRiskVars = sensitivityResults.filter(r => Object.values(r.sensitivity_metrics)[0].elasticity < 0.5 ); if (lowRiskVars.length > 0) { recommendations.push({ type: 'Strength', priority: 'Low', message: `Low sensitivity to ${lowRiskVars.map(v => v.variable).join(', ')}`, action: 'These factors provide stability to the investment' }); } return recommendations; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sigaihealth/realvestmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server