Jaspers Hub

jaspers / option-chain

pluginOfficialReviewed★ 0↓ 0

Options Chain: Shows a calls/puts straddle table by strike (IV, delta, OI, volume, max pain) sourced from Yahoo, Alpaca, Massive, or Databento plugins.

8.4 KB
1import { useData } from '@jaspers-ai/sdk'
2import type React from 'react'
3import { useMemo } from 'react'
4import type { ChainMeta, ChainRow } from './math.ts'
5import { buildAlpacaChain, filterStrikesNearSpot } from './vendors.ts'
6import { buildDatabentoChain, buildMassiveChain, selectDatabentoQuoteSymbols } from './vendors-extra.ts'
7
8export interface LoaderResult {
9 rows: ChainRow[]
10 meta: ChainMeta | null
11 loading: boolean
12 error: string | null
13 refetch: () => void
14}
15
16export function YahooProviderLoader({
17 symbol,
18 expiration,
19 strikes,
20 children,
21}: {
22 symbol: string
23 expiration: string | null
24 strikes: number
25 children: (res: LoaderResult) => React.ReactNode
26}): React.ReactElement {
27 const effectiveStrikes = strikes <= 0 ? 0 : strikes
28 const args = useMemo(() => {
29 const a: Record<string, unknown> = { symbol, strikes: effectiveStrikes }
30 if (expiration) a.expiration = expiration
31 return a
32 }, [symbol, expiration, effectiveStrikes])
33 const { data, meta, loading, error, refetch } = useData('option-chain/chain', args)
34 const rawRows = (data ?? []) as ChainRow[]
35 const rawMeta = meta as Record<string, unknown> | undefined
36 const unwrappedMeta = (rawMeta?.meta ? rawMeta.meta : rawMeta) as unknown as ChainMeta | null
37 const rows = useMemo(() => {
38 if (strikes <= 0) return rawRows
39 return filterStrikesNearSpot(rawRows, unwrappedMeta?.spot ?? null, strikes)
40 }, [rawRows, unwrappedMeta?.spot, strikes])
41 return children({
42 rows,
43 meta: unwrappedMeta ?? null,
44 loading,
45 error: error ?? null,
46 refetch,
47 }) as React.ReactElement
48}
49
50export function AlpacaProviderLoader({
51 symbol,
52 expiration,
53 alpacaFeed,
54 strikes,
55 children,
56}: {
57 symbol: string
58 expiration: string | null
59 alpacaFeed: 'indicative' | 'opra'
60 strikes: number
61 children: (res: LoaderResult) => React.ReactNode
62}): React.ReactElement {
63 const todayStr = useMemo(() => new Date().toISOString().slice(0, 10), [])
64 const baseChainArgs = useMemo(
65 () => ({ underlying: symbol, feed: alpacaFeed, expiration_date_gte: todayStr, limit: 1000 }),
66 [symbol, alpacaFeed, todayStr],
67 )
68 const expChainArgs = useMemo(
69 () => (expiration ? { underlying: symbol, feed: alpacaFeed, expiration_date: expiration, limit: 1000 } : {}),
70 [symbol, alpacaFeed, expiration],
71 )
72 const snapArgs = useMemo(() => ({ symbols: [symbol] }), [symbol])
73
74 const baseChainRes = useData('alpaca/option-chain', baseChainArgs)
75 const expChainRes = useData(expiration ? 'alpaca/option-chain' : '', expChainArgs)
76 const snapRes = useData('alpaca/stock-snapshots', snapArgs)
77
78 const built = useMemo(() => {
79 const activeChain = expiration ? expChainRes : baseChainRes
80 if (!activeChain.data && activeChain.loading && !baseChainRes.data) return null
81 const baseRows = (baseChainRes.data ?? []) as Record<string, unknown>[]
82 const expRows = (expChainRes.data ?? []) as Record<string, unknown>[]
83 const optRows = expiration && expRows.length > 0 ? expRows : baseRows
84 const allExpirations: string[] = []
85 for (const r of [...baseRows, ...expRows]) {
86 if (typeof r.expiration === 'string' && !allExpirations.includes(r.expiration)) {
87 allExpirations.push(r.expiration)
88 }
89 }
90 const snapRows = (snapRes.data ?? []) as Record<string, unknown>[]
91 return buildAlpacaChain(symbol, alpacaFeed, optRows, snapRows[0], expiration, strikes, allExpirations)
92 }, [
93 symbol,
94 alpacaFeed,
95 expiration,
96 strikes,
97 baseChainRes.data,
98 baseChainRes.loading,
99 expChainRes.data,
100 expChainRes.loading,
101 snapRes.data,
102 ])
103
104 const chainLoading = expiration ? expChainRes.loading || baseChainRes.loading : baseChainRes.loading
105 const chainError = (expiration ? expChainRes.error : null) ?? baseChainRes.error
106
107 return children({
108 rows: built?.rows ?? [],
109 meta: built?.meta ?? null,
110 loading: chainLoading || snapRes.loading,
111 error: chainError ?? snapRes.error ?? null,
112 refetch: () => {
113 baseChainRes.refetch()
114 if (expiration) expChainRes.refetch()
115 snapRes.refetch()
116 },
117 }) as React.ReactElement
118}
119
120export function MassiveProviderLoader({
121 symbol,
122 expiration,
123 strikes,
124 children,
125}: {
126 symbol: string
127 expiration: string | null
128 strikes: number
129 children: (res: LoaderResult) => React.ReactNode
130}): React.ReactElement {
131 const todayStr = useMemo(() => new Date().toISOString().slice(0, 10), [])
132 const contractArgs = useMemo(
133 () => ({ underlying: symbol, expiration_from: todayStr, limit: 1000 }),
134 [symbol, todayStr],
135 )
136 const chainArgs = useMemo(() => {
137 const a: Record<string, unknown> = { underlying: symbol, limit: 1000 }
138 if (expiration) a.expiration = expiration
139 return a
140 }, [symbol, expiration])
141 const prevArgs = useMemo(() => ({ ticker: symbol }), [symbol])
142
143 const contractsRes = useData('massive/option-contracts', contractArgs)
144 const chainRes = useData('massive/option-chain', chainArgs)
145 const prevRes = useData('massive/previous-close', prevArgs)
146
147 const built = useMemo(() => {
148 const contractRows = (contractsRes.data ?? []) as Record<string, unknown>[]
149 const chainRows = (chainRes.data ?? []) as Record<string, unknown>[]
150 const prevRows = (prevRes.data ?? []) as Record<string, unknown>[]
151 if (!contractsRes.data && !chainRes.data && (contractsRes.loading || chainRes.loading)) return null
152 const note = chainRes.error
153 ? `OPRA snapshot unavailable (${chainRes.error.slice(0, 80)}); using contracts + previous close.`
154 : null
155 return buildMassiveChain(symbol, contractRows, chainRows, prevRows[0], expiration, strikes, note)
156 }, [
157 symbol,
158 contractsRes.data,
159 contractsRes.loading,
160 chainRes.data,
161 chainRes.loading,
162 chainRes.error,
163 prevRes.data,
164 expiration,
165 strikes,
166 ])
167
168 const fatalError = contractsRes.error && chainRes.error ? contractsRes.error : null
169
170 return children({
171 rows: built?.rows ?? [],
172 meta: built?.meta ?? null,
173 loading: contractsRes.loading && chainRes.loading,
174 error: fatalError,
175 refetch: () => {
176 contractsRes.refetch()
177 chainRes.refetch()
178 prevRes.refetch()
179 },
180 }) as React.ReactElement
181}
182
183export function DatabentoProviderLoader({
184 symbol,
185 expiration,
186 strikes,
187 children,
188}: {
189 symbol: string
190 expiration: string | null
191 strikes: number
192 children: (res: LoaderResult) => React.ReactNode
193}): React.ReactElement {
194 const dateStr = useMemo(() => {
195 const d = new Date(Date.now() - 2 * 86_400_000)
196 const day = d.getUTCDay()
197 if (day === 0) d.setUTCDate(d.getUTCDate() - 2)
198 else if (day === 6) d.setUTCDate(d.getUTCDate() - 1)
199 return d.toISOString().slice(0, 10)
200 }, [])
201
202 const defArgs = useMemo(() => {
203 return { ticker: symbol, start: dateStr, limit: 1000 }
204 }, [symbol, dateStr])
205
206 const ohlcvArgs = useMemo(() => {
207 const start = new Date(Date.parse(`${dateStr}T00:00:00Z`) - 7 * 86_400_000).toISOString().slice(0, 10)
208 return { ticker: symbol, start, end: dateStr }
209 }, [symbol, dateStr])
210
211 const defsRes = useData('databento/definitions', defArgs)
212 const ohlcvRes = useData('databento/underlying-ohlcv', ohlcvArgs)
213
214 const quoteSymbols = useMemo(() => {
215 const defsRows = (defsRes.data ?? []) as Record<string, unknown>[]
216 const ohlcvRows = (ohlcvRes.data ?? []) as Record<string, unknown>[]
217 if (defsRows.length === 0) return []
218 return selectDatabentoQuoteSymbols(defsRows, ohlcvRows, expiration, strikes)
219 }, [defsRes.data, ohlcvRes.data, expiration, strikes])
220
221 const quoteArgs = useMemo(
222 () => (quoteSymbols.length > 0 ? { symbols: quoteSymbols, start: dateStr, limit: 5000 } : {}),
223 [quoteSymbols, dateStr],
224 )
225 const quotesRes = useData(quoteSymbols.length > 0 ? 'databento/quotes' : '', quoteArgs)
226
227 const built = useMemo(() => {
228 if (!defsRes.data && defsRes.loading) return null
229 const defsRows = (defsRes.data ?? []) as Record<string, unknown>[]
230 const ohlcvRows = (ohlcvRes.data ?? []) as Record<string, unknown>[]
231 const quotesRows = (quotesRes.data ?? []) as Record<string, unknown>[]
232 return buildDatabentoChain(symbol, defsRows, quotesRows, ohlcvRows, expiration, strikes)
233 }, [symbol, defsRes.data, defsRes.loading, quotesRes.data, ohlcvRes.data, expiration, strikes])
234
235 return children({
236 rows: built?.rows ?? [],
237 meta: built?.meta ?? null,
238 loading: defsRes.loading || ohlcvRes.loading || (quoteSymbols.length > 0 && quotesRes.loading),
239 error: defsRes.error ?? ohlcvRes.error ?? null,
240 refetch: () => {
241 defsRes.refetch()
242 ohlcvRes.refetch()
243 if (quoteSymbols.length > 0) quotesRes.refetch()
244 },
245 }) as React.ReactElement
246}