summaryrefslogtreecommitdiff
path: root/frontend/lib
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-20 00:22:32 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-20 00:22:32 -0700
commit25360aacb8aab46e7e579707eb9704759af9536d (patch)
tree028f654f97dc23c7bc088bc3b625185f4fb71287 /frontend/lib
parent68b4f52829cdb2d6951faf8037fb002083ebd0a5 (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'frontend/lib')
-rw-r--r--frontend/lib/blackScholes.ts81
-rw-r--r--frontend/lib/overview.ts2
2 files changed, 82 insertions, 1 deletions
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 }