summaryrefslogtreecommitdiff
path: root/frontend/components/prism/options/OptionsPricer.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/components/prism/options/OptionsPricer.tsx')
-rw-r--r--frontend/components/prism/options/OptionsPricer.tsx274
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="&sigma;"
+ 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)}`}>
+ &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">&Delta;</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">&Gamma;</span>
+ <span className="v">{fmt(g.gamma, 4)}</span>
+ <span className="n">Gamma</span>
+ </div>
+ <div className="opt-greek">
+ <span className="g">&nu;</span>
+ <span className="v">{fmt(g.vega, 4)}</span>
+ <span className="n">Vega</span>
+ </div>
+ <div className="opt-greek">
+ <span className="g">&Theta;</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">&rho;</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">&sigma;</span>
+ <span className="l">Implied Vol</span>
+ <span className="v">{isNaN(iv) ? '—' : (iv * 100).toFixed(2) + '%'}</span>
+ </div>
+ <div className="row">
+ <span className="g">&#x2195;</span>
+ <span className="l">Breakeven</span>
+ <span className="v">${isNaN(breakeven) ? '—' : breakeven.toFixed(2)}</span>
+ </div>
+ <div className="row">
+ <span className="g">&#x25C6;</span>
+ <span className="l">Intrinsic</span>
+ <span className="v">${fmt(intrinsic)}</span>
+ </div>
+ <div className="row">
+ <span className="g">&#x25C7;</span>
+ <span className="l">Extrinsic</span>
+ <span className="v">${fmt(extrinsic)}</span>
+ </div>
+ </div>
+ );
+}