From 25360aacb8aab46e7e579707eb9704759af9536d Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Wed, 20 May 2026 00:22:32 -0700 Subject: feat: implement options tab with Black-Scholes pricer and vol surface Adds a fully interactive options tab: Terminal view (3-column Bloomberg- style with pricer, chain, smile/term-structure/greek curves) and Surface view (polar smile dial + IV heatmap). Uses synthetic vol surface until a live yfinance chain endpoint is wired up. Co-Authored-By: Claude Sonnet 4.6 --- frontend/components/prism/options/OptionsChain.tsx | 157 +++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 frontend/components/prism/options/OptionsChain.tsx (limited to 'frontend/components/prism/options/OptionsChain.tsx') diff --git a/frontend/components/prism/options/OptionsChain.tsx b/frontend/components/prism/options/OptionsChain.tsx new file mode 100644 index 0000000..be6b518 --- /dev/null +++ b/frontend/components/prism/options/OptionsChain.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useMemo } from "react"; +import { bsPrice, bsGreeks, bsSynthIV } from "@/lib/blackScholes"; +import type { ChainRow, OptionType } from "./types"; + +function hashRand(seed: number): number { + const x = Math.sin(seed) * 10000; + return x - Math.floor(x); +} + +export function buildChain(S: number, T: number, r: number, q: number, atmSigma: number, expirySeed: number): ChainRow[] { + const rawMin = Math.round(S * 0.85 / 5) * 5; + const rawMax = Math.round(S * 1.20 / 5) * 5; + const rows: ChainRow[] = []; + + for (let K = rawMin; K <= rawMax; K += 5) { + const iv = bsSynthIV(S, K, T, atmSigma); + const cMid = bsPrice(S, K, T, r, q, iv, 'C'); + const pMid = bsPrice(S, K, T, r, q, iv, 'P'); + const cG = bsGreeks(S, K, T, r, q, iv, 'C'); + const pG = bsGreeks(S, K, T, r, q, iv, 'P'); + const seed = expirySeed * 1000 + K; + const cOi = Math.round(hashRand(seed + 1) * 50000 + 1000); + const pOi = Math.round(hashRand(seed + 2) * 40000 + 800); + const cVol = Math.round(hashRand(seed + 3) * 20000 + 200); + const pVol = Math.round(hashRand(seed + 4) * 15000 + 150); + rows.push({ + K, + cMid, pMid, + cIv: iv, pIv: iv, + cDelta: cG.delta, pDelta: pG.delta, + cOi, pOi, + cVol, pVol, + }); + } + return rows; +} + +export function findAtmStrike(strikes: number[], S: number): number { + return strikes.reduce((prev, curr) => Math.abs(curr - S) < Math.abs(prev - S) ? curr : prev, strikes[0]); +} + +function fmtOi(n: number): string { + if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; + return n.toString(); +} + +interface ChainTableProps { + rows: ChainRow[]; + atmStrike: number; + selectedK: number; + type: OptionType; + onPick: (K: number) => void; + compact?: boolean; +} + +export function ChainTable({ rows, atmStrike, selectedK, type, onPick, compact = false }: ChainTableProps) { + return ( +
+
+

Option Chain

+ {compact ? 'Compact' : 'Full'} · {rows.length} strikes +
+
+ + + + {!compact && } + {compact && } + + {!compact && } + {compact && } + + {!compact ? ( + + + + + + + + + + + + ) : ( + + + + + + + + + + )} + + + {rows.map((row) => { + const isAtm = row.K === atmStrike; + const isSel = row.K === selectedK; + const cItm = type === 'C' ? row.K < atmStrike : row.K > atmStrike; + const pItm = type === 'P' ? row.K < atmStrike : row.K > atmStrike; + return ( + onPick(row.K)} + > + {!compact && } + + + + + + + + {!compact && } + + ); + })} + +
— calls —— calls —strike— puts —— puts —
OIIVlastΔKΔlastIVOI
IVlastΔKΔlastIV
{fmtOi(row.cOi)}{(row.cIv * 100).toFixed(1)}%{row.cMid.toFixed(2)}{row.cDelta.toFixed(2)}{row.K.toFixed(0)}{row.pDelta.toFixed(2)}{row.pMid.toFixed(2)}{(row.pIv * 100).toFixed(1)}%{fmtOi(row.pOi)}
+
+
+ ); +} + +interface OptionsChainProps { + S: number; + T: number; + r: number; + q: number; + atmSigma: number; + expirySeed: number; + selectedK: number; + type: OptionType; + onPick: (K: number) => void; + compact?: boolean; +} + +export function OptionsChain({ S, T, r, q, atmSigma, expirySeed, selectedK, type, onPick, compact }: OptionsChainProps) { + const rows = useMemo(() => buildChain(S, T, r, q, atmSigma, expirySeed), [S, T, r, q, atmSigma, expirySeed]); + const atmStrike = useMemo(() => findAtmStrike(rows.map(rw => rw.K), S), [rows, S]); + + return ( + + ); +} -- cgit v1.3-2-g0d8e