Skip to main content
Glama
Ademscodeisnotsobad

Quant Companion MCP

volSurface.ts6.04 kB
/** * Volatility surface analytics */ import type { OptionsChain, VolSurface, VolSurfacePoint, VolSmile } from "./types"; import { computeVolSmile, type ComputeVolSmileParams } from "./volSmile"; export interface ComputeVolSurfaceParams { /** Options chain with multiple expirations */ chain: OptionsChain; /** Minimum open interest filter (default: 10) */ minOpenInterest?: number; /** Maximum bid-ask spread % (default: 0.5) */ maxBidAskSpreadPct?: number; /** Use OTM only (default: true) */ useOtmOnly?: boolean; /** Maximum number of expirations to include (default: all) */ maxExpirations?: number; } /** * Compute full volatility surface from options chain */ export function computeVolSurface(params: ComputeVolSurfaceParams): VolSurface { const { chain, minOpenInterest = 10, maxBidAskSpreadPct = 0.5, useOtmOnly = true, maxExpirations, } = params; const spot = chain.underlyingPrice; const expirations = maxExpirations ? chain.expirations.slice(0, maxExpirations) : chain.expirations; // Compute smile for each expiration const smiles: VolSmile[] = []; const allPoints: VolSurfacePoint[] = []; const termStructure: VolSurface["termStructure"] = []; for (const exp of expirations) { try { const smile = computeVolSmile({ chain, expiration: exp, minOpenInterest, maxBidAskSpreadPct, useOtmOnly, }); smiles.push(smile); // Add to term structure termStructure.push({ expiration: exp, timeToMaturityYears: smile.timeToMaturityYears, atmIv: smile.atmIv, }); // Convert smile points to surface points for (const p of smile.points) { allPoints.push({ strike: p.strike, moneyness: p.moneyness, timeToMaturityYears: smile.timeToMaturityYears, expiration: exp, iv: p.iv, }); } } catch { // Skip expirations with insufficient data continue; } } if (smiles.length === 0) { throw new Error("No valid smiles could be computed from the options chain"); } // Compute stats const ivValues = allPoints.map((p) => p.iv); const minIv = Math.min(...ivValues); const maxIv = Math.max(...ivValues); const avgAtmIv = termStructure.reduce((sum, t) => sum + t.atmIv, 0) / termStructure.length; // Term structure slope: (long dated ATM IV - short dated ATM IV) / time diff let termSlope: number | null = null; if (termStructure.length >= 2) { const first = termStructure[0]; const last = termStructure[termStructure.length - 1]; const timeDiff = last.timeToMaturityYears - first.timeToMaturityYears; if (timeDiff > 0) { termSlope = (last.atmIv - first.atmIv) / timeDiff; } } return { symbol: chain.symbol, spot, asOf: chain.asOf, points: allPoints, smiles, termStructure, stats: { minIv, maxIv, avgAtmIv, termSlope, }, }; } /** * Interpolate IV at a specific (strike, maturity) point on the surface */ export function interpolateSurfaceIv( surface: VolSurface, strike: number, timeToMaturity: number ): number | null { const { smiles, spot } = surface; if (smiles.length === 0) return null; // Find bracketing expirations const sortedSmiles = [...smiles].sort( (a, b) => a.timeToMaturityYears - b.timeToMaturityYears ); let below: VolSmile | null = null; let above: VolSmile | null = null; for (const smile of sortedSmiles) { if (smile.timeToMaturityYears <= timeToMaturity) { below = smile; } if (smile.timeToMaturityYears >= timeToMaturity && !above) { above = smile; } } // Interpolate IV at strike for each bracketing smile const moneyness = strike / spot; const ivBelow = below ? interpolateSmileIv(below, moneyness) : null; const ivAbove = above ? interpolateSmileIv(above, moneyness) : null; if (ivBelow !== null && ivAbove !== null && below && above && below !== above) { // Linear interpolation in time const w = (timeToMaturity - below.timeToMaturityYears) / (above.timeToMaturityYears - below.timeToMaturityYears); return ivBelow + w * (ivAbove - ivBelow); } return ivBelow ?? ivAbove; } /** * Interpolate IV at a specific moneyness on a smile */ function interpolateSmileIv(smile: VolSmile, targetMoneyness: number): number | null { const { points } = smile; if (points.length === 0) return null; // Find bracketing points let below: typeof points[0] | null = null; let above: typeof points[0] | 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; } /** * Get a slice of the surface at a fixed maturity */ export function getMaturitySlice( surface: VolSurface, expiration: string ): VolSmile | null { return surface.smiles.find((s) => s.expiration === expiration) ?? null; } /** * Get a slice of the surface at a fixed strike (term structure at that strike) */ export function getStrikeSlice( surface: VolSurface, strike: number ): Array<{ expiration: string; timeToMaturityYears: number; iv: number }> { const result: Array<{ expiration: string; timeToMaturityYears: number; iv: number }> = []; for (const smile of surface.smiles) { const iv = interpolateSmileIv(smile, strike / surface.spot); if (iv !== null) { result.push({ expiration: smile.expiration, timeToMaturityYears: smile.timeToMaturityYears, iv, }); } } return result.sort((a, b) => a.timeToMaturityYears - b.timeToMaturityYears); }

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