Options Chain: Shows a multi-vendor options chain straddle table with IV, delta, OI, volume, and max pain for a ticker.
| 1 | import { useData } from '@jaspers-ai/sdk' |
| 2 | import type React from 'react' |
| 3 | import { useMemo } from 'react' |
| 4 | import type { ChainMeta, ChainRow } from './math.ts' |
| 5 | import { buildAlpacaChain, filterStrikesNearSpot } from './vendors.ts' |
| 6 | import { buildDatabentoChain, buildMassiveChain, selectDatabentoQuoteSymbols } from './vendors-extra.ts' |
| 7 | |
| 8 | export interface LoaderResult { |
| 9 | rows: ChainRow[] |
| 10 | meta: ChainMeta | null |
| 11 | loading: boolean |
| 12 | error: string | null |
| 13 | refetch: () => void |
| 14 | } |
| 15 | |
| 16 | export 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 | |
| 50 | export 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 | |
| 120 | export 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 | |
| 183 | export 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 | } |