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.5 KB
1import { computeSummary, toOcc, type ChainMeta, type ChainRow } from './math.ts'
2
3export function filterStrikesNearSpot<T extends { strike: number }>(
4 rows: T[],
5 spot: number | null,
6 maxStrikes: number,
7): T[] {
8 const sorted = [...rows].sort((a, b) => a.strike - b.strike)
9 if (!maxStrikes || maxStrikes <= 0 || sorted.length <= maxStrikes) return sorted
10 const ref = spot ?? sorted[Math.floor(sorted.length / 2)]!.strike
11 return [...sorted]
12 .sort((a, b) => Math.abs(a.strike - ref) - Math.abs(b.strike - ref) || a.strike - b.strike)
13 .slice(0, maxStrikes)
14 .sort((a, b) => a.strike - b.strike)
15}
16
17export function buildAlpacaChain(
18 symbol: string,
19 alpacaFeed: 'indicative' | 'opra',
20 optRows: Record<string, unknown>[],
21 snapRow: Record<string, unknown> | undefined,
22 requestedExp: string | null | undefined,
23 maxStrikes: number,
24 allExpirations?: string[],
25): { rows: ChainRow[]; meta: ChainMeta } {
26 const sym = symbol.toUpperCase()
27 const spot =
28 typeof snapRow?.last === 'number'
29 ? snapRow.last
30 : typeof snapRow?.price === 'number'
31 ? snapRow.price
32 : typeof snapRow?.prev_close === 'number'
33 ? snapRow.prev_close
34 : typeof snapRow?.prevClose === 'number'
35 ? snapRow.prevClose
36 : null
37 const change = typeof snapRow?.change === 'number' ? snapRow.change : null
38 const changePct =
39 typeof snapRow?.change_pct === 'number'
40 ? snapRow.change_pct
41 : typeof snapRow?.changePct === 'number'
42 ? snapRow.changePct
43 : null
44 const dayHigh =
45 typeof snapRow?.day_high === 'number'
46 ? snapRow.day_high
47 : typeof snapRow?.dayHigh === 'number'
48 ? snapRow.dayHigh
49 : null
50 const dayLow =
51 typeof snapRow?.day_low === 'number'
52 ? snapRow.day_low
53 : typeof snapRow?.dayLow === 'number'
54 ? snapRow.dayLow
55 : null
56 const dayRange = dayLow != null && dayHigh != null ? `${dayLow.toFixed(2)}-${dayHigh.toFixed(2)}` : null
57 const volume =
58 typeof snapRow?.day_volume === 'number'
59 ? snapRow.day_volume
60 : typeof snapRow?.dayVolume === 'number'
61 ? snapRow.dayVolume
62 : null
63
64 let sessionDate: string | null =
65 typeof snapRow?.day_time === 'string'
66 ? snapRow.day_time.slice(0, 10)
67 : typeof snapRow?.last_time === 'string'
68 ? snapRow.last_time.slice(0, 10)
69 : null
70
71 const expSet = new Set<string>(allExpirations ?? [])
72 for (const r of optRows) {
73 const e = typeof r.expiration === 'string' ? r.expiration : null
74 if (e) expSet.add(e)
75 const qt = typeof r.quote_time === 'string' ? r.quote_time.slice(0, 10) : null
76 if (qt && (!sessionDate || qt > sessionDate)) sessionDate = qt
77 }
78 const expirations = [...expSet].sort()
79 const selectedExpiration =
80 (requestedExp && expSet.has(requestedExp) ? requestedExp : expirations[0]) ?? requestedExp ?? null
81
82 const byStrike = new Map<number, ChainRow>()
83 for (const r of optRows) {
84 const exp = typeof r.expiration === 'string' ? r.expiration : null
85 if (selectedExpiration && exp !== selectedExpiration) continue
86 const strike = typeof r.strike === 'number' ? r.strike : Number(r.strike)
87 if (!Number.isFinite(strike)) continue
88 const type = r.type === 'put' ? 'put' : 'call'
89 const expStr = exp ?? selectedExpiration ?? ''
90 let row = byStrike.get(strike)
91 if (!row) {
92 const cc = toOcc(sym, expStr, 'C', strike)
93 const pc = toOcc(sym, expStr, 'P', strike)
94 row = {
95 contract: `${cc} / ${pc}`,
96 callContract: cc,
97 putContract: pc,
98 expiration: expStr,
99 strike,
100 callBid: null,
101 callBidSize: null,
102 callAsk: null,
103 callAskSize: null,
104 callLast: null,
105 callLastTime: null,
106 callChg: null,
107 callVol: null,
108 callOi: null,
109 callIv: null,
110 callDelta: null,
111 callItm: spot != null ? strike < spot : false,
112 putBid: null,
113 putBidSize: null,
114 putAsk: null,
115 putAskSize: null,
116 putLast: null,
117 putLastTime: null,
118 putChg: null,
119 putVol: null,
120 putOi: null,
121 putIv: null,
122 putDelta: null,
123 putItm: spot != null ? strike > spot : false,
124 }
125 byStrike.set(strike, row)
126 }
127 const rawIv =
128 typeof r.iv === 'number' ? (r.iv <= 5 ? Number((r.iv * 100).toFixed(2)) : Number(r.iv.toFixed(2))) : null
129 const lastTime = typeof r.last_time === 'string' ? r.last_time : null
130 const isCurrentSessionTrade =
131 !sessionDate || (lastTime != null && lastTime.slice(0, 10) >= sessionDate)
132 const rawLast =
133 typeof r.last === 'number' ? r.last : typeof r.lastPrice === 'number' ? r.lastPrice : null
134 const rawVol =
135 typeof r.day_volume === 'number' ? r.day_volume : typeof r.volume === 'number' ? r.volume : null
136 const rawOi =
137 typeof r.open_interest === 'number' ? r.open_interest : typeof r.openInterest === 'number' ? r.openInterest : null
138 const lastPx = isCurrentSessionTrade ? rawLast : null
139 const volNum = isCurrentSessionTrade ? rawVol : null
140 const bidSz = typeof r.bid_size === 'number' ? r.bid_size : typeof r.bidSize === 'number' ? r.bidSize : null
141 const askSz = typeof r.ask_size === 'number' ? r.ask_size : typeof r.askSize === 'number' ? r.askSize : null
142
143 if (type === 'call') {
144 if (typeof r.symbol === 'string') row.callContract = r.symbol
145 row.callBid = typeof r.bid === 'number' ? r.bid : null
146 row.callBidSize = bidSz
147 row.callAsk = typeof r.ask === 'number' ? r.ask : null
148 row.callAskSize = askSz
149 row.callLast = lastPx
150 row.callLastTime = lastTime
151 row.callVol = volNum
152 row.callOi = rawOi
153 row.callIv = rawIv
154 row.callDelta = typeof r.delta === 'number' ? Number(r.delta.toFixed(3)) : null
155 } else {
156 if (typeof r.symbol === 'string') row.putContract = r.symbol
157 row.putBid = typeof r.bid === 'number' ? r.bid : null
158 row.putBidSize = bidSz
159 row.putAsk = typeof r.ask === 'number' ? r.ask : null
160 row.putAskSize = askSz
161 row.putLast = lastPx
162 row.putLastTime = lastTime
163 row.putVol = volNum
164 row.putOi = rawOi
165 row.putIv = rawIv
166 row.putDelta = typeof r.delta === 'number' ? Number(r.delta.toFixed(3)) : null
167 }
168 row.contract = `${row.callContract} / ${row.putContract}`
169 }
170
171 const filtered = filterStrikesNearSpot([...byStrike.values()], spot, maxStrikes)
172 return computeSummary(filtered, {
173 symbol: sym,
174 provider: 'alpaca',
175 feedLabel: `Alpaca (${alpacaFeed.toUpperCase()})`,
176 spot,
177 change,
178 changePct,
179 dayRange,
180 volume,
181 expirations,
182 selectedExpiration,
183 })
184}