import { useData } from '@jaspers-ai/sdk' import type React from 'react' import { useMemo } from 'react' import type { ChainMeta, ChainRow } from './math.ts' import { buildAlpacaChain, filterStrikesNearSpot } from './vendors.ts' import { buildDatabentoChain, buildMassiveChain, selectDatabentoQuoteSymbols } from './vendors-extra.ts' export interface LoaderResult { rows: ChainRow[] meta: ChainMeta | null loading: boolean error: string | null refetch: () => void } export function YahooProviderLoader({ symbol, expiration, strikes, children, }: { symbol: string expiration: string | null strikes: number children: (res: LoaderResult) => React.ReactNode }): React.ReactElement { const effectiveStrikes = strikes <= 0 ? 0 : strikes const args = useMemo(() => { const a: Record = { symbol, strikes: effectiveStrikes } if (expiration) a.expiration = expiration return a }, [symbol, expiration, effectiveStrikes]) const { data, meta, loading, error, refetch } = useData('option-chain/chain', args) const rawRows = (data ?? []) as ChainRow[] const rawMeta = meta as Record | undefined const unwrappedMeta = (rawMeta?.meta ? rawMeta.meta : rawMeta) as unknown as ChainMeta | null const rows = useMemo(() => { if (strikes <= 0) return rawRows return filterStrikesNearSpot(rawRows, unwrappedMeta?.spot ?? null, strikes) }, [rawRows, unwrappedMeta?.spot, strikes]) return children({ rows, meta: unwrappedMeta ?? null, loading, error: error ?? null, refetch, }) as React.ReactElement } export function AlpacaProviderLoader({ symbol, expiration, alpacaFeed, strikes, children, }: { symbol: string expiration: string | null alpacaFeed: 'indicative' | 'opra' strikes: number children: (res: LoaderResult) => React.ReactNode }): React.ReactElement { const todayStr = useMemo(() => new Date().toISOString().slice(0, 10), []) const baseChainArgs = useMemo( () => ({ underlying: symbol, feed: alpacaFeed, expiration_date_gte: todayStr, limit: 1000 }), [symbol, alpacaFeed, todayStr], ) const expChainArgs = useMemo( () => (expiration ? { underlying: symbol, feed: alpacaFeed, expiration_date: expiration, limit: 1000 } : {}), [symbol, alpacaFeed, expiration], ) const snapArgs = useMemo(() => ({ symbols: [symbol] }), [symbol]) const baseChainRes = useData('alpaca/option-chain', baseChainArgs) const expChainRes = useData(expiration ? 'alpaca/option-chain' : '', expChainArgs) const snapRes = useData('alpaca/stock-snapshots', snapArgs) const built = useMemo(() => { const activeChain = expiration ? expChainRes : baseChainRes if (!activeChain.data && activeChain.loading && !baseChainRes.data) return null const baseRows = (baseChainRes.data ?? []) as Record[] const expRows = (expChainRes.data ?? []) as Record[] const optRows = expiration && expRows.length > 0 ? expRows : baseRows const allExpirations: string[] = [] for (const r of [...baseRows, ...expRows]) { if (typeof r.expiration === 'string' && !allExpirations.includes(r.expiration)) { allExpirations.push(r.expiration) } } const snapRows = (snapRes.data ?? []) as Record[] return buildAlpacaChain(symbol, alpacaFeed, optRows, snapRows[0], expiration, strikes, allExpirations) }, [ symbol, alpacaFeed, expiration, strikes, baseChainRes.data, baseChainRes.loading, expChainRes.data, expChainRes.loading, snapRes.data, ]) const chainLoading = expiration ? expChainRes.loading || baseChainRes.loading : baseChainRes.loading const chainError = (expiration ? expChainRes.error : null) ?? baseChainRes.error return children({ rows: built?.rows ?? [], meta: built?.meta ?? null, loading: chainLoading || snapRes.loading, error: chainError ?? snapRes.error ?? null, refetch: () => { baseChainRes.refetch() if (expiration) expChainRes.refetch() snapRes.refetch() }, }) as React.ReactElement } export function MassiveProviderLoader({ symbol, expiration, strikes, children, }: { symbol: string expiration: string | null strikes: number children: (res: LoaderResult) => React.ReactNode }): React.ReactElement { const todayStr = useMemo(() => new Date().toISOString().slice(0, 10), []) const contractArgs = useMemo( () => ({ underlying: symbol, expiration_from: todayStr, limit: 1000 }), [symbol, todayStr], ) const chainArgs = useMemo(() => { const a: Record = { underlying: symbol, limit: 1000 } if (expiration) a.expiration = expiration return a }, [symbol, expiration]) const prevArgs = useMemo(() => ({ ticker: symbol }), [symbol]) const contractsRes = useData('massive/option-contracts', contractArgs) const chainRes = useData('massive/option-chain', chainArgs) const prevRes = useData('massive/previous-close', prevArgs) const built = useMemo(() => { const contractRows = (contractsRes.data ?? []) as Record[] const chainRows = (chainRes.data ?? []) as Record[] const prevRows = (prevRes.data ?? []) as Record[] if (!contractsRes.data && !chainRes.data && (contractsRes.loading || chainRes.loading)) return null const note = chainRes.error ? `OPRA snapshot unavailable (${chainRes.error.slice(0, 80)}); using contracts + previous close.` : null return buildMassiveChain(symbol, contractRows, chainRows, prevRows[0], expiration, strikes, note) }, [ symbol, contractsRes.data, contractsRes.loading, chainRes.data, chainRes.loading, chainRes.error, prevRes.data, expiration, strikes, ]) const fatalError = contractsRes.error && chainRes.error ? contractsRes.error : null return children({ rows: built?.rows ?? [], meta: built?.meta ?? null, loading: contractsRes.loading && chainRes.loading, error: fatalError, refetch: () => { contractsRes.refetch() chainRes.refetch() prevRes.refetch() }, }) as React.ReactElement } export function DatabentoProviderLoader({ symbol, expiration, strikes, children, }: { symbol: string expiration: string | null strikes: number children: (res: LoaderResult) => React.ReactNode }): React.ReactElement { const dateStr = useMemo(() => { const d = new Date(Date.now() - 2 * 86_400_000) const day = d.getUTCDay() if (day === 0) d.setUTCDate(d.getUTCDate() - 2) else if (day === 6) d.setUTCDate(d.getUTCDate() - 1) return d.toISOString().slice(0, 10) }, []) const defArgs = useMemo(() => { return { ticker: symbol, start: dateStr, limit: 1000 } }, [symbol, dateStr]) const ohlcvArgs = useMemo(() => { const start = new Date(Date.parse(`${dateStr}T00:00:00Z`) - 7 * 86_400_000).toISOString().slice(0, 10) return { ticker: symbol, start, end: dateStr } }, [symbol, dateStr]) const defsRes = useData('databento/definitions', defArgs) const ohlcvRes = useData('databento/underlying-ohlcv', ohlcvArgs) const quoteSymbols = useMemo(() => { const defsRows = (defsRes.data ?? []) as Record[] const ohlcvRows = (ohlcvRes.data ?? []) as Record[] if (defsRows.length === 0) return [] return selectDatabentoQuoteSymbols(defsRows, ohlcvRows, expiration, strikes) }, [defsRes.data, ohlcvRes.data, expiration, strikes]) const quoteArgs = useMemo( () => (quoteSymbols.length > 0 ? { symbols: quoteSymbols, start: dateStr, limit: 5000 } : {}), [quoteSymbols, dateStr], ) const quotesRes = useData(quoteSymbols.length > 0 ? 'databento/quotes' : '', quoteArgs) const built = useMemo(() => { if (!defsRes.data && defsRes.loading) return null const defsRows = (defsRes.data ?? []) as Record[] const ohlcvRows = (ohlcvRes.data ?? []) as Record[] const quotesRows = (quotesRes.data ?? []) as Record[] return buildDatabentoChain(symbol, defsRows, quotesRows, ohlcvRows, expiration, strikes) }, [symbol, defsRes.data, defsRes.loading, quotesRes.data, ohlcvRes.data, expiration, strikes]) return children({ rows: built?.rows ?? [], meta: built?.meta ?? null, loading: defsRes.loading || ohlcvRes.loading || (quoteSymbols.length > 0 && quotesRes.loading), error: defsRes.error ?? ohlcvRes.error ?? null, refetch: () => { defsRes.refetch() ohlcvRes.refetch() if (quoteSymbols.length > 0) quotesRes.refetch() }, }) as React.ReactElement }