UNPKG

@spaik/mcp-server-roi

Version:

MCP server for AI ROI prediction and tracking with Monte Carlo simulations

231 lines 10.2 kB
import { Piscina } from 'piscina'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { createLogger } from '../../utils/logger.js'; import { WorkerError, CalculationError, TimeoutError } from '../../utils/errors.js'; import { validateIterations, validateFinancialAmount } from '../../utils/validators.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export class MonteCarloSimulator { piscina; logger = createLogger({ component: 'MonteCarloSimulator' }); constructor() { const workerPath = join(__dirname, '../../workers/monte-carlo-worker.js'); const maxThreads = parseInt(process.env.WORKER_POOL_SIZE || '4'); this.logger.info('Initializing Monte Carlo simulator', { workerPath, maxThreads, maxIterations: process.env.MAX_SIMULATION_ITERATIONS || 100000 }); try { this.piscina = new Piscina({ filename: workerPath, maxThreads, idleTimeout: 60000, // 1 minute maxQueue: 100 // Prevent memory issues with large queues }); this.logger.info('Worker pool initialized successfully'); } catch (error) { this.logger.error('Failed to initialize worker pool', error); throw new WorkerError('Failed to initialize Monte Carlo worker pool', { workerPath, error: error.message }); } } async runSimulation(config, projectionId) { const startTime = Date.now(); try { // Validate configuration this.logger.debug('Validating simulation config'); validateIterations(config.iterations); validateFinancialAmount(config.baseCase.monthlyBenefit, 'monthlyBenefit'); validateFinancialAmount(config.baseCase.totalInvestment, 'totalInvestment'); // Check worker pool health if (this.piscina.threads.length === 0) { throw new WorkerError('No worker threads available'); } const workerCount = this.piscina.threads.length; const iterationsPerWorker = Math.ceil(config.iterations / workerCount); this.logger.info('Starting simulation', { projectionId, totalIterations: config.iterations, workerCount, iterationsPerWorker }); // Create tasks for each worker const tasks = []; for (let i = 0; i < workerCount; i++) { const remainingIterations = config.iterations - (i * iterationsPerWorker); if (remainingIterations <= 0) break; tasks.push({ ...config, iterations: Math.min(iterationsPerWorker, remainingIterations), seed: Date.now() + i // Different seed for each worker }); } // Set timeout for worker tasks const timeout = parseInt(process.env.SIMULATION_TIMEOUT || '300000'); // 5 minutes default // Run simulations in parallel with timeout const workerResults = await Promise.race([ Promise.all(tasks.map((task, index) => this.piscina.run(task, { name: 'default' }) .then(result => { if (!result.success) { throw new WorkerError(`Worker ${index} failed: ${result.error.message}`, { workerIndex: index, error: result.error }); } return result; }))), new Promise((_, reject) => setTimeout(() => reject(new TimeoutError('Monte Carlo simulation', timeout)), timeout)) ]); // Extract and validate results const allResults = []; let totalSuccessful = 0; for (const workerResult of workerResults) { if (workerResult.results && Array.isArray(workerResult.results)) { allResults.push(...workerResult.results); totalSuccessful += workerResult.metadata.successfulIterations; } } if (allResults.length === 0) { throw new CalculationError('No simulation results produced'); } const successRate = totalSuccessful / config.iterations; if (successRate < 0.9) { this.logger.warn('Low simulation success rate', { successRate, totalRequested: config.iterations, totalSuccessful }); } // Analyze results this.logger.debug('Analyzing simulation results', { resultCount: allResults.length }); const analysis = this.analyzeResults(allResults); const duration = Date.now() - startTime; this.logger.info('Simulation completed', { projectionId, duration_ms: duration, totalResults: allResults.length, successRate }); return { projection_id: projectionId, simulation_count: allResults.length, run_date: new Date().toISOString(), roi_distribution: analysis.roiDistribution, payback_distribution: analysis.paybackDistribution, risk_analysis: analysis.riskAnalysis }; } catch (error) { this.logger.error('Simulation failed', error, { projectionId }); throw error; } finally { const endTime = Date.now(); this.logger.info('Monte Carlo simulation completed', { duration: endTime - startTime, projectionId }); } } analyzeResults(results) { // Sort ROI values const roiValues = results.map(r => r.roi).sort((a, b) => a - b); const npvValues = results.map(r => r.npv).sort((a, b) => a - b); const paybackValues = results.map(r => r.paybackMonths).sort((a, b) => a - b); // Calculate ROI distribution const roiDistribution = { percentiles: { p5: this.percentile(roiValues, 0.05), p25: this.percentile(roiValues, 0.25), p50: this.percentile(roiValues, 0.50), p75: this.percentile(roiValues, 0.75), p95: this.percentile(roiValues, 0.95) }, mean: this.mean(roiValues), std_dev: this.standardDeviation(roiValues), confidence_interval_95: [ this.percentile(roiValues, 0.025), this.percentile(roiValues, 0.975) ] }; // Calculate payback distribution const paybackDistribution = { percentiles: { 'p10': this.percentile(paybackValues, 0.10), 'p25': this.percentile(paybackValues, 0.25), 'p50': this.percentile(paybackValues, 0.50), 'p75': this.percentile(paybackValues, 0.75), 'p90': this.percentile(paybackValues, 0.90) }, probability_within_12_months: paybackValues.filter(v => v <= 12).length / paybackValues.length, probability_within_24_months: paybackValues.filter(v => v <= 24).length / paybackValues.length }; // Risk analysis const lossCount = npvValues.filter(v => v < 0).length; const valueAtRisk95 = this.percentile(npvValues, 0.05); // Identify key risk drivers through correlation analysis const riskDrivers = this.identifyRiskDrivers(results); return { roiDistribution, paybackDistribution, riskAnalysis: { probability_of_loss: lossCount / results.length, value_at_risk_95: valueAtRisk95, key_risk_drivers: riskDrivers } }; } percentile(sortedValues, p) { const index = Math.ceil(sortedValues.length * p) - 1; return sortedValues[Math.max(0, Math.min(index, sortedValues.length - 1))]; } mean(values) { return values.reduce((sum, val) => sum + val, 0) / values.length; } standardDeviation(values) { const avg = this.mean(values); const squareDiffs = values.map(val => Math.pow(val - avg, 2)); return Math.sqrt(this.mean(squareDiffs)); } identifyRiskDrivers(results) { // Calculate correlations between input variables and ROI const roiValues = results.map(r => r.roi); const factors = [ { name: 'Adoption Rate', values: results.map(r => r.finalAdoptionRate) }, { name: 'Efficiency Gain', values: results.map(r => r.finalEfficiencyGain) }, { name: 'Total Cost', values: results.map(r => r.totalCost) } ]; return factors.map(factor => { const correlation = this.correlation(factor.values, roiValues); const impact = Math.abs(correlation) * 100; return { factor: factor.name, impact_percentage: impact, correlation: correlation }; }).sort((a, b) => b.impact_percentage - a.impact_percentage); } correlation(x, y) { const n = x.length; const sumX = x.reduce((a, b) => a + b, 0); const sumY = y.reduce((a, b) => a + b, 0); const sumXY = x.reduce((total, xi, i) => total + xi * y[i], 0); const sumX2 = x.reduce((total, xi) => total + xi * xi, 0); const sumY2 = y.reduce((total, yi) => total + yi * yi, 0); const numerator = n * sumXY - sumX * sumY; const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); return denominator === 0 ? 0 : numerator / denominator; } async destroy() { await this.piscina.destroy(); } } //# sourceMappingURL=monte-carlo.js.map