/**
* MCP Tool: simulate_price
*
* Simulate future price paths and analyze probability distributions.
*/
import { z } from "zod";
import { simulateTerminalPrices, mean, stdDev, percentiles } from "@quant-companion/core";
import { LIMITS } from "../errors";
export const simulatePriceSchema = z.object({
spot: z.number().positive().describe("Current spot price"),
rate: z
.number()
.describe("Risk-free rate (annualized, e.g., 0.05 for 5%)"),
vol: z
.number()
.positive()
.describe("Volatility (annualized, e.g., 0.25 for 25%)"),
timeHorizonYears: z
.number()
.positive()
.describe("Time horizon in years (e.g., 0.5 for 6 months)"),
dividendYield: z
.number()
.optional()
.default(0)
.describe("Dividend yield (annualized, default 0)"),
paths: z
.number()
.int()
.positive()
.max(LIMITS.MAX_PATHS)
.default(LIMITS.DEFAULT_PATHS)
.describe(`Number of simulation paths (default ${LIMITS.DEFAULT_PATHS}, max ${LIMITS.MAX_PATHS})`),
targetPrice: z
.number()
.positive()
.optional()
.describe("Optional target price to calculate probability of reaching"),
});
export type SimulatePriceInput = z.infer<typeof simulatePriceSchema>;
export interface PriceDistribution {
mean: number;
median: number;
stdDev: number;
percentile5: number;
percentile25: number;
percentile75: number;
percentile95: number;
min: number;
max: number;
}
export interface TargetAnalysis {
targetPrice: number;
targetReturnPercent: number;
probabilityAbove: number;
probabilityBelow: number;
}
export interface SimulatePriceOutput {
spot: number;
timeHorizonYears: number;
paths: number;
distribution: PriceDistribution;
expectedReturn: number;
expectedReturnPercent: number;
targetAnalysis?: TargetAnalysis;
}
export function simulatePrice(input: SimulatePriceInput): SimulatePriceOutput {
// Enforce limits with specific error for LIMIT_EXCEEDED
if (input.paths > LIMITS.MAX_PATHS) {
const err = new Error(`Limit exceeded: paths must be <= ${LIMITS.MAX_PATHS}`);
err.name = "LIMIT_EXCEEDED";
throw err;
}
const terminalPrices = simulateTerminalPrices({
spot: input.spot,
rate: input.rate,
vol: input.vol,
timeToMaturity: input.timeHorizonYears,
dividendYield: input.dividendYield,
paths: input.paths,
});
// Compute percentiles efficiently
const pctls = percentiles(terminalPrices, [0.05, 0.25, 0.5, 0.75, 0.95]);
// Compute stats
const sorted = [...terminalPrices].sort((a, b) => a - b);
const n = sorted.length;
const avgPrice = mean(terminalPrices);
const distribution: PriceDistribution = {
mean: avgPrice,
median: pctls[0.5],
stdDev: stdDev(terminalPrices),
percentile5: pctls[0.05],
percentile25: pctls[0.25],
percentile75: pctls[0.75],
percentile95: pctls[0.95],
min: sorted[0],
max: sorted[n - 1],
};
const expectedReturn = avgPrice - input.spot;
const expectedReturnPercent = (expectedReturn / input.spot) * 100;
const result: SimulatePriceOutput = {
spot: input.spot,
timeHorizonYears: input.timeHorizonYears,
paths: input.paths,
distribution,
expectedReturn,
expectedReturnPercent,
};
// Target analysis if specified
if (input.targetPrice !== undefined) {
const aboveTarget = terminalPrices.filter((p) => p >= input.targetPrice!).length;
const probabilityAbove = aboveTarget / n;
result.targetAnalysis = {
targetPrice: input.targetPrice,
targetReturnPercent: ((input.targetPrice - input.spot) / input.spot) * 100,
probabilityAbove,
probabilityBelow: 1 - probabilityAbove,
};
}
return result;
}
export const simulatePriceDefinition = {
name: "simulate_price",
description:
`Simulate future price paths using Geometric Brownian Motion and analyze the probability distribution of terminal prices. Optionally calculate the probability of reaching a target price. Use this to answer questions like 'What's the probability NVDA ends 20% higher in 6 months?' Max ${LIMITS.MAX_PATHS} paths.`,
inputSchema: {
type: "object",
properties: {
spot: {
type: "number",
description: "Current spot price",
},
rate: {
type: "number",
description:
"Risk-free rate (annualized decimal, e.g., 0.05 for 5%)",
},
vol: {
type: "number",
description: "Volatility (annualized decimal, e.g., 0.25 for 25%)",
},
timeHorizonYears: {
type: "number",
description: "Time horizon in years (e.g., 0.5 for 6 months)",
},
dividendYield: {
type: "number",
description: "Dividend yield (annualized decimal, default 0)",
},
paths: {
type: "number",
description: `Number of simulation paths (default ${LIMITS.DEFAULT_PATHS}, max ${LIMITS.MAX_PATHS})`,
},
targetPrice: {
type: "number",
description:
"Optional target price to calculate probability of reaching",
},
},
required: ["spot", "rate", "vol", "timeHorizonYears"],
},
};