"use client"; import { useMemo } from "react"; import { bsPrice, bsGreeks, bsSynthIV } from "@/lib/blackScholes"; import type { Expiry, OptionType } from "./types"; function smoothPath(pts: [number, number][]): string { if (pts.length < 2) return ""; let d = `M ${pts[0][0]} ${pts[0][1]}`; for (let i = 1; i < pts.length; i++) { const [x0, y0] = pts[i - 1]; const [x1, y1] = pts[i]; const cx = (x0 + x1) / 2; d += ` C ${cx} ${y0}, ${cx} ${y1}, ${x1} ${y1}`; } return d; } function linePath(pts: [number, number][]): string { if (!pts.length) return ""; return pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x} ${y}`).join(' '); } interface SmileChartProps { S: number; T: number; r: number; q: number; atmSigma: number; K: number; type: OptionType; expiryLabel: string; allExpiries: Expiry[]; } export function SmileChart({ S, T, atmSigma, K, allExpiries }: SmileChartProps) { const W = 720, H = 240; const pad = { l: 40, r: 20, t: 20, b: 30 }; const iW = W - pad.l - pad.r; const iH = H - pad.t - pad.b; const kMin = S * 0.80, kMax = S * 1.22; const nPts = 60; const primaryPts = useMemo((): [number, number][] => { const pts: [number, number][] = []; for (let i = 0; i <= nPts; i++) { const strike = kMin + (kMax - kMin) * i / nPts; const iv = bsSynthIV(S, strike, T, atmSigma); pts.push([strike, iv]); } return pts; }, [S, T, atmSigma, kMin, kMax]); const otherCurves = useMemo(() => { return allExpiries .filter(e => Math.abs(e.T - T) > 0.001) .slice(0, 3) .map(e => { const pts: [number, number][] = []; for (let i = 0; i <= nPts; i++) { const strike = kMin + (kMax - kMin) * i / nPts; const iv = bsSynthIV(S, strike, e.T, atmSigma); pts.push([strike, iv]); } return pts; }); }, [S, T, atmSigma, allExpiries, kMin, kMax]); const allIvs = primaryPts.map(p => p[1]); const ivMin = Math.max(0, Math.min(...allIvs) - 0.02); const ivMax = Math.max(...allIvs) + 0.04; function toX(strike: number) { return pad.l + ((strike - kMin) / (kMax - kMin)) * iW; } function toY(iv: number) { return pad.t + iH - ((iv - ivMin) / (ivMax - ivMin)) * iH; } const svgPrimary = smoothPath(primaryPts.map(([k, iv]) => [toX(k), toY(iv)] as [number, number])); const fillPath = svgPrimary + ` L ${toX(kMax)} ${pad.t + iH} L ${toX(kMin)} ${pad.t + iH} Z`; const kX = toX(K); const kIv = bsSynthIV(S, K, T, atmSigma); const kY = toY(kIv); const atmX = toX(S); const yTicks = 4; const yLabels: number[] = []; for (let i = 0; i <= yTicks; i++) { yLabels.push(ivMin + (ivMax - ivMin) * i / yTicks); } return (

Vol Smile

Implied Volatility vs. Strike
{yLabels.map(iv => ( {(iv * 100).toFixed(0)}% ))} {otherCurves.map((pts, idx) => ( [toX(k), toY(iv)] as [number, number]))} /> ))} ATM {(kIv * 100).toFixed(1)}%
); } interface TermStructureProps { S: number; r: number; q: number; atmSigma: number; expiries: Expiry[]; selectedT: number; onPickT: (T: number) => void; } export function TermStructure({ S, atmSigma, expiries, selectedT, onPickT }: TermStructureProps) { const W = 360, H = 150; const pad = { l: 36, r: 16, t: 16, b: 30 }; const iW = W - pad.l - pad.r; const iH = H - pad.t - pad.b; const pts = useMemo(() => expiries.map(e => { const iv = bsSynthIV(S, S, e.T, atmSigma); return { T: e.T, sqrtT: Math.sqrt(e.T), iv, label: e.label, dte: e.dte }; }), [S, atmSigma, expiries]); const xs = pts.map(p => p.sqrtT); const ivs = pts.map(p => p.iv); const xMin = Math.min(...xs), xMax = Math.max(...xs); const ivMin = Math.min(...ivs) - 0.01, ivMax = Math.max(...ivs) + 0.01; function toX(sqrtT: number) { return pad.l + ((sqrtT - xMin) / (xMax - xMin + 0.001)) * iW; } function toY(iv: number) { return pad.t + iH - ((iv - ivMin) / (ivMax - ivMin + 0.001)) * iH; } const linePts: [number, number][] = pts.map(p => [toX(p.sqrtT), toY(p.iv)]); return (

Term Structure

ATM IV vs. Expiry
{pts.map((p) => { const cx = toX(p.sqrtT); const cy = toY(p.iv); const isSel = Math.abs(p.T - selectedT) < 0.001; return ( onPickT(p.T)}> {p.label} ); })}
); } interface GreekMiniProps { name: string; glyph: string; kind: 'delta' | 'gamma' | 'vega' | 'theta'; S: number; K: number; T: number; r: number; q: number; sigma: number; type: OptionType; } export function GreekMini({ name, glyph, kind, S, K, T, r, q, sigma, type }: GreekMiniProps) { const W = 160, H = 90; const pad = { l: 8, r: 8, t: 8, b: 8 }; const iW = W - pad.l - pad.r; const iH = H - pad.t - pad.b; const nPts = 40; const pts = useMemo((): [number, number][] => { const sMin = S * 0.75, sMax = S * 1.25; const result: [number, number][] = []; for (let i = 0; i <= nPts; i++) { const spot = sMin + (sMax - sMin) * i / nPts; const g = bsGreeks(spot, K, T, r, q, sigma, type); result.push([spot, g[kind]]); } return result; }, [S, K, T, r, q, sigma, type, kind]); const ys = pts.map(p => p[1]); const yMin = Math.min(...ys), yMax = Math.max(...ys); const xMin = pts[0][0], xMax = pts[pts.length - 1][0]; function toX(v: number) { return pad.l + ((v - xMin) / (xMax - xMin + 0.001)) * iW; } function toY(v: number) { return pad.t + iH - ((v - yMin) / (yMax - yMin + 0.001)) * iH; } const svgPts: [number, number][] = pts.map(([x, y]) => [toX(x), toY(y)]); const curValG = bsGreeks(S, K, T, r, q, sigma, type); const curVal = curValG[kind]; const dotX = toX(S); const dotY = toY(curVal); return (
{glyph} {name} {curVal.toFixed(4)}
); } interface PayoffProps { S: number; K: number; T: number; r: number; q: number; sigma: number; type: OptionType; mid?: number; } export function Payoff({ S, K, T, r, q, sigma, type, mid }: PayoffProps) { const W = 720, H = 140; const pad = { l: 44, r: 16, t: 16, b: 28 }; const iW = W - pad.l - pad.r; const iH = H - pad.t - pad.b; const premium = mid ?? bsPrice(S, K, T, r, q, sigma, type); const sMin = S * 0.70, sMax = S * 1.30; const nPts = 80; const pnlPts = useMemo((): [number, number][] => { const pts: [number, number][] = []; for (let i = 0; i <= nPts; i++) { const spot = sMin + (sMax - sMin) * i / nPts; const payoff = type === 'C' ? Math.max(spot - K, 0) : Math.max(K - spot, 0); pts.push([spot, payoff - premium]); } return pts; }, [K, type, premium, sMin, sMax]); const ys = pnlPts.map(p => p[1]); const yMin = Math.min(...ys, -premium * 1.1); const yMax = Math.max(...ys, premium * 1.1); function toX(v: number) { return pad.l + ((v - sMin) / (sMax - sMin)) * iW; } function toY(v: number) { return pad.t + iH - ((v - yMin) / (yMax - yMin + 0.001)) * iH; } const zeroY = toY(0); const gainPts = pnlPts.filter(p => p[1] >= 0); const lossPts = pnlPts.filter(p => p[1] <= 0); const allSvg = linePath(pnlPts.map(([x, y]) => [toX(x), toY(y)] as [number, number])); const gainArea = gainPts.length > 1 ? linePath(gainPts.map(([x, y]) => [toX(x), toY(y)] as [number, number])) + ` L ${toX(gainPts[gainPts.length - 1][0])} ${zeroY} L ${toX(gainPts[0][0])} ${zeroY} Z` : ''; const lossArea = lossPts.length > 1 ? linePath(lossPts.map(([x, y]) => [toX(x), toY(y)] as [number, number])) + ` L ${toX(lossPts[lossPts.length - 1][0])} ${zeroY} L ${toX(lossPts[0][0])} ${zeroY} Z` : ''; const beSpot = type === 'C' ? K + premium : K - premium; const beX = toX(beSpot); const spotX = toX(S); const kX = toX(K); return (
{gainArea && ( )} {lossArea && ( )} K={K.toFixed(0)} S {beSpot >= sMin && beSpot <= sMax && ( <> BE )}
); }