Jaspers Hub

biseroff / option-chain

pluginReviewed★ 0↓ 0

Options Chain: Shows a multi-vendor options chain straddle table with IV, delta, OI, volume, and max pain for a ticker.

6.1 KB
1export interface ChainRow {
2 contract: string
3 callContract: string
4 putContract: string
5 expiration: string
6 strike: number
7 callBid: number | null
8 callBidSize?: number | null
9 callAsk: number | null
10 callAskSize?: number | null
11 callLast: number | null
12 callLastTime?: string | null
13 callChg: number | null
14 callVol: number | null
15 callOi: number | null
16 callIv: number | null
17 callDelta: number | null
18 callItm: boolean
19 putBid: number | null
20 putBidSize?: number | null
21 putAsk: number | null
22 putAskSize?: number | null
23 putLast: number | null
24 putLastTime?: string | null
25 putChg: number | null
26 putVol: number | null
27 putOi: number | null
28 putIv: number | null
29 putDelta: number | null
30 putItm: boolean
31 [key: string]: unknown
32}
33
34export interface ChainMeta {
35 symbol: string
36 provider: 'yahoo' | 'alpaca' | 'massive' | 'databento'
37 feedLabel: string
38 spot: number | null
39 change: number | null
40 changePct: number | null
41 dayRange: string | null
42 week52Range: string | null
43 volume: number | null
44 expirations: string[]
45 selectedExpiration: string | null
46 dte: number | null
47 atmStrike: number | null
48 atmIv: number | null
49 straddlePrice: number | null
50 straddlePct: number | null
51 pcOiRatio: number | null
52 pcVolRatio: number | null
53 callOiTotal: number
54 putOiTotal: number
55 callVolTotal: number
56 putVolTotal: number
57 maxPain: number | null
58 note: string | null
59}
60
61export function toOcc(symbol: string, expiration: string, type: 'C' | 'P', strike: number): string {
62 const clean = symbol.replace(/[^A-Z0-9]/gi, '').toUpperCase().slice(0, 6)
63 const exp = expiration.slice(0, 10).replace(/-/g, '').slice(2, 8)
64 const k = Math.round(strike * 1000).toString().padStart(8, '0')
65 return `${clean}${exp}${type}${k}`
66}
67
68export function calcDte(expiration: string | null): number | null {
69 if (!expiration) return null
70 const expMs = Date.parse(`${expiration.slice(0, 10)}T16:00:00Z`)
71 if (!Number.isFinite(expMs)) return null
72 const diff = Math.ceil((expMs - Date.now()) / 86_400_000)
73 return Math.max(0, diff)
74}
75
76function normCdf(x: number): number {
77 const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741, a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911
78 const sign = x < 0 ? -1 : 1
79 const absX = Math.abs(x) / Math.sqrt(2)
80 const t = 1 / (1 + p * absX)
81 const y = 1 - (((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t) * Math.exp(-absX * absX)
82 return 0.5 * (1 + sign * y)
83}
84
85export function approxDelta(
86 spot: number | null,
87 strike: number,
88 dte: number | null,
89 ivPct: number | null,
90 isCall: boolean,
91): number | null {
92 if (!spot || spot <= 0 || strike <= 0) return null
93 if (ivPct == null || !Number.isFinite(ivPct) || ivPct <= 0) return null
94 if (dte == null || !Number.isFinite(dte) || dte < 0) return null
95 const sigma = ivPct / 100
96 const T = Math.max(dte / 365, 1 / 365)
97 const d1 = (Math.log(spot / strike) + (0.045 + 0.5 * sigma * sigma) * T) / (sigma * Math.sqrt(T))
98 const cd = normCdf(d1)
99 return Number((isCall ? cd : cd - 1).toFixed(3))
100}
101
102export function computeSummary(
103 rows: ChainRow[],
104 base: Partial<ChainMeta> & { symbol: string; provider: ChainMeta['provider'] },
105): { rows: ChainRow[]; meta: ChainMeta } {
106 const spot = base.spot ?? null
107 const exp = base.selectedExpiration !== undefined ? base.selectedExpiration : (rows[0]?.expiration ?? null)
108 const dte = base.dte !== undefined ? base.dte : calcDte(exp)
109 let callOiTotal = 0, putOiTotal = 0, callVolTotal = 0, putVolTotal = 0
110
111 for (const r of rows) {
112 callOiTotal += r.callOi ?? 0
113 putOiTotal += r.putOi ?? 0
114 callVolTotal += r.callVol ?? 0
115 putVolTotal += r.putVol ?? 0
116 if (r.callDelta === null && spot) r.callDelta = approxDelta(spot, r.strike, dte, r.callIv, true)
117 if (r.putDelta === null && spot) r.putDelta = approxDelta(spot, r.strike, dte, r.putIv, false)
118 }
119
120 let atmRow: ChainRow | null = null
121 if (rows.length > 0) {
122 atmRow = rows.reduce((best, r) => {
123 if (!spot) return best
124 return Math.abs(r.strike - spot) < Math.abs(best.strike - spot) ? r : best
125 }, rows[0]!)
126 }
127
128 const atmStrike = atmRow?.strike ?? null
129 const callMid = atmRow
130 ? atmRow.callBid != null && atmRow.callAsk != null && (atmRow.callBid > 0 || atmRow.callAsk > 0)
131 ? (atmRow.callBid + atmRow.callAsk) / 2
132 : atmRow.callLast
133 : null
134 const putMid = atmRow
135 ? atmRow.putBid != null && atmRow.putAsk != null && (atmRow.putBid > 0 || atmRow.putAsk > 0)
136 ? (atmRow.putBid + atmRow.putAsk) / 2
137 : atmRow.putLast
138 : null
139 const straddlePrice = callMid != null && putMid != null ? Number((callMid + putMid).toFixed(2)) : null
140 const straddlePct = straddlePrice != null && spot && spot > 0 ? Number(((straddlePrice / spot) * 100).toFixed(2)) : null
141 const atmIv = atmRow
142 ? atmRow.callIv != null && atmRow.putIv != null
143 ? Number(((atmRow.callIv + atmRow.putIv) / 2).toFixed(2))
144 : (atmRow.callIv ?? atmRow.putIv ?? null)
145 : null
146
147 let maxPain: number | null = null
148 if (rows.length > 0 && (callOiTotal > 0 || putOiTotal > 0)) {
149 let minVal = Infinity
150 for (const settle of rows) {
151 let pain = 0
152 for (const r of rows) {
153 if (settle.strike > r.strike) pain += (settle.strike - r.strike) * (r.callOi ?? 0)
154 if (settle.strike < r.strike) pain += (r.strike - settle.strike) * (r.putOi ?? 0)
155 }
156 if (pain < minVal) {
157 minVal = pain
158 maxPain = settle.strike
159 }
160 }
161 }
162
163 return {
164 rows,
165 meta: {
166 symbol: base.symbol,
167 provider: base.provider,
168 feedLabel: base.feedLabel ?? base.provider.toUpperCase(),
169 spot,
170 change: base.change ?? null,
171 changePct: base.changePct ?? null,
172 dayRange: base.dayRange ?? null,
173 week52Range: base.week52Range ?? null,
174 volume: base.volume ?? null,
175 expirations: base.expirations ?? (exp ? [exp] : []),
176 selectedExpiration: exp,
177 dte,
178 atmStrike,
179 atmIv,
180 straddlePrice,
181 straddlePct,
182 pcOiRatio: callOiTotal > 0 ? Number((putOiTotal / callOiTotal).toFixed(2)) : null,
183 pcVolRatio: callVolTotal > 0 ? Number((putVolTotal / callVolTotal).toFixed(2)) : null,
184 callOiTotal,
185 putOiTotal,
186 callVolTotal,
187 putVolTotal,
188 maxPain,
189 note: base.note ?? null,
190 },
191 }
192}