"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
);
}
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
);
}
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 (
);
}