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/lib/blackScholes.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++ frontend/lib/overview.ts | 2 +- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 frontend/lib/blackScholes.ts (limited to 'frontend/lib') diff --git a/frontend/lib/blackScholes.ts b/frontend/lib/blackScholes.ts new file mode 100644 index 0000000..b4c831a --- /dev/null +++ b/frontend/lib/blackScholes.ts @@ -0,0 +1,81 @@ +export type OptionType = 'C' | 'P'; + +export interface Greeks { + delta: number; + gamma: number; + vega: number; + theta: number; + rho: number; +} + +function erf(x: number): number { + const sign = x < 0 ? -1 : 1; + x = Math.abs(x); + const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741, + a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911; + const t = 1.0 / (1.0 + p * x); + const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); + return sign * y; +} + +function normCdf(x: number): number { + return 0.5 * (1 + erf(x / Math.SQRT2)); +} + +function normPdf(x: number): number { + return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI); +} + +function d1d2(S: number, K: number, T: number, r: number, q: number, sigma: number): [number, number] { + const vt = sigma * Math.sqrt(T); + const d1 = (Math.log(S / K) + (r - q + 0.5 * sigma * sigma) * T) / vt; + return [d1, d1 - vt]; +} + +export function bsPrice(S: number, K: number, T: number, r: number, q: number, sigma: number, type: OptionType): number { + if (T <= 0 || sigma <= 0) return type === 'C' ? Math.max(S - K, 0) : Math.max(K - S, 0); + const [d1, d2] = d1d2(S, K, T, r, q, sigma); + const eqt = Math.exp(-q * T), ert = Math.exp(-r * T); + if (type === 'C') return S * eqt * normCdf(d1) - K * ert * normCdf(d2); + return K * ert * normCdf(-d2) - S * eqt * normCdf(-d1); +} + +export function bsGreeks(S: number, K: number, T: number, r: number, q: number, sigma: number, type: OptionType): Greeks { + const safeT = Math.max(T, 1e-6), safeS = Math.max(sigma, 1e-6); + const [d1, d2] = d1d2(S, K, safeT, r, q, safeS); + const eqt = Math.exp(-q * safeT), ert = Math.exp(-r * safeT); + const pdf1 = normPdf(d1); + const delta = type === 'C' ? eqt * normCdf(d1) : -eqt * normCdf(-d1); + const gamma = eqt * pdf1 / (S * safeS * Math.sqrt(safeT)); + const vega = S * eqt * pdf1 * Math.sqrt(safeT) * 0.01; + const thetaYr = type === 'C' + ? (-S * pdf1 * safeS * eqt / (2 * Math.sqrt(safeT)) - r * K * ert * normCdf(d2) + q * S * eqt * normCdf(d1)) + : (-S * pdf1 * safeS * eqt / (2 * Math.sqrt(safeT)) + r * K * ert * normCdf(-d2) - q * S * eqt * normCdf(-d1)); + const theta = thetaYr / 365; + const rho = type === 'C' + ? K * safeT * ert * normCdf(d2) * 0.01 + : -K * safeT * ert * normCdf(-d2) * 0.01; + return { delta, gamma, vega, theta, rho }; +} + +export function bsImpliedVol(S: number, K: number, T: number, r: number, q: number, mktPrice: number, type: OptionType): number { + if (T <= 0) return NaN; + let lo = 0.0001, hi = 5.0; + if (mktPrice < bsPrice(S, K, T, r, q, lo, type) || mktPrice > bsPrice(S, K, T, r, q, hi, type)) return NaN; + for (let i = 0; i < 100; i++) { + const mid = (lo + hi) / 2; + const pMid = bsPrice(S, K, T, r, q, mid, type); + if (Math.abs(pMid - mktPrice) < 1e-6) return mid; + if (pMid < mktPrice) lo = mid; else hi = mid; + } + return (lo + hi) / 2; +} + +export function bsSynthIV(S: number, K: number, T: number, atmSigma: number): number { + const logM = Math.log(K / S); + const t = Math.max(T, 1 / 365); + const termLift = 0.014 * (Math.sqrt(t) - Math.sqrt(30 / 365)); + const skew = -0.085 * logM / Math.sqrt(t * 4); + const smile = 0.32 * (logM * logM) / Math.sqrt(t); + return Math.max(0.04, Math.min(1.5, atmSigma + termLift + skew + smile)); +} diff --git a/frontend/lib/overview.ts b/frontend/lib/overview.ts index 6993397..bb63e83 100644 --- a/frontend/lib/overview.ts +++ b/frontend/lib/overview.ts @@ -19,7 +19,7 @@ export const OVERVIEW_NAV_ITEMS: NavItem[] = [ { key: "overview", label: "Overview", icon: "chart" }, { key: "financials", label: "Financials", icon: "ledger" }, { key: "valuation", label: "Valuation", icon: "dollar" }, - { key: "options", label: "Options", icon: "window", disabled: true }, + { key: "options", label: "Options", icon: "window" }, { key: "insiders", label: "Insiders", icon: "pulse", disabled: true }, { key: "filings", label: "Filings", icon: "folder", disabled: true }, { key: "news", label: "News", icon: "terminal", disabled: true } -- cgit v1.3-2-g0d8e