diff options
Diffstat (limited to 'frontend/components/prism/options/OptionsChain.tsx')
| -rw-r--r-- | frontend/components/prism/options/OptionsChain.tsx | 157 |
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">Δ</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} + /> + ); +} |
