Skip to main content
Glama
Ademscodeisnotsobad

Quant Companion MCP

volSmile.ts6.29 kB
/** * Volatility smile analytics */ import type { OptionContract, VolSmile, VolSmilePoint, OptionsChain } from "./types"; export interface ComputeVolSmileParams { /** Options chain data */ chain: OptionsChain; /** Expiration to analyze (ISO string). If omitted, uses nearest expiration. */ expiration?: string; /** Minimum open interest to include a strike (default: 10) */ minOpenInterest?: number; /** Maximum bid-ask spread % to include (default: 0.5 = 50%) */ maxBidAskSpreadPct?: number; /** Use OTM options only for cleaner smile (default: true) */ useOtmOnly?: boolean; } /** * Compute volatility smile from options chain data */ export function computeVolSmile(params: ComputeVolSmileParams): VolSmile { const { chain, expiration, minOpenInterest = 10, maxBidAskSpreadPct = 0.5, useOtmOnly = true, } = params; const spot = chain.underlyingPrice; const targetExp = expiration ?? chain.expirations[0]; if (!targetExp) { throw new Error("No expiration dates available in options chain"); } // Filter contracts for target expiration const expContracts = chain.contracts.filter((c) => c.expiration === targetExp); if (expContracts.length === 0) { throw new Error(`No contracts found for expiration ${targetExp}`); } const timeToMaturity = expContracts[0].timeToMaturityYears; // Build smile points const points: VolSmilePoint[] = []; const strikeSet = new Set<number>(); for (const contract of expContracts) { // Skip if no IV if (contract.impliedVol == null || contract.impliedVol <= 0) continue; // Skip low liquidity if (contract.openInterest < minOpenInterest) continue; // Calculate bid-ask spread const bidAskSpread = contract.ask - contract.bid; const bidAskSpreadPct = contract.mid > 0 ? bidAskSpread / contract.mid : 1; if (bidAskSpreadPct > maxBidAskSpreadPct) continue; const moneyness = contract.strike / spot; const isOtm = (contract.right === "call" && contract.strike > spot) || (contract.right === "put" && contract.strike < spot); // If useOtmOnly, skip ITM options (to avoid put-call parity issues) if (useOtmOnly && !isOtm && Math.abs(moneyness - 1) > 0.01) continue; // Avoid duplicates at same strike (prefer OTM) const strikeKey = contract.strike; if (strikeSet.has(strikeKey)) continue; strikeSet.add(strikeKey); points.push({ strike: contract.strike, moneyness, logMoneyness: Math.log(moneyness), iv: contract.impliedVol, optionType: contract.right, bidAskSpreadPct, openInterest: contract.openInterest, }); } // Sort by strike points.sort((a, b) => a.strike - b.strike); if (points.length === 0) { throw new Error("No valid IV points found after filtering"); } // Compute ATM IV (interpolate at moneyness = 1.0) const atmIv = interpolateAtmIv(points, spot); // Compute skew metrics const skew = computeSkewMetrics(points, spot, atmIv); return { symbol: chain.symbol, spot, expiration: targetExp, timeToMaturityYears: timeToMaturity, atmIv, points, skew, }; } /** * Interpolate IV at ATM (moneyness = 1.0) */ function interpolateAtmIv(points: VolSmilePoint[], spot: number): number { // Find the two points closest to ATM let below: VolSmilePoint | null = null; let above: VolSmilePoint | null = null; for (const p of points) { if (p.strike <= spot) { if (!below || p.strike > below.strike) below = p; } if (p.strike >= spot) { if (!above || p.strike < above.strike) above = p; } } if (below && above && below !== above) { // Linear interpolation const w = (spot - below.strike) / (above.strike - below.strike); return below.iv + w * (above.iv - below.iv); } // Fallback to closest point return below?.iv ?? above?.iv ?? points[0].iv; } /** * Compute skew metrics from smile points */ function computeSkewMetrics( points: VolSmilePoint[], spot: number, atmIv: number ): VolSmile["skew"] { // Find approximate 25-delta strikes (roughly 5-10% OTM) // For simplicity, use fixed moneyness proxies: 0.95 for puts, 1.05 for calls const putMoneyness = 0.95; const callMoneyness = 1.05; const putIv = findIvAtMoneyness(points, putMoneyness); const callIv = findIvAtMoneyness(points, callMoneyness); let riskReversal25d: number | null = null; let butterfly25d: number | null = null; let simpleSkew: number | null = null; if (putIv !== null && callIv !== null) { // Risk reversal: put IV - call IV (positive means puts are more expensive) riskReversal25d = putIv - callIv; // Butterfly: average of wings minus ATM (positive means smile/wings are elevated) butterfly25d = (putIv + callIv) / 2 - atmIv; // Simple skew simpleSkew = (putIv - callIv) / 2; } return { riskReversal25d, butterfly25d, simpleSkew, }; } /** * Find IV at a specific moneyness level (linear interpolation) */ function findIvAtMoneyness(points: VolSmilePoint[], targetMoneyness: number): number | null { if (points.length === 0) return null; // Find bracketing points let below: VolSmilePoint | null = null; let above: VolSmilePoint | null = null; for (const p of points) { if (p.moneyness <= targetMoneyness) { if (!below || p.moneyness > below.moneyness) below = p; } if (p.moneyness >= targetMoneyness) { if (!above || p.moneyness < above.moneyness) above = p; } } if (below && above && below !== above) { const w = (targetMoneyness - below.moneyness) / (above.moneyness - below.moneyness); return below.iv + w * (above.iv - below.iv); } return below?.iv ?? above?.iv ?? null; } /** * Compute vol smiles for all expirations in a chain */ export function computeVolSmiles( chain: OptionsChain, options?: Omit<ComputeVolSmileParams, "chain" | "expiration"> ): VolSmile[] { const smiles: VolSmile[] = []; for (const exp of chain.expirations) { try { const smile = computeVolSmile({ chain, expiration: exp, ...options, }); smiles.push(smile); } catch { // Skip expirations with insufficient data continue; } } return smiles; }

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