diff options
Diffstat (limited to 'frontend/components/prism/options/OptionsPage.tsx')
| -rw-r--r-- | frontend/components/prism/options/OptionsPage.tsx | 292 |
1 files changed, 292 insertions, 0 deletions
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> + ); +} |
