"use client"; import { useMemo } from "react"; import { bsPrice, bsGreeks, bsSynthIV } from "@/lib/blackScholes"; import type { Expiry, OptionType } from "./types"; interface PolarSmileProps { S: number; K: number; T: number; r: number; q: number; atmSigma: number; type: OptionType; expiries: Expiry[]; selectedT: number; onPickT: (T: number) => void; } export function PolarSmile({ S, K, T, r, q, atmSigma, type, expiries, selectedT, onPickT }: PolarSmileProps) { const W = 680, H = 680; const cx = W / 2, cy = H / 2; const rOuter = 240; const eyeR = 64; // Strike range ±15% moneyness const N = 48; const kMin = S * 0.85; const kMax = S * 1.15; function strikeAt(i: number): number { return kMin + (i / N) * (kMax - kMin); } // Piecewise angle: puts descend LEFT (counterclockwise), calls descend RIGHT (clockwise). // ATM → top (−π/2); both wings meet at BOTTOM (−3π/2 ≡ +π/2). Full closed lens. function angleFor(K_: number): number { if (K_ <= S) { const t = (S - K_) / (S - kMin || 1); return -Math.PI / 2 - t * Math.PI; } const t = (K_ - S) / (kMax - S || 1); return -Math.PI / 2 + t * Math.PI; } // Strike tick labels — round multiples of a step fitted to the price range const rawStep = (kMax - kMin) / 8; const tickStep = rawStep >= 40 ? 50 : rawStep >= 20 ? 25 : rawStep >= 8 ? 10 : 5; const tickStrikes: number[] = []; for (let kk = Math.ceil(kMin / tickStep) * tickStep; kk <= kMax; kk += tickStep) { tickStrikes.push(kk); } // Dynamic IV range across all expiries → defines radial scale const { ivLo, ivHi } = useMemo(() => { let lo = Infinity, hi = -Infinity; expiries.forEach(e => { for (let i = 0; i <= N; i++) { const iv = bsSynthIV(S, strikeAt(i), e.T, atmSigma); if (iv < lo) lo = iv; if (iv > hi) hi = iv; } }); return { ivLo: Math.max(0.05, lo - 0.02), ivHi: hi + 0.02 }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [S, atmSigma, expiries]); // Radial scale: inner floor 60px, outer wall rOuter function ivToR(iv: number): number { return 60 + ((iv - ivLo) / (ivHi - ivLo)) * (rOuter - 60); } // 5 evenly-spaced IV ring labels const ivLabels = useMemo( () => Array.from({ length: 5 }, (_, step) => ivLo + (step / 4) * (ivHi - ivLo)), [ivLo, ivHi] ); // Smooth closed curves (N+1 samples, joined with Z so wings meet at bottom) const curves = useMemo(() => { return expiries.map(e => { const pts: [number, number][] = []; for (let i = 0; i <= N; i++) { const Kk = strikeAt(i); const iv = bsSynthIV(S, Kk, e.T, atmSigma); const a = angleFor(Kk); const rr = ivToR(iv); pts.push([cx + Math.cos(a) * rr, cy + Math.sin(a) * rr]); } return { expiry: e, pts, isSelected: Math.abs(e.T - selectedT) < 1e-6 }; }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [S, atmSigma, expiries, selectedT, ivLo, ivHi]); function pathD(pts: [number, number][]): string { const inner = pts.map(([px, py]) => `${px.toFixed(2)} ${py.toFixed(2)}`).join(' L '); return `M ${inner} Z`; } function styleFor(idx: number, isSelected: boolean) { if (isSelected) return { stroke: 'var(--brass-bright)', strokeWidth: 2.5, strokeDasharray: undefined, fill: 'rgba(194,170,122,0.12)', }; const strokes = ['var(--fg-1)', 'var(--fg-2)', 'var(--fg-3)', 'var(--fg-4)']; const dashes = [undefined, '4 3', '5 4', '2 4']; const widths = [1.4, 1.2, 1.2, 1.1]; const i = Math.min(3, idx); return { stroke: strokes[i], strokeWidth: widths[i], strokeDasharray: dashes[i], fill: 'none' }; } // Selected expiry object const selectedExpiry = expiries.find(e => Math.abs(e.T - selectedT) < 1e-6) ?? expiries[0]; // Current K dot on selected-expiry curve const curIv = bsSynthIV(S, K, selectedExpiry?.T ?? T, atmSigma); const aCur = angleFor(K); const rCur = ivToR(curIv); const dotX = cx + Math.cos(aCur) * rCur; const dotY = cy + Math.sin(aCur) * rCur; const dotLabelRight = aCur > -Math.PI / 2 && aCur < Math.PI / 2; // Fair value + greeks for selected K & expiry const selT = selectedExpiry?.T ?? T; const fair = useMemo(() => bsPrice(S, K, selT, r, q, atmSigma, type), [S, K, selT, r, q, atmSigma, type]); const gr = useMemo(() => bsGreeks(S, K, selT, r, q, atmSigma, type), [S, K, selT, r, q, atmSigma, type]); return (