Skip to main content
Glama
Ademscodeisnotsobad

Quant Companion MCP

compareModelsForecast.ts10.5 kB
/** * MCP Tool: compare_models_forecast_distribution * * Compare price distribution forecasts across multiple models: * GBM, Local Vol, SABR, and Heston. */ import { z } from "zod"; import { simulateTerminalPrices, simulateWithLocalVol, computeVolSurface, computeHistoricalVol, computeVolSmile, calibrateSabrSmile, sampleSabrDistribution, simulateHestonPaths, calibrateHestonSurface, computeDistributionStats, computeDisagreement, daysToYears, type ForecastDistribution, type DisagreementMetrics, } from "@quant-companion/core"; import { getDefaultProvider } from "../marketData"; export const compareModelsForecastSchema = z.object({ symbol: z.string().describe("Stock/ETF ticker symbol"), horizonDays: z.number().int().min(1).max(365).describe("Forecast horizon in days"), models: z .array(z.enum(["gbm", "local_vol", "sabr", "heston"])) .optional() .default(["gbm", "local_vol", "sabr", "heston"]) .describe("Models to compare (default: all)"), paths: z .number() .int() .min(1000) .max(100000) .optional() .default(30000) .describe("Simulation paths per model"), }); export type CompareModelsForecastInput = z.infer<typeof compareModelsForecastSchema>; export interface ModelForecast { name: "gbm" | "local_vol" | "sabr" | "heston"; available: boolean; error?: string; mean?: number; median?: number; std?: number; percentile5?: number; percentile25?: number; percentile75?: number; percentile95?: number; probUp20?: number; } export interface CompareModelsForecastOutput { symbol: string; spot: number; horizonDays: number; horizonYears: number; riskFreeRate: number; historicalVol: number; models: ModelForecast[]; disagreementIndex: DisagreementMetrics; summary: string; } export const compareModelsForecastDefinition = { name: "compare_models_forecast_distribution", description: `Compare price forecasts across multiple stochastic models. Models available: - GBM: Standard Geometric Brownian Motion with historical vol - Local Vol: Uses implied vol surface for skew-adjusted simulation - SABR: Stochastic alpha-beta-rho model calibrated to near-term smile - Heston: Stochastic volatility model with mean reversion Returns: - Distribution stats per model (mean, median, percentiles) - Probability of +20% move for each model - Model disagreement index (LOW/MEDIUM/HIGH) Use this to understand model uncertainty and tail risk differences.`, inputSchema: { type: "object" as const, properties: { symbol: { type: "string", description: "Stock/ETF ticker symbol" }, horizonDays: { type: "number", description: "Forecast horizon in days (1-365)" }, models: { type: "array", items: { type: "string", enum: ["gbm", "local_vol", "sabr", "heston"] }, description: "Models to compare (default: all)", }, paths: { type: "number", description: "Simulation paths (default: 30000)" }, }, required: ["symbol", "horizonDays"], }, }; export async function compareModelsForecast( input: CompareModelsForecastInput ): Promise<CompareModelsForecastOutput> { const provider = getDefaultProvider(); const symbol = input.symbol.toUpperCase(); const horizonYears = daysToYears(input.horizonDays); const paths = input.paths; // Fetch market data const quote = await provider.getQuote(symbol); const spot = quote.price; // Get rate and dividend yield const rateCurve = provider.getRateCurve ? await provider.getRateCurve() : [{ maturityYears: 1, rate: 0.045 }]; const rate = rateCurve[0]?.rate ?? 0.045; const dividendYield = provider.getDividendYield ? await provider.getDividendYield(symbol) : 0; // Get historical volatility const end = new Date(); const start = new Date(); start.setDate(start.getDate() - 60); const historical = await provider.getHistoricalOHLCV({ symbol, start, end, interval: "1d" }); const closes = historical.map((c) => c.close); const hvResult = computeHistoricalVol(closes, 30); const historicalVol = hvResult.volatility; const targetPrice = spot * 1.2; // +20% target const models: ModelForecast[] = []; // GBM Model if (input.models.includes("gbm")) { try { const terminalPrices = simulateTerminalPrices({ spot, rate, vol: historicalVol, timeToMaturity: horizonYears, dividendYield, paths, }); const stats = computeDistributionStats(terminalPrices, targetPrice); models.push({ name: "gbm", available: true, mean: stats.mean, median: stats.median, std: stats.std, percentile5: stats.percentile5, percentile25: stats.percentile25, percentile75: stats.percentile75, percentile95: stats.percentile95, probUp20: stats.probAboveTarget, }); } catch (e) { models.push({ name: "gbm", available: false, error: e instanceof Error ? e.message : "Unknown error", }); } } // Local Vol Model if (input.models.includes("local_vol")) { try { const chain = await provider.getOptionsChain({ symbol }); const surface = computeVolSurface({ chain, maxExpirations: 6 }); const lvResult = simulateWithLocalVol({ spot, rate, dividendYield, timeHorizonYears: horizonYears, surface, paths, steps: 50, targetPrice, }); models.push({ name: "local_vol", available: true, mean: lvResult.distribution.mean, median: lvResult.distribution.median, std: lvResult.distribution.stdDev, percentile5: lvResult.distribution.percentile5, percentile25: lvResult.distribution.percentile25, percentile75: lvResult.distribution.percentile75, percentile95: lvResult.distribution.percentile95, probUp20: lvResult.targetProbability, }); } catch (e) { models.push({ name: "local_vol", available: false, error: e instanceof Error ? e.message : "Unknown error", }); } } // SABR Model if (input.models.includes("sabr")) { try { const chain = await provider.getOptionsChain({ symbol }); // Get the nearest expiration smile const smile = computeVolSmile({ chain, minOpenInterest: 5 }); // Calibrate SABR to the smile const forward = spot * Math.exp((rate - dividendYield) * smile.timeToMaturityYears); const strikes = smile.points.map((p) => p.strike); const marketIvs = smile.points.map((p) => p.iv); if (strikes.length < 3) { throw new Error("Insufficient strikes for SABR calibration"); } const sabrParams = calibrateSabrSmile({ forward, timeToMaturityYears: horizonYears, strikes, marketIvs, beta: 0.5, // Use backbone model }); // Sample from SABR distribution const terminalPrices = sampleSabrDistribution( { forward, timeToMaturityYears: horizonYears, alpha: sabrParams.alpha, beta: sabrParams.beta, rho: sabrParams.rho, nu: sabrParams.nu, }, rate, paths ); const stats = computeDistributionStats(terminalPrices, targetPrice); models.push({ name: "sabr", available: true, mean: stats.mean, median: stats.median, std: stats.std, percentile5: stats.percentile5, percentile25: stats.percentile25, percentile75: stats.percentile75, percentile95: stats.percentile95, probUp20: stats.probAboveTarget, }); } catch (e) { models.push({ name: "sabr", available: false, error: e instanceof Error ? e.message : "Unknown error", }); } } // Heston Model if (input.models.includes("heston")) { try { // Use simplified Heston with estimated parameters // In production, would calibrate to vol surface const v0 = historicalVol * historicalVol; const theta = v0; // Long-term variance = current variance const kappa = 2.0; // Mean reversion speed const xi = 0.4; // Vol of vol const rho = -0.7; // Typical negative correlation const hestonResult = simulateHestonPaths({ spot, rate, kappa, theta, xi, rho, v0, timeToMaturityYears: horizonYears, steps: 50, paths, dividendYield, }); const stats = computeDistributionStats(hestonResult.terminalPrices, targetPrice); models.push({ name: "heston", available: true, mean: stats.mean, median: stats.median, std: stats.std, percentile5: stats.percentile5, percentile25: stats.percentile25, percentile75: stats.percentile75, percentile95: stats.percentile95, probUp20: stats.probAboveTarget, }); } catch (e) { models.push({ name: "heston", available: false, error: e instanceof Error ? e.message : "Unknown error", }); } } // Compute disagreement across available models const availableForecasts = models .filter((m) => m.available && m.median !== undefined) .map((m) => ({ median: m.median!, percentile95: m.percentile95!, probUp20: m.probUp20!, })); const disagreementIndex = computeDisagreement(availableForecasts); // Generate summary const availableCount = models.filter((m) => m.available).length; const medians = models.filter((m) => m.available).map((m) => m.median!); const avgMedian = medians.length > 0 ? medians.reduce((a, b) => a + b, 0) / medians.length : 0; const probs = models.filter((m) => m.available).map((m) => m.probUp20!); const avgProbUp20 = probs.length > 0 ? probs.reduce((a, b) => a + b, 0) / probs.length : 0; const summary = `${availableCount}/${input.models.length} models computed. ` + `Average median forecast: $${avgMedian.toFixed(2)} (${((avgMedian / spot - 1) * 100).toFixed(1)}%). ` + `Average prob of +20%: ${(avgProbUp20 * 100).toFixed(1)}%. ` + `Model disagreement: ${disagreementIndex.level}.`; return { symbol, spot, horizonDays: input.horizonDays, horizonYears, riskFreeRate: rate, historicalVol, models, disagreementIndex, summary, }; }

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/Ademscodeisnotsobad/Quant-Companion-MCP'

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