summaryrefslogtreecommitdiff
path: root/frontend/components/prism/options/OptionsChain.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/components/prism/options/OptionsChain.tsx')
-rw-r--r--frontend/components/prism/options/OptionsChain.tsx157
1 files changed, 157 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">&Delta;</th>
+ <th className="k">K</th>
+ <th className="side-p">&Delta;</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">&Delta;</th>
+ <th className="k">K</th>
+ <th className="side-p">&Delta;</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}
+ />
+ );
+}