/**
* MCP Tool: get_vol_smile
*
* Fetches options chain and computes volatility smile for analysis.
*/
import { z } from "zod";
import { computeVolSmile, type VolSmile, type QuantError } from "@quant-companion/core";
import { getDefaultProvider } from "../marketData";
import { createError } from "../errors";
// Input schema
export const getVolSmileSchema = z.object({
symbol: z.string().describe("Stock/ETF ticker symbol (e.g., AAPL, SPY, NVDA)"),
expiration: z
.string()
.optional()
.describe("Expiration date in ISO format (YYYY-MM-DD). If omitted, uses nearest expiration."),
minOpenInterest: z
.number()
.min(0)
.default(10)
.describe("Minimum open interest to include a strike (default: 10)"),
maxBidAskSpreadPct: z
.number()
.min(0)
.max(1)
.default(0.5)
.describe("Maximum bid-ask spread as fraction of mid to include (default: 0.5 = 50%)"),
useOtmOnly: z
.boolean()
.default(true)
.describe("Use only OTM options for cleaner smile (default: true)"),
});
export type GetVolSmileInput = z.infer<typeof getVolSmileSchema>;
export interface GetVolSmileOutput {
symbol: string;
spot: number;
expiration: string;
timeToMaturityYears: number;
atmIv: number;
points: Array<{
strike: number;
moneyness: number;
logMoneyness: number;
iv: number;
optionType: "call" | "put";
bidAskSpreadPct: number;
openInterest: number;
}>;
skew: {
riskReversal25d: number | null;
butterfly25d: number | null;
simpleSkew: number | null;
};
interpretation: {
skewDirection: "put" | "call" | "neutral";
skewMagnitude: "steep" | "moderate" | "flat";
smileShape: "smile" | "smirk" | "flat";
};
}
// Tool definition for MCP
export const getVolSmileDefinition = {
name: "get_vol_smile",
description: `Fetch options chain and compute volatility smile (IV vs strike) for a symbol.
Returns:
- ATM implied volatility
- Smile curve points (strike, moneyness, IV)
- Skew metrics (risk reversal, butterfly, simple skew)
- Interpretation (skew direction, magnitude, smile shape)
Use this to analyze:
- Is the skew steep (puts expensive vs calls)?
- Are wings overpriced vs ATM?
- Is the market pricing tail risk?`,
inputSchema: {
type: "object" as const,
properties: {
symbol: {
type: "string",
description: "Stock/ETF ticker symbol (e.g., AAPL, SPY, NVDA)",
},
expiration: {
type: "string",
description:
"Expiration date in ISO format (YYYY-MM-DD). If omitted, uses nearest expiration.",
},
minOpenInterest: {
type: "number",
description: "Minimum open interest to include a strike (default: 10)",
default: 10,
},
maxBidAskSpreadPct: {
type: "number",
description: "Maximum bid-ask spread as fraction of mid (default: 0.5)",
default: 0.5,
},
useOtmOnly: {
type: "boolean",
description: "Use only OTM options for cleaner smile (default: true)",
default: true,
},
},
required: ["symbol"],
},
};
/**
* Interpret the smile metrics
*/
function interpretSmile(smile: VolSmile): GetVolSmileOutput["interpretation"] {
const { riskReversal25d, butterfly25d } = smile.skew;
// Skew direction: positive RR means puts are more expensive
let skewDirection: "put" | "call" | "neutral" = "neutral";
if (riskReversal25d !== null) {
if (riskReversal25d > 0.02) skewDirection = "put";
else if (riskReversal25d < -0.02) skewDirection = "call";
}
// Skew magnitude
let skewMagnitude: "steep" | "moderate" | "flat" = "flat";
if (riskReversal25d !== null) {
const absRR = Math.abs(riskReversal25d);
if (absRR > 0.08) skewMagnitude = "steep";
else if (absRR > 0.03) skewMagnitude = "moderate";
}
// Smile shape: butterfly > 0 means wings elevated (smile), else smirk
let smileShape: "smile" | "smirk" | "flat" = "flat";
if (butterfly25d !== null) {
if (butterfly25d > 0.02) smileShape = "smile";
else if (butterfly25d < -0.01) smileShape = "smirk";
}
return { skewDirection, skewMagnitude, smileShape };
}
/**
* Get vol smile handler
*/
export async function getVolSmile(
input: GetVolSmileInput
): Promise<GetVolSmileOutput | { error: QuantError }> {
const provider = getDefaultProvider();
try {
// Fetch options chain
const chain = await provider.getOptionsChain({
symbol: input.symbol.toUpperCase(),
expiration: input.expiration,
});
// Compute smile
const smile = computeVolSmile({
chain,
expiration: input.expiration,
minOpenInterest: input.minOpenInterest,
maxBidAskSpreadPct: input.maxBidAskSpreadPct,
useOtmOnly: input.useOtmOnly,
});
// Add interpretation
const interpretation = interpretSmile(smile);
return {
symbol: smile.symbol,
spot: smile.spot,
expiration: smile.expiration,
timeToMaturityYears: smile.timeToMaturityYears,
atmIv: smile.atmIv,
points: smile.points,
skew: smile.skew,
interpretation,
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return { error: createError("DATA_NOT_FOUND", `Failed to compute vol smile: ${message}`) };
}
}