import { Fragment, type ReactElement } from 'react' import { useData, usePublish, type PanelRef } from '@jaspers-ai/sdk' import { display as d } from './format' import { Bar, Empty, Loading, Section, Signed, SymbolField, useEvery, usePanelStateValue, useStateWriter } from './shared' import { list, num, record, text, type Row } from './values' import { optionsOutput, readOptionsState, REFRESH_MS, STRIKE_CHOICES, type OptionsState } from './views' import './styles.css' // One symbol's option chain for one expiration: calls on the left, puts on the right, the strike // between them, the in-the-money side shaded, and a rule where the price falls. The chain is asked // again every minute. A new symbol clears the expiration, since its dates are the old symbol's. const COLUMNS = ['Last', 'Bid', 'Ask', 'Volume', 'Open int.', 'IV'] as const export function OptionsView({ panel }: { panel: PanelRef }): ReactElement { const state = usePanelStateValue(panel) const write = useStateWriter(panel) if (state === undefined) return const read = readOptionsState(state) return (
symbol !== read.symbol && write([['expiration', null], ['symbol', symbol]])} />
{read.symbol ? ( write([['expiration', expiration]])} /> ) : ( Enter a symbol with listed options: AAPL, SPY, TSLA. )}
) } function Chain({ panel, state, onExpiration }: { panel: PanelRef; state: OptionsState; onExpiration: (expiration: string) => void }): ReactElement { const args: Record = { symbol: state.symbol, strikes: state.strikes } if (state.expiration) args['expiration'] = state.expiration const run = useData('yfinance/options', args) useEvery(run.refetch, REFRESH_MS) usePublish(panel, optionsOutput(state, run)) const meta = run.meta const hint = num(meta['priceHint']) const underlying = record(meta['underlying']) ?? {} const price = num(underlying['price']) const totals = record(meta['totals']) ?? {} const atm = record(meta['atm']) const expirations = list(meta['expirations']).map(text).filter((e): e is string => e !== null) const expiration = text(meta['expiration']) const rows = run.data ?? [] // The price rule goes before the first strike above the price. const above = price === null ? -1 : rows.findIndex((row) => (num(row['strike']) ?? 0) >= price) return (
Expires{' '} ) : undefined } empty={rows.length === 0} >

{d.price(price, hint)} {' '} {' · '} {num(totals['calls'])} calls, {num(totals['puts'])} puts · put/call {d.fixed(num(totals['putCallVolumeRatio']))} by volume,{' '} {d.fixed(num(totals['putCallOpenInterestRatio']))} by open interest {atm && ` · at ${d.price(num(atm['strike']), hint)}: call IV ${d.percent(num(atm['callIvPercent']), 1)}, put IV ${d.percent(num(atm['putIvPercent']), 1)}`}

{COLUMNS.map((c) => ( ))} {COLUMNS.map((c) => ( ))} {rows.map((row, index) => ( {index === above && } ))} {above === -1 && rows.length > 0 && price !== null && }
Calls Puts
{c} Strike {c}
{d.price(num(row['strike']), hint)}
) } function PriceRule({ price, hint }: { price: number | null; hint: number | null }): ReactElement { return ( price {d.price(price, hint)} ) } function Side({ contract, hint }: { contract: Row | null; hint: number | null }): ReactElement { if (!contract) { return ( <> {COLUMNS.map((c) => ( — ))} ) } const itm = contract['inTheMoney'] === true ? ' yf-itm' : '' const cells = [ d.price(num(contract['last']), hint), d.price(num(contract['bid']), hint), d.price(num(contract['ask']), hint), d.fixed(num(contract['volume']), 0), d.fixed(num(contract['openInterest']), 0), d.percent(num(contract['ivPercent']), 1), ] return ( <> {cells.map((cell, i) => ( {cell} ))} ) }