Options Chain: Shows a multi-vendor options chain straddle table with IV, delta, OI, volume, and max pain for a ticker.
| 1 | import { Fragment } from 'react' |
| 2 | import type { ChainRow } from './math.ts' |
| 3 | |
| 4 | function fmt(n: number | null | undefined, digits = 2): string { |
| 5 | if (n == null || !Number.isFinite(n)) return '-' |
| 6 | return n.toFixed(digits) |
| 7 | } |
| 8 | |
| 9 | function fmtInt(n: number | null | undefined): string { |
| 10 | if (n == null || !Number.isFinite(n)) return '-' |
| 11 | return Math.round(n).toLocaleString() |
| 12 | } |
| 13 | |
| 14 | function fmtQuote(px: number | null | undefined, sz: number | null | undefined): string { |
| 15 | if (px == null || !Number.isFinite(px)) return '-' |
| 16 | if (sz != null && Number.isFinite(sz) && sz > 0) return `${px.toFixed(2)} x${Math.round(sz)}` |
| 17 | return px.toFixed(2) |
| 18 | } |
| 19 | |
| 20 | export function StraddleTable({ |
| 21 | rows, |
| 22 | spot, |
| 23 | atmStrike, |
| 24 | onPickContract, |
| 25 | }: { |
| 26 | rows: ChainRow[] |
| 27 | spot: number | null |
| 28 | atmStrike: number | null |
| 29 | onPickContract?: (occ: string) => void |
| 30 | }) { |
| 31 | let maxOi = 1 |
| 32 | for (const r of rows) { |
| 33 | if ((r.callOi ?? 0) > maxOi) maxOi = r.callOi! |
| 34 | if ((r.putOi ?? 0) > maxOi) maxOi = r.putOi! |
| 35 | } |
| 36 | |
| 37 | let atmInserted = false |
| 38 | const pick = (occ: string) => { |
| 39 | if (onPickContract) onPickContract(occ) |
| 40 | } |
| 41 | |
| 42 | return ( |
| 43 | <div className="oc-table-wrap"> |
| 44 | <table className="oc-table"> |
| 45 | <thead> |
| 46 | <tr> |
| 47 | <th colSpan={7} className="oc-center">Calls</th> |
| 48 | <th className="oc-center">Strike</th> |
| 49 | <th colSpan={7} className="oc-center">Puts</th> |
| 50 | </tr> |
| 51 | <tr> |
| 52 | <th>OI</th> |
| 53 | <th>Vol</th> |
| 54 | <th>IV%</th> |
| 55 | <th>Delta</th> |
| 56 | <th>Bid</th> |
| 57 | <th>Ask</th> |
| 58 | <th>Last</th> |
| 59 | <th className="oc-center">K</th> |
| 60 | <th>Last</th> |
| 61 | <th>Bid</th> |
| 62 | <th>Ask</th> |
| 63 | <th>Delta</th> |
| 64 | <th>IV%</th> |
| 65 | <th>Vol</th> |
| 66 | <th>OI</th> |
| 67 | </tr> |
| 68 | </thead> |
| 69 | <tbody> |
| 70 | {rows.map((r, i) => { |
| 71 | const next = rows[i + 1] |
| 72 | const showDividerAfter = |
| 73 | !atmInserted && |
| 74 | spot != null && |
| 75 | r.strike <= spot && |
| 76 | (next == null || next.strike > spot) |
| 77 | if (showDividerAfter) atmInserted = true |
| 78 | |
| 79 | const callOiPct = r.callOi ? Math.min(100, Math.round((r.callOi / maxOi) * 100)) : 0 |
| 80 | const putOiPct = r.putOi ? Math.min(100, Math.round((r.putOi / maxOi) * 100)) : 0 |
| 81 | const isAtm = atmStrike != null && r.strike === atmStrike |
| 82 | const cClass = `oc-clickable ${r.callItm ? 'oc-itm' : ''}` |
| 83 | const pClass = `oc-clickable ${r.putItm ? 'oc-itm' : ''}` |
| 84 | |
| 85 | return ( |
| 86 | <Fragment key={r.strike}> |
| 87 | <tr> |
| 88 | <td className={cClass} onClick={() => pick(r.callContract)} title={r.callContract}> |
| 89 | {callOiPct > 0 && <span className="oc-oi-bar oc-oi-call" style={{ width: `${callOiPct}%` }} />} |
| 90 | {fmtInt(r.callOi)} |
| 91 | </td> |
| 92 | <td className={cClass} onClick={() => pick(r.callContract)}>{fmtInt(r.callVol)}</td> |
| 93 | <td className={cClass} onClick={() => pick(r.callContract)}>{r.callIv != null ? `${fmt(r.callIv, 1)}%` : '-'}</td> |
| 94 | <td className={cClass} onClick={() => pick(r.callContract)}>{fmt(r.callDelta, 2)}</td> |
| 95 | <td className={cClass} onClick={() => pick(r.callContract)}>{fmtQuote(r.callBid, r.callBidSize)}</td> |
| 96 | <td className={cClass} onClick={() => pick(r.callContract)}>{fmtQuote(r.callAsk, r.callAskSize)}</td> |
| 97 | <td className={cClass} onClick={() => pick(r.callContract)}>{fmt(r.callLast)}</td> |
| 98 | <td className="oc-strike"> |
| 99 | {fmt(r.strike)} |
| 100 | {isAtm ? ' *' : ''} |
| 101 | </td> |
| 102 | <td className={pClass} onClick={() => pick(r.putContract)} title={r.putContract}>{fmt(r.putLast)}</td> |
| 103 | <td className={pClass} onClick={() => pick(r.putContract)}>{fmtQuote(r.putBid, r.putBidSize)}</td> |
| 104 | <td className={pClass} onClick={() => pick(r.putContract)}>{fmtQuote(r.putAsk, r.putAskSize)}</td> |
| 105 | <td className={pClass} onClick={() => pick(r.putContract)}>{fmt(r.putDelta, 2)}</td> |
| 106 | <td className={pClass} onClick={() => pick(r.putContract)}>{r.putIv != null ? `${fmt(r.putIv, 1)}%` : '-'}</td> |
| 107 | <td className={pClass} onClick={() => pick(r.putContract)}>{fmtInt(r.putVol)}</td> |
| 108 | <td className={pClass} onClick={() => pick(r.putContract)} title={r.putContract}> |
| 109 | {putOiPct > 0 && <span className="oc-oi-bar oc-oi-put" style={{ width: `${putOiPct}%` }} />} |
| 110 | {fmtInt(r.putOi)} |
| 111 | </td> |
| 112 | </tr> |
| 113 | {showDividerAfter && ( |
| 114 | <tr className="oc-atm-divider"> |
| 115 | <td colSpan={15}> |
| 116 | CALLS ITM - UNDERLYING SPOT ${fmt(spot)} - PUTS ITM |
| 117 | </td> |
| 118 | </tr> |
| 119 | )} |
| 120 | </Fragment> |
| 121 | ) |
| 122 | })} |
| 123 | </tbody> |
| 124 | </table> |
| 125 | </div> |
| 126 | ) |
| 127 | } |