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 --- .../components/prism/options/OptionsPricer.tsx | 274 +++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 frontend/components/prism/options/OptionsPricer.tsx (limited to 'frontend/components/prism/options/OptionsPricer.tsx') diff --git a/frontend/components/prism/options/OptionsPricer.tsx b/frontend/components/prism/options/OptionsPricer.tsx new file mode 100644 index 0000000..79abe0b --- /dev/null +++ b/frontend/components/prism/options/OptionsPricer.tsx @@ -0,0 +1,274 @@ +"use client"; + +import { useMemo } from "react"; +import { bsPrice, bsGreeks, bsImpliedVol } from "@/lib/blackScholes"; +import type { OptionInputs } from "./types"; + +function fmt(n: number, d = 2): string { + return isNaN(n) || !isFinite(n) ? "—" : n.toFixed(d); +} + +function fmtPct(n: number, d = 1): string { + return isNaN(n) || !isFinite(n) ? "—" : (n * 100).toFixed(d) + "%"; +} + +function pctColor(n: number): string { + if (isNaN(n) || !isFinite(n)) return ""; + return n >= 0 ? "pos" : "neg"; +} + +interface SliderProps { + glyph: string; + label: string; + value: number; + min: number; + max: number; + step: number; + unit?: string; + displayScale?: number; + meta?: string; + onReset?: () => void; + onChange: (v: number) => void; + className?: string; +} + +function Slider({ glyph, value, min, max, step, unit, displayScale = 1, meta, onReset, onChange, className }: SliderProps) { + const displayed = value * displayScale; + return ( +
+ {glyph} + onChange(parseFloat(e.target.value) / displayScale)} + /> +
+ { + const v = parseFloat(e.target.value) / displayScale; + if (!isNaN(v)) onChange(Math.max(min, Math.min(max, v))); + }} + /> + {unit && {unit}} +
+ {(meta || onReset) && ( +
+ {meta && {meta}} + {onReset && } +
+ )} +
+ ); +} + +interface PricerProps { + inputs: OptionInputs; + spot: number; + onChange: (partial: Partial) => void; +} + +export function Pricer({ inputs, spot, onChange }: PricerProps) { + const { S, K, T, r, q, sigma, type } = inputs; + + const fair = useMemo(() => bsPrice(S, K, T, r, q, sigma, type), [S, K, T, r, q, sigma, type]); + const g = useMemo(() => bsGreeks(S, K, T, r, q, sigma, type), [S, K, T, r, q, sigma, type]); + + const mid = bsPrice(S, K, T, r, q, bsImpliedVol(S, K, T, r, q, fair, type), type); + const iv = bsImpliedVol(S, K, T, r, q, fair, type); + const ivPct = isNaN(iv) ? 0.5 : Math.min(1, Math.max(0, (iv - 0.04) / (1.5 - 0.04))); + const sigmaPct = Math.min(1, Math.max(0, (sigma - 0.04) / (1.5 - 0.04))); + + const dte = Math.round(T * 365); + + return ( +
+
+

Pricer

+
+ + +
+
+ +
+ onChange({ S: spot })} + onChange={v => onChange({ S: v })} + /> + onChange({ K: v })} + /> + onChange({ T: v })} + /> + onChange({ r: v })} + /> + onChange({ sigma: v })} + /> + onChange({ q: v })} + /> +
+ +
+
+
Fair Value
+
+ $ + {fmt(fair)} +
+
+ Δ {fmt(g.delta, 4)} +
+
+
+
Market Mid
+
${isNaN(mid) ? '—' : mid.toFixed(2)}
+
+
+ IV +
+
+
+
+ {fmtPct(sigma)} +
+
+ +
+
+ Δ + {fmt(g.delta, 4)} + Delta +
+
+ Γ + {fmt(g.gamma, 4)} + Gamma +
+
+ ν + {fmt(g.vega, 4)} + Vega +
+
+ Θ + {fmt(g.theta, 4)} + Theta/d +
+
+ ρ + {fmt(g.rho, 4)} + Rho +
+
+
+ ); +} + +interface SolvePanelProps { + inputs: OptionInputs; +} + +export function SolvePanel({ inputs }: SolvePanelProps) { + const { S, K, T, r, q, sigma, type } = inputs; + + const fair = useMemo(() => bsPrice(S, K, T, r, q, sigma, type), [S, K, T, r, q, sigma, type]); + const iv = useMemo(() => bsImpliedVol(S, K, T, r, q, fair, type), [S, K, T, r, q, fair, type]); + + const intrinsic = type === 'C' ? Math.max(S - K, 0) : Math.max(K - S, 0); + const extrinsic = Math.max(0, fair - intrinsic); + const breakeven = type === 'C' ? K + fair : K - fair; + + return ( +
+
Analytics
+
+ σ + Implied Vol + {isNaN(iv) ? '—' : (iv * 100).toFixed(2) + '%'} +
+
+ + Breakeven + ${isNaN(breakeven) ? '—' : breakeven.toFixed(2)} +
+
+ + Intrinsic + ${fmt(intrinsic)} +
+
+ + Extrinsic + ${fmt(extrinsic)} +
+
+ ); +} -- cgit v1.3-2-g0d8e