import { useBridge, usePanelState, usePublish, usePublishText, type PanelRef } from '@jaspers-ai/sdk' import { useEffect, useMemo, useState } from 'react' import '../styles.css' import { calcDte, type ChainMeta, type ChainRow } from './math.ts' import { AlpacaProviderLoader, DatabentoProviderLoader, MassiveProviderLoader, YahooProviderLoader, } from './ProviderLoader.tsx' import { StraddleTable } from './StraddleTable.tsx' const WATCHLIST = ['FLWS', 'BRCB', 'GO', 'CWH', 'JBSS', 'WEN', 'PLBY', 'SPY'] const PROVIDERS = [ { id: 'yahoo', label: 'Yahoo' }, { id: 'alpaca', label: 'Alpaca' }, { id: 'massive', label: 'Massive' }, { id: 'databento', label: 'Databento' }, ] as const const STRIKE_CHOICES = [ { value: 10, label: '10 Strikes' }, { value: 20, label: '20 Strikes' }, { value: 40, label: '40 Strikes' }, { value: 0, label: 'All Strikes' }, ] function fmtNum(v: number | null | undefined, d = 2): string { if (v == null || !Number.isFinite(v)) return '-' return v.toFixed(d) } export function TableView({ panel }: { panel: PanelRef }) { const [symbolState, setSymbol] = usePanelState(panel, 'symbol', 'FLWS') const [providerState, setProvider] = usePanelState<'yahoo' | 'alpaca' | 'massive' | 'databento'>( panel, 'provider', 'yahoo', ) const [alpacaFeedState] = usePanelState<'indicative' | 'opra'>(panel, 'alpacaFeed', 'indicative') const [expirationState, setExpiration] = usePanelState(panel, 'expiration', null) const [strikesState, setStrikes] = usePanelState(panel, 'strikes', 20) const symbol = (symbolState || 'FLWS').toUpperCase() const provider = providerState || 'yahoo' const alpacaFeed = alpacaFeedState || 'indicative' const expiration = expirationState ?? null const strikes = strikesState ?? 20 const [tickerInput, setTickerInput] = useState(symbol) useEffect(() => { setTickerInput(symbol) }, [symbol]) const commitTicker = (raw: string) => { const clean = raw.trim().toUpperCase() if (!clean) return setTickerInput(clean) if (clean !== symbol) { setExpiration(null) setSymbol(clean) } } const tickerChoices = useMemo( () => (WATCHLIST.includes(symbol) ? WATCHLIST : [symbol, ...WATCHLIST]), [symbol], ) const Loader = provider === 'alpaca' ? AlpacaProviderLoader : provider === 'massive' ? MassiveProviderLoader : provider === 'databento' ? DatabentoProviderLoader : YahooProviderLoader return (
{(res) => { const expirations = res.meta?.expirations ?? [] const activeExp = res.meta?.selectedExpiration ?? expiration ?? expirations[0] ?? '' return ( <>
Ticker setTickerInput(e.target.value.toUpperCase())} onKeyDown={(e) => { if (e.key === 'Enter') commitTicker(tickerInput) }} />
Vendor
Expiration
Strikes
) }}
) } function ChainBody({ panel, symbol, provider, expiration, rows, meta, loading, error, }: { panel: PanelRef symbol: string provider: string expiration: string | null rows: ChainRow[] meta: ChainMeta | null loading: boolean error: string | null }) { const bridge = useBridge() const [copiedOcc, setCopiedOcc] = useState(null) const activeExp = meta?.selectedExpiration ?? expiration ?? meta?.expirations?.[0] ?? null usePublish(panel, { symbol, provider, expiration: activeExp, spot: meta?.spot ?? null, strikeCount: rows.length, expirationsCount: meta?.expirations?.length ?? 0, atmIv: meta?.atmIv ?? null, atmStraddle: meta?.straddlePrice ?? null, pcRatioOi: meta?.pcOiRatio ?? null, maxPain: meta?.maxPain ?? null, }) const textSummary = useMemo(() => { if (!meta && rows.length === 0) { return `Option Chain: ${symbol} (${provider}) - ${loading ? 'Loading' : error || 'No data'}` } const header = `Option Chain ${symbol} (${meta?.feedLabel ?? provider}) | Spot: $${fmtNum(meta?.spot)} | Exp: ${activeExp ?? '-'} (${meta?.dte ?? '-'}d) | ATM Strike: ${fmtNum(meta?.atmStrike)} | ATM IV: ${fmtNum(meta?.atmIv)}% | Straddle: $${fmtNum(meta?.straddlePrice)} | P/C OI: ${fmtNum(meta?.pcOiRatio)} | P/C Vol: ${fmtNum(meta?.pcVolRatio)} | Max Pain: $${fmtNum(meta?.maxPain)}` const cols = 'CallBid | CallAsk | CallLast | CallVol | CallOI | CallIV% | CallDelta | Strike | PutDelta | PutIV% | PutOI | PutVol | PutLast | PutBid | PutAsk' const lines = rows.map( (r) => `${fmtNum(r.callBid)}${r.callBidSize ? 'x' + r.callBidSize : ''} | ${fmtNum(r.callAsk)}${r.callAskSize ? 'x' + r.callAskSize : ''} | ${fmtNum(r.callLast)} | ${r.callVol ?? 0} | ${r.callOi ?? 0} | ${fmtNum(r.callIv)} | ${fmtNum(r.callDelta, 3)} | [${fmtNum(r.strike)}] | ${fmtNum(r.putDelta, 3)} | ${fmtNum(r.putIv)} | ${r.putOi ?? 0} | ${r.putVol ?? 0} | ${fmtNum(r.putLast)} | ${fmtNum(r.putBid)}${r.putBidSize ? 'x' + r.putBidSize : ''} | ${fmtNum(r.putAsk)}${r.putAskSize ? 'x' + r.putAskSize : ''}`, ) return [header, cols, ...lines].join('\n') }, [symbol, provider, meta, activeExp, rows, loading, error]) usePublishText(panel, textSummary) const handlePickContract = (occ: string) => { bridge.copyText(occ) setCopiedOcc(occ) } return ( <> {meta && (
Spot ${fmtNum(meta.spot)}
Exp / DTE {activeExp ?? '-'} ({meta.dte ?? '-'}d)
ATM IV {meta.atmIv != null ? `${fmtNum(meta.atmIv)}%` : '-'}
ATM Straddle {meta.straddlePrice != null ? `$${fmtNum(meta.straddlePrice)}` : '-'} {meta.straddlePct != null ? ` (${fmtNum(meta.straddlePct)}%)` : ''}
P/C OI {fmtNum(meta.pcOiRatio)}
P/C Vol {fmtNum(meta.pcVolRatio)}
Max Pain {meta.maxPain != null ? `$${fmtNum(meta.maxPain)}` : '-'}
Feed {meta.feedLabel} {copiedOcc ? ` | Copied ${copiedOcc}` : ''}
)} {meta?.note &&
{meta.note}
} {error &&
{error}
} {loading && rows.length === 0 &&
Loading {symbol} option chain...
} {!loading && !error && rows.length === 0 && (
No option contracts found for {symbol}.
)} {rows.length > 0 && ( )} ) }