diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-20 00:22:32 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-20 00:22:32 -0700 |
| commit | 25360aacb8aab46e7e579707eb9704759af9536d (patch) | |
| tree | 028f654f97dc23c7bc088bc3b625185f4fb71287 /frontend/components/prism/options | |
| parent | 68b4f52829cdb2d6951faf8037fb002083ebd0a5 (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/components/prism/options')
| -rw-r--r-- | frontend/components/prism/options/OptionsChain.tsx | 157 | ||||
| -rw-r--r-- | frontend/components/prism/options/OptionsCharts.tsx | 335 | ||||
| -rw-r--r-- | frontend/components/prism/options/OptionsPage.tsx | 292 | ||||
| -rw-r--r-- | frontend/components/prism/options/OptionsPricer.tsx | 274 | ||||
| -rw-r--r-- | frontend/components/prism/options/OptionsSurface.tsx | 272 | ||||
| -rw-r--r-- | frontend/components/prism/options/types.ts | 47 |
6 files changed, 1377 insertions, 0 deletions
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 ( + <div className={`opt-chain-wrap${compact ? ' compact' : ''}`}> + <div className="opt-chain-head"> + <h3>Option Chain</h3> + <span className="sub">{compact ? 'Compact' : 'Full'} · {rows.length} strikes</span> + </div> + <div className="opt-chain-scroll"> + <table className="opt-chain"> + <thead> + <tr> + {!compact && <th className="group side-c" colSpan={2} style={{ textAlign: 'center' }}>— calls —</th>} + {compact && <th className="group side-c" colSpan={3} style={{ textAlign: 'center' }}>— calls —</th>} + <th className="group k" style={{ textAlign: 'center' }}>strike</th> + {!compact && <th className="group side-p" colSpan={2} style={{ textAlign: 'center' }}>— puts —</th>} + {compact && <th className="group side-p" colSpan={3} style={{ textAlign: 'center' }}>— puts —</th>} + </tr> + {!compact ? ( + <tr> + <th className="side-c">OI</th> + <th className="side-c">IV</th> + <th className="side-c">last</th> + <th className="side-c">Δ</th> + <th className="k">K</th> + <th className="side-p">Δ</th> + <th className="side-p">last</th> + <th className="side-p">IV</th> + <th className="side-p">OI</th> + </tr> + ) : ( + <tr> + <th className="side-c">IV</th> + <th className="side-c">last</th> + <th className="side-c">Δ</th> + <th className="k">K</th> + <th className="side-p">Δ</th> + <th className="side-p">last</th> + <th className="side-p">IV</th> + </tr> + )} + </thead> + <tbody> + {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 ( + <tr + key={row.K} + className={`${isAtm ? 'atm' : ''} ${isSel ? 'selected' : ''}`} + onClick={() => onPick(row.K)} + > + {!compact && <td className="dim">{fmtOi(row.cOi)}</td>} + <td className="iv">{(row.cIv * 100).toFixed(1)}%</td> + <td className={cItm ? 'itm' : 'otm'}>{row.cMid.toFixed(2)}</td> + <td className={cItm ? 'itm' : 'otm'}>{row.cDelta.toFixed(2)}</td> + <td className="k">{row.K.toFixed(0)}</td> + <td className={pItm ? 'itm' : 'otm'}>{row.pDelta.toFixed(2)}</td> + <td className={pItm ? 'itm' : 'otm'}>{row.pMid.toFixed(2)}</td> + <td className="iv">{(row.pIv * 100).toFixed(1)}%</td> + {!compact && <td className="dim">{fmtOi(row.pOi)}</td>} + </tr> + ); + })} + </tbody> + </table> + </div> + </div> + ); +} + +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 ( + <ChainTable + rows={rows} + atmStrike={atmStrike} + selectedK={selectedK} + type={type} + onPick={onPick} + compact={compact} + /> + ); +} diff --git a/frontend/components/prism/options/OptionsCharts.tsx b/frontend/components/prism/options/OptionsCharts.tsx new file mode 100644 index 0000000..337035f --- /dev/null +++ b/frontend/components/prism/options/OptionsCharts.tsx @@ -0,0 +1,335 @@ +"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 ( + <div className="opt-chart-card opt-svg"> + <div className="head"> + <h4>Vol Smile</h4> + <span className="eyebrow">Implied Volatility vs. Strike</span> + </div> + <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={H} style={{ display: 'block' }}> + {yLabels.map(iv => ( + <g key={iv}> + <line className="grid" x1={pad.l} y1={toY(iv)} x2={pad.l + iW} y2={toY(iv)} /> + <text x={pad.l - 4} y={toY(iv) + 4} textAnchor="end" className="opt-svg">{(iv * 100).toFixed(0)}%</text> + </g> + ))} + <line className="axis" x1={pad.l} y1={pad.t} x2={pad.l} y2={pad.t + iH} /> + <line className="axis" x1={pad.l} y1={pad.t + iH} x2={pad.l + iW} y2={pad.t + iH} /> + {otherCurves.map((pts, idx) => ( + <path + key={idx} + className={idx === 0 ? 'curve fade1' : 'curve fade2'} + d={smoothPath(pts.map(([k, iv]) => [toX(k), toY(iv)] as [number, number]))} + /> + ))} + <path className="curve fill" d={fillPath} /> + <path className="curve accent" d={svgPrimary} /> + <line className="spoke" x1={atmX} y1={pad.t} x2={atmX} y2={pad.t + iH} strokeDasharray="3 3" stroke="var(--brass)" strokeWidth="1" /> + <text x={atmX} y={pad.t - 6} textAnchor="middle" className="atm">ATM</text> + <line className="crosshair" x1={kX} y1={pad.t} x2={kX} y2={pad.t + iH} /> + <circle cx={kX} cy={kY} r={4} className="marker" /> + <text x={kX + 6} y={kY - 6} className="brass">{(kIv * 100).toFixed(1)}%</text> + </svg> + </div> + ); +} + +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 ( + <div className="opt-chart-card opt-svg"> + <div className="head"> + <h4>Term Structure</h4> + <span className="eyebrow">ATM IV vs. Expiry</span> + </div> + <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={H} style={{ display: 'block' }}> + <line className="axis" x1={pad.l} y1={pad.t} x2={pad.l} y2={pad.t + iH} /> + <line className="axis" x1={pad.l} y1={pad.t + iH} x2={pad.l + iW} y2={pad.t + iH} /> + <path className="curve fade1" d={linePath(linePts)} /> + {pts.map((p) => { + const cx = toX(p.sqrtT); + const cy = toY(p.iv); + const isSel = Math.abs(p.T - selectedT) < 0.001; + return ( + <g key={p.label} style={{ cursor: 'pointer' }} onClick={() => onPickT(p.T)}> + <circle + cx={cx} cy={cy} r={isSel ? 5 : 4} + fill={isSel ? 'var(--brass-bright)' : 'var(--fg-3)'} + stroke="var(--ink-0)" strokeWidth="1.5" + /> + <text x={cx} y={pad.t + iH + 14} textAnchor="middle" fontSize="9">{p.label}</text> + </g> + ); + })} + </svg> + </div> + ); +} + +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 ( + <div className="opt-greek-mini"> + <div className="lbl"> + <span>{glyph} {name}</span> + <span className="v">{curVal.toFixed(4)}</span> + </div> + <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={H} style={{ display: 'block' }} className="opt-svg"> + <path className="curve accent" d={smoothPath(svgPts)} /> + <circle cx={dotX} cy={dotY} r={3} className="marker" /> + </svg> + </div> + ); +} + +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 ( + <div className="opt-payoff opt-svg"> + <svg viewBox={`0 0 ${W} ${H}`} width="100%" height={H} style={{ display: 'block' }}> + <line className="grid" x1={pad.l} y1={zeroY} x2={pad.l + iW} y2={zeroY} /> + {gainArea && ( + <path d={gainArea} fill="var(--positive)" opacity="0.18" /> + )} + {lossArea && ( + <path d={lossArea} fill="var(--negative)" opacity="0.18" /> + )} + <path className="curve accent" d={allSvg} /> + <line className="marker-line" x1={kX} y1={pad.t} x2={kX} y2={pad.t + iH} /> + <text x={kX} y={pad.t + iH + 16} textAnchor="middle">K={K.toFixed(0)}</text> + <line x1={spotX} y1={pad.t} x2={spotX} y2={pad.t + iH} stroke="var(--fg-3)" strokeWidth="1" strokeDasharray="2 3" /> + <text x={spotX} y={pad.t - 4} textAnchor="middle" className="brass">S</text> + {beSpot >= sMin && beSpot <= sMax && ( + <> + <line className="marker-line" x1={beX} y1={pad.t} x2={beX} y2={pad.t + iH} /> + <text x={beX} y={pad.t + 14} textAnchor="middle" className="brass">BE</text> + </> + )} + </svg> + </div> + ); +} diff --git a/frontend/components/prism/options/OptionsPage.tsx b/frontend/components/prism/options/OptionsPage.tsx new file mode 100644 index 0000000..7fcf5c3 --- /dev/null +++ b/frontend/components/prism/options/OptionsPage.tsx @@ -0,0 +1,292 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { bsSynthIV } from "@/lib/blackScholes"; +import type { TickerOverview } from "@/types/api"; +import type { OptionInputs } from "./types"; +import { EXPIRIES } from "./types"; +import { Pricer, SolvePanel } from "./OptionsPricer"; +import { OptionsChain } from "./OptionsChain"; +import { SmileChart, TermStructure, GreekMini, Payoff } from "./OptionsCharts"; +import { PolarSmile, IvHeatmap } from "./OptionsSurface"; + +interface OptionsPageProps { + overview: TickerOverview | null; + ticker: string; +} + +export function OptionsPage({ overview, ticker }: OptionsPageProps) { + const [view, setView] = useState<'terminal' | 'surface'>('terminal'); + const [expiryIdx, setExpiryIdx] = useState(1); + + const spot = overview?.quote.price ?? 0; + const chgAbs = overview?.quote.change ?? 0; + const chgPct = overview?.quote.change_pct ?? 0; + const r = 0.0425; + const q = overview?.ratios.dividend_yield_ttm ?? 0; + const atmSigma30 = 0.243; + const sym = overview?.profile.symbol ?? ticker; + const name = overview?.profile.name ?? ""; + + const expiry = EXPIRIES[expiryIdx]; + + const [inputs, setInputs] = useState<OptionInputs>(() => ({ + S: spot || 100, + K: spot ? Math.round(spot / 5) * 5 : 100, + T: expiry.T, + r, + q, + sigma: atmSigma30, + type: 'C', + })); + + useEffect(() => { + if (spot > 0) { + setInputs(prev => ({ + ...prev, + S: spot, + K: Math.round(spot / 5) * 5, + r, + q, + })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [spot]); + + function patchInputs(partial: Partial<OptionInputs>) { + setInputs(prev => ({ ...prev, ...partial })); + } + + function selectExpiry(idx: number) { + setExpiryIdx(idx); + setInputs(prev => ({ ...prev, T: EXPIRIES[idx].T })); + } + + const atmIv = bsSynthIV(inputs.S, inputs.S, expiry.T, atmSigma30); + const d25C = 0.243 + 0.012; + const d25P = 0.243 + 0.012; + const rr25 = (d25C - d25P) * 100; + const bf25 = ((d25C + d25P) / 2 - atmIv) * 100; + const pcRatio = 0.87; + + if (!overview || spot === 0) { + return ( + <section className="psm-state-panel"> + <span className="psm-status-chip">Options</span> + <h1>Select a ticker to view options</h1> + <p>Load a ticker from the search bar to access the Black-Scholes pricer, option chain, vol surface, and greek visualizations.</p> + </section> + ); + } + + const terminalView = ( + <div className="opt-grid"> + <div className="opt-col"> + <Pricer inputs={inputs} spot={spot} onChange={patchInputs} /> + <SolvePanel inputs={inputs} /> + </div> + <div className="opt-col"> + <OptionsChain + S={inputs.S} + T={expiry.T} + r={r} + q={q} + atmSigma={atmSigma30} + expirySeed={expiryIdx} + selectedK={inputs.K} + type={inputs.type} + onPick={K => patchInputs({ K })} + /> + <Payoff + S={inputs.S} + K={inputs.K} + T={inputs.T} + r={inputs.r} + q={inputs.q} + sigma={inputs.sigma} + type={inputs.type} + /> + </div> + <div className="opt-col"> + <SmileChart + S={inputs.S} + T={expiry.T} + r={r} + q={q} + atmSigma={atmSigma30} + K={inputs.K} + type={inputs.type} + expiryLabel={expiry.label} + allExpiries={EXPIRIES} + /> + <TermStructure + S={inputs.S} + r={r} + q={q} + atmSigma={atmSigma30} + expiries={EXPIRIES} + selectedT={expiry.T} + onPickT={T => { + const idx = EXPIRIES.findIndex(e => Math.abs(e.T - T) < 0.001); + if (idx >= 0) selectExpiry(idx); + }} + /> + <div className="opt-greek-multi"> + <GreekMini name="Delta" glyph="Δ" kind="delta" S={inputs.S} K={inputs.K} T={inputs.T} r={inputs.r} q={inputs.q} sigma={inputs.sigma} type={inputs.type} /> + <GreekMini name="Gamma" glyph="Γ" kind="gamma" S={inputs.S} K={inputs.K} T={inputs.T} r={inputs.r} q={inputs.q} sigma={inputs.sigma} type={inputs.type} /> + <GreekMini name="Vega" glyph="ν" kind="vega" S={inputs.S} K={inputs.K} T={inputs.T} r={inputs.r} q={inputs.q} sigma={inputs.sigma} type={inputs.type} /> + <GreekMini name="Theta" glyph="Θ" kind="theta" S={inputs.S} K={inputs.K} T={inputs.T} r={inputs.r} q={inputs.q} sigma={inputs.sigma} type={inputs.type} /> + </div> + </div> + </div> + ); + + const surfaceView = ( + <div className="opt-grid"> + <div className="opt-col"> + <Pricer inputs={inputs} spot={spot} onChange={patchInputs} /> + <SolvePanel inputs={inputs} /> + </div> + <div className="opt-col"> + <PolarSmile + S={inputs.S} + r={r} + q={q} + atmSigma={atmSigma30} + K={inputs.K} + T={expiry.T} + type={inputs.type} + expiries={EXPIRIES} + selectedExpiryIdx={expiryIdx} + /> + <IvHeatmap + S={inputs.S} + atmSigma={atmSigma30} + expiries={EXPIRIES} + selectedExpiryIdx={expiryIdx} + selectedK={inputs.K} + onSelect={(eIdx, K) => { + selectExpiry(eIdx); + patchInputs({ K }); + }} + /> + </div> + <div className="opt-col"> + <Payoff + S={inputs.S} + K={inputs.K} + T={inputs.T} + r={inputs.r} + q={inputs.q} + sigma={inputs.sigma} + type={inputs.type} + /> + <OptionsChain + S={inputs.S} + T={expiry.T} + r={r} + q={q} + atmSigma={atmSigma30} + expirySeed={expiryIdx} + selectedK={inputs.K} + type={inputs.type} + onPick={K => patchInputs({ K })} + compact + /> + </div> + </div> + ); + + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sp-4)' }}> + <div className="opt-header"> + <div className="ticker"> + <span className="tab-eyebrow">Options · Black–Scholes</span> + <div className="ticker-row"> + <span className="sym">{sym}</span> + <span className="name">{name}</span> + <div className="px-block"> + <span className="px">${spot.toFixed(2)}</span> + <span className={`chg ${chgPct >= 0 ? 'pos' : 'neg'}`}> + {chgPct >= 0 ? '+' : ''}{chgAbs.toFixed(2)} · {chgPct >= 0 ? '+' : ''}{chgPct.toFixed(2)}% + </span> + </div> + </div> + </div> + <div className="opt-view" role="tablist"> + <button + type="button" + className={view === 'terminal' ? 'active' : ''} + onClick={() => setView('terminal')} + > + <span className="glyph">▦</span> Terminal + </button> + <button + type="button" + className={view === 'surface' ? 'active' : ''} + onClick={() => setView('surface')} + > + <span className="glyph">⚴</span> Surface + </button> + </div> + </div> + + <div className="opt-expiry-bar"> + <span className="lbl">Expiry</span> + <div className="opt-expiries"> + {EXPIRIES.map((e, idx) => ( + <button + key={e.label} + type="button" + className={`opt-exp-chip${idx === expiryIdx ? ' active' : ''}`} + onClick={() => selectExpiry(idx)} + > + {e.label} + <span className="dte">{e.dte}d</span> + </button> + ))} + </div> + </div> + + <div className="opt-strip"> + <div> + <span className="k">ATM IV</span> + <span className="v accent">{(atmIv * 100).toFixed(1)}%</span> + <span className="s">{expiry.label}</span> + </div> + <div> + <span className="k">25Δ RR</span> + <span className={`v ${rr25 >= 0 ? 'gain' : 'loss'}`}>{rr25 >= 0 ? '+' : ''}{rr25.toFixed(2)}v</span> + <span className="s">call skew</span> + </div> + <div> + <span className="k">25Δ BF</span> + <span className="v">{bf25 >= 0 ? '+' : ''}{bf25.toFixed(2)}v</span> + <span className="s">smile</span> + </div> + <div> + <span className="k">P/C Ratio</span> + <span className="v">{pcRatio.toFixed(2)}</span> + <span className="s">put / call OI</span> + </div> + <div> + <span className="k">Rate r</span> + <span className="v">{(r * 100).toFixed(2)}%</span> + <span className="s">risk-free</span> + </div> + <div> + <span className="k">Div q</span> + <span className="v">{(q * 100).toFixed(2)}%</span> + <span className="s">yield</span> + </div> + <div> + <span className="k">Contract</span> + <span className="v">{inputs.K.toFixed(0)}</span> + <span className="s">{inputs.type === 'C' ? 'Call' : 'Put'} · {expiry.label}</span> + </div> + </div> + + {view === 'terminal' ? terminalView : surfaceView} + </div> + ); +} 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 ( + <div className={`opt-slide${className ? ' ' + className : ''}`}> + <span className="g">{glyph}</span> + <input + type="range" + min={min * displayScale} + max={max * displayScale} + step={step * displayScale} + value={displayed} + onChange={e => onChange(parseFloat(e.target.value) / displayScale)} + /> + <div className="val"> + <input + type="number" + min={min * displayScale} + max={max * displayScale} + step={step * displayScale} + value={displayed} + onChange={e => { + const v = parseFloat(e.target.value) / displayScale; + if (!isNaN(v)) onChange(Math.max(min, Math.min(max, v))); + }} + /> + {unit && <span className="unit">{unit}</span>} + </div> + {(meta || onReset) && ( + <div className="meta"> + {meta && <span>{meta}</span>} + {onReset && <button type="button" onClick={onReset}>RESET</button>} + </div> + )} + </div> + ); +} + +interface PricerProps { + inputs: OptionInputs; + spot: number; + onChange: (partial: Partial<OptionInputs>) => 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 ( + <div className="opt-pricer"> + <div className="head"> + <h3>Pricer</h3> + <div className="opt-cp"> + <button + type="button" + className={`${type === 'C' ? 'active C' : ''}`} + onClick={() => onChange({ type: 'C' })} + >CALL</button> + <button + type="button" + className={`${type === 'P' ? 'active P' : ''}`} + onClick={() => onChange({ type: 'P' })} + >PUT</button> + </div> + </div> + + <div className="opt-sliders"> + <Slider + glyph="S" + label="Spot" + value={S} + min={spot * 0.5} + max={spot * 2} + step={0.01} + unit="$" + meta={`spot ${spot.toFixed(2)}`} + onReset={() => onChange({ S: spot })} + onChange={v => onChange({ S: v })} + /> + <Slider + glyph="K" + label="Strike" + value={K} + min={spot * 0.5} + max={spot * 2} + step={1} + unit="$" + onChange={v => onChange({ K: v })} + /> + <Slider + glyph="T" + label="Days to Expiry" + value={T} + min={1 / 365} + max={644 / 365} + step={1 / 365} + displayScale={365} + unit="d" + meta={`${dte}d`} + onChange={v => onChange({ T: v })} + /> + <Slider + glyph="r" + label="Risk-free Rate" + value={r} + min={0} + max={0.15} + step={0.0001} + displayScale={100} + unit="%" + onChange={v => onChange({ r: v })} + /> + <Slider + glyph="σ" + label="Volatility" + value={sigma} + min={0.01} + max={1.5} + step={0.001} + displayScale={100} + unit="%" + onChange={v => onChange({ sigma: v })} + /> + <Slider + glyph="q" + label="Dividend Yield" + value={q} + min={0} + max={0.15} + step={0.0001} + displayScale={100} + unit="%" + onChange={v => onChange({ q: v })} + /> + </div> + + <div className="opt-output"> + <div> + <div className="fair-lbl">Fair Value</div> + <div className="fair"> + <span className="cur">$</span> + {fmt(fair)} + </div> + <div className={`delta ${pctColor(g.delta)}`}> + Δ {fmt(g.delta, 4)} + </div> + </div> + <div> + <div className="mid-lbl">Market Mid</div> + <div className="mid">${isNaN(mid) ? '—' : mid.toFixed(2)}</div> + </div> + <div className="iv-bar"> + <span className="lbl">IV</span> + <div className="iv-track"> + <div className="iv-mkt" style={{ left: `${ivPct * 100}%` }} /> + <div className="iv-solved" style={{ left: `${sigmaPct * 100}%` }} /> + </div> + <span className="iv-val">{fmtPct(sigma)}</span> + </div> + </div> + + <div className="opt-greeks"> + <div className="opt-greek"> + <span className="g">Δ</span> + <span className={`v${g.delta < 0 ? ' neg' : ''}`}>{fmt(g.delta, 4)}</span> + <span className="n">Delta</span> + </div> + <div className="opt-greek"> + <span className="g">Γ</span> + <span className="v">{fmt(g.gamma, 4)}</span> + <span className="n">Gamma</span> + </div> + <div className="opt-greek"> + <span className="g">ν</span> + <span className="v">{fmt(g.vega, 4)}</span> + <span className="n">Vega</span> + </div> + <div className="opt-greek"> + <span className="g">Θ</span> + <span className={`v${g.theta < 0 ? ' neg' : ''}`}>{fmt(g.theta, 4)}</span> + <span className="n">Theta/d</span> + </div> + <div className="opt-greek"> + <span className="g">ρ</span> + <span className={`v${g.rho < 0 ? ' neg' : ''}`}>{fmt(g.rho, 4)}</span> + <span className="n">Rho</span> + </div> + </div> + </div> + ); +} + +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 ( + <div className="opt-solve"> + <div className="head">Analytics</div> + <div className="row"> + <span className="g">σ</span> + <span className="l">Implied Vol</span> + <span className="v">{isNaN(iv) ? '—' : (iv * 100).toFixed(2) + '%'}</span> + </div> + <div className="row"> + <span className="g">↕</span> + <span className="l">Breakeven</span> + <span className="v">${isNaN(breakeven) ? '—' : breakeven.toFixed(2)}</span> + </div> + <div className="row"> + <span className="g">◆</span> + <span className="l">Intrinsic</span> + <span className="v">${fmt(intrinsic)}</span> + </div> + <div className="row"> + <span className="g">◇</span> + <span className="l">Extrinsic</span> + <span className="v">${fmt(extrinsic)}</span> + </div> + </div> + ); +} diff --git a/frontend/components/prism/options/OptionsSurface.tsx b/frontend/components/prism/options/OptionsSurface.tsx new file mode 100644 index 0000000..da55012 --- /dev/null +++ b/frontend/components/prism/options/OptionsSurface.tsx @@ -0,0 +1,272 @@ +"use client"; + +import { useMemo } from "react"; +import { bsPrice, bsSynthIV } from "@/lib/blackScholes"; +import type { Expiry, OptionType } from "./types"; + +interface PolarSmileProps { + S: number; + r: number; + q: number; + atmSigma: number; + K: number; + T: number; + type: OptionType; + expiries: Expiry[]; + selectedExpiryIdx: number; +} + +export function PolarSmile({ S, r, q, atmSigma, K, T, type, expiries, selectedExpiryIdx }: PolarSmileProps) { + const SIZE = 680; + const cx = SIZE / 2, cy = SIZE / 2; + const maxR = SIZE * 0.42; + + const ivMin = 0.04, ivMax = 0.8; + const ivRings = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]; + + const kMin = S * 0.82, kMax = S * 1.18; + const nSpokes = 13; + const strikes = useMemo(() => { + const arr: number[] = []; + for (let i = 0; i < nSpokes; i++) { + arr.push(kMin + (kMax - kMin) * i / (nSpokes - 1)); + } + return arr; + }, [kMin, kMax]); + + function strikeToAngle(strike: number): number { + const norm = (strike - kMin) / (kMax - kMin); + return -Math.PI / 2 + (norm - 0.5) * Math.PI * 1.5; + } + + function ivToR(iv: number): number { + return maxR * Math.max(0, Math.min(1, (iv - ivMin) / (ivMax - ivMin))); + } + + function polarToXY(angle: number, radius: number): [number, number] { + return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]; + } + + const fairVal = useMemo(() => bsPrice(S, K, T, r, q, atmSigma, type), [S, K, T, r, q, atmSigma, type]); + + const curves = useMemo(() => { + function _strikeToAngle(strike: number): number { + const norm = (strike - kMin) / (kMax - kMin); + return -Math.PI / 2 + (norm - 0.5) * Math.PI * 1.5; + } + function _ivToR(iv: number): number { + return maxR * Math.max(0, Math.min(1, (iv - ivMin) / (ivMax - ivMin))); + } + function _polarToXY(angle: number, radius: number): [number, number] { + return [cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)]; + } + return expiries.map(e => { + return strikes.map(strike => { + const iv = bsSynthIV(S, strike, e.T, atmSigma); + const angle = _strikeToAngle(strike); + const radius = _ivToR(iv); + return _polarToXY(angle, radius); + }); + }); + }, [S, atmSigma, expiries, strikes, kMin, kMax, maxR, ivMin, ivMax, cx, cy]); + + function curvePath(pts: [number, number][]): string { + return pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`).join(' '); + } + + const selCurve = curves[selectedExpiryIdx]; + const kAngle = strikeToAngle(K); + const kIv = bsSynthIV(S, K, T, atmSigma); + const kR = ivToR(kIv); + const [dotX, dotY] = polarToXY(kAngle, kR); + + const atmAngle = -Math.PI / 2; + + const eyeR = 28; + const legendColors = [ + 'var(--fg-4)', 'var(--fg-3)', 'var(--fg-2)', 'var(--fg-2)', 'var(--brass)', 'var(--brass-bright)' + ]; + + return ( + <div className="opt-surface"> + <div className="head"> + <h3>Vol Surface <em>polar</em></h3> + </div> + <div className="opt-polar-wrap"> + <svg + viewBox={`0 0 ${SIZE} ${SIZE}`} + width="100%" + height={SIZE} + className="opt-polar" + style={{ display: 'block' }} + > + {ivRings.map(iv => { + const r = ivToR(iv); + return ( + <g key={iv}> + <circle cx={cx} cy={cy} r={r} className={iv === Math.max(...ivRings) ? 'ring outer' : 'ring'} /> + <text x={cx + 4} y={cy - r + 4} className="iv">{(iv * 100).toFixed(0)}%</text> + </g> + ); + })} + {strikes.map((strike, i) => { + const angle = strikeToAngle(strike); + const [x1, y1] = polarToXY(angle, 0); + const [x2, y2] = polarToXY(angle, maxR); + const isAtm = Math.abs(strike - S) < (kMax - kMin) / (nSpokes * 2); + return ( + <g key={i}> + <line + x1={x1.toFixed(1)} y1={y1.toFixed(1)} + x2={x2.toFixed(1)} y2={y2.toFixed(1)} + className={isAtm ? 'spoke atm' : 'spoke'} + /> + <text + x={(cx + (maxR + 14) * Math.cos(angle)).toFixed(1)} + y={(cy + (maxR + 14) * Math.sin(angle)).toFixed(1)} + textAnchor="middle" + dominantBaseline="middle" + className={isAtm ? 'tick atm' : 'tick'} + fontSize="9" + > + {strike.toFixed(0)} + </text> + </g> + ); + })} + {curves.map((pts, idx) => { + const isSel = idx === selectedExpiryIdx; + if (isSel) return null; + const color = legendColors[Math.min(idx, legendColors.length - 1)]; + return ( + <path + key={idx} + d={curvePath(pts)} + className="expiry" + stroke={color} + strokeWidth="1" + opacity="0.5" + /> + ); + })} + {selCurve && ( + <path + d={curvePath(selCurve)} + className="expiry" + stroke="var(--brass-bright)" + strokeWidth="2" + fill="none" + /> + )} + <circle cx={dotX.toFixed(1)} cy={dotY.toFixed(1)} r={5} className="dot" /> + <line + x1={(cx + eyeR * Math.cos(atmAngle)).toFixed(1)} + y1={(cy + eyeR * Math.sin(atmAngle)).toFixed(1)} + x2={(cx + (maxR + 2) * Math.cos(atmAngle)).toFixed(1)} + y2={(cy + (maxR + 2) * Math.sin(atmAngle)).toFixed(1)} + className="spoke atm" + /> + <circle cx={cx} cy={cy} r={eyeR} className="eye" /> + <text x={cx} y={cy - 8} textAnchor="middle" className="eye-lbl">{type === 'C' ? 'Call' : 'Put'}</text> + <text x={cx} y={cy + 10} textAnchor="middle" className="eye-num">${fairVal.toFixed(2)}</text> + </svg> + </div> + <div className="opt-surface-legend"> + {expiries.map((e, idx) => { + const color = legendColors[Math.min(idx, legendColors.length - 1)]; + const isSel = idx === selectedExpiryIdx; + return ( + <div key={e.label} className={`item${isSel ? '' : ' muted'}`}> + <div className="swatch" style={{ background: isSel ? 'var(--brass-bright)' : color }} /> + {e.label} + </div> + ); + })} + </div> + </div> + ); +} + +interface IvHeatmapProps { + S: number; + atmSigma: number; + expiries: Expiry[]; + selectedExpiryIdx: number; + selectedK: number; + onSelect: (expiryIdx: number, K: number) => void; +} + +export function IvHeatmap({ S, atmSigma, expiries, selectedExpiryIdx, selectedK, onSelect }: IvHeatmapProps) { + const nStrikes = 13; + const kMin = S * 0.85, kMax = S * 1.15; + + const strikes = useMemo(() => { + const arr: number[] = []; + for (let i = 0; i < nStrikes; i++) { + arr.push(Math.round((kMin + (kMax - kMin) * i / (nStrikes - 1)) / 5) * 5); + } + return arr; + }, [kMin, kMax]); + + const ivGrid = useMemo(() => { + return expiries.map(e => + strikes.map(K => bsSynthIV(S, K, e.T, atmSigma)) + ); + }, [S, atmSigma, expiries, strikes]); + + const allIvs = ivGrid.flat(); + const ivGridMin = Math.min(...allIvs); + const ivGridMax = Math.max(...allIvs); + + function cellColor(iv: number): string { + const pct = Math.round(((iv - ivGridMin) / (ivGridMax - ivGridMin + 0.001)) * 80); + return `color-mix(in oklch, var(--ink-2), var(--brass-deep) ${pct}%)`; + } + + function textColor(iv: number): string { + const pct = (iv - ivGridMin) / (ivGridMax - ivGridMin + 0.001); + return pct > 0.5 ? 'var(--fg-1)' : 'var(--fg-3)'; + } + + const atmK = Math.round(S / 5) * 5; + + return ( + <div className="opt-surface"> + <div className="head"> + <h3>IV Heatmap <em>surface</em></h3> + </div> + <div className="opt-heat"> + <div className="ylabs"> + {expiries.map(e => ( + <span key={e.label}>{e.label}</span> + ))} + </div> + <div> + <div className="grid" style={{ gridTemplateColumns: `repeat(${nStrikes}, 1fr)` }}> + {ivGrid.map((row, eIdx) => + row.map((iv, kIdx) => { + const K = strikes[kIdx]; + const isCursor = eIdx === selectedExpiryIdx && K === selectedK; + return ( + <div + key={`${eIdx}-${kIdx}`} + className={`cell${isCursor ? ' cursor' : ''}`} + style={{ background: cellColor(iv), color: textColor(iv) }} + onClick={() => onSelect(eIdx, K)} + > + {(iv * 100).toFixed(0)}% + </div> + ); + }) + )} + </div> + <div className="xlabs" style={{ gridTemplateColumns: `repeat(${nStrikes}, 1fr)` }}> + {strikes.map(K => ( + <span key={K} className={K === atmK ? 'atm' : ''}>{K}</span> + ))} + </div> + </div> + </div> + </div> + ); +} diff --git a/frontend/components/prism/options/types.ts b/frontend/components/prism/options/types.ts new file mode 100644 index 0000000..fb90a7a --- /dev/null +++ b/frontend/components/prism/options/types.ts @@ -0,0 +1,47 @@ +export type OptionType = 'C' | 'P'; + +export interface Expiry { + label: string; + dte: number; + T: number; +} + +export interface OptionInputs { + S: number; + K: number; + T: number; + r: number; + q: number; + sigma: number; + type: OptionType; +} + +export interface ChainRow { + K: number; + cMid: number; pMid: number; + cIv: number; pIv: number; + cDelta: number; pDelta: number; + cOi: number; pOi: number; + cVol: number; pVol: number; +} + +export interface TickerDefaults { + sym: string; + name: string; + sector: string; + spot: number; + chgAbs: number; + chgPct: number; + r: number; + q: number; + atmSigma30: number; +} + +export const EXPIRIES: Expiry[] = [ + { label: 'Apr 19', dte: 14, T: 14 / 365 }, + { label: 'May 17', dte: 30, T: 30 / 365 }, + { label: 'Jun 21', dte: 65, T: 65 / 365 }, + { label: 'Sep 20', dte: 156, T: 156 / 365 }, + { label: "Jan '27", dte: 280, T: 280 / 365 }, + { label: "Jan '28", dte: 644, T: 644 / 365 }, +]; |
