"use client"; import { useMemo } from "react"; import { bsPrice, bsSynthIV } from "@/lib/blackScholes"; import type { Expiry, OptionType } from "./types"; interface PolarSmileProps { S: number; r: number; q: number; atmSigma: number; K: number; T: number; type: OptionType; expiries: Expiry[]; selectedExpiryIdx: number; } export function PolarSmile({ S, r, q, atmSigma, K, T, type, expiries, selectedExpiryIdx }: PolarSmileProps) { const SIZE = 680; const cx = SIZE / 2, cy = SIZE / 2; const maxR = SIZE * 0.42; const ivMin = 0.04, ivMax = 0.8; const ivRings = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]; const kMin = S * 0.82, kMax = S * 1.18; const nSpokes = 13; const strikes = useMemo(() => { const arr: number[] = []; for (let i = 0; i < nSpokes; i++) { arr.push(kMin + (kMax - kMin) * i / (nSpokes - 1)); } return arr; }, [kMin, kMax]); function strikeToAngle(strike: number): number { const norm = (strike - kMin) / (kMax - kMin); return -Math.PI / 2 + (norm - 0.5) * Math.PI * 1.5; } function ivToR(iv: number): number { return maxR * Math.max(0, Math.min(1, (iv - ivMin) / (ivMax - ivMin))); } function polarToXY(angle: number, radius: number): [number, number] { return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]; } const fairVal = useMemo(() => bsPrice(S, K, T, r, q, atmSigma, type), [S, K, T, r, q, atmSigma, type]); const curves = useMemo(() => { function _strikeToAngle(strike: number): number { const norm = (strike - kMin) / (kMax - kMin); return -Math.PI / 2 + (norm - 0.5) * Math.PI * 1.5; } function _ivToR(iv: number): number { return maxR * Math.max(0, Math.min(1, (iv - ivMin) / (ivMax - ivMin))); } function _polarToXY(angle: number, radius: number): [number, number] { return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]; } return expiries.map(e => { return strikes.map(strike => { const iv = bsSynthIV(S, strike, e.T, atmSigma); const angle = _strikeToAngle(strike); const radius = _ivToR(iv); return _polarToXY(angle, radius); }); }); }, [S, atmSigma, expiries, strikes, kMin, kMax, maxR, ivMin, ivMax, cx, cy]); function curvePath(pts: [number, number][]): string { return pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`).join(' '); } const selCurve = curves[selectedExpiryIdx]; const kAngle = strikeToAngle(K); const kIv = bsSynthIV(S, K, T, atmSigma); const kR = ivToR(kIv); const [dotX, dotY] = polarToXY(kAngle, kR); const atmAngle = -Math.PI / 2; const eyeR = 28; const legendColors = [ 'var(--fg-4)', 'var(--fg-3)', 'var(--fg-2)', 'var(--fg-2)', 'var(--brass)', 'var(--brass-bright)' ]; return (

Vol Surface polar

{ivRings.map(iv => { const r = ivToR(iv); return ( {(iv * 100).toFixed(0)}% ); })} {strikes.map((strike, i) => { const angle = strikeToAngle(strike); const [x1, y1] = polarToXY(angle, 0); const [x2, y2] = polarToXY(angle, maxR); const isAtm = Math.abs(strike - S) < (kMax - kMin) / (nSpokes * 2); return ( {strike.toFixed(0)} ); })} {curves.map((pts, idx) => { const isSel = idx === selectedExpiryIdx; if (isSel) return null; const color = legendColors[Math.min(idx, legendColors.length - 1)]; return ( ); })} {selCurve && ( )} {type === 'C' ? 'Call' : 'Put'} ${fairVal.toFixed(2)}
{expiries.map((e, idx) => { const color = legendColors[Math.min(idx, legendColors.length - 1)]; const isSel = idx === selectedExpiryIdx; return (
{e.label}
); })}
); } interface IvHeatmapProps { S: number; atmSigma: number; expiries: Expiry[]; selectedExpiryIdx: number; selectedK: number; onSelect: (expiryIdx: number, K: number) => void; } export function IvHeatmap({ S, atmSigma, expiries, selectedExpiryIdx, selectedK, onSelect }: IvHeatmapProps) { const nStrikes = 13; const kMin = S * 0.85, kMax = S * 1.15; const strikes = useMemo(() => { const arr: number[] = []; for (let i = 0; i < nStrikes; i++) { arr.push(Math.round((kMin + (kMax - kMin) * i / (nStrikes - 1)) / 5) * 5); } return arr; }, [kMin, kMax]); const ivGrid = useMemo(() => { return expiries.map(e => strikes.map(K => bsSynthIV(S, K, e.T, atmSigma)) ); }, [S, atmSigma, expiries, strikes]); const allIvs = ivGrid.flat(); const ivGridMin = Math.min(...allIvs); const ivGridMax = Math.max(...allIvs); function cellColor(iv: number): string { const pct = Math.round(((iv - ivGridMin) / (ivGridMax - ivGridMin + 0.001)) * 80); return `color-mix(in oklch, var(--ink-2), var(--brass-deep) ${pct}%)`; } function textColor(iv: number): string { const pct = (iv - ivGridMin) / (ivGridMax - ivGridMin + 0.001); return pct > 0.5 ? 'var(--fg-1)' : 'var(--fg-3)'; } const atmK = Math.round(S / 5) * 5; return (

IV Heatmap surface

{expiries.map(e => ( {e.label} ))}
{ivGrid.map((row, eIdx) => row.map((iv, kIdx) => { const K = strikes[kIdx]; const isCursor = eIdx === selectedExpiryIdx && K === selectedK; return (
onSelect(eIdx, K)} > {(iv * 100).toFixed(0)}%
); }) )}
{strikes.map(K => ( {K} ))}
); }