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